@astryxdesign/cli 0.4.2 → 0.4.3-canary.07f9998
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 +23 -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/build/build.font-warning.test.mjs +23 -14
- package/api/theme/build/build.icons-specifier.test.mjs +149 -0
- package/api/theme/build/build.mjs +88 -16
- package/api/theme/build/build.test.mjs +185 -14
- package/api/theme/theme.type.d.mts +22 -0
- package/api/theme/theme.type.mjs +15 -1
- package/assets/cdn.template.html +124 -0
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +11 -5
- 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/templates/blocks/components/ComplexSelector/ComplexSelectorDeadlinePicker.doc.mjs +20 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorDeadlinePicker.tsx +87 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.tsx +199 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorTreeSearch.doc.mjs +14 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorTreeSearch.tsx +188 -0
- package/assets/templates/themes/neutral/neutralTheme.ts +6 -4
- package/assets/theme.template.ts +4 -3
- package/authoring/doctypes/base/type.ts +9 -0
- package/clients/cli/commands/build-theme.font-warning.test.mjs +9 -6
- package/clients/cli/commands/build-theme.icons-specifier.test.mjs +132 -7
- 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/fs/path-safety.mjs +2 -2
- package/foundation/fs/path-safety.test.mjs +7 -0
- 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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file CLI behavior for `astryx template --cdn`.
|
|
5
|
+
*
|
|
6
|
+
* The API leaf is covered by api/template/cdn/cdn.test.mjs; what is only
|
|
7
|
+
* reachable here is the terminal binding — that the flag short-circuits the
|
|
8
|
+
* family's name resolution, which message the user sees, and the JSON envelope.
|
|
9
|
+
* A receipt read at the wrong depth (`result.written` instead of
|
|
10
|
+
* `result.data.written`) writes the file and then reports a skip, and no unit
|
|
11
|
+
* test of the leaf can see it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import * as os from 'node:os';
|
|
18
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
19
|
+
|
|
20
|
+
let tmpDir;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-cli-template-cdn-'));
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(tmpDir, 'package.json'),
|
|
25
|
+
JSON.stringify({name: 'tmp', private: true}),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const read = f => fs.readFileSync(path.join(tmpDir, f), 'utf-8');
|
|
33
|
+
|
|
34
|
+
describe('astryx template --cdn', () => {
|
|
35
|
+
it('writes the page and says it wrote it', async () => {
|
|
36
|
+
const {status, stdout} = await runCli(['template', '--cdn'], {cwd: tmpDir});
|
|
37
|
+
|
|
38
|
+
expect(status).toBe(0);
|
|
39
|
+
expect(stdout).toMatch(/Wrote cdn\.template\.html/);
|
|
40
|
+
expect(stdout).not.toMatch(/already exists/);
|
|
41
|
+
expect(read('cdn.template.html')).toMatch(/<script type="importmap">/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('leaves an existing file alone, and says that instead', async () => {
|
|
45
|
+
fs.writeFileSync(path.join(tmpDir, 'cdn.template.html'), '<!-- mine -->\n');
|
|
46
|
+
|
|
47
|
+
const {status, stdout} = await runCli(['template', '--cdn'], {cwd: tmpDir});
|
|
48
|
+
|
|
49
|
+
expect(status).toBe(0);
|
|
50
|
+
expect(stdout).toMatch(/already exists/);
|
|
51
|
+
expect(read('cdn.template.html')).toBe('<!-- mine -->\n');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('replaces it with --overwrite', async () => {
|
|
55
|
+
fs.writeFileSync(path.join(tmpDir, 'cdn.template.html'), '<!-- mine -->\n');
|
|
56
|
+
|
|
57
|
+
const {status} = await runCli(['template', '--cdn', '--overwrite'], {
|
|
58
|
+
cwd: tmpDir,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(status).toBe(0);
|
|
62
|
+
expect(read('cdn.template.html')).toMatch(/importmap/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('writes to the path given to the flag', async () => {
|
|
66
|
+
const {status} = await runCli(['template', '--cdn', 'public/demo.html'], {
|
|
67
|
+
cwd: tmpDir,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(status).toBe(0);
|
|
71
|
+
expect(read(path.join('public', 'demo.html'))).toMatch(/importmap/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns a template.cdn envelope under --json', async () => {
|
|
75
|
+
const {status, stdout} = await runCli(['--json', 'template', '--cdn'], {
|
|
76
|
+
cwd: tmpDir,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(status).toBe(0);
|
|
80
|
+
const payload = JSON.parse(stdout);
|
|
81
|
+
expect(payload.type).toBe('template.cdn');
|
|
82
|
+
expect(payload.data).toEqual({
|
|
83
|
+
path: 'cdn.template.html',
|
|
84
|
+
version: expect.stringMatching(/^\d+\.\d+\.\d+/),
|
|
85
|
+
written: true,
|
|
86
|
+
reason: null,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('refuses a path that escapes the project', async () => {
|
|
91
|
+
const {status, stderr} = await runCli(
|
|
92
|
+
['template', '--cdn', '../escaped.html'],
|
|
93
|
+
{cwd: tmpDir},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(status).toBe(1);
|
|
97
|
+
expect(stderr).toMatch(/outside the project root/);
|
|
98
|
+
expect(fs.existsSync(path.join(path.dirname(tmpDir), 'escaped.html'))).toBe(
|
|
99
|
+
false,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// The flag exists because the positional cannot: `template cdn` would resolve
|
|
104
|
+
// `cdn` against everything discoverAll() finds, so a template with that id
|
|
105
|
+
// would shadow the starter page. The flag answers before discovery runs.
|
|
106
|
+
it('does not shadow, or get shadowed by, a discovered template id', async () => {
|
|
107
|
+
const {status, stderr} = await runCli(['template', 'cdn'], {cwd: tmpDir});
|
|
108
|
+
|
|
109
|
+
expect(status).toBe(1);
|
|
110
|
+
expect(stderr).toMatch(/Unknown template "cdn"/);
|
|
111
|
+
expect(fs.existsSync(path.join(tmpDir, 'cdn.template.html'))).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -18,7 +18,9 @@ export const doc = {
|
|
|
18
18
|
description:
|
|
19
19
|
'One entry point for the template family: with no name it lists the discovered ' +
|
|
20
20
|
'templates; with a name it shows the source or a layout skeleton, or scaffolds it ' +
|
|
21
|
-
'into the project at a target path. Narrow an ambiguous name with --type and/or --package.'
|
|
21
|
+
'into the project at a target path. Narrow an ambiguous name with --type and/or --package. ' +
|
|
22
|
+
'--cdn writes the no-build-step CDN starter page, which ships as an asset rather than as ' +
|
|
23
|
+
'a discovered template.',
|
|
22
24
|
fn: 'template',
|
|
23
25
|
args: [
|
|
24
26
|
{name: 'name', param: 'name', required: false},
|
|
@@ -43,6 +45,12 @@ export const doc = {
|
|
|
43
45
|
description:
|
|
44
46
|
'Show layout skeleton with spatial annotations (padding, gap, nesting)',
|
|
45
47
|
},
|
|
48
|
+
{
|
|
49
|
+
flag: '--cdn [path]',
|
|
50
|
+
param: 'options.cdn',
|
|
51
|
+
description:
|
|
52
|
+
'Write the no-build-step CDN starter page (default: cdn.template.html)',
|
|
53
|
+
},
|
|
46
54
|
{
|
|
47
55
|
flag: '-f, --overwrite',
|
|
48
56
|
param: 'options.overwrite',
|
|
@@ -55,9 +63,11 @@ export const doc = {
|
|
|
55
63
|
label: 'Scaffold into the app',
|
|
56
64
|
cli: 'astryx template dashboard ./src/app',
|
|
57
65
|
},
|
|
66
|
+
{label: 'CDN starter page', cli: 'astryx template --cdn'},
|
|
67
|
+
{label: 'CDN starter page, elsewhere', cli: 'astryx template --cdn public/demo.html'},
|
|
58
68
|
],
|
|
59
69
|
exitCodes: [
|
|
60
|
-
{code: 0, when: 'success'},
|
|
70
|
+
{code: 0, when: 'success, including a CDN page left untouched because it already exists'},
|
|
61
71
|
{
|
|
62
72
|
code: 1,
|
|
63
73
|
when: 'unknown or ambiguous template, no source, a path escape, or an existing target without --overwrite',
|
|
@@ -41,7 +41,8 @@ export {discoverTemplates, listTemplates} from '../../../api/template/template.m
|
|
|
41
41
|
* import('../../../api/template/template.type.mjs').TemplateListResponse |
|
|
42
42
|
* import('../../../api/template/template.type.mjs').TemplateShowResponse |
|
|
43
43
|
* import('../../../api/template/template.type.mjs').TemplateSkeletonResponse |
|
|
44
|
-
* import('../../../api/template/template.type.mjs').TemplateCopyResponse
|
|
44
|
+
* import('../../../api/template/template.type.mjs').TemplateCopyResponse |
|
|
45
|
+
* import('../../../api/template/template.type.mjs').TemplateCdnResponse
|
|
45
46
|
* )} TemplateResponse
|
|
46
47
|
*/
|
|
47
48
|
|
|
@@ -55,7 +56,7 @@ export function registerTemplate(program) {
|
|
|
55
56
|
/**
|
|
56
57
|
* @param {string | undefined} name
|
|
57
58
|
* @param {string | undefined} targetPath
|
|
58
|
-
* @param {{list?: boolean, type?: string, package?: string, skeleton?: boolean, overwrite?: boolean}} options
|
|
59
|
+
* @param {{list?: boolean, type?: string, package?: string, skeleton?: boolean, cdn?: boolean | string, overwrite?: boolean}} options
|
|
59
60
|
*/
|
|
60
61
|
async (name, targetPath, options) => {
|
|
61
62
|
const json = program.opts().json || false;
|
|
@@ -83,7 +84,8 @@ export function registerTemplate(program) {
|
|
|
83
84
|
name &&
|
|
84
85
|
targetPath &&
|
|
85
86
|
!options.list &&
|
|
86
|
-
!options.skeleton
|
|
87
|
+
!options.skeleton &&
|
|
88
|
+
!options.cdn
|
|
87
89
|
) {
|
|
88
90
|
const collision = await detectTemplateCollision(name, targetPath);
|
|
89
91
|
if (collision && !options.overwrite) {
|
|
@@ -103,6 +105,7 @@ export function registerTemplate(program) {
|
|
|
103
105
|
await templateApi(name, {
|
|
104
106
|
list: options.list,
|
|
105
107
|
skeleton: options.skeleton,
|
|
108
|
+
cdn: options.cdn,
|
|
106
109
|
type: /** @type {'page' | 'block' | undefined} */ (options.type),
|
|
107
110
|
package: options.package,
|
|
108
111
|
targetPath,
|
|
@@ -147,6 +150,7 @@ export function registerTemplate(program) {
|
|
|
147
150
|
`${run} template <id> --skeleton Layout reference`,
|
|
148
151
|
`${run} template --list --type block List only blocks`,
|
|
149
152
|
`${run} template --list --package <pkg> List from one package`,
|
|
153
|
+
`${run} template --cdn CDN starter page, no build step`,
|
|
150
154
|
].join('\n'),
|
|
151
155
|
),
|
|
152
156
|
);
|
|
@@ -179,6 +183,23 @@ export function registerTemplate(program) {
|
|
|
179
183
|
);
|
|
180
184
|
break;
|
|
181
185
|
}
|
|
186
|
+
|
|
187
|
+
case 'template.cdn': {
|
|
188
|
+
if (!result.data.written) {
|
|
189
|
+
emit(
|
|
190
|
+
text(`[skip] ${result.data.path} already exists — left as is.`),
|
|
191
|
+
text('Pass --overwrite to replace it with a fresh copy.'),
|
|
192
|
+
);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
emit(
|
|
196
|
+
text(`[ok] Wrote ${result.data.path}`),
|
|
197
|
+
text(
|
|
198
|
+
`Open it in a browser — no bundler, no install, no build step. Every CDN URL is pinned to ${result.data.version}, and the annotations mark the parts that are load-bearing.`,
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
182
203
|
}
|
|
183
204
|
},
|
|
184
205
|
});
|
|
@@ -14,17 +14,19 @@ export const doc = {
|
|
|
14
14
|
name: 'theme build',
|
|
15
15
|
displayName: 'astryx theme build',
|
|
16
16
|
namespace: 'cli',
|
|
17
|
-
summary: 'Compile
|
|
17
|
+
summary: 'Compile one or more defineTheme files to CSS + JS',
|
|
18
18
|
description:
|
|
19
19
|
'Compiles a file that calls defineTheme() into a scoped CSS file, a JS module, and ' +
|
|
20
|
-
'type declarations: the exact CSS the <Theme> runtime emits.
|
|
20
|
+
'type declarations: the exact CSS the <Theme> runtime emits. Takes any number of theme ' +
|
|
21
|
+
'files and compiles them in one process, in argument order, stopping at the first ' +
|
|
22
|
+
'failure — an app with several themes does not need a shell loop. With --check it writes ' +
|
|
21
23
|
'nothing and instead reports whether the committed outputs have drifted from source. ' +
|
|
22
24
|
'When a separate build step emits the icon registry, --icons-specifier declares the ' +
|
|
23
25
|
'fully specified module path that the generated JS should import.',
|
|
24
26
|
fn: 'themeBuild',
|
|
25
|
-
args: [{name: '
|
|
27
|
+
args: [{name: 'files', param: 'file', required: true, variadic: true}],
|
|
26
28
|
options: [
|
|
27
|
-
{flag: '-o, --out <path>', param: 'options.out', description: 'Output CSS file path'},
|
|
29
|
+
{flag: '-o, --out <path>', param: 'options.out', description: 'Output CSS file path (single theme only)'},
|
|
28
30
|
{
|
|
29
31
|
flag: '--icons-specifier <specifier>',
|
|
30
32
|
param: 'options.iconsSpecifier',
|
|
@@ -33,7 +35,7 @@ export const doc = {
|
|
|
33
35
|
},
|
|
34
36
|
{
|
|
35
37
|
flag: '-w, --watch',
|
|
36
|
-
description: 'Rebuild automatically when
|
|
38
|
+
description: 'Rebuild automatically when a theme file changes (Ctrl-C to stop)',
|
|
37
39
|
},
|
|
38
40
|
{
|
|
39
41
|
flag: '-c, --check',
|
|
@@ -47,6 +49,10 @@ export const doc = {
|
|
|
47
49
|
label: 'Build to a CSS file',
|
|
48
50
|
cli: 'astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css',
|
|
49
51
|
},
|
|
52
|
+
{
|
|
53
|
+
label: 'Build every theme in a directory',
|
|
54
|
+
cli: 'astryx theme build ./src/themes/*.ts',
|
|
55
|
+
},
|
|
50
56
|
{
|
|
51
57
|
label: 'Check for drift (CI)',
|
|
52
58
|
cli: 'astryx theme build ./src/themes/ocean.ts --check',
|
|
@@ -61,7 +61,7 @@ describe('--json shim: --help renders JSON envelope', () => {
|
|
|
61
61
|
expect(parsed.apiVersion).toBe(1);
|
|
62
62
|
expect(parsed.type).toBe('help');
|
|
63
63
|
expect(parsed.data.command).toBe('astryx theme build');
|
|
64
|
-
expect(parsed.data.usage).toMatch(/<
|
|
64
|
+
expect(parsed.data.usage).toMatch(/<files\.\.\.>/);
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -70,9 +70,10 @@ export const RESPONSE_TYPES = {
|
|
|
70
70
|
'template.show',
|
|
71
71
|
'template.skeleton',
|
|
72
72
|
'template.copy',
|
|
73
|
+
'template.cdn',
|
|
73
74
|
],
|
|
74
75
|
hook: ['hook.list', 'hook.detail', 'hook.detail.params'],
|
|
75
|
-
'theme build': ['theme.build', 'theme.build.check'],
|
|
76
|
+
'theme build': ['theme.build', 'theme.build.check', 'theme.build.batch'],
|
|
76
77
|
'theme list': ['theme.list'],
|
|
77
78
|
'theme add': ['theme.list', 'theme.add'],
|
|
78
79
|
'theme template': ['theme.template'],
|
|
@@ -103,7 +104,7 @@ const EXAMPLES = {
|
|
|
103
104
|
],
|
|
104
105
|
build: ['astryx build', 'astryx build "analytics dashboard" --json'],
|
|
105
106
|
swizzle: ['astryx swizzle XDSButton'],
|
|
106
|
-
template: ['astryx template --json', 'astryx template dashboard ./src/app'],
|
|
107
|
+
template: ['astryx template --json', 'astryx template dashboard ./src/app', 'astryx template --cdn'],
|
|
107
108
|
hook: ['astryx hook', 'astryx hook useFocusTrap --json'],
|
|
108
109
|
'theme build': [
|
|
109
110
|
'astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css',
|
|
@@ -146,8 +146,10 @@ describe('manifest: shape', () => {
|
|
|
146
146
|
const component = allEntries.find((c) => c.name === 'component');
|
|
147
147
|
expect(component.arguments.map((a) => a.name)).toContain('name');
|
|
148
148
|
const themeBuild = allEntries.find((c) => c.name === 'theme build');
|
|
149
|
-
expect(themeBuild.arguments.map((a) => a.name)).toContain('
|
|
150
|
-
|
|
149
|
+
expect(themeBuild.arguments.map((a) => a.name)).toContain('files');
|
|
150
|
+
const files = themeBuild.arguments.find((a) => a.name === 'files');
|
|
151
|
+
expect(files.required).toBe(true);
|
|
152
|
+
expect(files.variadic).toBe(true);
|
|
151
153
|
});
|
|
152
154
|
});
|
|
153
155
|
|
|
@@ -154,9 +154,9 @@ export function sanitizeName(name, options = {}) {
|
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
if (name === '.' || name === '..' || name.startsWith('
|
|
157
|
+
if (name === '.' || name === '..' || name.startsWith('.')) {
|
|
158
158
|
throw new PathSafetyError(
|
|
159
|
-
`Invalid ${label} "${name}": must not
|
|
159
|
+
`Invalid ${label} "${name}": must not start with '.'.`,
|
|
160
160
|
'NAME_TRAVERSAL',
|
|
161
161
|
);
|
|
162
162
|
}
|
|
@@ -91,6 +91,13 @@ describe('sanitizeName', () => {
|
|
|
91
91
|
expect(() => sanitizeName('.')).toThrow(PathSafetyError);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it('rejects dotfile names (.env, .htaccess) that could create hidden files', () => {
|
|
95
|
+
expect(() => sanitizeName('.env')).toThrow(PathSafetyError);
|
|
96
|
+
expect(() => sanitizeName('.htaccess')).toThrow(PathSafetyError);
|
|
97
|
+
expect(() => sanitizeName('.bashrc')).toThrow(PathSafetyError);
|
|
98
|
+
expect(() => sanitizeName('.gitignore')).toThrow(PathSafetyError);
|
|
99
|
+
});
|
|
100
|
+
|
|
94
101
|
it('rejects NUL bytes', () => {
|
|
95
102
|
expect(() => sanitizeName('foo\0bar')).toThrow(PathSafetyError);
|
|
96
103
|
});
|
|
@@ -68,6 +68,23 @@ describe('warnOnIntegrationIssues', () => {
|
|
|
68
68
|
expect(errLines).toHaveLength(0);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it('warns for a manifest that failed to load, which declares no roots', async () => {
|
|
72
|
+
// loadIntegrations records __loadError and no contribution roots, so every
|
|
73
|
+
// on-disk check finds nothing — without the load error itself counting as
|
|
74
|
+
// an issue, a stale manifest is silent everywhere.
|
|
75
|
+
const integration = {
|
|
76
|
+
...loaded({name: '@acme/stale'}),
|
|
77
|
+
__loadError: 'createIntegration is not a function',
|
|
78
|
+
};
|
|
79
|
+
await warnOnIntegrationIssues([integration], {json: false});
|
|
80
|
+
|
|
81
|
+
expect(errLines).toHaveLength(1);
|
|
82
|
+
expect(errLines[0]).toBe(
|
|
83
|
+
'Warning: @acme/stale has 1 integration issue(s). ' +
|
|
84
|
+
'Run: astryx validate-integration @acme/stale',
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
71
88
|
it('emits nothing for an integration with no issues', async () => {
|
|
72
89
|
// Existing components root with no broken contributions → no issues.
|
|
73
90
|
const componentsRoot = path.join(tmpDir, 'components');
|
|
@@ -149,6 +149,13 @@ export async function validateLoadedIntegration(loaded) {
|
|
|
149
149
|
/** @type {Issue[]} */
|
|
150
150
|
const issues = [];
|
|
151
151
|
if (!loaded || typeof loaded !== 'object') return issues;
|
|
152
|
+
// A manifest that threw on import (or failed the schema) carries a load-error
|
|
153
|
+
// marker and no contribution roots, so every check below would find nothing
|
|
154
|
+
// and report a clean integration — which is how a stale manifest used to go
|
|
155
|
+
// silently invisible. The load error IS the issue.
|
|
156
|
+
if (loaded.__loadError) {
|
|
157
|
+
return [issueError('integration_error', loaded.__loadError)];
|
|
158
|
+
}
|
|
152
159
|
checkRoots(
|
|
153
160
|
{
|
|
154
161
|
components: loaded.components,
|
|
@@ -150,6 +150,12 @@ export const doc = {
|
|
|
150
150
|
'A scaffold receipt: template id, output directory, written file name, and file count.',
|
|
151
151
|
},
|
|
152
152
|
|
|
153
|
+
{
|
|
154
|
+
value: 'template.cdn',
|
|
155
|
+
description:
|
|
156
|
+
'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.',
|
|
157
|
+
},
|
|
158
|
+
|
|
153
159
|
// hook
|
|
154
160
|
{
|
|
155
161
|
value: 'hook.list',
|
|
@@ -173,6 +179,11 @@ export const doc = {
|
|
|
173
179
|
description:
|
|
174
180
|
'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.',
|
|
175
181
|
},
|
|
182
|
+
{
|
|
183
|
+
value: 'theme.build.batch',
|
|
184
|
+
description:
|
|
185
|
+
'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.',
|
|
186
|
+
},
|
|
176
187
|
{
|
|
177
188
|
value: 'theme.list',
|
|
178
189
|
description:
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Every file in this repo carries the Meta copyright header, and several
|
|
8
8
|
* commands copy repo files out verbatim — `theme add` (bundled theme sources),
|
|
9
|
-
* `init --features theme` (the annotated theme template)
|
|
10
|
-
* source tree should not inherit our
|
|
11
|
-
* reject it.
|
|
9
|
+
* `init --features theme` (the annotated theme template), `template --cdn` (the
|
|
10
|
+
* CDN starter page). A consumer's own source tree should not inherit our
|
|
11
|
+
* boilerplate, and their lint may well reject it.
|
|
12
12
|
*
|
|
13
13
|
* SYNC: the same regex is applied by apps/docsite/src/lib/codeExamples.ts for
|
|
14
14
|
* rendered code samples. That copy lives in a different package (the docsite
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
const META_COPYRIGHT_HEADER_RE =
|
|
24
24
|
/^(\uFEFF?(?:#![^\r\n]*(?:\r?\n))?)\/\/ Copyright \(c\) Meta Platforms, Inc\. and affiliates\.\r?\n(?:\r?\n)*/;
|
|
25
25
|
|
|
26
|
+
/** The same header in HTML comment syntax, as scripts/add-copyright.sh writes
|
|
27
|
+
* it into the .html assets we scaffold. */
|
|
28
|
+
const META_COPYRIGHT_HEADER_HTML_RE =
|
|
29
|
+
/^(\uFEFF?)<!-- Copyright \(c\) Meta Platforms, Inc\. and affiliates\. -->\r?\n(?:\r?\n)*/;
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* Remove the leading Meta copyright header, preserving any BOM/shebang before
|
|
28
33
|
* it. Returns the source unchanged when the header is absent.
|
|
@@ -31,5 +36,7 @@ const META_COPYRIGHT_HEADER_RE =
|
|
|
31
36
|
* @returns {string}
|
|
32
37
|
*/
|
|
33
38
|
export function stripCopyrightHeader(source) {
|
|
34
|
-
return source
|
|
39
|
+
return source
|
|
40
|
+
.replace(META_COPYRIGHT_HEADER_RE, '$1')
|
|
41
|
+
.replace(META_COPYRIGHT_HEADER_HTML_RE, '$1');
|
|
35
42
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3-canary.07f9998",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"agent-docs"
|
|
26
26
|
],
|
|
27
27
|
"type": "module",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.13.0"
|
|
30
|
+
},
|
|
28
31
|
"bin": {
|
|
29
32
|
"astryx": "./clients/cli/bin/astryx.mjs",
|
|
30
33
|
"cli": "./clients/cli/bin/astryx.mjs"
|
|
@@ -84,10 +87,10 @@
|
|
|
84
87
|
"zod": "^4.4.3"
|
|
85
88
|
},
|
|
86
89
|
"peerDependencies": {
|
|
87
|
-
"@astryxdesign/charts": "
|
|
88
|
-
"@astryxdesign/core": "
|
|
89
|
-
"@astryxdesign/lab": "
|
|
90
|
-
"@astryxdesign/theme-neutral": "
|
|
90
|
+
"@astryxdesign/charts": "0.4.3-canary.07f9998",
|
|
91
|
+
"@astryxdesign/core": "0.4.3-canary.07f9998",
|
|
92
|
+
"@astryxdesign/lab": "0.4.3-canary.07f9998",
|
|
93
|
+
"@astryxdesign/theme-neutral": "0.4.3-canary.07f9998",
|
|
91
94
|
"gpt-tokenizer": "^3.4.0"
|
|
92
95
|
},
|
|
93
96
|
"peerDependenciesMeta": {
|
|
@@ -105,10 +108,10 @@
|
|
|
105
108
|
}
|
|
106
109
|
},
|
|
107
110
|
"devDependencies": {
|
|
108
|
-
"@astryxdesign/charts": "
|
|
109
|
-
"@astryxdesign/core": "
|
|
110
|
-
"@astryxdesign/lab": "
|
|
111
|
-
"@astryxdesign/theme-neutral": "
|
|
111
|
+
"@astryxdesign/charts": "0.4.3-canary.07f9998",
|
|
112
|
+
"@astryxdesign/core": "0.4.3-canary.07f9998",
|
|
113
|
+
"@astryxdesign/lab": "0.4.3-canary.07f9998",
|
|
114
|
+
"@astryxdesign/theme-neutral": "0.4.3-canary.07f9998",
|
|
112
115
|
"gpt-tokenizer": "^3.4.0"
|
|
113
116
|
},
|
|
114
117
|
"scripts": {
|