From 15e1c36ad1693aa99a640aafd125991556b2de0f Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Sun, 3 May 2026 09:37:38 -0500 Subject: [PATCH] fix(cli): enhance install script and recursive path resolve - Updated the install script to use dynamic binary names for macOS and Linux targets. - Improved error messages for release fetching in the install script. - Refactored CLI command registration to conditionally include extended commands based on the WEBQ_EXTENDED environment variable. - Modified tests to ensure proper handling of custom styles and attributes, including recursive directory resolution. - Introduced new resolver functions for handling multiple file resolutions in the attributes and styles modules. Signed-off-by: Cory Rylan --- projects/cli/install.sh | 24 ++--- projects/cli/src/cli.ts | 100 +++++++++--------- projects/cli/src/cli/helpers.test.ts | 17 +++ projects/cli/src/cli/helpers.ts | 81 +++++++++----- .../src/internal/attributes/resolver.test.ts | 17 +-- .../cli/src/internal/attributes/resolver.ts | 6 +- projects/cli/src/internal/dtcg/index.ts | 1 + .../cli/src/internal/dtcg/resolver.test.ts | 27 +++++ projects/cli/src/internal/dtcg/resolver.ts | 7 ++ .../src/internal/patterns/resolver.test.ts | 17 +-- .../cli/src/internal/patterns/resolver.ts | 6 +- .../cli/src/internal/resolve/resolve.test.ts | 37 ++++++- projects/cli/src/internal/resolve/resolve.ts | 35 +++++- .../cli/src/internal/styles/resolver.test.ts | 17 +-- projects/cli/src/internal/styles/resolver.ts | 6 +- .../cli/src/internal/styles/tools/get.test.ts | 2 +- projects/cli/src/internal/styles/tools/get.ts | 6 +- .../src/internal/styles/tools/list.test.ts | 4 +- .../cli/src/internal/styles/tools/list.ts | 2 +- 19 files changed, 290 insertions(+), 122 deletions(-) create mode 100644 projects/cli/src/internal/dtcg/resolver.test.ts create mode 100644 projects/cli/src/internal/dtcg/resolver.ts diff --git a/projects/cli/install.sh b/projects/cli/install.sh index a998a55..6ec0551 100755 --- a/projects/cli/install.sh +++ b/projects/cli/install.sh @@ -13,15 +13,15 @@ ARCH="$(uname -m)" case "$OS" in Darwin) case "$ARCH" in - arm64) TARGET="webq-macos-arm64" ;; - x86_64) TARGET="webq-macos-x64" ;; + arm64) TARGET="$BINARY_NAME-macos-arm64" ;; + x86_64) TARGET="$BINARY_NAME-macos-x64" ;; *) echo "Unsupported architecture: $ARCH"; exit 1 ;; esac ;; Linux) case "$ARCH" in - x86_64) TARGET="webq-linux-x64" ;; - aarch64) TARGET="webq-linux-arm64" ;; + x86_64) TARGET="$BINARY_NAME-linux-x64" ;; + aarch64) TARGET="$BINARY_NAME-linux-arm64" ;; *) echo "Unsupported architecture: $ARCH"; exit 1 ;; esac ;; @@ -42,11 +42,11 @@ if [ -f "$SCRIPT_DIR/dist/$TARGET" ]; then else echo "Downloading $TARGET..." TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases" \ - | grep -o '"tag_name": *"@webq/cli-v[^"]*"' \ + | grep -o '"tag_name": *"'$BINARY_NAME'-v[^"]*"' \ | head -1 \ | cut -d'"' -f4) if [ -z "$TAG" ]; then - echo "Could not find latest @webq/cli release"; exit 1 + echo "Could not find latest release"; exit 1 fi DOWNLOAD_URL="https://github.com/$REPO/releases/download/$TAG/$TARGET" SOURCE="$(mktemp)" @@ -55,11 +55,6 @@ fi chmod +x "$SOURCE" -# macOS requires ad-hoc code signature for binaries to execute -if [ "$OS" = "Darwin" ] && command -v codesign >/dev/null 2>&1; then - codesign --sign - --force "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || warn "Ad-hoc code signing failed — binary may not run." -fi - DEST="$INSTALL_DIR/$BINARY_NAME" if [ -w "$INSTALL_DIR" ]; then @@ -69,6 +64,11 @@ else sudo cp "$SOURCE" "$DEST" fi +# macOS requires ad-hoc code signature for binaries to execute +if [ "$OS" = "Darwin" ] && command -v codesign >/dev/null 2>&1; then + codesign --sign - --force "$DEST" 2>/dev/null || echo "Ad-hoc code signing failed — binary may not run." +fi + echo "Installed $BINARY_NAME to $DEST" echo "" -echo "Run 'webq --help' to get started." +echo "Run '$BINARY_NAME --help' to get started." diff --git a/projects/cli/src/cli.ts b/projects/cli/src/cli.ts index 38a288a..0ce923e 100644 --- a/projects/cli/src/cli.ts +++ b/projects/cli/src/cli.ts @@ -299,54 +299,6 @@ const cli = yargs(hideBin(process.argv)) tagNamePositional, elementHandler(elementGet, extractTagName) ) - .command( - elementAttributes.metadata.command, - elementAttributes.metadata.summary, - tagNamePositional, - elementHandler(elementAttributes, extractTagName) - ) - .command( - elementProperties.metadata.command, - elementProperties.metadata.summary, - tagNamePositional, - elementHandler(elementProperties, extractTagName) - ) - .command( - elementMethods.metadata.command, - elementMethods.metadata.summary, - tagNamePositional, - elementHandler(elementMethods, extractTagName) - ) - .command( - elementEvents.metadata.command, - elementEvents.metadata.summary, - tagNamePositional, - elementHandler(elementEvents, extractTagName) - ) - .command( - elementSlots.metadata.command, - elementSlots.metadata.summary, - tagNamePositional, - elementHandler(elementSlots, extractTagName) - ) - .command( - elementCommands.metadata.command, - elementCommands.metadata.summary, - tagNamePositional, - elementHandler(elementCommands, extractTagName) - ) - .command( - elementCSSProperties.metadata.command, - elementCSSProperties.metadata.summary, - tagNamePositional, - elementHandler(elementCSSProperties, extractTagName) - ) - .command( - elementCSSParts.metadata.command, - elementCSSParts.metadata.summary, - tagNamePositional, - elementHandler(elementCSSParts, extractTagName) - ) // Attribute commands .command( attributeList.metadata.command, @@ -460,6 +412,58 @@ const cli = yargs(hideBin(process.argv)) .fail(false) .help(); +if (process.env.WEBQ_EXTENDED) { + cli + .command( + elementAttributes.metadata.command, + elementAttributes.metadata.summary, + tagNamePositional, + elementHandler(elementAttributes, extractTagName) + ) + .command( + elementProperties.metadata.command, + elementProperties.metadata.summary, + tagNamePositional, + elementHandler(elementProperties, extractTagName) + ) + .command( + elementMethods.metadata.command, + elementMethods.metadata.summary, + tagNamePositional, + elementHandler(elementMethods, extractTagName) + ) + .command( + elementEvents.metadata.command, + elementEvents.metadata.summary, + tagNamePositional, + elementHandler(elementEvents, extractTagName) + ) + .command( + elementSlots.metadata.command, + elementSlots.metadata.summary, + tagNamePositional, + elementHandler(elementSlots, extractTagName) + ) + .command( + elementCommands.metadata.command, + elementCommands.metadata.summary, + tagNamePositional, + elementHandler(elementCommands, extractTagName) + ) + .command( + elementCSSProperties.metadata.command, + elementCSSProperties.metadata.summary, + tagNamePositional, + elementHandler(elementCSSProperties, extractTagName) + ) + .command( + elementCSSParts.metadata.command, + elementCSSParts.metadata.summary, + tagNamePositional, + elementHandler(elementCSSParts, extractTagName) + ); +} + cli.wrap(MAX_WIDTH); try { diff --git a/projects/cli/src/cli/helpers.test.ts b/projects/cli/src/cli/helpers.test.ts index ee01b55..e7a8c4b 100644 --- a/projects/cli/src/cli/helpers.test.ts +++ b/projects/cli/src/cli/helpers.test.ts @@ -202,4 +202,21 @@ describe('loadCustomStylesStore', () => { const store = await loadCustomStylesStore(cfg, testdataPath); expect(store).toBeDefined(); }); + + test('auto-discovers tokens.json from path when tokensPath is not set', async () => { + const cfg = emptyConfig(); + const store = await loadCustomStylesStore(cfg, testdataPath); + if (!store) throw new Error('expected store to be defined'); + const props = store.getCSSCustomProperties().map(p => p.name); + expect(props).toContain('--spacing-sm'); + }); + + test('configured tokensPath wins over auto-discovery', async () => { + const cfg = emptyConfig(); + cfg.global.tokensPath = join(testdataPath, 'tokens.json'); + const store = await loadCustomStylesStore(cfg, '/nonexistent'); + if (!store) throw new Error('expected store to be defined'); + const props = store.getCSSCustomProperties().map(p => p.name); + expect(props).toContain('--spacing-sm'); + }); }); diff --git a/projects/cli/src/cli/helpers.ts b/projects/cli/src/cli/helpers.ts index b885a30..d193267 100644 --- a/projects/cli/src/cli/helpers.ts +++ b/projects/cli/src/cli/helpers.ts @@ -14,6 +14,7 @@ import { parseCustomStyles } from '../internal/styles/parser.js'; import { resolve as resolveStyles } from '../internal/styles/resolver.js'; import { load as loadVSCode } from '../internal/vscode/load.js'; import { load as loadDTCG } from '../internal/dtcg/load.js'; +import { resolve as resolveTokens } from '../internal/dtcg/resolver.js'; import type { VSCodeResult } from '../internal/vscode/load.js'; import type { Manifest } from '../internal/elements/types.js'; import type { ValidateConfig } from '../internal/validate/types.js'; @@ -64,47 +65,62 @@ export function printJSON(data: unknown): void { console.log(JSON.stringify(data, null, 2)); } -async function discoverOptionalFile( +async function discoverOptionalFiles( cfg: Config, - resolveFn: (path: string) => Promise, + resolveFn: (path: string) => Promise, pathFlag?: string -): Promise { +): Promise { const path = resolvedPath(cfg, pathFlag); - if (!path) return ''; + if (!path) return []; + const matches: string[] = []; for (const pathArg of path.split(',')) { const trimmed = pathArg.trim(); if (!trimmed) continue; - const resolved = await resolveFn(trimmed); - if (resolved) return resolved; + matches.push(...(await resolveFn(trimmed))); } + return matches; +} - return ''; +async function resolvePatternsPaths(cfg: Config, pathFlag?: string): Promise { + const configured = cfg.global.patternsPath ?? ''; + if (configured) return [configured]; + return discoverOptionalFiles(cfg, resolvePatterns, pathFlag); } export async function loadPatternsStore(cfg: Config, pathFlag?: string): Promise { - let path = cfg.global.patternsPath ?? ''; - if (!path) { - path = await discoverOptionalFile(cfg, resolvePatterns, pathFlag); + const paths = await resolvePatternsPaths(cfg, pathFlag); + if (paths.length === 0) return undefined; + + const [first, ...rest] = paths; + const pf = await parsePatterns(first); + for (const path of rest) { + const extra = await parsePatterns(path); + pf.patterns.push(...extra.patterns); } - if (!path) return undefined; - - const pf = await parsePatterns(path); return new PatternStore(pf); } +async function resolveAttributesPaths(cfg: Config, pathFlag?: string): Promise { + const configured = cfg.global.attributesPath ?? ''; + if (configured) return [configured]; + return discoverOptionalFiles(cfg, resolveAttributes, pathFlag); +} + export async function loadCustomAttributesStore( cfg: Config, pathFlag?: string ): Promise { let caf; - let path = cfg.global.attributesPath ?? ''; - if (!path) { - path = await discoverOptionalFile(cfg, resolveAttributes, pathFlag); - } - if (path) { - caf = await parseCustomAttributes(path); + const paths = await resolveAttributesPaths(cfg, pathFlag); + for (const path of paths) { + const next = await parseCustomAttributes(path); + if (caf) { + caf.attributes.push(...next.attributes); + } else { + caf = next; + } } const vscodeData = await loadVSCodeData(cfg, pathFlag); @@ -120,10 +136,10 @@ export async function loadCustomAttributesStore( return new CustomAttributeStore(caf); } -async function resolveStylesPath(cfg: Config, pathFlag?: string): Promise { +async function resolveStylesPaths(cfg: Config, pathFlag?: string): Promise { const configured = cfg.global.stylesPath ?? ''; - if (configured) return configured; - return discoverOptionalFile(cfg, resolveStyles, pathFlag); + if (configured) return [configured]; + return discoverOptionalFiles(cfg, resolveStyles, pathFlag); } type StylesFile = Awaited>; @@ -135,18 +151,29 @@ function mergeStyles(base: StylesFile | undefined, extra: StylesFile | undefined return base; } -async function loadDTCGTokens(tokensPath?: string): Promise { - if (!tokensPath) return undefined; +async function resolveTokensPaths(cfg: Config, pathFlag?: string): Promise { + const configured = cfg.global.tokensPath ?? ''; + if (configured) return [configured]; + return discoverOptionalFiles(cfg, resolveTokens, pathFlag); +} + +async function loadDTCGTokens(tokensPath: string): Promise { return (await loadDTCG(tokensPath)) ?? undefined; } export async function loadCustomStylesStore(cfg: Config, pathFlag?: string): Promise { - const path = await resolveStylesPath(cfg, pathFlag); - let csf: StylesFile | undefined = path ? await parseCustomStyles(path) : undefined; + let csf: StylesFile | undefined; + + for (const path of await resolveStylesPaths(cfg, pathFlag)) { + csf = mergeStyles(csf, await parseCustomStyles(path)); + } const vscodeData = await loadVSCodeData(cfg, pathFlag); csf = mergeStyles(csf, vscodeData?.styles); - csf = mergeStyles(csf, await loadDTCGTokens(cfg.global.tokensPath)); + + for (const path of await resolveTokensPaths(cfg, pathFlag)) { + csf = mergeStyles(csf, await loadDTCGTokens(path)); + } if (!csf) return undefined; return new CustomStyleStore(csf); diff --git a/projects/cli/src/internal/attributes/resolver.test.ts b/projects/cli/src/internal/attributes/resolver.test.ts index d145472..a7b379e 100644 --- a/projects/cli/src/internal/attributes/resolver.test.ts +++ b/projects/cli/src/internal/attributes/resolver.test.ts @@ -7,16 +7,21 @@ const testdataPath = join(import.meta.dir, '../../../testdata'); describe('attributes resolver', () => { test('finds custom-attributes.json in testdata', async () => { const result = await resolve(testdataPath); - expect(result).toBe(join(testdataPath, CustomAttributesFilename)); + expect(result).toContain(join(testdataPath, CustomAttributesFilename)); }); - test('returns empty string for directory without attributes file', async () => { - const result = await resolve(join(testdataPath, '..', 'src')); - expect(result).toBe(''); + test('walks recursively', async () => { + const result = await resolve(join(testdataPath, '..')); + expect(result.some(p => p.endsWith(CustomAttributesFilename))).toBe(true); }); - test('returns empty string for non-existent directory', async () => { + test('returns empty array for directory without attributes file', async () => { + const result = await resolve(join(testdataPath, '..', 'src', 'internal', 'config')); + expect(result).toEqual([]); + }); + + test('returns empty array for non-existent directory', async () => { const result = await resolve(join(testdataPath, 'no-such-dir')); - expect(result).toBe(''); + expect(result).toEqual([]); }); }); diff --git a/projects/cli/src/internal/attributes/resolver.ts b/projects/cli/src/internal/attributes/resolver.ts index f46a00e..f9aa69a 100644 --- a/projects/cli/src/internal/attributes/resolver.ts +++ b/projects/cli/src/internal/attributes/resolver.ts @@ -1,7 +1,7 @@ -import { resolveFile } from '../resolve/resolve.js'; +import { resolveFiles } from '../resolve/resolve.js'; export const CustomAttributesFilename = 'custom-attributes.json'; -export async function resolve(path: string): Promise { - return resolveFile(path, CustomAttributesFilename); +export async function resolve(path: string): Promise { + return resolveFiles(path, CustomAttributesFilename); } diff --git a/projects/cli/src/internal/dtcg/index.ts b/projects/cli/src/internal/dtcg/index.ts index f0dfa67..dca5d3b 100644 --- a/projects/cli/src/internal/dtcg/index.ts +++ b/projects/cli/src/internal/dtcg/index.ts @@ -2,3 +2,4 @@ export * from './types.js'; export * from './parser.js'; export * from './convert.js'; export { load } from './load.js'; +export { resolve, DTCGTokensFilename } from './resolver.js'; diff --git a/projects/cli/src/internal/dtcg/resolver.test.ts b/projects/cli/src/internal/dtcg/resolver.test.ts new file mode 100644 index 0000000..fdaa341 --- /dev/null +++ b/projects/cli/src/internal/dtcg/resolver.test.ts @@ -0,0 +1,27 @@ +import { describe, test, expect } from 'bun:test'; +import { join } from 'path'; +import { resolve, DTCGTokensFilename } from './resolver.js'; + +const testdataPath = join(import.meta.dir, '../../../testdata'); + +describe('dtcg resolver', () => { + test('finds tokens.json in testdata', async () => { + const result = await resolve(testdataPath); + expect(result).toContain(join(testdataPath, DTCGTokensFilename)); + }); + + test('walks recursively', async () => { + const result = await resolve(join(testdataPath, '..')); + expect(result.some(p => p.endsWith(DTCGTokensFilename))).toBe(true); + }); + + test('returns empty array for directory without tokens file', async () => { + const result = await resolve(join(testdataPath, '..', 'src', 'internal', 'config')); + expect(result).toEqual([]); + }); + + test('returns empty array for non-existent directory', async () => { + const result = await resolve(join(testdataPath, 'no-such-dir')); + expect(result).toEqual([]); + }); +}); diff --git a/projects/cli/src/internal/dtcg/resolver.ts b/projects/cli/src/internal/dtcg/resolver.ts new file mode 100644 index 0000000..05c44fc --- /dev/null +++ b/projects/cli/src/internal/dtcg/resolver.ts @@ -0,0 +1,7 @@ +import { resolveFiles } from '../resolve/resolve.js'; + +export const DTCGTokensFilename = 'tokens.json'; + +export async function resolve(path: string): Promise { + return resolveFiles(path, DTCGTokensFilename); +} diff --git a/projects/cli/src/internal/patterns/resolver.test.ts b/projects/cli/src/internal/patterns/resolver.test.ts index c54c9d1..8fb7ea6 100644 --- a/projects/cli/src/internal/patterns/resolver.test.ts +++ b/projects/cli/src/internal/patterns/resolver.test.ts @@ -7,16 +7,21 @@ const testdataPath = join(import.meta.dir, '../../../testdata'); describe('patterns resolver', () => { test('finds custom-patterns.json in testdata', async () => { const result = await resolve(testdataPath); - expect(result).toBe(join(testdataPath, PatternsFilename)); + expect(result).toContain(join(testdataPath, PatternsFilename)); }); - test('returns empty string for directory without patterns file', async () => { - const result = await resolve(join(testdataPath, '..', 'src')); - expect(result).toBe(''); + test('walks recursively', async () => { + const result = await resolve(join(testdataPath, '..')); + expect(result.some(p => p.endsWith(PatternsFilename))).toBe(true); }); - test('returns empty string for non-existent directory', async () => { + test('returns empty array for directory without patterns file', async () => { + const result = await resolve(join(testdataPath, '..', 'src', 'internal', 'config')); + expect(result).toEqual([]); + }); + + test('returns empty array for non-existent directory', async () => { const result = await resolve(join(testdataPath, 'no-such-dir')); - expect(result).toBe(''); + expect(result).toEqual([]); }); }); diff --git a/projects/cli/src/internal/patterns/resolver.ts b/projects/cli/src/internal/patterns/resolver.ts index 45d141f..90839e8 100644 --- a/projects/cli/src/internal/patterns/resolver.ts +++ b/projects/cli/src/internal/patterns/resolver.ts @@ -1,7 +1,7 @@ -import { resolveFile } from '../resolve/resolve.js'; +import { resolveFiles } from '../resolve/resolve.js'; export const PatternsFilename = 'custom-patterns.json'; -export async function resolve(path: string): Promise { - return resolveFile(path, PatternsFilename); +export async function resolve(path: string): Promise { + return resolveFiles(path, PatternsFilename); } diff --git a/projects/cli/src/internal/resolve/resolve.test.ts b/projects/cli/src/internal/resolve/resolve.test.ts index d9a393a..38c980d 100644 --- a/projects/cli/src/internal/resolve/resolve.test.ts +++ b/projects/cli/src/internal/resolve/resolve.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect, afterEach } from 'bun:test'; -import { resolveFile } from './resolve.js'; +import { resolveFile, resolveFiles } from './resolve.js'; import { join } from 'path'; -import { mkdtemp, writeFile, rm } from 'fs/promises'; +import { mkdir, mkdtemp, writeFile, rm } from 'fs/promises'; import { tmpdir } from 'os'; const testdataPath = join(import.meta.dir, '../../../testdata'); @@ -49,3 +49,36 @@ describe('resolveFile', () => { expect(result).toBe(join(dir, 'test.json')); }); }); + +describe('resolveFiles', () => { + test('finds matches recursively', async () => { + const dir = await makeTempDir(); + const nested = join(dir, 'a', 'b'); + await mkdir(nested, { recursive: true }); + await writeFile(join(dir, 'tokens.json'), '{}'); + await writeFile(join(nested, 'tokens.json'), '{}'); + + const result = await resolveFiles(dir, 'tokens.json'); + expect(result.sort()).toEqual([join(dir, 'tokens.json'), join(nested, 'tokens.json')].sort()); + }); + + test('skips well-known build/cache dirs', async () => { + const dir = await makeTempDir(); + const skipped = join(dir, '.git'); + await mkdir(skipped, { recursive: true }); + await writeFile(join(skipped, 'tokens.json'), '{}'); + + const result = await resolveFiles(dir, 'tokens.json'); + expect(result).toEqual([]); + }); + + test('returns empty array for non-existent directory', async () => { + const result = await resolveFiles(join(testdataPath, 'no-such-dir'), 'tokens.json'); + expect(result).toEqual([]); + }); + + test('throws when path is not a directory', async () => { + const filePath = join(testdataPath, 'custom-elements.json'); + await expect(resolveFiles(filePath, 'tokens.json')).rejects.toThrow('is not a directory'); + }); +}); diff --git a/projects/cli/src/internal/resolve/resolve.ts b/projects/cli/src/internal/resolve/resolve.ts index 115c9d1..6d4863f 100644 --- a/projects/cli/src/internal/resolve/resolve.ts +++ b/projects/cli/src/internal/resolve/resolve.ts @@ -1,6 +1,8 @@ -import { stat } from 'fs/promises'; +import { stat, readdir } from 'fs/promises'; import { join } from 'path'; +const skipDirs = new Set(['.wireit', '.git', '.cache', '.turbo', '.nx', '.parcel-cache']); + export async function resolveFile(dir: string, filename: string): Promise { try { const info = await stat(dir); @@ -21,3 +23,34 @@ export async function resolveFile(dir: string, filename: string): Promise { + let info; + try { + info = await stat(dir); + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw err; + } + if (!info.isDirectory()) { + throw new Error(`path "${dir}" is not a directory`); + } + + const matches: string[] = []; + await walkDir(dir, filename, matches); + return matches; +} + +async function walkDir(dir: string, filename: string, matches: string[]): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + if (!skipDirs.has(entry.name)) { + await walkDir(fullPath, filename, matches); + } + } else if (entry.name === filename) { + matches.push(fullPath); + } + } +} diff --git a/projects/cli/src/internal/styles/resolver.test.ts b/projects/cli/src/internal/styles/resolver.test.ts index 640534b..94e41b2 100644 --- a/projects/cli/src/internal/styles/resolver.test.ts +++ b/projects/cli/src/internal/styles/resolver.test.ts @@ -7,16 +7,21 @@ const testdataPath = join(import.meta.dir, '../../../testdata'); describe('styles resolver', () => { test('finds custom-styles.json in testdata', async () => { const result = await resolve(testdataPath); - expect(result).toBe(join(testdataPath, CustomStylesFilename)); + expect(result).toContain(join(testdataPath, CustomStylesFilename)); }); - test('returns empty string for directory without styles file', async () => { - const result = await resolve(join(testdataPath, '..', 'src')); - expect(result).toBe(''); + test('walks recursively', async () => { + const result = await resolve(join(testdataPath, '..')); + expect(result.some(p => p.endsWith(CustomStylesFilename))).toBe(true); }); - test('returns empty string for non-existent directory', async () => { + test('returns empty array for directory without styles file', async () => { + const result = await resolve(join(testdataPath, '..', 'src', 'internal', 'config')); + expect(result).toEqual([]); + }); + + test('returns empty array for non-existent directory', async () => { const result = await resolve(join(testdataPath, 'no-such-dir')); - expect(result).toBe(''); + expect(result).toEqual([]); }); }); diff --git a/projects/cli/src/internal/styles/resolver.ts b/projects/cli/src/internal/styles/resolver.ts index cbf9003..464cc80 100644 --- a/projects/cli/src/internal/styles/resolver.ts +++ b/projects/cli/src/internal/styles/resolver.ts @@ -1,7 +1,7 @@ -import { resolveFile } from '../resolve/resolve.js'; +import { resolveFiles } from '../resolve/resolve.js'; export const CustomStylesFilename = 'custom-styles.json'; -export async function resolve(path: string): Promise { - return resolveFile(path, CustomStylesFilename); +export async function resolve(path: string): Promise { + return resolveFiles(path, CustomStylesFilename); } diff --git a/projects/cli/src/internal/styles/tools/get.test.ts b/projects/cli/src/internal/styles/tools/get.test.ts index 0664505..d970173 100644 --- a/projects/cli/src/internal/styles/tools/get.test.ts +++ b/projects/cli/src/internal/styles/tools/get.test.ts @@ -50,7 +50,7 @@ describe('stylePropertyGet', () => { test('toJSON throws when no store', () => { expect(() => stylePropertyGet.toJSON(makeCtx(false), { name: '--bp-color-blue-0' })).toThrow( - 'No custom styles file loaded' + 'No CSS custom properties found' ); }); diff --git a/projects/cli/src/internal/styles/tools/get.ts b/projects/cli/src/internal/styles/tools/get.ts index 7c093c5..d1f7bbf 100644 --- a/projects/cli/src/internal/styles/tools/get.ts +++ b/projects/cli/src/internal/styles/tools/get.ts @@ -18,15 +18,17 @@ export const metadata = { }) }; +const errNoStyleSources = 'No CSS custom properties found (no custom-styles.json or DTCG tokens.json)'; + export function toMarkdown(ctx: ToolContext, input: { name: string }): string { - if (!ctx.customStyleStore) throw new Error('No custom styles file loaded'); + if (!ctx.customStyleStore) throw new Error(errNoStyleSources); const prop = ctx.customStyleStore.getCSSCustomProperty(input.name); if (!prop) throw new Error(`CSS custom property "${input.name}" not found`); return formatCSSCustomPropertyDetail(prop); } export function toJSON(ctx: ToolContext, input: { name: string }): CSSCustomProperty { - if (!ctx.customStyleStore) throw new Error('No custom styles file loaded'); + if (!ctx.customStyleStore) throw new Error(errNoStyleSources); const prop = ctx.customStyleStore.getCSSCustomProperty(input.name); if (!prop) throw new Error(`CSS custom property "${input.name}" not found`); return prop; diff --git a/projects/cli/src/internal/styles/tools/list.test.ts b/projects/cli/src/internal/styles/tools/list.test.ts index a100ec1..8b8117c 100644 --- a/projects/cli/src/internal/styles/tools/list.test.ts +++ b/projects/cli/src/internal/styles/tools/list.test.ts @@ -50,6 +50,8 @@ describe('stylePropertyList', () => { test('toMarkdown returns message when no store', () => { const md = stylePropertyList.toMarkdown(makeCtx(false)); - expect(md).toContain('No custom-styles.json found'); + expect(md).toContain('No CSS custom properties found'); + expect(md).toContain('custom-styles.json'); + expect(md).toContain('tokens.json'); }); }); diff --git a/projects/cli/src/internal/styles/tools/list.ts b/projects/cli/src/internal/styles/tools/list.ts index a12d36b..73ac4a0 100644 --- a/projects/cli/src/internal/styles/tools/list.ts +++ b/projects/cli/src/internal/styles/tools/list.ts @@ -17,7 +17,7 @@ export const metadata = { }; export function toMarkdown(ctx: ToolContext): string { - if (!ctx.customStyleStore) return 'No custom-styles.json found\n'; + if (!ctx.customStyleStore) return 'No CSS custom properties found (no custom-styles.json or DTCG tokens.json)\n'; return formatCSSCustomPropertySummaries(ctx.customStyleStore.getCSSCustomProperties()); }