From 4501aff1371c0e4cc30b1f33b9c991962f30c39f Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:20:48 +1000 Subject: [PATCH 1/2] fix(ai-code-mode-skills): make root export Worker/browser-safe (#486) The root entry re-exported `createFileSkillStorage` via `export * from './storage'`, eagerly pulling in `node:fs`/`node:path`. This broke Cloudflare Workers and browser bundlers even for consumers that only used non-storage helpers like `createSkillManagementTools`. The Node-only file storage now lives only behind the `@tanstack/ai-code-mode-skills/storage` subpath. The root re-exports only the browser-safe `createMemorySkillStorage`. Docs, README, JSDoc, and a changeset are updated, plus a regression test that walks the root module graph (static + dynamic imports) and asserts it reaches zero `node:` builtins and that the public export surface is correct. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../code-mode-skills-worker-safe-root.md | 11 ++ docs/code-mode/code-mode-with-skills.md | 13 +- docs/config.json | 3 +- packages/ai-code-mode-skills/README.md | 20 ++- .../src/code-mode-with-skills.ts | 3 + packages/ai-code-mode-skills/src/index.ts | 8 +- .../tests/root-export-worker-safe.test.ts | 115 ++++++++++++++++++ 7 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 .changeset/code-mode-skills-worker-safe-root.md create mode 100644 packages/ai-code-mode-skills/tests/root-export-worker-safe.test.ts diff --git a/.changeset/code-mode-skills-worker-safe-root.md b/.changeset/code-mode-skills-worker-safe-root.md new file mode 100644 index 000000000..652b6baca --- /dev/null +++ b/.changeset/code-mode-skills-worker-safe-root.md @@ -0,0 +1,11 @@ +--- +'@tanstack/ai-code-mode-skills': minor +--- + +Make the `@tanstack/ai-code-mode-skills` root export Worker/browser-safe. + +The root entry previously re-exported `createFileSkillStorage` (via `export * from './storage'`), which eagerly pulled in `node:fs` / `node:path`. This broke Cloudflare Workers and browser bundlers even for consumers that only used non-storage helpers like `createSkillManagementTools` or `createSkillsSystemPrompt`. + +The Node-only file storage now lives **only** behind the `@tanstack/ai-code-mode-skills/storage` subpath. The root entry still re-exports the browser-safe `createMemorySkillStorage`. + +**Breaking:** import `createFileSkillStorage` from `@tanstack/ai-code-mode-skills/storage` instead of the package root. diff --git a/docs/code-mode/code-mode-with-skills.md b/docs/code-mode/code-mode-with-skills.md index a77a0b736..8b867ad10 100644 --- a/docs/code-mode/code-mode-with-skills.md +++ b/docs/code-mode/code-mode-with-skills.md @@ -205,6 +205,11 @@ Skills are persisted through the `SkillStorage` interface. Two implementations a ### File storage (production) +`createFileSkillStorage` is Node-only — it imports `node:fs` / `node:path` — so +it lives behind the `/storage` subpath rather than the package root. This keeps +the root export safe to bundle for Cloudflare Workers and browser builds; only +reach for the subpath in a Node runtime. + ```typescript import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' @@ -227,15 +232,17 @@ Creates a directory structure: code.ts ``` -### Memory storage (testing) +### Memory storage (testing & edge runtimes) ```typescript -import { createMemorySkillStorage } from '@tanstack/ai-code-mode-skills/storage' +import { createMemorySkillStorage } from '@tanstack/ai-code-mode-skills' const storage = createMemorySkillStorage() ``` -Keeps everything in memory. Useful for tests and demos. +Keeps everything in memory — no `node:fs` dependency, so it is re-exported from +the package root and is safe to use in Workers and browsers. Useful for tests, +demos, and edge deployments. (It is also available from the `/storage` subpath.) ### Storage interface diff --git a/docs/config.json b/docs/config.json index e3fc3b712..ebb92ae66 100644 --- a/docs/config.json +++ b/docs/config.json @@ -207,7 +207,8 @@ { "label": "Code Mode with Skills", "to": "code-mode/code-mode-with-skills", - "addedAt": "2026-04-15" + "addedAt": "2026-04-15", + "updatedAt": "2026-06-10" }, { "label": "Code Mode Isolate Drivers", diff --git a/packages/ai-code-mode-skills/README.md b/packages/ai-code-mode-skills/README.md index 24b10ac76..cee4fb068 100644 --- a/packages/ai-code-mode-skills/README.md +++ b/packages/ai-code-mode-skills/README.md @@ -17,9 +17,11 @@ pnpm add @tanstack/ai-code-mode-skills ```typescript import { codeModeWithSkills, - createFileSkillStorage, createAlwaysTrustedStrategy, } from '@tanstack/ai-code-mode-skills' +// Node-only file storage lives behind the `/storage` subpath so the root +// export stays safe for Worker/browser bundlers. +import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node' // Create skill storage @@ -37,7 +39,7 @@ const codeModeConfig = { } // Build a dynamic registry and system prompt with skills -const { registry, systemPrompt, selectedSkills } = await codeModeWithSkills({ +const { toolsRegistry, systemPrompt, selectedSkills } = await codeModeWithSkills({ config: codeModeConfig, adapter: anthropic('claude-3-haiku'), // Cheap model for skill selection skills: { @@ -50,7 +52,7 @@ const { registry, systemPrompt, selectedSkills } = await codeModeWithSkills({ // Use in chat const stream = chat({ adapter: anthropic('claude-sonnet-4-20250514'), // Main model - toolRegistry: registry, + toolRegistry: toolsRegistry, messages, systemPrompts: [basePrompt, systemPrompt], }) @@ -151,11 +153,17 @@ Creates Code Mode tools and system prompt with skills integration. ### Storage -Storage is available from both the root export and the explicit storage subpath: +The worker/browser-safe in-memory storage (`createMemorySkillStorage`) is +re-exported from the root entry. The Node-only file storage +(`createFileSkillStorage`) imports `node:fs` / `node:path`, so it is only +available from the `/storage` subpath — keeping the root export safe to import +from Cloudflare Workers and browser bundlers: ```typescript -import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills' -// or +// Worker/browser-safe — root export +import { createMemorySkillStorage } from '@tanstack/ai-code-mode-skills' + +// Node-only — `/storage` subpath import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' ``` diff --git a/packages/ai-code-mode-skills/src/code-mode-with-skills.ts b/packages/ai-code-mode-skills/src/code-mode-with-skills.ts index 7592498cf..55c7a676f 100644 --- a/packages/ai-code-mode-skills/src/code-mode-with-skills.ts +++ b/packages/ai-code-mode-skills/src/code-mode-with-skills.ts @@ -30,6 +30,9 @@ export type { CodeModeWithSkillsOptions, CodeModeWithSkillsResult } * * @example * ```typescript + * // Node-only file storage lives behind the `/storage` subpath: + * import { createFileSkillStorage } from '@tanstack/ai-code-mode-skills/storage' + * * const { toolsRegistry, systemPrompt, selectedSkills } = await codeModeWithSkills({ * config: { * driver: createNodeIsolateDriver(), diff --git a/packages/ai-code-mode-skills/src/index.ts b/packages/ai-code-mode-skills/src/index.ts index 47ec280e3..36c92f789 100644 --- a/packages/ai-code-mode-skills/src/index.ts +++ b/packages/ai-code-mode-skills/src/index.ts @@ -37,7 +37,13 @@ export { createSkillsSystemPrompt } from './create-skills-system-prompt' export { generateSkillTypes } from './generate-skill-types' // Storage implementations -export * from './storage' +// +// Only the worker/browser-safe in-memory storage is re-exported from the root +// entry. The Node-only file storage (`createFileSkillStorage`) imports +// `node:fs` / `node:path`, so it lives behind the `@tanstack/ai-code-mode-skills/storage` +// subpath to keep this root export safe for Cloudflare Workers and browser bundlers. +export { createMemorySkillStorage } from './storage/memory-storage' +export type { MemorySkillStorageOptions } from './storage/memory-storage' // All types export type { diff --git a/packages/ai-code-mode-skills/tests/root-export-worker-safe.test.ts b/packages/ai-code-mode-skills/tests/root-export-worker-safe.test.ts new file mode 100644 index 000000000..f04bd8bac --- /dev/null +++ b/packages/ai-code-mode-skills/tests/root-export-worker-safe.test.ts @@ -0,0 +1,115 @@ +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import * as rootEntry from '../src/index' + +const SRC_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../src') + +/** Resolve a `.`-relative import specifier to its on-disk source file. */ +async function resolveRelative( + entry: string, + spec: string, +): Promise { + const base = resolve(dirname(entry), spec) + // Try the literal path first (covers `./foo.js` NodeNext/ESM-style specifiers + // whose source is `foo.ts`), then the usual TS source extensions and barrels. + const candidates = [ + base, + base.replace(/\.[cm]?js$/, '.ts'), + base.replace(/\.[cm]?js$/, '.tsx'), + `${base}.ts`, + `${base}.tsx`, + resolve(base, 'index.ts'), + resolve(base, 'index.tsx'), + ] + for (const candidate of candidates) { + try { + await readFile(candidate, 'utf8') + return candidate + } catch (err) { + // Only "file not found" means "try the next candidate"; anything else + // (permissions, decode failure) is a real problem and must surface. + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err + } + } + return undefined +} + +/** + * Walk the transitive module graph of an entry file (following only relative + * imports — package imports are treated as leaves) and collect every + * `node:`-prefixed builtin it statically references. Both static `from '…'` + * forms and dynamic `import('…')` calls are followed. + * + * This guards the regression behind issue #486: the root entry must stay safe + * to bundle for Cloudflare Workers / browsers, so it must not statically reach + * the Node-only file storage that imports `node:fs` / `node:path`. + */ +async function collectNodeBuiltins( + entry: string, + seen = new Set(), + builtins = new Set(), +): Promise> { + if (seen.has(entry)) return builtins + seen.add(entry) + + const source = await readFile(entry, 'utf8') + // Static `import/export … from '…'` plus dynamic `import('…')`. + const specRe = + /(?:(?:import|export)[\s\S]*?from\s*['"]([^'"]+)['"])|(?:import\s*\(\s*['"]([^'"]+)['"]\s*\))/g + + for (const match of source.matchAll(specRe)) { + const spec = match[1] ?? match[2] + if (!spec) continue + + if (spec.startsWith('node:')) { + builtins.add(spec) + continue + } + + if (spec.startsWith('.')) { + const resolved = await resolveRelative(entry, spec) + if (!resolved) { + // A real relative import the walker can't follow would silently drop a + // subtree (and any node: import in it), turning this guard into a false + // pass. Fail loudly instead. + throw new Error( + `Could not resolve relative import "${spec}" from "${entry}". ` + + `The module-graph walk would silently skip this subtree and the ` + + `#486 guard would be unsound.`, + ) + } + await collectNodeBuiltins(resolved, seen, builtins) + } + } + + return builtins +} + +describe('root export worker/browser safety (#486)', () => { + it('does not statically import any node: builtin from the root entry', async () => { + const builtins = await collectNodeBuiltins(resolve(SRC_DIR, 'index.ts')) + expect([...builtins]).toEqual([]) + }) + + it('keeps node: builtins reachable through the /storage subpath', async () => { + const builtins = await collectNodeBuiltins( + resolve(SRC_DIR, 'storage/index.ts'), + ) + expect([...builtins].sort()).toEqual([ + 'node:fs', + 'node:fs/promises', + 'node:path', + ]) + }) + + it('re-exports the browser-safe memory storage but not the Node-only file storage from root', () => { + // The public contract issue #486 is about: the root entry must expose the + // in-memory storage and must NOT expose the Node-only file storage. + expect(typeof rootEntry.createMemorySkillStorage).toBe('function') + expect( + (rootEntry as Record).createFileSkillStorage, + ).toBeUndefined() + }) +}) From 60b45d2850c2f78c0805daea5b054ed940b67c9c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 04:22:11 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- packages/ai-code-mode-skills/README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/ai-code-mode-skills/README.md b/packages/ai-code-mode-skills/README.md index cee4fb068..ab301c923 100644 --- a/packages/ai-code-mode-skills/README.md +++ b/packages/ai-code-mode-skills/README.md @@ -39,15 +39,16 @@ const codeModeConfig = { } // Build a dynamic registry and system prompt with skills -const { toolsRegistry, systemPrompt, selectedSkills } = await codeModeWithSkills({ - config: codeModeConfig, - adapter: anthropic('claude-3-haiku'), // Cheap model for skill selection - skills: { - storage: skillStorage, - maxSkillsInContext: 5, - }, - messages, -}) +const { toolsRegistry, systemPrompt, selectedSkills } = + await codeModeWithSkills({ + config: codeModeConfig, + adapter: anthropic('claude-3-haiku'), // Cheap model for skill selection + skills: { + storage: skillStorage, + maxSkillsInContext: 5, + }, + messages, + }) // Use in chat const stream = chat({