@astryxdesign/cli 0.4.1 → 0.4.2-canary.464a445
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 +34 -0
- package/README.md +1 -0
- package/api/init/init.doc.mjs +2 -2
- package/api/init/init.test.mjs +19 -2
- package/api/init/init.type.d.mts +9 -1
- package/api/init/init.type.mjs +3 -1
- package/api/init/run/run.mjs +36 -6
- package/api/theme/add/add.mjs +2 -13
- package/api/theme/build/build.font-warning.test.mjs +161 -0
- package/api/theme/build/build.mjs +19 -12
- package/api/theme/build/build.test.mjs +53 -0
- package/api/theme/build/font-warning.d.mts +26 -0
- package/api/theme/build/font-warning.mjs +214 -0
- package/api/theme/build/font-warning.test.mjs +242 -0
- package/api/theme/template/template.d.mts +21 -0
- package/api/theme/template/template.mjs +69 -0
- package/api/theme/template/template.test.mjs +82 -0
- package/api/theme/theme.d.mts +1 -0
- package/api/theme/theme.mjs +1 -0
- package/api/theme/theme.type.d.mts +13 -0
- package/api/theme/theme.type.mjs +11 -1
- package/api/theme/themeTemplate.doc.d.mts +11 -0
- package/api/theme/themeTemplate.doc.mjs +64 -0
- package/assets/docs/getting-started.doc.mjs +11 -1
- package/assets/docs/theme.doc.dense.mjs +2 -2
- package/assets/docs/theme.doc.mjs +20 -9
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/typography.doc.mjs +38 -0
- package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.doc.mjs +13 -0
- package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.tsx +55 -0
- package/assets/templates/pages/ai-chat/page.tsx +18 -13
- package/assets/templates/themes/butter/butterTheme.ts +4 -1
- package/assets/templates/themes/chocolate/chocolateTheme.ts +4 -1
- package/assets/templates/themes/gothic/gothicTheme.ts +4 -1
- package/assets/templates/themes/stone/stoneTheme.ts +4 -1
- package/assets/theme.template.ts +322 -0
- package/authoring/doctypes/base/type.ts +9 -1
- package/authoring/doctypes/component/component.doc.mjs +1 -1
- package/clients/cli/commands/build-theme.font-warning.test.mjs +138 -0
- package/clients/cli/commands/build-theme.mjs +50 -0
- package/clients/cli/commands/init.behavior.test.mjs +13 -2
- package/clients/cli/commands/theme-template.behavior.test.mjs +85 -0
- package/clients/cli/commands/theme-template.doc.mjs +42 -0
- package/clients/cli/commands/theme.doc.mjs +2 -2
- package/clients/cli/index.mjs +1 -0
- package/clients/cli/lib/manifest.mjs +2 -0
- package/foundation/agent-docs/agent-docs.mjs +1 -1
- package/foundation/response/response-types.doc.mjs +5 -0
- package/foundation/text/copyright-header.d.mts +11 -0
- package/foundation/text/copyright-header.mjs +35 -0
- package/foundation/text/copyright-header.test.mjs +58 -0
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @xds/cli
|
|
2
2
|
|
|
3
|
+
# 0.4.2
|
|
4
|
+
|
|
5
|
+
#### New Features
|
|
6
|
+
|
|
7
|
+
- `astryx theme build` warns when a theme names fonts it does not load. The resolved `--font-family-*` tokens and component-override `fontFamily` values are checked against CSS generics and known system families; anything else gets one warning per family in the receipt and, after the install instructions, the `<link>`/`@font-face` snippet to add. `astryx docs typography` gains a Loading Custom Fonts section (Google Fonts and self-hosted recipes, `font-display: swap`, real fallback stacks), and the theme docs' production-build section points at it (#5015).
|
|
8
|
+
- `astryx theme template` writes an annotated theme template into your project (#5048).
|
|
9
|
+
New sibling of `theme add`: where `add` starts you from a theme we ship, `template` starts you from a blank annotated one. `astryx init --features theme` calls the same leaf, so project setup writes it too — it previously printed a one-line hint and wrote nothing, which is the weakest form of the help a theme author needs, since the first problem is not knowing the command but not knowing what the theme surface contains. The file is `theme.template.ts`: every `defineTheme` field with a note on when to reach for it, the token families, the component override syntax, and the consumption steps (providing the theme, loading the fonts you name, building for SSR), each section naming the CLI command that prints its authoritative reference. An existing file is never clobbered.
|
|
10
|
+
|
|
11
|
+
This came out of a vibe test (#5047): agents given an annotated template reached twice as far into the theme surface as agents given only the docs (17 component targets vs 8, and the only arm to use interaction states, custom variants and `onDark`), and shipped a third of the contrast defects.
|
|
12
|
+
|
|
13
|
+
A template that lies is worse than no template, so its claims are machine-checked against live sources rather than trusted: `scripts/check-theme-template.test.mjs` fails when a `defineTheme` field is added and left undocumented, when a token family is missing from the inventory, when a CSS variable or component key it names does not exist, when it cites a docs topic that does not, or when a theme source drops its SYNC reference. `theme build` compiles it warning-free in CI, and the CLI typecheck now covers it.
|
|
14
|
+
|
|
15
|
+
#### Fixes
|
|
16
|
+
|
|
17
|
+
- Heading's `type` is a documented theming target, and the docs stop teaching a CSS variable that does not exist (#5016).
|
|
18
|
+
`Heading` reflects `type` as a theme selector — `typography.scale` generates `heading: {'type:display-1' …}` rules for it — but `theming.targets` listed only `level` and `color`, so `astryx theme build` warned `Unknown prop "type" on component "heading"` on every theme that sets a type scale, including the shipped `neutralTheme`. The drift guard missed it twice over: it read a conditional spread (`{level, color, ...(type && {type})}`) as an unknown bag, and it only checked a component against a doc file in its own directory, so `Heading/` — documented from `Text/Text.doc.mjs` — was never checked at all. Both are fixed, which brings three more previously unchecked directories under the guard.
|
|
19
|
+
|
|
20
|
+
Separately, the theme docs' component-override example set `--button-press-scale`, which no component defines: copying it produces CSS that silently never applies. It now sets a real public var, and the example no longer declares the same `button` key twice.
|
|
21
|
+
|
|
22
|
+
- Two guards left failing on `main` by their own landings, so every PR since has been red through no fault of its own. #4963 gave Thumbnail's remove button a coarse-pointer hit-area var and did not document it, which the derived-var guard reads as an undocumented private var; the var is an `inset` on a `::after` overlay, so it is documented as private and listed alongside the other vars no standard CSS property maps onto. #5026 moved `borderDefaults` into `CoreTokenName` — the landing the theme-template guard was explicitly waiting for (its comment says "when #5017 lands, this guard starts requiring the template to cover it") — so the template's token inventory now names `--border-width`.
|
|
23
|
+
|
|
24
|
+
#### Documentation
|
|
25
|
+
|
|
26
|
+
- MobileNavToggle preview simulates a mobile AppShell instead of an empty stage: new playground.appShellMobile for components that render nothing without AppShell mobile context (#4983)
|
|
27
|
+
|
|
28
|
+
#### Contributors
|
|
29
|
+
|
|
30
|
+
Thanks to everyone who contributed to this release:
|
|
31
|
+
|
|
32
|
+
- @AKnassa
|
|
33
|
+
- @cixzhang
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
3
37
|
# 0.4.1
|
|
4
38
|
|
|
5
39
|
#### Fixes
|
package/README.md
CHANGED
|
@@ -416,6 +416,7 @@ Every response has a `type` discriminant. The full set is below (generated from
|
|
|
416
416
|
| `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. |
|
|
417
417
|
| `theme.list` | Every bundled theme as a ThemeListEntry[]: each with slug, displayName, description, and a maintained flag. |
|
|
418
418
|
| `theme.add` | A scaffold receipt: resolved slug, displayName, maintained flag, outputDir (relative to cwd), the theme entry file, its exportName, and the files written. |
|
|
419
|
+
| `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. |
|
|
419
420
|
| `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
|
|
420
421
|
| `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
|
|
421
422
|
| `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. |
|
package/api/init/init.doc.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export const doc = {
|
|
|
30
30
|
name: 'options.features',
|
|
31
31
|
type: 'string',
|
|
32
32
|
description:
|
|
33
|
-
'Comma-separated features to install: agents, theme, template.',
|
|
33
|
+
'Comma-separated features to install: agents (agent docs), theme (writes the annotated theme.template.ts), template (page-template guidance).',
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
name: 'options.all',
|
|
@@ -64,7 +64,7 @@ export const doc = {
|
|
|
64
64
|
{
|
|
65
65
|
type: 'init.run',
|
|
66
66
|
description:
|
|
67
|
-
'The install receipt: the `mode` (`default` | `features`), the features run, agent-doc files written, any soft docsError,
|
|
67
|
+
'The install receipt: the `mode` (`default` | `features`), the features run, agent-doc files written, any soft docsError, the theme-template outcome (`created` | `skipped` | `failed`) plus its path, the page-template outcome (`workflow` | `created` | `skipped`) plus its path, and whether the next-steps were emitted.',
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
type: 'init.remove',
|
package/api/init/init.test.mjs
CHANGED
|
@@ -57,13 +57,30 @@ describe('init() — receipts + side effects', () => {
|
|
|
57
57
|
expect(res.type === 'init.run' && res.data.docsWritten).toContain('AGENTS.md');
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it('--features theme
|
|
60
|
+
it('--features theme writes the annotated template and reports it on the receipt', async () => {
|
|
61
61
|
const res = await init({features: 'theme'}, {cwd: tmpDir});
|
|
62
62
|
expect(res.type).toBe('init.run');
|
|
63
63
|
if (res.type !== 'init.run') return;
|
|
64
64
|
expect(res.data.theme).toBe(true);
|
|
65
|
+
expect(res.data.themeTemplate).toBe('created');
|
|
66
|
+
expect(res.data.themeTemplatePath).toBe('theme.template.ts');
|
|
65
67
|
expect(res.data.docsWritten).toEqual([]);
|
|
66
|
-
expect(fs.readdirSync(tmpDir)).toEqual([]);
|
|
68
|
+
expect(fs.readdirSync(tmpDir)).toEqual(['theme.template.ts']);
|
|
69
|
+
// The consumer's copy is their file: it must not carry our repo header,
|
|
70
|
+
// which their own lint would flag.
|
|
71
|
+
const written = fs.readFileSync(path.join(tmpDir, 'theme.template.ts'), 'utf-8');
|
|
72
|
+
expect(written).not.toMatch(/Copyright \(c\) Meta Platforms/);
|
|
73
|
+
expect(written.startsWith('/**')).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('--features theme reports `skipped` rather than overwriting an existing template', async () => {
|
|
77
|
+
fs.writeFileSync(path.join(tmpDir, 'theme.template.ts'), '// mine\n');
|
|
78
|
+
const res = await init({features: 'theme'}, {cwd: tmpDir});
|
|
79
|
+
expect(res.type).toBe('init.run');
|
|
80
|
+
if (res.type !== 'init.run') return;
|
|
81
|
+
expect(res.data.themeTemplate).toBe('skipped');
|
|
82
|
+
expect(res.data.themeTemplatePath).toBe(null);
|
|
83
|
+
expect(fs.readFileSync(path.join(tmpDir, 'theme.template.ts'), 'utf-8')).toBe('// mine\n');
|
|
67
84
|
});
|
|
68
85
|
|
|
69
86
|
it('--features template returns the workflow (or skipped) outcome, no crash', async () => {
|
package/api/init/init.type.d.mts
CHANGED
|
@@ -22,9 +22,17 @@ export type InitRunData = {
|
|
|
22
22
|
message?: string;
|
|
23
23
|
} | null;
|
|
24
24
|
/**
|
|
25
|
-
* Whether theme
|
|
25
|
+
* Whether the theme feature ran.
|
|
26
26
|
*/
|
|
27
27
|
theme: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Outcome of writing the annotated theme template (`skipped` = a file was already there).
|
|
30
|
+
*/
|
|
31
|
+
themeTemplate: "created" | "skipped" | "failed" | null;
|
|
32
|
+
/**
|
|
33
|
+
* Relative output path when `themeTemplate === 'created'`.
|
|
34
|
+
*/
|
|
35
|
+
themeTemplatePath: string | null;
|
|
28
36
|
/**
|
|
29
37
|
* Template outcome (`workflow` is the CLI default; `created`/`skipped` are programmatic).
|
|
30
38
|
*/
|
package/api/init/init.type.mjs
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* @property {string[]} features Features that were run, in order.
|
|
13
13
|
* @property {string[]} docsWritten Agent-doc files written (empty if agents weren't run or install failed).
|
|
14
14
|
* @property {{kind: 'path-safety' | 'install-failed', message?: string} | null} docsError Soft agent-docs failure, if any. `path-safety` also implies a non-zero exit.
|
|
15
|
-
* @property {boolean} theme Whether theme
|
|
15
|
+
* @property {boolean} theme Whether the theme feature ran.
|
|
16
|
+
* @property {'created' | 'skipped' | 'failed' | null} themeTemplate Outcome of writing the annotated theme template (`skipped` = a file was already there).
|
|
17
|
+
* @property {string | null} themeTemplatePath Relative output path when `themeTemplate === 'created'`.
|
|
16
18
|
* @property {'workflow' | 'created' | 'skipped' | null} template Template outcome (`workflow` is the CLI default; `created`/`skipped` are programmatic).
|
|
17
19
|
* @property {string | null} templatePath Relative output path when `template === 'created'`.
|
|
18
20
|
* @property {boolean} nextSteps Whether the getting-started "Next steps" were emitted (default mode).
|
package/api/init/run/run.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {CLI_ROOT} from '../../../foundation/fs/paths.mjs';
|
|
|
20
20
|
import {PathSafetyError} from '../../../foundation/fs/path-safety.mjs';
|
|
21
21
|
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
22
22
|
import {installAgentDocs} from '../../../foundation/agent-docs/agent-docs.mjs';
|
|
23
|
+
import {themeTemplate} from '../../theme/template/template.mjs';
|
|
23
24
|
import {listTemplates} from '../../template/template.mjs';
|
|
24
25
|
import {AstryxError} from '../../error.mjs';
|
|
25
26
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
@@ -106,6 +107,36 @@ function applyAgents(cwd, options, invocation, data) {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Write the annotated theme template, via the same leaf `astryx theme template`
|
|
112
|
+
* uses — init is a convenience wrapper over the theme command, not a second
|
|
113
|
+
* implementation of it.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} cwd
|
|
116
|
+
* @param {string} invocation
|
|
117
|
+
* @param {import('../init.type.mjs').InitRunData} data
|
|
118
|
+
*/
|
|
119
|
+
function applyTheme(cwd, invocation, data) {
|
|
120
|
+
data.theme = true;
|
|
121
|
+
try {
|
|
122
|
+
const {path: written, written: didWrite} = themeTemplate({cwd}).data;
|
|
123
|
+
data.themeTemplate = didWrite ? 'created' : 'skipped';
|
|
124
|
+
data.themeTemplatePath = didWrite ? written : null;
|
|
125
|
+
logger.log(
|
|
126
|
+
didWrite
|
|
127
|
+
? `✓ Theme template written → ${written}`
|
|
128
|
+
: `• ${written} already exists — left as is.`,
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
// Soft failure, like agent docs: the guidance below is still useful.
|
|
132
|
+
data.themeTemplate = 'failed';
|
|
133
|
+
logger.error('Could not write the theme template.');
|
|
134
|
+
}
|
|
135
|
+
logger.log(
|
|
136
|
+
` Copy it to your theme file and edit, or run \`${invocation} theme add <slug>\` to start from a shipped theme (\`${invocation} theme list\` to browse).`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
109
140
|
/**
|
|
110
141
|
* Emit the template guidance, or (programmatic-only) scaffold a named template.
|
|
111
142
|
* The CLI never passes `templateName`, so from the CLI this always emits the
|
|
@@ -210,18 +241,15 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
210
241
|
docsWritten: [],
|
|
211
242
|
docsError: null,
|
|
212
243
|
theme: false,
|
|
244
|
+
themeTemplate: null,
|
|
245
|
+
themeTemplatePath: null,
|
|
213
246
|
template: null,
|
|
214
247
|
templatePath: null,
|
|
215
248
|
nextSteps: false,
|
|
216
249
|
};
|
|
217
250
|
for (const feature of features) {
|
|
218
251
|
if (feature === 'agents') applyAgents(cwd, options, invocation, data);
|
|
219
|
-
if (feature === 'theme')
|
|
220
|
-
logger.log(
|
|
221
|
-
`✓ For a custom theme, run \`${invocation} theme\` (browse) or \`${invocation} theme add <slug>\` (scaffold).`,
|
|
222
|
-
);
|
|
223
|
-
data.theme = true;
|
|
224
|
-
}
|
|
252
|
+
if (feature === 'theme') applyTheme(cwd, invocation, data);
|
|
225
253
|
if (feature === 'template') {
|
|
226
254
|
applyTemplate(cwd, {templateName: options.templateName}, invocation, data);
|
|
227
255
|
}
|
|
@@ -238,6 +266,8 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
|
|
|
238
266
|
docsWritten: [],
|
|
239
267
|
docsError: null,
|
|
240
268
|
theme: false,
|
|
269
|
+
themeTemplate: null,
|
|
270
|
+
themeTemplatePath: null,
|
|
241
271
|
template: null,
|
|
242
272
|
templatePath: null,
|
|
243
273
|
nextSteps: true,
|
package/api/theme/add/add.mjs
CHANGED
|
@@ -13,19 +13,8 @@ import {assertWithin, PathSafetyError} from '../../../foundation/fs/path-safety.
|
|
|
13
13
|
import {AstryxError} from '../../error.mjs';
|
|
14
14
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
15
15
|
import {THEMES_DIR, listThemes, findTheme} from '../_adapter.mjs';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// repo boilerplate (mirrors the docsite). Preserves a leading BOM/shebang.
|
|
19
|
-
const META_COPYRIGHT_HEADER_RE =
|
|
20
|
-
/^(\uFEFF?(?:#![^\r\n]*(?:\r?\n))?)\/\/ Copyright \(c\) Meta Platforms, Inc\. and affiliates\.\r?\n(?:\r?\n)*/;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {string} source
|
|
24
|
-
* @returns {string}
|
|
25
|
-
*/
|
|
26
|
-
function stripCopyrightHeader(source) {
|
|
27
|
-
return source.replace(META_COPYRIGHT_HEADER_RE, '$1');
|
|
28
|
-
}
|
|
16
|
+
// Scaffolded files must not carry our repo boilerplate into a consumer's tree.
|
|
17
|
+
import {stripCopyrightHeader} from '../../../foundation/text/copyright-header.mjs';
|
|
29
18
|
|
|
30
19
|
/**
|
|
31
20
|
* @param {string} slug
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* API-contract tests for the font-loading warning in `themeBuild()` (#5015):
|
|
5
|
+
* a theme that names font families it does not load gets one entry per family
|
|
6
|
+
* in the `theme.build` receipt's `warnings` array, on BOTH load paths — a raw
|
|
7
|
+
* typography config (resolved through core's defineTheme) and an
|
|
8
|
+
* already-resolved theme that sets `--font-family-*` tokens directly. Themes
|
|
9
|
+
* that only name generics or known system families warn about nothing, and
|
|
10
|
+
* the warning never breaks the API's silence contract (default noopLogger).
|
|
11
|
+
*
|
|
12
|
+
* Needs a built core — the `node` project's globalSetup
|
|
13
|
+
* (vitest.global-setup.node.mjs) builds it once before workers fork.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import * as os from 'node:os';
|
|
20
|
+
import {themeBuild} from './build.mjs';
|
|
21
|
+
|
|
22
|
+
vi.setConfig({testTimeout: 30000});
|
|
23
|
+
|
|
24
|
+
let tmpDir;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-theme-fonts-api-'));
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('themeBuild() — font-loading warnings in the receipt', () => {
|
|
33
|
+
it('warns once per unloaded family for a typography config (heading inherits body without duplicating)', async () => {
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
path.join(tmpDir, 'fonty.mjs'),
|
|
36
|
+
`export default {
|
|
37
|
+
name: 'fonty',
|
|
38
|
+
typography: {
|
|
39
|
+
body: {family: 'Space Grotesk', fallbacks: 'Arial, sans-serif'},
|
|
40
|
+
code: {family: 'JetBrains Mono'},
|
|
41
|
+
},
|
|
42
|
+
};\n`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const result = await themeBuild('fonty.mjs', {}, {cwd: tmpDir});
|
|
46
|
+
|
|
47
|
+
expect(result?.type).toBe('theme.build');
|
|
48
|
+
const warnings = result?.data.warnings ?? [];
|
|
49
|
+
expect(warnings).toEqual(
|
|
50
|
+
expect.arrayContaining([
|
|
51
|
+
expect.stringContaining('Font "Space Grotesk"'),
|
|
52
|
+
expect.stringContaining('Font "JetBrains Mono"'),
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
// Heading inherits body's family; the shared family warns exactly once.
|
|
56
|
+
expect(warnings.filter(w => w.includes('Space Grotesk'))).toHaveLength(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('warns for an already-resolved theme: font-family tokens and component overrides, nothing else', async () => {
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(tmpDir, 'raw.mjs'),
|
|
62
|
+
`export default {
|
|
63
|
+
name: 'raw',
|
|
64
|
+
tokens: {
|
|
65
|
+
'--font-family-body': '"Bungee", cursive',
|
|
66
|
+
'--font-size-base': '1rem',
|
|
67
|
+
},
|
|
68
|
+
components: {button: {base: {fontFamily: 'Orbitron'}}},
|
|
69
|
+
};\n`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const result = await themeBuild('raw.mjs', {}, {cwd: tmpDir});
|
|
73
|
+
|
|
74
|
+
// Exactly the two named families — a non-family --font-* token must not
|
|
75
|
+
// produce a bogus "Font \\"1rem\\"" entry, and the components half of the
|
|
76
|
+
// feature must survive the real themeBuild path, not just the unit helper.
|
|
77
|
+
const fontWarnings = (result?.data.warnings ?? []).filter(w =>
|
|
78
|
+
w.startsWith('Font "'),
|
|
79
|
+
);
|
|
80
|
+
expect(fontWarnings).toEqual([
|
|
81
|
+
expect.stringContaining('Font "Bungee"'),
|
|
82
|
+
expect.stringContaining('Font "Orbitron"'),
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('warns for a family named only inside a pseudo-class component override (defineTheme path)', async () => {
|
|
87
|
+
// ':hover' blocks are legal override values (generateThemeRules), and the
|
|
88
|
+
// typography path deep-merges author components with generated ones — a
|
|
89
|
+
// webfont hiding at that depth must survive the merge and still warn.
|
|
90
|
+
fs.writeFileSync(
|
|
91
|
+
path.join(tmpDir, 'pseudo.mjs'),
|
|
92
|
+
`export default {
|
|
93
|
+
name: 'pseudo',
|
|
94
|
+
typography: {
|
|
95
|
+
body: {family: 'Helvetica', fallbacks: 'Arial, sans-serif'},
|
|
96
|
+
},
|
|
97
|
+
components: {
|
|
98
|
+
button: {base: {':hover': {fontFamily: '"Rubik Doodle", cursive'}}},
|
|
99
|
+
},
|
|
100
|
+
};\n`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const result = await themeBuild('pseudo.mjs', {}, {cwd: tmpDir});
|
|
104
|
+
|
|
105
|
+
const fontWarnings = (result?.data.warnings ?? []).filter(w =>
|
|
106
|
+
w.startsWith('Font "'),
|
|
107
|
+
);
|
|
108
|
+
// Exactly the hidden family — Helvetica/Arial are system fonts.
|
|
109
|
+
expect(fontWarnings).toEqual([
|
|
110
|
+
expect.stringContaining('Font "Rubik Doodle"'),
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('warns about nothing when every named family is a generic or known system font', async () => {
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
path.join(tmpDir, 'sys.mjs'),
|
|
117
|
+
`export default {
|
|
118
|
+
name: 'sys',
|
|
119
|
+
tokens: {
|
|
120
|
+
'--color-bg': '#fff',
|
|
121
|
+
'--font-family-body': 'Helvetica, Arial, sans-serif',
|
|
122
|
+
},
|
|
123
|
+
};\n`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const result = await themeBuild('sys.mjs', {}, {cwd: tmpDir});
|
|
127
|
+
|
|
128
|
+
expect(result?.type).toBe('theme.build');
|
|
129
|
+
expect(result?.data.warnings).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('stays silent under the default noopLogger even when font warnings fire', async () => {
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(tmpDir, 'loud.mjs'),
|
|
135
|
+
`export default { name: 'loud', tokens: { '--font-family-body': '"Orbitron", sans-serif' } };\n`,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
139
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
140
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
141
|
+
const outSpy = vi
|
|
142
|
+
.spyOn(process.stdout, 'write')
|
|
143
|
+
.mockImplementation(() => true);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const result = await themeBuild('loud.mjs', {}, {cwd: tmpDir});
|
|
147
|
+
expect(result?.data.warnings).toEqual(
|
|
148
|
+
expect.arrayContaining([expect.stringContaining('Font "Orbitron"')]),
|
|
149
|
+
);
|
|
150
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
151
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
152
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
153
|
+
expect(outSpy).not.toHaveBeenCalled();
|
|
154
|
+
} finally {
|
|
155
|
+
logSpy.mockRestore();
|
|
156
|
+
warnSpy.mockRestore();
|
|
157
|
+
errSpy.mockRestore();
|
|
158
|
+
outSpy.mockRestore();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* - A JS module that re-exports the built theme (+ icon registry)
|
|
12
12
|
* - A .d.ts (plus an optional .variants.d.ts for custom prop values)
|
|
13
13
|
*
|
|
14
|
-
* It performs the writes and returns a `theme.build` receipt
|
|
15
|
-
*
|
|
14
|
+
* It performs the writes and returns a `theme.build` receipt — its `warnings`
|
|
15
|
+
* carry override problems and any fonts the theme names but does not load
|
|
16
|
+
* (font-warning.mjs) — or `null` when the theme produced no CSS (nothing to
|
|
17
|
+
* build). Errors throw AstryxError (with
|
|
16
18
|
* a stable code). Human progress is emitted through the shared `logger`
|
|
17
19
|
* (silent by default), so the CLI keeps its exact output while a programmatic
|
|
18
20
|
* caller stays quiet.
|
|
@@ -41,6 +43,10 @@ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
|
41
43
|
import {AstryxError} from '../../error.mjs';
|
|
42
44
|
import {logger} from '../../logger.mjs';
|
|
43
45
|
import {loadComponentDoc} from '../../../foundation/discovery/component-loader.mjs';
|
|
46
|
+
import {
|
|
47
|
+
collectUnloadedFonts,
|
|
48
|
+
formatFontLoadingHelp,
|
|
49
|
+
} from './font-warning.mjs';
|
|
44
50
|
|
|
45
51
|
// Import shared theme processing from core. `astryx theme build` MUST produce the
|
|
46
52
|
// exact same CSS as the `<Theme>` runtime, so it has exactly one generation
|
|
@@ -1341,16 +1347,17 @@ Or with a <link> tag:
|
|
|
1341
1347
|
</Theme>
|
|
1342
1348
|
`);
|
|
1343
1349
|
|
|
1344
|
-
//
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1350
|
+
// Fonts the theme names but nothing loads (#5015). Resolved tokens and
|
|
1351
|
+
// component overrides carry the final font-family values on both load
|
|
1352
|
+
// paths, so this sees jiti-resolved and legacy themes alike.
|
|
1353
|
+
const unloadedFonts = collectUnloadedFonts(resolvedTheme);
|
|
1354
|
+
for (const family of unloadedFonts) {
|
|
1355
|
+
const msg = `Font "${family}" is named by this theme but not loaded — add a <link> or @font-face in your app (recipe: astryx docs typography)`;
|
|
1356
|
+
warningMessages.push(msg);
|
|
1357
|
+
logger.warn(` ⚠ ${msg}`);
|
|
1358
|
+
}
|
|
1359
|
+
if (unloadedFonts.length > 0) {
|
|
1360
|
+
logger.log(formatFontLoadingHelp(themeDef.name, unloadedFonts));
|
|
1354
1361
|
}
|
|
1355
1362
|
|
|
1356
1363
|
return {
|
|
@@ -254,6 +254,28 @@ describe('themeBuild() — component override validation', () => {
|
|
|
254
254
|
expect(result?.data.warnings).toEqual([]);
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
it('accepts the heading type rules a type scale generates', async () => {
|
|
258
|
+
// `typography.scale` makes defineTheme emit `heading: {'type:display-1' …}`
|
|
259
|
+
// (Heading renders a `type:` class alongside `level:`), so any theme with a
|
|
260
|
+
// type scale carried override keys the validator called unknown — including
|
|
261
|
+
// the shipped neutralTheme.
|
|
262
|
+
const themeFile = path.join(tmpDir, 'typescale.mjs');
|
|
263
|
+
fs.writeFileSync(
|
|
264
|
+
themeFile,
|
|
265
|
+
`export default {
|
|
266
|
+
name: 'typescale',
|
|
267
|
+
tokens: {'--color-bg': '#0a0a0a'},
|
|
268
|
+
components: {
|
|
269
|
+
heading: {'type:display-1': {letterSpacing: '0.01em'}},
|
|
270
|
+
},
|
|
271
|
+
};\n`,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const result = await themeBuild('typescale.mjs', {}, {cwd: tmpDir});
|
|
275
|
+
|
|
276
|
+
expect(result?.data.warnings).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
|
|
257
279
|
it('still warns on a key that is neither a visual prop nor a state', async () => {
|
|
258
280
|
// Widening the known set to states must not turn the guard off.
|
|
259
281
|
const themeFile = path.join(tmpDir, 'bogus.mjs');
|
|
@@ -273,3 +295,34 @@ describe('themeBuild() — component override validation', () => {
|
|
|
273
295
|
]);
|
|
274
296
|
});
|
|
275
297
|
});
|
|
298
|
+
|
|
299
|
+
describe('themeBuild() — the shipped theme template', () => {
|
|
300
|
+
// `assets/theme.template.ts` is what `astryx theme template` puts in a
|
|
301
|
+
// consumer's project. It is the one theme file we hand out, so it has to
|
|
302
|
+
// compile as shipped — and cleanly apart from the font warnings it earns on
|
|
303
|
+
// purpose: a template that greets its first reader with warnings teaches them
|
|
304
|
+
// to ignore warnings. The claims its comments make are checked separately by
|
|
305
|
+
// scripts/check-theme-template.test.mjs.
|
|
306
|
+
it('compiles as shipped, warning only about the fonts it deliberately names', async () => {
|
|
307
|
+
const src = path.resolve(
|
|
308
|
+
import.meta.dirname,
|
|
309
|
+
'../../../assets/theme.template.ts',
|
|
310
|
+
);
|
|
311
|
+
fs.copyFileSync(src, path.join(tmpDir, 'theme.template.ts'));
|
|
312
|
+
|
|
313
|
+
const result = await themeBuild('theme.template.ts', {}, {cwd: tmpDir});
|
|
314
|
+
|
|
315
|
+
// The template names Inter and Geist Mono to teach "SHIP THE FONTS YOU
|
|
316
|
+
// NAME", and loads neither — so the unloaded-font warning firing here is
|
|
317
|
+
// the lesson landing, not a defect. Any OTHER warning still fails.
|
|
318
|
+
const unexpected = (result?.data.warnings ?? []).filter(
|
|
319
|
+
w => !/^Font "(Inter|Geist Mono)" is named by this theme but not loaded/.test(w),
|
|
320
|
+
);
|
|
321
|
+
expect(unexpected).toEqual([]);
|
|
322
|
+
expect(result?.data.warnings).toHaveLength(2);
|
|
323
|
+
expect(fs.existsSync(path.join(tmpDir, 'my-theme.css'))).toBe(true);
|
|
324
|
+
// The template teaches custom variants; the augmentation it promises the
|
|
325
|
+
// reader has to actually be generated.
|
|
326
|
+
expect(fs.existsSync(path.join(tmpDir, 'my-theme.variants.d.ts'))).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* Collect the font families a resolved theme names but does not load —
|
|
6
|
+
* every `--font-family-*` token plus every component-override `fontFamily`,
|
|
7
|
+
* minus generics, CSS-wide keywords, `var()` references, and known system
|
|
8
|
+
* families. Source order, first-seen casing, deduped case-insensitively.
|
|
9
|
+
*
|
|
10
|
+
* @param {{tokens?: Record<string, string>, components?: object}} resolvedTheme
|
|
11
|
+
* @returns {string[]}
|
|
12
|
+
*/
|
|
13
|
+
export function collectUnloadedFonts(resolvedTheme: {
|
|
14
|
+
tokens?: Record<string, string>;
|
|
15
|
+
components?: object;
|
|
16
|
+
}): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Render the human fix printed after the install instructions: which fonts
|
|
19
|
+
* the theme names but does not load, the Google Fonts `<link>` recipe, and
|
|
20
|
+
* the self-hosted `@font-face` alternative.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} themeName
|
|
23
|
+
* @param {string[]} families
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function formatFontLoadingHelp(themeName: string, families: string[]): string;
|