From 47e671aa739ca65f0ce2fb009696fafaf2501c5e Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Sat, 2 May 2026 22:31:58 -0700 Subject: [PATCH 1/5] refactor workspace pattern parsing --- packages/intent/package.json | 3 +- packages/intent/src/workspace-patterns.ts | 267 +++++++++--------- .../intent/tests/workspace-patterns.test.ts | 100 +++++++ pnpm-lock.yaml | 96 ++++++- 4 files changed, 315 insertions(+), 151 deletions(-) diff --git a/packages/intent/package.json b/packages/intent/package.json index 78696a3..216da35 100644 --- a/packages/intent/package.json +++ b/packages/intent/package.json @@ -32,7 +32,8 @@ ], "dependencies": { "cac": "^6.7.14", - "yaml": "^2.7.0" + "jsonc-parser": "^3.3.1", + "yaml": "2.8.3" }, "devDependencies": { "@verdaccio/node-api": "6.0.0-6-next.76", diff --git a/packages/intent/src/workspace-patterns.ts b/packages/intent/src/workspace-patterns.ts index 04997f8..15d6890 100644 --- a/packages/intent/src/workspace-patterns.ts +++ b/packages/intent/src/workspace-patterns.ts @@ -1,5 +1,6 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs' import { dirname, join } from 'node:path' +import { parse as parseJsonc, type ParseError } from 'jsonc-parser' import { parse as parseYaml } from 'yaml' import { findSkillFiles } from './utils.js' @@ -13,174 +14,160 @@ function normalizeWorkspacePatterns(patterns: Array): Array { ].sort((a, b) => a.localeCompare(b)) } -function parseWorkspacePatterns(value: unknown): Array | null { +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function parseWorkspacePatternList( + value: unknown, + fieldName: string, +): Array | null { + if (value === undefined || value === null) { + return null + } + if (!Array.isArray(value)) { + throw new TypeError(`${fieldName} must be an array of strings`) + } + + if (value.some((pattern) => typeof pattern !== 'string')) { + throw new TypeError(`${fieldName} must be an array of strings`) + } + + return normalizeWorkspacePatterns(value) +} + +function parseWorkspacePatternField( + value: unknown, + fieldName: string, + nestedKey: string, +): Array | null { + if (value === undefined || value === null) { return null } - return normalizeWorkspacePatterns( - value.filter((pattern): pattern is string => typeof pattern === 'string'), + if (Array.isArray(value)) { + return parseWorkspacePatternList(value, fieldName) + } + + if (isRecord(value)) { + return parseWorkspacePatternList( + value[nestedKey], + `${fieldName}.${nestedKey}`, + ) + } + + throw new TypeError( + `${fieldName} must be an array of strings or an object with ${nestedKey}`, ) } -function hasPackageJson(dir: string): boolean { - return existsSync(join(dir, 'package.json')) +function hasWorkspaceManifest(dir: string): boolean { + return ( + existsSync(join(dir, 'package.json')) || + existsSync(join(dir, 'deno.json')) || + existsSync(join(dir, 'deno.jsonc')) + ) } -function stripJsonCommentsAndTrailingCommas(source: string): string { - let result = '' - let inString = false - let escaped = false - - for (let index = 0; index < source.length; index += 1) { - const char = source[index]! - const next = source[index + 1] - - if (inString) { - result += char - if (escaped) { - escaped = false - } else if (char === '\\') { - escaped = true - } else if (char === '"') { - inString = false - } - continue - } +function readYamlFile(path: string): unknown { + return parseYaml(readFileSync(path, 'utf8')) +} - if (char === '"') { - inString = true - result += char - continue - } +function readJsonFile(path: string): unknown { + return JSON.parse(readFileSync(path, 'utf8')) +} - if (char === '/' && next === '/') { - while (index < source.length && source[index] !== '\n') { - index += 1 - } - if (index < source.length) { - result += source[index]! - } - continue - } +function readJsoncFile(path: string): unknown { + const errors: Array = [] + const value = parseJsonc(readFileSync(path, 'utf8'), errors, { + allowTrailingComma: true, + }) - if (char === '/' && next === '*') { - const commentStart = index - index += 2 - while ( - index < source.length && - !(source[index] === '*' && source[index + 1] === '/') - ) { - index += 1 - } - if (index >= source.length) { - throw new SyntaxError( - `Unterminated block comment starting at position ${commentStart}`, + if (errors.length > 0) { + throw new SyntaxError( + errors + .map( + (error) => + `JSONC parse error ${error.error} at offset ${error.offset}`, ) - } - index += 1 - continue - } - - if (char === ',') { - let lookahead = index + 1 - while (lookahead < source.length) { - const la = source[lookahead]! - if (/\s/.test(la)) { - lookahead += 1 - } else if (la === '/' && source[lookahead + 1] === '/') { - lookahead += 2 - while (lookahead < source.length && source[lookahead] !== '\n') { - lookahead += 1 - } - } else if (la === '/' && source[lookahead + 1] === '*') { - lookahead += 2 - while ( - lookahead < source.length && - !(source[lookahead] === '*' && source[lookahead + 1] === '/') - ) { - lookahead += 1 - } - lookahead += 2 - } else { - break - } - } - if (source[lookahead] === '}' || source[lookahead] === ']') { - continue - } - } - - result += char + .join('; '), + ) } - return result + return value } -function readJsonFile(path: string, jsonc = false): unknown { - const source = readFileSync(path, 'utf8') - return JSON.parse(jsonc ? stripJsonCommentsAndTrailingCommas(source) : source) +function warnConfigError(path: string, err: unknown): void { + const verb = err instanceof SyntaxError ? 'parse' : 'read' + console.error( + `Warning: failed to ${verb} ${path}: ${err instanceof Error ? err.message : err}`, + ) } -export function readWorkspacePatterns(root: string): Array | null { - const pnpmWs = join(root, 'pnpm-workspace.yaml') - if (existsSync(pnpmWs)) { - try { - const config = parseYaml(readFileSync(pnpmWs, 'utf8')) as Record< - string, - unknown - > - const patterns = parseWorkspacePatterns(config.packages) - if (patterns) { - return patterns - } - } catch (err: unknown) { - const verb = err instanceof SyntaxError ? 'parse' : 'read' - console.error( - `Warning: failed to ${verb} ${pnpmWs}: ${err instanceof Error ? err.message : err}`, - ) - } - } +type WorkspacePatternSource = { + fileName: string + read: (path: string) => unknown + getPatterns: (config: unknown) => Array | null +} - const pkgPath = join(root, 'package.json') - if (existsSync(pkgPath)) { - try { - const pkg = readJsonFile(pkgPath) as Record - const workspaces = pkg.workspaces as Record | undefined - const patterns = - parseWorkspacePatterns(workspaces) ?? - parseWorkspacePatterns(workspaces?.packages) - if (patterns) { - return patterns - } - } catch (err: unknown) { - const verb = err instanceof SyntaxError ? 'parse' : 'read' - console.error( - `Warning: failed to ${verb} ${pkgPath}: ${err instanceof Error ? err.message : err}`, - ) - } - } +const workspacePatternSources: Array = [ + { + fileName: 'pnpm-workspace.yaml', + read: readYamlFile, + getPatterns: (config) => + parseWorkspacePatternList( + isRecord(config) ? config.packages : undefined, + 'pnpm-workspace.yaml#packages', + ), + }, + { + fileName: 'package.json', + read: readJsonFile, + getPatterns: (config) => + parseWorkspacePatternField( + isRecord(config) ? config.workspaces : undefined, + 'package.json#workspaces', + 'packages', + ), + }, + { + fileName: 'deno.json', + read: readJsoncFile, + getPatterns: (config) => + parseWorkspacePatternField( + isRecord(config) ? config.workspace : undefined, + 'deno.json#workspace', + 'members', + ), + }, + { + fileName: 'deno.jsonc', + read: readJsoncFile, + getPatterns: (config) => + parseWorkspacePatternField( + isRecord(config) ? config.workspace : undefined, + 'deno.jsonc#workspace', + 'members', + ), + }, +] - for (const denoConfigName of ['deno.json', 'deno.jsonc']) { - const denoConfigPath = join(root, denoConfigName) - if (!existsSync(denoConfigPath)) { +export function readWorkspacePatterns(root: string): Array | null { + for (const source of workspacePatternSources) { + const path = join(root, source.fileName) + + if (!existsSync(path)) { continue } try { - const denoConfig = readJsonFile(denoConfigPath, true) as Record< - string, - unknown - > - const patterns = parseWorkspacePatterns(denoConfig.workspace) + const patterns = source.getPatterns(source.read(path)) if (patterns) { return patterns } } catch (err: unknown) { - const verb = err instanceof SyntaxError ? 'parse' : 'read' - console.error( - `Warning: failed to ${verb} ${denoConfigPath}: ${err instanceof Error ? err.message : err}`, - ) + warnConfigError(path, err) } } @@ -219,7 +206,7 @@ function resolveWorkspacePatternSegments( result: Set, ): void { if (segments.length === 0) { - if (hasPackageJson(dir)) { + if (hasWorkspaceManifest(dir)) { result.add(dir) } return diff --git a/packages/intent/tests/workspace-patterns.test.ts b/packages/intent/tests/workspace-patterns.test.ts index aa31f42..d3858d7 100644 --- a/packages/intent/tests/workspace-patterns.test.ts +++ b/packages/intent/tests/workspace-patterns.test.ts @@ -36,6 +36,15 @@ function writePackage(root: string, ...parts: Array): void { ) } +function writeDenoPackage(root: string, ...parts: Array): void { + const dir = join(root, ...parts) + mkdirSync(dir, { recursive: true }) + writeFileSync( + join(dir, 'deno.json'), + JSON.stringify({ name: parts.join('/') }), + ) +} + function writeDir(root: string, ...parts: Array): void { mkdirSync(join(root, ...parts), { recursive: true }) } @@ -80,6 +89,32 @@ describe('readWorkspacePatterns', () => { ]) }) + it('reads workspace patterns from package.json object-form workspaces', () => { + const root = createRoot() + + writeFileSync( + join(root, 'package.json'), + JSON.stringify({ + workspaces: { + packages: ['./packages/*/', 'apps\\*'], + }, + }), + ) + + expect(readWorkspacePatterns(root)).toEqual(['apps/*', 'packages/*']) + }) + + it('reads workspace patterns from pnpm-workspace.yaml', () => { + const root = createRoot() + + writeFileSync( + join(root, 'pnpm-workspace.yaml'), + ['packages:', ' - ./packages/*/', ' - apps\\*'].join('\n'), + ) + + expect(readWorkspacePatterns(root)).toEqual(['apps/*', 'packages/*']) + }) + it('reads workspace patterns from deno.json', () => { const root = createRoot() @@ -111,6 +146,21 @@ describe('readWorkspacePatterns', () => { expect(readWorkspacePatterns(root)).toEqual(['apps/*', 'packages/*']) }) + it('reads workspace patterns from Deno object-form workspace members', () => { + const root = createRoot() + + writeFileSync( + join(root, 'deno.json'), + JSON.stringify({ + workspace: { + members: ['./packages/*/', 'apps\\*'], + }, + }), + ) + + expect(readWorkspacePatterns(root)).toEqual(['apps/*', 'packages/*']) + }) + it('prefers package.json workspaces over Deno workspace config', () => { const root = createRoot() @@ -126,6 +176,43 @@ describe('readWorkspacePatterns', () => { expect(readWorkspacePatterns(root)).toEqual(['packages/*']) }) + it('falls back when a higher-priority config has no workspace field', () => { + const root = createRoot() + + writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'root' })) + writeFileSync( + join(root, 'deno.json'), + JSON.stringify({ workspace: ['packages/*'] }), + ) + + expect(readWorkspacePatterns(root)).toEqual(['packages/*']) + }) + + it('warns and falls back when package.json workspaces contains non-string entries', () => { + const root = createRoot() + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) + + writeFileSync( + join(root, 'package.json'), + JSON.stringify({ workspaces: [123] }), + ) + writeFileSync( + join(root, 'deno.json'), + JSON.stringify({ workspace: ['packages/*'] }), + ) + + expect(readWorkspacePatterns(root)).toEqual(['packages/*']) + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining( + `Warning: failed to read ${join(root, 'package.json')}`, + ), + ) + + consoleErrorSpy.mockRestore() + }) + it('warns and returns null for invalid Deno config', () => { const root = createRoot() const consoleErrorSpy = vi @@ -232,6 +319,19 @@ describe('resolveWorkspacePackages', () => { resolveWorkspacePackages(root, ['packages/*', '!packages/excluded']), ).toEqual([join(root, 'packages', 'alpha')]) }) + + it('resolves Deno-only workspace members', () => { + const root = createRoot() + + writeDenoPackage(root, 'packages', 'deno-lib') + writePackage(root, 'packages', 'node-lib') + writeDir(root, 'packages', 'not-a-package') + + expect(resolveWorkspacePackages(root, ['packages/*'])).toEqual([ + join(root, 'packages', 'deno-lib'), + join(root, 'packages', 'node-lib'), + ]) + }) }) describe('workspace helpers', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81eb432..06c3db1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,22 +73,25 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^5.0.1 - version: 5.2.0(tinybench@2.9.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2))(vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2)) + version: 5.2.0(tinybench@2.9.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))(vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) typescript: specifier: 5.9.3 version: 5.9.3 vitest: specifier: ^4.0.17 - version: 4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2) + version: 4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) packages/intent: dependencies: cac: specifier: ^6.7.14 version: 6.7.14 + jsonc-parser: + specifier: ^3.3.1 + version: 3.3.1 yaml: - specifier: ^2.7.0 - version: 2.8.2 + specifier: 2.8.3 + version: 2.8.3 devDependencies: '@verdaccio/node-api': specifier: 6.0.0-6-next.76 @@ -2690,6 +2693,9 @@ packages: jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -4190,6 +4196,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4401,13 +4412,13 @@ snapshots: transitivePeerDependencies: - debug - '@codspeed/vitest-plugin@5.2.0(tinybench@2.9.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2))(vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2))': + '@codspeed/vitest-plugin@5.2.0(tinybench@2.9.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))(vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@codspeed/core': 5.2.0 tinybench: 2.9.0 - vitest: 4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2) + vitest: 4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) transitivePeerDependencies: - debug @@ -5682,6 +5693,14 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2) + '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.0.17 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + '@vitest/pretty-format@4.0.17': dependencies: tinyrainbow: 3.0.3 @@ -7081,6 +7100,8 @@ snapshots: jsonc-parser@3.2.0: {} + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -7464,7 +7485,7 @@ snapshots: tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tslib: 2.8.1 - yaml: 2.8.2 + yaml: 2.8.3 yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: @@ -8387,7 +8408,7 @@ snapshots: typedoc-plugin-frontmatter@1.3.0(typedoc-plugin-markdown@4.9.0(typedoc@0.28.14(typescript@5.9.3))): dependencies: typedoc-plugin-markdown: 4.9.0(typedoc@0.28.14(typescript@5.9.3)) - yaml: 2.8.2 + yaml: 2.8.3 typedoc-plugin-markdown@4.9.0(typedoc@0.28.14(typescript@5.9.3)): dependencies: @@ -8400,7 +8421,7 @@ snapshots: markdown-it: 14.1.1 minimatch: 9.0.5 typescript: 5.9.3 - yaml: 2.8.2 + yaml: 2.8.3 typescript-eslint@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: @@ -8590,6 +8611,21 @@ snapshots: lightningcss: 1.32.0 yaml: 2.8.2 + vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.0.9 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.32.0 + yaml: 2.8.3 + vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.17 @@ -8628,6 +8664,44 @@ snapshots: - tsx - yaml + vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): + dependencies: + '@vitest/expect': 4.0.17 + '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.0.17 + '@vitest/runner': 4.0.17 + '@vitest/snapshot': 4.0.17 + '@vitest/spy': 4.0.17 + '@vitest/utils': 4.0.17 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.0.9 + happy-dom: 20.3.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 4.4.3 @@ -8692,6 +8766,8 @@ snapshots: yaml@2.8.2: {} + yaml@2.8.3: {} + yargs-parser@21.1.1: {} yargs@17.7.2: From 477d3691163d3b422bdc20be2f18fc17d870d189 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Sat, 2 May 2026 22:34:33 -0700 Subject: [PATCH 2/5] fix workspace package.json --- package.json | 2 +- pnpm-lock.yaml | 74 ++------------------------------------------------ 2 files changed, 4 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index aeea99d..33064f6 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "tinyglobby": "^0.2.15", "typescript": "5.9.3", "vitest": "^4.0.17", - "yaml": "^2.7.0" + "yaml": "2.8.3" }, "overrides": {}, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06c3db1..54d65f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,10 +64,10 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.17 - version: 4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2) + version: 4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3) yaml: - specifier: ^2.7.0 - version: 2.8.2 + specifier: 2.8.3 + version: 2.8.3 benchmarks/intent: devDependencies: @@ -4191,11 +4191,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} - engines: {node: '>= 14.6'} - hasBin: true - yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} @@ -5685,14 +5680,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2))': - dependencies: - '@vitest/spy': 4.0.17 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2) - '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.17 @@ -8596,21 +8583,6 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2): - dependencies: - esbuild: 0.27.2 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.55.1 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.0.9 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.32.0 - yaml: 2.8.2 - vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: esbuild: 0.27.2 @@ -8626,44 +8598,6 @@ snapshots: lightningcss: 1.32.0 yaml: 2.8.3 - vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2): - dependencies: - '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.17 - '@vitest/runner': 4.0.17 - '@vitest/snapshot': 4.0.17 - '@vitest/spy': 4.0.17 - '@vitest/utils': 4.0.17 - es-module-lexer: 1.7.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 25.0.9 - happy-dom: 20.3.1 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - vitest@4.0.17(@types/node@25.0.9)(happy-dom@20.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.17 @@ -8764,8 +8698,6 @@ snapshots: yallist@4.0.0: {} - yaml@2.8.2: {} - yaml@2.8.3: {} yargs-parser@21.1.1: {} From 230a5269549f3bb6d60a010f5363bbfd5163c8d7 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Sat, 2 May 2026 23:00:59 -0700 Subject: [PATCH 3/5] refactor workspace pattern discovery and caching --- packages/intent/src/cli-support.ts | 28 +++--- packages/intent/src/workspace-patterns.ts | 99 ++++++++++++++++--- .../intent/tests/workspace-patterns.test.ts | 8 ++ 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts index cf15b5e..bd4b824 100644 --- a/packages/intent/src/cli-support.ts +++ b/packages/intent/src/cli-support.ts @@ -149,29 +149,33 @@ export async function resolveStaleTargets( } } - const { findPackagesWithSkills, findWorkspacePackages, findWorkspaceRoot } = - await import('./workspace-patterns.js') + const { findWorkspaceRoot, getWorkspaceInfo } = await import( + './workspace-patterns.js' + ) const workspaceRoot = findWorkspaceRoot(resolvedRoot) - if (workspaceRoot) { - const packageDirsWithSkills = findPackagesWithSkills(workspaceRoot) - const allPackageDirs = findWorkspacePackages(workspaceRoot) + const workspaceInfo = workspaceRoot ? getWorkspaceInfo(workspaceRoot) : null + if (workspaceInfo) { const reports = await Promise.all( - packageDirsWithSkills.map((packageDir) => - checkStaleness(packageDir, readPackageName(packageDir), workspaceRoot), + workspaceInfo.packageDirsWithSkills.map((packageDir) => + checkStaleness( + packageDir, + readPackageName(packageDir), + workspaceInfo.root, + ), ), ) const { readIntentArtifacts } = await import('./artifact-coverage.js') - const artifacts = existsSync(join(workspaceRoot, '_artifacts')) - ? readIntentArtifacts(workspaceRoot) + const artifacts = existsSync(join(workspaceInfo.root, '_artifacts')) + ? readIntentArtifacts(workspaceInfo.root) : null const coverageSignals = buildWorkspaceCoverageSignals({ - artifactRoot: workspaceRoot, + artifactRoot: workspaceInfo.root, artifacts, - packageDirs: allPackageDirs, + packageDirs: workspaceInfo.packageDirs, }) if (coverageSignals.length > 0) { reports.push({ - library: relative(process.cwd(), workspaceRoot) || 'workspace', + library: relative(process.cwd(), workspaceInfo.root) || 'workspace', currentVersion: null, skillVersion: null, versionDrift: null, diff --git a/packages/intent/src/workspace-patterns.ts b/packages/intent/src/workspace-patterns.ts index 15d6890..a9851b8 100644 --- a/packages/intent/src/workspace-patterns.ts +++ b/packages/intent/src/workspace-patterns.ts @@ -111,6 +111,13 @@ type WorkspacePatternSource = { getPatterns: (config: unknown) => Array | null } +export type WorkspaceInfo = { + root: string + patterns: Array + packageDirs: Array + packageDirsWithSkills: Array +} + const workspacePatternSources: Array = [ { fileName: 'pnpm-workspace.yaml', @@ -153,7 +160,22 @@ const workspacePatternSources: Array = [ }, ] +const workspacePatternsCache = new Map | null>() +const workspaceRootCache = new Map() +const workspacePackageDirsCache = new Map | null>() +const workspaceInfoCache = new Map() + export function readWorkspacePatterns(root: string): Array | null { + if (workspacePatternsCache.has(root)) { + return workspacePatternsCache.get(root) ?? null + } + + const patterns = readWorkspacePatternsUncached(root) + workspacePatternsCache.set(root, patterns) + return patterns +} + +function readWorkspacePatternsUncached(root: string): Array | null { for (const source of workspacePatternSources) { const path = join(root, source.fileName) @@ -174,6 +196,49 @@ export function readWorkspacePatterns(root: string): Array | null { return null } +function readWorkspacePackageDirs(root: string): Array | null { + if (workspacePackageDirsCache.has(root)) { + return workspacePackageDirsCache.get(root) ?? null + } + + const patterns = readWorkspacePatterns(root) + if (!patterns) { + workspacePackageDirsCache.set(root, null) + return null + } + + const packageDirs = resolveWorkspacePackages(root, patterns) + workspacePackageDirsCache.set(root, packageDirs) + return packageDirs +} + +export function getWorkspaceInfo(root: string): WorkspaceInfo | null { + if (workspaceInfoCache.has(root)) { + return workspaceInfoCache.get(root) ?? null + } + + const patterns = readWorkspacePatterns(root) + if (!patterns) { + workspaceInfoCache.set(root, null) + return null + } + + const packageDirs = readWorkspacePackageDirs(root) ?? [] + const packageDirsWithSkills = packageDirs.filter((dir) => { + const skillsDir = join(dir, 'skills') + return existsSync(skillsDir) && findSkillFiles(skillsDir).length > 0 + }) + const info = { + root, + patterns, + packageDirs, + packageDirsWithSkills, + } + + workspaceInfoCache.set(root, info) + return info +} + export function resolveWorkspacePackages( root: string, patterns: Array, @@ -258,31 +323,41 @@ function readChildDirectories(dir: string): Array { export function findWorkspaceRoot(start: string): string | null { let dir = start + const visited: Array = [] while (true) { + const cached = workspaceRootCache.get(dir) + if (cached !== undefined) { + for (const visitedDir of visited) { + workspaceRootCache.set(visitedDir, cached) + } + return cached + } + + visited.push(dir) + if (readWorkspacePatterns(dir)) { + for (const visitedDir of visited) { + workspaceRootCache.set(visitedDir, dir) + } return dir } const next = dirname(dir) - if (next === dir) return null + if (next === dir) { + for (const visitedDir of visited) { + workspaceRootCache.set(visitedDir, null) + } + return null + } dir = next } } export function findPackagesWithSkills(root: string): Array { - const patterns = readWorkspacePatterns(root) - if (!patterns) return [] - - return resolveWorkspacePackages(root, patterns).filter((dir) => { - const skillsDir = join(dir, 'skills') - return existsSync(skillsDir) && findSkillFiles(skillsDir).length > 0 - }) + return getWorkspaceInfo(root)?.packageDirsWithSkills ?? [] } export function findWorkspacePackages(root: string): Array { - const patterns = readWorkspacePatterns(root) - if (!patterns) return [] - - return resolveWorkspacePackages(root, patterns) + return readWorkspacePackageDirs(root) ?? [] } diff --git a/packages/intent/tests/workspace-patterns.test.ts b/packages/intent/tests/workspace-patterns.test.ts index d3858d7..cfb8338 100644 --- a/packages/intent/tests/workspace-patterns.test.ts +++ b/packages/intent/tests/workspace-patterns.test.ts @@ -12,6 +12,7 @@ import { findPackagesWithSkills, findWorkspacePackages, findWorkspaceRoot, + getWorkspaceInfo, readWorkspacePatterns, resolveWorkspacePackages, } from '../src/workspace-patterns.js' @@ -361,6 +362,13 @@ describe('workspace helpers', () => { withCwd(nestedDir) expect(findWorkspaceRoot(process.cwd())).toBe(root) + expect(getWorkspaceInfo(root)?.packageDirs).toEqual([ + join(root, 'packages', 'alpha'), + join(root, 'packages', 'beta'), + ]) + expect(getWorkspaceInfo(root)?.packageDirsWithSkills).toEqual([ + join(root, 'packages', 'alpha'), + ]) expect(findPackagesWithSkills(root)).toEqual([ join(root, 'packages', 'alpha'), ]) From 34a12eacf27591f09d3bbb5cd707e6392adc7344 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 06:01:48 +0000 Subject: [PATCH 4/5] ci: apply automated fixes --- packages/intent/src/cli-support.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts index bd4b824..21128b7 100644 --- a/packages/intent/src/cli-support.ts +++ b/packages/intent/src/cli-support.ts @@ -149,9 +149,8 @@ export async function resolveStaleTargets( } } - const { findWorkspaceRoot, getWorkspaceInfo } = await import( - './workspace-patterns.js' - ) + const { findWorkspaceRoot, getWorkspaceInfo } = + await import('./workspace-patterns.js') const workspaceRoot = findWorkspaceRoot(resolvedRoot) const workspaceInfo = workspaceRoot ? getWorkspaceInfo(workspaceRoot) : null if (workspaceInfo) { From db75840d4e7f2ebbd27b062f7939d8f2074c95f5 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Sat, 2 May 2026 23:06:41 -0700 Subject: [PATCH 5/5] changeset --- .changeset/perky-vans-yell.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/perky-vans-yell.md diff --git a/.changeset/perky-vans-yell.md b/.changeset/perky-vans-yell.md new file mode 100644 index 0000000..c85ae8f --- /dev/null +++ b/.changeset/perky-vans-yell.md @@ -0,0 +1,7 @@ +--- +'@tanstack/intent': patch +--- + +Refactor workspace pattern discovery to use a JSONC parser for Deno configs, support additional workspace config shapes, and cache workspace roots, parsed patterns, and resolved package directories during CLI commands. + +This also allows Deno workspace members with `deno.json` or `deno.jsonc` manifests to be resolved as workspace packages.