diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 022070b..a5093e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,13 +40,8 @@ jobs: - name: Install wasm-pack uses: jetli/wasm-pack-action@v0.4.0 - - name: Build WASM (Node.js and Web) - run: | - wasm-pack build --target nodejs --out-dir ts/wasm - wasm-pack build --target web --out-dir ts/wasm-web - cp ts/wasm-web/sqlparser_rs_wasm.js ts/wasm/sqlparser_rs_wasm_web.js - cp ts/wasm-web/sqlparser_rs_wasm_bg.wasm ts/wasm/sqlparser_rs_wasm_web_bg.wasm - rm -rf ts/wasm-web + - name: Build WASM + run: wasm-pack build --target web --out-dir ts/wasm - name: Build TypeScript working-directory: ts diff --git a/.gitignore b/.gitignore index 4097c72..f195dc0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ node_modules/ # WASM build output /pkg /ts/wasm -/ts/wasm-web # TypeScript build output /ts/dist diff --git a/scripts/build.sh b/scripts/build.sh index 04722b5..90c637c 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,7 +6,6 @@ echo "==========================================" echo "Building sqlparser-ts npm package" echo "==========================================" -# Get the script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" @@ -14,7 +13,6 @@ echo "" echo "Project directory: $PROJECT_DIR" echo "" -# Check for wasm-pack if ! command -v wasm-pack &> /dev/null; then echo "Error: wasm-pack is not installed." echo "Install it with: cargo install wasm-pack" @@ -22,19 +20,9 @@ if ! command -v wasm-pack &> /dev/null; then exit 1 fi -# Build WASM for Node.js -echo "Step 1a: Building WASM module for Node.js..." +echo "Step 1: Building WASM module..." cd "$PROJECT_DIR" -wasm-pack build --target nodejs --out-dir ts/wasm - -# Build WASM for Web (browser) -echo "Step 1b: Building WASM module for Web (browser)..." -wasm-pack build --target web --out-dir ts/wasm-web - -# Copy web wasm files to wasm directory with _web suffix -cp ts/wasm-web/sqlparser_rs_wasm.js ts/wasm/sqlparser_rs_wasm_web.js -cp ts/wasm-web/sqlparser_rs_wasm_bg.wasm ts/wasm/sqlparser_rs_wasm_web_bg.wasm -rm -rf ts/wasm-web +wasm-pack build --target web --out-dir ts/wasm echo "" echo "Step 2: Installing npm dependencies..." @@ -51,10 +39,10 @@ echo "Build complete!" echo "==========================================" echo "" echo "Output files:" -echo " - WASM: ts/wasm/" -echo " - ESM: ts/dist/esm/" -echo " - CJS: ts/dist/cts/" -echo " - Types: ts/dist/types/" +echo " - WASM: ts/wasm/" +echo " - ESM: ts/dist/index.mjs" +echo " - CJS: ts/dist/index.cjs" +echo " - Types: ts/dist/index.d.{mts,cts}" echo "" echo "To run tests: cd ts && npm test" echo "To publish: cd ts && npm publish" diff --git a/ts/README.md b/ts/README.md index 6ad1ce1..b019771 100644 --- a/ts/README.md +++ b/ts/README.md @@ -25,7 +25,10 @@ npm install @guanmingchiu/sqlparser-ts ## Usage ```typescript -import { parse, format, validate } from '@guanmingchiu/sqlparser-ts'; +import { init, parse, format, validate } from '@guanmingchiu/sqlparser-ts'; + +// Initialize WASM module (must be called once before using any parser functions) +await init(); // Parse SQL into AST const ast = parse('SELECT * FROM users'); @@ -41,6 +44,19 @@ const sql = format('select * from users'); validate('SELECT * FROM users'); // ok ``` +### Vite Configuration + +WASM packages must be excluded from Vite's dev server [dependency pre-bundling](https://github.com/vitejs/vite/discussions/9256). This only affects the dev server. Production builds use Rollup instead of esbuild and handle WASM files correctly. + +```typescript +// vite.config.ts +export default defineConfig({ + optimizeDeps: { + exclude: ['@guanmingchiu/sqlparser-ts'], + }, +}); +``` + ### Working with AST ```typescript diff --git a/ts/package.json b/ts/package.json index fcfea47..578f027 100644 --- a/ts/package.json +++ b/ts/package.json @@ -20,8 +20,7 @@ ], "scripts": { "build": "npm run build:wasm && npm run build:ts", - "build:wasm": "cd .. && wasm-pack build --target nodejs --out-dir ts/wasm && rm -f ts/wasm/.gitignore", - "build:wasm:web": "cd .. && wasm-pack build --target web --out-dir ts/wasm-web", + "build:wasm": "cd .. && wasm-pack build --target web --out-dir ts/wasm && rm -f ts/wasm/.gitignore", "build:ts": "tsdown", "test": "vitest run", "test:watch": "vitest", diff --git a/ts/src/index.ts b/ts/src/index.ts index aecfa58..33e4a7c 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -4,7 +4,7 @@ */ // Parser -export { Parser, initWasm, ready, parse, validate, format } from './parser.js'; +export { Parser, init, parse, validate, format } from './parser.js'; export type { ParserOptions, DialectInput } from './parser.js'; // Dialects diff --git a/ts/src/parser.ts b/ts/src/parser.ts index f449a29..20e8da7 100644 --- a/ts/src/parser.ts +++ b/ts/src/parser.ts @@ -4,7 +4,7 @@ import { ParserError } from './types/errors.js'; import type { Statement } from './types/ast.js'; import { getWasmModule } from './wasm.js'; -export { initWasm, ready } from './wasm.js'; +export { init } from './wasm.js'; /** Dialect can be specified as a Dialect instance or a string name */ export type DialectInput = Dialect | DialectName; diff --git a/ts/src/wasm.ts b/ts/src/wasm.ts index b77bd8a..d124a56 100644 --- a/ts/src/wasm.ts +++ b/ts/src/wasm.ts @@ -13,76 +13,51 @@ export interface WasmModule { let wasmModule: WasmModule | null = null; let initPromise: Promise | null = null; -let initStarted = false; const isBrowser = typeof window !== 'undefined' && typeof process === 'undefined'; -function startInit(): void { - if (initStarted) return; - initStarted = true; - initPromise = initWasm().catch(() => { - // Silently ignore init errors - they'll be thrown when APIs are used - }); -} - -// Start init on module load -startInit(); - /** Get initialized WASM module or throw */ export function getWasmModule(): WasmModule { if (wasmModule) { return wasmModule; } throw new WasmInitError( - 'WASM module not yet initialized. Use the async import or wait for module to load: import("@guanmingchiu/sqlparser-ts").then(({ parse }) => parse(sql))' + 'WASM module not yet initialized. Call `await init()` before using the parser.' ); } /** - * Wait for WASM module to be ready - */ -export async function ready(): Promise { - startInit(); - await initPromise; -} - -/** - * Initialize the WASM module explicitly. - * Usually not needed - the module auto-initializes on first use. + * Initialize the WASM module. Must be called before using any parser functions. + * Safe to call multiple times, subsequent calls are no-ops. */ -export async function initWasm(): Promise { +export async function init(): Promise { if (wasmModule) { return; } - if (isBrowser) { - try { - const wasmJsUrl = new URL('../wasm/sqlparser_rs_wasm_web.js', import.meta.url); - const wasmBinaryUrl = new URL('../wasm/sqlparser_rs_wasm_web_bg.wasm', import.meta.url); - - const wasm = await import(/* @vite-ignore */ wasmJsUrl.href); - - if (typeof wasm.default === 'function') { - await wasm.default({ module_or_path: wasmBinaryUrl }); + if (!initPromise) { + initPromise = (async () => { + try { + const wasm = await import(/* @vite-ignore */ '../wasm/sqlparser_rs_wasm.js'); + + if (isBrowser) { + const wasmUrl = new URL('../wasm/sqlparser_rs_wasm_bg.wasm', import.meta.url); + await wasm.default({ module_or_path: wasmUrl }); + } else { + const { readFile } = await import(/* @vite-ignore */ 'node:fs/promises'); + const wasmPath = new URL('../wasm/sqlparser_rs_wasm_bg.wasm', import.meta.url); + const bytes = await readFile(wasmPath); + await wasm.default({ module_or_path: bytes }); + } + + wasmModule = wasm as WasmModule; + } catch (error) { + throw new WasmInitError( + `Failed to load WASM module: ${error instanceof Error ? error.message : String(error)}` + ); } - - wasmModule = wasm as WasmModule; - } catch (error) { - throw new WasmInitError( - `Failed to load WASM module in browser: ${error instanceof Error ? error.message : String(error)}` - ); - } - return; + })(); } - // Node.js - try { - const wasmUrl = new URL('../wasm/sqlparser_rs_wasm.js', import.meta.url); - const wasm = await import(/* @vite-ignore */ wasmUrl.href); - wasmModule = wasm as WasmModule; - } catch (error) { - throw new WasmInitError( - `Failed to load WASM module: ${error instanceof Error ? error.message : String(error)}` - ); - } + await initPromise; } diff --git a/ts/tests/builds.test.ts b/ts/tests/builds.test.ts index c69d12f..6063552 100644 --- a/ts/tests/builds.test.ts +++ b/ts/tests/builds.test.ts @@ -60,22 +60,22 @@ describe('CJS build', () => { }); test('parse works', async () => { - const { parse, ready } = require('../dist/index.cjs'); - await ready(); + const { parse, init } = require('../dist/index.cjs'); + await init(); const result = parse('SELECT 1'); expect(Array.isArray(result)).toBe(true); }); test('format works', async () => { - const { format, ready } = require('../dist/index.cjs'); - await ready(); + const { format, init } = require('../dist/index.cjs'); + await init(); const result = format('select 1'); expect(result).toBe('SELECT 1'); }); test('dialect string works', async () => { - const { parse, ready } = require('../dist/index.cjs'); - await ready(); + const { parse, init } = require('../dist/index.cjs'); + await init(); const result = parse('SELECT $1', 'postgresql'); expect(Array.isArray(result)).toBe(true); }); diff --git a/ts/tests/setup.ts b/ts/tests/setup.ts index a07c3a1..719d779 100644 --- a/ts/tests/setup.ts +++ b/ts/tests/setup.ts @@ -1,4 +1,4 @@ -import { initWasm } from '../src'; +import { init } from '../src'; // Initialize WASM before running tests -await initWasm(); +await init();