ci: docker_tag=ext - Fix WASM compilation for web target
Some checks failed
Build and Push Docker image (ext) / docker (push) Failing after 3m44s

- Recompile sdk_client WASM with --target web for ES modules
- Update Dockerfile to build WASM in web target instead of bundler
- Modify index.html to initialize WASM before router
- Update service.ts and router.ts to use ES module imports
- Fix 'module is not defined' error in browser
This commit is contained in:
4NK CI Bot 2025-09-18 15:41:05 +00:00
parent 5da295be1a
commit 01b1b50d6b
4 changed files with 83 additions and 35 deletions

View File

@ -1,13 +1,46 @@
# syntax=docker/dockerfile:1.4
# Stage 1: Build WASM for web target
FROM rust:1.82-alpine AS wasm-builder
WORKDIR /build
# Install dependencies for WASM compilation
RUN apk update && apk add --no-cache git openssh-client curl nodejs npm build-base pkgconfig clang llvm musl-dev
# Install wasm-bindgen-cli
RUN cargo install wasm-bindgen-cli --version 0.2.103 --locked && rustup target add wasm32-unknown-unknown
# Setup SSH for git clone
RUN mkdir -p /root/.ssh && ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
# Copy project files
COPY . ihm_client/
# Clone and build sdk_client for web target
RUN --mount=type=ssh git clone -b dev ssh://git@git.4nkweb.com/4nk/sdk_client.git
WORKDIR /build/sdk_client
# Build WASM for web target (ES modules)
RUN cargo build --target wasm32-unknown-unknown --profile dev && \
wasm-bindgen target/wasm32-unknown-unknown/debug/sdk_client.wasm \
--out-dir /build/ihm_client/pkg \
--typescript \
--target web \
--debug
# Stage 2: Final application
FROM node:20-alpine
WORKDIR /app
# Installation des dépendances nécessaires
RUN apk update && apk add --no-cache git nginx
# Copie du projet (incluant pkg pré-compilé)
# Copy project files
COPY . .
# Copy the web-compiled WASM package
COPY --from=wasm-builder /build/ihm_client/pkg ./pkg
# Installation des dépendances Node.js
RUN npm install

View File

@ -17,9 +17,23 @@
</div>
<!-- <script type="module" src="/src/index.ts"></script> -->
<script type="module">
import { init } from '/src/router.ts';
// Initialize WASM first
import init, * as sdk from './pkg/sdk_client.js';
// Initialize the application
import { init as initRouter } from '/src/router.ts';
(async () => {
await init();
try {
// Initialize WASM
await init();
console.log('WASM initialized successfully');
// Initialize the router
await initRouter();
} catch (error) {
console.error('Failed to initialize application:', error);
}
})();
</script>
</body>

View File

@ -10,7 +10,7 @@ import { prepareAndSendPairingTx } from './utils/sp-address.utils';
import ModalService from './services/modal.service';
import { MessageType } from './models/process.model';
import { splitPrivateData, isValid32ByteHex } from './utils/service.utils';
import { MerkleProofResult } from 'pkg/sdk_client';
import { MerkleProofResult } from 'pkg/sdk_client.js';
const routes: { [key: string]: string } = {
home: '/src/pages/home/home.html',

View File

@ -1,7 +1,7 @@
import { INotification } from '~/models/notification.model';
import { IProcess } from '~/models/process.model';
import { initWebsocket, sendMessage } from '../websockets';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client.js';
import ModalService from './modal.service';
import Database from './database.service';
import { navigate } from '../router';
@ -58,7 +58,8 @@ export default class Services {
public async init(): Promise<void> {
this.notifications = this.getNotifications();
this.sdkClient = await import('../../pkg/sdk_client');
// SDK is now imported as ES module at the top of the file
this.sdkClient = await import('../../pkg/sdk_client.js');
this.sdkClient.setup();
for (const wsurl of Object.values(BOOTSTRAPURL)) {
this.updateRelay(wsurl, '');
@ -87,7 +88,7 @@ export default class Services {
*/
public async connectAllRelays(): Promise<void> {
const connectedUrls: string[] = [];
// Connect to all relays
for (const wsurl of Object.keys(this.relayAddresses)) {
try {
@ -99,7 +100,7 @@ export default class Services {
console.error(`Failed to connect to ${wsurl}:`, error);
}
}
// Wait for at least one handshake message if we have connections
if (connectedUrls.length > 0) {
await this.waitForHandshakeMessage();
@ -381,18 +382,18 @@ export default class Services {
roles: Record<string, RoleDefinition>,
): Promise<ApiReturn> {
let relayAddress = this.getAllRelays()[0]?.spAddress;
if (!relayAddress || relayAddress === '') {
console.log('No relay address found, connecting to relays...');
await this.connectAllRelays();
// After connectAllRelays completes, relay addresses should be updated
relayAddress = this.getAllRelays()[0]?.spAddress;
if (!relayAddress || relayAddress === '') {
throw new Error('No relay address available after connecting to relays');
}
}
const feeRate = 1;
// We can't encode files as the rest because Uint8Array is not valid json
@ -400,12 +401,12 @@ export default class Services {
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData)
};
const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData)
};
@ -413,10 +414,10 @@ export default class Services {
console.log('encoded data:', encodedPublicData);
const result = this.sdkClient.create_new_process (
encodedPrivateData,
encodedPrivateData,
roles,
encodedPublicData,
relayAddress,
relayAddress,
feeRate,
this.getAllMembers()
);
@ -440,12 +441,12 @@ export default class Services {
}
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData)
};
const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData)
};
try {
@ -569,7 +570,7 @@ export default class Services {
}
async parseNewTx(newTxMsg: string) {
const parsedMsg: NewTxMessage = JSON.parse(newTxMsg);
const parsedMsg: NewTxMessage = JSON.parse(newTxMsg);
if (parsedMsg.error !== null) {
console.error('Received error in new tx message:', parsedMsg.error);
return;
@ -864,7 +865,7 @@ export default class Services {
try {
const device = await this.getDeviceFromDatabase();
if (device) {
const pairedMember = device['paired_member'];
const pairedMember = device['paired_member'];
return pairedMember.sp_addresses;
} else {
return null;
@ -1051,7 +1052,7 @@ export default class Services {
} catch (e) {
console.error(`Failed to save data to db: ${e}`);
}
}
}
public async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance();
@ -1178,7 +1179,7 @@ export default class Services {
});
}
// Now we can transfer them to memory
// Now we can transfer them to memory
await this.restoreSecretsFromDB();
}
@ -1219,7 +1220,7 @@ export default class Services {
if (!key) {
const roles = state.roles;
let hasAccess = false;
// If we're not supposed to have access to this attribute, ignore
// If we're not supposed to have access to this attribute, ignore
for (const role of Object.values(roles)) {
for (const rule of Object.values(role.validation_rules)) {
if (rule.fields.includes(attribute)) {
@ -1241,7 +1242,7 @@ export default class Services {
const maxRetries = 5;
const retryDelay = 500; // delay in milliseconds
let retries = 0;
while ((!hash || !key) && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting
@ -1274,7 +1275,7 @@ export default class Services {
}
}
}
return null;
}
@ -1316,7 +1317,7 @@ export default class Services {
await this.resetDevice();
await this.saveDeviceInDatabase(device);
this.restoreDevice(device);
// TODO restore secrets and processes from file
@ -1495,7 +1496,7 @@ export default class Services {
private async waitForHandshakeMessage(timeoutMs: number = 10000): Promise<void> {
const startTime = Date.now();
const pollInterval = 100; // Check every 100ms
return new Promise<void>((resolve, reject) => {
const checkForHandshake = () => {
// Check if we have any members or any relays (indicating handshake was received)
@ -1504,17 +1505,17 @@ export default class Services {
resolve();
return;
}
// Check timeout
if (Date.now() - startTime >= timeoutMs) {
reject(new Error(`No handshake message received after ${timeoutMs}ms timeout`));
return;
}
// Continue polling
setTimeout(checkForHandshake, pollInterval);
};
checkForHandshake();
});
}
@ -1565,7 +1566,7 @@ export default class Services {
this.sendCommitMessage(JSON.stringify(content));
}, 1000)
}
public getRoles(process: Process): Record<string, RoleDefinition> | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
@ -1745,7 +1746,7 @@ export default class Services {
return process.states[index + 1];
}
return null;
return null;
}
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {