@astryxdesign/cli 0.7.2-canary.ebcc404 → 0.7.2-canary.f8f7cf1

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 (134) hide show
  1. package/README.md +9 -8
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +14 -0
  4. package/api/component/_adapter.mjs +48 -0
  5. package/api/component/component.mjs +10 -2
  6. package/api/component/integration-export-name.test.mjs +233 -8
  7. package/api/docs/_adapter.d.mts +11 -0
  8. package/api/docs/_adapter.mjs +244 -1
  9. package/api/docs/integrationDocs.test.mjs +124 -0
  10. package/api/doctor/theme-checks.mjs +1 -1
  11. package/api/gap-report/gap-report.mjs +2 -1
  12. package/api/index.d.mts +2 -0
  13. package/api/index.mjs +2 -0
  14. package/api/init/run/run.mjs +2 -2
  15. package/api/integration/_adapter.d.mts +12 -0
  16. package/api/integration/_adapter.mjs +54 -0
  17. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  18. package/api/integration/add-contribution.mjs +8 -4
  19. package/api/integration/add-contribution.test.mjs +4 -2
  20. package/api/integration/authoring-checks.mjs +11 -2
  21. package/api/integration/authoring-checks.test.mjs +39 -0
  22. package/api/integration/authoring-checks.type.d.mts +9 -0
  23. package/api/integration/authoring-checks.type.mjs +14 -1
  24. package/api/integration/check/check.d.mts +96 -0
  25. package/api/integration/check/check.mjs +357 -0
  26. package/api/integration/check/check.test.mjs +489 -0
  27. package/api/integration/integration-check.type.d.mts +118 -0
  28. package/api/integration/integration-check.type.mjs +84 -0
  29. package/api/integration/integrationCheck.doc.d.mts +10 -0
  30. package/api/integration/integrationCheck.doc.mjs +73 -0
  31. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  32. package/api/integration/pack-check.mjs +48 -3
  33. package/api/integration/pack-check.test.mjs +247 -0
  34. package/api/integration/validate-integration.mjs +8 -0
  35. package/api/integration/validate-integration.test.mjs +11 -0
  36. package/api/integration/validate-integration.type.d.mts +8 -0
  37. package/api/integration/validate-integration.type.mjs +12 -1
  38. package/api/integration/validateIntegration.doc.mjs +17 -3
  39. package/api/search/search.integration-check.test.mjs +24 -0
  40. package/api/search/search.mjs +20 -8
  41. package/api/theme/build/build.family.test.mjs +54 -6
  42. package/api/theme/build/build.mjs +114 -17
  43. package/api/theme/themeBuild.doc.mjs +4 -4
  44. package/assets/docs/README.md +10 -0
  45. package/assets/docs/author-a-theme.doc.mjs +1 -1
  46. package/assets/docs/getting-started.doc.mjs +3 -3
  47. package/assets/docs/theme.doc.dense.mjs +1 -1
  48. package/assets/docs/theme.doc.mjs +1 -1
  49. package/assets/docs/theme.doc.zh.mjs +1 -1
  50. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  51. package/assets/docs/tree/block-template.doc.mjs +2 -2
  52. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  53. package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
  54. package/assets/docs/tree/checks.doc.mjs +21 -13
  55. package/assets/docs/tree/component-family.doc.mjs +5 -1
  56. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  57. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  58. package/assets/docs/tree/links.doc.mjs +2 -2
  59. package/assets/docs/tree/page-template.doc.mjs +1 -1
  60. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  61. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  62. package/assets/docs/tree/single-component.doc.mjs +3 -2
  63. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  64. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  65. package/assets/docs/tree/versioning.doc.mjs +1 -0
  66. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  67. package/assets/docs/use-a-theme.doc.mjs +2 -2
  68. package/assets/theme.template.ts +1 -1
  69. package/authoring/doctypes/component/component.doc.mjs +1 -1
  70. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  71. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  72. package/authoring/doctypes/template/parse.d.mts +6 -2
  73. package/authoring/doctypes/template/parse.mjs +3 -1
  74. package/authoring/doctypes/template/template.doc.mjs +1 -1
  75. package/authoring/doctypes/template/type.ts +22 -0
  76. package/authoring/index.d.ts +1 -0
  77. package/authoring/integration/integration.doc.mjs +8 -1
  78. package/authoring/integration/parse.test.mjs +10 -0
  79. package/authoring/integration/schema.d.mts +2 -0
  80. package/authoring/integration/schema.mjs +1 -0
  81. package/authoring/integration/type.ts +4 -0
  82. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  83. package/clients/cli/commands/build-theme.mjs +1 -1
  84. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  85. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  86. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  87. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  88. package/clients/cli/commands/doctor.mjs +190 -0
  89. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  90. package/clients/cli/commands/integration-check.test.mjs +267 -0
  91. package/clients/cli/commands/integration.doc.mjs +6 -2
  92. package/clients/cli/commands/integration.mjs +107 -1
  93. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  94. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  95. package/clients/cli/index.mjs +1 -0
  96. package/foundation/config/project.mjs +82 -0
  97. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  98. package/foundation/discovery/component-discovery.d.mts +108 -17
  99. package/foundation/discovery/component-discovery.export-names.test.mjs +469 -2
  100. package/foundation/discovery/component-discovery.mjs +380 -47
  101. package/foundation/discovery/component-replacement.test.mjs +20 -0
  102. package/foundation/discovery/doc-types.d.mts +157 -0
  103. package/foundation/discovery/doc-types.mjs +1224 -0
  104. package/foundation/discovery/doc-types.test.mjs +652 -0
  105. package/foundation/discovery/docs-discovery.d.mts +81 -0
  106. package/foundation/discovery/docs-discovery.mjs +57 -0
  107. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  108. package/foundation/discovery/integration-doc-files.mjs +231 -0
  109. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  110. package/foundation/discovery/module-exports.d.mts +6 -0
  111. package/foundation/discovery/module-exports.mjs +15 -2
  112. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  113. package/foundation/discovery/package-tsconfig.mjs +228 -0
  114. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  115. package/foundation/discovery/project-typescript.d.mts +23 -0
  116. package/foundation/discovery/project-typescript.mjs +61 -0
  117. package/foundation/discovery/prop-drift.d.mts +278 -0
  118. package/foundation/discovery/prop-drift.mjs +2263 -0
  119. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  120. package/foundation/discovery/type-match.d.mts +53 -0
  121. package/foundation/discovery/type-match.mjs +660 -0
  122. package/foundation/discovery/type-match.test.mjs +169 -0
  123. package/foundation/discovery/type-text.d.mts +68 -0
  124. package/foundation/discovery/type-text.mjs +658 -0
  125. package/foundation/discovery/type-text.test.mjs +196 -0
  126. package/foundation/discovery/unresolved-references.d.mts +62 -0
  127. package/foundation/discovery/unresolved-references.mjs +790 -0
  128. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  129. package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
  130. package/foundation/doc-compiler/links.d.mts +9 -5
  131. package/foundation/doc-compiler/links.mjs +4 -2
  132. package/foundation/integrations/contribution-inventory.mjs +6 -1
  133. package/foundation/response/response-types.doc.mjs +7 -2
  134. package/package.json +9 -9
@@ -92,7 +92,9 @@ import {
92
92
  familyMember,
93
93
  findComponentReadme,
94
94
  forEachIntegrationDoc,
95
+ integrationMemberEntry,
95
96
  integrationRecordHold,
97
+ isIntegrationMember,
96
98
  resolveImportPath,
97
99
  resolveIntegrationImportPath,
98
100
  CORE_PACKAGE,
@@ -1144,24 +1146,31 @@ async function gatherIntegrationComponents(loadedIntegrations) {
1144
1146
  const renamed = new Set(
1145
1147
  discovered
1146
1148
  .filter(rec => integrationRecordHold(rec) === 'doc')
1149
+ .filter(rec => !isIntegrationMember(rec))
1147
1150
  .map(rec => rec.docPath),
1148
1151
  );
1149
1152
  forEachIntegrationDoc(integration.components, (docPath, stem) => {
1150
1153
  if (!renamed.has(docPath)) hold(fileNamed, stem, docPath);
1151
1154
  });
1152
1155
  for (const rec of components) {
1153
- const doc = await loadModuleDoc(rec.docPath);
1156
+ const loaded = await loadModuleDoc(rec.docPath);
1157
+ // A family member is indexed from its own entry: the family's keywords
1158
+ // and guidance describe the family.
1159
+ const member = isIntegrationMember(rec);
1160
+ const doc = member ? integrationMemberEntry(loaded, rec) : loaded;
1154
1161
  // A doc that names another export of its file still answers to the
1155
1162
  // file's name, as `component <file name>` does.
1156
1163
  const fileName = docStem(rec.docPath);
1157
- if (renamed.has(rec.docPath)) {
1164
+ if (integrationRecordHold(rec) === 'doc') {
1158
1165
  hold(documented, rec.name, rec.docPath);
1166
+ }
1167
+ if (renamed.has(rec.docPath) && !member) {
1159
1168
  hold(documented, fileName, rec.docPath);
1160
1169
  }
1161
1170
  candidates.push({
1162
1171
  domain: 'component',
1163
1172
  name: rec.name,
1164
- aliases: fileName !== rec.name ? [fileName] : [],
1173
+ aliases: !member && fileName !== rec.name ? [fileName] : [],
1165
1174
  _hold: integrationRecordHold(rec),
1166
1175
  _docPath: rec.docPath,
1167
1176
  keywords: doc && Array.isArray(doc.keywords) ? doc.keywords : [],
@@ -1183,7 +1192,7 @@ async function gatherIntegrationComponents(loadedIntegrations) {
1183
1192
  packageName: rec.package,
1184
1193
  },
1185
1194
  rec.name,
1186
- doc?.import,
1195
+ loaded?.import,
1187
1196
  ),
1188
1197
  });
1189
1198
  }
@@ -1319,10 +1328,13 @@ export async function componentKeywords(coreDir, cwd) {
1319
1328
  const integrations = loadedIntegrations.map(async integration => {
1320
1329
  const {components} = await discoverValidIntegrationComponents(integration);
1321
1330
  return Promise.all(
1322
- components.map(async rec => ({
1323
- name: rec.name,
1324
- keywords: keywordsOf(await loadModuleDoc(rec.docPath)),
1325
- })),
1331
+ components.map(async rec => {
1332
+ const loaded = await loadModuleDoc(rec.docPath);
1333
+ return {
1334
+ name: rec.name,
1335
+ keywords: keywordsOf(integrationMemberEntry(loaded, rec) ?? loaded),
1336
+ };
1337
+ }),
1326
1338
  );
1327
1339
  });
1328
1340
  return [
@@ -13,6 +13,7 @@ import {themeBuild, themeBuildFamily} from './build.mjs';
13
13
  import {interceptCore} from './core-interception.mjs';
14
14
  import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
15
15
  import {runCli} from '../../../test-utils/run-cli.mjs';
16
+ import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
16
17
  vi.setConfig({testTimeout: 120_000});
17
18
  const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
18
19
  const FAMILY_KEY = 'ocean-family';
@@ -252,22 +253,69 @@ describe('themeBuildFamily()', () => {
252
253
  expect(fs.existsSync(dtsPath)).toBe(false);
253
254
  });
254
255
 
255
- it('refuses family outputs outside cwd before checking or writing', async () => {
256
+ it('refuses to write family outputs outside cwd, and checks them read-only', async () => {
256
257
  const dir = makeDir();
257
258
  const files = writeFamily(dir).map(file => path.join(dir, file));
258
259
  const app = path.join(dir, 'app');
259
260
  fs.mkdirSync(app);
260
261
 
261
- for (const check of [false, true]) {
262
- await expect(
263
- themeBuildFamily(files, {familyKey: FAMILY_KEY, check}, {cwd: app}),
264
- ).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
265
- }
262
+ await expect(
263
+ themeBuildFamily(files, {familyKey: FAMILY_KEY}, {cwd: app}),
264
+ ).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
265
+ const checked = await themeBuildFamily(
266
+ files,
267
+ {familyKey: FAMILY_KEY, check: true},
268
+ {cwd: app},
269
+ );
270
+ expect(checked?.type).toBe('theme.build.check');
271
+ expect(checked?.data.upToDate).toBe(false);
272
+ // The `.css.d.ts` import stub may be absent in check mode.
273
+ expect(checked?.data.stale).toEqual(
274
+ OUTPUTS.filter(file => !file.endsWith('.css.d.ts')).map(file => ({
275
+ path: `../themes/${file}`,
276
+ reason: 'missing',
277
+ })),
278
+ );
266
279
  for (const output of OUTPUTS) {
267
280
  expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(false);
268
281
  }
269
282
  });
270
283
 
284
+ it('ends a stale family report for outputs outside cwd with a remedy that works', async () => {
285
+ const dir = makeDir();
286
+ const files = writeFamily(dir).map(file => path.join(dir, file));
287
+ const app = path.join(dir, 'app');
288
+ fs.mkdirSync(app);
289
+ const family = ['--family', ...files, '--family-key', FAMILY_KEY];
290
+
291
+ const stale = await runCli(['theme', 'build', ...family, '--check'], app);
292
+ expect(stale.code).toBe(1);
293
+ // The old remedy ran the build from cwd, which refuses these outputs.
294
+ expect(stale.stderr).not.toContain('Rebuild with:');
295
+
296
+ const lines = stale.stderr.split('\n').map(line => line.trim());
297
+ const runDir = lines.find(line => line.startsWith('cd '))?.slice(3);
298
+ expect(runDir).toBe(fs.realpathSync(path.join(dir, 'themes')));
299
+ const stem = `${getCliInvocation(runDir)} theme build `;
300
+ const [buildArgs, checkArgs] = lines
301
+ .filter(line => line.startsWith(stem))
302
+ .map(line => line.slice(stem.length).split(' '));
303
+ expect(checkArgs).toEqual([...buildArgs, '--check']);
304
+
305
+ // Following the remedy builds, and the check from there passes.
306
+ const built = await runCli(['theme', 'build', ...buildArgs], runDir);
307
+ expect(built.code).toBe(0);
308
+ for (const output of OUTPUTS) {
309
+ expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(true);
310
+ }
311
+ const checked = await runCli(
312
+ ['--json', 'theme', 'build', ...checkArgs],
313
+ runDir,
314
+ );
315
+ expect(checked.code).toBe(0);
316
+ expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
317
+ });
318
+
271
319
  it('fails without publishing a mixed trio when staging a write fails', async () => {
272
320
  const dir = makeDir();
273
321
  const files = writeFamily(dir);
@@ -27,8 +27,10 @@
27
27
  * files; an internal callback lets the CLI make only the process status fail.
28
28
  * Human progress is emitted through the shared `logger` (silent by default), so
29
29
  * the CLI keeps its exact output while a programmatic caller stays quiet.
30
- * Every output, explicit or beside the theme file, stays inside `cwd`
31
- * (`ERR_PATH_TRAVERSAL` otherwise), in check mode too.
30
+ * Every output a build writes, explicit or beside the theme file, stays inside
31
+ * `cwd` (`ERR_PATH_TRAVERSAL` otherwise). Check mode writes nothing, so it
32
+ * compares outputs outside `cwd` read-only; only a relative `out` path that
33
+ * escapes `cwd` is refused there too.
32
34
  *
33
35
  * The installed `@astryxdesign/core` is an independently versioned optional
34
36
  * peer, so it can be older than the CLI. A theme that uses only baseline
@@ -60,7 +62,10 @@ import {isBuiltin} from 'node:module';
60
62
  import {fileURLToPath, pathToFileURL} from 'node:url';
61
63
  import {parse} from '@babel/parser';
62
64
  import {createJiti} from 'jiti';
63
- import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
65
+ import {
66
+ formatCliCommand,
67
+ getCliInvocation,
68
+ } from '../../../foundation/env/package-manager.mjs';
64
69
  import {
65
70
  CLI_ROOT,
66
71
  findCoreDir,
@@ -568,6 +573,86 @@ function confineOutputs(writes, root, remedy) {
568
573
  for (const write of writes) confineOutput(write.dest, root, remedy);
569
574
  }
570
575
 
576
+ /** @param {string} dest absolute output path @param {string} root @returns {boolean} whether a build at `root` refuses to write `dest` */
577
+ function refusedOutput(dest, root) {
578
+ try {
579
+ confineOutput(dest, root, '');
580
+ return false;
581
+ } catch (err) {
582
+ if (
583
+ err instanceof AstryxError &&
584
+ err.code === ERROR_CODES.ERR_PATH_TRAVERSAL
585
+ )
586
+ return true;
587
+ throw err;
588
+ }
589
+ }
590
+
591
+ /** @param {string[]} dirs absolute directories @returns {string | null} the deepest directory containing all of them, or null across roots */
592
+ function commonDirectory(dirs) {
593
+ let common = dirs[0];
594
+ for (const dir of dirs.slice(1)) {
595
+ while (
596
+ path.relative(common, dir).startsWith('..') ||
597
+ path.isAbsolute(path.relative(common, dir))
598
+ ) {
599
+ const parent = path.dirname(common);
600
+ if (parent === common) return null;
601
+ common = parent;
602
+ }
603
+ }
604
+ return common;
605
+ }
606
+
607
+ /** @param {string} value @returns {string} `value` as one shell word */
608
+ function shellWord(value) {
609
+ if (/^[A-Za-z0-9@%+=:,./_-]+$/.test(value)) return value;
610
+ return `'${value.replace(/'/g, `'\\''`)}'`;
611
+ }
612
+
613
+ /**
614
+ * How a stale check report ends when outputs sit outside the write root. A
615
+ * build there refuses them, and each output's `Source:` header records the
616
+ * directory its build ran from, so the build and the check both run from the
617
+ * nearest directory that contains every theme file and output.
618
+ * @param {Array<{dest: string}>} writes
619
+ * @param {string[]} sources absolute theme files the build reads
620
+ * @param {string} root the directory a build here keeps its writes inside
621
+ * @param {(relative: (file: string) => string) => string[]} args the arguments after `theme build`, given each path relative to the directory to run from
622
+ * @returns {string[] | null} report lines, or null when every output is inside `root`
623
+ */
624
+ function outsideCheckRemedy(writes, sources, root, args) {
625
+ if (!writes.some(write => refusedOutput(write.dest, root))) return null;
626
+ const real = (/** @type {string} */ file) => realLocation(path.resolve(file));
627
+ const dir = commonDirectory([
628
+ ...sources.map(file => path.dirname(real(file))),
629
+ ...writes.map(write => path.dirname(real(write.dest))),
630
+ ]);
631
+ if (!dir) {
632
+ return [
633
+ 'These outputs are outside the working directory, so a build here refuses them. Build and check them from one directory that contains the theme files and the outputs.',
634
+ ];
635
+ }
636
+ const command = (/** @type {string[]} */ extra) =>
637
+ formatCliCommand(
638
+ [
639
+ 'theme',
640
+ 'build',
641
+ ...args(file => path.relative(dir, real(file))),
642
+ ...extra,
643
+ ]
644
+ .map(shellWord)
645
+ .join(' '),
646
+ dir,
647
+ );
648
+ return [
649
+ 'These outputs are outside the working directory, so a build here refuses them, and each output records the directory it was built from. Build and check them from a directory that contains them:',
650
+ ` cd ${shellWord(dir)}`,
651
+ ` ${command([])}`,
652
+ ` ${command(['--check'])}`,
653
+ ];
654
+ }
655
+
571
656
  /** @param {Array<{dest: string, content: string}>} writes */
572
657
  function writeBuildOutputs(writes) {
573
658
  if (writes.length === 0) return;
@@ -2859,9 +2944,13 @@ async function themeBuildInternal(
2859
2944
  const writeRoot = options.__writeRoot ?? cwd;
2860
2945
  let outPath;
2861
2946
  if (options.out) {
2862
- // Relative or absolute, the explicit output stays inside the write root.
2863
2947
  outPath = path.resolve(cwd, options.out);
2864
- confineOutput(outPath, writeRoot, OUT_REMEDY, options.out);
2948
+ // A build keeps the explicit output inside the write root, relative or
2949
+ // absolute. Check mode writes nothing, so it refuses only a relative path
2950
+ // that escapes cwd.
2951
+ if (!options.check || !path.isAbsolute(options.out)) {
2952
+ confineOutput(outPath, writeRoot, OUT_REMEDY, options.out);
2953
+ }
2865
2954
  } else {
2866
2955
  outPath = path.join(path.dirname(filePath), `${baseName}.css`);
2867
2956
  }
@@ -2957,10 +3046,12 @@ async function themeBuildInternal(
2957
3046
  writes.push({dest: variantDtsPath, content: variantContent});
2958
3047
  }
2959
3048
 
2960
- // Every output stays inside cwd, including the defaults beside the theme
2961
- // file. Check mode refuses the same paths, so it never passes outputs that a
2962
- // build would refuse to write.
2963
- confineOutputs(writes, writeRoot, options.out ? OUT_REMEDY : SOURCE_REMEDY);
3049
+ // Every output a build writes stays inside cwd, including the defaults
3050
+ // beside the theme file. Check mode writes nothing, so it compares outputs
3051
+ // outside cwd read-only.
3052
+ if (!options.check) {
3053
+ confineOutputs(writes, writeRoot, options.out ? OUT_REMEDY : SOURCE_REMEDY);
3054
+ }
2964
3055
 
2965
3056
  // Check mode: compare generated content against what's on disk instead of
2966
3057
  // writing. A file is "stale" if it's missing or its content differs once the
@@ -2980,7 +3071,10 @@ async function themeBuildInternal(
2980
3071
  ` ${s.reason === 'missing' ? 'missing' : 'stale'}: ${s.path}`,
2981
3072
  );
2982
3073
  }
2983
- logger.error(`\n Rebuild with: ${buildCommand}`);
3074
+ // prettier-ignore
3075
+ const remedy = outsideCheckRemedy(writes, [filePath], writeRoot, relative => [relative(filePath), ...(options.out ? ['--out', relative(outPath)] : []), ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
3076
+ if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
3077
+ else logger.error(`\n Rebuild with: ${buildCommand}`);
2984
3078
  }
2985
3079
  return {
2986
3080
  type: 'theme.build.check',
@@ -3093,7 +3187,7 @@ function printBuildTrailer(trailer) {
3093
3187
  logger.log(`
3094
3188
  Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
3095
3189
 
3096
- import { Theme } from '@astryxdesign/core';
3190
+ import { Theme } from '@astryxdesign/core/theme';
3097
3191
  import { ${trailer.exportName} } from '${trailer.jsImport}';
3098
3192
  import '${trailer.cssImport}';
3099
3193
 
@@ -3103,7 +3197,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
3103
3197
 
3104
3198
  Or with a <link> tag:
3105
3199
 
3106
- import { Theme } from '@astryxdesign/core';
3200
+ import { Theme } from '@astryxdesign/core/theme';
3107
3201
  import { ${trailer.exportName} } from '${trailer.jsImport}';
3108
3202
 
3109
3203
  <link rel="stylesheet" href="${trailer.cssImport}" />
@@ -3129,7 +3223,7 @@ export function printBatchTrailer(trailers) {
3129
3223
  logger.log(`
3130
3224
  Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
3131
3225
 
3132
- import { Theme } from '@astryxdesign/core';
3226
+ import { Theme } from '@astryxdesign/core/theme';
3133
3227
  import { ${first.exportName} } from '${first.jsImport}';
3134
3228
  import '${first.cssImport}';
3135
3229
 
@@ -3139,7 +3233,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
3139
3233
 
3140
3234
  Or with a <link> tag:
3141
3235
 
3142
- import { Theme } from '@astryxdesign/core';
3236
+ import { Theme } from '@astryxdesign/core/theme';
3143
3237
  import { ${first.exportName} } from '${first.jsImport}';
3144
3238
 
3145
3239
  <link rel="stylesheet" href="${first.cssImport}" />
@@ -3506,8 +3600,8 @@ export async function themeBuildFamily(
3506
3600
  js: path.relative(cwd, jsPath),
3507
3601
  dts: path.relative(cwd, dtsPath),
3508
3602
  };
3509
- // The family's outputs stay inside cwd too, in check mode as well.
3510
- confineOutputs(writes, cwd, FAMILY_REMEDY);
3603
+ // A family build's outputs stay inside cwd too; check mode only reads them.
3604
+ if (!options.check) confineOutputs(writes, cwd, FAMILY_REMEDY);
3511
3605
  if (options.check) {
3512
3606
  const stale = staleBuildOutputs(writes, cwd);
3513
3607
  const upToDate = stale.length === 0;
@@ -3524,7 +3618,10 @@ export async function themeBuildFamily(
3524
3618
  ` ${entry.reason === 'missing' ? 'missing' : 'stale'}: ${entry.path}`,
3525
3619
  );
3526
3620
  }
3527
- logger.error(`\n Rebuild with: ${buildCommand}`);
3621
+ // prettier-ignore
3622
+ const remedy = outsideCheckRemedy(writes, members.map(member => member.filePath), cwd, relative => ['--family', ...members.map(member => relative(member.filePath)), '--family-key', baseName, ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
3623
+ if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
3624
+ else logger.error(`\n Rebuild with: ${buildCommand}`);
3528
3625
  }
3529
3626
  return {
3530
3627
  type: 'theme.build.check',
@@ -58,13 +58,13 @@ export const doc = {
58
58
  name: 'options.out',
59
59
  type: 'string',
60
60
  description:
61
- 'Override the output CSS path, relative or absolute; it must resolve inside cwd. The .js, .d.ts and any .variants.d.ts are written in the same directory, named after the theme (<name>.js), not after the CSS file.',
61
+ 'Override the output CSS path, relative or absolute; a build refuses a path outside cwd, and check mode refuses only a relative path that escapes cwd. The .js, .d.ts and any .variants.d.ts are written in the same directory, named after the theme (<name>.js), not after the CSS file.',
62
62
  },
63
63
  {
64
64
  name: 'options.check',
65
65
  type: 'boolean',
66
66
  description:
67
- 'Compile in memory and compare each output against what is on disk instead of writing: the CI drift guard.',
67
+ 'Compile in memory and compare each output against what is on disk instead of writing: the CI drift guard. Because it writes nothing, it also compares outputs outside cwd; when those are stale, the report names the directory that contains them and the build and check commands to run from there, since a build refuses them and each output records the directory it was built from.',
68
68
  default: 'false',
69
69
  },
70
70
  {
@@ -77,7 +77,7 @@ export const doc = {
77
77
  name: 'ctx.cwd',
78
78
  type: 'string',
79
79
  description:
80
- 'Directory the theme file, a relative out path, and the returned output paths resolve against. Every output is written inside it, including the default outputs beside the theme file.',
80
+ 'Directory the theme file, a relative out path, and the returned output paths resolve against. Every output a build writes is inside it, including the default outputs beside the theme file.',
81
81
  default: 'process.cwd()',
82
82
  },
83
83
  ],
@@ -105,7 +105,7 @@ export const doc = {
105
105
  },
106
106
  {
107
107
  code: 'ERR_PATH_TRAVERSAL',
108
- when: 'the theme name contains a path separator or traversal marker, or an output resolves outside cwd (including through a symlink): an `out` path, relative or absolute, or the default outputs beside a theme file outside cwd (also rejected in check mode)',
108
+ when: 'the theme name contains a path separator or traversal marker, or a written output resolves outside cwd (including through a symlink): an `out` path, relative or absolute, or the default outputs beside a theme file outside cwd. Check mode writes nothing, so it refuses only a relative `out` path that escapes cwd',
109
109
  },
110
110
  {
111
111
  code: 'ERR_CORE_NOT_FOUND',
@@ -51,6 +51,16 @@ The material is usually good; the finding is placement, not quality. It goes in
51
51
 
52
52
  Worked example: a responsive-and-interaction readiness rubric is grading criteria → **Component-Audit-Rubric**, or **Component-Lifecycle** if it is a promotion gate.
53
53
 
54
+ ## Imports in code samples
55
+
56
+ Import each Core export from the path `astryx component <Name>` prints: the
57
+ component's own entry when Core has one (`@astryxdesign/core/VStack`,
58
+ `@astryxdesign/core/Heading`), and `@astryxdesign/core/theme` for `Theme`,
59
+ `defineTheme` and the rest of the theme API. A reader copies one sample and then
60
+ reads `astryx component`, and two paths for one export read as two different
61
+ things. The no-build CDN page is the exception: its import map names only the
62
+ package root.
63
+
54
64
  ## Sections are read one at a time
55
65
 
56
66
  `astryx docs <topic> --index` lists a topic's sections, and readers then open
@@ -120,7 +120,7 @@ export const docs = {
120
120
  ['ocean.variants.d.ts', "(Optional) Module augmentations for custom component prop values"],
121
121
  ]},
122
122
  {type: 'prose', text: 'The `__built: true` flag tells Theme to skip runtime `<style>` injection; the CSS file handles it. Load the generated CSS wherever you load the module.'},
123
- {type: 'code', lang: 'tsx', label: 'Using a custom built theme', code: "import {Theme} from '@astryxdesign/core';\nimport {oceanTheme} from './themes/ocean';\nimport './themes/ocean.css';\n\n<Theme theme={oceanTheme}>\n <App />\n</Theme>"},
123
+ {type: 'code', lang: 'tsx', label: 'Using a custom built theme', code: "import {Theme} from '@astryxdesign/core/theme';\nimport {oceanTheme} from './themes/ocean';\nimport './themes/ocean.css';\n\n<Theme theme={oceanTheme}>\n <App />\n</Theme>"},
124
124
  {type: 'prose', text: "After upgrading Astryx, rerun `astryx theme build` for every custom prebuilt theme. Deploy the regenerated files together. The runtime intentionally trusts `__built: true` and will not repair stale CSS from an older build. The build also warns when the theme names font families it does not load. See {@link generic:font-setup} for the full recipe."},
125
125
  {type: 'prose', text: 'For the runtime vs built tradeoff, see the Runtime vs Built section of {@link generic:use-a-theme}.'},
126
126
  ],
@@ -86,7 +86,7 @@ export const docs = {
86
86
  type: 'code',
87
87
  lang: 'tsx',
88
88
  label: 'App root',
89
- code: `import {Theme} from '@astryxdesign/core';
89
+ code: `import {Theme} from '@astryxdesign/core/theme';
90
90
  import {themes, defaultThemeSlug} from './astryx-themes';
91
91
 
92
92
  <Theme theme={themes[defaultThemeSlug]}><App /></Theme>`,
@@ -119,14 +119,14 @@ import {themes, defaultThemeSlug} from './astryx-themes';
119
119
  content: [
120
120
  {
121
121
  type: 'prose',
122
- text: 'Components are imported from per-category subpath entrypoints. This keeps bundles small and makes intent clear.',
122
+ text: 'Import each component from its own entry, the path `astryx component <Name>` prints. This keeps bundles small and makes intent clear.',
123
123
  },
124
124
  {
125
125
  type: 'code',
126
126
  lang: 'tsx',
127
127
  label: 'app/page.tsx',
128
128
  code: `import {Button} from '@astryxdesign/core/Button';
129
- import {VStack} from '@astryxdesign/core/Layout';
129
+ import {VStack} from '@astryxdesign/core/VStack';
130
130
 
131
131
  export default function Page() {
132
132
  return (
@@ -9,7 +9,7 @@ export const docsDense = {
9
9
  section: 'Wrap your app in a theme',
10
10
  title: 'Quick start',
11
11
  content: [
12
- {type: 'prose', text: "Install a theme, wrap in `<Theme>` (`import {Theme} from '@astryxdesign/core'`). {@link generic:use-a-theme} for full guide."},
12
+ {type: 'prose', text: "Install a theme, wrap in `<Theme>` (`import {Theme} from '@astryxdesign/core/theme'`). {@link generic:use-a-theme} for full guide."},
13
13
  ],
14
14
  },
15
15
  {
@@ -23,7 +23,7 @@ export const docs = {
23
23
  type: 'code',
24
24
  lang: 'tsx',
25
25
  label: 'Wire the generated module once',
26
- code: "import {Theme} from '@astryxdesign/core';\nimport {themes, defaultThemeSlug} from './astryx-themes';\n\nfunction App() {\n return (\n <Theme theme={themes[defaultThemeSlug]}>\n <YourApp />\n </Theme>\n );\n}",
26
+ code: "import {Theme} from '@astryxdesign/core/theme';\nimport {themes, defaultThemeSlug} from './astryx-themes';\n\nfunction App() {\n return (\n <Theme theme={themes[defaultThemeSlug]}>\n <YourApp />\n </Theme>\n );\n}",
27
27
  },
28
28
  ],
29
29
  },
@@ -11,7 +11,7 @@ export const docsZh = {
11
11
  content: [
12
12
  {
13
13
  type: 'prose',
14
- text: "安装主题包,用 `<Theme>`(`import {Theme} from '@astryxdesign/core'`)包裹应用,选择浅色或深色模式。完整指南——可用主题、集成主题、深色模式、嵌套主题和生产构建——请参阅 {@link generic:use-a-theme}。",
14
+ text: "安装主题包,用 `<Theme>`(`import {Theme} from '@astryxdesign/core/theme'`)包裹应用,选择浅色或深色模式。完整指南——可用主题、集成主题、深色模式、嵌套主题和生产构建——请参阅 {@link generic:use-a-theme}。",
15
15
  },
16
16
  ],
17
17
  },
@@ -71,5 +71,15 @@ Declare component root ./components in astryx.integration.mjs.
71
71
  },
72
72
  ],
73
73
  },
74
+ {
75
+ id: 'document-an-existing-component',
76
+ title: 'Document a component you already have',
77
+ content: [
78
+ {
79
+ type: 'prose',
80
+ text: "To document a component your package already has, write its doc beside the source file and name the doc after that file, such as `acme-gallery.doc.mjs` beside `acme-gallery.tsx`. Set `name` to the export apps import, such as `name: 'AcmeGallery'`, and keep both files inside the components root that `astryx.integration.mjs` declares. The file does not have to be named after the component; {@link generic:component-imports} shows how apps then find it.",
81
+ },
82
+ ],
83
+ },
74
84
  ],
75
85
  };
@@ -31,7 +31,7 @@ export const docs = {
31
31
  type: 'code',
32
32
  lang: 'javascript',
33
33
  label: 'templates/acme-stat-card.doc.mjs',
34
- code: `/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
34
+ code: `/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */
35
35
  export default {
36
36
  type: 'block',
37
37
  name: 'acme-stat-card',
@@ -75,7 +75,7 @@ export default {
75
75
  type: 'code',
76
76
  lang: 'javascript',
77
77
  label: 'templates/acme-status-card-showcase.doc.mjs',
78
- code: `/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
78
+ code: `/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */
79
79
  export default {
80
80
  type: 'block',
81
81
  name: 'acme-status-card-showcase',