diff --git a/.gitignore b/.gitignore index 9f5e845..d39427d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,9 @@ crates/js_sdk/pkg # BMAD Method temporary files .current-feature-branch -.bmad-temp/ \ No newline at end of file +.bmad-temp/ + + +node_modules/ + +pnpm-lock.yaml \ No newline at end of file diff --git a/crates/node_sdk/README.md b/crates/node_sdk/README.md index 5f347ba..464b7e2 100644 --- a/crates/node_sdk/README.md +++ b/crates/node_sdk/README.md @@ -1,7 +1,39 @@ +
+ # source_map_parser_node -**高性能 Source Map 解析 & 错误堆栈映射 (WASM)** -Rust 实现 + wasm-bindgen 导出,面向 Node.js 生产错误还原、调试定位、上下文截取。 +高性能 Source Map 解析 & 错误堆栈映射(Rust + WASM) + +`dist/` 目录提供稳定的库模式入口;`pkg/` 保留底层 wasm-bindgen 原始输出。 + +
+ +> 自 v0.1.x 起:推荐使用 **库模式封装层 (dist)**。仍可通过 `source_map_parser_node/raw` 访问原始绑定。完全 **ESM only**,不再提供 CJS 入口。 + +## 🚀 TL;DR + +```ts +import smp, { + lookup_token, + mapErrorStackWithResolver, +} from 'source_map_parser_node'; + +await smp.init(); // 幂等,可省略 + +const token = JSON.parse(lookup_token(sourceMapContent, 1, 0)); + +const batch = await smp.mapErrorStackWithResolver({ + errorStack: someStackString, + resolveSourceMap: (p) => cache.get(p), +}); +``` + +| 层级 | 入口 | 用途 | 特点 | +| -------- | ---------------------------- | ---------------- | ---------------------------------------- | +| 高级封装 | `source_map_parser_node` | 直接业务使用 | 有 `init`、辅助包装函数 | +| 原始绑定 | `source_map_parser_node/raw` | 自己做包装、调试 | wasm-pack 生成;所有函数返回 JSON 字符串 | + +--- ## ✨ 特性 @@ -21,12 +53,13 @@ npm install source_map_parser_node > 如果你是从源码构建,请在仓库根执行 `bash scripts/build-wasm-node.sh`,然后 `require('./crates/node_sdk/pkg')`。 -## ⚡ 快速上手 +## ⚡ 快速上手(库模式) -```js -const wasm = require('source_map_parser_node'); +```ts +import smp, { lookup_token } from 'source_map_parser_node'; + +// 你也可以:import * as raw from 'source_map_parser_node/raw' -// 示例最小 sourcemap const sm = JSON.stringify({ version: 3, sources: ['a.js'], @@ -35,23 +68,20 @@ const sm = JSON.stringify({ mappings: 'AAAA', }); -// 所有导出函数都返回 JSON 字符串,需要再 JSON.parse 一次 -const token = JSON.parse(wasm.lookup_token(sm, 1, 0)); +await smp.init(); // 幂等 +const token = JSON.parse(lookup_token(sm, 1, 0)); console.log(token); ``` -### 一个便捷的包装函数 - -```js -const W = require('source_map_parser_node'); -const call = (fn, ...args) => JSON.parse(W[fn](...args)); +### 原始层快速包装 -const tok = call('lookup_token', sm, 1, 0); +```ts +import * as raw from 'source_map_parser_node/raw'; +const json = raw.lookup_token(sm, 1, 0); +const tok = JSON.parse(json); ``` -## 🧪 API 速览 - -所有函数同步返回 JSON 字符串,请自行 `JSON.parse`。 +## 🧪 API 速览(均返回 JSON 字符串) | 函数 | 作用 | 关键参数 | 返回结构(概念) | | ------------------------------------------------------------------------ | ------------------------ | ------------------------------ | -------------------------------------------- | @@ -65,7 +95,7 @@ const tok = call('lookup_token', sm, 1, 0); | `generate_token_by_single_stack(line,column,sm,contextOffset?)` | 直接行列生成 | 可选上下文偏移 | `Token \| null` | | `generate_token_by_stack_raw(stackRaw, formatter?, resolver?, onError?)` | 批量任务模式 | 自定义路径改写/内容解析 | `{ stacks, success, fail }` | -### generate_token_by_stack_raw 说明 +### `generate_token_by_stack_raw` 说明 ```ts generate_token_by_stack_raw( @@ -137,3 +167,106 @@ MIT --- 欢迎提 Issue / PR 改进 API;更多开发 / 发布流程参见仓库根 `CONTRIBUTORS.md`。 + +## 🔀 模块与分层策略 + +| 目录/入口 | 说明 | 适用场景 | +| ---------------------------- | ------------------------------------------- | ------------------------------------ | +| `dist/index.es.js` | 库模式(Vite 构建),顶层已完成 wasm 初始化 | 生产业务、通用集成 | +| `pkg/*.js/wasm` | wasm-pack 原始输出 | 调试、二次封装、对 wasm 行为精准控制 | +| `source_map_parser_node/raw` | 指向 `pkg/source_map_parser_node.js` | 需要最原始绑定 | + +特性: + +- 仅 ESM:无需 CJS 分发路径,减少条件分支 +- wasm 静态导入:让现代打包器可执行拓扑分析与缓存 +- 测试使用 alias 指向 dist,保证真实发布路径被验证 + +### 常见集成模式 + +| 场景 | 推荐 | 说明 | +| -------------- | ------ | --------------------- | +| Web 服务 / SSR | 库模式 | 直接 import 即可 | +| CLI / 本地工具 | 库模式 | 体积接受、维护简单 | +| 极限性能实验 | 原始层 | 自行管理缓存/解析策略 | + +### 从旧版本迁移 + +旧:`import * as wasm from 'source_map_parser_node'` (直接就是原始层) +新: + +```diff +- import * as wasm from 'source_map_parser_node'; ++ import smp, * as wasm from 'source_map_parser_node'; // 保持原有 API 同时获得封装 ++ await smp.init(); +``` + +## 🧠 高级封装:`mapErrorStackWithResolver` + +```ts +import smp from 'source_map_parser_node'; +const result = await smp.mapErrorStackWithResolver({ + errorStack: rawError.stack, + resolveSourceMap: (fp) => lru.get(fp), + formatter: (fp) => (fp.endsWith('.map') ? fp : fp + '.map'), + onError: (line, msg) => console.warn('[SM_FAIL]', line, msg), +}); +``` + +返回即为底层 `generate_token_by_stack_raw` 解析结构。 + +## 🧩 构建 & 测试 + +本仓库内部: + +```bash +pnpm run build:lib # 构建 dist +pnpm test # 预设 pretest 钩子可自动构建 +``` + +Vite / Vitest 需要: + +```ts +import wasm from 'vite-plugin-wasm'; +import topLevelAwait from 'vite-plugin-top-level-await'; +export default defineConfig({ + plugins: [wasm(), topLevelAwait()], +}); +``` + +## 📦 体积与优化建议 + +- 若生产体积仍偏大,可使用 `wasm-opt -Oz`(需要安装 binaryen) +- 频繁重复解析同一 sourcemap:上层缓存其字符串;或追加一个 JS 侧 LRU +- 批量 stack 解析优先使用 `generate_token_by_stack_raw` 减少往返 + +## 🧪 返回 JSON 的再封装(可选) + +在你的代码中可创建一个轻量包装: + +```ts +import { lookup_token as _lookup } from 'source_map_parser_node'; +export const lookupToken = (sm: string, line: number, col: number) => + JSON.parse(_lookup(sm, line, col)); +``` + +## 🔒 运行时注意事项 + +- 行号传入:1-based;列:0-based +- sourcemap 必须符合 v3 标准;异常返回结构含有 `error` +- Node 需支持 ESM + WebAssembly(Node 16+ 建议 18+) + +## 🧩 Vite / Vitest 使用提示 + +由于 bundler 目标使用了 **WebAssembly ESM 集成提案** 语法,直接在 Vite 中需要插件支持: + +```ts +// vitest.config.ts / vite.config.ts +import wasm from 'vite-plugin-wasm'; +import topLevelAwait from 'vite-plugin-top-level-await'; +export default defineConfig({ + plugins: [wasm(), topLevelAwait()], +}); +``` + +若你的构建工具不支持上面语法,可改用 `wasm-pack --target nodejs` 或自己写 `fetch + WebAssembly.instantiate` 包装。 diff --git a/crates/node_sdk/package.json b/crates/node_sdk/package.json new file mode 100644 index 0000000..accf11b --- /dev/null +++ b/crates/node_sdk/package.json @@ -0,0 +1,44 @@ +{ + "name": "source_map_parser_node", + "collaborators": [ + "MasonChow " + ], + "description": "A WebAssembly package for source_map_parser", + "version": "0.2.1", + "license": "MIT", + "type": "module", + "files": [ + "dist/", + "README.md", + "LICENSE" + ], + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.es.js" + }, + "./wasm": "./pkg/source_map_parser_node_bg.wasm", + "./raw": { + "types": "./pkg/source_map_parser_node.d.ts", + "import": "./pkg/source_map_parser_node.js" + } + }, + "scripts": { + "build:lib": "vite build", + "build": "bash ../../scripts/build-wasm-node.sh && vite build", + "pretest": "pnpm run build:lib", + "test": "pnpm pretest && vitest --run", + "test:coverage": "vitest --coverage", + "deploy": "pnpm run build && npm publish" + }, + "devDependencies": { + "@vitest/coverage-v8": "^2.0.5", + "@vitest/ui": "^2.0.5", + "typescript": "^5.5.4", + "vite": "^5.4.0", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.5.0", + "vitest": "^2.0.5" + } +} \ No newline at end of file diff --git a/crates/node_sdk/src/index.ts b/crates/node_sdk/src/index.ts new file mode 100644 index 0000000..99fb1de --- /dev/null +++ b/crates/node_sdk/src/index.ts @@ -0,0 +1,43 @@ +// 高级入口:对 pkg 目录下 wasm 绑定做一层稳定封装 +// 目标:库模式构建 (Vite) 输出到 dist,并在使用端自动完成 wasm 初始化。 +// 注意:保持对原始 API 的命名导出,不修改 wasm 生成的函数签名。 + +// 直接引用已经生成的绑定代码。 +// Vite 在构建时会处理对 .wasm 的静态导入(需保留插件或默认支持)。 +import * as lowLevel from '../pkg/source_map_parser_node.js'; + +// 再导出所有低层 API,保持向后兼容。 +export * from '../pkg/source_map_parser_node.js'; + +// 提供一个可显式调用的 init(幂等),方便在某些 SSR/自定义加载场景中手动控制。 +let _inited = false; +export async function init(): Promise { + if (_inited) return; + // 这里实际上只要执行过绑定文件的顶层代码就已经初始化, + // 但为了语义化,仍然提供一个 Promise 接口,未来可在此扩展(例如自定义 wasm fetch)。 + _inited = true; +} + +// 提供一个辅助方法,对常见用例进行包装示例(非必须,可选增强)。 +export async function mapErrorStackWithResolver(options: { + errorStack: string; + resolveSourceMap: (filePath: string) => string | undefined | null; + formatter?: (filePath: string) => string; + onError?: (rawLine: string, message: string) => void; +}): Promise { + await init(); + const { errorStack, resolveSourceMap, formatter, onError } = options; + return lowLevel.generate_token_by_stack_raw( + errorStack, + formatter ?? null, + (p: string) => resolveSourceMap(p) ?? null, + onError ?? null + ); +} + +// 默认导出整体 API(含原始导出与封装方法)。 +export default { + init, + mapErrorStackWithResolver, + ...lowLevel, +}; diff --git a/crates/node_sdk/tests/test_basic.test.mjs b/crates/node_sdk/tests/test_basic.test.mjs new file mode 100644 index 0000000..88e3d9b --- /dev/null +++ b/crates/node_sdk/tests/test_basic.test.mjs @@ -0,0 +1,139 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import * as wasm from 'source_map_parser_node'; + +// await wasm.init(); + +beforeAll(async () => { + await wasm.init(); +}); + +// 简单 source map 生成器 +function simpleSM({ codeLines, src = 'src/a.js' }) { + const content = codeLines.join('\n') + '\n'; + return JSON.stringify({ + version: 3, + file: 'min.js', + sources: [src], + sourcesContent: [content], + names: [], + mappings: 'AAAA', + }); +} + +describe('node sdk basic exports', () => { + it('lookup_token returns source location', () => { + const sm = simpleSM({ codeLines: ['fn()'] }); + const raw = wasm.lookup_token(sm, 1, 0); // wasm 返回的是 string (JSON) + const tok = JSON.parse(raw); + expect(tok.line).toBe(0); // 原始源码行 (0-based in rust output) + expect(tok.src).toContain('src/a.js'); + }); + + it('lookup_token_with_context returns context token', () => { + const sm = simpleSM({ codeLines: ['l0()', 'l1()', 'l2()'] }); + const raw = wasm.lookup_token_with_context(sm, 1, 0, 1); + const tok = JSON.parse(raw); + expect(tok.source_code.length).toBeGreaterThanOrEqual(2); + const target = tok.source_code.find((lny) => lny.is_stack_line); + expect(target).toBeTruthy(); + }); + + it('lookup_context returns snippet', () => { + const sm = simpleSM({ codeLines: ['a()', 'b()', 'c()'] }); + const raw = wasm.lookup_context(sm, 1, 0, 1); + const snippet = JSON.parse(raw); + expect(snippet.context.length).toBeGreaterThanOrEqual(2); + }); + + it('map_stack_line maps a single line', () => { + const sm = simpleSM({ codeLines: ['fn()'] }); + const stackLine = 'at foo (https://example.com/min.js:1:0)'; + const raw = wasm.map_stack_line(sm, stackLine); + const tok = JSON.parse(raw); + expect(tok.line).toBe(0); + }); + + it('map_stack_trace maps multiple lines', () => { + const sm = simpleSM({ codeLines: ['l0()', 'l1()'] }); + const trace = [ + 'at foo (https://example.com/min.js:1:0)', + 'at bar (https://example.com/min.js:1:0)', + ].join('\n'); + const raw = wasm.map_stack_trace(sm, trace); + const list = JSON.parse(raw); + expect(Array.isArray(list)).toBe(true); + expect(list.length).toBe(2); + }); + + it('map_error_stack simple mapping without context', () => { + const sm = simpleSM({ codeLines: ['a()'] }); + const errorStackRaw = [ + 'ReferenceError: x is not defined', + ' at foo (https://example.com/min.js:1:0)', + ].join('\n'); + const raw = wasm.map_error_stack(sm, errorStackRaw, null); + const result = JSON.parse(raw); + expect(result.error_message).toMatch(/x is not defined/); + expect(result.frames.length).toBe(1); + }); + + it('map_error_stack with context', () => { + const sm = simpleSM({ codeLines: ['l0()', 'l1()', 'l2()'] }); + const errorStackRaw = [ + 'TypeError: boom', + ' at foo (https://example.com/min.js:1:0)', + ].join('\n'); + const raw = wasm.map_error_stack(sm, errorStackRaw, 1); + const result = JSON.parse(raw); + expect(result.frames_with_context.length).toBe(1); + expect( + result.frames_with_context[0].source_code.length + ).toBeGreaterThanOrEqual(2); + }); +}); + +describe('batch token generation', () => { + it('generate_token_by_single_stack returns token', () => { + const sm = simpleSM({ codeLines: ['fn()'] }); + const raw = wasm.generate_token_by_single_stack(1, 0, sm, null); + const tok = JSON.parse(raw); + expect(tok.line).toBe(0); + }); + + it('generate_token_by_stack_raw with resolver + formatter', () => { + const sm = simpleSM({ codeLines: ['l0()', 'l1()', 'l2()'] }); + const stackRaw = [ + 'Error: test', + ' at foo (https://example.com/min.js:1:0)', + ' at bar (https://example.com/min.js:1:0)', + ].join('\n'); + + const formatter = (p) => p; // 不做变换 + const resolver = (p) => sm; // 始终返回同一个 sourcemap + const errors = []; + const onError = (line, msg) => errors.push({ line, msg }); + + const raw = wasm.generate_token_by_stack_raw( + stackRaw, + formatter, + resolver, + onError + ); + const result = JSON.parse(raw); + expect(result.success.length).toBe(2); + expect(result.fail.length).toBe(0); + expect(errors.length).toBe(0); + }); + + it('generate_token_by_stack_raw when no resolver provided collects fails', () => { + const sm = simpleSM({ codeLines: ['l0()'] }); + const stackRaw = [ + 'Error: test', + ' at foo (https://example.com/min.js:1:0)', + ].join('\n'); + const raw = wasm.generate_token_by_stack_raw(stackRaw, null, null, null); + const result = JSON.parse(raw); + expect(result.success.length).toBe(0); + expect(result.fail.length).toBe(1); + }); +}); diff --git a/crates/node_sdk/tests/wasm_extended.rs b/crates/node_sdk/tests/wasm_extended.rs deleted file mode 100644 index 09a931a..0000000 --- a/crates/node_sdk/tests/wasm_extended.rs +++ /dev/null @@ -1,74 +0,0 @@ -use source_map_parser_node::{ - generate_token_by_single_stack, generate_token_by_stack_raw, map_error_stack, map_stack_trace, -}; -use wasm_bindgen_test::*; - -// 仅 Node 环境测试 (wasm-pack test --node),不配置浏览器宏 - -fn sm_one(content: &str) -> String { - let esc = content.replace('\n', "\\n"); - format!("{{\"version\":3,\"file\":\"min.js\",\"sources\":[\"a.js\"],\"sourcesContent\":[\"{esc}\"],\"names\":[],\"mappings\":\"AAAA\"}}") -} - -#[wasm_bindgen_test] -fn single_stack_token_ok() { - let sm = sm_one("fn()\n"); - let v = generate_token_by_single_stack(1, 0, sm, Some(1)); - let s = v.as_string().unwrap(); - assert!(s.contains("source_code")); -} - -#[wasm_bindgen_test] -fn single_stack_token_none_when_invalid_line() { - let sm = sm_one("fn()\n"); - let v = generate_token_by_single_stack(0, 0, sm, None); - let s = v.as_string().unwrap(); - assert_eq!(s, "null"); -} - -#[wasm_bindgen_test] -fn generate_token_by_stack_raw_with_resolver() { - use js_sys::Function; - let stack_raw = "Error: x\n at foo (https://example.com/min.js:1:0)"; - let sm = sm_one("fn()\n"); - // formatter: identity - let formatter = Function::new_no_args("return arguments[0];"); - // resolver: always return the sm - let resolver = Function::new_with_args("p", &format!("return `{}`;", sm)); - let js = generate_token_by_stack_raw( - stack_raw.to_string(), - Some(formatter.clone()), - Some(resolver.clone()), - None, - ); - let s = js.as_string().unwrap(); - assert!(s.contains("success")); - assert!(s.contains("\"fail\":[]")); -} - -#[wasm_bindgen_test] -fn map_error_stack_with_context_some() { - let sm = sm_one("l0()\nl1()\n"); - let err = "Error: boom\n at foo (https://example.com/min.js:1:0)"; - let js = map_error_stack(&sm, err, Some(1)); - let s = js.as_string().unwrap(); - assert!(s.contains("frames_with_context")); -} - -#[wasm_bindgen_test] -fn map_error_stack_without_context() { - let sm = sm_one("l0()\nl1()\n"); - let err = "Error: boom\n at foo (https://example.com/min.js:1:0)"; - let js = map_error_stack(&sm, err, None); - let s = js.as_string().unwrap(); - assert!(s.contains("frames\"")); -} - -#[wasm_bindgen_test] -fn map_stack_trace_multi() { - let sm = sm_one("l0()\n"); - let trace = "at foo (https://example.com/min.js:1:0)\n@https://example.com/min.js:1:0"; - let js = map_stack_trace(&sm, trace); - let s = js.as_string().unwrap(); - assert!(s.starts_with("[")); -} diff --git a/crates/node_sdk/tests/wasm_smoke.rs b/crates/node_sdk/tests/wasm_smoke.rs deleted file mode 100644 index 498c8a3..0000000 --- a/crates/node_sdk/tests/wasm_smoke.rs +++ /dev/null @@ -1,34 +0,0 @@ -use source_map_parser_node::{lookup_token, lookup_token_with_context, map_stack_line}; -use wasm_bindgen_test::*; -// Node 环境:无需显式配置宏 (browser 专用) - -fn sample_sm() -> String { - // simple one-line mapping - let sm = r#"{"version":3,"file":"min.js","sources":["a.js"],"sourcesContent":["fn()\n"],"names":[],"mappings":"AAAA"}"#; - sm.to_string() -} - -#[wasm_bindgen_test] -fn lookup_basic() { - let sm = sample_sm(); - let v = lookup_token(&sm, 1, 0); - let s = v.as_string().unwrap(); - assert!(s.contains("\"line\":0")); -} - -#[wasm_bindgen_test] -fn lookup_with_context() { - let sm = sample_sm(); - let v = lookup_token_with_context(&sm, 1, 0, 1); - let s = v.as_string().unwrap(); - assert!(s.contains("source_code")); -} - -#[wasm_bindgen_test] -fn map_stack_line_smoke() { - let sm = sample_sm(); - let line = "at foo (https://example.com/min.js:1:0)"; - let v = map_stack_line(&sm, line); - let s = v.as_string().unwrap(); - assert!(s.contains("line")); -} diff --git a/crates/node_sdk/vite.config.mjs b/crates/node_sdk/vite.config.mjs new file mode 100644 index 0000000..745d09b --- /dev/null +++ b/crates/node_sdk/vite.config.mjs @@ -0,0 +1,33 @@ +import { defineConfig } from 'vite'; +import wasm from 'vite-plugin-wasm'; +import topLevelAwait from 'vite-plugin-top-level-await'; +import { resolve } from 'path'; + +// 构建说明: +// - 入口使用 src/index.ts 封装 +// - 输出 dist/ 下 ESM + CJS +// - wasm 文件作为静态资产保留(由 wasm 绑定中的静态 import 触发复制) +// - 不做压缩,保持可读体积(可按需开启 minify) + +export default defineConfig({ + build: { + sourcemap: true, + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'SourceMapParserNode', + fileName: () => 'index.es.js', + formats: ['es'] + }, + rollupOptions: { + // 目前无需 external;若未来把 wasm 运行时或其他依赖拆分可在此声明 + external: [], + output: { + exports: 'named' + } + }, + outDir: 'dist', + emptyOutDir: true, + target: 'es2022' + }, + plugins: [wasm(), topLevelAwait()], +}); diff --git a/crates/node_sdk/vitest.config.mjs b/crates/node_sdk/vitest.config.mjs new file mode 100644 index 0000000..8b1fe03 --- /dev/null +++ b/crates/node_sdk/vitest.config.mjs @@ -0,0 +1,40 @@ +import wasm from 'vite-plugin-wasm'; +import topLevelAwait from 'vite-plugin-top-level-await'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { defineConfig, mergeConfig } from 'vitest/config'; +import viteConfig from './vite.config.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + environment: 'node', + globals: true, + // 支持 ts/tsx 及 js/jsx 的 spec 或 test 文件 + include: ['./tests/*.test.{ts,tsx,js,mjs,jsx}'], + // 避免扫描构建产物 + exclude: ['node_modules', 'pkg', 'target', 'dist'], + clearMocks: true, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'target/', + '**/vitest.config.{js,ts}', + '**/*.d.ts', + ], + }, + }, + resolve: { + alias: { + // 指向构建后的库模式入口(dist),确保测试覆盖发布产物 + source_map_parser_node: join(__dirname, './dist/index.es.js'), + }, + }, + }) +); diff --git a/scripts/build-wasm-node.sh b/scripts/build-wasm-node.sh index 927e293..ac2d610 100644 --- a/scripts/build-wasm-node.sh +++ b/scripts/build-wasm-node.sh @@ -3,14 +3,17 @@ set -euo pipefail ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) CRATE_DIR="$ROOT_DIR/crates/node_sdk" +OUT_DIR="$CRATE_DIR/pkg" if ! command -v wasm-pack >/dev/null 2>&1; then - echo "Error: wasm-pack not found. Install via: cargo install wasm-pack or official installer" >&2 + echo "Error: wasm-pack not found. Install via: cargo install wasm-pack" >&2 exit 1 fi +rm -rf "$OUT_DIR" pushd "$CRATE_DIR" >/dev/null -wasm-pack build --target nodejs --release "$@" +echo "[build] wasm-pack (bundler target) -> $OUT_DIR" +wasm-pack build --target bundler --release --out-dir "$OUT_DIR" "$@" popd >/dev/null -printf "\nDone. Output at crates/node_sdk/pkg\n" +echo "\nDone. Output at crates/node_sdk/pkg (pure ESM bundler target)"