bun-assemblyscript 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,177 @@
1
+ import { resolve, basename } from "path";
2
+ import { tmpdir } from "os";
3
+
4
+ export interface CompilerOptions {
5
+ optimizeLevel?: 0 | 1 | 2 | 3;
6
+ shrinkLevel?: 0 | 1 | 2;
7
+ runtime?: "minimal" | "stub" | "full" | "incremental";
8
+ sourceMap?: boolean;
9
+ debug?: boolean;
10
+ }
11
+
12
+ export interface CompilerResult {
13
+ success: boolean;
14
+ wasmBytes: Uint8Array | null;
15
+ sourceMapBytes: Uint8Array | null;
16
+ errors: string[];
17
+ }
18
+
19
+ /**
20
+ * Résout le chemin du binaire `asc.js` depuis node_modules.
21
+ * Lève une erreur claire si assemblyscript n'est pas installé.
22
+ */
23
+ function resolveAscJs(cwd: string): string {
24
+ const candidates = [
25
+ resolve(cwd, "node_modules", "assemblyscript", "bin", "asc.js"),
26
+ resolve(cwd, "..", "node_modules", "assemblyscript", "bin", "asc.js"),
27
+ ];
28
+
29
+ for (const candidate of candidates) {
30
+ try {
31
+ if (Bun.file(candidate).size > 0) return candidate;
32
+ } catch {}
33
+ }
34
+
35
+ throw new Error(
36
+ [
37
+ "AssemblyScript compiler (asc) introuvable.",
38
+ "Installez-le avec :",
39
+ " bun add -d assemblyscript",
40
+ ].join("\n")
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Crée un fichier `.ts` temporaire qui est une copie exacte du fichier `.as`.
46
+ *
47
+ * Pont .as → .ts : asc v0.28 n'accepte que les extensions `.ts`.
48
+ * On copie le source dans un fichier temporaire `.ts`, on compile,
49
+ * puis on supprime le temporaire.
50
+ *
51
+ * @returns Chemin absolu vers le fichier temporaire `.ts`.
52
+ */
53
+ async function createTsBridge(asFilePath: string): Promise<string> {
54
+ const source = await Bun.file(asFilePath).text();
55
+ const baseName = basename(asFilePath, ".as");
56
+ const tmpPath = resolve(tmpdir(), `asc_bridge_${baseName}_${Date.now()}.ts`);
57
+ await Bun.write(tmpPath, source);
58
+ return tmpPath;
59
+ }
60
+
61
+ /**
62
+ * Compile un fichier `.as` en bytes WASM.
63
+ *
64
+ * Stratégie :
65
+ * 1. Copier le `.as` en `.ts` temporaire (pont d'extension).
66
+ * 2. Invoquer `node asc.js` via Bun.spawn() — on utilise node car asc v0.28
67
+ * appelle WebAssembly.instantiateStreaming() en interne, non supporté par Bun.
68
+ * 3. Lire le fichier WASM généré.
69
+ * 4. Nettoyer les fichiers temporaires.
70
+ *
71
+ * @param filename Chemin absolu vers le fichier AssemblyScript source.
72
+ * @param options Options de compilation.
73
+ */
74
+ export async function compile(
75
+ filename: string,
76
+ options: CompilerOptions = {}
77
+ ): Promise<CompilerResult> {
78
+ const errors: string[] = [];
79
+
80
+ // 1. Résoudre asc.js
81
+ let ascJs: string;
82
+ try {
83
+ ascJs = resolveAscJs(process.cwd());
84
+ } catch (e) {
85
+ return {
86
+ success: false,
87
+ wasmBytes: null,
88
+ sourceMapBytes: null,
89
+ errors: [e instanceof Error ? e.message : String(e)],
90
+ };
91
+ }
92
+
93
+ // 2. Créer le pont .as → .ts
94
+ let tsBridgePath: string | null = null;
95
+ let outWasmPath: string | null = null;
96
+
97
+ try {
98
+ tsBridgePath = await createTsBridge(filename);
99
+ outWasmPath = tsBridgePath.replace(/\.ts$/, ".wasm");
100
+
101
+ // 3. Arguments CLI pour asc
102
+ const args = [
103
+ ascJs,
104
+ tsBridgePath,
105
+ "--outFile", outWasmPath,
106
+ "--optimizeLevel", String(options.optimizeLevel ?? 0),
107
+ "--runtime", options.runtime ?? "stub",
108
+ ];
109
+
110
+ if (options.shrinkLevel !== undefined) {
111
+ args.push("--shrinkLevel", String(options.shrinkLevel));
112
+ }
113
+ if (options.debug) args.push("--debug");
114
+ if (options.sourceMap) args.push("--sourceMap");
115
+
116
+ // 4. Lancer `node asc.js ...` — Node.js gère correctement WebAssembly.instantiateStreaming
117
+ const proc = Bun.spawn(["node", ...args], {
118
+ stdout: "pipe",
119
+ stderr: "pipe",
120
+ cwd: process.cwd(),
121
+ });
122
+
123
+ const [stderrText] = await Promise.all([
124
+ new Response(proc.stderr).text(),
125
+ proc.exited,
126
+ ]);
127
+
128
+ const exitCode = proc.exitCode;
129
+
130
+ // Collecter les erreurs stderr
131
+ for (const line of stderrText.split("\n")) {
132
+ const trimmed = line.trim();
133
+ if (trimmed.length > 0) errors.push(trimmed);
134
+ }
135
+
136
+ if (exitCode !== 0) {
137
+ return { success: false, wasmBytes: null, sourceMapBytes: null, errors };
138
+ }
139
+
140
+ // 5. Lire le WASM généré
141
+ const wasmFile = Bun.file(outWasmPath);
142
+ const wasmBytes = new Uint8Array(await wasmFile.arrayBuffer());
143
+
144
+ let sourceMapBytes: Uint8Array | null = null;
145
+ if (options.sourceMap) {
146
+ const mapPath = outWasmPath + ".map";
147
+ if (await Bun.file(mapPath).exists()) {
148
+ sourceMapBytes = new Uint8Array(await Bun.file(mapPath).arrayBuffer());
149
+ }
150
+ }
151
+
152
+ if (wasmBytes.byteLength === 0) {
153
+ return {
154
+ success: false,
155
+ wasmBytes: null,
156
+ sourceMapBytes: null,
157
+ errors: [...errors, "Aucun output WASM produit par asc."],
158
+ };
159
+ }
160
+
161
+ return { success: true, wasmBytes, sourceMapBytes, errors };
162
+
163
+ } catch (err) {
164
+ const message = err instanceof Error ? err.message : String(err);
165
+ errors.push(message);
166
+ return { success: false, wasmBytes: null, sourceMapBytes: null, errors };
167
+
168
+ } finally {
169
+ // 6. Nettoyer les fichiers temporaires dans tous les cas
170
+ const fs = await import("fs/promises");
171
+ for (const tmpFile of [tsBridgePath, outWasmPath, outWasmPath ? outWasmPath + ".map" : null]) {
172
+ if (tmpFile) {
173
+ try { await fs.unlink(tmpFile); } catch {}
174
+ }
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,45 @@
1
+ import { join } from "path";
2
+ import { existsSync } from "fs";
3
+
4
+ /**
5
+ * Lit le bunfig.toml du projet hôte et injecte "bun-plugin-assemblyscript"
6
+ * dans le tableau preload s'il n'est pas déjà présent.
7
+ *
8
+ * @param projectRoot Chemin racine du projet hôte
9
+ * @returns boolean indiquant si le fichier a été modifié
10
+ */
11
+ export async function patchBunfig(projectRoot: string): Promise<boolean> {
12
+ const bunfigPath = join(projectRoot, "bunfig.toml");
13
+ let content = "";
14
+ let changed = false;
15
+
16
+ if (existsSync(bunfigPath)) {
17
+ content = await Bun.file(bunfigPath).text();
18
+
19
+ // Si déjà présent, on ne fait rien pour ne pas dupliquer
20
+ if (!content.includes("bun-plugin-assemblyscript")) {
21
+ const preloadRegex = /preload\s*=\s*\[(.*?)\]/s;
22
+ const match = content.match(preloadRegex);
23
+
24
+ if (match) {
25
+ const inner = match[1].trim();
26
+ const addition = inner.length > 0 ? `, "bun-plugin-assemblyscript"` : `"bun-plugin-assemblyscript"`;
27
+ content = content.replace(preloadRegex, `preload = [${inner}${addition}]`);
28
+ changed = true;
29
+ } else {
30
+ content += `\npreload = ["bun-plugin-assemblyscript"]\n`;
31
+ changed = true;
32
+ }
33
+ }
34
+ } else {
35
+ // Fichier absent : on créé une version vierge minimale
36
+ content = `preload = ["bun-plugin-assemblyscript"]\n`;
37
+ changed = true;
38
+ }
39
+
40
+ if (changed) {
41
+ await Bun.write(bunfigPath, content);
42
+ }
43
+
44
+ return changed;
45
+ }
@@ -0,0 +1,142 @@
1
+ import { join } from "path";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { patchTsConfig } from "./tsconfig";
4
+ import { patchBunfig } from "./bunfig";
5
+ import { patchVsCode } from "./vscode";
6
+ import { Glob } from "bun";
7
+
8
+ async function run() {
9
+ // En mode postinstall NPM/Bun, INIT_CWD pointe vers le dossier du projet final installant le package
10
+ const hostRoot = process.env.INIT_CWD || process.cwd();
11
+
12
+ // Anti-boucle : si on est dans le dépôt originel de dev, on limite certains comportements
13
+ const isSelf = hostRoot === process.cwd() && existsSync(join(hostRoot, "src", "compiler.ts"));
14
+
15
+ console.log("");
16
+ console.log("\x1b[36m┌──────────────────────────────────────────────┐\x1b[0m");
17
+ console.log("\x1b[36m│ │\x1b[0m");
18
+ console.log("\x1b[36m│ \x1b[1m🚀 Installation bun-assemblyscript\x1b[0;36m │\x1b[0m");
19
+ console.log("\x1b[36m│ │\x1b[0m");
20
+
21
+ const results = {
22
+ ascParams: false,
23
+ bunfig: false,
24
+ globalDts: false,
25
+ tsconfig: false,
26
+ vscode: false,
27
+ example: false,
28
+ };
29
+
30
+ try {
31
+ // 1. Détecter assemblyscript
32
+ const pkgPath = join(hostRoot, "package.json");
33
+ if (existsSync(pkgPath)) {
34
+ const pkg = await Bun.file(pkgPath).json();
35
+ const hasAsc =
36
+ (pkg.dependencies && pkg.dependencies.assemblyscript) ||
37
+ (pkg.devDependencies && pkg.devDependencies.assemblyscript);
38
+ if (hasAsc) {
39
+ results.ascParams = true;
40
+ } else {
41
+ console.log(
42
+ "\x1b[33m[!] Warning: 'assemblyscript' n'est pas installé dans votre projet.\x1b[0m"
43
+ );
44
+ console.log(
45
+ "\x1b[33m => Lancez : bun add -d assemblyscript\x1b[0m\n"
46
+ );
47
+ }
48
+ }
49
+
50
+ // 2. bunfig.toml
51
+ await patchBunfig(hostRoot);
52
+ results.bunfig = true;
53
+
54
+ // 3. assemblyscript.d.ts (global)
55
+ const globalDest = join(hostRoot, "assemblyscript.d.ts");
56
+ if (!existsSync(globalDest) && !isSelf) {
57
+ // Résout depuis __dirname vers src/typegen/global.d.ts
58
+ const sourceGlobal = join(__dirname, "..", "typegen", "global.d.ts");
59
+ try {
60
+ const content = await Bun.file(sourceGlobal).text();
61
+ await Bun.write(globalDest, content);
62
+ } catch (e) {}
63
+ }
64
+ results.globalDts = true;
65
+
66
+ // 4. tsconfig.json
67
+ try {
68
+ await patchTsConfig(hostRoot);
69
+ results.tsconfig = true;
70
+ } catch (e) {}
71
+
72
+ // 5. VSCode Integration
73
+ try {
74
+ await patchVsCode(hostRoot, isSelf);
75
+ results.vscode = true;
76
+ } catch (e) {}
77
+
78
+ // 6. Créer example.as s'il n'y a pas de fichier .as dans le projet
79
+ let hasAsFile = false;
80
+ const asGlob = new Glob("**/*.as");
81
+ for (const file of asGlob.scanSync({ cwd: hostRoot })) {
82
+ if (!file.includes("node_modules") && !file.includes(".cache")) {
83
+ hasAsFile = true;
84
+ break;
85
+ }
86
+ }
87
+
88
+ let createdExample = false;
89
+ if (!hasAsFile && !isSelf) {
90
+ const wasmDir = join(hostRoot, "src", "wasm");
91
+ if (!existsSync(wasmDir)) {
92
+ mkdirSync(wasmDir, { recursive: true });
93
+ }
94
+
95
+ const asFile = join(wasmDir, "example.as");
96
+ const asCode = `// Exemple de fonction AssemblyScript
97
+ export function add(a: i32, b: i32): i32 {
98
+ return a + b;
99
+ }
100
+ `;
101
+ await Bun.write(asFile, asCode);
102
+
103
+ const tsFile = join(wasmDir, "example.ts");
104
+ const tsCode = `import { add } from "./example.as";
105
+
106
+ console.log("AssemblyScript add(10, 20) =", add(10, 20));
107
+ `;
108
+ await Bun.write(tsFile, tsCode);
109
+
110
+ createdExample = true;
111
+ }
112
+ results.example = true;
113
+
114
+ // Formatter de résultat [✓] et [✗]
115
+ const check = (ok: boolean) => (ok ? "\x1b[32m✓\x1b[36m" : "\x1b[31m✗\x1b[36m");
116
+
117
+ console.log("\x1b[36m│ " + check(results.ascParams) + " Vérification dépendance AssemblyScript │\x1b[0m");
118
+ console.log("\x1b[36m│ " + check(results.bunfig) + " Configuration automatique bunfig.toml │\x1b[0m");
119
+ console.log("\x1b[36m│ " + check(results.globalDts) + " Copie de assemblyscript.d.ts global │\x1b[0m");
120
+ console.log("\x1b[36m│ " + check(results.tsconfig) + " Configuration auto tsconfig.json │\x1b[0m");
121
+ console.log("\x1b[36m│ " + check(results.vscode) + " Intégration VSCode (.vscode) │\x1b[0m");
122
+ console.log("\x1b[36m│ " + check(results.example) + (createdExample ? " Fichier d'exemple généré " : " Projet contenant déjà des fichiers .as ") + "│\x1b[0m");
123
+ console.log("\x1b[36m│ │\x1b[0m");
124
+ console.log("\x1b[36m└──────────────────────────────────────────────┘\x1b[0m\n");
125
+
126
+ if (createdExample) {
127
+ console.log(
128
+ "\x1b[1m\x1b[32mInstallation réussie !\x1b[0m Testez l'exemple avec la commande ci-dessous :\n"
129
+ );
130
+ console.log(" \x1b[36mbun run src/wasm/example.ts\x1b[0m\n");
131
+ } else {
132
+ console.log(
133
+ "\x1b[1m\x1b[32mInstallation réussie !\x1b[0m Votre projet est prêt.\n"
134
+ );
135
+ }
136
+ } catch (err) {
137
+ console.error("\x1b[31m[bun-as] Erreur pendant l'installation:\x1b[0m", err);
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ run().catch(() => process.exit(0));
@@ -0,0 +1,61 @@
1
+ import { join } from "path";
2
+
3
+ /**
4
+ * Nettoie une chaîne JSON de ses commentaires (// et \/\* \*\/)
5
+ * afin de la rendre compatible avec JSON.parse().
6
+ */
7
+ function stripJsonComments(jsonStr: string): string {
8
+ return jsonStr.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
9
+ }
10
+
11
+ /**
12
+ * Lit, nettoie et parse le \`tsconfig.json\` du projet hôte.
13
+ * Fusionne les options pour inclure les dépendances gérées par le plugin.
14
+ * Ne supprime pas les configurations existantes de l'utilisateur.
15
+ *
16
+ * @param projectRoot Chemin vers la racine du projet hôte. (Par défaut \`process.cwd()\`)
17
+ */
18
+ export async function patchTsConfig(projectRoot: string = process.cwd()): Promise<void> {
19
+ const tsconfigPath = join(projectRoot, "tsconfig.json");
20
+ let config: any = {};
21
+
22
+ try {
23
+ const rawContent = await Bun.file(tsconfigPath).text();
24
+ config = JSON.parse(stripJsonComments(rawContent));
25
+ } catch (err) {
26
+ // Si absent ou invalide, on génère une configuration par défaut minimale
27
+ config = {
28
+ compilerOptions: {
29
+ target: "ESNext",
30
+ moduleResolution: "bundler",
31
+ },
32
+ };
33
+ }
34
+
35
+ // S'assurer que les clés minimales existent
36
+ if (!config.compilerOptions) {
37
+ config.compilerOptions = {};
38
+ }
39
+ // S'assurer que le tableau include existe à la racine
40
+ if (!Array.isArray(config.include)) {
41
+ if (config.include) config.include = [config.include];
42
+ else config.include = [];
43
+ }
44
+
45
+ const includes: string[] = config.include;
46
+ let changed = false;
47
+
48
+ const requiredIncludes = ["assemblyscript.d.ts", "**/*.as.d.ts"];
49
+
50
+ for (const req of requiredIncludes) {
51
+ if (!includes.includes(req)) {
52
+ includes.push(req);
53
+ changed = true;
54
+ }
55
+ }
56
+
57
+ if (changed) {
58
+ // Écriture du fichier modifié
59
+ await Bun.write(tsconfigPath, JSON.stringify(config, null, 2) + "\n");
60
+ }
61
+ }
@@ -0,0 +1,80 @@
1
+ import { join } from "path";
2
+ import { existsSync, mkdirSync } from "fs";
3
+
4
+ export async function patchVsCode(hostRoot: string, isSelf: boolean) {
5
+ // Optionnel: on ne le fait pas si on est dans notre propre repo de dev, sauf si demandé.
6
+ // Mais c'est pratique de l'avoir même en dev. On va l'exécuter dans tous les cas.
7
+
8
+ const vscodeDir = join(hostRoot, ".vscode");
9
+ if (!existsSync(vscodeDir)) {
10
+ mkdirSync(vscodeDir, { recursive: true });
11
+ }
12
+
13
+ // 6.1 settings.json
14
+ const settingsPath = join(vscodeDir, "settings.json");
15
+ let settings: any = {};
16
+ if (existsSync(settingsPath)) {
17
+ try {
18
+ settings = await Bun.file(settingsPath).json();
19
+ } catch (e) {
20
+ // Ignorer si le JSON est invalide
21
+ }
22
+ }
23
+ if (!settings["files.associations"]) {
24
+ settings["files.associations"] = {};
25
+ }
26
+ settings["files.associations"]["*.as"] = "typescript";
27
+ await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
28
+
29
+ // 6.2 as.code-snippets
30
+ const snippetsPath = join(vscodeDir, "as.code-snippets");
31
+ const snippets = {
32
+ "AssemblyScript Export": {
33
+ "prefix": "asexport",
34
+ "scope": "typescript",
35
+ "body": [
36
+ "export function ${1:name}(${2:a}: ${3:i32}): ${4:i32} {",
37
+ " $0",
38
+ "}"
39
+ ],
40
+ "description": "Exported function for AssemblyScript"
41
+ },
42
+ "AssemblyScript Class": {
43
+ "prefix": "asclass",
44
+ "scope": "typescript",
45
+ "body": [
46
+ "export class ${1:Name} {",
47
+ " constructor() {",
48
+ " $0",
49
+ " }",
50
+ "}"
51
+ ],
52
+ "description": "Exported class with constructor for AssemblyScript"
53
+ },
54
+ "AssemblyScript Import": {
55
+ "prefix": "asimport",
56
+ "scope": "typescript",
57
+ "body": [
58
+ "import { ${2:func} } from \"${1:./module.as}\";$0"
59
+ ],
60
+ "description": "Import pattern from an .as file"
61
+ }
62
+ };
63
+ await Bun.write(snippetsPath, JSON.stringify(snippets, null, 2));
64
+
65
+ // 6.3 extensions.json
66
+ const extensionsPath = join(vscodeDir, "extensions.json");
67
+ let extensions: any = {};
68
+ if (existsSync(extensionsPath)) {
69
+ try {
70
+ extensions = await Bun.file(extensionsPath).json();
71
+ } catch (e) {}
72
+ }
73
+ if (!extensions.recommendations) {
74
+ extensions.recommendations = [];
75
+ }
76
+ if (!extensions.recommendations.includes("saulecabrera.vscode-assemblyscript")) {
77
+ extensions.recommendations.push("saulecabrera.vscode-assemblyscript");
78
+ }
79
+ await Bun.write(extensionsPath, JSON.stringify(extensions, null, 2));
80
+ }
@@ -0,0 +1,90 @@
1
+ export interface ASExports {
2
+ [key: string]: (...args: number[]) => number | void;
3
+ }
4
+
5
+ export class AbortError extends Error {
6
+ constructor(
7
+ message: string,
8
+ public readonly file: string,
9
+ public readonly line: number,
10
+ public readonly column: number
11
+ ) {
12
+ super(message);
13
+ this.name = "AbortError";
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Lit une String AssemblyScript depuis la mémoire linéaire.
19
+ *
20
+ * Format mémoire AS (runtime stub / minimal / full) :
21
+ * ptr - 16 : ClassID (4 octets)
22
+ * ptr - 12 : reserved (4 octets)
23
+ * ptr - 8 : rtSize (4 octets) ← taille du buffer en octets
24
+ * ptr - 4 : length (4 octets) ← nombre de code units UTF-16
25
+ * ptr : données UTF-16 ← longueur = length * 2 octets
26
+ *
27
+ * Si ptr est 0 ou invalide, retourne la chaîne vide.
28
+ */
29
+ function readASString(memory: WebAssembly.Memory, ptr: number): string {
30
+ if (ptr === 0) return "";
31
+
32
+ const buf = memory.buffer;
33
+ const dataView = new DataView(buf);
34
+
35
+ // Lire la longueur (code units UTF-16) stockée à ptr - 4
36
+ const byteLength = dataView.getInt32(ptr - 4, true); // en octets
37
+ const length = byteLength >>> 1; // en code-units
38
+
39
+ if (length <= 0 || ptr + byteLength > buf.byteLength) return "";
40
+
41
+ const u16 = new Uint16Array(buf, ptr, length);
42
+ return String.fromCharCode(...u16);
43
+ }
44
+
45
+ /**
46
+ * Instancie un module WASM AssemblyScript.
47
+ *
48
+ * Fournit les imports obligatoires :
49
+ * - `env.memory` : WebAssembly.Memory (initial 1 page = 64 Ko)
50
+ * - `env.abort` : handler qui lève une AbortError lisible
51
+ *
52
+ * @returns Un objet plat ne contenant que les exports de type `function`.
53
+ */
54
+ export async function instantiate(wasmBytes: Uint8Array): Promise<ASExports> {
55
+ const memory = new WebAssembly.Memory({ initial: 1 });
56
+
57
+ const imports = {
58
+ env: {
59
+ memory,
60
+ abort(
61
+ msgPtr: number,
62
+ filePtr: number,
63
+ line: number,
64
+ col: number
65
+ ): void {
66
+ const message = readASString(memory, msgPtr);
67
+ const file = readASString(memory, filePtr);
68
+ throw new AbortError(
69
+ `AbortError: ${message || "(no message)"} — ${file || "(unknown file)"}:${line}:${col}`,
70
+ file,
71
+ line,
72
+ col
73
+ );
74
+ },
75
+ },
76
+ };
77
+
78
+ const { instance } = await WebAssembly.instantiate(wasmBytes, imports);
79
+ const rawExports = instance.exports as Record<string, unknown>;
80
+
81
+ // Ré-exporter nommément chaque fonction (objet plat, pas instance.exports)
82
+ const flatExports: ASExports = {};
83
+ for (const [key, value] of Object.entries(rawExports)) {
84
+ if (typeof value === "function") {
85
+ flatExports[key] = value as (...args: number[]) => number | void;
86
+ }
87
+ }
88
+
89
+ return flatExports;
90
+ }