@astryxdesign/cli 0.1.2-canary.ff01f6d → 0.1.3-canary.6d57c8d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -1
- package/README.md +49 -55
- package/docs/layout.doc.dense.mjs +90 -0
- package/docs/layout.doc.mjs +160 -0
- package/docs/principles.doc.dense.mjs +2 -2
- package/docs/principles.doc.mjs +4 -0
- package/docs/principles.doc.zh.mjs +2 -2
- package/package.json +7 -7
- package/src/api/blog.mjs +192 -0
- package/src/api/blog.test.mjs +144 -0
- package/src/api/doctor.mjs +3 -3
- package/src/api/index.mjs +1 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +23 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
- package/src/commands/agent-docs.mjs +4 -1
- package/src/commands/blog.mjs +60 -0
- package/src/commands/build-theme.import-path.test.mjs +2 -12
- package/src/commands/build-theme.mjs +117 -0
- package/src/commands/build-theme.prose.test.mjs +5 -15
- package/src/commands/build-theme.watch.test.mjs +149 -0
- package/src/commands/ensure-core-built.mjs +120 -0
- package/src/index.mjs +1 -0
- package/src/lib/error-codes.mjs +8 -0
- package/src/lib/site.mjs +20 -0
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
- package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
- package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
- package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
- package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
- package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
- package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
- package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
- package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
- package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
- package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
- package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
- package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
- package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
- package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
- package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
- package/templates/pages/kanban-board/page.tsx +729 -0
- package/templates/pages/kanban-board/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +0 -1
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: migrate `declare module "@xds/..."` augmentations to
|
|
5
|
+
* `@astryxdesign/...`
|
|
6
|
+
*
|
|
7
|
+
* The v0.1.0 release moved the public package scope from @xds to
|
|
8
|
+
* @astryxdesign. TypeScript module augmentations (`declare module "@xds/core"
|
|
9
|
+
* { ... }`) reference the package by its string specifier, so they are dead
|
|
10
|
+
* after the scope rename: TypeScript resolves the augmentation against a
|
|
11
|
+
* module that no longer exists.
|
|
12
|
+
*
|
|
13
|
+
* This transform rewrites the augmented module string on `TSModuleDeclaration`
|
|
14
|
+
* nodes whose `id` is a StringLiteral matching a renamed @xds package (or one
|
|
15
|
+
* of its subpaths). It only touches the module path — identifiers *inside* the
|
|
16
|
+
* augmentation (e.g. an augmented interface name) are a separate concern
|
|
17
|
+
* handled by drop-xds-prefix-imports.
|
|
18
|
+
*
|
|
19
|
+
* Ordered AFTER migrate-xds-module-specifiers in the v0.1.0 manifest: it uses
|
|
20
|
+
* the same PACKAGE_RENAMES mapping and cleans up the module-augmentation
|
|
21
|
+
* surface alongside the import/export/require specifier rename.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const meta = {
|
|
25
|
+
title: 'Migrate declare-module augmentations from @xds/* to @astryxdesign/*',
|
|
26
|
+
description:
|
|
27
|
+
'Rewrites the module specifier on TypeScript `declare module "@xds/..."` ' +
|
|
28
|
+
'augmentations to the @astryxdesign/* packages used by Astryx v0.1.0. ' +
|
|
29
|
+
'Only the augmented module path is changed; identifiers inside the ' +
|
|
30
|
+
'augmentation are left untouched.',
|
|
31
|
+
pr: '#3092',
|
|
32
|
+
fileExtensions: ['.ts', '.d.ts', '.tsx'],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const PACKAGE_RENAMES = new Map([
|
|
36
|
+
['@xds/build', '@astryxdesign/build'],
|
|
37
|
+
['@xds/cli', '@astryxdesign/cli'],
|
|
38
|
+
['@xds/core', '@astryxdesign/core'],
|
|
39
|
+
['@xds/lab', '@astryxdesign/lab'],
|
|
40
|
+
['@xds/theme-butter', '@astryxdesign/theme-butter'],
|
|
41
|
+
['@xds/theme-chocolate', '@astryxdesign/theme-chocolate'],
|
|
42
|
+
['@xds/theme-daily', '@astryxdesign/theme-neutral'],
|
|
43
|
+
['@xds/theme-default', '@astryxdesign/theme-neutral'],
|
|
44
|
+
['@xds/theme-gothic', '@astryxdesign/theme-gothic'],
|
|
45
|
+
['@xds/theme-matcha', '@astryxdesign/theme-matcha'],
|
|
46
|
+
['@xds/theme-neutral', '@astryxdesign/theme-neutral'],
|
|
47
|
+
['@xds/theme-stone', '@astryxdesign/theme-stone'],
|
|
48
|
+
['@xds/theme-y2k', '@astryxdesign/theme-y2k'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function renamePackageSpecifier(value) {
|
|
52
|
+
if (typeof value !== 'string') return value;
|
|
53
|
+
for (const [from, to] of PACKAGE_RENAMES) {
|
|
54
|
+
if (value === from) return to;
|
|
55
|
+
if (value.startsWith(from + '/')) return to + value.slice(from.length);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default function transformer(file, api) {
|
|
61
|
+
const j = api.jscodeshift;
|
|
62
|
+
const root = j(file.source);
|
|
63
|
+
let hasChanges = false;
|
|
64
|
+
|
|
65
|
+
root.find(j.TSModuleDeclaration).forEach(path => {
|
|
66
|
+
const id = path.node.id;
|
|
67
|
+
// Module augmentations name the module with a string literal id; a
|
|
68
|
+
// `namespace Foo {}` uses an Identifier id, which we ignore.
|
|
69
|
+
if (!id || (id.type !== 'StringLiteral' && id.type !== 'Literal')) return;
|
|
70
|
+
if (typeof id.value !== 'string') return;
|
|
71
|
+
const next = renamePackageSpecifier(id.value);
|
|
72
|
+
if (next === id.value) return;
|
|
73
|
+
id.value = next;
|
|
74
|
+
hasChanges = true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return hasChanges ? root.toSource() : undefined;
|
|
78
|
+
}
|
|
@@ -180,6 +180,9 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
|
|
|
180
180
|
// Rules — the top error-preventers.
|
|
181
181
|
lines.push('RULES:');
|
|
182
182
|
lines.push('- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.');
|
|
183
|
+
lines.push('- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (`astryx docs layout`).');
|
|
184
|
+
lines.push('- Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only.');
|
|
185
|
+
lines.push('- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration.');
|
|
183
186
|
// Styling guidance tailored to the project's configured system — never
|
|
184
187
|
// recommend a path that isn't compiled here (xstyle needs the StyleX compiler;
|
|
185
188
|
// utilities need Tailwind). Tokens are always the source of truth.
|
|
@@ -207,7 +210,7 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
|
|
|
207
210
|
.sort();
|
|
208
211
|
if (topics.length > 0) lines.push(` docs <topic> ${topics.join(', ')}`);
|
|
209
212
|
}
|
|
210
|
-
lines.push(' swizzle <Name> eject component source
|
|
213
|
+
lines.push(' swizzle <Name> eject component source for deep customization');
|
|
211
214
|
lines.push(' upgrade --apply run after any @astryxdesign/core bump');
|
|
212
215
|
lines.push(MARKER_END);
|
|
213
216
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file blog command (hidden) — read the Astryx blog from the published feed.
|
|
5
|
+
*
|
|
6
|
+
* Hidden on purpose: it's not part of the documented command surface and does
|
|
7
|
+
* not appear in --help or the manifest. It reads the blog the same way any
|
|
8
|
+
* feed reader would — over the public RSS feed — and prints a post's plaintext
|
|
9
|
+
* (.txt) variant. Nothing about the blog's structure has to change for this to
|
|
10
|
+
* work; the CLI is just a consumer of the feed.
|
|
11
|
+
*
|
|
12
|
+
* astryx blog List posts from the feed
|
|
13
|
+
* astryx blog <slug> Print a post as plaintext
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
17
|
+
import {humanLog} from '../lib/json.mjs';
|
|
18
|
+
import {cliError} from '../lib/cli-error.mjs';
|
|
19
|
+
import {blog as blogApi} from '../api/blog.mjs';
|
|
20
|
+
|
|
21
|
+
function formatList({feedUrl, posts}, run) {
|
|
22
|
+
const lines = [`\nAstryx blog · feed: ${feedUrl}\n`];
|
|
23
|
+
if (posts.length === 0) {
|
|
24
|
+
lines.push('No posts found in the feed.');
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
27
|
+
for (const p of posts) {
|
|
28
|
+
lines.push(` ${p.slug}`);
|
|
29
|
+
lines.push(` ${p.title}`);
|
|
30
|
+
if (p.type) lines.push(` ${p.type}`);
|
|
31
|
+
if (p.textUrl) lines.push(` ${p.textUrl}`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
}
|
|
34
|
+
lines.push(`Read one: ${run} astryx blog <slug>`);
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerBlog(program) {
|
|
39
|
+
program
|
|
40
|
+
.command('blog [slug]', {hidden: true})
|
|
41
|
+
.description('Read the Astryx blog from the published feed')
|
|
42
|
+
.action(async slug => {
|
|
43
|
+
const run = getRunPrefix();
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
result = await blogApi(slug);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
cliError(e.message, {suggestions: e.suggestions || [], code: e.code});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (result.type === 'blog.list') {
|
|
53
|
+
humanLog(formatList(result.data, run));
|
|
54
|
+
} else {
|
|
55
|
+
// blog.detail — print the feed URL, then the plaintext body.
|
|
56
|
+
humanLog(`Feed: ${result.data.feedUrl}\n`);
|
|
57
|
+
humanLog(result.data.text);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -18,14 +18,10 @@ import * as fs from 'node:fs';
|
|
|
18
18
|
import * as path from 'node:path';
|
|
19
19
|
import * as os from 'node:os';
|
|
20
20
|
import {fileURLToPath} from 'node:url';
|
|
21
|
+
import {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
21
22
|
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const CLI_BIN = path.resolve(__dirname, '../../bin/astryx.mjs');
|
|
24
|
-
const REPO_ROOT = path.resolve(__dirname, '../../../..');
|
|
25
|
-
const CORE_THEME_ENTRY = path.join(
|
|
26
|
-
REPO_ROOT,
|
|
27
|
-
'packages/core/dist/theme/index.js',
|
|
28
|
-
);
|
|
29
25
|
|
|
30
26
|
function runCli(args, cwd) {
|
|
31
27
|
try {
|
|
@@ -59,13 +55,7 @@ function writeTheme(dir, name) {
|
|
|
59
55
|
// in-CLI fallback generator). Build core once if it isn't already present so
|
|
60
56
|
// the suite works in any CI job, regardless of job ordering.
|
|
61
57
|
beforeAll(() => {
|
|
62
|
-
|
|
63
|
-
execFileSync('pnpm', ['-F', '@astryxdesign/core', 'build'], {
|
|
64
|
-
cwd: REPO_ROOT,
|
|
65
|
-
stdio: 'pipe',
|
|
66
|
-
timeout: 180_000,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
58
|
+
ensureCoreBuilt();
|
|
69
59
|
}, 200_000);
|
|
70
60
|
|
|
71
61
|
let tmpDir;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import {pathToFileURL, fileURLToPath} from 'node:url';
|
|
18
|
+
import {spawn} from 'node:child_process';
|
|
18
19
|
import {createJiti} from 'jiti';
|
|
19
20
|
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
20
21
|
import {
|
|
@@ -597,6 +598,104 @@ function validatePrivateVars(themeDef) {
|
|
|
597
598
|
return errors;
|
|
598
599
|
}
|
|
599
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Path to this CLI's real entry (bin/astryx.mjs), resolved from this module's
|
|
603
|
+
* location (src/commands/build-theme.mjs → ../../bin/astryx.mjs). Used to
|
|
604
|
+
* re-invoke `theme build` as a child process in watch mode.
|
|
605
|
+
*/
|
|
606
|
+
function resolveCliBin() {
|
|
607
|
+
const commandsDir = path.dirname(fileURLToPath(import.meta.url));
|
|
608
|
+
return path.resolve(commandsDir, '../../bin/astryx.mjs');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Run a single `theme build` as a child process, reusing the exact
|
|
613
|
+
* single-build code path (and its error handling) rather than duplicating it.
|
|
614
|
+
* Resolves with the child's exit code; never rejects.
|
|
615
|
+
*
|
|
616
|
+
* @param {string} file - The theme file argument, as the user passed it.
|
|
617
|
+
* @param {object} options - Parsed command options (only `out` is forwarded).
|
|
618
|
+
* @returns {Promise<number>}
|
|
619
|
+
*/
|
|
620
|
+
function runThemeBuildOnceChild(file, options) {
|
|
621
|
+
const cliBin = resolveCliBin();
|
|
622
|
+
const args = [cliBin, 'theme', 'build', file];
|
|
623
|
+
if (options.out) args.push('--out', options.out);
|
|
624
|
+
return new Promise(resolve => {
|
|
625
|
+
const child = spawn(process.execPath, args, {
|
|
626
|
+
stdio: 'inherit',
|
|
627
|
+
env: process.env,
|
|
628
|
+
});
|
|
629
|
+
child.on('close', code => resolve(code ?? 0));
|
|
630
|
+
child.on('error', () => resolve(1));
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Watch a theme file and rebuild on change. Runs an initial build, then
|
|
636
|
+
* rebuilds (debounced) whenever the file changes, until interrupted with
|
|
637
|
+
* Ctrl-C. Each rebuild runs in a child process so a build error (which the
|
|
638
|
+
* single-build path reports via a hard exit) is contained and the watcher
|
|
639
|
+
* keeps running.
|
|
640
|
+
*
|
|
641
|
+
* @param {string} file - The theme file argument, as the user passed it.
|
|
642
|
+
* @param {string} filePath - Absolute path to the theme file.
|
|
643
|
+
* @param {object} options - Parsed command options.
|
|
644
|
+
* @returns {Promise<void>} Resolves when the watcher is stopped (Ctrl-C).
|
|
645
|
+
*/
|
|
646
|
+
async function runThemeBuildWatch(file, filePath, options) {
|
|
647
|
+
const rel = path.relative(process.cwd(), filePath);
|
|
648
|
+
|
|
649
|
+
// Initial build.
|
|
650
|
+
await runThemeBuildOnceChild(file, options);
|
|
651
|
+
|
|
652
|
+
humanLog(`\n👀 Watching ${rel} for changes — press Ctrl-C to stop.`);
|
|
653
|
+
|
|
654
|
+
let building = false;
|
|
655
|
+
let queued = false;
|
|
656
|
+
let debounce = null;
|
|
657
|
+
|
|
658
|
+
const rebuild = async () => {
|
|
659
|
+
if (building) {
|
|
660
|
+
// Coalesce changes that land mid-build into a single follow-up run.
|
|
661
|
+
queued = true;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
building = true;
|
|
665
|
+
humanLog(`\n♻️ Change detected — rebuilding ${rel}...`);
|
|
666
|
+
await runThemeBuildOnceChild(file, options);
|
|
667
|
+
building = false;
|
|
668
|
+
humanLog(`\n👀 Watching ${rel} for changes — press Ctrl-C to stop.`);
|
|
669
|
+
if (queued) {
|
|
670
|
+
queued = false;
|
|
671
|
+
rebuild();
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// Some editors replace the file (rename) rather than writing in place, which
|
|
676
|
+
// can drop the watch. Watch the containing directory and filter to our file
|
|
677
|
+
// so edits survive atomic-save/rename.
|
|
678
|
+
const watchDir = path.dirname(filePath);
|
|
679
|
+
const baseName = path.basename(filePath);
|
|
680
|
+
const watcher = fs.watch(watchDir, (_eventType, changed) => {
|
|
681
|
+
if (changed && changed !== baseName) return;
|
|
682
|
+
clearTimeout(debounce);
|
|
683
|
+
// Debounce: editors often emit several events per save.
|
|
684
|
+
debounce = setTimeout(rebuild, 100);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
await new Promise(resolve => {
|
|
688
|
+
const stop = () => {
|
|
689
|
+
clearTimeout(debounce);
|
|
690
|
+
watcher.close();
|
|
691
|
+
humanLog('\nStopped watching.');
|
|
692
|
+
resolve();
|
|
693
|
+
};
|
|
694
|
+
process.once('SIGINT', stop);
|
|
695
|
+
process.once('SIGTERM', stop);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
600
699
|
export function registerTheme(program) {
|
|
601
700
|
const theme = program
|
|
602
701
|
.command('theme')
|
|
@@ -621,6 +720,10 @@ export function registerTheme(program) {
|
|
|
621
720
|
.command('build <file>')
|
|
622
721
|
.description('Compile a defineTheme file to CSS + JS')
|
|
623
722
|
.option('-o, --out <path>', 'Output CSS file path')
|
|
723
|
+
.option(
|
|
724
|
+
'-w, --watch',
|
|
725
|
+
'Rebuild automatically when the theme file changes (Ctrl-C to stop)',
|
|
726
|
+
)
|
|
624
727
|
.action(async (file, options) => {
|
|
625
728
|
const filePath = path.resolve(process.cwd(), file);
|
|
626
729
|
const json = program.opts().json || false;
|
|
@@ -630,6 +733,20 @@ export function registerTheme(program) {
|
|
|
630
733
|
return;
|
|
631
734
|
}
|
|
632
735
|
|
|
736
|
+
// Watch mode: run an initial build, then rebuild on every change to the
|
|
737
|
+
// theme file. Watch is a human-interactive, long-running mode — it is not
|
|
738
|
+
// supported in --json (machine) mode, which expects a single envelope.
|
|
739
|
+
if (options.watch) {
|
|
740
|
+
if (json) {
|
|
741
|
+
cliError('--watch is not supported with --json', {
|
|
742
|
+
code: ERROR_CODES.ERR_THEME_INVALID,
|
|
743
|
+
});
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
await runThemeBuildWatch(file, filePath, options);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
633
750
|
if (!json) humanLog(`\nBuilding theme from ${path.relative(process.cwd(), filePath)}...`);
|
|
634
751
|
|
|
635
752
|
// Extract theme definition
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* - paragraphs use the body font, not the heading font.
|
|
19
19
|
*
|
|
20
20
|
* Building `astryx theme build` requires a compiled @astryxdesign/core (there is no in-CLI
|
|
21
|
-
* fallback generator), so this suite builds core once in beforeAll
|
|
22
|
-
*
|
|
23
|
-
* ordering.
|
|
21
|
+
* fallback generator), so this suite builds core once in beforeAll via the
|
|
22
|
+
* shared ensureCoreBuilt() helper — which serializes concurrent Vitest workers
|
|
23
|
+
* behind a lock — to stay self-sufficient regardless of CI job ordering.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
|
|
@@ -29,14 +29,10 @@ import * as fs from 'node:fs';
|
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
import * as os from 'node:os';
|
|
31
31
|
import {fileURLToPath} from 'node:url';
|
|
32
|
+
import {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
32
33
|
|
|
33
34
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
35
|
const CLI_BIN = path.resolve(__dirname, '../../bin/astryx.mjs');
|
|
35
|
-
const REPO_ROOT = path.resolve(__dirname, '../../../..');
|
|
36
|
-
const CORE_THEME_ENTRY = path.join(
|
|
37
|
-
REPO_ROOT,
|
|
38
|
-
'packages/core/dist/theme/index.js',
|
|
39
|
-
);
|
|
40
36
|
|
|
41
37
|
function runCli(args, cwd) {
|
|
42
38
|
try {
|
|
@@ -71,13 +67,7 @@ function writeTheme(dir, name) {
|
|
|
71
67
|
// `astryx theme build` imports the compiled @astryxdesign/core/theme entry. Build core
|
|
72
68
|
// once if it isn't already present so the suite works in any CI job.
|
|
73
69
|
beforeAll(() => {
|
|
74
|
-
|
|
75
|
-
execFileSync('pnpm', ['-F', '@astryxdesign/core', 'build'], {
|
|
76
|
-
cwd: REPO_ROOT,
|
|
77
|
-
stdio: 'pipe',
|
|
78
|
-
timeout: 180_000,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
70
|
+
ensureCoreBuilt();
|
|
81
71
|
}, 200_000);
|
|
82
72
|
|
|
83
73
|
let tmpDir;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Tests for `astryx theme build --watch` (#3375).
|
|
5
|
+
*
|
|
6
|
+
* Watch mode runs an initial build, then rebuilds whenever the theme file
|
|
7
|
+
* changes, until interrupted. Each rebuild runs in a child process so a build
|
|
8
|
+
* error (reported by the single-build path via a hard exit) is contained and
|
|
9
|
+
* the watcher keeps running.
|
|
10
|
+
*
|
|
11
|
+
* Building `astryx theme build` requires a compiled @astryxdesign/core, so this
|
|
12
|
+
* suite builds core once in beforeAll via the shared ensureCoreBuilt() helper —
|
|
13
|
+
* which serializes concurrent Vitest workers behind a lock — to stay
|
|
14
|
+
* self-sufficient regardless of CI job ordering.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
|
|
18
|
+
import {execFileSync, spawn} from 'node:child_process';
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import * as os from 'node:os';
|
|
22
|
+
import {fileURLToPath} from 'node:url';
|
|
23
|
+
import {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const CLI_BIN = path.resolve(__dirname, '../../bin/astryx.mjs');
|
|
27
|
+
|
|
28
|
+
function runCli(args, cwd) {
|
|
29
|
+
try {
|
|
30
|
+
const out = execFileSync('node', [CLI_BIN, ...args], {
|
|
31
|
+
cwd,
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
|
+
env: {...process.env, FORCE_COLOR: '0'},
|
|
35
|
+
});
|
|
36
|
+
return {code: 0, stdout: out, stderr: ''};
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return {
|
|
39
|
+
code: e.status ?? 1,
|
|
40
|
+
stdout: e.stdout?.toString() ?? '',
|
|
41
|
+
stderr: e.stderr?.toString() ?? '',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Poll until `predicate()` is true or the timeout elapses. */
|
|
47
|
+
async function waitFor(predicate, {timeout = 8000, interval = 50} = {}) {
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (predicate()) return true;
|
|
51
|
+
if (Date.now() - start > timeout) return false;
|
|
52
|
+
await new Promise(r => setTimeout(r, interval));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeAll(() => {
|
|
57
|
+
ensureCoreBuilt();
|
|
58
|
+
}, 200_000);
|
|
59
|
+
|
|
60
|
+
let tmpDir;
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-watch-'));
|
|
63
|
+
});
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('theme build --watch', () => {
|
|
69
|
+
it('advertises the flag in --help', () => {
|
|
70
|
+
const result = runCli(['theme', 'build', '--help'], process.cwd());
|
|
71
|
+
const out = result.stdout + result.stderr;
|
|
72
|
+
expect(out).toMatch(/--watch/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects --watch together with --json (single-envelope contract)', () => {
|
|
76
|
+
const themeFile = path.join(tmpDir, 'wt.mjs');
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
themeFile,
|
|
79
|
+
`export default { name: 'wt', tokens: { '--color-bg': '#fff' } };\n`,
|
|
80
|
+
);
|
|
81
|
+
const result = runCli(
|
|
82
|
+
['--json', 'theme', 'build', path.relative(tmpDir, themeFile), '--watch'],
|
|
83
|
+
tmpDir,
|
|
84
|
+
);
|
|
85
|
+
expect(result.code).not.toBe(0);
|
|
86
|
+
expect(result.stdout + result.stderr).toMatch(/watch/i);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('builds initially, rebuilds on change, and stops cleanly on SIGINT', async () => {
|
|
90
|
+
const themeFile = path.join(tmpDir, 'wt.mjs');
|
|
91
|
+
const cssFile = path.join(tmpDir, 'wt.css');
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
themeFile,
|
|
94
|
+
`export default { name: 'wt', tokens: { '--color-bg': '#ffffff' } };\n`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const child = spawn(
|
|
98
|
+
process.execPath,
|
|
99
|
+
[CLI_BIN, 'theme', 'build', 'wt.mjs', '--watch'],
|
|
100
|
+
{cwd: tmpDir, env: {...process.env, FORCE_COLOR: '0'}},
|
|
101
|
+
);
|
|
102
|
+
let stdout = '';
|
|
103
|
+
child.stdout.on('data', d => (stdout += d.toString()));
|
|
104
|
+
child.stderr.on('data', d => (stdout += d.toString()));
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Initial build produces the CSS.
|
|
108
|
+
const built = await waitFor(() => fs.existsSync(cssFile));
|
|
109
|
+
expect(built).toBe(true);
|
|
110
|
+
const firstCss = fs.readFileSync(cssFile, 'utf-8');
|
|
111
|
+
expect(firstCss).toMatch(/#ffffff/);
|
|
112
|
+
|
|
113
|
+
// Wait until the watcher is actually watching before editing, so the
|
|
114
|
+
// change isn't missed.
|
|
115
|
+
await waitFor(() => /Watching/i.test(stdout));
|
|
116
|
+
|
|
117
|
+
// Change the theme — the token value changes so the CSS must change.
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
themeFile,
|
|
120
|
+
`export default { name: 'wt', tokens: { '--color-bg': '#010203' } };\n`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// The rebuilt CSS should reflect the new value.
|
|
124
|
+
const rebuilt = await waitFor(() => {
|
|
125
|
+
try {
|
|
126
|
+
return fs.readFileSync(cssFile, 'utf-8').includes('#010203');
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
expect(rebuilt).toBe(true);
|
|
132
|
+
expect(stdout).toMatch(/rebuild/i);
|
|
133
|
+
} finally {
|
|
134
|
+
// SIGINT must stop the watcher and exit cleanly.
|
|
135
|
+
child.kill('SIGINT');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const exited = await new Promise(resolve => {
|
|
139
|
+
let done = false;
|
|
140
|
+
child.on('exit', () => {
|
|
141
|
+
done = true;
|
|
142
|
+
resolve(true);
|
|
143
|
+
});
|
|
144
|
+
setTimeout(() => resolve(done), 4000);
|
|
145
|
+
});
|
|
146
|
+
expect(exited).toBe(true);
|
|
147
|
+
expect(stdout).toMatch(/Stopped watching/);
|
|
148
|
+
}, 30_000);
|
|
149
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Shared test helper: build @astryxdesign/core once, race-free.
|
|
5
|
+
*
|
|
6
|
+
* `astryx theme build` imports the compiled @astryxdesign/core/theme entry
|
|
7
|
+
* (there is no in-CLI fallback generator), so any test exercising it needs a
|
|
8
|
+
* built core. The CI `test` job runs `pnpm test` without a prior core build,
|
|
9
|
+
* and Vitest runs test files in parallel worker forks. When two build-theme
|
|
10
|
+
* suites each ran `if (!exists) pnpm -F @astryxdesign/core build` in their own
|
|
11
|
+
* beforeAll, both workers saw dist missing and launched concurrent builds that
|
|
12
|
+
* collided on the shared packages/core/dist (core's build starts with
|
|
13
|
+
* `rimraf dist`): one worker wiped dist while the other was mid-write, failing
|
|
14
|
+
* nondeterministically ("Could not resolve dist/index.js" / "ENOTEMPTY rmdir
|
|
15
|
+
* dist/hooks").
|
|
16
|
+
*
|
|
17
|
+
* This serializes the build behind a filesystem lock so exactly one worker
|
|
18
|
+
* builds and the rest wait for it to finish before reading dist.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {execFileSync} from 'node:child_process';
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import * as os from 'node:os';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import {fileURLToPath} from 'node:url';
|
|
26
|
+
|
|
27
|
+
const REPO_ROOT = path.resolve(
|
|
28
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
29
|
+
'../../../..',
|
|
30
|
+
);
|
|
31
|
+
const CORE_THEME_ENTRY = path.join(
|
|
32
|
+
REPO_ROOT,
|
|
33
|
+
'packages/core/dist/theme/index.js',
|
|
34
|
+
);
|
|
35
|
+
// A lock directory (mkdir is atomic across processes) guards the build.
|
|
36
|
+
const LOCK_DIR = path.join(os.tmpdir(), 'astryx-core-build.lock');
|
|
37
|
+
|
|
38
|
+
const BUILD_TIMEOUT_MS = 180_000;
|
|
39
|
+
// A lock older than this is assumed abandoned by a crashed/killed worker.
|
|
40
|
+
const STALE_LOCK_MS = BUILD_TIMEOUT_MS + 20_000;
|
|
41
|
+
const POLL_MS = 250;
|
|
42
|
+
|
|
43
|
+
function sleepSync(ms) {
|
|
44
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildCore() {
|
|
48
|
+
execFileSync('pnpm', ['-F', '@astryxdesign/core', 'build'], {
|
|
49
|
+
cwd: REPO_ROOT,
|
|
50
|
+
stdio: 'pipe',
|
|
51
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lockIsStale() {
|
|
56
|
+
try {
|
|
57
|
+
return Date.now() - fs.statSync(LOCK_DIR).mtimeMs > STALE_LOCK_MS;
|
|
58
|
+
} catch {
|
|
59
|
+
// Vanished between the exists check and stat — treat as released.
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Try to acquire the lock. Returns true if this worker now holds it. */
|
|
65
|
+
function tryAcquire() {
|
|
66
|
+
try {
|
|
67
|
+
fs.mkdirSync(LOCK_DIR);
|
|
68
|
+
return true;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.code !== 'EEXIST') {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
if (lockIsStale()) {
|
|
74
|
+
fs.rmSync(LOCK_DIR, {recursive: true, force: true});
|
|
75
|
+
try {
|
|
76
|
+
fs.mkdirSync(LOCK_DIR);
|
|
77
|
+
return true;
|
|
78
|
+
} catch (retryErr) {
|
|
79
|
+
if (retryErr.code !== 'EEXIST') {
|
|
80
|
+
throw retryErr;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Ensure packages/core/dist is built exactly once, even when called
|
|
90
|
+
* concurrently from parallel Vitest workers. Safe to call from every
|
|
91
|
+
* build-theme suite's beforeAll.
|
|
92
|
+
*/
|
|
93
|
+
export function ensureCoreBuilt() {
|
|
94
|
+
if (fs.existsSync(CORE_THEME_ENTRY)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const deadline = Date.now() + STALE_LOCK_MS;
|
|
99
|
+
while (Date.now() < deadline) {
|
|
100
|
+
if (fs.existsSync(CORE_THEME_ENTRY)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (tryAcquire()) {
|
|
104
|
+
try {
|
|
105
|
+
if (!fs.existsSync(CORE_THEME_ENTRY)) {
|
|
106
|
+
buildCore();
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
fs.rmSync(LOCK_DIR, {recursive: true, force: true});
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Another worker is building; wait for it to finish and release the lock.
|
|
114
|
+
sleepSync(POLL_MS);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Waited past the stale threshold without the artifact appearing — build it
|
|
118
|
+
// ourselves rather than let the suite fail on a missing entry.
|
|
119
|
+
buildCore();
|
|
120
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -245,6 +245,7 @@ const commands = [
|
|
|
245
245
|
{name: 'init', path: './commands/init.mjs', register: 'registerInit'},
|
|
246
246
|
{name: 'component', path: './commands/component/index.mjs', register: 'registerComponent'},
|
|
247
247
|
{name: 'docs', path: './commands/docs.mjs', register: 'registerDocs'},
|
|
248
|
+
{name: 'blog', path: './commands/blog.mjs', register: 'registerBlog'},
|
|
248
249
|
{name: 'swizzle', path: './commands/swizzle.mjs', register: 'registerSwizzle'},
|
|
249
250
|
// agent-docs folded into init — functions still importable from agent-docs.mjs
|
|
250
251
|
{name: 'template', path: './commands/template.mjs', register: 'registerTemplate'},
|
package/src/lib/error-codes.mjs
CHANGED
|
@@ -75,6 +75,8 @@
|
|
|
75
75
|
* | 'ERR_INVALID_VERSION'
|
|
76
76
|
* | 'ERR_DEP_MISSING'
|
|
77
77
|
* | 'ERR_GH_CLI'
|
|
78
|
+
* | 'ERR_UNKNOWN_POST'
|
|
79
|
+
* | 'ERR_FETCH_FAILED'
|
|
78
80
|
* | 'ERR_LAYOUT_PARSE'
|
|
79
81
|
* | 'ERR_LAYOUT_INVALID'
|
|
80
82
|
* )} ErrorCode
|
|
@@ -181,6 +183,12 @@ export const ERROR_CODES = Object.freeze({
|
|
|
181
183
|
/** GitHub CLI (`gh`) is not installed or not authenticated. */
|
|
182
184
|
ERR_GH_CLI: 'ERR_GH_CLI',
|
|
183
185
|
|
|
186
|
+
// ── Blog (read via the published RSS feed) ───────────────────────
|
|
187
|
+
/** No blog post matched the requested slug in the feed. */
|
|
188
|
+
ERR_UNKNOWN_POST: 'ERR_UNKNOWN_POST',
|
|
189
|
+
/** A network fetch (RSS feed or post text) failed. */
|
|
190
|
+
ERR_FETCH_FAILED: 'ERR_FETCH_FAILED',
|
|
191
|
+
|
|
184
192
|
// ── Layout expressions (XLE/XLO) ─────────────────────────────────
|
|
185
193
|
/** A layout expression failed to parse (syntax error, with line/col). */
|
|
186
194
|
ERR_LAYOUT_PARSE: 'ERR_LAYOUT_PARSE',
|