@astryxdesign/cli 0.4.1 → 0.4.2-canary.356d2f9

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -0
  3. package/api/init/init.doc.mjs +2 -2
  4. package/api/init/init.test.mjs +19 -2
  5. package/api/init/init.type.d.mts +9 -1
  6. package/api/init/init.type.mjs +3 -1
  7. package/api/init/run/run.mjs +36 -6
  8. package/api/theme/add/add.mjs +2 -13
  9. package/api/theme/build/build.font-warning.test.mjs +161 -0
  10. package/api/theme/build/build.mjs +95 -26
  11. package/api/theme/build/build.test.mjs +227 -0
  12. package/api/theme/build/font-warning.d.mts +26 -0
  13. package/api/theme/build/font-warning.mjs +214 -0
  14. package/api/theme/build/font-warning.test.mjs +242 -0
  15. package/api/theme/template/template.d.mts +21 -0
  16. package/api/theme/template/template.mjs +69 -0
  17. package/api/theme/template/template.test.mjs +82 -0
  18. package/api/theme/theme.d.mts +1 -0
  19. package/api/theme/theme.mjs +1 -0
  20. package/api/theme/theme.type.d.mts +13 -0
  21. package/api/theme/theme.type.mjs +11 -1
  22. package/api/theme/themeTemplate.doc.d.mts +11 -0
  23. package/api/theme/themeTemplate.doc.mjs +64 -0
  24. package/assets/docs/getting-started.doc.mjs +11 -1
  25. package/assets/docs/theme.doc.dense.mjs +2 -2
  26. package/assets/docs/theme.doc.mjs +26 -10
  27. package/assets/docs/theme.doc.zh.mjs +1 -1
  28. package/assets/docs/typography.doc.mjs +38 -0
  29. package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.doc.mjs +13 -0
  30. package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.tsx +55 -0
  31. package/assets/templates/pages/ai-chat/page.tsx +18 -13
  32. package/assets/templates/themes/butter/butterTheme.ts +4 -1
  33. package/assets/templates/themes/chocolate/chocolateTheme.ts +4 -1
  34. package/assets/templates/themes/gothic/gothicTheme.ts +4 -1
  35. package/assets/templates/themes/stone/stoneTheme.ts +4 -1
  36. package/assets/theme.template.ts +322 -0
  37. package/authoring/doctypes/base/type.ts +9 -1
  38. package/authoring/doctypes/component/component.doc.mjs +1 -1
  39. package/clients/cli/commands/build-theme.font-warning.test.mjs +138 -0
  40. package/clients/cli/commands/build-theme.mjs +50 -0
  41. package/clients/cli/commands/init.behavior.test.mjs +13 -2
  42. package/clients/cli/commands/theme-template.behavior.test.mjs +85 -0
  43. package/clients/cli/commands/theme-template.doc.mjs +42 -0
  44. package/clients/cli/commands/theme.doc.mjs +2 -2
  45. package/clients/cli/index.mjs +1 -0
  46. package/clients/cli/lib/manifest.mjs +2 -0
  47. package/foundation/agent-docs/agent-docs.mjs +1 -1
  48. package/foundation/response/response-types.doc.mjs +5 -0
  49. package/foundation/text/copyright-header.d.mts +11 -0
  50. package/foundation/text/copyright-header.mjs +35 -0
  51. package/foundation/text/copyright-header.test.mjs +58 -0
  52. package/package.json +9 -9
@@ -0,0 +1,138 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file End-to-end test for the `astryx theme build` font-loading warning
5
+ * (#5015). A theme that names webfont families gets, AFTER the install
6
+ * instructions, a stdout notice naming the fonts plus the copy-pasteable
7
+ * fix — the Google Fonts <link> pair and a self-hosted @font-face with
8
+ * font-display: swap — and still exits 0 (it is a warning, not an error).
9
+ * Themes that only name generics or known system stacks get none of it.
10
+ */
11
+
12
+ import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import * as os from 'node:os';
16
+ import {ensureCoreBuilt} from './ensure-core-built.mjs';
17
+ import {runCli} from '../../../test-utils/run-cli.mjs';
18
+
19
+ function writeTheme(dir, name, source) {
20
+ fs.mkdirSync(dir, {recursive: true});
21
+ const file = path.join(dir, `${name}.mjs`);
22
+ fs.writeFileSync(file, source);
23
+ return file;
24
+ }
25
+
26
+ // `astryx theme build` imports the compiled @astryxdesign/core/theme entry.
27
+ // Build core once if it isn't already present so the suite works in any CI
28
+ // job, regardless of job ordering.
29
+ beforeAll(() => {
30
+ ensureCoreBuilt();
31
+ }, 200_000);
32
+
33
+ let tmpDir;
34
+ beforeEach(() => {
35
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-fonts-'));
36
+ });
37
+ afterEach(() => {
38
+ fs.rmSync(tmpDir, {recursive: true, force: true});
39
+ });
40
+
41
+ describe('theme build font-loading warning', () => {
42
+ it('prints the unloaded fonts and the <link>/@font-face fix after the install instructions', async () => {
43
+ const project = path.join(tmpDir, 'project');
44
+ const themeFile = writeTheme(
45
+ project,
46
+ 'fonty',
47
+ `export default {
48
+ name: 'fonty',
49
+ typography: {
50
+ body: {family: 'Space Grotesk', fallbacks: 'Arial, sans-serif'},
51
+ code: {family: 'JetBrains Mono'},
52
+ },
53
+ };\n`,
54
+ );
55
+
56
+ const result = await runCli(
57
+ ['theme', 'build', path.relative(project, themeFile)],
58
+ project,
59
+ );
60
+
61
+ expect(result.code).toBe(0);
62
+ // The install instructions still come out intact…
63
+ expect(result.stdout).toContain("from './fonty'");
64
+ // …followed by the warning naming the theme and every unloaded family…
65
+ expect(result.stdout).toContain('names fonts it does not load');
66
+ expect(result.stdout).toContain('"Space Grotesk"');
67
+ expect(result.stdout).toContain('"JetBrains Mono"');
68
+ // …and the copy-pasteable fix, both flavors.
69
+ expect(result.stdout).toContain(
70
+ '<link rel="preconnect" href="https://fonts.googleapis.com"',
71
+ );
72
+ expect(result.stdout).toContain('family=Space+Grotesk');
73
+ expect(result.stdout).toContain('family=JetBrains+Mono');
74
+ expect(result.stdout).toContain('display=swap');
75
+ expect(result.stdout).toContain('@font-face');
76
+ expect(result.stdout).toContain('font-display: swap');
77
+ expect(result.stdout).toContain('astryx docs typography');
78
+ // The one-line summaries follow the CLI's stream contract: warnings on
79
+ // stderr, like the override-validation warnings in the same build.
80
+ expect(result.stderr).toContain('Font "Space Grotesk"');
81
+ expect(result.stderr).toContain('Font "JetBrains Mono"');
82
+ });
83
+
84
+ it('keeps --json stdout one valid envelope: warnings inside, snippet suppressed', async () => {
85
+ const project = path.join(tmpDir, 'project');
86
+ const themeFile = writeTheme(
87
+ project,
88
+ 'fonty',
89
+ `export default { name: 'fonty', tokens: { '--font-family-body': '"Space Grotesk", sans-serif' } };\n`,
90
+ );
91
+
92
+ const result = await runCli(
93
+ ['--json', 'theme', 'build', path.relative(project, themeFile)],
94
+ project,
95
+ );
96
+
97
+ expect(result.code).toBe(0);
98
+ // The whole stdout must parse — any human snippet leaking into --json
99
+ // mode corrupts the envelope, so this asserts the "JSON is always JSON"
100
+ // contract, not just substring presence.
101
+ const envelope = JSON.parse(result.stdout);
102
+ expect(envelope.type).toBe('theme.build');
103
+ expect(envelope.data.warnings).toEqual(
104
+ expect.arrayContaining([expect.stringContaining('Font "Space Grotesk"')]),
105
+ );
106
+ expect(result.stdout).not.toContain('fonts.googleapis.com');
107
+ // Human one-liners are silenced too — machine mode stays quiet.
108
+ expect(result.stderr).not.toContain('Font "');
109
+ });
110
+
111
+ it('prints nothing font-related for a theme of generics and system stacks', async () => {
112
+ const project = path.join(tmpDir, 'project');
113
+ const themeFile = writeTheme(
114
+ project,
115
+ 'sys',
116
+ `export default {
117
+ name: 'sys',
118
+ tokens: {
119
+ '--color-bg': '#fff',
120
+ '--font-family-body': 'Helvetica, Arial, sans-serif',
121
+ '--font-family-code': 'ui-monospace',
122
+ },
123
+ };\n`,
124
+ );
125
+
126
+ const result = await runCli(
127
+ ['theme', 'build', path.relative(project, themeFile)],
128
+ project,
129
+ );
130
+
131
+ expect(result.code).toBe(0);
132
+ expect(result.stdout).toContain("from './sys'");
133
+ expect(result.stdout).not.toContain('names fonts it does not load');
134
+ expect(result.stdout).not.toContain('fonts.googleapis.com');
135
+ expect(result.stdout).not.toContain('@font-face');
136
+ expect(result.stderr).not.toContain('Font "');
137
+ });
138
+ });
@@ -30,6 +30,7 @@ import {logger} from '../../../api/logger.mjs';
30
30
  import {cliError} from '../lib/cli-error.mjs';
31
31
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
32
32
  import {themeAdd} from '../../../api/theme/add/add.mjs';
33
+ import {themeTemplate} from '../../../api/theme/template/template.mjs';
33
34
  import {themeList} from '../../../api/theme/list/list.mjs';
34
35
  import {themeBuild, importSpecifier} from '../../../api/theme/build/build.mjs';
35
36
  import {defineCommand} from '../lib/define-command.mjs';
@@ -37,9 +38,11 @@ import {doc as themeGroup} from './theme.doc.mjs';
37
38
  import {doc as themeBuildCommand} from './theme-build.doc.mjs';
38
39
  import {doc as themeListCommand} from './theme-list.doc.mjs';
39
40
  import {doc as themeAddCommand} from './theme-add.doc.mjs';
41
+ import {doc as themeTemplateCommand} from './theme-template.doc.mjs';
40
42
  import {doc as themeBuildFn} from '../../../api/theme/themeBuild.doc.mjs';
41
43
  import {doc as themeListFn} from '../../../api/theme/themeList.doc.mjs';
42
44
  import {doc as themeAddFn} from '../../../api/theme/themeAdd.doc.mjs';
45
+ import {doc as themeTemplateFn} from '../../../api/theme/themeTemplate.doc.mjs';
43
46
 
44
47
  /**
45
48
  * Path to this CLI's real entry (clients/cli/bin/astryx.mjs), resolved from
@@ -363,4 +366,51 @@ export function registerTheme(program) {
363
366
  );
364
367
  },
365
368
  });
369
+
370
+ defineCommand(theme, themeTemplateCommand, {
371
+ fn: themeTemplateFn,
372
+ action: (
373
+ /** @type {string | undefined} */ targetPath,
374
+ /** @type {{overwrite?: boolean}} */ options,
375
+ ) => {
376
+ const json = program.opts().json || false;
377
+
378
+ /** @type {import('../../../api/theme/theme.type.mjs').ThemeTemplateResponse} */
379
+ let result;
380
+ try {
381
+ result = themeTemplate({
382
+ targetPath,
383
+ overwrite: options.overwrite,
384
+ cwd: process.cwd(),
385
+ });
386
+ } catch (e) {
387
+ const err =
388
+ /** @type {import('../../../api/error.mjs').AstryxError} */ (e);
389
+ cliError(err.message, {
390
+ suggestions: err.suggestions || [],
391
+ code: err.code,
392
+ });
393
+ return;
394
+ }
395
+
396
+ if (json) return jsonOut(result);
397
+
398
+ const invocation = getCliInvocation(process.cwd());
399
+ if (!result.data.written) {
400
+ emit(
401
+ text(`[skip] ${result.data.path} already exists — left as is.`),
402
+ text(`Pass --overwrite to replace it with a fresh copy.`),
403
+ );
404
+ return;
405
+ }
406
+ emit(
407
+ text(`[ok] Wrote ${result.data.path}`),
408
+ text(
409
+ 'It documents every defineTheme field, the token families, and the component override syntax. ' +
410
+ 'Copy what you need into your own theme file, then delete it.',
411
+ ),
412
+ code(`${invocation} theme build ${result.data.path}`),
413
+ );
414
+ },
415
+ });
366
416
  }
@@ -71,13 +71,24 @@ describe('astryx init --features', () => {
71
71
  expect(exists('AGENTS.md')).toBe(true);
72
72
  });
73
73
 
74
- it('--features theme writes no files and points at the theme workflow', async () => {
74
+ it('--features theme writes the annotated theme template', async () => {
75
75
  const {status, stdout} = await runCli(['init', '--features', 'theme'], {cwd: tmpDir});
76
76
  expect(status).toBe(0);
77
- expect(fs.readdirSync(tmpDir)).toEqual([]);
77
+ expect(exists('theme.template.ts')).toBe(true);
78
+ expect(read('theme.template.ts')).toMatch(/defineTheme/);
79
+ expect(read('theme.template.ts')).not.toMatch(/Copyright \(c\) Meta Platforms/);
78
80
  expect(stdout).toMatch(/theme/i);
79
81
  });
80
82
 
83
+ it('--features theme never clobbers an existing theme.template.ts', async () => {
84
+ // Someone's edited copy outranks ours; re-running init must be safe.
85
+ fs.writeFileSync(path.join(tmpDir, 'theme.template.ts'), '// mine\n');
86
+ const {status, stdout} = await runCli(['init', '--features', 'theme'], {cwd: tmpDir});
87
+ expect(status).toBe(0);
88
+ expect(read('theme.template.ts')).toBe('// mine\n');
89
+ expect(stdout).toMatch(/already exists/);
90
+ });
91
+
81
92
  it('rejects an unknown feature with exit 1 and a helpful message', async () => {
82
93
  const {status, stderr} = await runCli(['init', '--features', 'bogus'], {cwd: tmpDir});
83
94
  expect(status).toBe(1);
@@ -0,0 +1,85 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file CLI behavior for `astryx theme template`.
5
+ *
6
+ * The API leaf is covered by api/theme/template/template.test.mjs; what is only
7
+ * reachable here is the terminal binding — which message the user sees, and the
8
+ * JSON envelope. The first version of this command read `result.written`
9
+ * instead of `result.data.written`, so it wrote the file and then told the user
10
+ * it had skipped: a receipt read at the wrong depth is invisible to a unit test
11
+ * of the leaf.
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-theme-template-'));
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 theme template', () => {
35
+ it('writes the template and says it wrote it', async () => {
36
+ const {status, stdout} = await runCli(['theme', 'template'], {cwd: tmpDir});
37
+
38
+ expect(status).toBe(0);
39
+ expect(stdout).toMatch(/Wrote theme\.template\.ts/);
40
+ expect(stdout).not.toMatch(/already exists/);
41
+ expect(read('theme.template.ts')).toMatch(/defineTheme/);
42
+ });
43
+
44
+ it('leaves an existing file alone, and says that instead', async () => {
45
+ fs.writeFileSync(path.join(tmpDir, 'theme.template.ts'), '// mine\n');
46
+
47
+ const {status, stdout} = await runCli(['theme', 'template'], {cwd: tmpDir});
48
+
49
+ expect(status).toBe(0);
50
+ expect(stdout).toMatch(/already exists/);
51
+ expect(read('theme.template.ts')).toBe('// mine\n');
52
+ });
53
+
54
+ it('replaces it with --overwrite', async () => {
55
+ fs.writeFileSync(path.join(tmpDir, 'theme.template.ts'), '// mine\n');
56
+
57
+ const {status} = await runCli(['theme', 'template', '--overwrite'], {cwd: tmpDir});
58
+
59
+ expect(status).toBe(0);
60
+ expect(read('theme.template.ts')).toMatch(/defineTheme/);
61
+ });
62
+
63
+ it('returns a theme.template envelope under --json', async () => {
64
+ const {status, stdout} = await runCli(['--json', 'theme', 'template'], {cwd: tmpDir});
65
+
66
+ expect(status).toBe(0);
67
+ const payload = JSON.parse(stdout);
68
+ expect(payload.type).toBe('theme.template');
69
+ expect(payload.data).toEqual({
70
+ path: 'theme.template.ts',
71
+ written: true,
72
+ reason: null,
73
+ });
74
+ });
75
+
76
+ it('refuses a path that escapes the project', async () => {
77
+ const {status, stderr} = await runCli(['theme', 'template', '../escaped.ts'], {
78
+ cwd: tmpDir,
79
+ });
80
+
81
+ expect(status).toBe(1);
82
+ expect(stderr).toMatch(/outside the project root/);
83
+ expect(fs.existsSync(path.join(path.dirname(tmpDir), 'escaped.ts'))).toBe(false);
84
+ });
85
+ });
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file CommandDoc for `astryx theme template`. The terminal binding of the
5
+ * `themeTemplate()` function (referenced via `fn`); its args/flags map to that
6
+ * function's params so a converter can build Commander config + --help from one
7
+ * source of truth.
8
+ * @position packages/cli/clients/cli/commands — command documentation
9
+ */
10
+
11
+ /** @type {import('@astryxdesign/cli/authoring').CommandDoc} */
12
+ export const doc = {
13
+ type: 'command',
14
+ name: 'theme template',
15
+ displayName: 'astryx theme template',
16
+ namespace: 'cli',
17
+ summary: 'Write the annotated theme template into your project',
18
+ description:
19
+ 'Writes theme.template.ts: the annotated reference for the whole theme surface — every ' +
20
+ 'defineTheme field, the token families, the component override syntax, and how a theme is ' +
21
+ 'consumed — naming the CLI command that prints the authoritative reference for each. Read ' +
22
+ 'it, copy what you need into your own theme file, delete it. Use `theme add <slug>` instead ' +
23
+ 'to start from a theme we ship. Leaves an existing file untouched unless --overwrite.',
24
+ fn: 'themeTemplate',
25
+ args: [{name: 'path', param: 'options.targetPath', required: false}],
26
+ options: [
27
+ {
28
+ flag: '-f, --overwrite',
29
+ param: 'options.overwrite',
30
+ description: 'Replace an existing file',
31
+ },
32
+ ],
33
+ examples: [
34
+ {label: 'Write it at the project root', cli: 'astryx theme template'},
35
+ {label: 'Somewhere else', cli: 'astryx theme template src/themes/starter.ts'},
36
+ ],
37
+ exitCodes: [
38
+ {code: 0, when: 'success, including when an existing file was left untouched'},
39
+ {code: 1, when: 'the target path escapes the project'},
40
+ ],
41
+ related: ['theme add', 'theme list', 'theme build'],
42
+ };
@@ -17,8 +17,8 @@ export const doc = {
17
17
  description:
18
18
  'The theme command group. Running astryx theme with no subcommand prints the ' +
19
19
  'subcommand list; the work happens in the subcommands: compile a theme (build), ' +
20
- 'scaffold one into your project (add), or list the bundled themes (list).',
21
- subcommands: ['build', 'add', 'list'],
20
+ 'scaffold one into your project (add), start a custom one from the annotated template (template), or list the bundled themes (list).',
21
+ subcommands: ['build', 'add', 'list', 'template'],
22
22
  examples: [
23
23
  {label: 'List bundled themes', cli: 'astryx theme list'},
24
24
  {label: 'Scaffold a theme', cli: 'astryx theme add matcha'},
@@ -69,6 +69,7 @@ export const JSON_SUPPORTED = new Set([
69
69
  'theme build',
70
70
  'theme list',
71
71
  'theme add',
72
+ 'theme template',
72
73
  'upgrade',
73
74
  'manifest',
74
75
  'doctor',
@@ -75,6 +75,7 @@ export const RESPONSE_TYPES = {
75
75
  'theme build': ['theme.build', 'theme.build.check'],
76
76
  'theme list': ['theme.list'],
77
77
  'theme add': ['theme.list', 'theme.add'],
78
+ 'theme template': ['theme.template'],
78
79
  upgrade: ['upgrade.list', 'upgrade.status', 'upgrade.run'],
79
80
  manifest: ['manifest'],
80
81
  doctor: ['doctor'],
@@ -113,6 +114,7 @@ const EXAMPLES = {
113
114
  'astryx theme add matcha',
114
115
  'astryx theme add matcha ./src/themes/matcha',
115
116
  ],
117
+ 'theme template': ['astryx theme template', 'astryx theme template --json'],
116
118
  upgrade: ['astryx upgrade --json'],
117
119
  manifest: ['astryx manifest --json', 'astryx --json'],
118
120
  doctor: ['astryx doctor', 'astryx doctor --json'],
@@ -354,7 +354,7 @@ export function generateCompressedIndex(version, {coreDir, invocation = getCliIn
354
354
  } else {
355
355
  lines.push("- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)");
356
356
  }
357
- lines.push('- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-* in :root.');
357
+ lines.push('- Tokens for every value (`astryx docs tokens`). Brand/accent belongs in the theme (`astryx theme list` / `theme add <slug>`, or `astryx theme template` for a custom one) — never override --color-* in :root.');
358
358
  // Self-check — post-generation pass. Validated via vibe tests (internal/vibe-tests/
359
359
  // prompt-purity-test): on complex multi-step UIs the rules above alone still leave raw
360
360
  // CSS in ~11-13% of runs; a re-read-and-fix pass cuts that ~4x at negligible token cost.
@@ -183,6 +183,11 @@ export const doc = {
183
183
  description:
184
184
  'A scaffold receipt: resolved slug, displayName, maintained flag, outputDir (relative to cwd), the theme entry file, its exportName, and the files written.',
185
185
  },
186
+ {
187
+ value: 'theme.template',
188
+ description:
189
+ '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.',
190
+ },
186
191
 
187
192
  // upgrade
188
193
  {
@@ -0,0 +1,11 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * Remove the leading Meta copyright header, preserving any BOM/shebang before
6
+ * it. Returns the source unchanged when the header is absent.
7
+ *
8
+ * @param {string} source
9
+ * @returns {string}
10
+ */
11
+ export function stripCopyrightHeader(source: string): string;
@@ -0,0 +1,35 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Strips our repo's copyright header from files we scaffold into someone
5
+ * else's project.
6
+ *
7
+ * Every file in this repo carries the Meta copyright header, and several
8
+ * commands copy repo files out verbatim — `theme add` (bundled theme sources),
9
+ * `init --features theme` (the annotated theme template). A consumer's own
10
+ * source tree should not inherit our boilerplate, and their lint may well
11
+ * reject it.
12
+ *
13
+ * SYNC: the same regex is applied by apps/docsite/src/lib/codeExamples.ts for
14
+ * rendered code samples. That copy lives in a different package (the docsite
15
+ * cannot import CLI internals), so a change to the header format has to land in
16
+ * both.
17
+ */
18
+
19
+ /**
20
+ * Matches our header at the very start of a file, with the leading BOM and/or
21
+ * shebang captured so they survive the strip.
22
+ */
23
+ const META_COPYRIGHT_HEADER_RE =
24
+ /^(\uFEFF?(?:#![^\r\n]*(?:\r?\n))?)\/\/ Copyright \(c\) Meta Platforms, Inc\. and affiliates\.\r?\n(?:\r?\n)*/;
25
+
26
+ /**
27
+ * Remove the leading Meta copyright header, preserving any BOM/shebang before
28
+ * it. Returns the source unchanged when the header is absent.
29
+ *
30
+ * @param {string} source
31
+ * @returns {string}
32
+ */
33
+ export function stripCopyrightHeader(source) {
34
+ return source.replace(META_COPYRIGHT_HEADER_RE, '$1');
35
+ }
@@ -0,0 +1,58 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Unit tests for the shared copyright-header stripper.
5
+ *
6
+ * Two commands copy repo files into a consumer's project (`theme add`,
7
+ * `init --features theme`); this is what keeps our boilerplate out of their
8
+ * tree. The BOM/shebang cases are the ones worth pinning — a naive strip
9
+ * corrupts the file rather than merely leaving a stray comment.
10
+ */
11
+
12
+ import {describe, it, expect} from 'vitest';
13
+ import {stripCopyrightHeader} from './copyright-header.mjs';
14
+
15
+ const HEADER = '// Copyright (c) Meta Platforms, Inc. and affiliates.\n';
16
+
17
+ describe('stripCopyrightHeader', () => {
18
+ it('removes the header and the blank line after it', () => {
19
+ expect(stripCopyrightHeader(`${HEADER}\nexport const a = 1;\n`)).toBe(
20
+ 'export const a = 1;\n',
21
+ );
22
+ });
23
+
24
+ it('removes it with no blank line after', () => {
25
+ expect(stripCopyrightHeader(`${HEADER}export const a = 1;\n`)).toBe(
26
+ 'export const a = 1;\n',
27
+ );
28
+ });
29
+
30
+ it('keeps a shebang, which must stay on line 1', () => {
31
+ expect(stripCopyrightHeader(`#!/usr/bin/env node\n${HEADER}\nrun();\n`)).toBe(
32
+ '#!/usr/bin/env node\nrun();\n',
33
+ );
34
+ });
35
+
36
+ it('keeps a BOM', () => {
37
+ expect(stripCopyrightHeader(`\uFEFF${HEADER}\nexport const a = 1;\n`)).toBe(
38
+ '\uFEFFexport const a = 1;\n',
39
+ );
40
+ });
41
+
42
+ it('handles CRLF line endings', () => {
43
+ expect(
44
+ stripCopyrightHeader(
45
+ '// Copyright (c) Meta Platforms, Inc. and affiliates.\r\n\r\nexport const a = 1;\r\n',
46
+ ),
47
+ ).toBe('export const a = 1;\r\n');
48
+ });
49
+
50
+ it('leaves a file without the header alone', () => {
51
+ expect(stripCopyrightHeader('export const a = 1;\n')).toBe('export const a = 1;\n');
52
+ });
53
+
54
+ it('only strips at the top — a mention further down is content', () => {
55
+ const source = `export const a = 1;\n${HEADER}`;
56
+ expect(stripCopyrightHeader(source)).toBe(source);
57
+ });
58
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2-canary.356d2f9",
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",
@@ -84,10 +84,10 @@
84
84
  "zod": "^4.4.3"
85
85
  },
86
86
  "peerDependencies": {
87
- "@astryxdesign/charts": "*",
88
- "@astryxdesign/core": "*",
89
- "@astryxdesign/lab": "*",
90
- "@astryxdesign/theme-neutral": "*",
87
+ "@astryxdesign/charts": "0.4.2-canary.356d2f9",
88
+ "@astryxdesign/core": "0.4.2-canary.356d2f9",
89
+ "@astryxdesign/lab": "0.4.2-canary.356d2f9",
90
+ "@astryxdesign/theme-neutral": "0.4.2-canary.356d2f9",
91
91
  "gpt-tokenizer": "^3.4.0"
92
92
  },
93
93
  "peerDependenciesMeta": {
@@ -105,10 +105,10 @@
105
105
  }
106
106
  },
107
107
  "devDependencies": {
108
- "@astryxdesign/charts": "*",
109
- "@astryxdesign/core": "*",
110
- "@astryxdesign/lab": "*",
111
- "@astryxdesign/theme-neutral": "*",
108
+ "@astryxdesign/charts": "0.4.2-canary.356d2f9",
109
+ "@astryxdesign/core": "0.4.2-canary.356d2f9",
110
+ "@astryxdesign/lab": "0.4.2-canary.356d2f9",
111
+ "@astryxdesign/theme-neutral": "0.4.2-canary.356d2f9",
112
112
  "gpt-tokenizer": "^3.4.0"
113
113
  },
114
114
  "scripts": {