@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4
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/README.md +99 -66
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +115 -3
- package/api/build/blocks/blocks.d.mts +42 -0
- package/api/build/blocks/blocks.mjs +169 -0
- package/api/build/blocks/blocks.test.mjs +108 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/kit/answer.d.mts +14 -0
- package/api/build/kit/answer.mjs +85 -0
- package/api/build/kit/answer.test.mjs +38 -0
- package/api/build/kit/kind.d.mts +25 -0
- package/api/build/kit/kind.mjs +170 -0
- package/api/build/kit/kit.d.mts +2 -4
- package/api/build/kit/kit.mjs +8 -398
- package/api/build/kit/rank.d.mts +3 -32
- package/api/build/kit/rank.mjs +25 -342
- package/api/build/kit/rank.test.mjs +2 -1
- package/api/build/kit/signals.d.mts +60 -0
- package/api/build/kit/signals.mjs +284 -0
- package/api/build/kit/start.d.mts +32 -0
- package/api/build/kit/start.mjs +163 -0
- package/api/build/kit/words.d.mts +72 -0
- package/api/build/kit/words.mjs +202 -0
- package/api/build/tree.d.mts +20 -0
- package/api/build/tree.mjs +52 -0
- package/api/build/tree.test.mjs +177 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/component.mjs +16 -2
- package/api/component/component.test.mjs +15 -0
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.mjs +17 -5
- package/api/docs/docs.test.mjs +7 -1
- package/api/doctor/doctor.mjs +86 -12
- package/api/doctor/doctor.test.mjs +28 -3
- package/api/init/init.doc.mjs +20 -6
- package/api/init/init.test.mjs +18 -2
- package/api/init/init.type.d.mts +3 -3
- package/api/init/init.type.mjs +3 -3
- package/api/init/remove/remove.mjs +17 -4
- package/api/init/remove/remove.test.mjs +114 -2
- package/api/init/run/run.mjs +69 -6
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/pack-check.mjs +123 -4
- package/api/integration/pack-check.test.mjs +87 -0
- package/api/integration/validate-integration-case-disk.test.mjs +111 -0
- package/api/integration/validate-integration-gaps.test.mjs +1964 -0
- package/api/integration/validate-integration.d.mts +11 -2
- package/api/integration/validate-integration.mjs +226 -32
- package/api/integration/validate-integration.type.d.mts +58 -1
- package/api/integration/validate-integration.type.mjs +45 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +66 -2
- package/api/search/_adapter.mjs +118 -10
- package/api/search/_candidate-score.d.mts +40 -0
- package/api/search/_candidate-score.mjs +238 -0
- package/api/search/_cards.json +6098 -0
- package/api/search/_compound.d.mts +50 -0
- package/api/search/_compound.mjs +170 -0
- package/api/search/_doc-searcher.d.mts +4 -0
- package/api/search/_doc-searcher.mjs +23 -0
- package/api/search/_doc-searcher.test.mjs +36 -0
- package/api/search/_doc-searcher.weights.json +1 -0
- package/api/search/_doc-text.d.mts +30 -0
- package/api/search/_doc-text.mjs +87 -0
- package/api/search/_entries.d.mts +52 -0
- package/api/search/_entries.mjs +158 -0
- package/api/search/_features.d.mts +35 -0
- package/api/search/_features.mjs +155 -0
- package/api/search/_gather-components.d.mts +26 -0
- package/api/search/_gather-components.mjs +284 -0
- package/api/search/_gather-docs.d.mts +20 -0
- package/api/search/_gather-docs.mjs +254 -0
- package/api/search/_gather-hooks.d.mts +12 -0
- package/api/search/_gather-hooks.mjs +52 -0
- package/api/search/_gather-templates.d.mts +25 -0
- package/api/search/_gather-templates.mjs +107 -0
- package/api/search/_lexical.d.mts +135 -0
- package/api/search/_lexical.mjs +351 -0
- package/api/search/_main-searcher.d.mts +19 -0
- package/api/search/_main-searcher.mjs +115 -0
- package/api/search/_main-searcher.test.mjs +106 -0
- package/api/search/_main-searcher.weights.json +1 -0
- package/api/search/_module-doc.d.mts +32 -0
- package/api/search/_module-doc.mjs +78 -0
- package/api/search/_order.d.mts +32 -0
- package/api/search/_order.mjs +162 -0
- package/api/search/_order.test.mjs +72 -0
- package/api/search/_priority.d.mts +50 -0
- package/api/search/_priority.mjs +139 -0
- package/api/search/_query-words.d.mts +25 -0
- package/api/search/_query-words.mjs +92 -0
- package/api/search/_query.d.mts +65 -0
- package/api/search/_query.mjs +176 -0
- package/api/search/_result.d.mts +97 -0
- package/api/search/_result.mjs +150 -0
- package/api/search/_score.d.mts +60 -0
- package/api/search/_score.mjs +283 -0
- package/api/search/_synonyms.d.mts +21 -0
- package/api/search/_synonyms.mjs +67 -0
- package/api/search/_template-searcher.d.mts +4 -0
- package/api/search/_template-searcher.mjs +23 -0
- package/api/search/_template-searcher.test.mjs +40 -0
- package/api/search/_template-searcher.weights.json +1 -0
- package/api/search/_titles.d.mts +33 -0
- package/api/search/_titles.mjs +65 -0
- package/api/search/_words.d.mts +64 -0
- package/api/search/_words.mjs +157 -0
- package/api/search/search.d.mts +22 -305
- package/api/search/search.doc.mjs +7 -5
- package/api/search/search.mjs +53 -2483
- package/api/search/search.type.d.mts +6 -1
- package/api/search/search.type.mjs +6 -1
- package/api/search/tree.d.mts +15 -0
- package/api/search/tree.mjs +40 -0
- package/api/search/tree.test.mjs +66 -0
- package/api/search/tree.weights.json +1 -0
- package/api/theme/build/build.packed-old-core.test.mjs +1 -0
- package/api/upgrade/_adapter.d.mts +57 -9
- package/api/upgrade/_adapter.mjs +198 -28
- package/api/upgrade/registry/registry.d.mts +10 -4
- package/api/upgrade/registry/registry.mjs +33 -16
- package/api/upgrade/registry/registry.test.mjs +85 -0
- package/api/upgrade/run/run.d.mts +4 -2
- package/api/upgrade/run/run.mjs +13 -2
- package/api/upgrade/run/run.test.mjs +417 -3
- package/api/upgrade/upgrade.doc.mjs +10 -5
- package/api/upgrade/upgrade.mjs +13 -2
- package/api/upgrade/upgrade.test.mjs +194 -0
- package/api/upgrade/upgrade.type.d.mts +5 -3
- package/api/upgrade/upgrade.type.mjs +8 -6
- package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
- package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +6 -2
- package/assets/templates/themes/butter/butterTheme.ts +4 -2
- package/assets/templates/themes/stone/stoneTheme.ts +4 -2
- package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
- package/authoring/debug/parse.d.mts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/init.behavior.test.mjs +230 -1
- package/clients/cli/commands/init.doc.mjs +28 -9
- package/clients/cli/commands/init.flag-help.test.mjs +1 -0
- package/clients/cli/commands/init.mjs +2 -2
- package/clients/cli/commands/outside-project.test.mjs +2 -1
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- package/clients/cli/commands/upgrade.doc.mjs +4 -2
- package/clients/cli/commands/upgrade.mjs +9 -2
- package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
- package/clients/cli/index.mjs +19 -1
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
- package/foundation/agent-docs/agent-doc-state.mjs +35 -8
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1010 -99
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +678 -35
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/component-discovery.d.mts +37 -0
- package/foundation/discovery/component-discovery.mjs +80 -5
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/docs-discovery.mjs +14 -1
- package/foundation/discovery/module-exports.d.mts +41 -0
- package/foundation/discovery/module-exports.mjs +253 -0
- package/foundation/discovery/template-adapter.mjs +18 -1
- package/foundation/discovery/template-needs.mjs +1 -44
- package/foundation/discovery/theme-discovery.mjs +147 -3
- package/foundation/doc-compiler/doc-loads.test.mjs +8 -0
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/fs/paths.d.mts +70 -8
- package/foundation/fs/paths.mjs +556 -38
- package/foundation/fs/paths.test.mjs +430 -54
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/computer-integrations.d.mts +6 -3
- package/foundation/integrations/computer-integrations.mjs +38 -9
- package/foundation/integrations/contribution-inventory.d.mts +15 -20
- package/foundation/integrations/contribution-inventory.mjs +41 -5
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/run-context.mjs +14 -0
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/route/route.d.mts +285 -0
- package/foundation/route/route.mjs +324 -0
- package/foundation/route/route.test.mjs +153 -0
- package/foundation/runtime/hosted-runtime.test.mjs +403 -0
- package/foundation/runtime/locked-versions.d.mts +32 -0
- package/foundation/runtime/locked-versions.mjs +65 -0
- package/foundation/runtime/package-root.d.mts +91 -0
- package/foundation/runtime/package-root.mjs +174 -0
- package/foundation/runtime/package-set.d.mts +62 -0
- package/foundation/runtime/package-set.mjs +130 -0
- package/foundation/runtime/package-set.test.mjs +432 -0
- package/host/door-conformance.test.mjs +274 -0
- package/host/index.d.ts +116 -0
- package/host/index.mjs +295 -0
- package/host/index.test.mjs +302 -0
- package/package.json +14 -9
|
@@ -309,6 +309,259 @@ export function moduleExportSource(file, exportName, seen = new Set()) {
|
|
|
309
309
|
return found;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
/**
|
|
313
|
+
* The module's own top-level statements of one type: an `export` inside a
|
|
314
|
+
* TypeScript namespace names a member of that namespace, not of the module.
|
|
315
|
+
* @param {any} program
|
|
316
|
+
* @param {string} type
|
|
317
|
+
* @returns {any[]}
|
|
318
|
+
*/
|
|
319
|
+
function topLevel(program, type) {
|
|
320
|
+
return program.body.filter((/** @type {any} */ node) => node.type === type);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Every runtime name a module exports that this read can see (`default` for
|
|
325
|
+
* a default export), or null when it cannot see them all: an `export *`, `export =`, CommonJS exports, or a
|
|
326
|
+
* file that does not parse.
|
|
327
|
+
* @param {string} file
|
|
328
|
+
* @returns {string[] | null}
|
|
329
|
+
*/
|
|
330
|
+
export function moduleExportNames(file) {
|
|
331
|
+
let text;
|
|
332
|
+
let ast;
|
|
333
|
+
try {
|
|
334
|
+
if (!fs.statSync(file).isFile()) return null;
|
|
335
|
+
text = moduleText(file);
|
|
336
|
+
ast = parseModule(file);
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
if (/\bmodule\.exports\b|\bexports\.[$\w]/u.test(text)) return null;
|
|
341
|
+
if (
|
|
342
|
+
topLevel(ast.program, 'TSExportAssignment').length > 0 ||
|
|
343
|
+
topLevel(ast.program, 'ExportAllDeclaration').length > 0
|
|
344
|
+
) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
/** @type {Set<string>} */
|
|
348
|
+
const names = new Set();
|
|
349
|
+
for (const node of topLevel(ast.program, 'TSImportEqualsDeclaration')) {
|
|
350
|
+
if (!node.isExport) continue;
|
|
351
|
+
// A type-only alias has no runtime value; any other might.
|
|
352
|
+
if (node.importKind === 'type') continue;
|
|
353
|
+
if (typeof node.id?.name !== 'string') return null;
|
|
354
|
+
names.add(node.id.name);
|
|
355
|
+
}
|
|
356
|
+
for (const node of topLevel(ast.program, 'ExportNamedDeclaration')) {
|
|
357
|
+
if (node.exportKind === 'type') continue;
|
|
358
|
+
const declaration = node.declaration;
|
|
359
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
360
|
+
for (const item of declaration.declarations) {
|
|
361
|
+
if (item.id?.type !== 'Identifier') return null;
|
|
362
|
+
names.add(item.id.name);
|
|
363
|
+
}
|
|
364
|
+
} else if (declaration != null) {
|
|
365
|
+
if (!runtimeDefault(declaration)) continue;
|
|
366
|
+
if (typeof declaration.id?.name !== 'string') return null;
|
|
367
|
+
names.add(declaration.id.name);
|
|
368
|
+
}
|
|
369
|
+
for (const specifier of node.specifiers ?? []) {
|
|
370
|
+
if (specifier.exportKind === 'type') continue;
|
|
371
|
+
const exported = specifier.exported?.name ?? specifier.exported?.value;
|
|
372
|
+
if (typeof exported === 'string') names.add(exported);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (
|
|
376
|
+
topLevel(ast.program, 'ExportDefaultDeclaration').some(
|
|
377
|
+
(/** @type {any} */ node) => runtimeDefault(node.declaration),
|
|
378
|
+
)
|
|
379
|
+
) {
|
|
380
|
+
names.add('default');
|
|
381
|
+
}
|
|
382
|
+
return [...names].sort();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @typedef {{verdict: 'exports'} | {verdict: 'lacks'} | {verdict: 'unknown', reason: string}} ExportVerdict
|
|
387
|
+
*/
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Whether a module exports `exportName` at runtime, read without executing
|
|
391
|
+
* it: `exports` when a declaration or a followed re-export binds it, `lacks`
|
|
392
|
+
* only when every export the module writes was read and none binds it, and
|
|
393
|
+
* `unknown`, with the first reason, when some export could not be read: a
|
|
394
|
+
* re-export from another package or from a specifier that resolves to no
|
|
395
|
+
* file, a destructured binding, a namespace, an ambient or const enum
|
|
396
|
+
* declaration, `export =`, CommonJS exports, or a file that does not parse.
|
|
397
|
+
* @param {string} file
|
|
398
|
+
* @param {string} exportName
|
|
399
|
+
* @param {Set<string>} [seen]
|
|
400
|
+
* @returns {ExportVerdict}
|
|
401
|
+
*/
|
|
402
|
+
export function moduleExportVerdict(file, exportName, seen = new Set()) {
|
|
403
|
+
const identity = `${file}\0${exportName}`;
|
|
404
|
+
// A cycle adds nothing a module's own exports have not already said.
|
|
405
|
+
if (seen.has(identity)) return {verdict: 'lacks'};
|
|
406
|
+
seen.add(identity);
|
|
407
|
+
const shown = path.basename(file);
|
|
408
|
+
let text;
|
|
409
|
+
let ast;
|
|
410
|
+
try {
|
|
411
|
+
if (!fs.statSync(file).isFile()) {
|
|
412
|
+
return {verdict: 'unknown', reason: `${shown} is not a file.`};
|
|
413
|
+
}
|
|
414
|
+
text = moduleText(file);
|
|
415
|
+
ast = parseModule(file);
|
|
416
|
+
} catch {
|
|
417
|
+
return {
|
|
418
|
+
verdict: 'unknown',
|
|
419
|
+
reason: `${shown} could not be read or parsed.`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** @type {string[]} */
|
|
423
|
+
const unknown = [];
|
|
424
|
+
if (/\bmodule\.exports\b|\bexports\.[$\w]/u.test(text)) {
|
|
425
|
+
unknown.push(
|
|
426
|
+
`${shown} uses CommonJS exports, whose names this check does not read.`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (topLevel(ast.program, 'TSExportAssignment').length > 0) {
|
|
430
|
+
unknown.push(`${shown} uses \`export =\`, whose names this check does not read.`);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Follow a re-export of `imported` from `specifier`.
|
|
434
|
+
* @param {string} specifier
|
|
435
|
+
* @param {string} imported
|
|
436
|
+
* @param {string} what how the reason names what is re-exported
|
|
437
|
+
* @returns {boolean} true when it proves the export
|
|
438
|
+
*/
|
|
439
|
+
const follow = (specifier, imported, what) => {
|
|
440
|
+
if (!specifier.startsWith('.')) {
|
|
441
|
+
unknown.push(
|
|
442
|
+
`${shown} re-exports ${what} from "${specifier}", another package this check does not read.`,
|
|
443
|
+
);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
const target = resolveLocalModule(specifier, file);
|
|
447
|
+
if (!target) {
|
|
448
|
+
unknown.push(
|
|
449
|
+
`${shown} re-exports ${what} from "${specifier}", which resolves to no file this check can read.`,
|
|
450
|
+
);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
const inner = moduleExportVerdict(target, imported, seen);
|
|
454
|
+
if (inner.verdict === 'exports') return true;
|
|
455
|
+
if (inner.verdict === 'unknown') unknown.push(inner.reason);
|
|
456
|
+
return false;
|
|
457
|
+
};
|
|
458
|
+
// `export import Name = Some.Path;` binds a runtime value unless it is
|
|
459
|
+
// type-only.
|
|
460
|
+
for (const node of topLevel(ast.program, 'TSImportEqualsDeclaration')) {
|
|
461
|
+
if (!node.isExport || node.id?.name !== exportName) continue;
|
|
462
|
+
if (node.importKind === 'type') {
|
|
463
|
+
unknown.push(
|
|
464
|
+
`${shown} exports ${exportName} with \`export import type\`, which has no runtime value this check can see.`,
|
|
465
|
+
);
|
|
466
|
+
} else {
|
|
467
|
+
return {verdict: 'exports'};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const node of topLevel(ast.program, 'ExportNamedDeclaration')) {
|
|
471
|
+
if (node.exportKind === 'type') continue;
|
|
472
|
+
const declaration = node.declaration;
|
|
473
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
474
|
+
for (const item of declaration.declarations) {
|
|
475
|
+
if (item.id?.type !== 'Identifier') {
|
|
476
|
+
unknown.push(
|
|
477
|
+
`${shown} exports a destructured binding, whose names this check does not read.`,
|
|
478
|
+
);
|
|
479
|
+
} else if (item.id.name === exportName) {
|
|
480
|
+
if (declaration.declare === true) {
|
|
481
|
+
unknown.push(
|
|
482
|
+
`${shown} declares ${exportName} with \`declare\`, which has no runtime value this check can see.`,
|
|
483
|
+
);
|
|
484
|
+
} else {
|
|
485
|
+
return {verdict: 'exports'};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} else if (declaration != null && declaration.id?.name === exportName) {
|
|
490
|
+
if (
|
|
491
|
+
['TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(
|
|
492
|
+
declaration.type,
|
|
493
|
+
)
|
|
494
|
+
) {
|
|
495
|
+
// A type only: no runtime export.
|
|
496
|
+
} else if (declaration.type === 'TSModuleDeclaration') {
|
|
497
|
+
unknown.push(
|
|
498
|
+
`${shown} exports ${exportName} as a TypeScript namespace, which has a runtime value only when it holds one.`,
|
|
499
|
+
);
|
|
500
|
+
} else if (declaration.declare === true || declaration.const === true) {
|
|
501
|
+
unknown.push(
|
|
502
|
+
`${shown} declares ${exportName} as an ambient or const declaration, which has no runtime value this check can see.`,
|
|
503
|
+
);
|
|
504
|
+
} else if (runtimeDefault(declaration)) {
|
|
505
|
+
return {verdict: 'exports'};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const specifier of node.specifiers ?? []) {
|
|
509
|
+
if (specifier.exportKind === 'type') continue;
|
|
510
|
+
const exported = specifier.exported?.name ?? specifier.exported?.value;
|
|
511
|
+
if (exported !== exportName) continue;
|
|
512
|
+
if (!node.source || specifier.type === 'ExportNamespaceSpecifier') {
|
|
513
|
+
return {verdict: 'exports'};
|
|
514
|
+
}
|
|
515
|
+
if (specifier.type !== 'ExportSpecifier') {
|
|
516
|
+
unknown.push(`${shown} re-exports ${exportName} in a form this check does not read.`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const imported =
|
|
520
|
+
specifier.local?.name ?? specifier.local?.value ?? exportName;
|
|
521
|
+
if (follow(String(node.source.value), imported, exportName)) {
|
|
522
|
+
return {verdict: 'exports'};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (exportName === 'default') {
|
|
527
|
+
const defaults = topLevel(ast.program, 'ExportDefaultDeclaration');
|
|
528
|
+
if (
|
|
529
|
+
defaults.some((/** @type {any} */ node) =>
|
|
530
|
+
runtimeDefault(node.declaration),
|
|
531
|
+
)
|
|
532
|
+
) {
|
|
533
|
+
return {verdict: 'exports'};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const node of topLevel(ast.program, 'ExportAllDeclaration')) {
|
|
537
|
+
if (node.exportKind === 'type') continue;
|
|
538
|
+
const exported = node.exported?.name ?? node.exported?.value;
|
|
539
|
+
if (exported != null) {
|
|
540
|
+
if (exported === exportName) return {verdict: 'exports'};
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
// `export *` never re-exports a default.
|
|
544
|
+
if (exportName === 'default') continue;
|
|
545
|
+
if (follow(String(node.source?.value ?? ''), exportName, 'everything')) {
|
|
546
|
+
return {verdict: 'exports'};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return unknown.length > 0
|
|
550
|
+
? {verdict: 'unknown', reason: unknown[0]}
|
|
551
|
+
: {verdict: 'lacks'};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Whether a module provably does not export `exportName` at runtime: see
|
|
556
|
+
* {@link moduleExportVerdict}.
|
|
557
|
+
* @param {string} file
|
|
558
|
+
* @param {string} exportName
|
|
559
|
+
* @returns {boolean}
|
|
560
|
+
*/
|
|
561
|
+
export function moduleLacksExport(file, exportName) {
|
|
562
|
+
return moduleExportVerdict(file, exportName).verdict === 'lacks';
|
|
563
|
+
}
|
|
564
|
+
|
|
312
565
|
/**
|
|
313
566
|
* Marks an object {@link staticExportObject} returns (or one inside it) whose
|
|
314
567
|
* source also has properties or spreads it could not read as literals. Not
|
|
@@ -1192,6 +1192,23 @@ function reportSamePackageReplacements(pkg, autolinked, templates, errors) {
|
|
|
1192
1192
|
}
|
|
1193
1193
|
}
|
|
1194
1194
|
|
|
1195
|
+
/**
|
|
1196
|
+
* The fix for a template doc with no same-stem `.tsx`: rename a same-stem
|
|
1197
|
+
* source written in another extension, or add the `.tsx`.
|
|
1198
|
+
* @param {string} docPath
|
|
1199
|
+
* @param {string} sourcePath the `.tsx` it needs
|
|
1200
|
+
*/
|
|
1201
|
+
function missingTemplateSourceFix(docPath, sourcePath) {
|
|
1202
|
+
const source = path.basename(sourcePath);
|
|
1203
|
+
const stem = source.slice(0, -'.tsx'.length);
|
|
1204
|
+
const other = ['.jsx', '.js', '.ts', '.mjs']
|
|
1205
|
+
.map(extension => `${stem}${extension}`)
|
|
1206
|
+
.find(file => fs.existsSync(path.join(path.dirname(sourcePath), file)));
|
|
1207
|
+
return other
|
|
1208
|
+
? `Found ${other}, but a template source must be a .tsx file. Fix: rename ${other} to ${source}.`
|
|
1209
|
+
: `Fix: add ${source} beside ${path.basename(docPath)}, with the template as its default export.`;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1195
1212
|
/**
|
|
1196
1213
|
* Discover the templates contributed by a SINGLE integration. Same per-template
|
|
1197
1214
|
* rules as {@link discoverIntegrationTemplates} (same-stem source required,
|
|
@@ -1225,7 +1242,7 @@ export async function discoverIntegrationTemplatesForOne(integration) {
|
|
|
1225
1242
|
errors.push({
|
|
1226
1243
|
package: pkgLabel,
|
|
1227
1244
|
template: id,
|
|
1228
|
-
message: `Template "${id}" is missing its same-stem source file ${path.basename(sourcePath)}
|
|
1245
|
+
message: `Template "${id}" is missing its same-stem source file ${path.basename(sourcePath)}. ${missingTemplateSourceFix(docPath, sourcePath)}`,
|
|
1229
1246
|
});
|
|
1230
1247
|
// A replacement this unusable template declares still counts, so its
|
|
1231
1248
|
// target fails closed instead of going to a sibling.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import * as fs from 'node:fs';
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import module from 'node:module';
|
|
21
|
-
import {CLI_ROOT} from '../fs/paths.mjs';
|
|
21
|
+
import {CLI_ROOT, findWorkspaceRoot} from '../fs/paths.mjs';
|
|
22
22
|
import {detectStylingSystem} from '../agent-docs/agent-docs.mjs';
|
|
23
23
|
import {
|
|
24
24
|
decidePackageManagerIn,
|
|
@@ -71,49 +71,6 @@ const ADD_VERB = {
|
|
|
71
71
|
npm: 'npm install',
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* Whether a package.json declares a workspace (`workspaces` field).
|
|
76
|
-
* @param {string} dir
|
|
77
|
-
* @returns {{exists: boolean, declaresWorkspace: boolean}}
|
|
78
|
-
*/
|
|
79
|
-
function readPackageJson(dir) {
|
|
80
|
-
const pkgPath = path.join(dir, 'package.json');
|
|
81
|
-
if (!fs.existsSync(pkgPath)) return {exists: false, declaresWorkspace: false};
|
|
82
|
-
try {
|
|
83
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
84
|
-
return {exists: true, declaresWorkspace: pkg.workspaces != null};
|
|
85
|
-
} catch {
|
|
86
|
-
return {exists: true, declaresWorkspace: false};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* The workspace root (AST-048 FR15): the nearest directory at or above the
|
|
92
|
-
* target that declares a workspace, by `pnpm-workspace.yaml` or a package.json
|
|
93
|
-
* `workspaces` field; outside a workspace, the nearest package.json's
|
|
94
|
-
* directory; with no package.json, the target itself.
|
|
95
|
-
* @param {string} target
|
|
96
|
-
* @returns {string}
|
|
97
|
-
*/
|
|
98
|
-
function findWorkspaceRoot(target) {
|
|
99
|
-
let dir = path.resolve(target);
|
|
100
|
-
/** @type {string | null} */
|
|
101
|
-
let nearestPackage = null;
|
|
102
|
-
for (;;) {
|
|
103
|
-
const pkg = readPackageJson(dir);
|
|
104
|
-
if (
|
|
105
|
-
fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')) ||
|
|
106
|
-
pkg.declaresWorkspace
|
|
107
|
-
) {
|
|
108
|
-
return dir;
|
|
109
|
-
}
|
|
110
|
-
if (pkg.exists && nearestPackage == null) nearestPackage = dir;
|
|
111
|
-
const parent = path.dirname(dir);
|
|
112
|
-
if (parent === dir) return nearestPackage ?? path.resolve(target);
|
|
113
|
-
dir = parent;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
74
|
/**
|
|
118
75
|
* The project's package manager for an install command (AST-048 FR17, FR19):
|
|
119
76
|
* the nearest directory from the target up to the workspace root that has a
|
|
@@ -22,6 +22,7 @@ import * as path from 'node:path';
|
|
|
22
22
|
import {lowerDoc} from '../doc-compiler/compile.mjs';
|
|
23
23
|
import {packageSource} from '../doc-compiler/source.mjs';
|
|
24
24
|
import {CLI_ROOT} from '../fs/paths.mjs';
|
|
25
|
+
import {formatCliCommand} from '../env/package-manager.mjs';
|
|
25
26
|
import {LOCAL_THEME_ROOT} from '../config/theme-state.mjs';
|
|
26
27
|
import {assertWithin, PathSafetyError} from '../fs/path-safety.mjs';
|
|
27
28
|
|
|
@@ -958,6 +959,126 @@ function declaresThemeDoc(ast, exportDefault) {
|
|
|
958
959
|
});
|
|
959
960
|
}
|
|
960
961
|
|
|
962
|
+
/**
|
|
963
|
+
* Whether a file is the module `astryx theme build` writes rather than a
|
|
964
|
+
* theme source: plain JavaScript that carries the build's generated header
|
|
965
|
+
* and never calls defineTheme.
|
|
966
|
+
* @param {string} file
|
|
967
|
+
*/
|
|
968
|
+
function isBuiltThemeModule(file) {
|
|
969
|
+
if (!/\.m?js$/u.test(file)) return false;
|
|
970
|
+
let text;
|
|
971
|
+
try {
|
|
972
|
+
text = fs.readFileSync(file, 'utf8');
|
|
973
|
+
} catch {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
return (
|
|
977
|
+
/@generated by `astryx theme build`/u.test(text) &&
|
|
978
|
+
!/\bdefineTheme\b/u.test(text)
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* The fix when a descriptor's stem names both a theme source and the module
|
|
984
|
+
* `astryx theme build` writes beside it (`brand.ts` and its built `brand.js`):
|
|
985
|
+
* the build names its output after the theme, so the source and descriptor
|
|
986
|
+
* take the `<name>Theme` stem instead. Null when no built module is among
|
|
987
|
+
* the matches, or the stem already ends in `Theme`.
|
|
988
|
+
* @param {string} sourceDir
|
|
989
|
+
* @param {string[]} sources same-stem module file names
|
|
990
|
+
* @param {string} doc the descriptor's file name
|
|
991
|
+
* @param {string} exportName the descriptor's stem
|
|
992
|
+
* @returns {string | null}
|
|
993
|
+
*/
|
|
994
|
+
function builtThemeSourceFix(sourceDir, sources, doc, exportName) {
|
|
995
|
+
if (exportName.endsWith('Theme')) return null;
|
|
996
|
+
const built = sources.filter(file =>
|
|
997
|
+
isBuiltThemeModule(path.join(sourceDir, file)),
|
|
998
|
+
);
|
|
999
|
+
const source = sources.filter(file => !built.includes(file));
|
|
1000
|
+
if (built.length === 0 || source.length !== 1) return null;
|
|
1001
|
+
const extension = source[0].slice(exportName.length);
|
|
1002
|
+
const stem = `${exportName}Theme`;
|
|
1003
|
+
return `Fix: ${built.map(file => `"${file}"`).join(', ')} is the module \`${formatCliCommand('theme build', sourceDir)}\` writes, which takes the theme's name; rename ${source[0]} to ${stem}${extension} and ${doc} to ${stem}${THEME_DOC_SUFFIX}, export \`${stem}\` from it, and build again.`;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Why a descriptor pairs with the built module and the built module lacks
|
|
1008
|
+
* the export the descriptor's stem names (`ocean.doc.mjs` beside a built
|
|
1009
|
+
* `ocean.js` that exports `oceanTheme`): the descriptor is named for the
|
|
1010
|
+
* build's output, not the theme's source. The source is named only when the
|
|
1011
|
+
* built module's `Source:` header, or a source whose defineTheme gives the
|
|
1012
|
+
* theme's name, confirms it; otherwise the message stays generic. Null when
|
|
1013
|
+
* the entry is not a built module.
|
|
1014
|
+
* @param {string} label the descriptor, as messages name it
|
|
1015
|
+
* @param {string} sourceDir
|
|
1016
|
+
* @param {string} entry the file the descriptor pairs with
|
|
1017
|
+
* @param {string} exportName the descriptor's stem
|
|
1018
|
+
* @param {string} doc the descriptor's file name
|
|
1019
|
+
* @param {string} slug the theme's name
|
|
1020
|
+
* @param {string[]} files the theme folder's files
|
|
1021
|
+
* @returns {string | null}
|
|
1022
|
+
*/
|
|
1023
|
+
function builtModuleDescriptorMessage(
|
|
1024
|
+
label,
|
|
1025
|
+
sourceDir,
|
|
1026
|
+
entry,
|
|
1027
|
+
exportName,
|
|
1028
|
+
doc,
|
|
1029
|
+
slug,
|
|
1030
|
+
files,
|
|
1031
|
+
) {
|
|
1032
|
+
if (!isBuiltThemeModule(path.join(sourceDir, entry))) return null;
|
|
1033
|
+
const source = confirmedThemeSource(sourceDir, entry, slug, files);
|
|
1034
|
+
const head = `${label} shares its stem with the built module "${entry}", which does not export "${exportName}": a descriptor must share its stem with the source that calls defineTheme.`;
|
|
1035
|
+
if (source == null) {
|
|
1036
|
+
return `${head} Fix: rename ${doc} to share its stem with the theme's source (the module that calls defineTheme, such as ${exportName}Theme.ts).`;
|
|
1037
|
+
}
|
|
1038
|
+
const stem = source.replace(/\.[^.]+$/u, '');
|
|
1039
|
+
return `${head} The theme's source is "${source}". Fix: rename ${doc} to ${stem}${THEME_DOC_SUFFIX}.`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* The theme's source, when something confirms it: the file the built
|
|
1044
|
+
* module's `Source:` header names, if it is in the folder, else the one
|
|
1045
|
+
* module in the folder whose defineTheme gives the theme's name as a
|
|
1046
|
+
* literal. Null otherwise: a `<name>Theme` file alone may be a helper.
|
|
1047
|
+
* @param {string} sourceDir
|
|
1048
|
+
* @param {string} built the built module's file name
|
|
1049
|
+
* @param {string} slug
|
|
1050
|
+
* @param {string[]} files
|
|
1051
|
+
* @returns {string | null}
|
|
1052
|
+
*/
|
|
1053
|
+
function confirmedThemeSource(sourceDir, built, slug, files) {
|
|
1054
|
+
const own = files.filter(
|
|
1055
|
+
file =>
|
|
1056
|
+
!file.includes('/') &&
|
|
1057
|
+
file !== built &&
|
|
1058
|
+
THEME_MODULE_EXTENSIONS.some(extension => file.endsWith(extension)),
|
|
1059
|
+
);
|
|
1060
|
+
/** @param {string} file */
|
|
1061
|
+
const read = file => {
|
|
1062
|
+
try {
|
|
1063
|
+
return fs.readFileSync(path.join(sourceDir, file), 'utf8');
|
|
1064
|
+
} catch {
|
|
1065
|
+
return '';
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const header = /^\s*\*?\s*Source:\s*(\S+)\s*$/mu.exec(read(built));
|
|
1069
|
+
if (header) {
|
|
1070
|
+
const named = path.posix.basename(header[1].replace(/\\/gu, '/'));
|
|
1071
|
+
if (own.includes(named)) return named;
|
|
1072
|
+
}
|
|
1073
|
+
const quoted = slug.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
1074
|
+
const naming = new RegExp(
|
|
1075
|
+
`\\bdefineTheme\\s*\\(\\s*\\{[^}]*\\bname\\s*:\\s*['"\`]${quoted}['"\`]`,
|
|
1076
|
+
'u',
|
|
1077
|
+
);
|
|
1078
|
+
const matches = own.filter(file => naming.test(read(file)));
|
|
1079
|
+
return matches.length === 1 ? matches[0] : null;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
961
1082
|
/** A module named for the theme it exports, such as `oceanTheme.ts`. */
|
|
962
1083
|
const THEME_SOURCE_RE = /^[$A-Z_a-z][$\w]*Theme\.(?:mjs|js|mts|ts|tsx|jsx)$/u;
|
|
963
1084
|
|
|
@@ -1240,8 +1361,17 @@ export function discoverThemeDirectory(
|
|
|
1240
1361
|
: evidence.includes('/')
|
|
1241
1362
|
? `; "${evidence}" is in a subfolder`
|
|
1242
1363
|
: ` beside "${evidence}"`;
|
|
1364
|
+
const themeDoc = formatCliCommand('docs authoring theme-doc', sourceDir);
|
|
1365
|
+
const fix =
|
|
1366
|
+
docs.length > 1
|
|
1367
|
+
? `Fix: keep the one descriptor that shares its stem with the theme's source and delete the rest.`
|
|
1368
|
+
: evidence.includes('/')
|
|
1369
|
+
? `Fix: move "${evidence}" up into ${slug}/, beside where its descriptor goes.`
|
|
1370
|
+
: THEME_SOURCE_RE.test(evidence)
|
|
1371
|
+
? `Fix: add ${evidence.replace(/\.[^.]+$/u, '')}${THEME_DOC_SUFFIX} beside it with type: 'theme' and name: '${slug}'; \`${themeDoc}\` lists its fields.`
|
|
1372
|
+
: `Fix: add a descriptor that shares its stem with the theme's source, with type: 'theme' and name: '${slug}'; \`${themeDoc}\` lists its fields.`;
|
|
1243
1373
|
throw new Error(
|
|
1244
|
-
`${theme} must contain exactly one same-stem .doc.mjs descriptor; found ${docs.length}${detail}
|
|
1374
|
+
`${theme} must contain exactly one same-stem .doc.mjs descriptor; found ${docs.length}${detail}. ${fix}`,
|
|
1245
1375
|
);
|
|
1246
1376
|
}
|
|
1247
1377
|
const nested = descriptors.find(file => file.includes('/'));
|
|
@@ -1267,8 +1397,13 @@ export function discoverThemeDirectory(
|
|
|
1267
1397
|
extension => `${exportName}${extension}`,
|
|
1268
1398
|
).filter(file => listing.files.includes(file));
|
|
1269
1399
|
if (sources.length !== 1) {
|
|
1400
|
+
const fix =
|
|
1401
|
+
sources.length > 1
|
|
1402
|
+
? (builtThemeSourceFix(sourceDir, sources, docs[0], exportName) ??
|
|
1403
|
+
`Fix: keep one of them; the theme's source is a single module.`)
|
|
1404
|
+
: `Fix: add ${exportName}.ts beside ${docs[0]}, exporting \`${exportName}\` from defineTheme.`;
|
|
1270
1405
|
throw new Error(
|
|
1271
|
-
`${theme} must contain exactly one same-stem source for ${docs[0]}; found ${sources.length}${sources.length > 1 ? `: ${quoted(sources)}` : ''}
|
|
1406
|
+
`${theme} must contain exactly one same-stem source for ${docs[0]}; found ${sources.length}${sources.length > 1 ? `: ${quoted(sources)}` : ''}. ${fix}`,
|
|
1272
1407
|
);
|
|
1273
1408
|
}
|
|
1274
1409
|
|
|
@@ -1314,7 +1449,16 @@ export function discoverThemeDirectory(
|
|
|
1314
1449
|
)
|
|
1315
1450
|
) {
|
|
1316
1451
|
throw new ThemeRuntimeExportError(
|
|
1317
|
-
|
|
1452
|
+
builtModuleDescriptorMessage(
|
|
1453
|
+
label,
|
|
1454
|
+
sourceDir,
|
|
1455
|
+
entry,
|
|
1456
|
+
exportName,
|
|
1457
|
+
docs[0],
|
|
1458
|
+
slug,
|
|
1459
|
+
listing.files,
|
|
1460
|
+
) ??
|
|
1461
|
+
`Theme "${slug}" for ${owner} entry "${entry}" does not export "${exportName}".`,
|
|
1318
1462
|
);
|
|
1319
1463
|
}
|
|
1320
1464
|
} catch (error) {
|
|
@@ -248,6 +248,10 @@ const RUNNERS = {
|
|
|
248
248
|
runs: 'astryx.config, through the config parser',
|
|
249
249
|
sites: ['loadModuleWithParser(..., parseConfig)'],
|
|
250
250
|
},
|
|
251
|
+
'foundation/agent-docs/agent-docs.mjs': {
|
|
252
|
+
runs: 'git, sl or hg, to read whether the repository ignores an untracked agent file',
|
|
253
|
+
sites: ['child_process.spawnSync'],
|
|
254
|
+
},
|
|
251
255
|
'foundation/fs/file-protection.mjs': {
|
|
252
256
|
runs: 'the ignore matcher (CommonJS), loaded when a resolver first reads an ignore file',
|
|
253
257
|
sites: ['require(ignore)'],
|
|
@@ -339,6 +343,10 @@ const RUNNERS = {
|
|
|
339
343
|
runs: "the CLI release that answers a project's run, when it is not this copy",
|
|
340
344
|
sites: ['child_process.spawn'],
|
|
341
345
|
},
|
|
346
|
+
'host/index.mjs': {
|
|
347
|
+
runs: "another release's host entry, from one of the host's package sets, when that release answers a hosted run",
|
|
348
|
+
sites: ['import(<computed>)'],
|
|
349
|
+
},
|
|
342
350
|
'scripts/generate-cli-readme.mjs': {
|
|
343
351
|
runs: '`astryx manifest` and the error-code and response-type docs, to write the README tables (build time, not shipped)',
|
|
344
352
|
sites: ['child_process.spawnSync', 'import(<computed>)'],
|
|
@@ -173,8 +173,23 @@ export async function readDocView(file, options) {
|
|
|
173
173
|
);
|
|
174
174
|
if (result.loadFailed) throw result.loadFailure;
|
|
175
175
|
if (result.missing) {
|
|
176
|
-
if (strict)
|
|
176
|
+
if (strict) {
|
|
177
|
+
// A doc that exports nothing its reader takes (an empty file, or one
|
|
178
|
+
// with other exports) is said plainly, with what it does export and
|
|
179
|
+
// the fix; the schema would only say "(root): Invalid input".
|
|
180
|
+
const exported =
|
|
181
|
+
options.exports ??
|
|
182
|
+
/** @type {Record<string, readonly string[]>} */ (DOC_EXPORTS)[
|
|
183
|
+
options.root
|
|
184
|
+
] ??
|
|
185
|
+
[];
|
|
186
|
+
const problem =
|
|
187
|
+
result.missingValue === undefined
|
|
188
|
+
? await missingDocProblem(file, options, exported)
|
|
189
|
+
: null;
|
|
190
|
+
if (problem) throw new Error(`${options.label ?? file} ${problem}`);
|
|
177
191
|
parserFor(options.root)(result.missingValue, options.label ?? file);
|
|
192
|
+
}
|
|
178
193
|
return result.missingValue;
|
|
179
194
|
}
|
|
180
195
|
if (strict && result.failed) throw result.failure;
|
|
@@ -182,6 +197,49 @@ export async function readDocView(file, options) {
|
|
|
182
197
|
return result.view;
|
|
183
198
|
}
|
|
184
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Why a doc file exports no doc, with the fix: it is empty, or it exports
|
|
202
|
+
* other names than the ones its reader takes. Null when neither explains it.
|
|
203
|
+
* @param {string} file
|
|
204
|
+
* @param {ReadOptions} options
|
|
205
|
+
* @param {readonly string[]} exported the export names the reader takes
|
|
206
|
+
* @returns {Promise<string | null>}
|
|
207
|
+
*/
|
|
208
|
+
async function missingDocProblem(file, options, exported) {
|
|
209
|
+
// A theme descriptor is read statically, never imported.
|
|
210
|
+
if (options.root === 'themes') return null;
|
|
211
|
+
const fix =
|
|
212
|
+
'Fix: export the doc object as the default export: `export default {type: ..., name: ..., ...}`.';
|
|
213
|
+
let text;
|
|
214
|
+
try {
|
|
215
|
+
text = fs.readFileSync(file, 'utf8');
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const code = text
|
|
220
|
+
.replace(/^\uFEFF/u, '')
|
|
221
|
+
.replace(/\/\*[\s\S]*?\*\//gu, '')
|
|
222
|
+
.replace(/(^|[^:])\/\/.*$/gmu, '$1')
|
|
223
|
+
.trim();
|
|
224
|
+
if (code === '') return `is empty, so it documents nothing. ${fix}`;
|
|
225
|
+
/** @type {string[]} */
|
|
226
|
+
let names;
|
|
227
|
+
try {
|
|
228
|
+
const mod = await LOADERS[options.loader ?? 'user'](file);
|
|
229
|
+
names = Object.keys(mod ?? {}).filter(name => name !== 'default');
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const takes = exported.map(name =>
|
|
234
|
+
name === 'default' ? 'default export' : `\`${name}\` export`,
|
|
235
|
+
);
|
|
236
|
+
const found =
|
|
237
|
+
names.length > 0
|
|
238
|
+
? ` (it exports ${names.map(name => `\`${name}\``).join(', ')})`
|
|
239
|
+
: ' (it exports nothing)';
|
|
240
|
+
return `has no ${takes.join(' or ')}${found}, so it documents nothing. ${fix}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
185
243
|
/**
|
|
186
244
|
* Load one file as its root reads it: the doc its module exports and, for a
|
|
187
245
|
* component or hook, the translation it exports for `lang`.
|
|
@@ -109,10 +109,19 @@ describe('readDocView', () => {
|
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
it(
|
|
112
|
+
it('reads a file with no doc as undefined, or, checked, says what it exports instead', async () => {
|
|
113
113
|
const file = write('Empty.doc.mjs', 'export const unrelated = 1;\n');
|
|
114
114
|
await expect(loadDocs(file)).resolves.toBeUndefined();
|
|
115
|
-
await expect(loadComponentDoc(file)).rejects.toThrow(
|
|
115
|
+
await expect(loadComponentDoc(file)).rejects.toThrow(
|
|
116
|
+
`${file} has no default export or \`docs\` export (it exports \`unrelated\`), so it documents nothing. Fix: export the doc object as the default export`,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('says a checked doc file is empty', async () => {
|
|
121
|
+
const file = write('Blank.doc.mjs', '// nothing yet\n');
|
|
122
|
+
await expect(loadComponentDoc(file)).rejects.toThrow(
|
|
123
|
+
`${file} is empty, so it documents nothing.`,
|
|
124
|
+
);
|
|
116
125
|
});
|
|
117
126
|
|
|
118
127
|
it('applies the translation a module exports for the reading language', async () => {
|