From 2cde72d3402fa60b686cc4a39cfc188be36a170b Mon Sep 17 00:00:00 2001 From: James Irwin Date: Mon, 2 Mar 2026 14:39:37 +0100 Subject: [PATCH 1/4] Fix browser build --- packages/sdk/README.md | 20 +++++++++++++++++++- packages/sdk/package.json | 5 +++-- packages/sdk/plugins/wasmExtractPlugin.ts | 16 ++++++++++------ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 194d34f9..dd8ac205 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -782,6 +782,24 @@ For browser use without a bundler: ## CommonJS (CJS) Usage +### Next.js, webpack, Turbopack (browser build) + +When bundling for the browser, these tools use the `browser` export, which is a build that avoids top-level await and `import.meta.url`. **You must call `await init()` before using any API**: + +```typescript +import { init, transpile, format, Dialect } from '@polyglot-sql/sdk'; + +async function Example() { + await init(); // Required for browser/bundler builds + const result = transpile('SELECT IFNULL(a, b)', Dialect.MySQL, Dialect.PostgreSQL); + console.log(result.sql?.[0]); +} +``` + +If you see `Dialect` or other exports as `undefined`, or errors about `require("fs")`, ensure you're awaiting `init()` before use. + +### Node.js (CJS) + For Node.js projects using `require()`, the SDK ships a CJS build. Since WASM cannot be loaded synchronously, you must call `init()` before using any other function: ```javascript @@ -816,7 +834,7 @@ await init(); console.log(isInitialized()); // true ``` -> **Note:** The ESM build (`import`) auto-initializes via top-level `await`, so `init()` is not required there. The CJS build requires it because `require()` is synchronous. +> **Note:** The ESM build for Node (`import`) auto-initializes via top-level `await`, so `init()` is not required there. The CJS and browser builds (Next.js, webpack, etc.) require `await init()` before use. ## License diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3e4e518f..6f8d2a65 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -14,7 +14,7 @@ "import": "./dist/index.d.ts", "require": "./dist/index.d.cts" }, - "browser": "./dist/index.js", + "browser": "./dist/index.browser.js", "import": "./dist/index.js", "require": "./dist/index.cjs" }, @@ -31,11 +31,12 @@ "build:bindings": "cd ../.. && make generate-bindings", "build:wasm": "cd ../../crates/polyglot-sql-wasm && wasm-pack build --target bundler --release --out-dir ../../packages/sdk/wasm", "build:esm": "vite build", + "build:browser": "vite build --config vite.config.browser.ts", "build:cjs": "vite build --config vite.config.cjs.ts && cp dist/index.d.ts dist/index.d.cts", "build:umd": "vite build --config vite.config.umd.ts", "build:dialect": "vite build --config vite.config.dialect.ts", "build:dialects": "for d in postgresql mysql bigquery snowflake duckdb tsql clickhouse; do POLYGLOT_DIALECT=$d pnpm run build:dialect; done", - "build": "pnpm run build:esm && pnpm run build:cjs && pnpm run build:umd", + "build": "pnpm run build:esm && pnpm run build:cjs && pnpm run build:browser && pnpm run build:umd", "build:all": "pnpm run build && pnpm run build:dialects", "test": "vitest run", "test:watch": "vitest", diff --git a/packages/sdk/plugins/wasmExtractPlugin.ts b/packages/sdk/plugins/wasmExtractPlugin.ts index ef35d08b..d66f49b4 100644 --- a/packages/sdk/plugins/wasmExtractPlugin.ts +++ b/packages/sdk/plugins/wasmExtractPlugin.ts @@ -5,6 +5,8 @@ type WasmExtractPluginOptions = { wasmRelativePath: string; extractWasm: boolean; injectNodeCompat?: boolean; + /** When true, keep WASM as base64 data URL in bundle (no import.meta.url). Use for browser builds. */ + keepDataUrl?: boolean; }; const NODE_FILE_FETCH_COMPAT = [ @@ -23,7 +25,7 @@ const NODE_FILE_FETCH_COMPAT = [ const DATA_URL_REGEX = /(=\s*)(['"])data:application\/wasm;base64,([A-Za-z0-9+/=]+)\2/; export function wasmExtractPlugin(options: WasmExtractPluginOptions): Plugin { - const { wasmFilename, wasmRelativePath, extractWasm } = options; + const { wasmFilename, wasmRelativePath, extractWasm, keepDataUrl } = options; const injectNodeCompat = options.injectNodeCompat ?? true; let wroteWasm = false; @@ -41,7 +43,7 @@ export function wasmExtractPlugin(options: WasmExtractPluginOptions): Plugin { continue; } - if (extractWasm && !wroteWasm) { + if (extractWasm && !keepDataUrl && !wroteWasm) { const wasmBytes = Buffer.from(match[3], 'base64'); this.emitFile({ type: 'asset', @@ -51,10 +53,12 @@ export function wasmExtractPlugin(options: WasmExtractPluginOptions): Plugin { wroteWasm = true; } - item.code = item.code.replace( - DATA_URL_REGEX, - `$1new URL("${wasmRelativePath}",import.meta.url).href` - ); + if (!keepDataUrl) { + item.code = item.code.replace( + DATA_URL_REGEX, + `$1new URL("${wasmRelativePath}",import.meta.url).href` + ); + } if (injectNodeCompat && !item.code.includes('globalThis.process.versions?.node')) { item.code = `${NODE_FILE_FETCH_COMPAT}\n${item.code}`; From 2a9e9c8aa292f65743c4258690a4d36d1c90f050 Mon Sep 17 00:00:00 2001 From: James Irwin Date: Mon, 2 Mar 2026 14:41:04 +0100 Subject: [PATCH 2/4] add missing files --- packages/sdk/plugins/wasmBrowserPlugin.ts | 91 +++++++++++++++++++++++ packages/sdk/vite.config.browser.ts | 47 ++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 packages/sdk/plugins/wasmBrowserPlugin.ts create mode 100644 packages/sdk/vite.config.browser.ts diff --git a/packages/sdk/plugins/wasmBrowserPlugin.ts b/packages/sdk/plugins/wasmBrowserPlugin.ts new file mode 100644 index 00000000..000cdc33 --- /dev/null +++ b/packages/sdk/plugins/wasmBrowserPlugin.ts @@ -0,0 +1,91 @@ +import type { Plugin } from 'vite'; + +/** + * Vite plugin that transforms the ESM bundle for browser/bundler compatibility. + * Removes top-level await and uses lazy init so that: + * - Bundlers (Next.js, webpack, Turbopack) don't get async module interop issues + * - Dialect, format, transpile, etc. are available synchronously after init() + * + * Used with keepDataUrl: true in wasmExtractPlugin so WASM is inlined as base64, + * avoiding import.meta.url which can break with webpack. + */ +export function wasmBrowserPlugin(): Plugin { + return { + name: 'polyglot-wasm-browser', + apply: 'build', + generateBundle(_, bundle) { + for (const item of Object.values(bundle)) { + if (item.type !== 'chunk') continue; + + let code = item.code; + + // ── Transform 1: Remove NODE_FILE_FETCH_COMPAT shim ────────── + // Browser build doesn't need file:// URL support + code = code.replace( + /^\(\(\)=>\{if\(typeof globalThis\.process<"u".*?\}\}\)\(\);\n?/, + '', + ); + + // __vite__wasmUrl is either a data URL (with keepDataUrl) or new URL(...) + // We keep it as-is; no fs/path needed + + // ── Transform 2: Defer WASM initialization ─────────────────── + const initWasmCallRe = + /const __vite__wasmModule = await __vite__initWasm\((\{[\s\S]*?\})\s*,\s*__vite__wasmUrl\);/; + const initMatch = code.match(initWasmCallRe); + if (initMatch) { + const importsObj = initMatch[1]; + const replacement = [ + 'let __vite__wasmModule;', + `const __vite__wasmImports = ${importsObj};`, + 'let __initPromise;', + 'async function __polyglot_init_wasm() {', + ' if (__vite__wasmModule) return;', + ' if (__initPromise) return __initPromise;', + ' __initPromise = (async () => {', + ' const result = await __vite__initWasm(__vite__wasmImports, __vite__wasmUrl);', + ' __vite__wasmModule = result;', + ' __wbg_set_wasm(__vite__wasmModule);', + ' })();', + ' return __initPromise;', + '}', + ].join('\n'); + code = code.replace(initWasmCallRe, replacement); + } + + // ── Transform 3: Defer WASM export bindings ───────────────── + const wasmBindingRe = /^const ([\w$]+) = __vite__wasmModule\.([\w$]+);$/gm; + const bindingAssignments: string[] = []; + code = code.replace(wasmBindingRe, (_match, varName, propName) => { + bindingAssignments.push(` ${varName} = __vite__wasmModule.${propName};`); + return `let ${varName};`; + }); + if (bindingAssignments.length > 0) { + code = code.replace( + ' __vite__wasmModule = result;', + ' __vite__wasmModule = result;\n' + bindingAssignments.join('\n'), + ); + } + + // ── Transform 4: Remove top-level __wbg_set_wasm(wasm$2) ──── + code = code.replace(/\n__wbg_set_wasm\(wasm\$2\);\n/, '\n'); + + // ── Transform 5: Replace init() and isInitialized() ───────── + code = code.replace( + /async function init\(\)\s*\{[\s\S]*?return Promise\.resolve\(\);\s*\}/, + 'async function init() {\n await __polyglot_init_wasm();\n}', + ); + code = code.replace( + /function isInitialized\(\)\s*\{[\s\S]*?return true;\s*\}/, + 'function isInitialized() {\n return !!__vite__wasmModule;\n}', + ); + + // ── Transform 6: Remove __vite__initWasm from top-level (keep the function) + // Actually we need to keep __vite__initWasm - we call it from __polyglot_init_wasm + // So we don't remove it. Done. + + item.code = code; + } + }, + }; +} diff --git a/packages/sdk/vite.config.browser.ts b/packages/sdk/vite.config.browser.ts new file mode 100644 index 00000000..2d552045 --- /dev/null +++ b/packages/sdk/vite.config.browser.ts @@ -0,0 +1,47 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; +import wasm from 'vite-plugin-wasm'; +import { wasmExtractPlugin } from './plugins/wasmExtractPlugin'; +import { wasmBrowserPlugin } from './plugins/wasmBrowserPlugin'; + +/** + * Browser-specific ESM build. + * + * - No top-level await (lazy init via init()) + * - WASM inlined as base64 (no import.meta.url) + * - Compatible with Next.js, webpack, Turbopack + * + * Consumers must call await init() before using format, transpile, etc. + */ +export default defineConfig({ + plugins: [ + wasm(), + wasmExtractPlugin({ + wasmFilename: 'polyglot_sql_wasm_bg.wasm', + wasmRelativePath: './polyglot_sql_wasm_bg.wasm', + extractWasm: false, + injectNodeCompat: false, + keepDataUrl: true, + }), + wasmBrowserPlugin(), + ], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'PolyglotSQL', + formats: ['es'], + fileName: () => 'index.browser.js', + }, + rollupOptions: { + output: { exports: 'named' }, + }, + target: 'esnext', + sourcemap: false, + minify: false, + emptyOutDir: false, + }, + assetsInclude: ['**/*.wasm'], + optimizeDeps: { + exclude: ['./wasm/polyglot_sql_wasm.js'], + }, +}); From fefaf7c8081690167bffb458f1c63d9a6d416a14 Mon Sep 17 00:00:00 2001 From: James Irwin Date: Tue, 3 Mar 2026 16:30:55 +0100 Subject: [PATCH 3/4] test playground --- packages/playground-next/.gitignore | 17 + packages/playground-next/README.md | 25 + .../playground-next/components/FormatDemo.tsx | 104 ++++ packages/playground-next/next-env.d.ts | 6 + packages/playground-next/next.config.js | 15 + packages/playground-next/package.json | 23 + packages/playground-next/pages/_app.tsx | 6 + packages/playground-next/pages/index.tsx | 11 + packages/playground-next/styles/globals.css | 23 + packages/playground-next/tsconfig.json | 40 ++ packages/playground-shared/package.json | 29 + packages/playground-shared/src/index.ts | 1 + packages/playground-shared/src/sql.ts | 46 ++ packages/playground-shared/tsconfig.json | 17 + pnpm-lock.yaml | 554 ++++++++++++++++++ 15 files changed, 917 insertions(+) create mode 100644 packages/playground-next/.gitignore create mode 100644 packages/playground-next/README.md create mode 100644 packages/playground-next/components/FormatDemo.tsx create mode 100644 packages/playground-next/next-env.d.ts create mode 100644 packages/playground-next/next.config.js create mode 100644 packages/playground-next/package.json create mode 100644 packages/playground-next/pages/_app.tsx create mode 100644 packages/playground-next/pages/index.tsx create mode 100644 packages/playground-next/styles/globals.css create mode 100644 packages/playground-next/tsconfig.json create mode 100644 packages/playground-shared/package.json create mode 100644 packages/playground-shared/src/index.ts create mode 100644 packages/playground-shared/src/sql.ts create mode 100644 packages/playground-shared/tsconfig.json diff --git a/packages/playground-next/.gitignore b/packages/playground-next/.gitignore new file mode 100644 index 00000000..997409cb --- /dev/null +++ b/packages/playground-next/.gitignore @@ -0,0 +1,17 @@ +# Next.js +.next/ +out/ + +# Build +build/ +dist/ + +# Dependencies +node_modules/ + +# Debug +npm-debug.log* +.pnpm-debug.log* + +# Local env +.env*.local diff --git a/packages/playground-next/README.md b/packages/playground-next/README.md new file mode 100644 index 00000000..e3ad4eef --- /dev/null +++ b/packages/playground-next/README.md @@ -0,0 +1,25 @@ +# Polyglot SQL Playground (Next.js) + +A Next.js + Turbopack playground that mirrors GrowthBook's structure. Demonstrates `@polyglot-sql/sdk` via `playground-shared` (top-level import, like GrowthBook's shared package). + +## Setup + +From the polyglot repo root: + +```bash +pnpm install +pnpm --filter @polyglot-sql/playground-next run dev +``` + +Open http://localhost:3000. + +## Structure (mirrors GrowthBook) + +- **FormatDemo** – Loaded with `ssr: false`; imports from `playground-shared/sql` (top-level SDK import like GrowthBook) +- **playground-shared** – Wraps `@polyglot-sql/sdk`; `formatWithPolyglot` throws if format is undefined (top-level await issue) + +## Configuration + +- Next.js 16 with Turbopack +- Pages Router +- `transpilePackages: ["@polyglot-sql/sdk", "playground-shared"]` diff --git a/packages/playground-next/components/FormatDemo.tsx b/packages/playground-next/components/FormatDemo.tsx new file mode 100644 index 00000000..967e289d --- /dev/null +++ b/packages/playground-next/components/FormatDemo.tsx @@ -0,0 +1,104 @@ +import { + initPolyglotFormat, + formatWithPolyglot, +} from "playground-shared/sql"; +import { useState, useEffect, useCallback } from "react"; + +const DEFAULT_SQL = `select u.id,u.name,u.email,count(o.id) as order_count from users u left join orders o on u.id=o.user_id where u.active=true group by u.id,u.name,u.email limit 50;`; + +export default function FormatDemo() { + const [sql, setSql] = useState(DEFAULT_SQL); + const [output, setOutput] = useState(""); + const [initDone, setInitDone] = useState(false); + + useEffect(() => { + initPolyglotFormat().then(() => setInitDone(true)); + }, []); + + const handleFormat = useCallback(() => { + setOutput(""); + try { + const result = formatWithPolyglot(sql, "postgresql"); + setOutput(result ?? ""); + } catch (e) { + setOutput(String(e)); + } + }, [sql]); + + return ( +
+

Polyglot SQL Playground

+ +
+