|
1 | 1 | import { writeFile } from 'node:fs/promises' |
2 | 2 | import path from 'path' |
3 | | -import { parseFromFiles } from '@ts-ast-parser/core' |
| 3 | +import { createRequire } from 'node:module' |
| 4 | +import { base32 } from 'iso-base/rfc4648' |
4 | 5 | import * as esbuild from 'esbuild' |
5 | | -import { getTsconfig } from 'get-tsconfig' |
| 6 | +import { deleteAsync } from 'del' |
6 | 7 |
|
7 | 8 | // @ts-ignore |
8 | | -import { componentize } from '@bytecodealliance/componentize-js' |
9 | | - |
10 | | -/** |
11 | | - * @param {import('@ts-ast-parser/core').Type} type |
12 | | - * @returns {string} |
13 | | - */ |
14 | | -function primitiveType(type) { |
15 | | - if (type.text === 'string') { |
16 | | - return 'string' |
17 | | - } |
18 | | - |
19 | | - if (type.text === 'boolean') { |
20 | | - return 'bool' |
21 | | - } |
22 | | - |
23 | | - if (type.text === 'number') { |
24 | | - return 's64' |
25 | | - } |
26 | | - |
27 | | - if (type.text === 'Uint8Array') { |
28 | | - return 'list<u8>' |
29 | | - } |
| 9 | +import replacePlugin from 'esbuild-plugin-replace-regex' |
30 | 10 |
|
31 | | - if (type.kind === 'Array' && type.elementType) { |
32 | | - return `list<${primitiveType(type.elementType)}>` |
33 | | - } |
| 11 | +// @ts-ignore |
| 12 | +import { componentize } from '@bytecodealliance/componentize-js' |
| 13 | +import { wit } from './wit.js' |
34 | 14 |
|
35 | | - throw new Error(`Unknown type: ${JSON.stringify(type)}`) |
36 | | -} |
| 15 | +const require = createRequire(import.meta.url) |
37 | 16 |
|
38 | 17 | /** |
39 | | - * |
40 | | - * Generate a WIT file from a TypeScript file |
| 18 | + * Bundle the js code with WASI support |
41 | 19 | * |
42 | 20 | * @param {string} filePath - Path to a TypeScript file |
43 | 21 | */ |
44 | | -async function wit(filePath) { |
45 | | - const cfg = getTsconfig(filePath) |
46 | | - if (!cfg) { |
47 | | - throw new Error('No tsconfig found') |
48 | | - } |
49 | | - |
50 | | - const { project, errors } = await parseFromFiles([filePath], { |
51 | | - tsConfigFilePath: cfg.path, |
52 | | - }) |
53 | | - |
54 | | - if (errors.length > 0) { |
55 | | - console.error(errors) |
56 | | - // Handle the errors |
57 | | - |
58 | | - // process.exit(1) |
59 | | - } |
60 | | - |
61 | | - const result = project?.getModules().map((m) => m.serialize()) ?? [] |
62 | | - if (result.length > 0) { |
63 | | - // console.log( |
64 | | - // '🚀 ~ file: cli.js:23 ~ reflectedModules:', |
65 | | - // JSON.stringify(result, null, 2) |
66 | | - // ) |
67 | | - const { sourcePath, declarations } = result[0] |
68 | | - const world = path.basename(sourcePath).replace('.ts', '') |
69 | | - const exports = declarations.map((d) => { |
70 | | - if (d.kind === 'Function') { |
71 | | - /** @type {string[]} */ |
72 | | - const params = d.signatures[0].parameters |
73 | | - ? d.signatures[0].parameters.map( |
74 | | - (p) => `${p.name}: ${primitiveType(p.type)}` |
75 | | - ) |
76 | | - : [] |
77 | | - const name = d.name |
78 | | - const returnType = primitiveType(d.signatures[0].return.type) |
79 | | - |
80 | | - return ` export ${name}: func(${params.join(', ')}) -> ${returnType};` |
81 | | - } |
82 | | - |
83 | | - return '' |
84 | | - }) |
85 | | - |
86 | | - const wit = ` |
87 | | -package local:${world}; |
88 | | -
|
89 | | -world ${world} { |
90 | | -${exports.join('\n')} |
91 | | -} |
92 | | - ` |
93 | | - // console.log('🚀 ~ WIT World\n\n', wit) |
94 | | - return wit |
95 | | - } else { |
96 | | - throw new Error('No modules found') |
| 22 | +async function bundle(filePath) { |
| 23 | + const wasiImports = new Set() |
| 24 | + /** @type {import('esbuild').Plugin} */ |
| 25 | + const wasiPlugin = { |
| 26 | + name: 'wasi imports', |
| 27 | + setup(build) { |
| 28 | + // @ts-ignore |
| 29 | + build.onResolve({ filter: /^wasi:.*/ }, (args) => { |
| 30 | + wasiImports.add(`import ${args.path};`) |
| 31 | + }) |
| 32 | + }, |
97 | 33 | } |
98 | | -} |
99 | 34 |
|
100 | | -/** |
101 | | - * @param {string} filePath - Path to a TypeScript file |
102 | | - */ |
103 | | -async function bundle(filePath) { |
104 | 35 | const result = await esbuild.build({ |
105 | 36 | entryPoints: [filePath], |
106 | 37 | bundle: true, |
107 | 38 | format: 'esm', |
108 | 39 | platform: 'browser', |
109 | 40 | write: false, |
| 41 | + plugins: [ |
| 42 | + replacePlugin({ |
| 43 | + filter: /homestar-wit\/src\/logging.js$/, |
| 44 | + patterns: [ |
| 45 | + ['let wasiLog', '// let wasiLog'], |
| 46 | + [ |
| 47 | + "// import { log as wasiLog } from 'wasi:logging/logging'", |
| 48 | + "import { log as wasiLog } from 'wasi:logging/logging'", |
| 49 | + ], |
| 50 | + ], |
| 51 | + }), |
| 52 | + wasiPlugin, |
| 53 | + ], |
| 54 | + external: ['wasi:*'], |
110 | 55 | }) |
111 | | - // console.log('🚀 ~ bundle ~ result:', result.outputFiles[0].hash) |
112 | | - return result.outputFiles[0].text |
| 56 | + return { |
| 57 | + src: result.outputFiles[0].text, |
| 58 | + hash: result.outputFiles[0].hash, |
| 59 | + wasiImports, |
| 60 | + } |
113 | 61 | } |
114 | 62 |
|
115 | 63 | /** |
| 64 | + * Generate wasm component from a TypeScript file |
| 65 | + * |
116 | 66 | * @param {string} filePath - Path to a TypeScript file |
117 | 67 | * @param {string} outDir - Path to a directory to write the Wasm component file |
118 | 68 | */ |
119 | 69 | export async function build(filePath, outDir = process.cwd()) { |
120 | | - const outName = path |
121 | | - .basename(filePath) |
122 | | - .replace(path.extname(filePath), '.wasm') |
123 | | - const outPath = path.join(outDir, outName) |
124 | | - |
125 | | - const { component } = await componentize( |
126 | | - await bundle(filePath), |
127 | | - await wit(filePath) |
128 | | - ) |
129 | | - await writeFile(outPath, component) |
130 | | - |
131 | | - return { |
132 | | - outPath, |
| 70 | + const pkgPath = require.resolve('@fission-codes/homestar-wit') |
| 71 | + const witPath = path.join(pkgPath, '..', '..', 'wit') |
| 72 | + let witHash |
| 73 | + |
| 74 | + // Clean up any old WIT files in the wit directory |
| 75 | + // componentize process.exit(1) if it fails so we can't clean up after it |
| 76 | + await deleteAsync([`${witPath}/*.wit`], { force: true }) |
| 77 | + |
| 78 | + try { |
| 79 | + const { hash, src, wasiImports } = await bundle(filePath) |
| 80 | + witHash = base32.encode(hash).toLowerCase() |
| 81 | + |
| 82 | + // TODO: check the wit hash and only componentize if it has changed |
| 83 | + const witSource = await wit({ filePath, worldName: witHash, wasiImports }) |
| 84 | + const witFile = path.join(witPath, `${witHash}.wit`) |
| 85 | + await writeFile(witFile, witSource, 'utf8') |
| 86 | + const { component } = await componentize(src, { |
| 87 | + witPath, |
| 88 | + worldName: witHash, |
| 89 | + // debug: true, |
| 90 | + sourceName: filePath, |
| 91 | + }) |
| 92 | + const outPath = path.join(outDir, `${witHash}.wasm`) |
| 93 | + await writeFile(outPath, component) |
| 94 | + |
| 95 | + return { |
| 96 | + outPath, |
| 97 | + witHash, |
| 98 | + } |
| 99 | + } finally { |
| 100 | + if (witHash) { |
| 101 | + await deleteAsync([path.join(witPath, `${witHash}.wit`)], { force: true }) |
| 102 | + } |
133 | 103 | } |
134 | 104 | } |
0 commit comments