@astryxdesign/cli 0.6.3 → 0.6.4-canary.10dd683
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/CHANGELOG.md +194 -0
- package/README.md +117 -78
- package/api/blog/blog.doc.mjs +1 -0
- package/api/build/_adapter.d.mts +50 -0
- package/api/build/_adapter.mjs +60 -0
- package/api/build/build.doc.mjs +16 -9
- package/api/build/build.test.mjs +197 -8
- package/api/build/build.type.d.mts +91 -2
- package/api/build/build.type.mjs +52 -8
- package/api/build/help/help.d.mts +12 -5
- package/api/build/help/help.mjs +69 -6
- package/api/build/kit/kit.d.mts +4 -1
- package/api/build/kit/kit.mjs +165 -49
- package/api/build/kit/rank.d.mts +44 -0
- package/api/build/kit/rank.mjs +432 -0
- package/api/build/kit/rank.test.mjs +196 -0
- package/api/component/_adapter.d.mts +6 -12
- package/api/component/_adapter.mjs +20 -10
- package/api/component/component.doc.mjs +13 -3
- package/api/component/component.mjs +91 -14
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +22 -11
- package/api/component/component.type.mjs +32 -24
- package/api/component/detail/blocks/blocks.d.mts +2 -1
- package/api/component/detail/blocks/blocks.mjs +4 -3
- package/api/component/list/list.d.mts +0 -5
- package/api/component/list/list.mjs +40 -11
- package/api/discover/_adapter.d.mts +114 -6
- package/api/discover/_adapter.mjs +372 -17
- package/api/discover/_adapter.test.mjs +215 -0
- package/api/discover/_catalog-view.d.mts +115 -0
- package/api/discover/_catalog-view.mjs +203 -0
- package/api/discover/_catalog-view.test.mjs +128 -0
- package/api/discover/detail/detail.d.mts +18 -6
- package/api/discover/detail/detail.mjs +67 -13
- package/api/discover/detail/detail.test.mjs +85 -0
- package/api/discover/detail/item/item.d.mts +26 -0
- package/api/discover/detail/item/item.mjs +78 -0
- package/api/discover/detail/item/item.test.mjs +73 -0
- package/api/discover/discover.d.mts +3 -9
- package/api/discover/discover.doc.mjs +62 -18
- package/api/discover/discover.mjs +220 -36
- package/api/discover/discover.test.mjs +11 -2
- package/api/discover/discover.type.d.mts +150 -11
- package/api/discover/discover.type.mjs +107 -17
- package/api/discover/list/list.d.mts +20 -6
- package/api/discover/list/list.mjs +45 -12
- package/api/discover/list/list.test.mjs +46 -0
- package/api/discover/search/search.d.mts +18 -16
- package/api/discover/search/search.mjs +102 -56
- package/api/discover/search/search.test.mjs +144 -10
- package/api/docs/_adapter.d.mts +272 -41
- package/api/docs/_adapter.mjs +985 -108
- package/api/docs/compiled-topics.test.mjs +78 -0
- package/api/docs/detail/detail.mjs +22 -63
- package/api/docs/detail/section/section.d.mts +1 -1
- package/api/docs/detail/section/section.mjs +54 -19
- package/api/docs/detail/section/section.test.mjs +50 -0
- package/api/docs/docs.d.mts +10 -3
- package/api/docs/docs.doc.mjs +55 -16
- package/api/docs/docs.mjs +53 -10
- package/api/docs/docs.test.mjs +164 -4
- package/api/docs/docs.type.d.mts +221 -5
- package/api/docs/docs.type.mjs +153 -11
- package/api/docs/index/index.d.mts +18 -0
- package/api/docs/index/index.mjs +40 -0
- package/api/docs/index/index.test.mjs +62 -0
- package/api/docs/integration-tree.test.mjs +555 -0
- package/api/docs/integrationDocs.test.mjs +114 -8
- package/api/docs/list/list.mjs +28 -12
- package/api/docs/node/node.d.mts +43 -0
- package/api/docs/node/node.mjs +192 -0
- package/api/docs/reference-blocks.test.mjs +406 -0
- package/api/doctor/doctor.d.mts +99 -1
- package/api/doctor/doctor.doc.mjs +1 -0
- package/api/doctor/doctor.mjs +548 -1
- package/api/doctor/doctor.test.mjs +610 -1
- package/api/gap-report/gap-report.doc.mjs +8 -4
- package/api/hook/_adapter.mjs +19 -5
- package/api/hook/hook.doc.mjs +1 -0
- package/api/hook/hook.type.d.mts +3 -3
- package/api/hook/hook.type.mjs +11 -11
- package/api/hook/list/list.d.mts +2 -2
- package/api/hook/list/list.mjs +69 -17
- package/api/index.d.mts +1 -1
- package/api/index.mjs +1 -0
- package/api/init/init.doc.mjs +6 -1
- package/api/init/init.test.mjs +41 -1
- package/api/init/remove/remove.mjs +1 -1
- package/api/init/run/run.mjs +20 -10
- package/api/integration/add-contribution.component-names.test.mjs +120 -0
- package/api/integration/add-contribution.d.mts +2 -1
- package/api/integration/add-contribution.mjs +130 -15
- package/api/integration/add-contribution.test.mjs +258 -7
- package/api/integration/add-helpers.d.mts +5 -2
- package/api/integration/add-helpers.mjs +36 -9
- package/api/integration/add-theme.mjs +34 -64
- package/api/integration/add-theme.test.mjs +105 -21
- package/api/integration/authoring-checks.mjs +138 -28
- package/api/integration/authoring-checks.test.mjs +179 -7
- package/api/integration/authoring-checks.type.mjs +6 -1
- package/api/integration/integration-authoring.type.d.mts +3 -1
- package/api/integration/integration-authoring.type.mjs +2 -0
- package/api/integration/integration-block-exports.test.mjs +10 -6
- package/api/integration/integrationAdd.doc.mjs +14 -4
- package/api/integration/integrationAddAgentDoc.doc.mjs +1 -0
- package/api/integration/integrationAddCodemod.doc.mjs +1 -0
- package/api/integration/integrationAddComponent.doc.mjs +2 -1
- package/api/integration/integrationAddDoc.doc.mjs +8 -1
- package/api/integration/integrationAddTemplate.doc.mjs +1 -0
- package/api/integration/integrationAddTheme.doc.mjs +6 -5
- package/api/integration/integrationComponentConflicts.doc.mjs +1 -0
- package/api/integration/integrationDocConflicts.doc.mjs +2 -1
- package/api/integration/integrationPackCheck.doc.mjs +2 -1
- package/api/integration/integrationTemplateConflicts.doc.d.mts +3 -0
- package/api/integration/integrationTemplateConflicts.doc.mjs +4 -0
- package/api/integration/pack-check.mjs +83 -7
- package/api/integration/pack-check.test.mjs +387 -47
- package/api/integration/pack-check.type.d.mts +26 -2
- package/api/integration/pack-check.type.mjs +14 -1
- package/api/integration/summarizeIssues.doc.mjs +1 -0
- package/api/integration/template-conflict-compatibility.test.mjs +73 -0
- package/api/integration/validate-integration-fixes.test.mjs +1389 -0
- package/api/integration/validate-integration.mjs +52 -102
- package/api/integration/validate-integration.test.mjs +179 -26
- package/api/integration/validate-unread-theme-folders.test.mjs +110 -0
- package/api/integration/validateIntegration.doc.mjs +3 -2
- package/api/json/assertResponse.doc.mjs +1 -0
- package/api/json/envelope-types.test.mjs +76 -0
- package/api/json/index.ts +2 -0
- package/api/json/isError.doc.mjs +1 -0
- package/api/json/parseResponse.doc.mjs +3 -2
- package/api/layout/_adapter.mjs +20 -5
- package/api/layout/expand/expand.mjs +7 -5
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.mjs +2 -1
- package/api/layout/layoutCheck.doc.mjs +1 -0
- package/api/layout/layoutExpand.doc.mjs +2 -1
- package/api/layout/layoutGrammar.doc.mjs +1 -0
- package/api/search/search-return-type.test.mjs +54 -0
- package/api/search/search.d.mts +62 -11
- package/api/search/search.doc.mjs +8 -2
- package/api/search/search.mjs +471 -83
- package/api/search/search.test.mjs +124 -1
- package/api/search/search.type.d.mts +15 -3
- package/api/search/search.type.mjs +5 -2
- package/api/swizzle/copy/copy.mjs +28 -11
- package/api/swizzle/swizzle.doc.mjs +2 -1
- package/api/swizzle/swizzle.type.d.mts +2 -2
- package/api/swizzle/swizzle.type.mjs +2 -2
- package/api/template/copy/copy.mjs +17 -23
- package/api/template/copy/copy.test.mjs +17 -0
- package/api/template/list/list.mjs +1 -0
- package/api/template/table-floating-bulk-actions.test.mjs +66 -0
- package/api/template/template-integration.test.mjs +1072 -3
- package/api/template/template-suffix.test.mjs +41 -21
- package/api/template/template.d.mts +1 -1
- package/api/template/template.doc.mjs +30 -8
- package/api/template/template.mjs +45 -8
- package/api/template/template.type.d.mts +12 -14
- package/api/template/template.type.mjs +15 -14
- package/api/theme/_adapter.d.mts +2 -3
- package/api/theme/_adapter.mjs +4 -5
- package/api/theme/add/add.binary.test.mjs +84 -0
- package/api/theme/add/add.mjs +31 -22
- package/api/theme/add/add.rollback.test.mjs +158 -0
- package/api/theme/add/add.staging.test.mjs +83 -0
- package/api/theme/add/add.test.mjs +14 -1
- package/api/theme/build/build.family.test.mjs +7 -12
- package/api/theme/build/build.mjs +140 -59
- package/api/theme/build/build.public-component-vars.test.mjs +1 -1
- package/api/theme/build/build.receipt-doc.test.mjs +111 -0
- package/api/theme/build/build.rollback.test.mjs +148 -0
- package/api/theme/build/build.test.mjs +127 -0
- package/api/theme/build/font-warning.mjs +3 -3
- package/api/theme/build/font-warning.test.mjs +5 -2
- package/api/theme/generateTonalPalette.doc.mjs +1 -0
- package/api/theme/integration-themes.test.mjs +39 -28
- package/api/theme/list/list.test.mjs +19 -20
- package/api/theme/listThemes.doc.mjs +6 -5
- package/api/theme/palette/generate/generate.mjs +8 -3
- package/api/theme/palette/generate/generate.test.mjs +96 -0
- package/api/theme/palette/generate/generator.d.mts +10 -13
- package/api/theme/palette/generate/generator.mjs +15 -4
- package/api/theme/palette/generate/generator.test.mjs +10 -0
- package/api/theme/template/template.mjs +11 -2
- package/api/theme/template/template.test.mjs +20 -0
- package/api/theme/theme.type.d.mts +170 -11
- package/api/theme/theme.type.mjs +94 -27
- package/api/theme/themeAdd.doc.mjs +4 -3
- package/api/theme/themeBuild.doc.mjs +8 -4
- package/api/theme/themeList.doc.mjs +6 -3
- package/api/theme/themeListAvailable.doc.mjs +5 -3
- package/api/theme/themePaletteGenerate.doc.mjs +1 -0
- package/api/theme/themeTargets.doc.mjs +1 -0
- package/api/theme/themeTemplate.doc.mjs +6 -2
- package/api/upgrade/_adapter.d.mts +32 -5
- package/api/upgrade/_adapter.mjs +139 -22
- package/api/upgrade/list/list.mjs +2 -1
- package/api/upgrade/list/list.test.mjs +73 -0
- package/api/upgrade/project-context.test.mjs +272 -0
- package/api/upgrade/provider-agreement.test.mjs +152 -0
- package/api/upgrade/run/run.mjs +356 -59
- package/api/upgrade/status/status.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +12 -5
- package/api/upgrade/upgrade.type.d.mts +43 -5
- package/api/upgrade/upgrade.type.mjs +27 -11
- package/assets/codemods/__tests__/registry.test.mjs +1 -0
- package/assets/codemods/__tests__/runner.test.mjs +330 -8
- package/assets/codemods/integration-discovery.mjs +48 -4
- package/assets/codemods/integration-discovery.test.mjs +73 -0
- package/assets/codemods/integration-runner.mjs +56 -4
- package/assets/codemods/integration-runner.protection.test.mjs +153 -0
- package/assets/codemods/registry.mjs +1 -0
- package/assets/codemods/run-codemod.mjs +177 -34
- package/assets/codemods/runner.mjs +350 -102
- package/assets/codemods/term-log.mjs +32 -8
- package/assets/codemods/term-log.test.mjs +19 -1
- package/assets/codemods/transform-prop.mjs +109 -0
- package/assets/codemods/transform-prop.test.mjs +95 -0
- package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +86 -165
- package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +72 -210
- package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +83 -115
- package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +57 -186
- package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
- package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
- package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
- package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
- package/assets/codemods/transforms/v0.6.4/__tests__/migrate-native-picker-to-presentation.test.mjs +63 -0
- package/assets/codemods/transforms/v0.6.4/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +220 -0
- package/assets/codemods/transforms/v0.6.4/index.mjs +31 -0
- package/assets/codemods/transforms/v0.6.4/migrate-native-picker-to-presentation.mjs +148 -0
- package/assets/codemods/transforms/v0.6.4/migrate-theme-catalog-to-descriptors.mjs +141 -0
- package/assets/docs/README.md +9 -0
- package/assets/docs/authoring.doc.mjs +14 -0
- package/assets/docs/getting-started.doc.mjs +2 -2
- package/assets/docs/internationalization.doc.mjs +7 -5
- package/assets/docs/layout.doc.dense.mjs +2 -2
- package/assets/docs/layout.doc.mjs +1 -1
- package/assets/docs/principles.doc.mjs +6 -6
- package/assets/docs/styling-libraries.doc.mjs +4 -4
- package/assets/docs/styling.doc.mjs +4 -4
- package/assets/docs/theme.doc.mjs +5 -5
- package/assets/docs/tokens.doc.mjs +1 -1
- package/assets/docs/tree/api.doc.mjs +30 -0
- package/assets/docs/tree/cli.doc.mjs +23 -0
- package/assets/docs/tree/commands.doc.mjs +25 -0
- package/assets/docs/{cli-integrations.doc.mjs → tree/integrations.doc.mjs} +144 -26
- package/assets/docs/tree/integrations.test.mjs +62 -0
- package/assets/docs/tree/writing-docs.doc.mjs +286 -0
- package/assets/docs/working-with-ai.doc.mjs +4 -4
- package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
- package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
- package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +1 -1
- package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.doc.mjs +14 -0
- package/assets/templates/blocks/components/Item/ItemDocumentTabs.tsx +100 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +61 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +126 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +86 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +99 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +122 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +95 -0
- package/assets/templates/blocks/components/Table/TableBulkActionsTable.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/TableBulkActionsTable.tsx +71 -0
- package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.doc.mjs +19 -0
- package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.tsx +139 -0
- package/assets/templates/blocks/components/TimeInput/TimeInputConstrained.tsx +1 -0
- package/assets/templates/blocks/components/Timer/TimerFormats.doc.mjs +14 -0
- package/assets/templates/blocks/components/Timer/TimerFormats.tsx +34 -0
- package/assets/templates/blocks/components/Timer/TimerInline.doc.mjs +14 -0
- package/assets/templates/blocks/components/Timer/TimerInline.tsx +14 -0
- package/assets/templates/blocks/components/Timer/TimerShowcase.doc.mjs +13 -0
- package/assets/templates/blocks/components/Timer/TimerShowcase.tsx +47 -0
- package/assets/templates/blocks/components/Timer/TimerTypography.doc.mjs +14 -0
- package/assets/templates/blocks/components/Timer/TimerTypography.tsx +31 -0
- package/assets/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +19 -3
- package/assets/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +383 -65
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/assets/templates/themes/butter/butterTheme.doc.mjs +11 -0
- package/assets/templates/themes/chocolate/chocolateTheme.doc.mjs +11 -0
- package/assets/templates/themes/gothic/gothicTheme.doc.mjs +11 -0
- package/assets/templates/themes/matcha/matchaTheme.doc.mjs +11 -0
- package/assets/templates/themes/neutral/neutralTheme.doc.mjs +11 -0
- package/assets/templates/themes/stone/stoneTheme.doc.mjs +11 -0
- package/assets/templates/themes/y2k/y2kTheme.doc.mjs +11 -0
- package/authoring/_shared/contract.ts +22 -0
- package/authoring/codemod/codemod.doc.mjs +7 -2
- package/authoring/codemod/parse.d.mts +8 -8
- package/authoring/codemod/parse.mjs +8 -6
- package/authoring/codemod/type.ts +12 -0
- package/authoring/config/config.doc.mjs +10 -2
- package/authoring/config/debug-composition.test.mjs +92 -0
- package/authoring/config/parse.d.mts +15 -13
- package/authoring/config/parse.mjs +27 -8
- package/authoring/config/parse.test.mjs +8 -0
- package/authoring/config/type.ts +29 -6
- package/authoring/debug/debug.doc.d.mts +11 -0
- package/authoring/debug/debug.doc.mjs +182 -0
- package/authoring/debug/parse.d.mts +8 -8
- package/authoring/debug/parse.mjs +3 -3
- package/authoring/discover/discover.doc.d.mts +13 -0
- package/authoring/discover/discover.doc.mjs +138 -0
- package/authoring/discover/parse.d.mts +24 -0
- package/authoring/discover/parse.mjs +128 -0
- package/authoring/discover/parse.test.mjs +124 -0
- package/authoring/discover/type.ts +87 -0
- package/authoring/doctypes/_schema.d.mts +790 -23
- package/authoring/doctypes/_schema.mjs +543 -39
- package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
- package/authoring/doctypes/base/graph-fields.doc.mjs +64 -0
- package/authoring/doctypes/base/type.ts +41 -0
- package/authoring/doctypes/command/command.doc.mjs +4 -3
- package/authoring/doctypes/command/parse.d.mts +2 -2
- package/authoring/doctypes/command/parse.mjs +1 -1
- package/authoring/doctypes/command/type.ts +5 -4
- package/authoring/doctypes/component/component.doc.mjs +6 -3
- package/authoring/doctypes/component/parse.d.mts +2 -2
- package/authoring/doctypes/component/parse.mjs +1 -1
- package/authoring/doctypes/component/type.ts +6 -5
- package/authoring/doctypes/doctypes-new.test.mjs +48 -6
- package/authoring/doctypes/enum/enum.doc.mjs +1 -1
- package/authoring/doctypes/enum/parse.d.mts +2 -2
- package/authoring/doctypes/enum/parse.mjs +1 -1
- package/authoring/doctypes/enum/type.ts +4 -2
- package/authoring/doctypes/function/function.doc.mjs +7 -2
- package/authoring/doctypes/function/parse.d.mts +2 -2
- package/authoring/doctypes/function/parse.mjs +1 -1
- package/authoring/doctypes/function/type.ts +9 -4
- package/authoring/doctypes/hook/hook.doc.mjs +4 -0
- package/authoring/doctypes/hook/parse.d.mts +2 -2
- package/authoring/doctypes/hook/parse.mjs +1 -1
- package/authoring/doctypes/hook/type.ts +5 -4
- package/authoring/doctypes/legacy.d.mts +8 -6
- package/authoring/doctypes/legacy.mjs +5 -4
- package/authoring/doctypes/load-contract.test.mjs +233 -0
- package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
- package/authoring/doctypes/namespace/namespace.doc.mjs +128 -0
- package/authoring/doctypes/namespace/parse.d.mts +12 -0
- package/authoring/doctypes/namespace/parse.mjs +25 -0
- package/authoring/doctypes/namespace/parse.test.mjs +163 -0
- package/authoring/doctypes/namespace/type.ts +74 -0
- package/authoring/doctypes/parse.d.mts +22 -18
- package/authoring/doctypes/parse.mjs +22 -11
- package/authoring/doctypes/parse.test.mjs +77 -3
- package/authoring/doctypes/reference/parse.d.mts +2 -2
- package/authoring/doctypes/reference/parse.mjs +8 -5
- package/authoring/doctypes/reference/reference.doc.mjs +48 -6
- package/authoring/doctypes/reference/type.ts +70 -7
- package/authoring/doctypes/schema/parse.d.mts +2 -2
- package/authoring/doctypes/schema/parse.mjs +1 -1
- package/authoring/doctypes/schema/schema.doc.mjs +1 -1
- package/authoring/doctypes/schema/type.ts +4 -4
- package/authoring/doctypes/template/parse.d.mts +94 -1
- package/authoring/doctypes/template/parse.mjs +40 -2
- package/authoring/doctypes/template/parse.test.mjs +26 -2
- package/authoring/doctypes/template/template.doc.mjs +13 -3
- package/authoring/doctypes/template/type.ts +13 -2
- package/authoring/doctypes/theme/parse.d.mts +35 -0
- package/authoring/doctypes/theme/parse.mjs +76 -0
- package/authoring/doctypes/theme/theme.doc.d.mts +9 -0
- package/authoring/doctypes/theme/theme.doc.mjs +79 -0
- package/authoring/doctypes/theme/type.ts +42 -0
- package/authoring/doctypes/types.ts +12 -10
- package/authoring/gap-report/gap-report.doc.d.mts +12 -0
- package/authoring/gap-report/gap-report.doc.mjs +183 -0
- package/authoring/gap-report/parse.d.mts +10 -10
- package/authoring/gap-report/parse.mjs +6 -6
- package/authoring/gap-report/type.ts +1 -1
- package/authoring/identity/identity.doc.d.mts +9 -0
- package/authoring/identity/identity.doc.mjs +61 -0
- package/authoring/identity/type.ts +132 -0
- package/authoring/index.d.mts +3 -0
- package/authoring/index.d.ts +62 -17
- package/authoring/index.mjs +4 -1
- package/authoring/integration/integration.doc.mjs +15 -8
- package/authoring/integration/parse.d.mts +2 -2
- package/authoring/integration/parse.mjs +1 -1
- package/authoring/integration/parse.test.mjs +10 -1
- package/authoring/integration/schema.d.mts +6 -4
- package/authoring/integration/schema.mjs +9 -3
- package/authoring/integration/type.ts +19 -8
- package/authoring/shadcn/receipt.d.mts +6 -6
- package/clients/cli/__tests__/cliManifest.test.ts +27 -29
- package/clients/cli/command-load-failure.test.mjs +83 -0
- package/clients/cli/commands/blog.doc.mjs +1 -1
- package/clients/cli/commands/blog.mjs +23 -8
- package/clients/cli/commands/blog.test.mjs +42 -1
- package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
- package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
- package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
- package/clients/cli/commands/build-theme.mjs +16 -50
- package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
- package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
- package/clients/cli/commands/build.doc.mjs +16 -8
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
- package/clients/cli/commands/build.mjs +137 -114
- package/clients/cli/commands/build.playbook.test.mjs +75 -0
- package/clients/cli/commands/build.text-fields.test.mjs +81 -0
- package/clients/cli/commands/component/index.mjs +3 -8
- package/clients/cli/commands/component-ownership.test.mjs +3 -3
- package/clients/cli/commands/component-package.test.mjs +46 -0
- package/clients/cli/commands/component-resolution.test.mjs +21 -0
- package/clients/cli/commands/component.doc.mjs +1 -1
- package/clients/cli/commands/component.test.mjs +19 -0
- package/clients/cli/commands/detail-levels.test.mjs +2 -2
- package/clients/cli/commands/discover.broken-integration.test.mjs +52 -5
- package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
- package/clients/cli/commands/discover.doc.mjs +55 -9
- package/clients/cli/commands/discover.mjs +393 -118
- package/clients/cli/commands/discover.sources.test.mjs +267 -0
- package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
- package/clients/cli/commands/docs.doc.mjs +28 -6
- package/clients/cli/commands/docs.mjs +240 -26
- package/clients/cli/commands/docs.test.mjs +193 -1
- package/clients/cli/commands/doctor-integration-components.doc.mjs +1 -1
- package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -3
- package/clients/cli/commands/doctor-integration-templates.doc.mjs +15 -8
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +1 -1
- package/clients/cli/commands/doctor-integration.doc.mjs +1 -1
- package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
- package/clients/cli/commands/doctor-integration.test.mjs +90 -8
- package/clients/cli/commands/doctor.doc.mjs +1 -1
- package/clients/cli/commands/doctor.mjs +59 -32
- package/clients/cli/commands/doctor.test.mjs +42 -0
- package/clients/cli/commands/gap-report.doc.mjs +17 -6
- package/clients/cli/commands/gap-report.test.mjs +72 -0
- package/clients/cli/commands/hook/index.mjs +7 -17
- package/clients/cli/commands/hook.doc.mjs +1 -1
- package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
- package/clients/cli/commands/init.doc.mjs +20 -9
- package/clients/cli/commands/init.flag-help.test.mjs +153 -0
- package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
- package/clients/cli/commands/integration-add.doc.mjs +32 -6
- package/clients/cli/commands/integration-authoring.test.mjs +13 -9
- package/clients/cli/commands/integration-pack.doc.mjs +1 -1
- package/clients/cli/commands/integration-real-world.test.mjs +3 -9
- package/clients/cli/commands/integration.doc.mjs +1 -1
- package/clients/cli/commands/integration.mjs +1 -0
- package/clients/cli/commands/interactive-guard.test.mjs +101 -24
- package/clients/cli/commands/json-contract.test.mjs +33 -0
- package/clients/cli/commands/layout-check.doc.mjs +15 -4
- package/clients/cli/commands/layout-expand.doc.mjs +22 -5
- package/clients/cli/commands/layout-grammar.doc.mjs +1 -1
- package/clients/cli/commands/layout.doc.mjs +3 -3
- package/clients/cli/commands/layout.mjs +21 -9
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/manifest.doc.mjs +1 -1
- package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
- package/clients/cli/commands/search.doc.mjs +7 -4
- package/clients/cli/commands/search.mjs +28 -9
- package/clients/cli/commands/search.test.mjs +75 -0
- package/clients/cli/commands/setup-nudge.test.mjs +6 -0
- package/clients/cli/commands/swizzle.doc.mjs +3 -2
- package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
- package/clients/cli/commands/template.doc.mjs +52 -13
- package/clients/cli/commands/template.flag-help.test.mjs +117 -0
- package/clients/cli/commands/template.mjs +4 -91
- package/clients/cli/commands/template.path-help.test.mjs +40 -0
- package/clients/cli/commands/text-json-parity.test.mjs +719 -0
- package/clients/cli/commands/theme-add.doc.mjs +4 -3
- package/clients/cli/commands/theme-build.doc.mjs +8 -7
- package/clients/cli/commands/theme-list.doc.mjs +2 -2
- package/clients/cli/commands/theme-palette-generate.doc.mjs +9 -5
- package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
- package/clients/cli/commands/theme-palette.doc.mjs +1 -1
- package/clients/cli/commands/theme-targets.behavior.test.mjs +4 -3
- package/clients/cli/commands/theme-targets.doc.mjs +1 -1
- package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
- package/clients/cli/commands/theme-template.doc.mjs +2 -2
- package/clients/cli/commands/theme.doc.mjs +1 -1
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
- package/clients/cli/commands/upgrade.doc.mjs +22 -10
- package/clients/cli/commands/upgrade.file-protection.test.mjs +228 -0
- package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
- package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
- package/clients/cli/commands/upgrade.mjs +29 -7
- package/clients/cli/formatters/index.mjs +164 -1
- package/clients/cli/formatters/index.test.mjs +97 -0
- package/clients/cli/index.mjs +21 -30
- package/clients/cli/latest-version-env.test.mjs +50 -0
- package/clients/cli/lib/cli-error.test.mjs +7 -0
- package/clients/cli/lib/component-format.mjs +9 -9
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +32 -6
- package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
- package/clients/cli/lib/exit-codes.test.mjs +97 -0
- package/clients/cli/lib/hook-format.mjs +19 -10
- package/clients/cli/lib/json-shim.mjs +38 -2
- package/clients/cli/lib/json-shim.test.mjs +83 -0
- package/clients/cli/lib/manifest.d.ts +2 -0
- package/clients/cli/lib/manifest.mjs +37 -2
- package/clients/cli/lib/manifest.test.mjs +17 -0
- package/foundation/agent-docs/agent-docs.d.mts +7 -2
- package/foundation/agent-docs/agent-docs.mjs +82 -12
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
- package/foundation/agent-docs/agent-docs.test.mjs +19 -1
- package/foundation/config/integration-debug.test.mjs +28 -3
- package/foundation/config/project-themes.test.mjs +11 -19
- package/foundation/config/project.d.mts +20 -11
- package/foundation/config/project.mjs +263 -91
- package/foundation/config/project.test.mjs +270 -21
- package/foundation/discovery/authoring-self-docs.d.mts +87 -0
- package/foundation/discovery/authoring-self-docs.mjs +237 -0
- package/foundation/discovery/authoring-self-docs.test.mjs +170 -0
- package/foundation/discovery/authoring-surface.d.mts +74 -0
- package/foundation/discovery/authoring-surface.mjs +525 -0
- package/foundation/discovery/authoring-surface.test.mjs +392 -0
- package/foundation/discovery/cli-self-docs.d.mts +119 -0
- package/foundation/discovery/cli-self-docs.mjs +490 -0
- package/foundation/discovery/cli-self-docs.test.mjs +375 -0
- package/foundation/discovery/component-discovery.d.mts +39 -1
- package/foundation/discovery/component-discovery.mjs +50 -1
- package/foundation/discovery/component-loader.d.mts +35 -38
- package/foundation/discovery/component-loader.mjs +53 -222
- package/foundation/discovery/docs-discovery.d.mts +119 -11
- package/foundation/discovery/docs-discovery.mjs +423 -108
- package/foundation/discovery/docs-discovery.test.mjs +365 -20
- package/foundation/discovery/docs-output-budget.d.mts +28 -0
- package/foundation/discovery/docs-output-budget.mjs +50 -0
- package/foundation/discovery/docs-section-key.d.mts +116 -0
- package/foundation/discovery/docs-section-key.mjs +322 -0
- package/foundation/discovery/docs-section-key.test.mjs +246 -0
- package/foundation/discovery/template-adapter.d.mts +113 -11
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
- package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
- package/foundation/discovery/template-adapter.mjs +774 -83
- package/foundation/discovery/template-adapter.test.mjs +57 -0
- package/foundation/discovery/template-conflict-release.d.mts +13 -0
- package/foundation/discovery/template-conflict-release.mjs +40 -0
- package/foundation/discovery/template-conflict-release.test.mjs +40 -0
- package/foundation/discovery/theme-discovery.d.mts +67 -7
- package/foundation/discovery/theme-discovery.mjs +916 -186
- package/foundation/discovery/theme-discovery.test.mjs +613 -219
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/doc-compiler/bundle.d.mts +47 -0
- package/foundation/doc-compiler/bundle.mjs +278 -0
- package/foundation/doc-compiler/bundle.test.mjs +266 -0
- package/foundation/doc-compiler/compile.d.mts +343 -0
- package/foundation/doc-compiler/compile.mjs +558 -0
- package/foundation/doc-compiler/diagnostics.d.mts +126 -0
- package/foundation/doc-compiler/diagnostics.mjs +305 -0
- package/foundation/doc-compiler/doc-compiler.test.mjs +714 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +1642 -0
- package/foundation/doc-compiler/import.d.mts +24 -0
- package/foundation/doc-compiler/import.mjs +59 -0
- package/foundation/doc-compiler/inputs.d.mts +102 -0
- package/foundation/doc-compiler/inputs.mjs +291 -0
- package/foundation/doc-compiler/inputs.test.mjs +299 -0
- package/foundation/doc-compiler/ir.d.mts +22 -0
- package/foundation/doc-compiler/ir.mjs +471 -0
- package/foundation/doc-compiler/lenses.d.mts +36 -0
- package/foundation/doc-compiler/lenses.mjs +173 -0
- package/foundation/doc-compiler/links.d.mts +162 -0
- package/foundation/doc-compiler/links.mjs +294 -0
- package/foundation/doc-compiler/links.test.mjs +192 -0
- package/foundation/doc-compiler/lower-doc.test.mjs +495 -0
- package/foundation/doc-compiler/overlays.d.mts +37 -0
- package/foundation/doc-compiler/overlays.mjs +206 -0
- package/foundation/doc-compiler/parse-readable.d.mts +9 -0
- package/foundation/doc-compiler/parse-readable.mjs +29 -0
- package/foundation/doc-compiler/read.d.mts +127 -0
- package/foundation/doc-compiler/read.mjs +325 -0
- package/foundation/doc-compiler/read.test.mjs +313 -0
- package/foundation/doc-compiler/source.d.mts +33 -0
- package/foundation/doc-compiler/source.mjs +128 -0
- package/foundation/doc-compiler/tree.d.mts +288 -0
- package/foundation/doc-compiler/tree.mjs +876 -0
- package/foundation/doc-compiler/tree.test.mjs +598 -0
- package/foundation/fs/file-protection.d.mts +33 -0
- package/foundation/fs/file-protection.mjs +825 -0
- package/foundation/fs/file-protection.test.mjs +250 -0
- package/foundation/fs/module-loader.d.mts +1 -0
- package/foundation/fs/module-loader.mjs +50 -1
- package/foundation/fs/module-loader.stdout.test.mjs +332 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
- package/foundation/identity/provider-identity.d.mts +90 -0
- package/foundation/identity/provider-identity.mjs +320 -0
- package/foundation/identity/provider-identity.test.mjs +254 -0
- package/foundation/identity/providers.d.mts +7 -0
- package/foundation/identity/providers.mjs +16 -0
- package/foundation/integrations/autolink.d.mts +58 -1
- package/foundation/integrations/autolink.mjs +143 -45
- package/foundation/integrations/autolink.test.mjs +1 -1
- package/foundation/integrations/cli-requirement.d.mts +45 -0
- package/foundation/integrations/cli-requirement.mjs +154 -0
- package/foundation/integrations/cli-requirement.test.mjs +84 -0
- package/foundation/integrations/contribution-fixes.d.mts +145 -0
- package/foundation/integrations/contribution-fixes.mjs +1284 -0
- package/foundation/integrations/contribution-inventory.d.mts +3 -2
- package/foundation/integrations/contribution-inventory.mjs +27 -24
- package/foundation/integrations/contribution-inventory.test.mjs +86 -27
- package/foundation/integrations/integration-warnings.d.mts +9 -2
- package/foundation/integrations/integration-warnings.mjs +52 -21
- package/foundation/integrations/integration-warnings.test.mjs +74 -1
- package/foundation/integrations/integrations.d.mts +63 -3
- package/foundation/integrations/integrations.mjs +122 -9
- package/foundation/integrations/integrations.test.mjs +415 -1
- package/foundation/integrations/provider-conflicts.test.mjs +125 -0
- package/foundation/integrations/provider-ledger.test.mjs +275 -0
- package/foundation/integrations/provider-resolution.d.mts +152 -0
- package/foundation/integrations/provider-resolution.mjs +576 -0
- package/foundation/integrations/provider-resolution.test.mjs +369 -0
- package/foundation/integrations/theme-descriptor.d.mts +8 -0
- package/foundation/integrations/theme-descriptor.mjs +44 -0
- package/foundation/integrations/validate-contributions.d.mts +2 -0
- package/foundation/integrations/validate-contributions.mjs +131 -29
- package/foundation/response/base.d.ts +8 -4
- package/foundation/response/error-codes.d.mts +3 -1
- package/foundation/response/error-codes.d.ts +2 -0
- package/foundation/response/error-codes.doc.mjs +13 -4
- package/foundation/response/error-codes.mjs +8 -2
- package/foundation/response/error-codes.test.mjs +137 -10
- package/foundation/response/json-contract.test.mjs +57 -17
- package/foundation/response/json.d.mts +4 -2
- package/foundation/response/json.mjs +8 -10
- package/foundation/response/response-types.doc.d.mts +5 -1
- package/foundation/response/response-types.doc.mjs +41 -21
- package/foundation/response/response-types.doc.test.mjs +158 -0
- package/foundation/response/response.doc.mjs +1 -1
- package/foundation/text/string-utils.d.mts +8 -0
- package/foundation/text/string-utils.mjs +40 -10
- package/foundation/xle/expand.d.mts +2 -0
- package/foundation/xle/expand.mjs +4 -3
- package/foundation/xle/expand.test.mjs +54 -0
- package/foundation/xle/xle.test.mjs +13 -0
- package/package.json +10 -11
- package/assets/templates/themes/manifest.json +0 -95
- package/clients/cli/lib/update-check.mjs +0 -83
- package/clients/cli/lib/update-check.test.mjs +0 -137
- package/clients/cli/update-hint-commands.test.mjs +0 -54
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The `Fix:` advice for contribution metadata that no root reads, or
|
|
5
|
+
* that the wrong root reads.
|
|
6
|
+
*
|
|
7
|
+
* A fix must hold when followed literally. It never names a path outside the
|
|
8
|
+
* package, never makes the package root a root, and names only the offending
|
|
9
|
+
* file plus the same-stem source that moves with it. It offers a folder as a
|
|
10
|
+
* root only when that root would read nothing but this kind of metadata,
|
|
11
|
+
* overlap no other root, and orphan nothing the current root holds. Metadata
|
|
12
|
+
* is read statically: nothing here runs package code.
|
|
13
|
+
*
|
|
14
|
+
* @input a package directory, its resolved roots, and one misplaced file
|
|
15
|
+
* @output the `Fix: ...` sentence for that file
|
|
16
|
+
* @position packages/cli/foundation/integrations — shared by the unreachable
|
|
17
|
+
* scan in api/integration/validate-integration.mjs and the component checks
|
|
18
|
+
* in validate-contributions.mjs
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import jscodeshift from 'jscodeshift';
|
|
24
|
+
import {isValidSemver} from '../env/semver.mjs';
|
|
25
|
+
import {
|
|
26
|
+
isThemeFolder as readsAsTheme,
|
|
27
|
+
THEME_DOC_SUFFIX,
|
|
28
|
+
THEME_MODULE_EXTENSIONS,
|
|
29
|
+
THEME_SLUG_RE,
|
|
30
|
+
} from '../discovery/theme-discovery.mjs';
|
|
31
|
+
|
|
32
|
+
/** @typedef {'components' | 'templates' | 'docs' | 'themes'} MetadataRoot */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} ContributionStamp
|
|
36
|
+
* @property {string} type
|
|
37
|
+
* @property {string | null} name the static `name`, when it has one
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} FixContext
|
|
42
|
+
* @property {string} packageDir
|
|
43
|
+
* @property {string} manifest file name of the package's manifest
|
|
44
|
+
* @property {Partial<Record<MetadataRoot | 'codemods', string>>} roots
|
|
45
|
+
* declared roots, absolute
|
|
46
|
+
* @property {Map<string, ContributionStamp | null>} stamps
|
|
47
|
+
* @property {Map<string, string[] | null>} candidates
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
export const DOC_CANDIDATE_RE = /\.doc\.(?:ts|mjs|js)$/u;
|
|
51
|
+
export const TEMPLATE_CANDIDATE_RE = /\.template\.(?:ts|mjs|js)$/u;
|
|
52
|
+
const STATIC_DOC_TYPES = new Set([
|
|
53
|
+
'component',
|
|
54
|
+
'generic',
|
|
55
|
+
'page',
|
|
56
|
+
'block',
|
|
57
|
+
'theme',
|
|
58
|
+
]);
|
|
59
|
+
const STATIC_TEMPLATE_TYPES = new Set(['page', 'block']);
|
|
60
|
+
/** Stray-codemod and version-folder rules, shared with the codemod checks. */
|
|
61
|
+
export const CODEMOD_FILE_RE = /\.(?:ts|mjs|js)$/u;
|
|
62
|
+
export const CODEMOD_TEST_RE = /\.(?:test|spec|fixture)\.(?:ts|mjs|js)$/u;
|
|
63
|
+
export const CODEMOD_SKIP_DIRS = new Set([
|
|
64
|
+
'node_modules',
|
|
65
|
+
'.git',
|
|
66
|
+
'__tests__',
|
|
67
|
+
'__fixtures__',
|
|
68
|
+
]);
|
|
69
|
+
/** @type {MetadataRoot[]} */
|
|
70
|
+
const METADATA_ROOTS = ['components', 'templates', 'docs', 'themes'];
|
|
71
|
+
/** Past this many entries a folder's contents count as unknown. */
|
|
72
|
+
const FOLDER_SCAN_LIMIT = 5_000;
|
|
73
|
+
|
|
74
|
+
/** The manifest root that reads each kind of contribution metadata. */
|
|
75
|
+
export const ROOT_FOR_TYPE = /** @type {Record<string, MetadataRoot>} */ ({
|
|
76
|
+
component: 'components',
|
|
77
|
+
generic: 'docs',
|
|
78
|
+
page: 'templates',
|
|
79
|
+
block: 'templates',
|
|
80
|
+
theme: 'themes',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** What a root holds, as a fix names it. */
|
|
84
|
+
const ROOT_HOLDS = /** @type {Record<MetadataRoot, string>} */ ({
|
|
85
|
+
components: 'components',
|
|
86
|
+
docs: 'topics',
|
|
87
|
+
templates: 'templates',
|
|
88
|
+
themes: 'themes',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const j = jscodeshift.withParser('tsx');
|
|
92
|
+
|
|
93
|
+
/** @param {any} node @returns {any} */
|
|
94
|
+
function unwrapStaticExpression(node) {
|
|
95
|
+
let current = node;
|
|
96
|
+
while (
|
|
97
|
+
current &&
|
|
98
|
+
[
|
|
99
|
+
'TSSatisfiesExpression',
|
|
100
|
+
'TSAsExpression',
|
|
101
|
+
'TypeCastExpression',
|
|
102
|
+
'ParenthesizedExpression',
|
|
103
|
+
].includes(current.type)
|
|
104
|
+
) {
|
|
105
|
+
current = current.expression;
|
|
106
|
+
}
|
|
107
|
+
if (current?.type === 'CallExpression' && current.arguments.length > 0) {
|
|
108
|
+
current = unwrapStaticExpression(current.arguments[0]);
|
|
109
|
+
}
|
|
110
|
+
return current;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @param {any} property @param {string} name */
|
|
114
|
+
function staticPropertyNamed(property, name) {
|
|
115
|
+
if (
|
|
116
|
+
!property ||
|
|
117
|
+
!['ObjectProperty', 'Property'].includes(property.type) ||
|
|
118
|
+
property.computed
|
|
119
|
+
) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return (
|
|
123
|
+
(property.key?.type === 'Identifier' && property.key.name === name) ||
|
|
124
|
+
(['Literal', 'StringLiteral'].includes(property.key?.type) &&
|
|
125
|
+
property.key.value === name)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Identify contribution metadata without importing it. Doctor scans files that
|
|
131
|
+
* the manifest does not declare, so executing those files would run code the
|
|
132
|
+
* package never asked Astryx to load.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} file
|
|
135
|
+
* @param {boolean} templateOnly
|
|
136
|
+
* @returns {ContributionStamp | null} null when it is not contribution metadata
|
|
137
|
+
*/
|
|
138
|
+
export function readContributionStamp(file, templateOnly) {
|
|
139
|
+
let ast;
|
|
140
|
+
try {
|
|
141
|
+
ast = j(fs.readFileSync(file, 'utf-8'));
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/** @type {any[]} */
|
|
146
|
+
const candidates = [];
|
|
147
|
+
ast
|
|
148
|
+
.find(j.ExportDefaultDeclaration)
|
|
149
|
+
.forEach((/** @type {any} */ exportPath) => {
|
|
150
|
+
candidates.push(exportPath.value.declaration);
|
|
151
|
+
});
|
|
152
|
+
ast
|
|
153
|
+
.find(j.ExportNamedDeclaration)
|
|
154
|
+
.forEach((/** @type {any} */ exportPath) => {
|
|
155
|
+
const declaration = exportPath.value.declaration;
|
|
156
|
+
if (declaration?.type !== 'VariableDeclaration') return;
|
|
157
|
+
for (const declarator of declaration.declarations) {
|
|
158
|
+
if (
|
|
159
|
+
declarator.id?.type === 'Identifier' &&
|
|
160
|
+
declarator.id.name === 'docs'
|
|
161
|
+
) {
|
|
162
|
+
candidates.push(declarator.init);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const allowedTypes = templateOnly ? STATIC_TEMPLATE_TYPES : STATIC_DOC_TYPES;
|
|
168
|
+
for (const candidate of candidates) {
|
|
169
|
+
const object = unwrapStaticExpression(candidate);
|
|
170
|
+
if (object?.type !== 'ObjectExpression') continue;
|
|
171
|
+
/** @param {string} name @returns {unknown} */
|
|
172
|
+
const field = name =>
|
|
173
|
+
unwrapStaticExpression(
|
|
174
|
+
object.properties.find((/** @type {any} */ property) =>
|
|
175
|
+
staticPropertyNamed(property, name),
|
|
176
|
+
)?.value,
|
|
177
|
+
);
|
|
178
|
+
const type = /** @type {any} */ (field('type'));
|
|
179
|
+
if (
|
|
180
|
+
['Literal', 'StringLiteral'].includes(type?.type) &&
|
|
181
|
+
allowedTypes.has(type.value)
|
|
182
|
+
) {
|
|
183
|
+
const name = /** @type {any} */ (field('name'));
|
|
184
|
+
return {
|
|
185
|
+
type: type.value,
|
|
186
|
+
name: typeof name?.value === 'string' ? name.value : null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @param {string} candidate @param {string} root */
|
|
194
|
+
export function pathIsInside(candidate, root) {
|
|
195
|
+
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {string} packageDir
|
|
200
|
+
* @param {{components?: string, templates?: string, codemods?: string, docs?: string, themes?: string, __manifestFile?: string}} integration
|
|
201
|
+
* @returns {FixContext}
|
|
202
|
+
*/
|
|
203
|
+
export function createFixContext(packageDir, integration) {
|
|
204
|
+
return {
|
|
205
|
+
packageDir,
|
|
206
|
+
manifest: integration.__manifestFile
|
|
207
|
+
? path.basename(integration.__manifestFile)
|
|
208
|
+
: 'astryx.integration.mjs',
|
|
209
|
+
roots: {
|
|
210
|
+
components: integration.components,
|
|
211
|
+
templates: integration.templates,
|
|
212
|
+
codemods: integration.codemods,
|
|
213
|
+
docs: integration.docs,
|
|
214
|
+
themes: integration.themes,
|
|
215
|
+
},
|
|
216
|
+
stamps: new Map(),
|
|
217
|
+
candidates: new Map(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The static stamp of a candidate metadata file, read once per context.
|
|
223
|
+
* @param {FixContext} context
|
|
224
|
+
* @param {string} file
|
|
225
|
+
* @returns {ContributionStamp | null}
|
|
226
|
+
*/
|
|
227
|
+
export function stampOf(context, file) {
|
|
228
|
+
if (!context.stamps.has(file)) {
|
|
229
|
+
context.stamps.set(
|
|
230
|
+
file,
|
|
231
|
+
readContributionStamp(file, TEMPLATE_CANDIDATE_RE.test(file)),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return context.stamps.get(file) ?? null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @param {FixContext} context @param {string} file */
|
|
238
|
+
function shown(context, file) {
|
|
239
|
+
return path.relative(context.packageDir, file).split(path.sep).join('/');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @param {FixContext} context */
|
|
243
|
+
function declaredRoots(context) {
|
|
244
|
+
return /** @type {string[]} */ (Object.values(context.roots).filter(Boolean));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** @param {string} file */
|
|
248
|
+
function isFile(file) {
|
|
249
|
+
try {
|
|
250
|
+
return fs.statSync(file).isFile();
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Every doc or template file under `dir`, or null when there are too many
|
|
258
|
+
* entries to know.
|
|
259
|
+
* @param {FixContext} context
|
|
260
|
+
* @param {string} dir
|
|
261
|
+
* @returns {string[] | null}
|
|
262
|
+
*/
|
|
263
|
+
function candidatesUnder(context, dir) {
|
|
264
|
+
if (context.candidates.has(dir)) {
|
|
265
|
+
return context.candidates.get(dir) ?? null;
|
|
266
|
+
}
|
|
267
|
+
/** @type {string[] | null} */
|
|
268
|
+
let found = [];
|
|
269
|
+
let seen = 0;
|
|
270
|
+
/** @param {string} current */
|
|
271
|
+
const walk = current => {
|
|
272
|
+
let entries;
|
|
273
|
+
try {
|
|
274
|
+
entries = fs.readdirSync(current, {withFileTypes: true});
|
|
275
|
+
} catch {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (found == null) return;
|
|
280
|
+
if (entry.name === '.git') continue;
|
|
281
|
+
seen += 1;
|
|
282
|
+
if (seen > FOLDER_SCAN_LIMIT) {
|
|
283
|
+
found = null;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const full = path.join(current, entry.name);
|
|
287
|
+
if (entry.isDirectory()) {
|
|
288
|
+
walk(full);
|
|
289
|
+
} else if (
|
|
290
|
+
entry.isFile() &&
|
|
291
|
+
(DOC_CANDIDATE_RE.test(entry.name) ||
|
|
292
|
+
TEMPLATE_CANDIDATE_RE.test(entry.name))
|
|
293
|
+
) {
|
|
294
|
+
found.push(full);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
walk(dir);
|
|
299
|
+
context.candidates.set(dir, found);
|
|
300
|
+
return found;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A folder inside the package, not its root, that neither holds nor sits in a
|
|
305
|
+
* declared root.
|
|
306
|
+
* @param {FixContext} context
|
|
307
|
+
* @param {string} dir
|
|
308
|
+
*/
|
|
309
|
+
function isFreeFolder(context, dir) {
|
|
310
|
+
return (
|
|
311
|
+
dir !== context.packageDir &&
|
|
312
|
+
pathIsInside(dir, context.packageDir) &&
|
|
313
|
+
!declaredRoots(context).some(
|
|
314
|
+
root => pathIsInside(root, dir) || pathIsInside(dir, root),
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Whether `dir` is one complete theme: a slug folder whose only metadata is
|
|
321
|
+
* its theme descriptor, naming that folder, beside exactly one source that
|
|
322
|
+
* imports nothing outside the folder. The descriptor being fixed may lack its
|
|
323
|
+
* source or import from outside; the fix asks for both.
|
|
324
|
+
* @param {FixContext} context
|
|
325
|
+
* @param {string} dir
|
|
326
|
+
* @param {string} [fixing]
|
|
327
|
+
*/
|
|
328
|
+
function isCompleteTheme(context, dir, fixing) {
|
|
329
|
+
const found = candidatesUnder(context, dir);
|
|
330
|
+
if (found?.length !== 1) return false;
|
|
331
|
+
const [descriptor] = found;
|
|
332
|
+
const stamp = stampOf(context, descriptor);
|
|
333
|
+
const slug = path.basename(dir);
|
|
334
|
+
return (
|
|
335
|
+
THEME_SLUG_RE.test(slug) &&
|
|
336
|
+
path.dirname(descriptor) === dir &&
|
|
337
|
+
descriptor.endsWith(THEME_DOC_SUFFIX) &&
|
|
338
|
+
stamp?.type === 'theme' &&
|
|
339
|
+
stamp.name === slug &&
|
|
340
|
+
(descriptor === fixing ||
|
|
341
|
+
(sourcesBeside(descriptor, 'theme').length === 1 &&
|
|
342
|
+
importsOutside(descriptor)?.length === 0))
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Whether `dir` reads cleanly as a themes root: every folder in it that
|
|
348
|
+
* discovery reads as a theme is one complete theme, and no metadata sits
|
|
349
|
+
* directly in it.
|
|
350
|
+
* @param {FixContext} context
|
|
351
|
+
* @param {string} dir
|
|
352
|
+
* @param {{except?: string, ignore?: string, fixing?: string}} [options] a
|
|
353
|
+
* theme folder moving out of `dir`, a descriptor moving away, and the
|
|
354
|
+
* descriptor being fixed
|
|
355
|
+
*/
|
|
356
|
+
function readsAsThemesRoot(context, dir, {except, ignore, fixing} = {}) {
|
|
357
|
+
if (!fs.existsSync(dir)) return true;
|
|
358
|
+
if (fs.existsSync(path.join(dir, 'manifest.json'))) return false;
|
|
359
|
+
let entries;
|
|
360
|
+
try {
|
|
361
|
+
entries = fs.readdirSync(dir, {withFileTypes: true});
|
|
362
|
+
} catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
return entries.every(entry => {
|
|
366
|
+
const full = path.join(dir, entry.name);
|
|
367
|
+
if (entry.name.startsWith('.')) return true;
|
|
368
|
+
if (entry.isSymbolicLink()) return false;
|
|
369
|
+
if (entry.isDirectory()) {
|
|
370
|
+
return (
|
|
371
|
+
full === except ||
|
|
372
|
+
!readsAsTheme(full) ||
|
|
373
|
+
isCompleteTheme(context, full, fixing)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return (
|
|
377
|
+
full === ignore ||
|
|
378
|
+
!(
|
|
379
|
+
DOC_CANDIDATE_RE.test(entry.name) ||
|
|
380
|
+
TEMPLATE_CANDIDATE_RE.test(entry.name)
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Whether declaring `dir` as the `key` root reads only complete metadata of
|
|
388
|
+
* that kind, overlaps no root, and stays inside the package below its root.
|
|
389
|
+
* The file being fixed may lack its source; the fix asks for it.
|
|
390
|
+
* @param {FixContext} context
|
|
391
|
+
* @param {string} dir
|
|
392
|
+
* @param {MetadataRoot} key
|
|
393
|
+
* @param {string} fixing
|
|
394
|
+
*/
|
|
395
|
+
function canBeRoot(context, dir, key, fixing) {
|
|
396
|
+
if (!isFreeFolder(context, dir)) return false;
|
|
397
|
+
if (key === 'themes') return readsAsThemesRoot(context, dir, {fixing});
|
|
398
|
+
const found = candidatesUnder(context, dir);
|
|
399
|
+
if (found == null) return false;
|
|
400
|
+
const complete = found.every(file => {
|
|
401
|
+
const stamp = stampOf(context, file);
|
|
402
|
+
return (
|
|
403
|
+
stamp != null &&
|
|
404
|
+
ROOT_FOR_TYPE[stamp.type] === key &&
|
|
405
|
+
(file === fixing ||
|
|
406
|
+
stamp.type === 'generic' ||
|
|
407
|
+
sourcesBeside(file, stamp.type).length > 0)
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
const names = found.map(file => identity(context, file, key));
|
|
411
|
+
const unique = new Set(names).size === names.length;
|
|
412
|
+
return (
|
|
413
|
+
complete &&
|
|
414
|
+
unique &&
|
|
415
|
+
(key !== 'components' || uncoveredSources(dir, found).length === 0)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The name a root knows a contribution by, and must hold only once: a
|
|
421
|
+
* component's stem, a topic's lower-cased \`name\`, a template's path without
|
|
422
|
+
* its suffix. A file with no static name gets its path, which never repeats.
|
|
423
|
+
* @param {FixContext} context
|
|
424
|
+
* @param {string} file
|
|
425
|
+
* @param {MetadataRoot} key
|
|
426
|
+
*/
|
|
427
|
+
function identity(context, file, key) {
|
|
428
|
+
if (key === 'components') return docStem(file);
|
|
429
|
+
if (key === 'docs')
|
|
430
|
+
return stampOf(context, file)?.name?.toLowerCase() ?? file;
|
|
431
|
+
return path.join(path.dirname(file), docStem(file).toLowerCase());
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The PascalCase \`.tsx\` files directly in a folder that no doc under it
|
|
436
|
+
* covers. A components root there warns that Astryx ignores each one.
|
|
437
|
+
* @param {string} dir
|
|
438
|
+
* @param {string[]} found the doc and template files under it
|
|
439
|
+
* @returns {string[]} their file names
|
|
440
|
+
*/
|
|
441
|
+
function uncoveredSources(dir, found) {
|
|
442
|
+
const covered = new Set(
|
|
443
|
+
found.filter(file => DOC_CANDIDATE_RE.test(file)).map(docStem),
|
|
444
|
+
);
|
|
445
|
+
try {
|
|
446
|
+
return fs
|
|
447
|
+
.readdirSync(dir, {withFileTypes: true})
|
|
448
|
+
.filter(entry => entry.isFile() || entry.isSymbolicLink())
|
|
449
|
+
.map(entry => entry.name)
|
|
450
|
+
.filter(name => {
|
|
451
|
+
const match = /^([A-Z][A-Za-z0-9]+)\.tsx$/u.exec(name);
|
|
452
|
+
return match != null && !covered.has(match[1]);
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Whether a folder holds an entry by this name, compared the way a
|
|
461
|
+
* case-insensitive file system would.
|
|
462
|
+
* @param {string} dir
|
|
463
|
+
* @param {string} name
|
|
464
|
+
*/
|
|
465
|
+
function hasEntry(dir, name) {
|
|
466
|
+
const wanted = name.toLowerCase();
|
|
467
|
+
try {
|
|
468
|
+
return fs.readdirSync(dir).some(entry => entry.toLowerCase() === wanted);
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* What already holds the name \`file\` would take once moved to the top of
|
|
476
|
+
* \`root\`, or null when the name is free.
|
|
477
|
+
* @param {FixContext} context
|
|
478
|
+
* @param {string} file
|
|
479
|
+
* @param {ContributionStamp} stamp
|
|
480
|
+
* @param {string} root
|
|
481
|
+
* @returns {string | null} e.g. \`a topic named "guide" (docs/guide.doc.mjs)\`
|
|
482
|
+
*/
|
|
483
|
+
function takenIn(context, file, stamp, root) {
|
|
484
|
+
const key = ROOT_FOR_TYPE[stamp.type];
|
|
485
|
+
const stem = docStem(file);
|
|
486
|
+
const name = identity(context, path.join(root, path.basename(file)), key);
|
|
487
|
+
const same = (candidatesUnder(context, root) ?? []).find(
|
|
488
|
+
other =>
|
|
489
|
+
(key === 'templates' || DOC_CANDIDATE_RE.test(other)) &&
|
|
490
|
+
(key === 'docs'
|
|
491
|
+
? stamp.name != null &&
|
|
492
|
+
stampOf(context, other)?.name?.toLowerCase() ===
|
|
493
|
+
stamp.name.toLowerCase()
|
|
494
|
+
: identity(context, other, key) === name),
|
|
495
|
+
);
|
|
496
|
+
if (same) {
|
|
497
|
+
const kind = {
|
|
498
|
+
components: 'component',
|
|
499
|
+
docs: 'topic',
|
|
500
|
+
templates: 'template',
|
|
501
|
+
}[/** @type {'components' | 'docs' | 'templates'} */ (key)];
|
|
502
|
+
const called = key === 'docs' ? `"${stamp.name}"` : stem;
|
|
503
|
+
return `a ${kind} named ${called} (${shown(context, same)})`;
|
|
504
|
+
}
|
|
505
|
+
const clash = [path.basename(file), ...sourcesBeside(file, stamp.type)].find(
|
|
506
|
+
entry => hasEntry(root, entry),
|
|
507
|
+
);
|
|
508
|
+
return clash ? `a file named ${clash}` : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The local modules a theme source reaches, read statically the way theme
|
|
513
|
+
* discovery reads them. Null when a file in the graph cannot be parsed.
|
|
514
|
+
* @param {string} entry
|
|
515
|
+
* @returns {Array<{importer: string, specifier: string, target: string}> | null}
|
|
516
|
+
*/
|
|
517
|
+
function localImports(entry) {
|
|
518
|
+
/** @type {Array<{importer: string, specifier: string, target: string}>} */
|
|
519
|
+
const edges = [];
|
|
520
|
+
const seen = new Set();
|
|
521
|
+
const queue = [entry];
|
|
522
|
+
while (queue.length > 0) {
|
|
523
|
+
const file = /** @type {string} */ (queue.shift());
|
|
524
|
+
if (seen.has(file)) continue;
|
|
525
|
+
seen.add(file);
|
|
526
|
+
/** @type {string[]} */
|
|
527
|
+
const specifiers = [];
|
|
528
|
+
try {
|
|
529
|
+
const js = jscodeshift.withParser(
|
|
530
|
+
/\.(?:ts|tsx|mts)$/u.test(file) ? 'tsx' : 'babel',
|
|
531
|
+
);
|
|
532
|
+
const ast = js(fs.readFileSync(file, 'utf-8'));
|
|
533
|
+
/** @param {any} node */
|
|
534
|
+
const add = node => {
|
|
535
|
+
if (typeof node?.value === 'string') specifiers.push(node.value);
|
|
536
|
+
};
|
|
537
|
+
for (const type of [
|
|
538
|
+
js.ImportDeclaration,
|
|
539
|
+
js.ExportNamedDeclaration,
|
|
540
|
+
js.ExportAllDeclaration,
|
|
541
|
+
js.ImportExpression,
|
|
542
|
+
]) {
|
|
543
|
+
ast.find(type).forEach((/** @type {any} */ found) => {
|
|
544
|
+
add(found.node.source);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
ast.find(js.CallExpression).forEach((/** @type {any} */ call) => {
|
|
548
|
+
if (call.node.callee?.type === 'Import') add(call.node.arguments?.[0]);
|
|
549
|
+
});
|
|
550
|
+
} catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
for (const specifier of specifiers) {
|
|
554
|
+
if (!specifier.startsWith('.')) continue;
|
|
555
|
+
const base = path.resolve(path.dirname(file), specifier);
|
|
556
|
+
const target = [
|
|
557
|
+
base,
|
|
558
|
+
...THEME_MODULE_EXTENSIONS.map(extension => `${base}${extension}`),
|
|
559
|
+
...THEME_MODULE_EXTENSIONS.map(extension =>
|
|
560
|
+
path.join(base, `index${extension}`),
|
|
561
|
+
),
|
|
562
|
+
].find(isFile);
|
|
563
|
+
if (!target) continue;
|
|
564
|
+
edges.push({importer: file, specifier, target});
|
|
565
|
+
if (THEME_MODULE_EXTENSIONS.includes(path.extname(target))) {
|
|
566
|
+
queue.push(target);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return edges;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* A theme's imports that reach outside the folder its descriptor is in. Null
|
|
575
|
+
* when unknown.
|
|
576
|
+
* @param {string} file the theme descriptor
|
|
577
|
+
*/
|
|
578
|
+
function importsOutside(file) {
|
|
579
|
+
const [source] = sourcesBeside(file, 'theme');
|
|
580
|
+
if (!source) return [];
|
|
581
|
+
const home = path.dirname(file);
|
|
582
|
+
const edges = localImports(path.join(home, source));
|
|
583
|
+
return edges && edges.filter(edge => !pathIsInside(edge.target, home));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The sentence that brings the files a theme imports from outside its folder
|
|
588
|
+
* into the folder: discovery rejects any import that leaves it. Files move
|
|
589
|
+
* with the folder keep their paths; outside files are copied to its top.
|
|
590
|
+
* @param {FixContext} context
|
|
591
|
+
* @param {string} file the theme descriptor
|
|
592
|
+
* @param {string} destination the theme folder, as the fix names it
|
|
593
|
+
* @returns {string} a sentence with a leading space, or ''
|
|
594
|
+
*/
|
|
595
|
+
function themeImportNote(context, file, destination) {
|
|
596
|
+
const [source] = sourcesBeside(file, 'theme');
|
|
597
|
+
if (!source) return '';
|
|
598
|
+
const home = path.dirname(file);
|
|
599
|
+
const edges = localImports(path.join(home, source));
|
|
600
|
+
if (!edges) return '';
|
|
601
|
+
const copies = [
|
|
602
|
+
...new Set(
|
|
603
|
+
edges
|
|
604
|
+
.map(edge => edge.target)
|
|
605
|
+
.filter(target => !pathIsInside(target, home)),
|
|
606
|
+
),
|
|
607
|
+
];
|
|
608
|
+
if (
|
|
609
|
+
copies.length === 0 ||
|
|
610
|
+
copies.some(target => !pathIsInside(target, context.packageDir))
|
|
611
|
+
) {
|
|
612
|
+
return '';
|
|
613
|
+
}
|
|
614
|
+
const why = ': a theme can import only files inside its own folder.';
|
|
615
|
+
const listed = copies.map(target => shown(context, target));
|
|
616
|
+
const list =
|
|
617
|
+
listed.length === 1
|
|
618
|
+
? listed[0]
|
|
619
|
+
: `${listed.slice(0, -1).join(', ')} and ${listed.at(-1)}`;
|
|
620
|
+
const names = copies.map(target => path.basename(target).toLowerCase());
|
|
621
|
+
if (
|
|
622
|
+
new Set(names).size !== names.length ||
|
|
623
|
+
copies.some(target => hasEntry(home, path.basename(target)))
|
|
624
|
+
) {
|
|
625
|
+
return ` Also copy ${list} into ${destination} and point the imports of ${copies.length === 1 ? 'it at the copy' : 'them at the copies'}${why}`;
|
|
626
|
+
}
|
|
627
|
+
/** @param {string} target where a file ends up, relative to the theme folder */
|
|
628
|
+
const inTheme = target =>
|
|
629
|
+
(pathIsInside(target, home)
|
|
630
|
+
? path.relative(home, target)
|
|
631
|
+
: path.basename(target)
|
|
632
|
+
)
|
|
633
|
+
.split(path.sep)
|
|
634
|
+
.join('/');
|
|
635
|
+
/** @type {string[]} */
|
|
636
|
+
const changes = [];
|
|
637
|
+
for (const {importer, specifier, target} of edges) {
|
|
638
|
+
const extension = path.extname(specifier);
|
|
639
|
+
let next = inTheme(target);
|
|
640
|
+
if (!(extension && target.endsWith(extension))) {
|
|
641
|
+
next = next.slice(0, next.length - path.extname(next).length);
|
|
642
|
+
}
|
|
643
|
+
next = path.posix.relative(path.posix.dirname(inTheme(importer)), next);
|
|
644
|
+
if (!next.startsWith('.')) next = `./${next}`;
|
|
645
|
+
const change = `of ${specifier} in ${path.basename(importer)} to ${next}`;
|
|
646
|
+
if (next !== specifier && !changes.includes(change)) changes.push(change);
|
|
647
|
+
}
|
|
648
|
+
const changed =
|
|
649
|
+
changes.length === 1
|
|
650
|
+
? `the import ${changes[0]}`
|
|
651
|
+
: `the imports ${changes.slice(0, -1).join(', ')} and ${changes.at(-1)}`;
|
|
652
|
+
return ` Also copy ${list} into ${destination} and change ${changed}${why}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* The same-stem sources beside a doc: a component's or template's `.tsx`, or
|
|
657
|
+
* a theme's entry modules (a theme needs exactly one).
|
|
658
|
+
* @param {string} file
|
|
659
|
+
* @param {string} type
|
|
660
|
+
* @returns {string[]} their file names
|
|
661
|
+
*/
|
|
662
|
+
function sourcesBeside(file, type) {
|
|
663
|
+
if (type === 'generic') return [];
|
|
664
|
+
const dir = path.dirname(file);
|
|
665
|
+
const stem = docStem(file);
|
|
666
|
+
const names =
|
|
667
|
+
type === 'theme'
|
|
668
|
+
? THEME_MODULE_EXTENSIONS.map(extension => `${stem}${extension}`)
|
|
669
|
+
: [`${stem}.tsx`];
|
|
670
|
+
return names.filter(name => isFile(path.join(dir, name)));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** @param {string} file */
|
|
674
|
+
function docStem(file) {
|
|
675
|
+
return path
|
|
676
|
+
.basename(file)
|
|
677
|
+
.replace(DOC_CANDIDATE_RE, '')
|
|
678
|
+
.replace(TEMPLATE_CANDIDATE_RE, '');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* The sentences asking for what the doc needs wherever it ends up: a missing
|
|
683
|
+
* same-stem source, and for a theme, the descriptor's `.doc.mjs` name.
|
|
684
|
+
* @param {string} file
|
|
685
|
+
* @param {string} type
|
|
686
|
+
* @returns {string} a sentence with a leading space, or ''
|
|
687
|
+
*/
|
|
688
|
+
function sourceNote(file, type) {
|
|
689
|
+
// Theme discovery reads only `.doc.mjs` descriptors.
|
|
690
|
+
const rename =
|
|
691
|
+
type === 'theme' && !file.endsWith(THEME_DOC_SUFFIX)
|
|
692
|
+
? ` Also rename ${path.basename(file)} to ${docStem(file)}${THEME_DOC_SUFFIX}: a theme descriptor is a ${THEME_DOC_SUFFIX} file.`
|
|
693
|
+
: '';
|
|
694
|
+
if (type === 'generic' || sourcesBeside(file, type).length > 0) return rename;
|
|
695
|
+
return type === 'theme'
|
|
696
|
+
? `${rename} Also add its same-stem theme source, such as ${docStem(file)}.ts, beside it.`
|
|
697
|
+
: ` Also add ${docStem(file)}.tsx beside it.`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* How a fix names a folder.
|
|
702
|
+
* @param {FixContext} context
|
|
703
|
+
* @param {string} dir
|
|
704
|
+
*/
|
|
705
|
+
function place(context, dir) {
|
|
706
|
+
return dir === context.packageDir
|
|
707
|
+
? 'the package root'
|
|
708
|
+
: `${shown(context, dir)}/`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** @param {string} text */
|
|
712
|
+
function capitalize(text) {
|
|
713
|
+
return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Whether codemods can move into `dir`: it is missing, or holds nothing the
|
|
718
|
+
* codemod checks would read as a stray codemod or a mis-named version.
|
|
719
|
+
* @param {string} dir
|
|
720
|
+
*/
|
|
721
|
+
function holdsOnlyVersions(dir) {
|
|
722
|
+
if (!fs.existsSync(dir)) return true;
|
|
723
|
+
try {
|
|
724
|
+
return fs
|
|
725
|
+
.readdirSync(dir, {withFileTypes: true})
|
|
726
|
+
.every(entry =>
|
|
727
|
+
entry.isDirectory()
|
|
728
|
+
? isValidSemver(entry.name) || CODEMOD_SKIP_DIRS.has(entry.name)
|
|
729
|
+
: !CODEMOD_FILE_RE.test(entry.name) ||
|
|
730
|
+
CODEMOD_TEST_RE.test(entry.name),
|
|
731
|
+
);
|
|
732
|
+
} catch {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* The step that gives codemods a folder of their own, and the folder it names
|
|
739
|
+
* (null when codemods/ is taken and the author picks one).
|
|
740
|
+
* @param {FixContext} context
|
|
741
|
+
* @returns {{step: string, folder: string | null}}
|
|
742
|
+
*/
|
|
743
|
+
function ownCodemodsFolder(context) {
|
|
744
|
+
const target = path.join(context.packageDir, 'codemods');
|
|
745
|
+
return context.roots.codemods !== target && holdsOnlyVersions(target)
|
|
746
|
+
? {
|
|
747
|
+
step: `create codemods/, move each version folder into it, and set \`codemods: './codemods'\` in ${context.manifest}`,
|
|
748
|
+
folder: 'codemods',
|
|
749
|
+
}
|
|
750
|
+
: {
|
|
751
|
+
step: `move each version folder into a new folder of their own, and set \`codemods\` to that folder in ${context.manifest}`,
|
|
752
|
+
folder: null,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Declared roots other than codemods at or inside `dir`.
|
|
758
|
+
* @param {FixContext} context
|
|
759
|
+
* @param {string} dir
|
|
760
|
+
* @returns {Array<{kind: MetadataRoot, root: string}>}
|
|
761
|
+
*/
|
|
762
|
+
function metadataRootsIn(context, dir) {
|
|
763
|
+
/** @type {Array<{kind: MetadataRoot, root: string}>} */
|
|
764
|
+
const found = [];
|
|
765
|
+
for (const kind of METADATA_ROOTS) {
|
|
766
|
+
const root = context.roots[kind];
|
|
767
|
+
if (root && pathIsInside(root, dir)) found.push({kind, root});
|
|
768
|
+
}
|
|
769
|
+
return found;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* The fix for every codemods entry when the codemods root also holds the rest
|
|
774
|
+
* of the package: it is the package root, or it holds another root. No
|
|
775
|
+
* per-entry move or rename helps then.
|
|
776
|
+
* @param {FixContext} context
|
|
777
|
+
* @param {string} [stray] a stray file's name, which may itself be a codemod
|
|
778
|
+
* @returns {string | null}
|
|
779
|
+
*/
|
|
780
|
+
export function sharedCodemodsRootFix(context, stray) {
|
|
781
|
+
const root = context.roots.codemods;
|
|
782
|
+
if (!root) return null;
|
|
783
|
+
const {step, folder} = ownCodemodsFolder(context);
|
|
784
|
+
const also =
|
|
785
|
+
stray == null
|
|
786
|
+
? ''
|
|
787
|
+
: ` If it is a codemod, move it into the folder named for the version it migrates to, for example ${folder ? `${folder}/1.2.0/${stray}` : `1.2.0/${stray} in that folder`}.`;
|
|
788
|
+
if (root === context.packageDir) {
|
|
789
|
+
return `Fix: the codemods root is the package root, so everything in the package is read as a codemod or a version folder. ${capitalize(step)}.${also}`;
|
|
790
|
+
}
|
|
791
|
+
// codemods/ holding another root is fixed by moving that root out.
|
|
792
|
+
if (root === path.join(context.packageDir, 'codemods')) return null;
|
|
793
|
+
const [held] = metadataRootsIn(context, root);
|
|
794
|
+
if (!held) return null;
|
|
795
|
+
const relation =
|
|
796
|
+
held.root === root
|
|
797
|
+
? `is also the ${held.kind} root`
|
|
798
|
+
: `also holds the ${held.kind} root ${place(context, held.root)}`;
|
|
799
|
+
return `Fix: the codemods root ${place(context, root)} ${relation}. ${capitalize(step)}.${also}`;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* The fix for a codemods folder that holds another root, which a rename would
|
|
804
|
+
* break.
|
|
805
|
+
* @param {FixContext} context
|
|
806
|
+
* @param {string} folder
|
|
807
|
+
* @returns {string | null}
|
|
808
|
+
*/
|
|
809
|
+
export function heldRootFix(context, folder) {
|
|
810
|
+
const [held] = metadataRootsIn(context, folder);
|
|
811
|
+
const codemods = context.roots.codemods;
|
|
812
|
+
if (!held || !codemods) return null;
|
|
813
|
+
return `Fix: it holds the ${held.kind} root ${place(context, held.root)}; move that root out of ${place(context, codemods)} and update \`${held.kind}\` in ${context.manifest}.`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* The step that stops another root from also reading `dir`, the folder a fix
|
|
818
|
+
* puts `key` metadata in. `dir` is undefined for a folder the author picks,
|
|
819
|
+
* which only a root at the package root is sure to cover.
|
|
820
|
+
* @param {FixContext} context
|
|
821
|
+
* @param {string | undefined} dir
|
|
822
|
+
* @param {MetadataRoot} key
|
|
823
|
+
* @returns {string} a sentence with a leading space, or ''
|
|
824
|
+
*/
|
|
825
|
+
function overlapNote(context, dir, key) {
|
|
826
|
+
/** @param {string | undefined} root */
|
|
827
|
+
const covers = root =>
|
|
828
|
+
root != null &&
|
|
829
|
+
(dir === undefined ? root === context.packageDir : pathIsInside(dir, root));
|
|
830
|
+
const at = dir === undefined ? 'that folder' : place(context, dir);
|
|
831
|
+
let note = '';
|
|
832
|
+
const codemods = context.roots.codemods;
|
|
833
|
+
if (covers(codemods)) {
|
|
834
|
+
if (
|
|
835
|
+
dir !== undefined &&
|
|
836
|
+
codemods === path.join(context.packageDir, 'codemods')
|
|
837
|
+
) {
|
|
838
|
+
return ` The codemods root codemods/ also reads ${at}; move ${at} out of codemods/ and set \`${key}\` to its new path in ${context.manifest}.`;
|
|
839
|
+
}
|
|
840
|
+
const which =
|
|
841
|
+
codemods === context.packageDir
|
|
842
|
+
? 'is the package root, so it'
|
|
843
|
+
: place(context, /** @type {string} */ (codemods));
|
|
844
|
+
note = ` The codemods root ${which} also reads ${at}; ${ownCodemodsFolder(context).step}.`;
|
|
845
|
+
}
|
|
846
|
+
const own = path.join(context.packageDir, key);
|
|
847
|
+
const free = !fs.existsSync(own) && isFreeFolder(context, own);
|
|
848
|
+
for (const kind of METADATA_ROOTS) {
|
|
849
|
+
const other = context.roots[kind];
|
|
850
|
+
if (kind === key || other == null || !covers(other)) continue;
|
|
851
|
+
if (other === context.packageDir) {
|
|
852
|
+
note += ` The ${kind} root is the package root, so it also reads ${at}; create ${kind}/, move the ${ROOT_HOLDS[kind]} into it, and set \`${kind}: './${kind}'\` in ${context.manifest}.`;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
// Moving this root out of the outermost root clears every root around it.
|
|
856
|
+
if (other === dir) {
|
|
857
|
+
return free
|
|
858
|
+
? `${note} The ${kind} root is also ${at}; move the ${ROOT_HOLDS[key]} there into ${key}/ and set \`${key}: './${key}'\` in ${context.manifest}.`
|
|
859
|
+
: `${note} The ${kind} root is also ${at}; move the ${ROOT_HOLDS[key]} there into a folder of their own and set \`${key}\` to that folder in ${context.manifest}.`;
|
|
860
|
+
}
|
|
861
|
+
return free
|
|
862
|
+
? `${note} The ${kind} root ${place(context, other)} also reads ${at}; move ${at} to ${key}/ and set \`${key}: './${key}'\` in ${context.manifest}.`
|
|
863
|
+
: `${note} The ${kind} root ${place(context, other)} also reads ${at}; move ${at} out of it and set \`${key}\` to its new path in ${context.manifest}.`;
|
|
864
|
+
}
|
|
865
|
+
return note;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* @param {FixContext} context
|
|
870
|
+
* @param {string} file
|
|
871
|
+
* @param {ContributionStamp} stamp
|
|
872
|
+
*/
|
|
873
|
+
function themeSlug(context, file, stamp) {
|
|
874
|
+
const themeDir = path.dirname(file);
|
|
875
|
+
if (stamp.name) return stamp.name;
|
|
876
|
+
return themeDir === context.packageDir
|
|
877
|
+
? docStem(file)
|
|
878
|
+
: path.basename(themeDir);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* What to move for a theme: its whole folder when the folder holds only this
|
|
883
|
+
* theme and does not hold the target, else the descriptor and its source.
|
|
884
|
+
* @param {FixContext} context
|
|
885
|
+
* @param {string} file
|
|
886
|
+
* @param {string} [target] the folder it moves to
|
|
887
|
+
* @returns {{what: string, folder: boolean}}
|
|
888
|
+
*/
|
|
889
|
+
function themeFiles(context, file, target) {
|
|
890
|
+
const themeDir = path.dirname(file);
|
|
891
|
+
if (
|
|
892
|
+
themeDir !== context.packageDir &&
|
|
893
|
+
(target === undefined || !pathIsInside(target, themeDir)) &&
|
|
894
|
+
!declaredRoots(context).some(root => pathIsInside(root, themeDir))
|
|
895
|
+
) {
|
|
896
|
+
const found = candidatesUnder(context, themeDir);
|
|
897
|
+
if (found?.length === 1 && found[0] === file) {
|
|
898
|
+
return {what: `${shown(context, themeDir)}/`, folder: true};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const sources = sourcesBeside(file, 'theme');
|
|
902
|
+
const doc = path.basename(file);
|
|
903
|
+
if (sources.length === 0) return {what: doc, folder: false};
|
|
904
|
+
if (sources.length > 1) {
|
|
905
|
+
return {
|
|
906
|
+
what: `${doc} and its same-stem sources (with any local files they import)`,
|
|
907
|
+
folder: false,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
what: `${doc} and ${sources[0]} (with any local files it imports)`,
|
|
912
|
+
folder: false,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* @param {FixContext} context
|
|
918
|
+
* @param {string} file
|
|
919
|
+
* @param {ContributionStamp} stamp
|
|
920
|
+
* @param {string} root the themes root the theme moves under
|
|
921
|
+
* @param {string} [slug] the folder it takes there
|
|
922
|
+
*/
|
|
923
|
+
function moveTheme(
|
|
924
|
+
context,
|
|
925
|
+
file,
|
|
926
|
+
stamp,
|
|
927
|
+
root,
|
|
928
|
+
slug = themeSlug(context, file, stamp),
|
|
929
|
+
) {
|
|
930
|
+
const {what, folder} = themeFiles(context, file, path.join(root, slug));
|
|
931
|
+
const target = `${root === context.packageDir ? '' : `${shown(context, root)}/`}${slug}/`;
|
|
932
|
+
return folder ? `move ${what} to ${target}` : `move ${what} into ${target}`;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Moving metadata under its kind's declared root.
|
|
937
|
+
* @param {FixContext} context
|
|
938
|
+
* @param {string} file
|
|
939
|
+
* @param {ContributionStamp} stamp
|
|
940
|
+
* @param {string} root
|
|
941
|
+
* @param {string} [destination] the theme folder, as the fix names it
|
|
942
|
+
* @returns {{main: string, notes: string}}
|
|
943
|
+
*/
|
|
944
|
+
function moveUnder(context, file, stamp, root, destination) {
|
|
945
|
+
const key = ROOT_FOR_TYPE[stamp.type];
|
|
946
|
+
const overlap = overlapNote(context, root, key);
|
|
947
|
+
if (key === 'themes') {
|
|
948
|
+
const slugDir = path.join(root, themeSlug(context, file, stamp));
|
|
949
|
+
const taken = hasEntry(root, path.basename(slugDir));
|
|
950
|
+
const imports = themeImportNote(
|
|
951
|
+
context,
|
|
952
|
+
file,
|
|
953
|
+
destination ??
|
|
954
|
+
(taken ? `${place(context, root)}<slug>/` : place(context, slugDir)),
|
|
955
|
+
);
|
|
956
|
+
const notes = sourceNote(file, stamp.type) + imports + overlap;
|
|
957
|
+
if (taken) {
|
|
958
|
+
return {
|
|
959
|
+
main: `${place(context, slugDir)} is taken, so give this theme a new lower-kebab slug: ${moveTheme(context, file, stamp, root, '<slug>')} and set \`name\` in ${path.basename(file)} to <slug>`,
|
|
960
|
+
notes,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
return {main: moveTheme(context, file, stamp, root), notes};
|
|
964
|
+
}
|
|
965
|
+
const [source] = sourcesBeside(file, stamp.type);
|
|
966
|
+
const taken = takenIn(context, file, stamp, root);
|
|
967
|
+
if (taken) {
|
|
968
|
+
const name = key === 'components' ? '<Name>' : '<name>';
|
|
969
|
+
const suffix = path.basename(file).slice(docStem(file).length);
|
|
970
|
+
const renamed = source
|
|
971
|
+
? `rename it and ${source} to ${name}${suffix} and ${name}.tsx`
|
|
972
|
+
: `rename it to ${name}${suffix}`;
|
|
973
|
+
const missing =
|
|
974
|
+
stamp.type === 'generic' || source
|
|
975
|
+
? ''
|
|
976
|
+
: ` Also add ${name}.tsx beside it.`;
|
|
977
|
+
return {
|
|
978
|
+
main: `${place(context, root)} already has ${taken}, so give this one a new name: ${renamed}, set its \`name\` to ${name}, and move ${source ? 'them' : 'it'} under ${place(context, root)} (the ${key} root)`,
|
|
979
|
+
notes: missing + overlap,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
main: `move ${source ? `it and ${source}` : 'it'} under ${place(context, root)} (the ${key} root)`,
|
|
984
|
+
notes: sourceNote(file, stamp.type) + overlap,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Moving metadata whose kind has no root yet: into the conventional folder,
|
|
990
|
+
* when that folder would read cleanly as the new root.
|
|
991
|
+
* @param {FixContext} context
|
|
992
|
+
* @param {string} file
|
|
993
|
+
* @param {ContributionStamp} stamp
|
|
994
|
+
* @returns {{main: string, notes: string}}
|
|
995
|
+
*/
|
|
996
|
+
function moveToNewRoot(context, file, stamp) {
|
|
997
|
+
const key = ROOT_FOR_TYPE[stamp.type];
|
|
998
|
+
const target = path.join(context.packageDir, key);
|
|
999
|
+
const declare = `set \`${key}: './${key}'\` in ${context.manifest}`;
|
|
1000
|
+
const pointAtIt = `set \`${key}\` to that folder in ${context.manifest}`;
|
|
1001
|
+
const notes = sourceNote(file, stamp.type);
|
|
1002
|
+
const picked = notes + overlapNote(context, undefined, key);
|
|
1003
|
+
if (key === 'themes') {
|
|
1004
|
+
const themeDir = path.dirname(file);
|
|
1005
|
+
const slug = themeSlug(context, file, stamp);
|
|
1006
|
+
const slugDir = path.join(target, slug);
|
|
1007
|
+
const clear =
|
|
1008
|
+
isFreeFolder(context, target) &&
|
|
1009
|
+
readsAsThemesRoot(context, target, {
|
|
1010
|
+
except: path.dirname(themeDir) === target ? themeDir : undefined,
|
|
1011
|
+
ignore: file,
|
|
1012
|
+
fixing: file,
|
|
1013
|
+
}) &&
|
|
1014
|
+
(slugDir === themeDir || !fs.existsSync(slugDir));
|
|
1015
|
+
if (clear && slugDir === themeDir) {
|
|
1016
|
+
return {
|
|
1017
|
+
main: declare,
|
|
1018
|
+
notes: notes + themeImportNote(context, file, place(context, themeDir)),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
if (clear) {
|
|
1022
|
+
return {
|
|
1023
|
+
main: `${moveTheme(context, file, stamp, target)} and ${declare}`,
|
|
1024
|
+
notes: notes + themeImportNote(context, file, place(context, slugDir)),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
const {what, folder} = themeFiles(context, file);
|
|
1028
|
+
return {
|
|
1029
|
+
main: folder
|
|
1030
|
+
? `move ${what} into a folder that holds only themes, as ${slug}/, and ${pointAtIt}`
|
|
1031
|
+
: `move ${what} into a ${slug}/ folder inside a folder that holds only themes, and ${pointAtIt}`,
|
|
1032
|
+
notes:
|
|
1033
|
+
notes +
|
|
1034
|
+
themeImportNote(context, file, 'the theme folder') +
|
|
1035
|
+
overlapNote(context, undefined, key),
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const [source] = sourcesBeside(file, stamp.type);
|
|
1039
|
+
const what = source ? `it and ${source}` : 'it';
|
|
1040
|
+
const clear = fs.existsSync(target)
|
|
1041
|
+
? canBeRoot(context, target, key, file) &&
|
|
1042
|
+
takenIn(context, file, stamp, target) == null
|
|
1043
|
+
: isFreeFolder(context, target);
|
|
1044
|
+
if (clear) return {main: `move ${what} into ${key}/ and ${declare}`, notes};
|
|
1045
|
+
return {
|
|
1046
|
+
main: `move ${what} into a folder that holds only ${ROOT_HOLDS[key]}, and ${pointAtIt}`,
|
|
1047
|
+
notes: picked,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* The fix for contribution metadata that sits outside every declared root.
|
|
1053
|
+
* @param {FixContext} context
|
|
1054
|
+
* @param {string} file
|
|
1055
|
+
* @param {ContributionStamp} stamp
|
|
1056
|
+
* @returns {string}
|
|
1057
|
+
*/
|
|
1058
|
+
export function unreachableFix(context, file, stamp) {
|
|
1059
|
+
const key = ROOT_FOR_TYPE[stamp.type];
|
|
1060
|
+
const root = context.roots[key];
|
|
1061
|
+
// A theme is a directory under the themes root, so the root is its parent.
|
|
1062
|
+
const folder = path.dirname(key === 'themes' ? path.dirname(file) : file);
|
|
1063
|
+
const declare = `set \`${key}: './${shown(context, folder)}'\` in ${context.manifest}`;
|
|
1064
|
+
const folderCanBeRoot = canBeRoot(context, folder, key, file);
|
|
1065
|
+
const stays =
|
|
1066
|
+
sourceNote(file, stamp.type) +
|
|
1067
|
+
(key === 'themes'
|
|
1068
|
+
? themeImportNote(context, file, place(context, path.dirname(file)))
|
|
1069
|
+
: '');
|
|
1070
|
+
if (root) {
|
|
1071
|
+
// Swapping roots would orphan whatever the declared root already reads.
|
|
1072
|
+
const swappable =
|
|
1073
|
+
folderCanBeRoot && candidatesUnder(context, root)?.length === 0;
|
|
1074
|
+
if (swappable && overlapNote(context, root, key) !== '') {
|
|
1075
|
+
return `Fix: ${declare}.${stays}`;
|
|
1076
|
+
}
|
|
1077
|
+
const {main, notes} = moveUnder(
|
|
1078
|
+
context,
|
|
1079
|
+
file,
|
|
1080
|
+
stamp,
|
|
1081
|
+
root,
|
|
1082
|
+
swappable ? 'the theme folder' : undefined,
|
|
1083
|
+
);
|
|
1084
|
+
return `Fix: ${main}${swappable ? `, or ${declare}` : ''}.${notes}`;
|
|
1085
|
+
}
|
|
1086
|
+
if (folderCanBeRoot) return `Fix: ${declare}.${stays}`;
|
|
1087
|
+
const {main, notes} = moveToNewRoot(context, file, stamp);
|
|
1088
|
+
return `Fix: ${main}.${notes}`;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Whether the components can move into `dir` out of a components root at the
|
|
1093
|
+
* package root: it is missing, or holds only component docs, a doc for every
|
|
1094
|
+
* source, and no other root.
|
|
1095
|
+
* @param {FixContext} context
|
|
1096
|
+
* @param {string} dir
|
|
1097
|
+
*/
|
|
1098
|
+
function holdsOnlyComponents(context, dir) {
|
|
1099
|
+
if (!fs.existsSync(dir)) return true;
|
|
1100
|
+
const found = candidatesUnder(context, dir);
|
|
1101
|
+
return (
|
|
1102
|
+
found != null &&
|
|
1103
|
+
found.every(file => stampOf(context, file)?.type === 'component') &&
|
|
1104
|
+
uncoveredSources(dir, found).length === 0 &&
|
|
1105
|
+
!METADATA_ROOTS.some(kind => {
|
|
1106
|
+
const root = context.roots[kind];
|
|
1107
|
+
return kind !== 'components' && root != null && pathIsInside(root, dir);
|
|
1108
|
+
})
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* The step a component doc added to the components root also needs when
|
|
1114
|
+
* another root reads that folder too, and would read the new doc as its own.
|
|
1115
|
+
* @param {FixContext} context
|
|
1116
|
+
* @returns {string} a sentence with a leading space, or ''
|
|
1117
|
+
*/
|
|
1118
|
+
export function newComponentDocNote(context) {
|
|
1119
|
+
const components = context.roots.components;
|
|
1120
|
+
return components ? overlapNote(context, components, 'components') : '';
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* The theme beside \`file\` whose source imports it, if any: such a file is
|
|
1125
|
+
* part of that theme, not a codemod.
|
|
1126
|
+
* @param {FixContext} context
|
|
1127
|
+
* @param {string} file
|
|
1128
|
+
* @returns {{doc: string, stamp: ContributionStamp, source: string} | null}
|
|
1129
|
+
*/
|
|
1130
|
+
export function themeImporting(context, file) {
|
|
1131
|
+
const dir = path.dirname(file);
|
|
1132
|
+
let entries;
|
|
1133
|
+
try {
|
|
1134
|
+
entries = fs.readdirSync(dir);
|
|
1135
|
+
} catch {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
for (const name of entries.filter(entry => DOC_CANDIDATE_RE.test(entry))) {
|
|
1139
|
+
const doc = path.join(dir, name);
|
|
1140
|
+
const stamp = stampOf(context, doc);
|
|
1141
|
+
const [source] = stamp?.type === 'theme' ? sourcesBeside(doc, 'theme') : [];
|
|
1142
|
+
if (!stamp || !source) continue;
|
|
1143
|
+
const edges = localImports(path.join(dir, source));
|
|
1144
|
+
if (edges?.some(edge => edge.target === file)) return {doc, stamp, source};
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* The fix for contribution metadata at the top of the codemods root, which
|
|
1151
|
+
* the codemod checks read as a stray codemod. `source` names that doc's theme
|
|
1152
|
+
* source when the stray file is the source rather than the doc.
|
|
1153
|
+
* @param {FixContext} context
|
|
1154
|
+
* @param {string} file the metadata doc
|
|
1155
|
+
* @param {ContributionStamp} stamp
|
|
1156
|
+
* @param {string} [source]
|
|
1157
|
+
* @param {string} [stray] a file that source imports, when the stray file is
|
|
1158
|
+
* that one
|
|
1159
|
+
* @returns {string}
|
|
1160
|
+
*/
|
|
1161
|
+
export function notACodemodFix(context, file, stamp, source, stray) {
|
|
1162
|
+
const key = ROOT_FOR_TYPE[stamp.type];
|
|
1163
|
+
const root = context.roots[key];
|
|
1164
|
+
const codemods = context.roots.codemods;
|
|
1165
|
+
let lead = `Fix: ${path.basename(file)} has type: '${stamp.type}', so it is not a codemod`;
|
|
1166
|
+
if (source && stray) {
|
|
1167
|
+
lead = `Fix: ${stray} is imported by ${source}, the source of ${path.basename(file)}, which has type: '${stamp.type}', so none of them is a codemod`;
|
|
1168
|
+
} else if (source) {
|
|
1169
|
+
lead = `Fix: ${source} is the source of ${path.basename(file)}, which has type: '${stamp.type}', so neither is a codemod`;
|
|
1170
|
+
}
|
|
1171
|
+
if (root && codemods && pathIsInside(file, root)) {
|
|
1172
|
+
// Its own root reads it already; only the codemods root is in the way.
|
|
1173
|
+
const [companion] = sourcesBeside(file, stamp.type);
|
|
1174
|
+
return `${lead}; move ${path.basename(file)}${companion ? ` and ${companion}` : ''} out of ${place(context, codemods)} to another folder under ${place(context, root)} (the ${key} root).${sourceNote(file, stamp.type)}${overlapNote(context, root, key)}`;
|
|
1175
|
+
}
|
|
1176
|
+
const {main, notes} = root
|
|
1177
|
+
? moveUnder(context, file, stamp, root)
|
|
1178
|
+
: moveToNewRoot(context, file, stamp);
|
|
1179
|
+
return `${lead}; ${main}.${notes}`;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* The fix for a doc that the components root reads although its type says it
|
|
1184
|
+
* is another kind of metadata.
|
|
1185
|
+
* @param {FixContext} context
|
|
1186
|
+
* @param {string} file
|
|
1187
|
+
* @param {ContributionStamp} stamp a stamp whose type is not `component`
|
|
1188
|
+
* @returns {string}
|
|
1189
|
+
*/
|
|
1190
|
+
export function notAComponentFix(context, file, stamp) {
|
|
1191
|
+
const components = /** @type {string} */ (context.roots.components);
|
|
1192
|
+
const key = ROOT_FOR_TYPE[stamp.type];
|
|
1193
|
+
const root = context.roots[key];
|
|
1194
|
+
const lead = `Fix: ${path.basename(file)} has type: '${stamp.type}', so it is not a component`;
|
|
1195
|
+
if (components === context.packageDir) {
|
|
1196
|
+
const why = `${lead}, and the components root is the package root, which reads every doc in the package`;
|
|
1197
|
+
const target = path.join(context.packageDir, 'components');
|
|
1198
|
+
return holdsOnlyComponents(context, target)
|
|
1199
|
+
? `${why}; create components/, move the components into it, and set \`components: './components'\` in ${context.manifest}.${overlapNote(context, target, 'components')}`
|
|
1200
|
+
: `${why}; move the components into a folder that holds only components, and set \`components\` to that folder in ${context.manifest}.${overlapNote(context, undefined, 'components')}`;
|
|
1201
|
+
}
|
|
1202
|
+
if (root && pathIsInside(root, components)) {
|
|
1203
|
+
const {main, notes} = separateRoots(context, file, stamp, root);
|
|
1204
|
+
return `${lead}, and ${main}.${sourceNote(file, stamp.type)}${notes}`;
|
|
1205
|
+
}
|
|
1206
|
+
if (root && pathIsInside(file, root)) {
|
|
1207
|
+
return `${lead}; move it out of ${place(context, components)} (the components root) to another folder under ${place(context, root)} (the ${key} root).${sourceNote(file, stamp.type)}${overlapNote(context, root, key)}`;
|
|
1208
|
+
}
|
|
1209
|
+
const {main, notes} = root
|
|
1210
|
+
? moveUnder(context, file, stamp, root)
|
|
1211
|
+
: moveToNewRoot(context, file, stamp);
|
|
1212
|
+
return `${lead}; ${main}.${notes}`;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* The fix when a kind's root sits inside the components root, so nothing
|
|
1217
|
+
* placed in it escapes the components root.
|
|
1218
|
+
* @param {FixContext} context
|
|
1219
|
+
* @param {string} file
|
|
1220
|
+
* @param {ContributionStamp} stamp
|
|
1221
|
+
* @param {string} root the declared root for this kind
|
|
1222
|
+
* @returns {{main: string, notes: string}}
|
|
1223
|
+
*/
|
|
1224
|
+
function separateRoots(context, file, stamp, root) {
|
|
1225
|
+
const components = /** @type {string} */ (context.roots.components);
|
|
1226
|
+
const key = ROOT_FOR_TYPE[stamp.type];
|
|
1227
|
+
const target = path.join(context.packageDir, key);
|
|
1228
|
+
const free = !fs.existsSync(target) && isFreeFolder(context, target);
|
|
1229
|
+
const picked = overlapNote(context, undefined, key);
|
|
1230
|
+
if (root === components) {
|
|
1231
|
+
const both = `the ${key} root and the components root are both ${place(context, root)}`;
|
|
1232
|
+
return free
|
|
1233
|
+
? {
|
|
1234
|
+
main: `${both}; move the ${ROOT_HOLDS[key]} into ${key}/ and set \`${key}: './${key}'\` in ${context.manifest}`,
|
|
1235
|
+
notes: '',
|
|
1236
|
+
}
|
|
1237
|
+
: {
|
|
1238
|
+
main: `${both}; move the ${ROOT_HOLDS[key]} into a folder of their own and set \`${key}\` to that folder in ${context.manifest}`,
|
|
1239
|
+
notes: picked,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const overlap = `the components root ${place(context, components)} also reads the ${key} root ${place(context, root)} inside it`;
|
|
1243
|
+
const carried = pathIsInside(file, root);
|
|
1244
|
+
// The root's contents move with it, so a name it holds is still taken.
|
|
1245
|
+
const slug = themeSlug(context, file, stamp);
|
|
1246
|
+
const taken =
|
|
1247
|
+
!carried &&
|
|
1248
|
+
(key === 'themes'
|
|
1249
|
+
? hasEntry(root, slug)
|
|
1250
|
+
: takenIn(context, file, stamp, root) != null);
|
|
1251
|
+
const [source] = sourcesBeside(file, stamp.type);
|
|
1252
|
+
const name = '<name>';
|
|
1253
|
+
const suffix = path.basename(file).slice(docStem(file).length);
|
|
1254
|
+
const renamed = `give it a new name: ${source ? `rename it and ${source} to ${name}${suffix} and ${name}.tsx` : `rename it to ${name}${suffix}`}, set its \`name\` to ${name}, and move ${source ? 'them' : 'it'}`;
|
|
1255
|
+
const newSlug = taken ? '<slug>' : slug;
|
|
1256
|
+
const slugNote = taken
|
|
1257
|
+
? ` and set \`name\` in ${path.basename(file)} to <slug>`
|
|
1258
|
+
: '';
|
|
1259
|
+
if (free) {
|
|
1260
|
+
let then = '';
|
|
1261
|
+
if (!carried && key === 'themes') {
|
|
1262
|
+
then = `, then ${moveTheme(context, file, stamp, target, newSlug)}${slugNote}`;
|
|
1263
|
+
} else if (!carried) {
|
|
1264
|
+
then = `, then ${taken ? renamed : 'move it'} into ${key}/`;
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
main: `${overlap}; move ${place(context, root)} to ${key}/ and set \`${key}: './${key}'\` in ${context.manifest}${then}`,
|
|
1268
|
+
notes: '',
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
let then = '';
|
|
1272
|
+
if (!carried && key === 'themes') {
|
|
1273
|
+
const {what, folder} = themeFiles(context, file, root);
|
|
1274
|
+
then = folder
|
|
1275
|
+
? `, then move ${what} into that root as ${newSlug}/${slugNote}`
|
|
1276
|
+
: `, then move ${what} into a ${newSlug}/ folder in that root${slugNote}`;
|
|
1277
|
+
} else if (!carried) {
|
|
1278
|
+
then = `, then ${taken ? renamed : 'move it'} into that root`;
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
main: `${overlap}; move ${place(context, root)} out of ${place(context, components)} and set \`${key}\` to its new path in ${context.manifest}${then}`,
|
|
1282
|
+
notes: picked,
|
|
1283
|
+
};
|
|
1284
|
+
}
|