@astryxdesign/cli 0.7.2 → 0.7.3-canary.0bf23bd
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 +104 -71
- 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 +17 -3
- package/api/component/component.test.mjs +15 -0
- package/api/component/doc-links.test.mjs +2 -2
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.d.mts +2 -2
- package/api/docs/_adapter.mjs +78 -23
- package/api/docs/docs-homes.test.mjs +127 -0
- package/api/docs/docs.doc.mjs +3 -3
- package/api/docs/docs.test.mjs +35 -19
- package/api/docs/integration-tree.test.mjs +20 -20
- package/api/docs/integrationDocs.test.mjs +1 -1
- package/api/docs/namespace-section.test.mjs +6 -6
- package/api/docs/reference-blocks.test.mjs +2 -2
- package/api/doctor/doctor.mjs +102 -24
- package/api/doctor/doctor.test.mjs +37 -12
- package/api/hook/hook.mjs +1 -1
- 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/_adapter.d.mts +8 -0
- package/api/integration/_adapter.mjs +21 -1
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/authoring-checks.test.mjs +220 -0
- 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 +250 -32
- package/api/integration/validate-integration.type.d.mts +59 -1
- package/api/integration/validate-integration.type.mjs +46 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +82 -2
- package/api/search/_adapter.mjs +157 -10
- package/api/search/_adapter.test.mjs +173 -0
- 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 +214 -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 +8 -6
- package/api/search/search.mjs +56 -2482
- package/api/search/search.test.mjs +14 -14
- 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/author-a-theme.doc.mjs +2 -2
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/add-a-component.doc.mjs +1 -1
- package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
- package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
- package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
- package/assets/docs/tree/block-template.doc.mjs +1 -1
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/building-blocks.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +1 -1
- package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
- package/assets/docs/tree/checks.doc.mjs +2 -2
- package/assets/docs/tree/cli-command.doc.mjs +1 -1
- package/assets/docs/tree/cli.doc.mjs +1 -1
- package/assets/docs/tree/codemods.doc.mjs +1 -1
- package/assets/docs/tree/component-family.doc.mjs +1 -1
- package/assets/docs/tree/component-imports.doc.mjs +1 -1
- package/assets/docs/tree/components.doc.mjs +1 -1
- package/assets/docs/tree/configuration.doc.mjs +1 -1
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
- package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/docs.doc.mjs +1 -1
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
- package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
- package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
- package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
- package/assets/docs/tree/global-integrations.doc.mjs +2 -2
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
- package/assets/docs/tree/help.doc.mjs +2 -2
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/links.doc.mjs +1 -1
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/publishing.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/ship.doc.mjs +2 -2
- package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
- package/assets/docs/tree/single-component.doc.mjs +1 -1
- package/assets/docs/tree/start-a-template.doc.mjs +1 -1
- package/assets/docs/tree/subcomponent.doc.mjs +1 -1
- package/assets/docs/tree/template-assets.doc.mjs +1 -1
- package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
- package/assets/docs/tree/template-icons.doc.mjs +1 -1
- package/assets/docs/tree/template-images-media.doc.mjs +1 -1
- package/assets/docs/tree/template-styles.doc.mjs +1 -1
- package/assets/docs/tree/templates.doc.mjs +1 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
- package/assets/docs/tree/themes.doc.mjs +1 -1
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
- package/assets/docs/tree/upgrading.doc.mjs +1 -1
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
- package/assets/docs/tree/versioning.doc.mjs +1 -1
- package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
- package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
- package/assets/docs/use-integrations.doc.mjs +120 -0
- 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/authoring/doctypes/schema/schema.doc.mjs +1 -1
- package/authoring/integration/integration.doc.mjs +1 -1
- package/authoring/integration/type.ts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/command-result-coverage.test.mjs +6 -5
- package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
- package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
- package/clients/cli/commands/component.test.mjs +6 -6
- package/clients/cli/commands/docs.doc.mjs +2 -2
- package/clients/cli/commands/docs.mjs +1 -1
- package/clients/cli/commands/docs.test.mjs +14 -14
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
- package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
- 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/integration-check.test.mjs +48 -0
- package/clients/cli/commands/integration.doc.mjs +1 -1
- package/clients/cli/commands/manifest.doc.mjs +1 -1
- package/clients/cli/commands/manifest.mjs +67 -0
- package/clients/cli/commands/outside-project.test.mjs +2 -1
- package/clients/cli/commands/postinstall.doc.mjs +23 -0
- package/clients/cli/commands/postinstall.mjs +31 -0
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
- 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 +29 -86
- package/clients/cli/lib/component-format.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +12 -7
- package/clients/cli/lib/exit-codes.test.mjs +5 -0
- package/clients/cli/lib/manifest.test.mjs +12 -0
- package/clients/cli/lib/welcome-box.mjs +50 -0
- 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 +1012 -100
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +683 -39
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/authoring-self-docs.d.mts +7 -0
- package/foundation/discovery/authoring-self-docs.mjs +16 -4
- package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
- package/foundation/discovery/authoring-surface.d.mts +1 -1
- package/foundation/discovery/authoring-surface.mjs +2 -2
- package/foundation/discovery/authoring-surface.test.mjs +1 -1
- package/foundation/discovery/cli-self-docs.d.mts +2 -1
- package/foundation/discovery/cli-self-docs.mjs +4 -3
- package/foundation/discovery/cli-self-docs.test.mjs +2 -2
- 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/runner-allowlist.d.mts +2 -4
- package/foundation/discovery/runner-allowlist.mjs +2 -4
- package/foundation/discovery/runner-commands.d.mts +77 -0
- package/foundation/discovery/runner-commands.mjs +294 -0
- package/foundation/discovery/runner-commands.test.mjs +128 -0
- package/foundation/discovery/stray-pointers.test.mjs +1 -1
- 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 +33 -9
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
- package/foundation/doc-compiler/tree.d.mts +32 -6
- package/foundation/doc-compiler/tree.mjs +77 -43
- package/foundation/doc-compiler/tree.test.mjs +7 -7
- 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/fs/read-record.d.mts +18 -0
- package/foundation/fs/read-record.mjs +131 -0
- package/foundation/fs/read-record.test.mjs +77 -0
- 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
|
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
* command, except in `runnerExamples`, where `npx @astryxdesign/cli …` and a
|
|
15
15
|
* project's runner may (still never the unscoped name through npx).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* repo test runs that check over Core's and the CLI's own docs with this
|
|
20
|
-
* allowlist.
|
|
17
|
+
* The matcher is runnerCommandsIn (foundation/discovery/runner-commands.mjs),
|
|
18
|
+
* the rule `doctor integration validate` holds integrations to.
|
|
21
19
|
*
|
|
22
20
|
* @input nothing
|
|
23
21
|
* @output the allowlist
|
|
@@ -13,10 +13,8 @@
|
|
|
13
13
|
* command, except in `runnerExamples`, where `npx @astryxdesign/cli …` and a
|
|
14
14
|
* project's runner may (still never the unscoped name through npx).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* repo test runs that check over Core's and the CLI's own docs with this
|
|
19
|
-
* allowlist.
|
|
16
|
+
* The matcher is runnerCommandsIn (foundation/discovery/runner-commands.mjs),
|
|
17
|
+
* the rule `doctor integration validate` holds integrations to.
|
|
20
18
|
*
|
|
21
19
|
* @input nothing
|
|
22
20
|
* @output the allowlist
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
* Each runner command in a text, as written, with the bare command to write
|
|
6
|
+
* instead, in order.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} text
|
|
9
|
+
* @returns {{written: string, bare: string, download: boolean}[]}
|
|
10
|
+
*/
|
|
11
|
+
export function runnerCommandsIn(text: string): {
|
|
12
|
+
written: string;
|
|
13
|
+
bare: string;
|
|
14
|
+
download: boolean;
|
|
15
|
+
}[];
|
|
16
|
+
/**
|
|
17
|
+
* An Astryx command a topic writes with a package runner in front.
|
|
18
|
+
*
|
|
19
|
+
* @typedef {object} RunnerCommand
|
|
20
|
+
* @property {string} topic
|
|
21
|
+
* @property {string | null} section the title of the section it is in
|
|
22
|
+
* @property {string} file the doc file
|
|
23
|
+
* @property {number | null} line the file line that holds it, when found
|
|
24
|
+
* @property {string} command the command as written
|
|
25
|
+
* @property {string} message the bare command to write instead
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Runner commands in a package's topics.
|
|
29
|
+
*
|
|
30
|
+
* @param {{records: {name: string, path: string}[], guides?: {name: string, ref?: {topicFile?: string}}[]}} discovered
|
|
31
|
+
* the package's docs, as discoverIntegrationDocs returns them
|
|
32
|
+
* @returns {Promise<RunnerCommand[]>}
|
|
33
|
+
*/
|
|
34
|
+
export function findRunnerCommands(discovered: {
|
|
35
|
+
records: {
|
|
36
|
+
name: string;
|
|
37
|
+
path: string;
|
|
38
|
+
}[];
|
|
39
|
+
guides?: {
|
|
40
|
+
name: string;
|
|
41
|
+
ref?: {
|
|
42
|
+
topicFile?: string;
|
|
43
|
+
};
|
|
44
|
+
}[];
|
|
45
|
+
}): Promise<RunnerCommand[]>;
|
|
46
|
+
/**
|
|
47
|
+
* The CLI's top-level commands, the words that make a project runner's
|
|
48
|
+
* `astryx <word>` a command; runner-commands.test.mjs keeps the list equal to
|
|
49
|
+
* the command docs.
|
|
50
|
+
*/
|
|
51
|
+
export const CLI_COMMANDS: readonly string[];
|
|
52
|
+
/**
|
|
53
|
+
* An Astryx command a topic writes with a package runner in front.
|
|
54
|
+
*/
|
|
55
|
+
export type RunnerCommand = {
|
|
56
|
+
topic: string;
|
|
57
|
+
/**
|
|
58
|
+
* the title of the section it is in
|
|
59
|
+
*/
|
|
60
|
+
section: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* the doc file
|
|
63
|
+
*/
|
|
64
|
+
file: string;
|
|
65
|
+
/**
|
|
66
|
+
* the file line that holds it, when found
|
|
67
|
+
*/
|
|
68
|
+
line: number | null;
|
|
69
|
+
/**
|
|
70
|
+
* the command as written
|
|
71
|
+
*/
|
|
72
|
+
command: string;
|
|
73
|
+
/**
|
|
74
|
+
* the bare command to write instead
|
|
75
|
+
*/
|
|
76
|
+
message: string;
|
|
77
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runner commands in docs: an Astryx command written with a package runner in
|
|
5
|
+
* front. Docs write the bare `astryx <command>` and the CLI shows it with the
|
|
6
|
+
* reader's runner, so a written runner is wrong for every reader on another
|
|
7
|
+
* package manager.
|
|
8
|
+
*
|
|
9
|
+
* - A download runner (`npx`, `pnpm dlx`, `yarn dlx`, `bunx`, `bun x`,
|
|
10
|
+
* `npm exec`) with the unscoped name `astryx`, with or without an
|
|
11
|
+
* `@<version>` after it, always counts, even with nothing after it: it
|
|
12
|
+
* fetches an unrelated package of that name.
|
|
13
|
+
* - A project runner (`pnpm exec`, `pnpm`, `yarn`) counts when one of the
|
|
14
|
+
* CLI's commands follows on the same line (`pnpm exec astryx docs`). Named
|
|
15
|
+
* alone, or before a word that is not a command (`yarn astryx runs the
|
|
16
|
+
* copy…`), it is left alone.
|
|
17
|
+
* - A runner and what follows it are read within one line; flags between a
|
|
18
|
+
* download runner and the name are read past.
|
|
19
|
+
* - The scoped package (`npx @astryxdesign/cli init`) never counts: it is the
|
|
20
|
+
* one form that runs before the CLI is installed, for install steps.
|
|
21
|
+
* - Other tools' commands (`npx prettier`) never count.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
|
|
26
|
+
import {loadTopicModule} from './docs-discovery.mjs';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} file
|
|
30
|
+
* @returns {Promise<any>} the doc, or null for one that does not load (the
|
|
31
|
+
* load problem is reported elsewhere)
|
|
32
|
+
*/
|
|
33
|
+
async function loadQuietly(file) {
|
|
34
|
+
try {
|
|
35
|
+
return await loadTopicModule(file);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DOWNLOAD_RUNNERS = [
|
|
42
|
+
'pnpm dlx',
|
|
43
|
+
'yarn dlx',
|
|
44
|
+
'npm exec --',
|
|
45
|
+
'npm exec',
|
|
46
|
+
'npx',
|
|
47
|
+
'bunx',
|
|
48
|
+
'bun x',
|
|
49
|
+
];
|
|
50
|
+
const PROJECT_RUNNERS = ['pnpm exec', 'pnpm', 'yarn'];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The CLI's top-level commands, the words that make a project runner's
|
|
54
|
+
* `astryx <word>` a command; runner-commands.test.mjs keeps the list equal to
|
|
55
|
+
* the command docs.
|
|
56
|
+
*/
|
|
57
|
+
export const CLI_COMMANDS = Object.freeze([
|
|
58
|
+
'blog',
|
|
59
|
+
'build',
|
|
60
|
+
'component',
|
|
61
|
+
'discover',
|
|
62
|
+
'docs',
|
|
63
|
+
'doctor',
|
|
64
|
+
'gap-report',
|
|
65
|
+
'help',
|
|
66
|
+
'hook',
|
|
67
|
+
'init',
|
|
68
|
+
'integration',
|
|
69
|
+
'manifest',
|
|
70
|
+
'postinstall',
|
|
71
|
+
'search',
|
|
72
|
+
'swizzle',
|
|
73
|
+
'template',
|
|
74
|
+
'theme',
|
|
75
|
+
'upgrade',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** Blanks within one line. */
|
|
79
|
+
const BLANK = '[ \\t]+';
|
|
80
|
+
|
|
81
|
+
/** @param {readonly string[]} words */
|
|
82
|
+
const alternatives = words =>
|
|
83
|
+
words.map(each => each.replace(/ /g, BLANK)).join('|');
|
|
84
|
+
|
|
85
|
+
/** After `astryx`: the name ends here (a sentence's period may follow). */
|
|
86
|
+
const NAME_END = '(?![\\w@/-]|\\.\\w)';
|
|
87
|
+
|
|
88
|
+
const DOWNLOAD_COMMAND = new RegExp(
|
|
89
|
+
`(?<![\\w@/.-])(?:${alternatives(DOWNLOAD_RUNNERS)})(?:${BLANK}--?[a-z][\\w-]*)*${BLANK}astryx(?:@[\\w^~-]+(?:\\.[\\w^~-]+)*)?${NAME_END}(?:${BLANK}(${alternatives(CLI_COMMANDS)})(?![\\w-]))?`,
|
|
90
|
+
'g',
|
|
91
|
+
);
|
|
92
|
+
const PROJECT_COMMAND = new RegExp(
|
|
93
|
+
`(?<![\\w@/.-])(?:${alternatives(PROJECT_RUNNERS)})${BLANK}astryx${BLANK}(${alternatives(CLI_COMMANDS)})(?![\\w-])`,
|
|
94
|
+
'g',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Each runner command in a text, as written, with the bare command to write
|
|
99
|
+
* instead, in order.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} text
|
|
102
|
+
* @returns {{written: string, bare: string, download: boolean}[]}
|
|
103
|
+
*/
|
|
104
|
+
export function runnerCommandsIn(text) {
|
|
105
|
+
const found = [
|
|
106
|
+
...[...String(text).matchAll(DOWNLOAD_COMMAND)].map(match => ({
|
|
107
|
+
match,
|
|
108
|
+
download: true,
|
|
109
|
+
})),
|
|
110
|
+
...[...String(text).matchAll(PROJECT_COMMAND)].map(match => ({
|
|
111
|
+
match,
|
|
112
|
+
download: false,
|
|
113
|
+
})),
|
|
114
|
+
];
|
|
115
|
+
/** @type {{at: number, written: string, bare: string, download: boolean}[]} */
|
|
116
|
+
const unique = [];
|
|
117
|
+
for (const {match, download} of found) {
|
|
118
|
+
if (unique.some(each => each.at === match.index)) continue;
|
|
119
|
+
unique.push({
|
|
120
|
+
at: match.index,
|
|
121
|
+
written: match[0],
|
|
122
|
+
bare: match[1] ? `astryx ${match[1]}` : 'astryx',
|
|
123
|
+
download,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return unique
|
|
127
|
+
.sort((a, b) => a.at - b.at)
|
|
128
|
+
.map(({written, bare, download}) => ({written, bare, download}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Every string in a doc, with the title of the section it is in. Code counts:
|
|
133
|
+
* most commands sit in code blocks.
|
|
134
|
+
*
|
|
135
|
+
* @param {unknown} doc
|
|
136
|
+
* @returns {{text: string, section: string | null}[]}
|
|
137
|
+
*/
|
|
138
|
+
function allStrings(doc) {
|
|
139
|
+
/** @type {{text: string, section: string | null}[]} */
|
|
140
|
+
const strings = [];
|
|
141
|
+
// A doc that refers back to itself is walked once.
|
|
142
|
+
const visited = new WeakSet();
|
|
143
|
+
/**
|
|
144
|
+
* @param {unknown} value
|
|
145
|
+
* @param {string | null} section
|
|
146
|
+
*/
|
|
147
|
+
const walk = (value, section) => {
|
|
148
|
+
if (typeof value === 'string') {
|
|
149
|
+
strings.push({text: value, section});
|
|
150
|
+
} else if (value != null && typeof value === 'object') {
|
|
151
|
+
if (visited.has(value)) return;
|
|
152
|
+
visited.add(value);
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
for (const item of value) walk(item, section);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
158
|
+
for (const [key, field] of Object.entries(record)) {
|
|
159
|
+
if (key === 'sections' && Array.isArray(field)) {
|
|
160
|
+
for (const each of field) {
|
|
161
|
+
walk(each, typeof each?.title === 'string' ? each.title : section);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
walk(field, section);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
walk(doc, null);
|
|
170
|
+
return strings;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Where a file next holds the text, at or after a cursor (a 0-based line and
|
|
175
|
+
* column), else the first place it holds it, else null. A doc's strings are
|
|
176
|
+
* read in file order, so moving the cursor past each match puts a command the
|
|
177
|
+
* topic writes twice on its own line.
|
|
178
|
+
*
|
|
179
|
+
* @param {string[]} lines
|
|
180
|
+
* @param {string} written
|
|
181
|
+
* @param {{line: number, column: number}} cursor
|
|
182
|
+
* @returns {{line: number, column: number} | null}
|
|
183
|
+
*/
|
|
184
|
+
function locate(lines, written, cursor) {
|
|
185
|
+
for (let at = cursor.line; at < lines.length; at++) {
|
|
186
|
+
const column = lines[at].indexOf(
|
|
187
|
+
written,
|
|
188
|
+
at === cursor.line ? cursor.column : 0,
|
|
189
|
+
);
|
|
190
|
+
if (column >= 0) return {line: at, column};
|
|
191
|
+
}
|
|
192
|
+
const first = lines.findIndex(line => line.includes(written));
|
|
193
|
+
return first < 0
|
|
194
|
+
? null
|
|
195
|
+
: {line: first, column: lines[first].indexOf(written)};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* An Astryx command a topic writes with a package runner in front.
|
|
200
|
+
*
|
|
201
|
+
* @typedef {object} RunnerCommand
|
|
202
|
+
* @property {string} topic
|
|
203
|
+
* @property {string | null} section the title of the section it is in
|
|
204
|
+
* @property {string} file the doc file
|
|
205
|
+
* @property {number | null} line the file line that holds it, when found
|
|
206
|
+
* @property {string} command the command as written
|
|
207
|
+
* @property {string} message the bare command to write instead
|
|
208
|
+
*/
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Runner commands in a package's topics.
|
|
212
|
+
*
|
|
213
|
+
* @param {{records: {name: string, path: string}[], guides?: {name: string, ref?: {topicFile?: string}}[]}} discovered
|
|
214
|
+
* the package's docs, as discoverIntegrationDocs returns them
|
|
215
|
+
* @returns {Promise<RunnerCommand[]>}
|
|
216
|
+
*/
|
|
217
|
+
export async function findRunnerCommands(discovered) {
|
|
218
|
+
const topics = [
|
|
219
|
+
...discovered.records.map(record => ({
|
|
220
|
+
name: record.name,
|
|
221
|
+
file: record.path,
|
|
222
|
+
})),
|
|
223
|
+
...(discovered.guides ?? []).map(guide => ({
|
|
224
|
+
name: guide.name,
|
|
225
|
+
file: guide.ref?.topicFile,
|
|
226
|
+
})),
|
|
227
|
+
];
|
|
228
|
+
/** @type {RunnerCommand[]} */
|
|
229
|
+
const found = [];
|
|
230
|
+
for (const {name, file} of topics) {
|
|
231
|
+
const doc = file ? await loadQuietly(file) : null;
|
|
232
|
+
if (!doc || !file) continue;
|
|
233
|
+
/** @type {string[]} */
|
|
234
|
+
let lines = [];
|
|
235
|
+
try {
|
|
236
|
+
lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
237
|
+
} catch {
|
|
238
|
+
// The doc loaded, so only the line number is lost.
|
|
239
|
+
}
|
|
240
|
+
let cursor = {line: 0, column: 0};
|
|
241
|
+
for (const {text, section} of allStrings(doc)) {
|
|
242
|
+
const commands = runnerCommandsIn(text);
|
|
243
|
+
if (commands.length === 0) continue;
|
|
244
|
+
// A one-line string's own literal places it, wherever the walk reached
|
|
245
|
+
// it (a section in a constant above the doc object, say); else the
|
|
246
|
+
// cursor search, in file order.
|
|
247
|
+
const forms = [`'${text}'`, `"${text}"`, JSON.stringify(text)];
|
|
248
|
+
const holds = (/** @type {string} */ line) =>
|
|
249
|
+
forms.some(form => line.includes(form));
|
|
250
|
+
// On the cursor's own line, only a copy at or after the cursor counts:
|
|
251
|
+
// the copy just matched still sits before it.
|
|
252
|
+
const after = text.includes('\n')
|
|
253
|
+
? -1
|
|
254
|
+
: lines.findIndex((line, at) =>
|
|
255
|
+
at > cursor.line
|
|
256
|
+
? holds(line)
|
|
257
|
+
: at === cursor.line &&
|
|
258
|
+
forms.some(form => line.indexOf(form, cursor.column) >= 0),
|
|
259
|
+
);
|
|
260
|
+
const literal =
|
|
261
|
+
after >= 0 || text.includes('\n') ? after : lines.findIndex(holds);
|
|
262
|
+
const column = after >= 0 && after === cursor.line ? cursor.column : 0;
|
|
263
|
+
let from = literal >= 0 ? {line: literal, column} : cursor;
|
|
264
|
+
for (const {written, bare, download} of commands) {
|
|
265
|
+
const place = locate(lines, written, from);
|
|
266
|
+
if (place) {
|
|
267
|
+
from = {line: place.line, column: place.column + written.length};
|
|
268
|
+
if (
|
|
269
|
+
place.line > cursor.line ||
|
|
270
|
+
(place.line === cursor.line && place.column >= cursor.column)
|
|
271
|
+
) {
|
|
272
|
+
cursor = from;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const line = place ? place.line + 1 : null;
|
|
276
|
+
const where = section ? ` (section "${section}")` : '';
|
|
277
|
+
found.push({
|
|
278
|
+
topic: name,
|
|
279
|
+
section,
|
|
280
|
+
file,
|
|
281
|
+
line,
|
|
282
|
+
command: written,
|
|
283
|
+
message:
|
|
284
|
+
`"${name}"${where} writes \`${written}\`${line == null ? '' : ` (line ${line})`}. ` +
|
|
285
|
+
`Write \`${bare}\`; the CLI shows it with your project's runner` +
|
|
286
|
+
(download
|
|
287
|
+
? ', and a download runner with the bare name fetches an unrelated package called astryx.'
|
|
288
|
+
: '.'),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return found;
|
|
294
|
+
}
|