@astryxdesign/cli 0.4.3 → 0.4.4-canary.2022d94
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 +40 -0
- package/README.md +45 -43
- package/api/discover/_adapter.d.mts +5 -3
- package/api/discover/_adapter.mjs +6 -4
- package/api/template/cdn/cdn.d.mts +23 -0
- package/api/template/cdn/cdn.mjs +86 -0
- package/api/template/cdn/cdn.test.mjs +108 -0
- package/api/template/template.d.mts +2 -0
- package/api/template/template.doc.mjs +21 -3
- package/api/template/template.mjs +16 -2
- package/api/template/template.type.d.mts +19 -0
- package/api/template/template.type.mjs +12 -0
- package/api/theme/theme.type.d.mts +16 -0
- package/api/theme/theme.type.mjs +11 -0
- package/assets/cdn.template.html +124 -0
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +5 -4
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.doc.mjs +21 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.tsx +46 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.doc.mjs +23 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.tsx +100 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.tsx +47 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.tsx +51 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.doc.mjs +26 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.tsx +221 -0
- package/assets/theme.template.ts +4 -3
- package/authoring/doctypes/base/type.ts +9 -0
- package/clients/cli/commands/build-theme.mjs +154 -65
- package/clients/cli/commands/build-theme.multi.test.mjs +148 -0
- package/clients/cli/commands/build-theme.watch.test.mjs +67 -0
- package/clients/cli/commands/discover.broken-integration.test.mjs +112 -0
- package/clients/cli/commands/discover.mjs +12 -0
- package/clients/cli/commands/search.mjs +9 -0
- package/clients/cli/commands/template-cdn.behavior.test.mjs +113 -0
- package/clients/cli/commands/template.doc.mjs +12 -2
- package/clients/cli/commands/template.mjs +24 -3
- package/clients/cli/commands/theme-build.doc.mjs +11 -5
- package/clients/cli/lib/json-shim.test.mjs +1 -1
- package/clients/cli/lib/manifest.mjs +3 -2
- package/clients/cli/lib/manifest.test.mjs +4 -2
- package/foundation/integrations/integration-warnings.test.mjs +17 -0
- package/foundation/integrations/validate-contributions.mjs +7 -0
- package/foundation/response/response-types.doc.mjs +11 -0
- package/foundation/text/copyright-header.mjs +11 -4
- package/package.json +12 -9
- package/assets/templates/pages/table-page-chart/page.tsx +0 -577
- package/assets/templates/pages/table-page-chart/template.doc.mjs +0 -13
- package/assets/templates/pages/table-page-heatmap-status/page.tsx +0 -467
- package/assets/templates/pages/table-page-heatmap-status/template.doc.mjs +0 -13
- package/assets/templates/pages/table-page-shoe-store-heatmap/page.tsx +0 -931
- package/assets/templates/pages/table-page-shoe-store-heatmap/template.doc.mjs +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @xds/cli
|
|
2
2
|
|
|
3
|
+
# 0.4.4
|
|
4
|
+
|
|
5
|
+
#### New Components
|
|
6
|
+
|
|
7
|
+
- Promote `BottomSheet` and `BottomSheetSwitcher` from the canary-only Lab package to Core. The stable package now includes their existing native-dialog, drag-detent, transition, and mobile-keyboard behavior, plus Core documentation and examples (#5080).
|
|
8
|
+
|
|
9
|
+
#### New Features
|
|
10
|
+
|
|
11
|
+
- `astryx template --cdn` writes a working no-build-step CDN starter page (#5068).
|
|
12
|
+
A CDN starter is a template, so it joins the template family beside `--skeleton` rather than claiming a top-level command. It is a flag and not the positional `astryx template cdn` because the positional resolves against everything `discoverAll()` finds, where a `cdn` id would shadow a discovered template. `cdn.template.html` loads Astryx from jsDelivr and esm.sh with no bundler, no install and no build step, with every CDN URL pinned to the Astryx version you have installed — an unpinned CDN URL resolves to whatever is latest and is cached hard, so a page written today breaks tomorrow without being edited. An existing file is never clobbered; `--overwrite` replaces it, and `--json` returns the receipt.
|
|
13
|
+
|
|
14
|
+
The annotations are the things that are load-bearing and silent when missing: `?external=react,react-dom` (without it esm.sh bundles a second React and every hook throws `Cannot read properties of null (reading 'useState')`), `react/jsx-runtime` in the import map (the published bundle imports it; omitting it fails the page with `Failed to resolve module specifier`), and a `font-family` on `body` (nothing in the stylesheets sets a document font, so `Button` — which is `font: inherit` — otherwise renders its label in the browser's default serif).
|
|
15
|
+
|
|
16
|
+
Three more lessons came out of building a real app on it. The page now `<link>`s the theme's webfont from Google Fonts, because the theme _names_ Figtree and never loads it, so every viewer silently got the fallback stack (#5015 again). It imports the theme OBJECT and wraps in `<Theme theme={neutralTheme} mode="system">`, so light and dark follow the OS — the `data-astryx-theme` attribute alone scopes the stylesheet but cannot switch modes. And `#root:empty` carries a "Loading…" state, because ESM-from-CDN has real latency and a blank page reads as broken. Markup is `htm`, with a comment saying it is optional and `createElement` is the dependency-free alternative.
|
|
17
|
+
|
|
18
|
+
A recipe that is only read is a recipe that is only assumed to work, so CI renders it: `.github/scripts/cdn-template-smoke-test.mjs` scaffolds the page with the real CLI and opens it in headless Chromium, failing on any console error, page error or failed request, and on a page that loads without rendering.
|
|
19
|
+
- `astryx theme build` takes any number of theme files — `astryx theme build themes/*.ts` compiles them all in one process, so an app with several themes no longer hand-rolls a loop that re-enters the CLI once per theme. Outputs are byte-identical to the serial invocations; the run stops at the first failure and names the theme that failed. The CLI's Node floor (>=22.13) is now declared in `engines`, so a package manager can enforce it at install instead of the build failing later (#5121).
|
|
20
|
+
- `defineTheme`: `color.accent` accepts a `[light, dark]` tuple (#2279)
|
|
21
|
+
`ColorScaleConfig.accent` now takes either a single hex or a `[light, dark]` tuple, matching `TokenValue`. With a tuple, `expandColorScale` derives the light half of every generated `light-dark()` pair from the light seed's palettes and the dark half from the dark seed's, so each scheme gets a consistent derived palette (muted, on-accent, neutrals) instead of the `tokens['--color-accent']` workaround that skips scale generation. Single-string configs are unchanged, token for token. Also documents the precedence between `color` and `tokens` for accent-derived values: `tokens` entries win token by token, the `var(--color-accent)` reference tokens follow a `--color-accent` override at runtime, and the baked `--color-on-accent` stays derived from the `color.accent` seed.
|
|
22
|
+
|
|
23
|
+
#### Fixes
|
|
24
|
+
|
|
25
|
+
- Bottom Sheet showcase block: the filter checkboxes are interactive again (#5157).
|
|
26
|
+
`CheckboxInput` is fully controlled — `value` is required and the input only moves when the owner updates it. The showcase passed a literal `value={false}` with no `onChange`, so the three filters ("In stock", "On sale", "Free shipping") rendered but could never be toggled: on the docs site the first thing a reader tries in a Bottom Sheet does nothing, and anyone copying the block inherits three dead controls. Each filter now has its own `useState` and `onChange`, matching the checkbox wiring already used in the Bottom Sheet Switcher showcase.
|
|
27
|
+
- An integration whose manifest fails to load is no longer silent. A manifest that throws on import — the common case being one still calling a `create*` authoring factory, removed in 0.3.0 — contributes nothing, and the CLI treated that as if the package had never been configured: `astryx discover` answered `No integrations configured.` while `astryx.config.mjs` plainly configured one, and no command said a word. The only way to find out was to already suspect it and run `validate-integration` by name. Meta's internal `@nest/xds-meta` sat invisible to CLI discovery for a week that way, and the app team's conclusion was that the components did not exist (#5119).
|
|
28
|
+
The load error now counts as an integration issue, so the existing one-line stderr nudge fires on `component`, `template` and `upgrade`, and `discover` — the command whose whole job is listing integrations — nudges too, as does `search`. `discover` also stops reporting `configured: false` for a project that configured an integration that failed to load; the empty state now distinguishes "you configured nothing" from "what you configured contributed nothing", which is the distinction `meta.configured` was introduced to carry.
|
|
29
|
+
|
|
30
|
+
Nothing becomes fatal: the warning is best-effort, stderr-only, suppressed under `--json`, and never changes an exit code. Broken contributions are still skipped exactly as before.
|
|
31
|
+
|
|
32
|
+
#### Contributors
|
|
33
|
+
|
|
34
|
+
Thanks to everyone who contributed to this release:
|
|
35
|
+
|
|
36
|
+
- @cixzhang
|
|
37
|
+
- @imdreamrunner
|
|
38
|
+
- @jiunshinn
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
3
42
|
# 0.4.3
|
|
4
43
|
|
|
5
44
|
#### Fixes
|
|
@@ -8,6 +47,7 @@
|
|
|
8
47
|
The `theme.build` receipt now separates the two: `warnings` are defects the author should fix, `notices` are advisories about a correct theme. The font advisory moves to `notices` and to stdout with the rest of the build's progress; stderr stays for defects. The template guard is back to `warnings` being empty, and no longer needs to know which fonts the template names.
|
|
9
48
|
|
|
10
49
|
Programmatic callers reading `data.warnings` for font advisories should read `data.notices`; the message text is unchanged.
|
|
50
|
+
|
|
11
51
|
- `extends` now reaches the CSS. A theme that extended another built a stylesheet holding only the declarations it stated itself: the base's tokens, component overrides and surface rules were all absent, and because each theme is `@scope`d to its own `data-astryx-theme` value, loading the base's stylesheet alongside could not fill the gap either. Every consumer of an inheritance chain silently got stock geometry, elevation and type with a new palette painted over it (#5067). Nothing warned; the loss only showed up by diffing two generated stylesheets token by token.
|
|
12
52
|
The cause was `theme build` shadowing its own inputs. It writes `<name>.js` next to `<name>.ts`, and the loader resolved a plain `./<name>` specifier to that generated artifact before the source — so the second build of a family read the artifact, which carries no `components` and exports `<name>Theme` rather than whatever the source exports. A named import that missed became `extends: undefined`, and `defineTheme` treated an absent base as no base at all. The loader now resolves source extensions first, which is also the resolution the author's TypeScript sees, so the CSS a build emits matches the theme that type-checked.
|
|
13
53
|
|
package/README.md
CHANGED
|
@@ -383,49 +383,51 @@ Every response has a `type` discriminant. The full set is below (generated from
|
|
|
383
383
|
|
|
384
384
|
<!-- BEGIN GENERATED: response-types -->
|
|
385
385
|
|
|
386
|
-
| Type | What `data` carries
|
|
387
|
-
| --------------------------- |
|
|
388
|
-
| `component.list` | The component catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of names+package, brief entries, or a full ComponentDoc per entry.
|
|
389
|
-
| `component.detail` | One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available).
|
|
390
|
-
| `component.detail.props` | Just one component's props table (ComponentPropDoc[]).
|
|
391
|
-
| `component.detail.source` | One component's source file, as {component, source}.
|
|
392
|
-
| `component.detail.showcase` | One component's showcase example, as {component, aspectRatio, source}.
|
|
393
|
-
| `component.detail.blocks` | One component's example blocks, as {component, showcase, examples, related} of BlockEntry.
|
|
394
|
-
| `docs.list` | All reference-doc topics as DocsListEntry[] ({topic, description}), in discovery order.
|
|
395
|
-
| `docs.detail` | One topic's full ReferenceDoc, with token-ref blocks inlined.
|
|
396
|
-
| `docs.detail.section` | A single ReferenceSection of a topic: the first whose title contains the section query.
|
|
397
|
-
| `blog.list` | The feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL.
|
|
398
|
-
| `blog.detail` | One post's metadata plus the feed URL and the post's full plaintext body.
|
|
399
|
-
| `discover.list` | The configured external packages (name, category, components, version, description); when empty it carries meta.configured to tell "nothing configured" from "nothing discovered".
|
|
400
|
-
| `discover.detail` | A single external package entry, for an @scope/name query.
|
|
401
|
-
| `discover.detail.doc` | The validated ComponentDoc for one external component: an @scope/name/Component query, or a free-text term resolving to exactly one component.
|
|
402
|
-
| `discover.search` | The echoed query plus the matching {package, component} pairs, when a free-text term matches several components.
|
|
403
|
-
| `search` | The echoed query plus a ranked SearchResultEntry[] (domain, name, score, reason, description, follow-up command, and import path where relevant).
|
|
404
|
-
| `build.help` | A marker (`playbook: true`) that the renderer expands into the how-to-build-a-page workflow; emitted when no query is given.
|
|
405
|
-
| `build.kit` | The grouped composition kit: echoed query, hasResults/directMatch flags, the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays.
|
|
406
|
-
| `swizzle.list` | The names of swizzlable components discoverable from cwd's @astryxdesign/core.
|
|
407
|
-
| `swizzle.copy` | An eject receipt: component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an optional maintainer note.
|
|
408
|
-
| `template.list` | Every discovered template (page + block); each entry carries id, name, description, kind, owning package, optional category and componentsUsed, and readiness flags.
|
|
409
|
-
| `template.show` | The resolved template's raw source plus its description, kind, and the component names it composes.
|
|
410
|
-
| `template.skeleton` | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes.
|
|
411
|
-
| `template.copy` | A scaffold receipt: template id, output directory, written file name, and file count.
|
|
412
|
-
| `
|
|
413
|
-
| `hook.
|
|
414
|
-
| `hook.detail
|
|
415
|
-
| `
|
|
416
|
-
| `theme.build
|
|
417
|
-
| `theme.
|
|
418
|
-
| `theme.
|
|
419
|
-
| `theme.
|
|
420
|
-
| `
|
|
421
|
-
| `
|
|
422
|
-
| `upgrade.
|
|
423
|
-
| `
|
|
424
|
-
| `
|
|
425
|
-
| `
|
|
426
|
-
| `
|
|
427
|
-
| `
|
|
428
|
-
| `layout.
|
|
386
|
+
| Type | What `data` carries |
|
|
387
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
388
|
+
| `component.list` | The component catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of names+package, brief entries, or a full ComponentDoc per entry. |
|
|
389
|
+
| `component.detail` | One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available). |
|
|
390
|
+
| `component.detail.props` | Just one component's props table (ComponentPropDoc[]). |
|
|
391
|
+
| `component.detail.source` | One component's source file, as {component, source}. |
|
|
392
|
+
| `component.detail.showcase` | One component's showcase example, as {component, aspectRatio, source}. |
|
|
393
|
+
| `component.detail.blocks` | One component's example blocks, as {component, showcase, examples, related} of BlockEntry. |
|
|
394
|
+
| `docs.list` | All reference-doc topics as DocsListEntry[] ({topic, description}), in discovery order. |
|
|
395
|
+
| `docs.detail` | One topic's full ReferenceDoc, with token-ref blocks inlined. |
|
|
396
|
+
| `docs.detail.section` | A single ReferenceSection of a topic: the first whose title contains the section query. |
|
|
397
|
+
| `blog.list` | The feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL. |
|
|
398
|
+
| `blog.detail` | One post's metadata plus the feed URL and the post's full plaintext body. |
|
|
399
|
+
| `discover.list` | The configured external packages (name, category, components, version, description); when empty it carries meta.configured to tell "nothing configured" from "nothing discovered". |
|
|
400
|
+
| `discover.detail` | A single external package entry, for an @scope/name query. |
|
|
401
|
+
| `discover.detail.doc` | The validated ComponentDoc for one external component: an @scope/name/Component query, or a free-text term resolving to exactly one component. |
|
|
402
|
+
| `discover.search` | The echoed query plus the matching {package, component} pairs, when a free-text term matches several components. |
|
|
403
|
+
| `search` | The echoed query plus a ranked SearchResultEntry[] (domain, name, score, reason, description, follow-up command, and import path where relevant). |
|
|
404
|
+
| `build.help` | A marker (`playbook: true`) that the renderer expands into the how-to-build-a-page workflow; emitted when no query is given. |
|
|
405
|
+
| `build.kit` | The grouped composition kit: echoed query, hasResults/directMatch flags, the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays. |
|
|
406
|
+
| `swizzle.list` | The names of swizzlable components discoverable from cwd's @astryxdesign/core. |
|
|
407
|
+
| `swizzle.copy` | An eject receipt: component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an optional maintainer note. |
|
|
408
|
+
| `template.list` | Every discovered template (page + block); each entry carries id, name, description, kind, owning package, optional category and componentsUsed, and readiness flags. |
|
|
409
|
+
| `template.show` | The resolved template's raw source plus its description, kind, and the component names it composes. |
|
|
410
|
+
| `template.skeleton` | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes. |
|
|
411
|
+
| `template.copy` | A scaffold receipt: template id, output directory, written file name, and file count. |
|
|
412
|
+
| `template.cdn` | A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not — `exists` when a file was already there, which is a success. |
|
|
413
|
+
| `hook.list` | The hook catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of hook names, brief entries, or a full HookDoc per entry. |
|
|
414
|
+
| `hook.detail` | One hook's full authored HookDoc. |
|
|
415
|
+
| `hook.detail.params` | Just one hook's parameters table (HookParamDoc[]). |
|
|
416
|
+
| `theme.build` | A theme build receipt: name, token- and component-override counts, output size, the written outputs {css, js, dts, and variantsDts when applicable}, and any validation warnings. |
|
|
417
|
+
| `theme.build.check` | The --check receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: missing \| outdated}), and the full list of checked paths. Writes nothing. |
|
|
418
|
+
| `theme.build.batch` | Several themes built in one invocation: `count` plus one {file, receipt} per theme in argument order, where receipt is that theme's theme.build (or theme.build.check) envelope, or null when it produced no CSS. |
|
|
419
|
+
| `theme.list` | Every bundled theme as a ThemeListEntry[]: each with slug, displayName, description, and a maintained flag. |
|
|
420
|
+
| `theme.add` | A scaffold receipt: resolved slug, displayName, maintained flag, outputDir (relative to cwd), the theme entry file, its exportName, and the files written. |
|
|
421
|
+
| `theme.template` | A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not — `exists` when a file was already there, which is a success. |
|
|
422
|
+
| `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
|
|
423
|
+
| `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
|
|
424
|
+
| `upgrade.run` | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
|
|
425
|
+
| `manifest` | The self-describing CLI capability manifest: name, version, apiVersion, global options, the command tree (args, options, json flag, response types, examples), the jsonSupported allowlist, and the flat responseTypes index. |
|
|
426
|
+
| `doctor` | The health-check report: `checks` (each with id, label, status: pass \| warn \| fail \| info, a message, and a fix when not passing) plus a `summary` of counts per status. |
|
|
427
|
+
| `integration.validate` | The validation result: the package name and version (both null when no local manifest is found) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning \| error, message}. |
|
|
428
|
+
| `layout.expand` | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the output path, or null when nothing was written). |
|
|
429
|
+
| `layout.check` | The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline). |
|
|
430
|
+
| `layout.grammar` | The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name → canonical component) generated from this install's registry. |
|
|
429
431
|
|
|
430
432
|
<!-- END GENERATED: response-types -->
|
|
431
433
|
<!-- Generated by scripts/generate-cli-readme.mjs from the response-types EnumDoc. Run `pnpm -F @astryxdesign/cli readme`. -->
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* External packages come from configured integrations that declare a components
|
|
8
8
|
* root; each becomes a scannable package keyed by its docsDir. `configured`
|
|
9
|
-
* reports whether
|
|
10
|
-
*
|
|
11
|
-
* nothing
|
|
9
|
+
* reports whether the project configured ANY integration — including one whose
|
|
10
|
+
* manifest failed to load, which contributes nothing but is emphatically not
|
|
11
|
+
* "nothing configured". It lets an empty result distinguish "nothing
|
|
12
|
+
* configured" (`false`) from "configured but nothing discovered" (`true`),
|
|
13
|
+
* which the list leaf surfaces as `meta`.
|
|
12
14
|
*
|
|
13
15
|
* @returns {Promise<{packages: ScannedPackage[], configured: boolean}>}
|
|
14
16
|
*/
|
|
@@ -62,9 +62,11 @@ function validateDocs(docs) {
|
|
|
62
62
|
*
|
|
63
63
|
* External packages come from configured integrations that declare a components
|
|
64
64
|
* root; each becomes a scannable package keyed by its docsDir. `configured`
|
|
65
|
-
* reports whether
|
|
66
|
-
*
|
|
67
|
-
* nothing
|
|
65
|
+
* reports whether the project configured ANY integration — including one whose
|
|
66
|
+
* manifest failed to load, which contributes nothing but is emphatically not
|
|
67
|
+
* "nothing configured". It lets an empty result distinguish "nothing
|
|
68
|
+
* configured" (`false`) from "configured but nothing discovered" (`true`),
|
|
69
|
+
* which the list leaf surfaces as `meta`.
|
|
68
70
|
*
|
|
69
71
|
* @returns {Promise<{packages: ScannedPackage[], configured: boolean}>}
|
|
70
72
|
*/
|
|
@@ -84,7 +86,7 @@ export async function discoverPackages() {
|
|
|
84
86
|
docsDir: integration.components,
|
|
85
87
|
}));
|
|
86
88
|
if (explicitPackages.length === 0) {
|
|
87
|
-
return {packages: [], configured:
|
|
89
|
+
return {packages: [], configured: loadedIntegrations.length > 0};
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
const packages = scanAllPackages(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Write the annotated CDN starter page into a project.
|
|
6
|
+
*
|
|
7
|
+
* Refuses to overwrite without `overwrite`: an edited copy is the consumer's
|
|
8
|
+
* work, and this command is safe to re-run.
|
|
9
|
+
*
|
|
10
|
+
* @param {{targetPath?: string, overwrite?: boolean, cwd?: string}} [options]
|
|
11
|
+
* @returns {import('../template.type.mjs').TemplateCdnResponse}
|
|
12
|
+
*/
|
|
13
|
+
export function templateCdn(options?: {
|
|
14
|
+
targetPath?: string;
|
|
15
|
+
overwrite?: boolean;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}): import("../template.type.mjs").TemplateCdnResponse;
|
|
18
|
+
/** The annotated CDN starter that ships with the CLI. */
|
|
19
|
+
export const CDN_TEMPLATE_SRC: string;
|
|
20
|
+
/** Where it lands when the caller does not say. */
|
|
21
|
+
export const CDN_TEMPLATE_DEFAULT_PATH: "cdn.template.html";
|
|
22
|
+
/** The token every CDN URL in the asset carries in place of a version. */
|
|
23
|
+
export const CDN_VERSION_PLACEHOLDER: "__ASTRYX_VERSION__";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `template.cdn` leaf — writes the annotated no-build-step starter page
|
|
5
|
+
* into the consumer's project.
|
|
6
|
+
*
|
|
7
|
+
* A CDN starter is a template, so it lives in the template family. It is the
|
|
8
|
+
* one member that is not discovered: it ships as an asset rather than as a
|
|
9
|
+
* `page.tsx` + `template.doc.mjs` pair, which is why the dispatcher routes to
|
|
10
|
+
* it from a flag instead of resolving a name.
|
|
11
|
+
*
|
|
12
|
+
* Every CDN URL in the asset carries the `__ASTRYX_VERSION__` placeholder, which
|
|
13
|
+
* is substituted here. An unpinned CDN URL resolves to whatever is latest and is
|
|
14
|
+
* cached hard, so a page written today can break tomorrow without being edited.
|
|
15
|
+
*
|
|
16
|
+
* @position api/template/cdn — writes the CDN starter asset; the template
|
|
17
|
+
* dispatcher routes `--cdn` here before any discovery happens.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import {CLI_ROOT, findCoreDir} from '../../../foundation/fs/paths.mjs';
|
|
23
|
+
import {assertWithin, PathSafetyError} from '../../../foundation/fs/path-safety.mjs';
|
|
24
|
+
import {stripCopyrightHeader} from '../../../foundation/text/copyright-header.mjs';
|
|
25
|
+
import {getXdsVersion} from '../../../foundation/agent-docs/agent-docs.mjs';
|
|
26
|
+
import {AstryxError} from '../../error.mjs';
|
|
27
|
+
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
28
|
+
|
|
29
|
+
/** The annotated CDN starter that ships with the CLI. */
|
|
30
|
+
export const CDN_TEMPLATE_SRC = path.join(CLI_ROOT, 'assets', 'cdn.template.html');
|
|
31
|
+
|
|
32
|
+
/** Where it lands when the caller does not say. */
|
|
33
|
+
export const CDN_TEMPLATE_DEFAULT_PATH = 'cdn.template.html';
|
|
34
|
+
|
|
35
|
+
/** The token every CDN URL in the asset carries in place of a version. */
|
|
36
|
+
export const CDN_VERSION_PLACEHOLDER = '__ASTRYX_VERSION__';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write the annotated CDN starter page into a project.
|
|
40
|
+
*
|
|
41
|
+
* Refuses to overwrite without `overwrite`: an edited copy is the consumer's
|
|
42
|
+
* work, and this command is safe to re-run.
|
|
43
|
+
*
|
|
44
|
+
* @param {{targetPath?: string, overwrite?: boolean, cwd?: string}} [options]
|
|
45
|
+
* @returns {import('../template.type.mjs').TemplateCdnResponse}
|
|
46
|
+
*/
|
|
47
|
+
export function templateCdn(options = {}) {
|
|
48
|
+
const {
|
|
49
|
+
targetPath = CDN_TEMPLATE_DEFAULT_PATH,
|
|
50
|
+
overwrite = false,
|
|
51
|
+
cwd = process.cwd(),
|
|
52
|
+
} = options;
|
|
53
|
+
|
|
54
|
+
let resolved;
|
|
55
|
+
try {
|
|
56
|
+
resolved = assertWithin(targetPath, cwd, {label: 'cdn template path'});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err instanceof PathSafetyError) {
|
|
59
|
+
throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const relative = path.relative(cwd, resolved) || targetPath;
|
|
65
|
+
// The installed @astryxdesign/core when there is one, else this CLI's own
|
|
66
|
+
// version. core, the themes and the CLI are released as one fixed group, so
|
|
67
|
+
// either answer pins a set of packages that exist together.
|
|
68
|
+
const version = getXdsVersion(findCoreDir(cwd));
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(resolved) && !overwrite) {
|
|
71
|
+
return {
|
|
72
|
+
type: 'template.cdn',
|
|
73
|
+
data: {path: relative, version, written: false, reason: 'exists'},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const source = stripCopyrightHeader(fs.readFileSync(CDN_TEMPLATE_SRC, 'utf-8'));
|
|
78
|
+
const contents = source.replaceAll(CDN_VERSION_PLACEHOLDER, version);
|
|
79
|
+
fs.mkdirSync(path.dirname(resolved), {recursive: true});
|
|
80
|
+
fs.writeFileSync(resolved, contents);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
type: 'template.cdn',
|
|
84
|
+
data: {path: relative, version, written: true, reason: null},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Direct API tests for `templateCdn()` — the function behind
|
|
5
|
+
* `astryx template --cdn`.
|
|
6
|
+
*
|
|
7
|
+
* Two things matter here: it never destroys work (the page lands once, a second
|
|
8
|
+
* run leaves an edited copy alone, a path that escapes the project is refused),
|
|
9
|
+
* and the page it writes is pinned — a leftover version placeholder would ship
|
|
10
|
+
* a file whose every CDN URL 404s.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import * as os from 'node:os';
|
|
17
|
+
import {
|
|
18
|
+
templateCdn,
|
|
19
|
+
CDN_TEMPLATE_DEFAULT_PATH,
|
|
20
|
+
CDN_VERSION_PLACEHOLDER,
|
|
21
|
+
} from './cdn.mjs';
|
|
22
|
+
|
|
23
|
+
let tmpDir;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-template-cdn-'));
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const written = () =>
|
|
32
|
+
fs.readFileSync(path.join(tmpDir, CDN_TEMPLATE_DEFAULT_PATH), 'utf-8');
|
|
33
|
+
|
|
34
|
+
describe('templateCdn()', () => {
|
|
35
|
+
it('writes the page and returns a template.cdn receipt', () => {
|
|
36
|
+
const res = templateCdn({cwd: tmpDir});
|
|
37
|
+
|
|
38
|
+
expect(res.type).toBe('template.cdn');
|
|
39
|
+
expect(res.data.path).toBe(CDN_TEMPLATE_DEFAULT_PATH);
|
|
40
|
+
expect(res.data.written).toBe(true);
|
|
41
|
+
expect(res.data.reason).toBe(null);
|
|
42
|
+
expect(res.data.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
43
|
+
expect(written()).toMatch(/<script type="importmap">/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('pins every CDN url to the reported version', () => {
|
|
47
|
+
const {data} = templateCdn({cwd: tmpDir});
|
|
48
|
+
const html = written();
|
|
49
|
+
|
|
50
|
+
expect(html).not.toContain(CDN_VERSION_PLACEHOLDER);
|
|
51
|
+
const urls = html.match(/https:\/\/(?:cdn\.jsdelivr\.net\/npm|esm\.sh)\/@astryxdesign\/[^"?]+/g);
|
|
52
|
+
expect(urls?.length).toBeGreaterThan(0);
|
|
53
|
+
for (const url of urls ?? []) {
|
|
54
|
+
expect(url).toContain(`@${data.version}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('loads the font family the theme names', () => {
|
|
59
|
+
templateCdn({cwd: tmpDir});
|
|
60
|
+
const html = written();
|
|
61
|
+
|
|
62
|
+
// The theme names Figtree and never loads it (#5015): without the webfont
|
|
63
|
+
// link every viewer silently gets the fallback stack instead.
|
|
64
|
+
expect(html).toMatch(/fonts\.googleapis\.com\/css2\?family=Figtree/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not carry our copyright header into the consumer tree', () => {
|
|
68
|
+
templateCdn({cwd: tmpDir});
|
|
69
|
+
|
|
70
|
+
expect(written()).not.toMatch(/Copyright \(c\) Meta Platforms/);
|
|
71
|
+
expect(written().startsWith('<!doctype html>')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('leaves an existing file alone and says so', () => {
|
|
75
|
+
const dest = path.join(tmpDir, CDN_TEMPLATE_DEFAULT_PATH);
|
|
76
|
+
fs.writeFileSync(dest, '<!-- mine -->\n');
|
|
77
|
+
|
|
78
|
+
const res = templateCdn({cwd: tmpDir});
|
|
79
|
+
|
|
80
|
+
expect(res.data.written).toBe(false);
|
|
81
|
+
expect(res.data.reason).toBe('exists');
|
|
82
|
+
expect(fs.readFileSync(dest, 'utf-8')).toBe('<!-- mine -->\n');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('overwrites when asked', () => {
|
|
86
|
+
const dest = path.join(tmpDir, CDN_TEMPLATE_DEFAULT_PATH);
|
|
87
|
+
fs.writeFileSync(dest, '<!-- mine -->\n');
|
|
88
|
+
|
|
89
|
+
const res = templateCdn({cwd: tmpDir, overwrite: true});
|
|
90
|
+
|
|
91
|
+
expect(res.data.written).toBe(true);
|
|
92
|
+
expect(written()).toMatch(/importmap/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('honors a custom path and creates its directory', () => {
|
|
96
|
+
const res = templateCdn({cwd: tmpDir, targetPath: 'public/demo.html'});
|
|
97
|
+
|
|
98
|
+
expect(res.data.path).toBe(path.join('public', 'demo.html'));
|
|
99
|
+
expect(fs.existsSync(path.join(tmpDir, 'public/demo.html'))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('refuses a path that escapes the project', () => {
|
|
103
|
+
expect(() => templateCdn({cwd: tmpDir, targetPath: '../escaped.html'})).toThrow(
|
|
104
|
+
/cdn template path/,
|
|
105
|
+
);
|
|
106
|
+
expect(fs.existsSync(path.join(path.dirname(tmpDir), 'escaped.html'))).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* @param {boolean} [options.overwrite]
|
|
18
18
|
* @param {boolean} [options.list]
|
|
19
19
|
* @param {boolean} [options.skeleton]
|
|
20
|
+
* @param {boolean | string} [options.cdn] - Write the no-build-step CDN starter page; a string is used as the destination path.
|
|
20
21
|
* @param {boolean} [options.show]
|
|
21
22
|
* @param {'page'|'block'} [options.type] - Filter list views / narrow lookups by template kind.
|
|
22
23
|
* @param {string} [options.package] - Narrow lookups to a specific package (id-only matches across packages are ambiguous).
|
|
@@ -28,6 +29,7 @@ export function template(name?: string, options?: {
|
|
|
28
29
|
overwrite?: boolean | undefined;
|
|
29
30
|
list?: boolean | undefined;
|
|
30
31
|
skeleton?: boolean | undefined;
|
|
32
|
+
cdn?: string | boolean | undefined;
|
|
31
33
|
show?: boolean | undefined;
|
|
32
34
|
type?: "block" | "page" | undefined;
|
|
33
35
|
package?: string | undefined;
|
|
@@ -18,11 +18,13 @@ export const doc = {
|
|
|
18
18
|
"templates; with a name it returns that template's source, a layout skeleton, or " +
|
|
19
19
|
'scaffolds it into the project. Templates are discovered across core, external ' +
|
|
20
20
|
'packages, and integrations, so the same id can appear in more than one place; ' +
|
|
21
|
-
'narrow an ambiguous name with type and/or package.'
|
|
21
|
+
'narrow an ambiguous name with type and/or package. The cdn option writes the ' +
|
|
22
|
+
'annotated no-build-step CDN starter page, which ships as an asset rather than as ' +
|
|
23
|
+
'a discovered template.',
|
|
22
24
|
importPath: '@astryxdesign/cli/api',
|
|
23
25
|
signature:
|
|
24
|
-
'template(name?: string, options?: TemplateOptions): Promise<TemplateListResponse | TemplateShowResponse | TemplateSkeletonResponse | TemplateCopyResponse>',
|
|
25
|
-
keywords: ['template', 'scaffold', 'page', 'block', 'skeleton', 'starter'],
|
|
26
|
+
'template(name?: string, options?: TemplateOptions): Promise<TemplateListResponse | TemplateShowResponse | TemplateSkeletonResponse | TemplateCopyResponse | TemplateCdnResponse>',
|
|
27
|
+
keywords: ['template', 'scaffold', 'page', 'block', 'skeleton', 'starter', 'cdn', 'esm', 'importmap', 'no-build'],
|
|
26
28
|
params: [
|
|
27
29
|
{
|
|
28
30
|
name: 'name',
|
|
@@ -43,6 +45,13 @@ export const doc = {
|
|
|
43
45
|
'Return a compact layout skeleton (structural tags with spatial annotations) instead of the full source.',
|
|
44
46
|
default: 'false',
|
|
45
47
|
},
|
|
48
|
+
{
|
|
49
|
+
name: 'options.cdn',
|
|
50
|
+
type: 'boolean | string',
|
|
51
|
+
description:
|
|
52
|
+
'Write the annotated no-build-step CDN starter page instead of resolving a template. Answers before discovery, so no name is involved; pass a string to use it as the destination path.',
|
|
53
|
+
default: 'false',
|
|
54
|
+
},
|
|
46
55
|
{
|
|
47
56
|
name: 'options.show',
|
|
48
57
|
type: 'boolean',
|
|
@@ -102,6 +111,11 @@ export const doc = {
|
|
|
102
111
|
description:
|
|
103
112
|
'A receipt after scaffolding the template into the project: the template id, output directory, written file name, and file count.',
|
|
104
113
|
},
|
|
114
|
+
{
|
|
115
|
+
type: 'template.cdn',
|
|
116
|
+
description:
|
|
117
|
+
'A write receipt for the CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not — `exists` when a file was already there, which is a success.',
|
|
118
|
+
},
|
|
105
119
|
],
|
|
106
120
|
throws: [
|
|
107
121
|
{
|
|
@@ -136,6 +150,10 @@ export const doc = {
|
|
|
136
150
|
label: 'Scaffold into the project',
|
|
137
151
|
code: "await template('dashboard', {targetPath: './app/page.tsx'});",
|
|
138
152
|
},
|
|
153
|
+
{
|
|
154
|
+
label: 'CDN starter page',
|
|
155
|
+
code: 'await template(undefined, {cdn: true});',
|
|
156
|
+
},
|
|
139
157
|
],
|
|
140
158
|
command: 'template',
|
|
141
159
|
related: ['component', 'search', 'discover', 'init'],
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* This module is BOTH the template dispatcher and the stable import surface for
|
|
7
7
|
* the template family. `template()` discovers the available templates, resolves
|
|
8
|
-
* the requested one, and routes to a leaf (list/show/skeleton/copy). The shared
|
|
8
|
+
* the requested one, and routes to a leaf (list/show/skeleton/copy/cdn). The shared
|
|
9
9
|
* discovery/IO + cross-command helpers live in `foundation/discovery/template-adapter.mjs` and are
|
|
10
10
|
* RE-EXPORTED here so external import paths (`api/template/template.mjs`) —
|
|
11
11
|
* used by component, layout, search, init, discover, validate-integration, and
|
|
12
12
|
* lib/project — keep resolving unchanged.
|
|
13
13
|
*
|
|
14
14
|
* @position api/template — the template dispatcher + barrel; leaves live under
|
|
15
|
-
* ./list, ./show, ./skeleton, ./copy and shared discovery in foundation/discovery.
|
|
15
|
+
* ./list, ./show, ./skeleton, ./copy, ./cdn and shared discovery in foundation/discovery.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import {discoverAll, pkgOf} from '../../foundation/discovery/template-adapter.mjs';
|
|
@@ -22,6 +22,7 @@ import {templateList} from './list/list.mjs';
|
|
|
22
22
|
import {templateShow} from './show/show.mjs';
|
|
23
23
|
import {templateSkeleton} from './skeleton/skeleton.mjs';
|
|
24
24
|
import {templateCopy} from './copy/copy.mjs';
|
|
25
|
+
import {templateCdn} from './cdn/cdn.mjs';
|
|
25
26
|
|
|
26
27
|
// Re-export the shared discovery/IO + cross-command helpers so this module
|
|
27
28
|
// stays the single import surface for the template family (same exports as
|
|
@@ -56,6 +57,7 @@ export {
|
|
|
56
57
|
* @param {boolean} [options.overwrite]
|
|
57
58
|
* @param {boolean} [options.list]
|
|
58
59
|
* @param {boolean} [options.skeleton]
|
|
60
|
+
* @param {boolean | string} [options.cdn] - Write the no-build-step CDN starter page; a string is used as the destination path.
|
|
59
61
|
* @param {boolean} [options.show]
|
|
60
62
|
* @param {'page'|'block'} [options.type] - Filter list views / narrow lookups by template kind.
|
|
61
63
|
* @param {string} [options.package] - Narrow lookups to a specific package (id-only matches across packages are ambiguous).
|
|
@@ -67,12 +69,24 @@ export async function template(name, options = {}) {
|
|
|
67
69
|
list = false,
|
|
68
70
|
skeleton = false,
|
|
69
71
|
show = false,
|
|
72
|
+
cdn = false,
|
|
70
73
|
targetPath,
|
|
71
74
|
overwrite = false,
|
|
72
75
|
type,
|
|
73
76
|
package: packageFilter,
|
|
74
77
|
cwd = process.cwd(),
|
|
75
78
|
} = options;
|
|
79
|
+
|
|
80
|
+
// The CDN starter ships as an asset rather than as a discovered template, so
|
|
81
|
+
// it answers before discovery — nothing here needs a name resolved.
|
|
82
|
+
if (cdn) {
|
|
83
|
+
return templateCdn({
|
|
84
|
+
targetPath: typeof cdn === 'string' ? cdn : targetPath,
|
|
85
|
+
overwrite,
|
|
86
|
+
cwd,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
const templates = await discoverAll(cwd);
|
|
77
91
|
|
|
78
92
|
if (list || (!name && !skeleton)) {
|
|
@@ -68,6 +68,21 @@ export type TemplateCopyResponse = {
|
|
|
68
68
|
filesCopied: number;
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* xds --json template --cdn [path]
|
|
73
|
+
* `written: false` with `reason: 'exists'` is a success: the command is safe to
|
|
74
|
+
* re-run, and an edited page is the consumer's file to keep. `version` is the
|
|
75
|
+
* Astryx version every CDN URL in the file was pinned to.
|
|
76
|
+
*/
|
|
77
|
+
export type TemplateCdnResponse = {
|
|
78
|
+
type: "template.cdn";
|
|
79
|
+
data: {
|
|
80
|
+
path: string;
|
|
81
|
+
version: string;
|
|
82
|
+
written: boolean;
|
|
83
|
+
reason: "exists" | null;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
71
86
|
/**
|
|
72
87
|
* Options for `template()`.
|
|
73
88
|
*/
|
|
@@ -75,6 +90,10 @@ export type TemplateOptions = {
|
|
|
75
90
|
list?: boolean | undefined;
|
|
76
91
|
skeleton?: boolean | undefined;
|
|
77
92
|
show?: boolean | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Write the no-build-step CDN starter page instead of resolving a template. A string is used as the destination path.
|
|
95
|
+
*/
|
|
96
|
+
cdn?: string | boolean | undefined;
|
|
78
97
|
/**
|
|
79
98
|
* Filter templates by kind: 'page' or 'block'. Only applies to list views.
|
|
80
99
|
*/
|