Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions projects/cli/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand All @@ -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)"
Expand All @@ -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
Expand All @@ -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."
100 changes: 52 additions & 48 deletions projects/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions projects/cli/src/cli/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
81 changes: 54 additions & 27 deletions projects/cli/src/cli/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string>,
resolveFn: (path: string) => Promise<string[]>,
pathFlag?: string
): Promise<string> {
): Promise<string[]> {
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<string[]> {
const configured = cfg.global.patternsPath ?? '';
if (configured) return [configured];
return discoverOptionalFiles(cfg, resolvePatterns, pathFlag);
}

export async function loadPatternsStore(cfg: Config, pathFlag?: string): Promise<PatternStore | undefined> {
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<string[]> {
const configured = cfg.global.attributesPath ?? '';
if (configured) return [configured];
return discoverOptionalFiles(cfg, resolveAttributes, pathFlag);
}

export async function loadCustomAttributesStore(
cfg: Config,
pathFlag?: string
): Promise<CustomAttributeStore | undefined> {
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);
Expand All @@ -120,10 +136,10 @@ export async function loadCustomAttributesStore(
return new CustomAttributeStore(caf);
}

async function resolveStylesPath(cfg: Config, pathFlag?: string): Promise<string> {
async function resolveStylesPaths(cfg: Config, pathFlag?: string): Promise<string[]> {
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<ReturnType<typeof parseCustomStyles>>;
Expand All @@ -135,18 +151,29 @@ function mergeStyles(base: StylesFile | undefined, extra: StylesFile | undefined
return base;
}

async function loadDTCGTokens(tokensPath?: string): Promise<StylesFile | undefined> {
if (!tokensPath) return undefined;
async function resolveTokensPaths(cfg: Config, pathFlag?: string): Promise<string[]> {
const configured = cfg.global.tokensPath ?? '';
if (configured) return [configured];
return discoverOptionalFiles(cfg, resolveTokens, pathFlag);
}

async function loadDTCGTokens(tokensPath: string): Promise<StylesFile | undefined> {
return (await loadDTCG(tokensPath)) ?? undefined;
}

export async function loadCustomStylesStore(cfg: Config, pathFlag?: string): Promise<CustomStyleStore | undefined> {
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);
Expand Down
17 changes: 11 additions & 6 deletions projects/cli/src/internal/attributes/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
});
6 changes: 3 additions & 3 deletions projects/cli/src/internal/attributes/resolver.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
return resolveFile(path, CustomAttributesFilename);
export async function resolve(path: string): Promise<string[]> {
return resolveFiles(path, CustomAttributesFilename);
}
1 change: 1 addition & 0 deletions projects/cli/src/internal/dtcg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading