From a9893ad395405777e869e7727064664e2ca8cad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 21 Oct 2026 09:12:44 +0200 Subject: [PATCH 1/5] fix(app): keep manifest compiler out of CLI release bundles Install manifest extraction tooling in a dedicated frozen file-preview runtime instead of importing its TypeScript compiler into the standalone CLI. Parse app source in a credential-free Bun subprocess, then validate profile scope before starting the lakehouse proxy. Preserve published preview runtime dependencies without adding compiler tooling to generated starter apps. Verify single-file previews through both JavaScript and native CLI artifacts. --- cli/bun.lock | 5 - cli/package.json | 1 - cli/scripts/package-data-app.ts | 2 +- cli/src/commands/app/dev.ts | 8 +- cli/src/commands/app/lib/distribution.test.ts | 13 +- cli/src/commands/app/lib/distribution.ts | 17 +- cli/src/commands/app/lib/file-dev.test.ts | 8 +- cli/src/commands/app/lib/file-dev.ts | 73 +++++--- cli/src/commands/app/lib/payload.ts | 2 +- data-app/README.md | 20 ++- data-app/file-preview/bun.lock | 164 ++++++++++++++++++ data-app/file-preview/bunfig.toml | 2 + data-app/file-preview/extract-manifest.js | 5 + data-app/file-preview/package.json | 16 ++ 14 files changed, 286 insertions(+), 50 deletions(-) create mode 100644 data-app/file-preview/bun.lock create mode 100644 data-app/file-preview/bunfig.toml create mode 100644 data-app/file-preview/extract-manifest.js create mode 100644 data-app/file-preview/package.json diff --git a/cli/bun.lock b/cli/bun.lock index dd605180..018658d7 100644 --- a/cli/bun.lock +++ b/cli/bun.lock @@ -5,7 +5,6 @@ "": { "name": "cli", "dependencies": { - "@altertable/data-app-manifest": "0.73.0", "@clack/prompts": "1.7.0", "oauth4webapi": "3.8.7", "yaml": "2.9.0", @@ -24,8 +23,6 @@ }, }, "packages": { - "@altertable/data-app-manifest": ["@altertable/data-app-manifest@0.73.0", "", { "dependencies": { "typescript": "5.9.3" } }, "sha512-68NC4KuB2YoyCU9+MGdRHpnUJX43D/DJ8UnmBZ7dG5LezgClBf/w7wpRfX9jx/OTwGsa0+bFgGnCLOEA+5cnqA=="], - "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], @@ -378,8 +375,6 @@ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@altertable/data-app-manifest/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], } } diff --git a/cli/package.json b/cli/package.json index a4b4c074..53195087 100644 --- a/cli/package.json +++ b/cli/package.json @@ -49,7 +49,6 @@ "test:watch": "bun test --watch" }, "dependencies": { - "@altertable/data-app-manifest": "0.73.0", "@clack/prompts": "1.7.0", "oauth4webapi": "3.8.7", "yaml": "2.9.0" diff --git a/cli/scripts/package-data-app.ts b/cli/scripts/package-data-app.ts index 95820707..859aac53 100644 --- a/cli/scripts/package-data-app.ts +++ b/cli/scripts/package-data-app.ts @@ -2,7 +2,7 @@ import type { BunPlugin } from 'bun'; import { readDataAppPayload } from '@/commands/app/lib/distribution.ts'; -/** Both npm and native releases embed the same deterministic starter template. */ +/** Both npm and native releases embed the same deterministic project templates. */ export function dataAppPlugin(): BunPlugin { return { name: 'data-app-starter', diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts index e56ad7c9..a8aa4af8 100644 --- a/cli/src/commands/app/dev.ts +++ b/cli/src/commands/app/dev.ts @@ -51,11 +51,9 @@ export const appDevCommand = defineCommand({ : appDirectory(args.dir); requireAppScripts(directory, ['dev']); if (!file) await checkAppScope(directory, execution.profile); - const installExitCode = await runAppCommand( - file ? 'install-frozen' : 'install', - directory - ); - if (installExitCode !== 0) return { exitCode: EXIT_GENERIC }; + if (!file && (await runAppCommand('install', directory)) !== 0) { + return { exitCode: EXIT_GENERIC }; + } const proxy = startAppDevProxy(execution); try { const environment = { diff --git a/cli/src/commands/app/lib/distribution.test.ts b/cli/src/commands/app/lib/distribution.test.ts index c48c8d6e..94c4a6ec 100644 --- a/cli/src/commands/app/lib/distribution.test.ts +++ b/cli/src/commands/app/lib/distribution.test.ts @@ -24,8 +24,19 @@ afterEach(async () => { test('distribution embeds a deterministic starter with an exact package pin and no runtime', async () => { const payload = await readDataAppPayload(); expect(await readDataAppPayload()).toEqual(payload); - expect(Object.keys(payload)).toEqual(['starter']); + expect(Object.keys(payload)).toEqual(['starter', 'filePreview']); expect(Object.keys(payload.starter)).toContain('src/App.tsx'); + const previewPackage = JSON.parse(payload.filePreview['package.json']!); + const starterPackage = JSON.parse(payload.starter['package.json']!); + expect(previewPackage.dependencies).toEqual({ + ...starterPackage.dependencies, + '@altertable/data-app-manifest': recommendedDataAppVersion(), + }); + expect(payload.filePreview['bun.lock']).toContain('typescript'); + expect(payload.filePreview['extract-manifest.js']).toContain( + 'extractDataAppManifest' + ); + for (const path of Object.keys(payload.starter)) { expect(path).not.toMatch( /(^|\/)(tests|fixtures|node_modules|dist|\.altertable)(\/|$)|\.txt$/ diff --git a/cli/src/commands/app/lib/distribution.ts b/cli/src/commands/app/lib/distribution.ts index 7a45faca..8038a562 100644 --- a/cli/src/commands/app/lib/distribution.ts +++ b/cli/src/commands/app/lib/distribution.ts @@ -19,7 +19,10 @@ export const starterFiles = [ export const dataAppDirectory = fileURLToPath( new URL('../../../../../data-app/', import.meta.url) ); -export type DataAppPayload = { starter: Record }; +export type DataAppPayload = { + starter: Record; + filePreview: Record; +}; function assertRelativePath(path: string): void { if ( @@ -60,11 +63,19 @@ async function readFiles( return files; } -/** Release builds embed only the starter; its runtime is installed from npm. */ +/** Embed project files; runtime packages are installed from npm. */ export async function readDataAppPayload( directory = dataAppDirectory ): Promise { - return { starter: await readFiles(join(directory, 'starter'), starterFiles) }; + return { + starter: await readFiles(join(directory, 'starter'), starterFiles), + filePreview: await readFiles(join(directory, 'file-preview'), [ + 'package.json', + 'bun.lock', + 'bunfig.toml', + 'extract-manifest.js', + ]), + }; } export function createAppFiles( diff --git a/cli/src/commands/app/lib/file-dev.test.ts b/cli/src/commands/app/lib/file-dev.test.ts index baa20069..34b7b88f 100644 --- a/cli/src/commands/app/lib/file-dev.test.ts +++ b/cli/src/commands/app/lib/file-dev.test.ts @@ -46,11 +46,11 @@ throw new Error("must not execute");` const manifest = JSON.parse( await readFile(join(directory, 'package.json'), 'utf8') ); - const starter = JSON.parse(dataAppPayload.starter['package.json']!); + const starter = JSON.parse(dataAppPayload.filePreview['package.json']!); expect(manifest.dependencies).toEqual(starter.dependencies); expect(manifest.scripts).toEqual({ dev: 'data-app-preview' }); expect(await readFile(join(directory, 'bun.lock'), 'utf8')).toBe( - dataAppPayload.starter['bun.lock']! + dataAppPayload.filePreview['bun.lock']! ); expect(existsSync(join(directory, 'server.ts'))).toBe(false); expect( @@ -58,7 +58,7 @@ throw new Error("must not execute");` ).toBe(directory); }); -test('rejects invalid declarations and mismatched scopes before creating the runtime', async () => { +test('rejects invalid declarations and mismatched scopes before launching the preview', async () => { await rejects( prepare('export const app = 1;'), /Could not extract the data app manifest\.\n1:/ @@ -70,5 +70,5 @@ test('rejects invalid declarations and mismatched scopes before creating the run ]) { await rejects(prepare(app(scope)), /data app file scope does not match/); } - expect(existsSync(join(workspace.home, 'data-app-dev'))).toBe(false); + expect(existsSync(join(workspace.home, 'data-app-dev'))).toBe(true); }); diff --git a/cli/src/commands/app/lib/file-dev.ts b/cli/src/commands/app/lib/file-dev.ts index 477d21b5..a61e2d55 100644 --- a/cli/src/commands/app/lib/file-dev.ts +++ b/cli/src/commands/app/lib/file-dev.ts @@ -4,49 +4,72 @@ import { join } from 'node:path'; import { recommendedDataAppVersion } from '@/commands/app/lib/package.ts'; import { dataAppPayload } from '@/commands/app/lib/payload.ts'; import { readRequiredFile } from '@/commands/app/lib/remote.ts'; +import { runAppCommand } from '@/commands/app/lib/run.ts'; import { assertAppScope, profileAppScope } from '@/commands/app/lib/scope.ts'; import { configDir } from '@/lib/config-files.ts'; +import { copyProcessEnv } from '@/lib/env.ts'; import { ConfigurationError } from '@/lib/errors.ts'; -/** Prepare a regular dev project backed by the starter's pinned runtime. */ +/** Install the pinned preview tools and verify scope before granting lakehouse access. */ export async function prepareFilePreview( file: string, profile: string ): Promise { const source = await readRequiredFile(file, 'source'); - // Keep the TypeScript compiler off the startup path of other CLI commands. - const { extractDataAppManifest } = - await import('@altertable/data-app-manifest'); - const { manifest, diagnostics } = extractDataAppManifest(source); - if (!manifest) { - const details = diagnostics - .slice(0, 3) - .map(({ message, start }) => `${start.line}:${start.column}: ${message}`) - .join('\n'); - throw new ConfigurationError( - `Could not extract the data app manifest.\n${details}` - ); - } - assertAppScope(manifest.scope, profileAppScope(profile), 'data app file'); - const directory = join( configDir(), 'data-app-dev', recommendedDataAppVersion() ); - const starter = dataAppPayload.starter; - const packageJson = JSON.parse(starter['package.json']!); - packageJson.scripts = { dev: 'data-app-preview' }; await mkdir(directory, { recursive: true }); - const files = { - 'package.json': `${JSON.stringify(packageJson, null, 2)}\n`, - 'bun.lock': starter['bun.lock']!, - 'bunfig.toml': starter['bunfig.toml']!, - }; await Promise.all( - Object.entries(files).map(([name, content]) => + Object.entries(dataAppPayload.filePreview).map(([name, content]) => writeFile(join(directory, name), content) ) ); + if ((await runAppCommand('install-frozen', directory)) !== 0) { + throw new ConfigurationError('Could not install the file preview runtime.'); + } + + // Run the compiler in the installed preview project, outside the CLI bundle. + const env = copyProcessEnv(); + for (const key of Object.keys(env)) { + if (key.startsWith('ALTERTABLE_')) delete env[key]; + } + env.BUN_BE_BUN = '1'; + const child = Bun.spawn( + [process.execPath, join(directory, 'extract-manifest.js')], + { + cwd: directory, + env, + stdin: new Blob([source]), + stdout: 'pipe', + stderr: 'pipe', + } + ); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]); + if (exitCode !== 0) { + throw new ConfigurationError( + `Could not extract the data app manifest.\n${stderr.trim()}` + ); + } + const { manifest, diagnostics } = JSON.parse(stdout) as { + manifest: { scope?: unknown } | null; + diagnostics: { message: string; start: { line: number; column: number } }[]; + }; + if (!manifest) { + const details = diagnostics + .slice(0, 3) + .map(({ message, start }) => `${start.line}:${start.column}: ${message}`) + .join('\n'); + throw new ConfigurationError( + `Could not extract the data app manifest.\n${details}` + ); + } + assertAppScope(manifest.scope, profileAppScope(profile), 'data app file'); return directory; } diff --git a/cli/src/commands/app/lib/payload.ts b/cli/src/commands/app/lib/payload.ts index 93010ac2..bcdb997e 100644 --- a/cli/src/commands/app/lib/payload.ts +++ b/cli/src/commands/app/lib/payload.ts @@ -1,4 +1,4 @@ import { readDataAppPayload } from '@/commands/app/lib/distribution.ts'; -// Source execution reads the starter template. Release builds replace this module with literal data. +// Source execution reads project templates. Release builds replace this module with literal data. export const dataAppPayload = await readDataAppPayload(); diff --git a/data-app/README.md b/data-app/README.md index dc3f1784..01e9d9be 100644 --- a/data-app/README.md +++ b/data-app/README.md @@ -21,7 +21,8 @@ showing Connected. App queries, data context, views, and appearance remain app-o ## Ownership and distribution -The CLI embeds only the starter allowlist in `cli/src/commands/app/lib/distribution.ts`. +The CLI embeds the starter and internal file-preview project allowlists in +`cli/src/commands/app/lib/distribution.ts`. Both npm and native CLI builds can scaffold an app without reading this checkout or accessing the network. Installing the generated app needs registry access or a populated Bun cache. Generated projects commit their source, package manifest, and lockfile; they do not vendor runtime @@ -34,6 +35,7 @@ Each consumer declares only the packages it uses: | `@altertable/data-app` | Starter definitions, contracts, and clients; browser-test server | | `@altertable/data-app-react` | Starter UI and browser-test fixtures | | `@altertable/data-app-bun` | Starter local server and single-file preview | +| `@altertable/data-app-manifest` | Internal single-file preview scope validation | Browser-test fixtures live under `starter/` and resolve React authoring from the starter's own dependencies. Keep the browser-test server's manifest limited to its direct dependencies. @@ -42,9 +44,9 @@ The starter owns React, ReactDOM, and its authoring tools. The package owns its dependencies. The browser entry calls `injectDataAppStyles()` before mounting. The app's `AGENTS.md` points to the installed package guide and docs; dependency guides are not assumed to load automatically. -To adopt a package release, update the exact pins for packages used in `starter/package.json` and -`tests/package.json`, regenerate both lockfiles, and run the checks below. Never resolve `latest` -during app creation. Develop package changes in the package repository; the CLI has no runtime +To adopt a package release, update the exact pins for packages used in `starter/package.json`, +`tests/package.json`, and `file-preview/package.json`, regenerate their lockfiles, and run the checks +below. Never resolve `latest` during app creation. Develop package changes in the package repository; the CLI has no runtime source watcher. ## Upgrades @@ -74,3 +76,13 @@ at phone and desktop widths. CLI tests cover offline scaffolding, npm consumptio peer checks, and failed resolution. Release smoke checks create an app outside the checkout, install its frozen lockfile, and run `app lint` with the packaged CLI. The minimum Bun compatibility job checks scaffolding only; app builds use the repository's current toolchain. + +### Internal single-file preview runtime + +`file-preview/` owns the cached runtime used by `altertable app dev --file`. +Its package manifest, frozen lockfile, Bun configuration, and static manifest +extractor are embedded as project files in the CLI. Published packages, including +manifest tooling and TypeScript, install into the cache instead of the CLI bundle. +Keep its public runtime pins aligned with `starter/`; regenerate its lock with +`bun install --cwd data-app/file-preview` when updating dependencies. +Scope validation runs in this runtime before the CLI grants lakehouse access. diff --git a/data-app/file-preview/bun.lock b/data-app/file-preview/bun.lock new file mode 100644 index 00000000..ebf65152 --- /dev/null +++ b/data-app/file-preview/bun.lock @@ -0,0 +1,164 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "data-app-file-preview", + "dependencies": { + "@altertable/data-app": "0.73.0", + "@altertable/data-app-bun": "0.73.0", + "@altertable/data-app-manifest": "0.73.0", + "@altertable/data-app-react": "0.73.0", + "react": "19.3.0", + "react-dom": "19.3.0", + }, + }, + }, + "packages": { + "@altertable/data-app": ["@altertable/data-app@0.73.0", "", { "dependencies": { "@internationalized/date": "3.12.4" } }, "sha512-JiNaFWAq0xryikh0B6rHQAEEHnxU22Hve+bOTy/6uQNBYEhkuogrXE6VoI4YBuWJdzj9KqQKO+uho+vOU7ZCIQ=="], + + "@altertable/data-app-bun": ["@altertable/data-app-bun@0.73.0", "", { "dependencies": { "@altertable/data-app": "0.73.0", "@altertable/lakehouse": "0.4.1", "get-tsconfig": "4.14.3" }, "bin": { "data-app-preview": "bin/preview.js" } }, "sha512-bhgXGb0AHjSpfOljD+lwiyJzr+dDqmoMhnBc6YDx2BL8hAW7iSm9dpCqpoYsQGjyLW7dISxshUIfPlZ6TRiAaw=="], + + "@altertable/data-app-manifest": ["@altertable/data-app-manifest@0.73.0", "", { "dependencies": { "typescript": "5.9.3" } }, "sha512-68NC4KuB2YoyCU9+MGdRHpnUJX43D/DJ8UnmBZ7dG5LezgClBf/w7wpRfX9jx/OTwGsa0+bFgGnCLOEA+5cnqA=="], + + "@altertable/data-app-react": ["@altertable/data-app-react@0.73.0", "", { "dependencies": { "@altertable/data-app": "0.73.0", "@floating-ui/react": "0.27.20", "@internationalized/date": "3.12.4", "@tanstack/react-query": "5.104.3", "fflate": "0.8.3", "fuzzysort": "4.0.2", "html-to-image": "1.11.13", "lucide-react": "1.59.0", "react-aria-components": "1.22.1", "recharts": "3.10.1" }, "peerDependencies": { "react": ">=19.2.0", "react-dom": ">=19.2.0" } }, "sha512-EJiGeX5YSnR6+qZLR6d6WQBOCx/IIvQj1xck+s6b1A45kdLK2GozBcfexQveuBq1RGG568oIdEvY2Zzel3UXTA=="], + + "@altertable/lakehouse": ["@altertable/lakehouse@0.4.1", "", {}, "sha512-m7d15cWt+WUGW7lVw2KYGD+Se8MzO25G7SWSuFd+OasBxWmOJNa2IrgvOHGVEbnZ+kexTUy9NjAGQ69sG4V91w=="], + + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], + + "@floating-ui/react": ["@floating-ui/react@0.27.20", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-CMqMy7OaXl9W0eq1Uy7L7i2Y/anPvHmFmESd2CEw0t5YvZhcVCeo4MBevAmswRllX7Y2dEidA4ozGPunLSTQpw=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + + "@internationalized/date": ["@internationalized/date@3.12.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ=="], + + "@internationalized/number": ["@internationalized/number@3.6.9", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-GgWy2caO/+hV9sT/oCTWGHvbjmJMPT9kkXzoKRLnkEq7adghhR7UprhvY9sXrKfrp3NcDfhF6QQHXR1KfQbB8g=="], + + "@internationalized/string": ["@internationalized/string@3.2.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA=="], + + "@react-types/shared": ["@react-types/shared@3.37.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-VNlZuifu4tHdxWPfHvoMfKsHA9kVTCGkduv5vKVTdinm8wHFJNhyVB5RzIMiRFj0q3RKoCbP68u2zaoBSiFueg=="], + + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.13.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-f6jwVRfwW30+tnllz4YcsHHpPYYOE7abcHkyUCovD+P7DyhdAI5prz2Fh/8TLhakjpN+7aIEion1s5L/tvlYlA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.104.3", "", {}, "sha512-DZY9TuJAOgU3UyHUhXb4CcTYEHNZszlwkPg4VViIwTxOKzLprPUU+rbaPZQd3srqkGOtpaDdKpW972UV/LKlcg=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.104.3", "", { "dependencies": { "@tanstack/query-core": "5.104.3" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-5Ts1ImwJKqoJgPkLxl9WrPQy1JEoEmti3MaaODCfBamHUGFk57IFrc+AIksPq+BAm10EsroX56qm398Z42LG7w=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.2.0", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "es-toolkit": ["es-toolkit@1.52.0", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "fuzzysort": ["fuzzysort@4.0.2", "", {}, "sha512-3c7yD6rK1EXIsqpcu1ZVzXJugODWNMzvUij6yTiziaAHrpNS8kzFOI9mJLE8pmG4K9JOaVgzQRgklfOEk8f4Tg=="], + + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], + + "html-to-image": ["html-to-image@1.11.13", "", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="], + + "immer": ["immer@11.2.6", "", {}, "sha512-AI5pkEyzJZLsu91LASM15eZ0L9CK9SJZ+yI/MwO3Gsv9ajEwTwILrzxnxRqquLVKG7oCalEgXpAPsJfyPuWk/w=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "lucide-react": ["lucide-react@1.59.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw=="], + + "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], + + "react-aria": ["react-aria@3.53.1", "", { "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/number": "^3.6.9", "@internationalized/string": "^3.2.10", "@react-types/shared": "^3.37.0", "@swc/helpers": "^0.5.0", "aria-hidden": "^1.2.3", "clsx": "^2.0.0", "react-stately": "3.51.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-dikisyOjJd3D2fFrIDWmVWzqiLz5B86GzZv1zxs7xI4dVc0JIz/1Wa52I20gYvVL2WXeFXlygYsle/QcOMhtcg=="], + + "react-aria-components": ["react-aria-components@1.22.1", "", { "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/string": "^3.2.10", "@react-types/shared": "^3.37.0", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "3.53.1", "react-stately": "3.51.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-UTHaYHOaM71bOCjGPF50DcLywlrwAodE63IP64LB4kO+OizatQEL8TesVEOM8+TkBeSgHoflJ45388IlJfa7aw=="], + + "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="], + + "react-is": ["react-is@19.3.1", "", {}, "sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q=="], + + "react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], + + "react-stately": ["react-stately@3.51.0", "", { "dependencies": { "@internationalized/date": "^3.12.4", "@internationalized/number": "^3.6.9", "@internationalized/string": "^3.2.10", "@react-types/shared": "^3.37.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-YJcmkAXu2XOQP97hO/qz/cC3qv9kni3oVhwBlxWr358BHePJUpY15XwL3YSRMKxYt4EkslPqAz11JRirMfANGw=="], + + "recharts": ["recharts@3.10.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], + + "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], + + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + } +} diff --git a/data-app/file-preview/bunfig.toml b/data-app/file-preview/bunfig.toml new file mode 100644 index 00000000..b6874be1 --- /dev/null +++ b/data-app/file-preview/bunfig.toml @@ -0,0 +1,2 @@ +[install] +exact = true diff --git a/data-app/file-preview/extract-manifest.js b/data-app/file-preview/extract-manifest.js new file mode 100644 index 00000000..ffa27603 --- /dev/null +++ b/data-app/file-preview/extract-manifest.js @@ -0,0 +1,5 @@ +import { extractDataAppManifest } from "@altertable/data-app-manifest"; + +// Parse source without evaluating the app or resolving its imports. +const source = await new Response(Bun.stdin.stream()).text(); +console.log(JSON.stringify(extractDataAppManifest(source))); diff --git a/data-app/file-preview/package.json b/data-app/file-preview/package.json new file mode 100644 index 00000000..c2527c03 --- /dev/null +++ b/data-app/file-preview/package.json @@ -0,0 +1,16 @@ +{ + "name": "data-app-file-preview", + "private": true, + "type": "module", + "scripts": { + "dev": "data-app-preview" + }, + "dependencies": { + "@altertable/data-app": "0.73.0", + "@altertable/data-app-bun": "0.73.0", + "@altertable/data-app-react": "0.73.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "@altertable/data-app-manifest": "0.73.0" + } +} From 06b12b599dcd54184ac373120d5470b983bfa93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 21 Oct 2026 09:12:44 +0200 Subject: [PATCH 2/5] build(cli): enforce a 1 MiB bundle budget in CI and releases Check the uncompressed standalone JavaScript bundle in the shared builder used by CI, npm packaging, and every native release target. Report actual size and fail before compilation or publication when the budget is exceeded. The fixed bundle is 891,100 bytes; the shipped v1.13.0 bundle at 9,680,174 bytes fails this guard. Cover missing artifacts and budget boundaries, and document how to investigate future growth. --- DEVELOPMENT.md | 5 +++++ cli/scripts/check-bundle-size.test.ts | 26 ++++++++++++++++++++++++++ cli/scripts/check-bundle-size.ts | 17 +++++++++++++++++ cli/scripts/npm-bundle.ts | 6 ++---- 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 cli/scripts/check-bundle-size.test.ts create mode 100644 cli/scripts/check-bundle-size.ts diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e0151090..978c5ad8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -89,6 +89,11 @@ git submodule update --init --recursive ## Build and package +Every JavaScript build enforces a **1 MiB uncompressed size budget** in +`cli/scripts/check-bundle-size.ts`, including CI package checks and release builds. +The bundle also feeds native compilation, so this catches dependency bloat before +it reaches either distribution. Review bundled dependencies before raising the limit. + The JS bundle is the npm `bin` entry and a GitHub Release asset: ```bash diff --git a/cli/scripts/check-bundle-size.test.ts b/cli/scripts/check-bundle-size.test.ts new file mode 100644 index 00000000..5c08dc09 --- /dev/null +++ b/cli/scripts/check-bundle-size.test.ts @@ -0,0 +1,26 @@ +import { test } from 'bun:test'; +import { rejects } from 'node:assert/strict'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { checkBundleSize } from '@/../scripts/check-bundle-size.ts'; + +test('enforces the shipped bundle budget, including missing and empty artifacts', async () => { + const directory = await mkdtemp(join(tmpdir(), 'cli-bundle-budget-')); + const path = join(directory, 'cli.js'); + try { + await rejects(checkBundleSize(path), /Missing or empty/); + await Bun.write(path, ''); + await rejects(checkBundleSize(path), /Missing or empty/); + await Bun.write(path, new Uint8Array(1024 * 1024)); + await checkBundleSize(path); + await Bun.write(path, new Uint8Array(1024 * 1024 + 1)); + await rejects( + checkBundleSize(path), + /1,048,577 bytes \/ 1,048,576 byte budget/ + ); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/cli/scripts/check-bundle-size.ts b/cli/scripts/check-bundle-size.ts new file mode 100644 index 00000000..bb66bc6c --- /dev/null +++ b/cli/scripts/check-bundle-size.ts @@ -0,0 +1,17 @@ +// The standalone JavaScript bundle also feeds every native release build. +// Keep compiler and app runtime dependencies in installed projects instead. +const MAX_BUNDLE_BYTES = 1024 * 1024; + +export async function checkBundleSize(path: string): Promise { + const file = Bun.file(path); + if (!(await file.exists()) || file.size === 0) { + throw new Error(`Missing or empty CLI bundle: ${path}.`); + } + const message = `CLI bundle: ${file.size.toLocaleString('en-US')} bytes / ${MAX_BUNDLE_BYTES.toLocaleString('en-US')} byte budget`; + if (file.size > MAX_BUNDLE_BYTES) { + throw new Error( + `${message}. Inspect bundled dependencies before changing the budget in scripts/check-bundle-size.ts.` + ); + } + console.log(message); +} diff --git a/cli/scripts/npm-bundle.ts b/cli/scripts/npm-bundle.ts index ac91a570..3a2ac045 100644 --- a/cli/scripts/npm-bundle.ts +++ b/cli/scripts/npm-bundle.ts @@ -1,6 +1,7 @@ import { mkdir } from 'node:fs/promises'; import { basename, dirname, join } from 'node:path'; +import { checkBundleSize } from '@/../scripts/check-bundle-size.ts'; import { dataAppPlugin } from '@/../scripts/package-data-app.ts'; export const NPM_BUNDLE_OPTIONS = { @@ -29,10 +30,7 @@ export async function buildNpmBundle( `npm bundle failed:\n${result.logs.map(String).join('\n')}` ); } - const output = Bun.file(outputPath); - if (!(await output.exists()) || output.size === 0) { - throw new Error(`npm bundle is missing or empty: ${outputPath}.`); - } + await checkBundleSize(outputPath); return outputPath; } From 50ad563eaf881808829b62d19cf02c4ab81881ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 21 Oct 2026 09:15:48 +0200 Subject: [PATCH 3/5] build(cli): tighten bundle budget to 900 KiB Leave 30,500 bytes of headroom above the current bundle and trim redundant documentation while preserving dependency upgrade guidance. Update and verify the exact budget boundaries. --- DEVELOPMENT.md | 5 +---- cli/scripts/check-bundle-size.test.ts | 6 +++--- cli/scripts/check-bundle-size.ts | 2 +- data-app/README.md | 15 ++------------- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 978c5ad8..55cc2002 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -89,10 +89,7 @@ git submodule update --init --recursive ## Build and package -Every JavaScript build enforces a **1 MiB uncompressed size budget** in -`cli/scripts/check-bundle-size.ts`, including CI package checks and release builds. -The bundle also feeds native compilation, so this catches dependency bloat before -it reaches either distribution. Review bundled dependencies before raising the limit. +Builds enforce a 900 KiB uncompressed bundle budget in `cli/scripts/check-bundle-size.ts`. The JS bundle is the npm `bin` entry and a GitHub Release asset: diff --git a/cli/scripts/check-bundle-size.test.ts b/cli/scripts/check-bundle-size.test.ts index 5c08dc09..67878f68 100644 --- a/cli/scripts/check-bundle-size.test.ts +++ b/cli/scripts/check-bundle-size.test.ts @@ -13,12 +13,12 @@ test('enforces the shipped bundle budget, including missing and empty artifacts' await rejects(checkBundleSize(path), /Missing or empty/); await Bun.write(path, ''); await rejects(checkBundleSize(path), /Missing or empty/); - await Bun.write(path, new Uint8Array(1024 * 1024)); + await Bun.write(path, new Uint8Array(900 * 1024)); await checkBundleSize(path); - await Bun.write(path, new Uint8Array(1024 * 1024 + 1)); + await Bun.write(path, new Uint8Array(900 * 1024 + 1)); await rejects( checkBundleSize(path), - /1,048,577 bytes \/ 1,048,576 byte budget/ + /921,601 bytes \/ 921,600 byte budget/ ); } finally { await rm(directory, { recursive: true, force: true }); diff --git a/cli/scripts/check-bundle-size.ts b/cli/scripts/check-bundle-size.ts index bb66bc6c..1016b5bd 100644 --- a/cli/scripts/check-bundle-size.ts +++ b/cli/scripts/check-bundle-size.ts @@ -1,6 +1,6 @@ // The standalone JavaScript bundle also feeds every native release build. // Keep compiler and app runtime dependencies in installed projects instead. -const MAX_BUNDLE_BYTES = 1024 * 1024; +const MAX_BUNDLE_BYTES = 900 * 1024; export async function checkBundleSize(path: string): Promise { const file = Bun.file(path); diff --git a/data-app/README.md b/data-app/README.md index 01e9d9be..2f07b1f0 100644 --- a/data-app/README.md +++ b/data-app/README.md @@ -35,7 +35,6 @@ Each consumer declares only the packages it uses: | `@altertable/data-app` | Starter definitions, contracts, and clients; browser-test server | | `@altertable/data-app-react` | Starter UI and browser-test fixtures | | `@altertable/data-app-bun` | Starter local server and single-file preview | -| `@altertable/data-app-manifest` | Internal single-file preview scope validation | Browser-test fixtures live under `starter/` and resolve React authoring from the starter's own dependencies. Keep the browser-test server's manifest limited to its direct dependencies. @@ -46,8 +45,8 @@ points to the installed package guide and docs; dependency guides are not assume To adopt a package release, update the exact pins for packages used in `starter/package.json`, `tests/package.json`, and `file-preview/package.json`, regenerate their lockfiles, and run the checks -below. Never resolve `latest` during app creation. Develop package changes in the package repository; the CLI has no runtime -source watcher. +below. Never resolve `latest` during app creation. Develop package changes in the package +repository; the CLI has no runtime source watcher. ## Upgrades @@ -76,13 +75,3 @@ at phone and desktop widths. CLI tests cover offline scaffolding, npm consumptio peer checks, and failed resolution. Release smoke checks create an app outside the checkout, install its frozen lockfile, and run `app lint` with the packaged CLI. The minimum Bun compatibility job checks scaffolding only; app builds use the repository's current toolchain. - -### Internal single-file preview runtime - -`file-preview/` owns the cached runtime used by `altertable app dev --file`. -Its package manifest, frozen lockfile, Bun configuration, and static manifest -extractor are embedded as project files in the CLI. Published packages, including -manifest tooling and TypeScript, install into the cache instead of the CLI bundle. -Keep its public runtime pins aligned with `starter/`; regenerate its lock with -`bun install --cwd data-app/file-preview` when updating dependencies. -Scope validation runs in this runtime before the CLI grants lakehouse access. From 447b8fbc07ead64574e78a23b65d825c4ae2e2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 21 Oct 2026 09:17:08 +0200 Subject: [PATCH 4/5] build(cli): restore the 1 MiB bundle budget --- DEVELOPMENT.md | 2 +- cli/scripts/check-bundle-size.test.ts | 6 +++--- cli/scripts/check-bundle-size.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 55cc2002..2ee8a2cc 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -89,7 +89,7 @@ git submodule update --init --recursive ## Build and package -Builds enforce a 900 KiB uncompressed bundle budget in `cli/scripts/check-bundle-size.ts`. +Builds enforce a 1 MiB uncompressed bundle budget in `cli/scripts/check-bundle-size.ts`. The JS bundle is the npm `bin` entry and a GitHub Release asset: diff --git a/cli/scripts/check-bundle-size.test.ts b/cli/scripts/check-bundle-size.test.ts index 67878f68..5c08dc09 100644 --- a/cli/scripts/check-bundle-size.test.ts +++ b/cli/scripts/check-bundle-size.test.ts @@ -13,12 +13,12 @@ test('enforces the shipped bundle budget, including missing and empty artifacts' await rejects(checkBundleSize(path), /Missing or empty/); await Bun.write(path, ''); await rejects(checkBundleSize(path), /Missing or empty/); - await Bun.write(path, new Uint8Array(900 * 1024)); + await Bun.write(path, new Uint8Array(1024 * 1024)); await checkBundleSize(path); - await Bun.write(path, new Uint8Array(900 * 1024 + 1)); + await Bun.write(path, new Uint8Array(1024 * 1024 + 1)); await rejects( checkBundleSize(path), - /921,601 bytes \/ 921,600 byte budget/ + /1,048,577 bytes \/ 1,048,576 byte budget/ ); } finally { await rm(directory, { recursive: true, force: true }); diff --git a/cli/scripts/check-bundle-size.ts b/cli/scripts/check-bundle-size.ts index 1016b5bd..bb66bc6c 100644 --- a/cli/scripts/check-bundle-size.ts +++ b/cli/scripts/check-bundle-size.ts @@ -1,6 +1,6 @@ // The standalone JavaScript bundle also feeds every native release build. // Keep compiler and app runtime dependencies in installed projects instead. -const MAX_BUNDLE_BYTES = 900 * 1024; +const MAX_BUNDLE_BYTES = 1024 * 1024; export async function checkBundleSize(path: string): Promise { const file = Bun.file(path); From 31f755b28014bf9e4c967267c89b9a8c2b63c2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 21 Oct 2026 09:21:24 +0200 Subject: [PATCH 5/5] docs(changelog): note smaller CLI release downloads --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0dbd1c..e1d1f48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Keep the TypeScript manifest compiler out of CLI downloads by installing it with the cached single-file preview runtime. + - Complete stdin-based credential configuration before exiting on Windows. - Keep profiles and credentials in the user's home directory when `HOME` is unset, and reject relative config and shell-completion directory settings.