@astryxdesign/cli 0.7.2 → 0.7.3-canary.02f5fe4
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 +127 -3
- package/api/build/blocks/blocks.d.mts +92 -0
- package/api/build/blocks/blocks.mjs +528 -0
- package/api/build/blocks/blocks.test.mjs +238 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/build.type.mjs +1 -1
- package/api/build/kit/answer.d.mts +18 -0
- package/api/build/kit/answer.mjs +195 -0
- package/api/build/kit/answer.test.mjs +108 -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 +79 -0
- package/api/build/kit/signals.mjs +295 -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 +53 -0
- package/api/build/tree.test.mjs +185 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/_adapter.mjs +39 -5
- package/api/component/component-replacement.test.mjs +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.d.mts +1 -2
- package/api/discover/_adapter.mjs +42 -17
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/discover/discover.mjs +0 -1
- package/api/docs/_adapter.d.mts +2 -2
- package/api/docs/_adapter.mjs +82 -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/_adapter.d.mts +33 -13
- package/api/doctor/_adapter.mjs +55 -25
- package/api/doctor/doctor.d.mts +23 -5
- package/api/doctor/doctor.mjs +183 -40
- package/api/doctor/doctor.test.mjs +95 -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-fixes.test.mjs +6 -5
- package/api/integration/validate-integration-gaps.test.mjs +1975 -0
- package/api/integration/validate-integration.d.mts +15 -2
- package/api/integration/validate-integration.mjs +298 -39
- package/api/integration/validate-integration.type.d.mts +90 -1
- package/api/integration/validate-integration.type.mjs +67 -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 +63 -0
- package/api/search/_entries.mjs +232 -0
- package/api/search/_entries.test.mjs +48 -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/template/template.mjs +5 -1
- 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 +3 -3
- package/assets/docs/tree/ci-and-containers.doc.mjs +13 -0
- 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 +5 -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-integration.test.mjs +49 -1
- package/clients/cli/commands/doctor.doc.mjs +1 -1
- package/clients/cli/commands/doctor.mjs +40 -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 +101 -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 +78 -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 +54 -94
- 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 +32 -7
- package/foundation/agent-docs/agent-doc-state.mjs +58 -9
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1017 -105
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +683 -39
- package/foundation/agent-docs/computer-agent-text.d.mts +39 -2
- package/foundation/agent-docs/computer-agent-text.mjs +135 -8
- package/foundation/agent-docs/computer-agent-text.test.mjs +99 -2
- 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/cli-peer-version.d.mts +36 -0
- package/foundation/integrations/cli-peer-version.mjs +132 -0
- package/foundation/integrations/cli-peer-version.test.mjs +354 -0
- 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/exports-subpaths.d.mts +57 -0
- package/foundation/integrations/exports-subpaths.mjs +113 -0
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/public-imports.d.mts +118 -0
- package/foundation/integrations/public-imports.mjs +1769 -0
- package/foundation/integrations/public-imports.test.mjs +1793 -0
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/cache.test.mjs +24 -21
- package/foundation/release/global-state.d.mts +8 -4
- package/foundation/release/global-state.mjs +4 -2
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/notices.d.mts +62 -8
- package/foundation/release/notices.mjs +136 -25
- package/foundation/release/notices.test.mjs +21 -0
- 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/foundation/text/string-utils.d.mts +2 -1
- package/foundation/text/string-utils.mjs +16 -5
- package/foundation/text/suggest-order.d.mts +72 -0
- package/foundation/text/suggest-order.mjs +363 -0
- package/foundation/text/suggest-order.test.mjs +259 -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
|
@@ -0,0 +1,1769 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Source-level checks that what an integration package tells apps to
|
|
5
|
+
* import is importable, read without a build.
|
|
6
|
+
*
|
|
7
|
+
* @input A package directory and its loaded integration record.
|
|
8
|
+
* @output Notes, listed on validate's result and never counted (they fail
|
|
9
|
+
* `--strict` from the next minor), for: a component's import (the doc's
|
|
10
|
+
* `import`, or the one the CLI derives) or a template's public import whose
|
|
11
|
+
* subpath package.json `exports` does not declare (or that names another
|
|
12
|
+
* package), or whose module does not export the component; a component
|
|
13
|
+
* example that imports an undeclared subpath or a name its package does not
|
|
14
|
+
* export; a template source with no default export, or one that imports a
|
|
15
|
+
* relative path outside the templates root, an undeclared subpath of its own
|
|
16
|
+
* package or of Core, or a name its package does not export. Imports it
|
|
17
|
+
* cannot judge are listed in a report.
|
|
18
|
+
* @position foundation/integrations, beside the other on-disk contribution
|
|
19
|
+
* validators; run by `astryx doctor integration validate` (and so by
|
|
20
|
+
* `integration check`) through api/integration/validate-integration.mjs.
|
|
21
|
+
*
|
|
22
|
+
* An `exports` target may point at build output that does not exist yet, so
|
|
23
|
+
* only the declared subpath is judged here, never the target file. A name is
|
|
24
|
+
* judged only when a declared target exists on disk and its export list can be
|
|
25
|
+
* read completely, and (for a name other than a doc's component) no
|
|
26
|
+
* TypeScript file in the package has a `declare module` block for one of its
|
|
27
|
+
* own modules (which can add names to it); anything less is left to
|
|
28
|
+
* `integration verify`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import {parse} from '@babel/parser';
|
|
34
|
+
import {findProjectCoreDir} from '../fs/paths.mjs';
|
|
35
|
+
import {normalizeProviderId} from '../identity/provider-identity.mjs';
|
|
36
|
+
import {
|
|
37
|
+
discoverIntegrationComponents,
|
|
38
|
+
resolveIntegrationImportPath,
|
|
39
|
+
} from '../discovery/component-discovery.mjs';
|
|
40
|
+
import {loadComponentDoc} from '../discovery/component-loader.mjs';
|
|
41
|
+
import {discoverIntegrationTemplatesForOne} from '../discovery/template-adapter.mjs';
|
|
42
|
+
import {
|
|
43
|
+
allTargets,
|
|
44
|
+
importable,
|
|
45
|
+
isRecord,
|
|
46
|
+
subpathDeclaration,
|
|
47
|
+
} from './exports-subpaths.mjs';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {import('./issue').AstryxIntegrationIssue} Issue
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const CORE = '@astryxdesign/core';
|
|
54
|
+
const MODULE_EXTENSIONS = [
|
|
55
|
+
'.tsx',
|
|
56
|
+
'.ts',
|
|
57
|
+
'.mts',
|
|
58
|
+
'.jsx',
|
|
59
|
+
'.mjs',
|
|
60
|
+
'.js',
|
|
61
|
+
'.cjs',
|
|
62
|
+
'.cts',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** @param {string} file */
|
|
66
|
+
function readJson(file) {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {string} file */
|
|
75
|
+
function isFile(file) {
|
|
76
|
+
try {
|
|
77
|
+
return fs.statSync(file).isFile();
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @param {string} from @param {string} to */
|
|
84
|
+
function rel(from, to) {
|
|
85
|
+
return path.relative(from, to).split(path.sep).join('/');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Import specifiers ───────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Split a bare specifier into its package name and `./`-prefixed subpath.
|
|
92
|
+
* @param {string} specifier
|
|
93
|
+
* @returns {{pkg: string, subpath: string} | null}
|
|
94
|
+
*/
|
|
95
|
+
export function splitSpecifier(specifier) {
|
|
96
|
+
const m = /^((?:@[^/\s]+\/)?[^/\s]+)(\/.*)?$/.exec(specifier);
|
|
97
|
+
if (!m || specifier.startsWith('.') || specifier.startsWith('/')) return null;
|
|
98
|
+
return {pkg: m[1], subpath: m[2] ? `.${m[2]}` : '.'};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {object} ImportBinding
|
|
103
|
+
* @property {string} imported the exported name (`default` for a default import)
|
|
104
|
+
* @property {boolean} typeOnly
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @typedef {object} ImportStatement
|
|
109
|
+
* @property {string} source
|
|
110
|
+
* @property {ImportBinding[]} names
|
|
111
|
+
* @property {boolean} namespace
|
|
112
|
+
* @property {boolean} [typeOnly] the whole statement is `import type`
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
const PARSE_PLUGINS = /** @type {any} */ ([
|
|
116
|
+
'jsx',
|
|
117
|
+
'typescript',
|
|
118
|
+
'decorators-legacy',
|
|
119
|
+
'importAttributes',
|
|
120
|
+
'exportDefaultFrom',
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
/** The plugins without `jsx`: `.ts` files allow `<T>value` assertions. */
|
|
124
|
+
const TS_PARSE_PLUGINS = PARSE_PLUGINS.filter(
|
|
125
|
+
(/** @type {unknown} */ plugin) => plugin !== 'jsx',
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} text
|
|
130
|
+
* @param {{jsx?: boolean}} [options] `jsx: false` parses TypeScript only
|
|
131
|
+
*/
|
|
132
|
+
function parseModuleText(text, {jsx = true} = {}) {
|
|
133
|
+
return parse(text, {
|
|
134
|
+
sourceType: 'module',
|
|
135
|
+
allowImportExportEverywhere: true,
|
|
136
|
+
allowReturnOutsideFunction: true,
|
|
137
|
+
allowAwaitOutsideFunction: true,
|
|
138
|
+
errorRecovery: true,
|
|
139
|
+
plugins: jsx ? PARSE_PLUGINS : TS_PARSE_PLUGINS,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const IMPORT_RE =
|
|
144
|
+
/(?:^|[;\n])\s*import\s+(type\s+)?([^'";]*?)\s*from\s*['"]([^'"]+)['"]/g;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read the import statements of a module or a snippet: named, default,
|
|
148
|
+
* aliased, namespace and type-only. A snippet that does not parse (an example
|
|
149
|
+
* with elisions, say) falls back to reading `import … from '…'` lines.
|
|
150
|
+
* @param {string} text
|
|
151
|
+
* @returns {ImportStatement[]}
|
|
152
|
+
*/
|
|
153
|
+
export function readImports(text) {
|
|
154
|
+
/** @type {ImportStatement[]} */
|
|
155
|
+
const out = [];
|
|
156
|
+
let ast;
|
|
157
|
+
try {
|
|
158
|
+
ast = parseModuleText(text);
|
|
159
|
+
} catch {
|
|
160
|
+
// Not a module: read the import lines below.
|
|
161
|
+
}
|
|
162
|
+
if (ast) {
|
|
163
|
+
for (const node of ast.program.body) {
|
|
164
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
165
|
+
const typeOnly = node.importKind === 'type';
|
|
166
|
+
/** @type {ImportStatement} */
|
|
167
|
+
const statement = {
|
|
168
|
+
source: String(node.source.value),
|
|
169
|
+
names: [],
|
|
170
|
+
namespace: false,
|
|
171
|
+
typeOnly,
|
|
172
|
+
};
|
|
173
|
+
for (const s of node.specifiers ?? []) {
|
|
174
|
+
if (s.type === 'ImportDefaultSpecifier') {
|
|
175
|
+
statement.names.push({imported: 'default', typeOnly});
|
|
176
|
+
} else if (s.type === 'ImportNamespaceSpecifier') {
|
|
177
|
+
statement.namespace = true;
|
|
178
|
+
} else if (s.type === 'ImportSpecifier') {
|
|
179
|
+
const imported =
|
|
180
|
+
s.imported.type === 'Identifier'
|
|
181
|
+
? s.imported.name
|
|
182
|
+
: String(s.imported.value);
|
|
183
|
+
statement.names.push({
|
|
184
|
+
imported,
|
|
185
|
+
typeOnly: typeOnly || s.importKind === 'type',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
out.push(statement);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
for (const m of text.matchAll(IMPORT_RE)) {
|
|
194
|
+
const typeOnly = Boolean(m[1]);
|
|
195
|
+
const clause = m[2].trim();
|
|
196
|
+
/** @type {ImportStatement} */
|
|
197
|
+
const statement = {
|
|
198
|
+
source: m[3],
|
|
199
|
+
names: [],
|
|
200
|
+
namespace: /\*\s*as\s/.test(clause),
|
|
201
|
+
typeOnly,
|
|
202
|
+
};
|
|
203
|
+
const braces = /\{([^}]*)\}/.exec(clause);
|
|
204
|
+
const head = clause
|
|
205
|
+
.replace(/\{[^}]*\}/, '')
|
|
206
|
+
.replace(/\*\s*as\s+[\w$]+/, '');
|
|
207
|
+
const defaultName = /^([\w$]+)/.exec(head.trim());
|
|
208
|
+
if (defaultName) statement.names.push({imported: 'default', typeOnly});
|
|
209
|
+
if (braces) {
|
|
210
|
+
for (const part of braces[1].split(',')) {
|
|
211
|
+
const item = part.trim();
|
|
212
|
+
if (!item) continue;
|
|
213
|
+
const t = /^type\s+/.test(item);
|
|
214
|
+
const name = item
|
|
215
|
+
.replace(/^type\s+/, '')
|
|
216
|
+
.split(/\s+as\s+/)[0]
|
|
217
|
+
.trim();
|
|
218
|
+
if (/^[\w$]+$/.test(name))
|
|
219
|
+
statement.names.push({imported: name, typeOnly: typeOnly || t});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
out.push(statement);
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── package.json exports ────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/** @typedef {import('./exports-subpaths.mjs').SubpathDeclaration} SubpathDeclaration */
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The files on disk a declared subpath can load, in condition order of the
|
|
233
|
+
* package.json (any condition); empty when none exists yet (build output).
|
|
234
|
+
* @param {string} packageDir
|
|
235
|
+
* @param {SubpathDeclaration} declaration
|
|
236
|
+
* @returns {string[]}
|
|
237
|
+
*/
|
|
238
|
+
function existingTargets(packageDir, declaration) {
|
|
239
|
+
if (declaration.state !== 'declared') return [];
|
|
240
|
+
const root = path.resolve(packageDir);
|
|
241
|
+
return allTargets(declaration.value)
|
|
242
|
+
.filter(target => target.startsWith('./'))
|
|
243
|
+
.map(target =>
|
|
244
|
+
path.resolve(root, target.replaceAll('*', declaration.wildcard ?? '')),
|
|
245
|
+
)
|
|
246
|
+
.filter(
|
|
247
|
+
file =>
|
|
248
|
+
(file === root || file.startsWith(root + path.sep)) && isFile(file),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* How a package declares one subpath. A package with no `exports` keeps its
|
|
254
|
+
* legacy deep paths: a subpath that names a file on disk (with or without a
|
|
255
|
+
* module extension, or a folder index) counts as declared.
|
|
256
|
+
* @param {unknown} exportsField
|
|
257
|
+
* @param {string} packageDir
|
|
258
|
+
* @param {string} subpath
|
|
259
|
+
* @returns {SubpathDeclaration}
|
|
260
|
+
*/
|
|
261
|
+
function declarationIn(exportsField, packageDir, subpath) {
|
|
262
|
+
const declaration = subpathDeclaration(exportsField, subpath);
|
|
263
|
+
if (declaration.state !== 'no_exports') return declaration;
|
|
264
|
+
if (subpath === '.') return {state: 'declared', value: null, wildcard: ''};
|
|
265
|
+
const root = path.resolve(packageDir);
|
|
266
|
+
const base = path.resolve(root, subpath.slice(2));
|
|
267
|
+
if (!base.startsWith(root + path.sep)) return declaration;
|
|
268
|
+
const file = [
|
|
269
|
+
base,
|
|
270
|
+
...MODULE_EXTENSIONS.map(ext => `${base}${ext}`),
|
|
271
|
+
...MODULE_EXTENSIONS.map(ext => path.join(base, `index${ext}`)),
|
|
272
|
+
].find(isFile);
|
|
273
|
+
return file
|
|
274
|
+
? {state: 'declared', value: `./${rel(root, file)}`, wildcard: ''}
|
|
275
|
+
: declaration;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A declared subpath of the package whose target is `file`, if any: an exact
|
|
280
|
+
* key, or a wildcard key whose target pattern matches the file.
|
|
281
|
+
* @param {unknown} exportsField
|
|
282
|
+
* @param {string} packageDir
|
|
283
|
+
* @param {string} file
|
|
284
|
+
* @returns {string | null}
|
|
285
|
+
*/
|
|
286
|
+
function subpathFor(exportsField, packageDir, file) {
|
|
287
|
+
if (!isRecord(exportsField)) return null;
|
|
288
|
+
const wanted = `./${rel(packageDir, file)}`;
|
|
289
|
+
for (const [key, value] of Object.entries(exportsField)) {
|
|
290
|
+
if (!key.startsWith('.') || !importable(value)) continue;
|
|
291
|
+
for (const target of allTargets(value)) {
|
|
292
|
+
if (!key.includes('*')) {
|
|
293
|
+
if (target === wanted) return key;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const star = target.indexOf('*');
|
|
297
|
+
if (star === -1) continue;
|
|
298
|
+
const prefix = target.slice(0, star);
|
|
299
|
+
const suffix = target.slice(star + 1);
|
|
300
|
+
if (
|
|
301
|
+
wanted.startsWith(prefix) &&
|
|
302
|
+
wanted.endsWith(suffix) &&
|
|
303
|
+
wanted.length >= prefix.length + suffix.length
|
|
304
|
+
) {
|
|
305
|
+
return key.replace(
|
|
306
|
+
'*',
|
|
307
|
+
wanted.slice(prefix.length, wanted.length - suffix.length),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The condition names an export value uses, at any depth, in order.
|
|
317
|
+
* @param {unknown} value
|
|
318
|
+
* @param {string[]} [out]
|
|
319
|
+
* @returns {string[]}
|
|
320
|
+
*/
|
|
321
|
+
function conditionNames(value, out = []) {
|
|
322
|
+
if (Array.isArray(value)) for (const v of value) conditionNames(v, out);
|
|
323
|
+
else if (isRecord(value)) {
|
|
324
|
+
for (const [key, v] of Object.entries(value)) {
|
|
325
|
+
if (!out.includes(key)) out.push(key);
|
|
326
|
+
conditionNames(v, out);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Why a declared state is not importable, in words.
|
|
334
|
+
* @param {SubpathDeclaration} declaration
|
|
335
|
+
* @param {string} subpath
|
|
336
|
+
*/
|
|
337
|
+
function whyUndeclared(declaration, subpath) {
|
|
338
|
+
const {state} = declaration;
|
|
339
|
+
switch (state) {
|
|
340
|
+
case 'no_exports':
|
|
341
|
+
return `package.json has no "exports" and no file exists at "${subpath}"`;
|
|
342
|
+
case 'root_only':
|
|
343
|
+
return `package.json "exports" names only the package root, which blocks every subpath`;
|
|
344
|
+
case 'blocked':
|
|
345
|
+
return `package.json "exports" maps "${subpath}" to null`;
|
|
346
|
+
case 'not_importable': {
|
|
347
|
+
const names = conditionNames(declaration.value);
|
|
348
|
+
const listed = names.map(name => `"${name}"`).join(', ');
|
|
349
|
+
return names.length === 0
|
|
350
|
+
? `package.json "exports" gives "${subpath}" no target an ES module import can load`
|
|
351
|
+
: `package.json "exports" gives "${subpath}" only the ${listed} condition${names.length === 1 ? '' : 's'}, none of which an ES module import loads`;
|
|
352
|
+
}
|
|
353
|
+
default:
|
|
354
|
+
return `package.json "exports" does not declare "${subpath}"`;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The paste-ready package.json change that declares `subpath` → `target`.
|
|
360
|
+
* @param {unknown} exportsField
|
|
361
|
+
* @param {SubpathDeclaration['state']} state
|
|
362
|
+
* @param {string} subpath
|
|
363
|
+
* @param {string} target `./`-prefixed package-relative file
|
|
364
|
+
*/
|
|
365
|
+
function exportsFix(exportsField, state, subpath, target) {
|
|
366
|
+
const entry = `${JSON.stringify(subpath)}: ${JSON.stringify(target)}`;
|
|
367
|
+
if (state === 'no_exports') {
|
|
368
|
+
return `point it at a file the package ships, or add "exports": {${entry}} to package.json (an "exports" map makes every path it does not list private, so list "." and the package's other public paths too).`;
|
|
369
|
+
}
|
|
370
|
+
if (state === 'root_only') {
|
|
371
|
+
return `make "exports" a subpath map: {".": ${JSON.stringify(exportsField)}, ${entry}}.`;
|
|
372
|
+
}
|
|
373
|
+
if (state === 'not_importable') {
|
|
374
|
+
return `add an ES module condition to "${subpath}" in package.json "exports", e.g. "default": ${JSON.stringify(target)} (or the built file).`;
|
|
375
|
+
}
|
|
376
|
+
if (state === 'blocked') {
|
|
377
|
+
return `replace the null for "${subpath}" in package.json "exports" with ${JSON.stringify(target)}, or stop advertising it.`;
|
|
378
|
+
}
|
|
379
|
+
return `add ${entry} to package.json "exports" (or the built file it compiles to).`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Module export names ─────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Export lists read during one public-import check, by file. Scoped to that
|
|
386
|
+
* check, so a later check in the same process rereads every module.
|
|
387
|
+
* @typedef {Map<string, {names: Set<string>, complete: boolean, types: Set<string>, typed: boolean}>} ExportNameCache
|
|
388
|
+
*/
|
|
389
|
+
|
|
390
|
+
const DECLARATION_RE = /\.d\.[cm]?ts$/u;
|
|
391
|
+
const DECLARATION_EXTENSIONS = ['.d.ts', '.d.mts', '.d.cts'];
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The declaration file beside a built module (`x.js` → `x.d.ts`), if any.
|
|
395
|
+
* @param {string} file
|
|
396
|
+
* @returns {string | null}
|
|
397
|
+
*/
|
|
398
|
+
function declarationTwin(file) {
|
|
399
|
+
const m = /\.([cm]?)jsx?$/u.exec(file);
|
|
400
|
+
if (!m) return null;
|
|
401
|
+
const stem = file.slice(0, m.index);
|
|
402
|
+
return (
|
|
403
|
+
[`.d.${m[1]}ts`, '.d.ts'].map(ext => `${stem}${ext}`).find(isFile) ?? null
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** @param {string} specifier @param {string} fromFile */
|
|
408
|
+
function resolveRelative(specifier, fromFile) {
|
|
409
|
+
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
410
|
+
const stripped = base.replace(/\.(?:m?js|jsx|cjs)$/u, '');
|
|
411
|
+
// TypeScript source resolves `./x.js` to `./x.ts` (or .tsx, .d.ts) first.
|
|
412
|
+
if (TYPED_RE.test(fromFile) && !DECLARATION_RE.test(fromFile)) {
|
|
413
|
+
const source = [
|
|
414
|
+
...['.ts', '.tsx', '.mts', '.cts'].map(ext => `${stripped}${ext}`),
|
|
415
|
+
...DECLARATION_EXTENSIONS.map(ext => `${stripped}${ext}`),
|
|
416
|
+
].find(isFile);
|
|
417
|
+
if (source && stripped !== base) return source;
|
|
418
|
+
}
|
|
419
|
+
// A declaration file's `export * from './x.js'` names `./x.d.ts`.
|
|
420
|
+
if (DECLARATION_RE.test(fromFile)) {
|
|
421
|
+
const twin = [
|
|
422
|
+
...DECLARATION_EXTENSIONS.map(ext => `${stripped}${ext}`),
|
|
423
|
+
...DECLARATION_EXTENSIONS.map(ext => path.join(base, `index${ext}`)),
|
|
424
|
+
].find(isFile);
|
|
425
|
+
if (twin) return twin;
|
|
426
|
+
}
|
|
427
|
+
const candidates = [
|
|
428
|
+
base,
|
|
429
|
+
...MODULE_EXTENSIONS.map(ext => `${base}${ext}`),
|
|
430
|
+
...MODULE_EXTENSIONS.map(ext => `${stripped}${ext}`),
|
|
431
|
+
...MODULE_EXTENSIONS.map(ext => path.join(base, `index${ext}`)),
|
|
432
|
+
];
|
|
433
|
+
return candidates.find(isFile) ?? null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Every name (runtime or type) a module exports, and whether the list is
|
|
438
|
+
* complete: an `export *` from another package, `export =`, or a module that
|
|
439
|
+
* does not parse leaves it incomplete, and then no name is judged missing.
|
|
440
|
+
* @param {string} file
|
|
441
|
+
* @param {ExportNameCache} [cache] export lists already read in this check
|
|
442
|
+
* @returns {{names: Set<string>, complete: boolean, types: Set<string>, typed: boolean}}
|
|
443
|
+
* `types`: the names exported only as types (an interface, a type alias,
|
|
444
|
+
* `export type`); `typed`: the list includes every type the module
|
|
445
|
+
* exports (TypeScript source, or a JavaScript module whose declaration
|
|
446
|
+
* file was read in full), so a name absent from it is absent as a type too
|
|
447
|
+
*/
|
|
448
|
+
export function moduleExportNames(file, cache = new Map()) {
|
|
449
|
+
const {names, complete, types, typed} = readExportNames(file, cache, []);
|
|
450
|
+
return {names, complete, types, typed};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** TypeScript source or a declaration file: its exports include its types. */
|
|
454
|
+
const TYPED_RE = /\.(?:[cm]?tsx?)$/u;
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* One module's export list, following local `export *` declarations. `stack`
|
|
458
|
+
* holds the modules whose lists are being read: an `export *` that reaches one
|
|
459
|
+
* of them closes a cycle, and a list read through a cycle the module does not
|
|
460
|
+
* close is not cached, since it lacks what the module on the stack exports.
|
|
461
|
+
* @param {string} file
|
|
462
|
+
* @param {ExportNameCache} cache
|
|
463
|
+
* @param {string[]} stack
|
|
464
|
+
* @returns {{names: Set<string>, complete: boolean, types: Set<string>, typed: boolean, cycleTo: Set<string>}}
|
|
465
|
+
*/
|
|
466
|
+
function readExportNames(file, cache, stack) {
|
|
467
|
+
if (stack.includes(file)) {
|
|
468
|
+
return {
|
|
469
|
+
names: new Set(),
|
|
470
|
+
complete: true,
|
|
471
|
+
types: new Set(),
|
|
472
|
+
typed: true,
|
|
473
|
+
cycleTo: new Set([file]),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const cached = cache.get(file);
|
|
477
|
+
if (cached) return {...cached, cycleTo: new Set()};
|
|
478
|
+
const own = readOwnExportNames(file, cache, stack);
|
|
479
|
+
const {names, types, cycleTo} = own;
|
|
480
|
+
let {complete, typed} = own;
|
|
481
|
+
// A JavaScript module's declaration file lists its types, at every level
|
|
482
|
+
// an `export *` reaches.
|
|
483
|
+
const twin = declarationTwin(file);
|
|
484
|
+
if (twin && !stack.includes(twin)) {
|
|
485
|
+
const declared = readExportNames(twin, cache, [...stack, file]);
|
|
486
|
+
for (const name of declared.names) names.add(name);
|
|
487
|
+
for (const name of declared.types) types.add(name);
|
|
488
|
+
for (const each of declared.cycleTo) cycleTo.add(each);
|
|
489
|
+
if (declared.complete && declared.typed) {
|
|
490
|
+
complete = true;
|
|
491
|
+
typed = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
cycleTo.delete(file);
|
|
495
|
+
const value = {names, complete, types, typed};
|
|
496
|
+
if (cycleTo.size === 0) cache.set(file, value);
|
|
497
|
+
return {...value, cycleTo};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The exports one module itself declares, following local `export *` (each
|
|
502
|
+
* through {@link readExportNames}); its declaration file is merged by the
|
|
503
|
+
* caller.
|
|
504
|
+
* @param {string} file
|
|
505
|
+
* @param {ExportNameCache} cache
|
|
506
|
+
* @param {string[]} stack
|
|
507
|
+
* @returns {{names: Set<string>, complete: boolean, types: Set<string>, typed: boolean, cycleTo: Set<string>}}
|
|
508
|
+
*/
|
|
509
|
+
function readOwnExportNames(file, cache, stack) {
|
|
510
|
+
/** @type {Set<string>} */
|
|
511
|
+
const names = new Set();
|
|
512
|
+
/** @type {Set<string>} */
|
|
513
|
+
const types = new Set();
|
|
514
|
+
/** @type {Set<string>} */
|
|
515
|
+
const cycleTo = new Set();
|
|
516
|
+
let typed = TYPED_RE.test(file);
|
|
517
|
+
/** @param {boolean} complete */
|
|
518
|
+
const done = complete => ({names, complete, types, typed, cycleTo});
|
|
519
|
+
// A CommonJS module's exports cannot be read from declarations: leave it
|
|
520
|
+
// unjudged rather than read it as exporting nothing. That is any .cjs/.cts
|
|
521
|
+
// file, any file (built `dist/*.js` included) with no ES module import or
|
|
522
|
+
// export, and any file that also assigns `module.exports` or `exports.*`.
|
|
523
|
+
if (/\.c[jt]s$/u.test(file)) return done(false);
|
|
524
|
+
let complete = true;
|
|
525
|
+
let ast;
|
|
526
|
+
let text;
|
|
527
|
+
try {
|
|
528
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
529
|
+
ast = parseModuleText(text);
|
|
530
|
+
if (ast.errors?.length) complete = false;
|
|
531
|
+
} catch {
|
|
532
|
+
return done(false);
|
|
533
|
+
}
|
|
534
|
+
const esm = ast.program.body.some(
|
|
535
|
+
(/** @type {any} */ node) =>
|
|
536
|
+
node.type === 'ImportDeclaration' ||
|
|
537
|
+
node.type === 'ExportNamedDeclaration' ||
|
|
538
|
+
node.type === 'ExportDefaultDeclaration' ||
|
|
539
|
+
node.type === 'ExportAllDeclaration' ||
|
|
540
|
+
node.type === 'TSExportAssignment',
|
|
541
|
+
);
|
|
542
|
+
// A declaration file with no import or export is a global script: no
|
|
543
|
+
// module import can read it (TypeScript: "is not a module"). Not judged.
|
|
544
|
+
if (!esm) return done(false);
|
|
545
|
+
if (/\b(?:module\.exports|exports\.[\w$]+)\s*=/u.test(text)) {
|
|
546
|
+
return done(false);
|
|
547
|
+
}
|
|
548
|
+
// A declaration file that is a module exports every top-level declaration,
|
|
549
|
+
// marked or not, unless it has an export list (`export {}`, `export {…}
|
|
550
|
+
// from`), `export *`, `export =`, or a default export of an expression
|
|
551
|
+
// (TypeScript's rule for ambient modules). A default-exported declaration
|
|
552
|
+
// (`export default function`, `class`, `interface`) does not end it.
|
|
553
|
+
if (DECLARATION_RE.test(file)) {
|
|
554
|
+
const implicit = !ast.program.body.some(
|
|
555
|
+
(/** @type {any} */ node) =>
|
|
556
|
+
(node.type === 'ExportNamedDeclaration' && !node.declaration) ||
|
|
557
|
+
(node.type === 'ExportDefaultDeclaration' &&
|
|
558
|
+
![
|
|
559
|
+
'FunctionDeclaration',
|
|
560
|
+
'TSDeclareFunction',
|
|
561
|
+
'ClassDeclaration',
|
|
562
|
+
'TSInterfaceDeclaration',
|
|
563
|
+
].includes(node.declaration?.type)) ||
|
|
564
|
+
node.type === 'TSExportAssignment' ||
|
|
565
|
+
node.type === 'ExportAllDeclaration',
|
|
566
|
+
);
|
|
567
|
+
if (implicit) {
|
|
568
|
+
for (const node of ast.program.body) {
|
|
569
|
+
const d = /** @type {any} */ (
|
|
570
|
+
node.type === 'ExportNamedDeclaration' ? node.declaration : node
|
|
571
|
+
);
|
|
572
|
+
if (!d) continue;
|
|
573
|
+
if (d.type === 'VariableDeclaration') {
|
|
574
|
+
for (const v of d.declarations ?? []) {
|
|
575
|
+
if (v.id?.type === 'Identifier') names.add(v.id.name);
|
|
576
|
+
else complete = false;
|
|
577
|
+
}
|
|
578
|
+
} else if (
|
|
579
|
+
d.type === 'TSInterfaceDeclaration' ||
|
|
580
|
+
d.type === 'TSTypeAliasDeclaration'
|
|
581
|
+
) {
|
|
582
|
+
names.add(d.id.name);
|
|
583
|
+
types.add(d.id.name);
|
|
584
|
+
} else if (
|
|
585
|
+
[
|
|
586
|
+
'FunctionDeclaration',
|
|
587
|
+
'TSDeclareFunction',
|
|
588
|
+
'ClassDeclaration',
|
|
589
|
+
'TSEnumDeclaration',
|
|
590
|
+
'TSModuleDeclaration',
|
|
591
|
+
].includes(d.type) &&
|
|
592
|
+
d.id?.type === 'Identifier'
|
|
593
|
+
) {
|
|
594
|
+
names.add(d.id.name);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
for (const node of ast.program.body) {
|
|
600
|
+
if (node.type === 'ExportDefaultDeclaration') names.add('default');
|
|
601
|
+
else if (node.type === 'TSExportAssignment') complete = false;
|
|
602
|
+
else if (
|
|
603
|
+
node.type === 'TSImportEqualsDeclaration' &&
|
|
604
|
+
/** @type {any} */ (node).isExport
|
|
605
|
+
) {
|
|
606
|
+
// `export import Alias = Inner.Value` exports Alias. Whether a
|
|
607
|
+
// type-only one names a value cannot be read, so the list is no longer
|
|
608
|
+
// complete.
|
|
609
|
+
names.add(/** @type {any} */ (node).id.name);
|
|
610
|
+
if (/** @type {any} */ (node).importKind === 'type') {
|
|
611
|
+
types.add(/** @type {any} */ (node).id.name);
|
|
612
|
+
complete = false;
|
|
613
|
+
}
|
|
614
|
+
} else if (node.type === 'ExportNamedDeclaration') {
|
|
615
|
+
const d = /** @type {any} */ (node.declaration);
|
|
616
|
+
const typeOnly = /** @type {any} */ (node).exportKind === 'type';
|
|
617
|
+
if (d) {
|
|
618
|
+
if (d.id?.name) {
|
|
619
|
+
names.add(d.id.name);
|
|
620
|
+
if (
|
|
621
|
+
typeOnly ||
|
|
622
|
+
d.type === 'TSInterfaceDeclaration' ||
|
|
623
|
+
d.type === 'TSTypeAliasDeclaration'
|
|
624
|
+
) {
|
|
625
|
+
types.add(d.id.name);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (const v of d.declarations ?? []) {
|
|
629
|
+
if (v.id?.type === 'Identifier') names.add(v.id.name);
|
|
630
|
+
else complete = false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
for (const sp of /** @type {any[]} */ (node.specifiers ?? [])) {
|
|
634
|
+
const exported = sp.exported?.name ?? sp.exported?.value;
|
|
635
|
+
if (!exported) continue;
|
|
636
|
+
names.add(exported);
|
|
637
|
+
if (typeOnly || sp.exportKind === 'type') types.add(exported);
|
|
638
|
+
}
|
|
639
|
+
} else if (node.type === 'ExportAllDeclaration') {
|
|
640
|
+
const n = /** @type {any} */ (node);
|
|
641
|
+
if (n.exported) {
|
|
642
|
+
names.add(n.exported.name ?? n.exported.value);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const source = String(n.source.value);
|
|
646
|
+
const target = source.startsWith('.')
|
|
647
|
+
? resolveRelative(source, file)
|
|
648
|
+
: null;
|
|
649
|
+
if (!target) {
|
|
650
|
+
complete = false;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const inner = readExportNames(target, cache, [...stack, file]);
|
|
654
|
+
for (const name of inner.names) if (name !== 'default') names.add(name);
|
|
655
|
+
for (const name of inner.types) types.add(name);
|
|
656
|
+
if (!inner.complete) complete = false;
|
|
657
|
+
if (!inner.typed) typed = false;
|
|
658
|
+
for (const each of inner.cycleTo) cycleTo.add(each);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return done(complete);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Every file a declared subpath names, and whether each is on disk.
|
|
666
|
+
* @param {string} packageDir
|
|
667
|
+
* @param {SubpathDeclaration} declaration
|
|
668
|
+
* @returns {{files: string[], allPresent: boolean}}
|
|
669
|
+
*/
|
|
670
|
+
function declaredTargets(packageDir, declaration) {
|
|
671
|
+
if (declaration.state !== 'declared') return {files: [], allPresent: false};
|
|
672
|
+
const root = path.resolve(packageDir);
|
|
673
|
+
const all = allTargets(declaration.value)
|
|
674
|
+
.filter(target => target.startsWith('./'))
|
|
675
|
+
.map(target =>
|
|
676
|
+
path.resolve(root, target.replaceAll('*', declaration.wildcard ?? '')),
|
|
677
|
+
)
|
|
678
|
+
.filter(file => file === root || file.startsWith(root + path.sep));
|
|
679
|
+
const files = all.filter(isFile);
|
|
680
|
+
return {files, allPresent: files.length === all.length};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* The names an import statement asks for that the module provably lacks.
|
|
685
|
+
* A name is missing only when every file the subpath names is on disk and
|
|
686
|
+
* read in full, and at least one of those lists includes its types: any
|
|
687
|
+
* construct the reader does not model leaves the import unchecked.
|
|
688
|
+
* @param {ImportStatement} statement
|
|
689
|
+
* @param {string[]} files candidate targets on disk
|
|
690
|
+
* @param {ExportNameCache} cache
|
|
691
|
+
* @param {boolean} [allPresent] every target the subpath names is on disk
|
|
692
|
+
* @param {boolean} [values] every name is known to be a value (a component),
|
|
693
|
+
* so a list without the module's types can still judge it
|
|
694
|
+
* @returns {string[] | null} null when it cannot be judged
|
|
695
|
+
*/
|
|
696
|
+
function missingNames(
|
|
697
|
+
statement,
|
|
698
|
+
files,
|
|
699
|
+
cache,
|
|
700
|
+
allPresent = true,
|
|
701
|
+
values = false,
|
|
702
|
+
) {
|
|
703
|
+
// A type-only name may live only in a declaration file: not judged.
|
|
704
|
+
const wanted = statement.names
|
|
705
|
+
.filter(binding => !binding.typeOnly)
|
|
706
|
+
.map(binding => binding.imported);
|
|
707
|
+
if (wanted.length === 0) return [];
|
|
708
|
+
if (files.length === 0) return null;
|
|
709
|
+
// Any condition's file (source or built) that has every name settles it.
|
|
710
|
+
let incomplete = false;
|
|
711
|
+
let typed = false;
|
|
712
|
+
/** @type {Set<string>} */
|
|
713
|
+
const seen = new Set();
|
|
714
|
+
for (const file of files) {
|
|
715
|
+
const list = moduleExportNames(file, cache);
|
|
716
|
+
if (wanted.every(name => list.names.has(name))) return [];
|
|
717
|
+
if (!list.complete) incomplete = true;
|
|
718
|
+
if (list.typed) typed = true;
|
|
719
|
+
for (const name of list.names) seen.add(name);
|
|
720
|
+
}
|
|
721
|
+
// A target not built yet, a list cut short, or one without the module's
|
|
722
|
+
// types (a name written without `type` may be a type) is not judged.
|
|
723
|
+
if (incomplete || (!typed && !values) || !allPresent) return null;
|
|
724
|
+
return wanted.filter(name => !seen.has(name));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const AUGMENTATION_HINT_RE = /\bdeclare\s+module\b/u;
|
|
728
|
+
const TYPESCRIPT_FILE_RE = /\.(?:d\.[cm]?ts|[cm]?ts|tsx)$/u;
|
|
729
|
+
const AUGMENTATION_WALK_LIMIT = 5000;
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Whether a TypeScript file has a real `declare module '<spec>'` block for a
|
|
733
|
+
* module of this package: a relative spec, or the package's own name or a
|
|
734
|
+
* subpath of it. Comments, `declare global`, wildcard specs (`'*.css'`) and
|
|
735
|
+
* other packages' names do not count. A `.tsx` file is parsed with JSX, any
|
|
736
|
+
* other TypeScript file without it (so `<T>value` assertions parse); a file
|
|
737
|
+
* with `declare module` in its text that still fails to parse counts as
|
|
738
|
+
* augmenting.
|
|
739
|
+
* @param {Context} ctx
|
|
740
|
+
* @param {string} text
|
|
741
|
+
* @param {string} file
|
|
742
|
+
*/
|
|
743
|
+
function augmentsOwnModule(ctx, text, file) {
|
|
744
|
+
if (!AUGMENTATION_HINT_RE.test(text)) return false;
|
|
745
|
+
let body;
|
|
746
|
+
try {
|
|
747
|
+
body = parseModuleText(text, {jsx: file.endsWith('.tsx')}).program.body;
|
|
748
|
+
} catch {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
return body.some(node => {
|
|
752
|
+
const d = node.type === 'ExportNamedDeclaration' ? node.declaration : node;
|
|
753
|
+
if (d?.type !== 'TSModuleDeclaration' || d.id?.type !== 'StringLiteral')
|
|
754
|
+
return false;
|
|
755
|
+
const spec = String(d.id.value);
|
|
756
|
+
if (spec.includes('*')) return false;
|
|
757
|
+
if (spec.startsWith('./') || spec.startsWith('../')) return true;
|
|
758
|
+
return (
|
|
759
|
+
ctx.name != null && (spec === ctx.name || spec.startsWith(`${ctx.name}/`))
|
|
760
|
+
);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* The first TypeScript file in the package (outside `node_modules` and dot
|
|
766
|
+
* folders) with a `declare module '…'` block for a module of the package,
|
|
767
|
+
* read once per check. Such a block can add names to that module, so while
|
|
768
|
+
* one exists no name is judged missing: what TypeScript merges is not
|
|
769
|
+
* modelled. A package too large to walk counts as augmented.
|
|
770
|
+
* @param {Context} ctx
|
|
771
|
+
* @returns {string | null} the reason names were not checked, or null
|
|
772
|
+
*/
|
|
773
|
+
function augmentationReason(ctx) {
|
|
774
|
+
if (ctx.augmentation !== undefined) return ctx.augmentation;
|
|
775
|
+
const root = path.resolve(ctx.packageDir);
|
|
776
|
+
/** @type {string[]} */
|
|
777
|
+
const queue = [root];
|
|
778
|
+
let seen = 0;
|
|
779
|
+
/** @type {string | null} */
|
|
780
|
+
let reason = null;
|
|
781
|
+
walk: while (queue.length > 0) {
|
|
782
|
+
const current = /** @type {string} */ (queue.shift());
|
|
783
|
+
let entries;
|
|
784
|
+
try {
|
|
785
|
+
entries = fs.readdirSync(current, {withFileTypes: true});
|
|
786
|
+
} catch {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
entries.sort((x, y) => x.name.localeCompare(y.name));
|
|
790
|
+
for (const entry of entries) {
|
|
791
|
+
if (++seen > AUGMENTATION_WALK_LIMIT) {
|
|
792
|
+
reason = `the package has more than ${AUGMENTATION_WALK_LIMIT} files to search for a \`declare module\` block, which can add names to its modules, so the names it imports were not checked`;
|
|
793
|
+
break walk;
|
|
794
|
+
}
|
|
795
|
+
const full = path.join(current, entry.name);
|
|
796
|
+
if (entry.isDirectory()) {
|
|
797
|
+
if (entry.name !== 'node_modules' && !entry.name.startsWith('.'))
|
|
798
|
+
queue.push(full);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (!entry.isFile() || !TYPESCRIPT_FILE_RE.test(entry.name)) continue;
|
|
802
|
+
let text;
|
|
803
|
+
try {
|
|
804
|
+
text = fs.readFileSync(full, 'utf-8');
|
|
805
|
+
} catch {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (augmentsOwnModule(ctx, text, entry.name)) {
|
|
809
|
+
reason = `"${rel(root, full)}" has a \`declare module\` block for a module of the package, which can add names to it, so the names it imports were not checked`;
|
|
810
|
+
break walk;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
ctx.augmentation = reason;
|
|
815
|
+
return reason;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Why an import's names could not be judged.
|
|
820
|
+
* @param {string[]} files
|
|
821
|
+
* @param {boolean} [allPresent]
|
|
822
|
+
*/
|
|
823
|
+
function uncheckedReason(files, allPresent = true) {
|
|
824
|
+
if (files.length === 0 || !allPresent) {
|
|
825
|
+
return 'a file its package.json "exports" entry names does not exist yet (build output), so the names it imports were not checked';
|
|
826
|
+
}
|
|
827
|
+
return "its module's export list cannot be read in full (CommonJS, an `export *` from another package, a parse error, or JavaScript with no declaration file for its types), so the names it imports were not checked";
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Whether `name` reads as a component: PascalCase, not ALL CAPS.
|
|
832
|
+
* @param {string} name
|
|
833
|
+
*/
|
|
834
|
+
function isComponentName(name) {
|
|
835
|
+
return /^[A-Z]/u.test(name) && /[a-z]/u.test(name);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* The export a template's default export should be: an exported function or
|
|
840
|
+
* class first, else an exported `const`, each only when its name is
|
|
841
|
+
* PascalCase (not ALL CAPS) and not only a type; null when there is none to
|
|
842
|
+
* suggest (a hook such as `useAccount`, a helper such as `formatBalance`, a
|
|
843
|
+
* constant such as `ACCOUNT_LIMIT`), and then the fix stays generic.
|
|
844
|
+
* @param {string} text the template source
|
|
845
|
+
* @param {{names: Set<string>, types: Set<string>}} exported
|
|
846
|
+
* @returns {string | null}
|
|
847
|
+
*/
|
|
848
|
+
function defaultExportCandidate(text, exported) {
|
|
849
|
+
let body;
|
|
850
|
+
try {
|
|
851
|
+
body = parseModuleText(text).program.body;
|
|
852
|
+
} catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
/** @type {Map<string, 'callable' | 'const'>} top-level bindings by kind */
|
|
856
|
+
const local = new Map();
|
|
857
|
+
/** @param {any} d */
|
|
858
|
+
const record = d => {
|
|
859
|
+
if (
|
|
860
|
+
(d?.type === 'FunctionDeclaration' || d?.type === 'ClassDeclaration') &&
|
|
861
|
+
d.id?.name
|
|
862
|
+
) {
|
|
863
|
+
local.set(d.id.name, 'callable');
|
|
864
|
+
} else if (d?.type === 'VariableDeclaration' && d.kind === 'const') {
|
|
865
|
+
for (const v of d.declarations ?? []) {
|
|
866
|
+
if (v.id?.type === 'Identifier') local.set(v.id.name, 'const');
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
for (const node of body) {
|
|
871
|
+
record(node.type === 'ExportNamedDeclaration' ? node.declaration : node);
|
|
872
|
+
}
|
|
873
|
+
/** @type {{name: string, kind: 'callable' | 'const'}[]} */
|
|
874
|
+
const candidates = [];
|
|
875
|
+
for (const node of body) {
|
|
876
|
+
if (node.type !== 'ExportNamedDeclaration') continue;
|
|
877
|
+
const n = /** @type {any} */ (node);
|
|
878
|
+
if (n.exportKind === 'type') continue;
|
|
879
|
+
const d = n.declaration;
|
|
880
|
+
/** @type {string[]} */
|
|
881
|
+
const names = d
|
|
882
|
+
? d.id?.name
|
|
883
|
+
? [d.id.name]
|
|
884
|
+
: (d.declarations ?? [])
|
|
885
|
+
.map((/** @type {any} */ v) => v.id?.name)
|
|
886
|
+
.filter(Boolean)
|
|
887
|
+
: n.source
|
|
888
|
+
? []
|
|
889
|
+
: (n.specifiers ?? [])
|
|
890
|
+
.filter((/** @type {any} */ sp) => sp.exportKind !== 'type')
|
|
891
|
+
.filter(
|
|
892
|
+
(/** @type {any} */ sp) =>
|
|
893
|
+
(sp.exported?.name ?? sp.exported?.value) === sp.local?.name,
|
|
894
|
+
)
|
|
895
|
+
.map((/** @type {any} */ sp) => sp.local.name);
|
|
896
|
+
for (const name of names) {
|
|
897
|
+
const kind = local.get(name);
|
|
898
|
+
if (kind) candidates.push({name, kind});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const fits = (/** @type {{name: string}} */ c) =>
|
|
902
|
+
isComponentName(c.name) && !exported.types.has(c.name);
|
|
903
|
+
return (
|
|
904
|
+
candidates.find(c => c.kind === 'callable' && fits(c))?.name ??
|
|
905
|
+
candidates.find(c => c.kind === 'const' && fits(c))?.name ??
|
|
906
|
+
null
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Whether `name` is a bare npm package name (an optional `@scope/` and a
|
|
912
|
+
* name), the only form an import can name the package by.
|
|
913
|
+
* @param {string} name
|
|
914
|
+
*/
|
|
915
|
+
function isPackageName(name) {
|
|
916
|
+
try {
|
|
917
|
+
normalizeProviderId(name);
|
|
918
|
+
return true;
|
|
919
|
+
} catch {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ── Listed findings ─────────────────────────────────────────────────
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* A public-import finding as a note on validate's result: listed with its
|
|
928
|
+
* fix inside the message, never counted or failed on. It becomes a failing
|
|
929
|
+
* warning in the next minor release (a new failure code joins only in a
|
|
930
|
+
* minor).
|
|
931
|
+
* @param {string} code
|
|
932
|
+
* @param {string} message
|
|
933
|
+
* @returns {PublicImportNote}
|
|
934
|
+
*/
|
|
935
|
+
function listed(code, message) {
|
|
936
|
+
return {code, message};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// ── Checks ──────────────────────────────────────────────────────────
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* @typedef {object} Context
|
|
943
|
+
* @property {string} packageDir
|
|
944
|
+
* @property {string | null} name the package name; null when package.json has
|
|
945
|
+
* none, and then no import of the package's own name is judged
|
|
946
|
+
* @property {unknown} exportsField
|
|
947
|
+
* @property {string | null} coreDir
|
|
948
|
+
* @property {any} corePkg
|
|
949
|
+
* @property {PublicImportUnchecked[]} unchecked what could not be judged
|
|
950
|
+
* @property {ExportNameCache} exportNames export lists read in this check
|
|
951
|
+
* @property {string | null} [augmentation] why no name is judged missing
|
|
952
|
+
* (a module augmentation in the package); null when none, unset until read
|
|
953
|
+
* @property {string[]} files the package.json `files` entries, normalized
|
|
954
|
+
* @property {string[]} roots the integration's contribution roots, absolute
|
|
955
|
+
*/
|
|
956
|
+
|
|
957
|
+
const BUILT_EXTENSION_RE = /\.[cm]?jsx?$/u;
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Whether an undeclared deep path, in a package with no `exports`, points
|
|
961
|
+
* into build output that is not built yet: its top folder is missing, or is
|
|
962
|
+
* a `files` folder that holds no contribution root. Such an import cannot be
|
|
963
|
+
* judged from source.
|
|
964
|
+
* @param {Context} ctx
|
|
965
|
+
* @param {string} subpath
|
|
966
|
+
* @param {SubpathDeclaration} declaration
|
|
967
|
+
*/
|
|
968
|
+
function unbuiltOutput(ctx, subpath, declaration) {
|
|
969
|
+
if (declaration.state !== 'no_exports' || subpath === '.') return false;
|
|
970
|
+
const top = subpath.slice(2).split('/')[0];
|
|
971
|
+
if (!top) return false;
|
|
972
|
+
const topDir = path.resolve(ctx.packageDir, top);
|
|
973
|
+
if (!fs.existsSync(topDir)) return true;
|
|
974
|
+
return (
|
|
975
|
+
ctx.files.includes(top) &&
|
|
976
|
+
!ctx.roots.some(
|
|
977
|
+
root => root === topDir || root.startsWith(topDir + path.sep),
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* The reason an import into unbuilt output is listed as not checked.
|
|
984
|
+
* @param {string} subpath
|
|
985
|
+
*/
|
|
986
|
+
function unbuiltReason(subpath) {
|
|
987
|
+
return `package.json has no "exports", and "${subpath}" is build output that is not built yet, so it was not checked`;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* The target a fix maps `subpath` to: the component's source for a source
|
|
992
|
+
* path, never a TypeScript source for a built `.js` path.
|
|
993
|
+
* @param {string} subpath
|
|
994
|
+
* @param {string | null} sourceTarget
|
|
995
|
+
*/
|
|
996
|
+
function fixTarget(subpath, sourceTarget) {
|
|
997
|
+
if (
|
|
998
|
+
sourceTarget &&
|
|
999
|
+
!(
|
|
1000
|
+
BUILT_EXTENSION_RE.test(subpath) && !BUILT_EXTENSION_RE.test(sourceTarget)
|
|
1001
|
+
)
|
|
1002
|
+
) {
|
|
1003
|
+
return sourceTarget;
|
|
1004
|
+
}
|
|
1005
|
+
return BUILT_EXTENSION_RE.test(subpath)
|
|
1006
|
+
? `<the built file "${subpath}" names>`
|
|
1007
|
+
: `./${subpath.slice(2)}.tsx`;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** @typedef {import('../../api/integration/validate-integration.type.mjs').PublicImportUnchecked} PublicImportUnchecked */
|
|
1011
|
+
/** @typedef {import('../../api/integration/validate-integration.type.mjs').PublicImportsReport} PublicImportsReport */
|
|
1012
|
+
/** @typedef {{code: string, message: string}} PublicImportNote */
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Whether nothing a statement imports exists at run time: a statement-level
|
|
1016
|
+
* `import type`, or named bindings that are each an inline `type`.
|
|
1017
|
+
* @param {ImportStatement} statement
|
|
1018
|
+
*/
|
|
1019
|
+
function typeOnlyStatement(statement) {
|
|
1020
|
+
if (statement.typeOnly) return true;
|
|
1021
|
+
return (
|
|
1022
|
+
!statement.namespace &&
|
|
1023
|
+
statement.names.length > 0 &&
|
|
1024
|
+
statement.names.every(binding => binding.typeOnly)
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* The files the `types` conditions of an export value name that an ES module
|
|
1030
|
+
* import can reach (never one under `require`), with a wildcard filled in.
|
|
1031
|
+
* @param {unknown} value
|
|
1032
|
+
* @param {string} wildcard
|
|
1033
|
+
* @param {boolean} [underTypes]
|
|
1034
|
+
* @param {string[]} [out]
|
|
1035
|
+
* @returns {string[]}
|
|
1036
|
+
*/
|
|
1037
|
+
function typesTargets(value, wildcard, underTypes = false, out = []) {
|
|
1038
|
+
if (typeof value === 'string') {
|
|
1039
|
+
if (underTypes) out.push(value.replace('*', wildcard));
|
|
1040
|
+
} else if (Array.isArray(value)) {
|
|
1041
|
+
for (const v of value) typesTargets(v, wildcard, underTypes, out);
|
|
1042
|
+
} else if (isRecord(value)) {
|
|
1043
|
+
for (const [condition, v] of Object.entries(value)) {
|
|
1044
|
+
if (condition === 'require') continue;
|
|
1045
|
+
typesTargets(v, wildcard, underTypes || condition === 'types', out);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return out;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* The names a declaration file exports as types: exported interfaces and
|
|
1053
|
+
* type aliases, `export type {…}`, and `export {…}` of a local interface or
|
|
1054
|
+
* type alias. Anything else (a value, an `export *`) is not counted, so an
|
|
1055
|
+
* import of it is listed as not checked. Null when the file cannot be read.
|
|
1056
|
+
* @param {string} file
|
|
1057
|
+
* @returns {Set<string> | null}
|
|
1058
|
+
*/
|
|
1059
|
+
function declaredTypeNames(file) {
|
|
1060
|
+
let ast;
|
|
1061
|
+
try {
|
|
1062
|
+
ast = parse(fs.readFileSync(file, 'utf-8'), {
|
|
1063
|
+
sourceType: 'module',
|
|
1064
|
+
errorRecovery: true,
|
|
1065
|
+
plugins: /** @type {any} */ ([
|
|
1066
|
+
['typescript', {dts: /\.d\.[cm]?ts$/u.test(file)}],
|
|
1067
|
+
]),
|
|
1068
|
+
});
|
|
1069
|
+
} catch {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
/** @type {Set<string>} */
|
|
1073
|
+
const local = new Set();
|
|
1074
|
+
/** @type {Set<string>} */
|
|
1075
|
+
const names = new Set();
|
|
1076
|
+
/** @param {any} node */
|
|
1077
|
+
const typeName = node =>
|
|
1078
|
+
node &&
|
|
1079
|
+
(node.type === 'TSInterfaceDeclaration' ||
|
|
1080
|
+
node.type === 'TSTypeAliasDeclaration')
|
|
1081
|
+
? node.id.name
|
|
1082
|
+
: null;
|
|
1083
|
+
for (const node of ast.program.body) {
|
|
1084
|
+
const own = typeName(node);
|
|
1085
|
+
if (own) local.add(own);
|
|
1086
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
1087
|
+
const declared = typeName(node.declaration);
|
|
1088
|
+
if (declared) local.add(declared);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
/** @param {any} n */
|
|
1092
|
+
const exportedName = n =>
|
|
1093
|
+
n.type === 'Identifier' ? n.name : String(n.value);
|
|
1094
|
+
for (const node of ast.program.body) {
|
|
1095
|
+
if (node.type !== 'ExportNamedDeclaration') continue;
|
|
1096
|
+
const declared = typeName(node.declaration);
|
|
1097
|
+
if (declared) names.add(declared);
|
|
1098
|
+
for (const s of node.specifiers ?? []) {
|
|
1099
|
+
if (s.type !== 'ExportSpecifier') continue;
|
|
1100
|
+
const exported = exportedName(s.exported);
|
|
1101
|
+
if (node.exportKind === 'type' || s.exportKind === 'type')
|
|
1102
|
+
names.add(exported);
|
|
1103
|
+
else if (!node.source && local.has(exportedName(s.local)))
|
|
1104
|
+
names.add(exported);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return names;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* An import of a subpath whose `exports` entry has only `types` targets is
|
|
1112
|
+
* never judged as a run-time import: accepted when it is `import type`, its
|
|
1113
|
+
* every binding is an inline `type`, or every name it imports is one the
|
|
1114
|
+
* `types` file declares as a type; otherwise the reason it was not checked.
|
|
1115
|
+
* @param {Context} ctx
|
|
1116
|
+
* @param {ImportStatement} statement
|
|
1117
|
+
* @param {string} subpath
|
|
1118
|
+
* @param {SubpathDeclaration} declaration
|
|
1119
|
+
* @returns {string | null}
|
|
1120
|
+
*/
|
|
1121
|
+
function typesOnlyImport(ctx, statement, subpath, declaration) {
|
|
1122
|
+
if (typeOnlyStatement(statement)) return null;
|
|
1123
|
+
const not = `"${subpath}" reaches an ES module import only through a "types" target in package.json "exports", which nothing loads at run time`;
|
|
1124
|
+
if (statement.namespace || statement.names.length === 0)
|
|
1125
|
+
return `${not}, and a namespace or side-effect import of it cannot be judged from source, so it was not checked`;
|
|
1126
|
+
const files = typesTargets(declaration.value, declaration.wildcard ?? '');
|
|
1127
|
+
/** @type {Set<string>} */
|
|
1128
|
+
const types = new Set();
|
|
1129
|
+
let read = false;
|
|
1130
|
+
for (const target of files) {
|
|
1131
|
+
const found = declaredTypeNames(path.resolve(ctx.packageDir, target));
|
|
1132
|
+
if (!found) continue;
|
|
1133
|
+
read = true;
|
|
1134
|
+
for (const n of found) types.add(n);
|
|
1135
|
+
}
|
|
1136
|
+
if (!read)
|
|
1137
|
+
return `${not}, and its types file ${files.map(f => `"${f}"`).join(', ')} could not be read (it may be build output), so it was not checked`;
|
|
1138
|
+
const unknown = statement.names
|
|
1139
|
+
.filter(binding => !binding.typeOnly && !types.has(binding.imported))
|
|
1140
|
+
.map(binding => binding.imported);
|
|
1141
|
+
if (unknown.length === 0) return null;
|
|
1142
|
+
return `${not}, and its types file does not declare ${unknown.join(', ')} as a type, so it was not checked`;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* How an import of the package's own name is judged: the declaration when
|
|
1147
|
+
* `exports` does not declare its subpath, the names its module provably
|
|
1148
|
+
* lacks, or the reason it was not checked.
|
|
1149
|
+
* @param {Context} ctx
|
|
1150
|
+
* @param {ImportStatement} statement
|
|
1151
|
+
* @param {string} subpath
|
|
1152
|
+
* @returns {{undeclared: SubpathDeclaration | null, missing: string[] | null, reason: string | null}}
|
|
1153
|
+
*/
|
|
1154
|
+
function judgeOwnImport(ctx, statement, subpath) {
|
|
1155
|
+
const declaration = declarationIn(ctx.exportsField, ctx.packageDir, subpath);
|
|
1156
|
+
// A subpath an ES module import reaches only through `types` (any other
|
|
1157
|
+
// condition beside it, such as `require`, loads nothing for it) is judged as
|
|
1158
|
+
// a type import, never as a run-time one.
|
|
1159
|
+
if (
|
|
1160
|
+
declaration.state === 'not_importable' &&
|
|
1161
|
+
typesTargets(declaration.value, declaration.wildcard ?? '').length > 0
|
|
1162
|
+
) {
|
|
1163
|
+
const reason = typesOnlyImport(ctx, statement, subpath, declaration);
|
|
1164
|
+
return {undeclared: null, missing: [], reason};
|
|
1165
|
+
}
|
|
1166
|
+
if (declaration.state !== 'declared')
|
|
1167
|
+
return {undeclared: declaration, missing: null, reason: null};
|
|
1168
|
+
if (subpath === '.' && declaration.value == null) {
|
|
1169
|
+
return {
|
|
1170
|
+
undeclared: null,
|
|
1171
|
+
missing: null,
|
|
1172
|
+
reason:
|
|
1173
|
+
'package.json has no "exports", and the root module "main" names is not read, so the names it imports were not checked',
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
const {files, allPresent} = declaredTargets(ctx.packageDir, declaration);
|
|
1177
|
+
const missing = missingNames(statement, files, ctx.exportNames, allPresent);
|
|
1178
|
+
const augmented = missing?.length ? augmentationReason(ctx) : null;
|
|
1179
|
+
if (augmented) return {undeclared: null, missing: null, reason: augmented};
|
|
1180
|
+
return {
|
|
1181
|
+
undeclared: null,
|
|
1182
|
+
missing,
|
|
1183
|
+
reason: missing == null ? uncheckedReason(files, allPresent) : null,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* A declared subpath of the package whose module, read in full, exports
|
|
1189
|
+
* `name`: the one the component's source is exported under first.
|
|
1190
|
+
* @param {Context} ctx
|
|
1191
|
+
* @param {string} name
|
|
1192
|
+
* @param {string | null} preferred
|
|
1193
|
+
* @returns {string | null}
|
|
1194
|
+
*/
|
|
1195
|
+
function subpathExporting(ctx, name, preferred) {
|
|
1196
|
+
if (!isRecord(ctx.exportsField)) return null;
|
|
1197
|
+
/** @param {string} subpath */
|
|
1198
|
+
const exportsName = subpath =>
|
|
1199
|
+
existingTargets(
|
|
1200
|
+
ctx.packageDir,
|
|
1201
|
+
declarationIn(ctx.exportsField, ctx.packageDir, subpath),
|
|
1202
|
+
).some(file => moduleExportNames(file, ctx.exportNames).names.has(name));
|
|
1203
|
+
// The source's own subpath, which may come from a wildcard key, first.
|
|
1204
|
+
if (preferred && exportsName(preferred)) return preferred;
|
|
1205
|
+
for (const key of Object.keys(ctx.exportsField)) {
|
|
1206
|
+
if (!key.startsWith('.') || key.includes('*') || key === preferred)
|
|
1207
|
+
continue;
|
|
1208
|
+
if (exportsName(key)) return key;
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* A component doc's import specifier, and every example's imports.
|
|
1215
|
+
* @param {Context} ctx
|
|
1216
|
+
* @param {any} integration
|
|
1217
|
+
* @returns {Promise<PublicImportNote[]>}
|
|
1218
|
+
*/
|
|
1219
|
+
async function componentIssues(ctx, integration) {
|
|
1220
|
+
/** @type {PublicImportNote[]} */
|
|
1221
|
+
const issues = [];
|
|
1222
|
+
// Every component check compares imports with the package's own name.
|
|
1223
|
+
if (ctx.name == null) return issues;
|
|
1224
|
+
let records;
|
|
1225
|
+
try {
|
|
1226
|
+
records = discoverIntegrationComponents(integration);
|
|
1227
|
+
} catch {
|
|
1228
|
+
return issues; // an unreadable root is reported by the root validators
|
|
1229
|
+
}
|
|
1230
|
+
/** @type {Map<string, typeof records>} */
|
|
1231
|
+
const byDoc = new Map();
|
|
1232
|
+
for (const record of records) {
|
|
1233
|
+
const list = byDoc.get(record.docPath) ?? [];
|
|
1234
|
+
list.push(record);
|
|
1235
|
+
byDoc.set(record.docPath, list);
|
|
1236
|
+
}
|
|
1237
|
+
for (const [docPath, list] of byDoc) {
|
|
1238
|
+
let doc;
|
|
1239
|
+
try {
|
|
1240
|
+
doc = await loadComponentDoc(docPath);
|
|
1241
|
+
} catch {
|
|
1242
|
+
continue; // reported by the doc validators
|
|
1243
|
+
}
|
|
1244
|
+
if (!isRecord(doc)) continue;
|
|
1245
|
+
const shownDoc = rel(ctx.packageDir, docPath);
|
|
1246
|
+
const record = list[0];
|
|
1247
|
+
const source = record.sourcePath;
|
|
1248
|
+
const sourceTarget = source ? `./${rel(ctx.packageDir, source)}` : null;
|
|
1249
|
+
// The import the doc writes, or the one the CLI derives for it (its
|
|
1250
|
+
// folder's subpath when exported, else the package root).
|
|
1251
|
+
const authored = typeof doc.import === 'string' ? doc.import : null;
|
|
1252
|
+
const specifier =
|
|
1253
|
+
authored ??
|
|
1254
|
+
resolveIntegrationImportPath(
|
|
1255
|
+
{
|
|
1256
|
+
exportsMap: /** @type {any} */ (ctx.exportsField) ?? null,
|
|
1257
|
+
packageDir: ctx.packageDir,
|
|
1258
|
+
docPath,
|
|
1259
|
+
packageName: ctx.name,
|
|
1260
|
+
},
|
|
1261
|
+
record.name,
|
|
1262
|
+
);
|
|
1263
|
+
const split = splitSpecifier(specifier);
|
|
1264
|
+
// The entry that exports this component alone, and the doc import that
|
|
1265
|
+
// uses it: never one component's file as the package root or folder.
|
|
1266
|
+
const entryKey = sourceTarget
|
|
1267
|
+
? sourceTarget.replace(/\.[cm]?[jt]sx?$/u, '')
|
|
1268
|
+
: null;
|
|
1269
|
+
const perComponent = entryKey
|
|
1270
|
+
? `add "${entryKey}": "${sourceTarget}" to package.json "exports" and set import: '${ctx.name}${entryKey.slice(1)}' in the doc`
|
|
1271
|
+
: null;
|
|
1272
|
+
const gives = authored
|
|
1273
|
+
? `gives import "${specifier}"`
|
|
1274
|
+
: `has no import, so apps are told to import from "${specifier}"`;
|
|
1275
|
+
const declared = source
|
|
1276
|
+
? subpathFor(ctx.exportsField, ctx.packageDir, source)
|
|
1277
|
+
: null;
|
|
1278
|
+
const suggested =
|
|
1279
|
+
declared ??
|
|
1280
|
+
(sourceTarget ? sourceTarget.replace(/\.[cm]?[jt]sx?$/u, '') : null);
|
|
1281
|
+
if (split && split.pkg !== ctx.name) {
|
|
1282
|
+
issues.push(
|
|
1283
|
+
listed(
|
|
1284
|
+
'component_import_other_package',
|
|
1285
|
+
`Component doc "${shownDoc}" ${gives}, which names package "${split.pkg}", not this package "${ctx.name}", so apps are told to import the component from somewhere it is not. Fix: set import: '${ctx.name}${suggested ? suggested.slice(1) : ''}' in the doc.`,
|
|
1286
|
+
),
|
|
1287
|
+
);
|
|
1288
|
+
} else if (split) {
|
|
1289
|
+
const declaration = declarationIn(
|
|
1290
|
+
ctx.exportsField,
|
|
1291
|
+
ctx.packageDir,
|
|
1292
|
+
split.subpath,
|
|
1293
|
+
);
|
|
1294
|
+
if (unbuiltOutput(ctx, split.subpath, declaration)) {
|
|
1295
|
+
ctx.unchecked.push({
|
|
1296
|
+
file: shownDoc,
|
|
1297
|
+
specifier,
|
|
1298
|
+
reason: unbuiltReason(split.subpath),
|
|
1299
|
+
});
|
|
1300
|
+
} else if (
|
|
1301
|
+
declaration.state === 'declared' &&
|
|
1302
|
+
split.subpath === '.' &&
|
|
1303
|
+
declaration.value == null
|
|
1304
|
+
) {
|
|
1305
|
+
// A package with no exports resolves its root through main: not read.
|
|
1306
|
+
ctx.unchecked.push({
|
|
1307
|
+
file: shownDoc,
|
|
1308
|
+
specifier,
|
|
1309
|
+
reason:
|
|
1310
|
+
'package.json has no "exports", and the root module "main" names is not read, so the component names were not checked',
|
|
1311
|
+
});
|
|
1312
|
+
} else if (declaration.state === 'declared') {
|
|
1313
|
+
// A doc whose `name` differs from the name it is listed under is
|
|
1314
|
+
// judged by the doc-name check (`doc_name_mismatch`), never here.
|
|
1315
|
+
const judged = list.filter(
|
|
1316
|
+
r => typeof doc.name !== 'string' || doc.name === r.name,
|
|
1317
|
+
);
|
|
1318
|
+
const {files, allPresent} = declaredTargets(
|
|
1319
|
+
ctx.packageDir,
|
|
1320
|
+
declaration,
|
|
1321
|
+
);
|
|
1322
|
+
const statement = {
|
|
1323
|
+
source: specifier,
|
|
1324
|
+
names: judged.map(r => ({imported: r.name, typeOnly: false})),
|
|
1325
|
+
namespace: false,
|
|
1326
|
+
};
|
|
1327
|
+
// A component is a run-time export, which no augmentation adds, so
|
|
1328
|
+
// a missing one is reported whatever the package declares.
|
|
1329
|
+
const missing =
|
|
1330
|
+
judged.length === 0
|
|
1331
|
+
? []
|
|
1332
|
+
: missingNames(statement, files, ctx.exportNames, allPresent, true);
|
|
1333
|
+
if (missing == null) {
|
|
1334
|
+
ctx.unchecked.push({
|
|
1335
|
+
file: shownDoc,
|
|
1336
|
+
specifier,
|
|
1337
|
+
reason: uncheckedReason(files, allPresent),
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
for (const name of missing ?? []) {
|
|
1341
|
+
const there = subpathExporting(ctx, name, declared);
|
|
1342
|
+
// The per-component entry helps only when the component's source
|
|
1343
|
+
// exports the name itself and the doc does not already use it.
|
|
1344
|
+
const ownEntry =
|
|
1345
|
+
perComponent != null &&
|
|
1346
|
+
source != null &&
|
|
1347
|
+
entryKey !== split.subpath &&
|
|
1348
|
+
moduleExportNames(source, ctx.exportNames).names.has(name);
|
|
1349
|
+
const fix = there
|
|
1350
|
+
? `set import: '${ctx.name}${there === '.' ? '' : there.slice(1)}' in the doc (that subpath exports it), or export "${name}" from the module "${specifier}" loads.`
|
|
1351
|
+
: `export "${name}" from the module "${specifier}" loads${ownEntry ? `, or ${perComponent}` : ''}.`;
|
|
1352
|
+
issues.push(
|
|
1353
|
+
listed(
|
|
1354
|
+
'component_import_missing_name',
|
|
1355
|
+
`Component doc "${shownDoc}" ${gives}, but that module does not export "${name}", so apps that follow the doc get nothing. Fix: ${fix}`,
|
|
1356
|
+
),
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
} else {
|
|
1360
|
+
const already =
|
|
1361
|
+
declared && declared !== split.subpath
|
|
1362
|
+
? `set import: '${ctx.name}${declared.slice(1)}' (already exported), or `
|
|
1363
|
+
: '';
|
|
1364
|
+
issues.push(
|
|
1365
|
+
listed(
|
|
1366
|
+
'component_import_not_exported',
|
|
1367
|
+
`Component doc "${shownDoc}" ${gives}, but ${whyUndeclared(declaration, split.subpath)}, so apps cannot import the component from it. Fix: ${already}${split.subpath === '.' && perComponent ? `${perComponent}.` : exportsFix(ctx.exportsField, declaration.state, split.subpath, fixTarget(split.subpath, sourceTarget))}`,
|
|
1368
|
+
),
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Whether the module at a declared subpath exports every value name the
|
|
1375
|
+
* statement imports (and it imports at least one).
|
|
1376
|
+
* @param {ImportStatement} statement
|
|
1377
|
+
* @param {string} candidate
|
|
1378
|
+
*/
|
|
1379
|
+
const providesAll = (statement, candidate) => {
|
|
1380
|
+
if (!statement.names.some(binding => !binding.typeOnly)) return false;
|
|
1381
|
+
const declaration = declarationIn(
|
|
1382
|
+
ctx.exportsField,
|
|
1383
|
+
ctx.packageDir,
|
|
1384
|
+
candidate,
|
|
1385
|
+
);
|
|
1386
|
+
if (declaration.state !== 'declared' || declaration.value == null)
|
|
1387
|
+
return false;
|
|
1388
|
+
const {files, allPresent} = declaredTargets(ctx.packageDir, declaration);
|
|
1389
|
+
const missing = missingNames(
|
|
1390
|
+
statement,
|
|
1391
|
+
files,
|
|
1392
|
+
ctx.exportNames,
|
|
1393
|
+
allPresent,
|
|
1394
|
+
);
|
|
1395
|
+
return Array.isArray(missing) && missing.length === 0;
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Where an example with an undeclared import should import from: the
|
|
1400
|
+
* doc's own import, when it differs from the example's and is declared;
|
|
1401
|
+
* else the subpath the component's source is already exported under;
|
|
1402
|
+
* else the exact `exports` entry to add for the example's subpath.
|
|
1403
|
+
* Another subpath is offered only when the statement imports at least one
|
|
1404
|
+
* value name and its module provably exports every one of them.
|
|
1405
|
+
* @param {ImportStatement} statement
|
|
1406
|
+
* @param {string} subpath
|
|
1407
|
+
* @param {SubpathDeclaration} undeclared
|
|
1408
|
+
*/
|
|
1409
|
+
const exampleFix = (statement, subpath, undeclared) => {
|
|
1410
|
+
/** @param {string} candidate */
|
|
1411
|
+
const provides = candidate => providesAll(statement, candidate);
|
|
1412
|
+
const values = statement.names
|
|
1413
|
+
.filter(binding => !binding.typeOnly)
|
|
1414
|
+
.map(binding => binding.imported);
|
|
1415
|
+
// A statement that imports no value name (a side-effect, namespace or
|
|
1416
|
+
// all-type import) is never sent to another subpath: no names prove it.
|
|
1417
|
+
const own = authored ? splitSpecifier(authored) : null;
|
|
1418
|
+
if (
|
|
1419
|
+
values.length > 0 &&
|
|
1420
|
+
authored &&
|
|
1421
|
+
authored !== statement.source &&
|
|
1422
|
+
own?.pkg === ctx.name &&
|
|
1423
|
+
provides(own.subpath)
|
|
1424
|
+
) {
|
|
1425
|
+
return `import from '${authored}' in the example, as the doc's import does.`;
|
|
1426
|
+
}
|
|
1427
|
+
if (
|
|
1428
|
+
values.length > 0 &&
|
|
1429
|
+
declared &&
|
|
1430
|
+
declared !== subpath &&
|
|
1431
|
+
provides(declared)
|
|
1432
|
+
) {
|
|
1433
|
+
return `import from '${ctx.name}${declared.slice(1)}' in the example (already exported).`;
|
|
1434
|
+
}
|
|
1435
|
+
// The entry for the example's own subpath: the file there, or the
|
|
1436
|
+
// component's source when it exports every name, else a placeholder.
|
|
1437
|
+
const atSubpath = [
|
|
1438
|
+
'',
|
|
1439
|
+
...MODULE_EXTENSIONS,
|
|
1440
|
+
...MODULE_EXTENSIONS.map(ext => `/index${ext}`),
|
|
1441
|
+
]
|
|
1442
|
+
.map(ext => path.resolve(ctx.packageDir, subpath.slice(2) + ext))
|
|
1443
|
+
.find(
|
|
1444
|
+
file =>
|
|
1445
|
+
file.startsWith(path.resolve(ctx.packageDir) + path.sep) &&
|
|
1446
|
+
isFile(file),
|
|
1447
|
+
);
|
|
1448
|
+
const sourceHasAll =
|
|
1449
|
+
values.length > 0 &&
|
|
1450
|
+
source != null &&
|
|
1451
|
+
values.every(name =>
|
|
1452
|
+
moduleExportNames(source, ctx.exportNames).names.has(name),
|
|
1453
|
+
);
|
|
1454
|
+
const target = atSubpath
|
|
1455
|
+
? `./${rel(ctx.packageDir, atSubpath)}`
|
|
1456
|
+
: sourceHasAll && sourceTarget
|
|
1457
|
+
? fixTarget(subpath, sourceTarget)
|
|
1458
|
+
: values.length > 0
|
|
1459
|
+
? `<the file that exports ${values.join(', ')}>`
|
|
1460
|
+
: '<the file it loads>';
|
|
1461
|
+
return exportsFix(ctx.exportsField, undeclared.state, subpath, target);
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* The fix for an example that imports names its module lacks: import
|
|
1466
|
+
* this doc's components that module does export, else the doc's own
|
|
1467
|
+
* import when it differs, else export the names from it.
|
|
1468
|
+
* @param {ImportStatement} statement
|
|
1469
|
+
* @param {string} subpath
|
|
1470
|
+
* @param {string[]} missing
|
|
1471
|
+
*/
|
|
1472
|
+
const missingNameFix = (statement, subpath, missing) => {
|
|
1473
|
+
const declaration = declarationIn(
|
|
1474
|
+
ctx.exportsField,
|
|
1475
|
+
ctx.packageDir,
|
|
1476
|
+
subpath,
|
|
1477
|
+
);
|
|
1478
|
+
/** @type {Set<string>} */
|
|
1479
|
+
const there = new Set();
|
|
1480
|
+
for (const file of existingTargets(ctx.packageDir, declaration)) {
|
|
1481
|
+
for (const name of moduleExportNames(file, ctx.exportNames).names)
|
|
1482
|
+
there.add(name);
|
|
1483
|
+
}
|
|
1484
|
+
const docNames = list.map(r => r.name).filter(name => there.has(name));
|
|
1485
|
+
const exportIt = `export ${missing.length === 1 ? 'the name' : 'the names'} from it`;
|
|
1486
|
+
if (docNames.length > 0) {
|
|
1487
|
+
return `import a name that module exports (${docNames.map(n => `"${n}"`).join(', ')} for this doc), or ${exportIt}.`;
|
|
1488
|
+
}
|
|
1489
|
+
const own = authored ? splitSpecifier(authored) : null;
|
|
1490
|
+
if (
|
|
1491
|
+
authored &&
|
|
1492
|
+
authored !== statement.source &&
|
|
1493
|
+
own?.pkg === ctx.name &&
|
|
1494
|
+
providesAll(statement, own.subpath)
|
|
1495
|
+
) {
|
|
1496
|
+
return `import from '${authored}', as the doc's import does, or ${exportIt}.`;
|
|
1497
|
+
}
|
|
1498
|
+
return `${exportIt[0].toUpperCase()}${exportIt.slice(1)}.`;
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
const examples = Array.isArray(doc.examples) ? doc.examples : [];
|
|
1502
|
+
for (const [index, example] of examples.entries()) {
|
|
1503
|
+
if (!isRecord(example) || typeof example.code !== 'string') continue;
|
|
1504
|
+
const label =
|
|
1505
|
+
typeof example.label === 'string'
|
|
1506
|
+
? `"${example.label}"`
|
|
1507
|
+
: `#${index + 1}`;
|
|
1508
|
+
for (const statement of readImports(example.code)) {
|
|
1509
|
+
const parts = splitSpecifier(statement.source);
|
|
1510
|
+
if (!parts || parts.pkg !== ctx.name) continue;
|
|
1511
|
+
const {undeclared, missing, reason} = judgeOwnImport(
|
|
1512
|
+
ctx,
|
|
1513
|
+
statement,
|
|
1514
|
+
parts.subpath,
|
|
1515
|
+
);
|
|
1516
|
+
if (reason) {
|
|
1517
|
+
ctx.unchecked.push({
|
|
1518
|
+
file: shownDoc,
|
|
1519
|
+
specifier: statement.source,
|
|
1520
|
+
reason: `example ${label}: ${reason}`,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
if (undeclared && unbuiltOutput(ctx, parts.subpath, undeclared)) {
|
|
1524
|
+
ctx.unchecked.push({
|
|
1525
|
+
file: shownDoc,
|
|
1526
|
+
specifier: statement.source,
|
|
1527
|
+
reason: `example ${label}: ${unbuiltReason(parts.subpath)}`,
|
|
1528
|
+
});
|
|
1529
|
+
} else if (undeclared) {
|
|
1530
|
+
issues.push(
|
|
1531
|
+
listed(
|
|
1532
|
+
'example_import_not_exported',
|
|
1533
|
+
`Component doc "${shownDoc}" example ${label} imports "${statement.source}", but ${whyUndeclared(undeclared, parts.subpath)}, so the example fails when an app copies it. Fix: ${exampleFix(statement, parts.subpath, undeclared)}`,
|
|
1534
|
+
),
|
|
1535
|
+
);
|
|
1536
|
+
} else if (missing && missing.length > 0) {
|
|
1537
|
+
issues.push(
|
|
1538
|
+
listed(
|
|
1539
|
+
'example_import_missing_name',
|
|
1540
|
+
`Component doc "${shownDoc}" example ${label} imports ${missing.map(n => `"${n}"`).join(', ')} from "${statement.source}", which does not export ${missing.length === 1 ? 'it' : 'them'}, so the example fails when an app copies it. Fix: ${missingNameFix(statement, parts.subpath, missing)}`,
|
|
1541
|
+
),
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return issues;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Template public imports and what each template source imports.
|
|
1552
|
+
* @param {Context} ctx
|
|
1553
|
+
* @param {any} integration
|
|
1554
|
+
* @returns {Promise<PublicImportNote[]>}
|
|
1555
|
+
*/
|
|
1556
|
+
async function templateIssues(ctx, integration) {
|
|
1557
|
+
/** @type {PublicImportNote[]} */
|
|
1558
|
+
const issues = [];
|
|
1559
|
+
const root = integration.templates;
|
|
1560
|
+
if (!root) return issues;
|
|
1561
|
+
let templates;
|
|
1562
|
+
try {
|
|
1563
|
+
({templates} = await discoverIntegrationTemplatesForOne(integration));
|
|
1564
|
+
} catch {
|
|
1565
|
+
return issues;
|
|
1566
|
+
}
|
|
1567
|
+
for (const template of templates) {
|
|
1568
|
+
const id = template.dirName;
|
|
1569
|
+
const file = template.filePath;
|
|
1570
|
+
const shown = rel(ctx.packageDir, file);
|
|
1571
|
+
const sourceTarget = `./${shown}`;
|
|
1572
|
+
const subpath = `./${shown.replace(/\.tsx?$/u, '')}`;
|
|
1573
|
+
const declaration = declarationIn(
|
|
1574
|
+
ctx.exportsField,
|
|
1575
|
+
ctx.packageDir,
|
|
1576
|
+
subpath,
|
|
1577
|
+
);
|
|
1578
|
+
if (ctx.name != null && declaration.state !== 'declared') {
|
|
1579
|
+
issues.push(
|
|
1580
|
+
listed(
|
|
1581
|
+
'template_import_not_exported',
|
|
1582
|
+
`Template "${id}" has public import "${ctx.name}${subpath.slice(1)}", but ${whyUndeclared(declaration, subpath)}, so it cannot be imported once published. Fix: ${exportsFix(ctx.exportsField, declaration.state, subpath, sourceTarget)}`,
|
|
1583
|
+
),
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
let text;
|
|
1588
|
+
try {
|
|
1589
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
1590
|
+
} catch {
|
|
1591
|
+
text = '';
|
|
1592
|
+
}
|
|
1593
|
+
// `astryx template` copies the source as written, but the package's
|
|
1594
|
+
// public import, the gallery preview and a page that renders the
|
|
1595
|
+
// template import its default export.
|
|
1596
|
+
const exported = text ? moduleExportNames(file, ctx.exportNames) : null;
|
|
1597
|
+
if (exported?.complete && !exported.names.has('default')) {
|
|
1598
|
+
const named = defaultExportCandidate(text, exported);
|
|
1599
|
+
const fix = named
|
|
1600
|
+
? `add \`export default ${named};\` to ${shown}, or make ${named} the default export`
|
|
1601
|
+
: `export the template's component as the default export of ${shown} (\`export default function …\`)`;
|
|
1602
|
+
// With no package name there is no public import to name.
|
|
1603
|
+
const what =
|
|
1604
|
+
ctx.name == null
|
|
1605
|
+
? 'anything that renders the template gets nothing'
|
|
1606
|
+
: `its public import "${ctx.name}${subpath.slice(1)}" and anything that renders the template get nothing`;
|
|
1607
|
+
issues.push(
|
|
1608
|
+
listed(
|
|
1609
|
+
'template_no_default_export',
|
|
1610
|
+
`Template "${id}" (${shown}) has no default export, so ${what}. Fix: ${fix}.`,
|
|
1611
|
+
),
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
for (const statement of readImports(text)) {
|
|
1615
|
+
const spec = statement.source;
|
|
1616
|
+
if (spec.startsWith('.')) {
|
|
1617
|
+
const target = path.resolve(path.dirname(file), spec);
|
|
1618
|
+
if (target !== root && !target.startsWith(root + path.sep)) {
|
|
1619
|
+
issues.push(
|
|
1620
|
+
listed(
|
|
1621
|
+
'template_import_outside_root',
|
|
1622
|
+
`Template "${id}" (${shown}) imports "${spec}", a path outside the templates root; \`astryx template\` copies only the template into an app, where that path does not exist. Fix: import it through the package's public name (e.g. '${ctx.name ?? '<package name>'}/…' declared in package.json "exports"), or move the code into the template.`,
|
|
1623
|
+
),
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
const parts = splitSpecifier(spec);
|
|
1629
|
+
if (!parts) continue;
|
|
1630
|
+
if (ctx.name != null && parts.pkg === ctx.name) {
|
|
1631
|
+
const {undeclared, missing, reason} = judgeOwnImport(
|
|
1632
|
+
ctx,
|
|
1633
|
+
statement,
|
|
1634
|
+
parts.subpath,
|
|
1635
|
+
);
|
|
1636
|
+
if (reason) {
|
|
1637
|
+
ctx.unchecked.push({file: shown, specifier: spec, reason});
|
|
1638
|
+
}
|
|
1639
|
+
if (undeclared && unbuiltOutput(ctx, parts.subpath, undeclared)) {
|
|
1640
|
+
ctx.unchecked.push({
|
|
1641
|
+
file: shown,
|
|
1642
|
+
specifier: spec,
|
|
1643
|
+
reason: unbuiltReason(parts.subpath),
|
|
1644
|
+
});
|
|
1645
|
+
} else if (undeclared) {
|
|
1646
|
+
issues.push(
|
|
1647
|
+
listed(
|
|
1648
|
+
'template_private_import',
|
|
1649
|
+
`Template "${id}" (${shown}) imports "${spec}", but ${whyUndeclared(undeclared, parts.subpath)}, so the template breaks in an app. Fix: import from a subpath package.json "exports" declares, or add one for it.`,
|
|
1650
|
+
),
|
|
1651
|
+
);
|
|
1652
|
+
} else if (missing && missing.length > 0) {
|
|
1653
|
+
issues.push(
|
|
1654
|
+
listed(
|
|
1655
|
+
'template_import_missing_name',
|
|
1656
|
+
`Template "${id}" (${shown}) imports ${missing.map(n => `"${n}"`).join(', ')} from "${spec}", which does not export ${missing.length === 1 ? 'it' : 'them'}, so the template breaks in an app. Fix: import a name that module exports, or export the name from it.`,
|
|
1657
|
+
),
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
} else if (parts.pkg === CORE && !ctx.corePkg) {
|
|
1661
|
+
ctx.unchecked.push({
|
|
1662
|
+
file: shown,
|
|
1663
|
+
specifier: spec,
|
|
1664
|
+
reason:
|
|
1665
|
+
'@astryxdesign/core is not installed, so Core\'s package.json "exports" could not be read',
|
|
1666
|
+
});
|
|
1667
|
+
} else if (parts.pkg === CORE && ctx.corePkg && parts.subpath !== '.') {
|
|
1668
|
+
const core = declarationIn(
|
|
1669
|
+
ctx.corePkg.exports,
|
|
1670
|
+
/** @type {string} */ (ctx.coreDir),
|
|
1671
|
+
parts.subpath,
|
|
1672
|
+
);
|
|
1673
|
+
if (core.state !== 'declared') {
|
|
1674
|
+
const top = parts.subpath
|
|
1675
|
+
.split('/')
|
|
1676
|
+
.filter(s => s && s !== '.' && s !== 'src')[0];
|
|
1677
|
+
const hint =
|
|
1678
|
+
top &&
|
|
1679
|
+
subpathDeclaration(ctx.corePkg.exports, `./${top}`).state ===
|
|
1680
|
+
'declared'
|
|
1681
|
+
? `'${CORE}/${top}'`
|
|
1682
|
+
: `a subpath Core's package.json "exports" declares`;
|
|
1683
|
+
issues.push(
|
|
1684
|
+
listed(
|
|
1685
|
+
'template_private_import',
|
|
1686
|
+
`Template "${id}" (${shown}) imports "${spec}", a path Core's package.json "exports" does not declare, so the template breaks in an app. Fix: import from ${hint}.`,
|
|
1687
|
+
),
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return issues;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Every public-import finding for one package, as notes, and the report of
|
|
1698
|
+
* what could not be judged. It returns no issues: the notes are listed
|
|
1699
|
+
* only, and fail `--strict` from the next minor release.
|
|
1700
|
+
* @param {string} packageDir
|
|
1701
|
+
* @param {any} integration the loaded integration record
|
|
1702
|
+
* @returns {Promise<{issues: Issue[], notes: PublicImportNote[], report: PublicImportsReport}>}
|
|
1703
|
+
* `issues` is always empty: kept so validate's call site stays one shape
|
|
1704
|
+
*/
|
|
1705
|
+
export async function publicImportIssues(packageDir, integration) {
|
|
1706
|
+
/** @type {PublicImportsReport} */
|
|
1707
|
+
const report = {status: 'checked', findings: [], unchecked: []};
|
|
1708
|
+
const pkg = readJson(path.join(packageDir, 'package.json'));
|
|
1709
|
+
// What cannot run is listed as not checked, never a skip `--strict` would
|
|
1710
|
+
// fail on.
|
|
1711
|
+
if (!isRecord(pkg)) {
|
|
1712
|
+
report.unchecked.push({
|
|
1713
|
+
file: 'package.json',
|
|
1714
|
+
specifier: '',
|
|
1715
|
+
reason: 'package.json could not be read, so no import was checked',
|
|
1716
|
+
});
|
|
1717
|
+
return {issues: [], notes: [], report};
|
|
1718
|
+
}
|
|
1719
|
+
const raw = typeof pkg.name === 'string' && pkg.name !== '' ? pkg.name : null;
|
|
1720
|
+
// A name that is not a bare package name (`acme/kit`) cannot be told apart
|
|
1721
|
+
// from an import of another package: checked as if there were none.
|
|
1722
|
+
const name = raw != null && isPackageName(raw) ? raw : null;
|
|
1723
|
+
if (name == null) {
|
|
1724
|
+
report.unchecked.push({
|
|
1725
|
+
file: 'package.json',
|
|
1726
|
+
specifier: '',
|
|
1727
|
+
reason: `${raw == null ? 'package.json has no "name"' : `package.json "name" ${JSON.stringify(raw)} is not a bare package name`}, so imports of the package's own name were not checked; only template imports outside the templates root and of Core were`,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
// The Core installed beside the package, which its templates' imports
|
|
1731
|
+
// resolve against; never the CLI's own copy, which may be another release.
|
|
1732
|
+
// A workspace's own `packages/core` (another package) is not it: then Core
|
|
1733
|
+
// counts as not installed and its imports are listed as not checked.
|
|
1734
|
+
const foundCore = name === CORE ? packageDir : findProjectCoreDir(packageDir);
|
|
1735
|
+
const foundCorePkg = foundCore
|
|
1736
|
+
? readJson(path.join(foundCore, 'package.json'))
|
|
1737
|
+
: null;
|
|
1738
|
+
const isCore = isRecord(foundCorePkg) && foundCorePkg.name === CORE;
|
|
1739
|
+
/** @type {Context} */
|
|
1740
|
+
const ctx = {
|
|
1741
|
+
packageDir,
|
|
1742
|
+
name,
|
|
1743
|
+
exportsField: pkg.exports,
|
|
1744
|
+
coreDir: isCore ? foundCore : null,
|
|
1745
|
+
corePkg: isCore ? foundCorePkg : null,
|
|
1746
|
+
unchecked: report.unchecked,
|
|
1747
|
+
exportNames: new Map(),
|
|
1748
|
+
files: Array.isArray(pkg.files)
|
|
1749
|
+
? pkg.files
|
|
1750
|
+
.filter(entry => typeof entry === 'string')
|
|
1751
|
+
.map(entry => entry.replace(/^\.\//u, '').replace(/\/+$/u, ''))
|
|
1752
|
+
: [],
|
|
1753
|
+
roots: [
|
|
1754
|
+
integration.components,
|
|
1755
|
+
integration.templates,
|
|
1756
|
+
integration.docs,
|
|
1757
|
+
integration.themes,
|
|
1758
|
+
integration.codemods,
|
|
1759
|
+
]
|
|
1760
|
+
.filter(root => typeof root === 'string')
|
|
1761
|
+
.map(root => path.resolve(root)),
|
|
1762
|
+
};
|
|
1763
|
+
const notes = [
|
|
1764
|
+
...(await componentIssues(ctx, integration)),
|
|
1765
|
+
...(await templateIssues(ctx, integration)),
|
|
1766
|
+
];
|
|
1767
|
+
// Listed only: none is an issue, so none counts or fails a check.
|
|
1768
|
+
return {issues: [], notes, report};
|
|
1769
|
+
}
|