@astryxdesign/cli 0.6.4 → 0.6.5-canary.00f1ed9
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 +56 -0
- package/README.md +103 -95
- package/api/build/_adapter.d.mts +36 -2
- package/api/build/_adapter.mjs +41 -10
- package/api/build/build.doc.mjs +8 -3
- package/api/build/build.test.mjs +60 -2
- package/api/build/kit/kit.mjs +109 -26
- package/api/build/kit/rank.d.mts +24 -8
- package/api/build/kit/rank.mjs +277 -97
- package/api/build/kit/rank.test.mjs +231 -48
- package/api/component/_adapter.d.mts +25 -0
- package/api/component/_adapter.mjs +59 -5
- package/api/component/component.d.mts +6 -3
- package/api/component/component.doc.mjs +37 -17
- package/api/component/component.mjs +249 -9
- package/api/component/component.type.d.mts +25 -0
- package/api/component/component.type.mjs +44 -0
- 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 +61 -18
- package/api/discover/discover.mjs +220 -36
- package/api/discover/discover.test.mjs +11 -2
- package/api/discover/discover.type.d.mts +147 -8
- package/api/discover/discover.type.mjs +102 -12
- 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 +8 -3
- package/api/docs/_adapter.mjs +14 -6
- package/api/docs/docOverlays.test.mjs +27 -1
- package/api/docs/docs.doc.mjs +2 -2
- package/api/docs/docs.test.mjs +54 -18
- package/api/docs/integration-tree.test.mjs +17 -0
- package/api/docs/integrationDocs.test.mjs +27 -5
- package/api/doctor/doctor.d.mts +8 -3
- package/api/doctor/doctor.doc.mjs +17 -8
- package/api/doctor/doctor.mjs +90 -9
- package/api/doctor/doctor.test.mjs +122 -10
- package/api/doctor/doctor.type.d.mts +1 -1
- package/api/doctor/doctor.type.mjs +1 -1
- package/api/error.d.mts +22 -0
- package/api/error.mjs +42 -0
- package/api/gap-report/gap-report.doc.mjs +19 -10
- package/api/hook/hook.doc.mjs +6 -3
- package/api/index.d.mts +1 -0
- package/api/index.mjs +5 -3
- package/api/init/init.doc.mjs +17 -12
- package/api/integration/add-contribution.d.mts +2 -1
- package/api/integration/add-contribution.mjs +7 -3
- package/api/integration/add-contribution.test.mjs +3 -3
- package/api/integration/add-helpers.d.mts +5 -2
- package/api/integration/add-helpers.mjs +36 -9
- package/api/integration/add-theme.mjs +266 -23
- package/api/integration/add-theme.test.mjs +247 -0
- package/api/integration/authoring-checks.mjs +12 -9
- package/api/integration/authoring-checks.type.d.mts +8 -0
- package/api/integration/authoring-checks.type.mjs +7 -3
- package/api/integration/integration-authoring.type.d.mts +4 -1
- package/api/integration/integration-authoring.type.mjs +6 -1
- package/api/integration/integrationAdd.doc.mjs +6 -0
- package/api/integration/integrationAddTheme.doc.mjs +10 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +1 -1
- package/api/integration/integrationDocConflicts.doc.mjs +1 -1
- package/api/integration/integrationPackCheck.doc.mjs +3 -3
- package/api/integration/integrationTemplateConflicts.doc.mjs +1 -1
- package/api/integration/pack-check.lifecycle-output.test.mjs +107 -0
- package/api/integration/pack-check.mjs +92 -10
- package/api/integration/pack-check.test.mjs +140 -1
- package/api/integration/pack-check.type.mjs +1 -1
- package/api/integration/validate-integration.d.mts +4 -2
- package/api/integration/validate-integration.mjs +7 -2
- package/api/integration/validate-integration.test.mjs +55 -0
- package/api/integration/validate-integration.type.d.mts +5 -0
- package/api/integration/validate-integration.type.mjs +5 -1
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/json/assertResponse.doc.mjs +1 -1
- package/api/json/isError.doc.mjs +1 -1
- package/api/layout/expand/expand.mjs +12 -7
- package/api/layout/expand/expand.receipt.test.mjs +74 -0
- package/api/layout/layout.type.d.mts +1 -0
- package/api/layout/layout.type.mjs +1 -0
- package/api/layout/layoutExpand.doc.mjs +1 -1
- package/api/search/search.d.mts +51 -1
- package/api/search/search.doc.mjs +2 -2
- package/api/search/search.mjs +299 -17
- package/api/search/search.test.mjs +216 -18
- package/api/swizzle/copy/copy.mjs +66 -3
- package/api/swizzle/swizzle.doc.mjs +11 -5
- package/api/template/copy/copy.mjs +15 -9
- package/api/template/copy/copy.receipt.test.mjs +77 -0
- package/api/template/copy/copy.test.mjs +9 -0
- package/api/template/show/show.mjs +15 -4
- package/api/template/show/show.test.mjs +76 -0
- package/api/template/template-integration.test.mjs +14 -0
- package/api/template/template.d.mts +1 -1
- package/api/template/template.doc.mjs +8 -3
- package/api/template/template.mjs +1 -0
- package/api/template/template.type.d.mts +2 -0
- package/api/template/template.type.mjs +2 -0
- package/api/theme/add/add.mjs +17 -25
- package/api/theme/add/add.rollback.test.mjs +158 -0
- package/api/theme/add/add.staging.test.mjs +40 -23
- package/api/theme/build/build.d.mts +24 -0
- package/api/theme/build/build.family.test.mjs +7 -12
- package/api/theme/build/build.mjs +243 -26
- package/api/theme/build/build.project-core.test.mjs +165 -0
- package/api/theme/build/build.rollback.test.mjs +148 -0
- package/api/theme/generateTonalPalette.doc.mjs +1 -2
- package/api/theme/listThemes.doc.mjs +1 -1
- package/api/theme/themeAdd.doc.mjs +9 -10
- package/api/theme/themeBuild.doc.mjs +13 -13
- package/api/theme/themeList.doc.mjs +1 -1
- package/api/theme/themeListAvailable.doc.mjs +2 -1
- package/api/theme/themePaletteGenerate.doc.mjs +15 -8
- package/api/theme/themeTargets.doc.mjs +3 -2
- package/api/theme/themeTemplate.doc.mjs +2 -1
- package/api/upgrade/run/files-changed.test.mjs +111 -0
- package/api/upgrade/run/run.mjs +25 -6
- package/api/upgrade/run/run.test.mjs +45 -1
- package/api/upgrade/upgrade.doc.mjs +24 -22
- package/api/upgrade/upgrade.type.d.mts +1 -0
- package/api/upgrade/upgrade.type.mjs +3 -2
- package/assets/codemods/__tests__/runner.test.mjs +3 -1
- package/assets/codemods/file-count.test.mjs +163 -0
- package/assets/codemods/integration-runner.mjs +3 -3
- package/assets/codemods/runner.mjs +5 -4
- package/assets/docs/README.md +4 -2
- package/assets/docs/browser-support.doc.mjs +11 -11
- package/assets/docs/color.doc.mjs +8 -2
- package/assets/docs/elevation.doc.mjs +6 -4
- package/assets/docs/getting-started.doc.mjs +5 -16
- package/assets/docs/icons.doc.mjs +3 -21
- package/assets/docs/illustrations.doc.mjs +7 -15
- package/assets/docs/internationalization.doc.mjs +7 -5
- package/assets/docs/layout.doc.dense.mjs +130 -82
- package/assets/docs/layout.doc.mjs +133 -77
- package/assets/docs/migration.doc.mjs +19 -21
- package/assets/docs/motion.doc.mjs +16 -3
- package/assets/docs/principles.doc.dense.mjs +5 -5
- package/assets/docs/principles.doc.mjs +8 -0
- package/assets/docs/principles.doc.zh.mjs +6 -6
- package/assets/docs/shape.doc.mjs +8 -3
- package/assets/docs/spacing.doc.mjs +7 -2
- package/assets/docs/styling-libraries.doc.mjs +6 -2
- package/assets/docs/styling.doc.mjs +19 -23
- package/assets/docs/theme.doc.dense.mjs +58 -18
- package/assets/docs/theme.doc.mjs +57 -47
- package/assets/docs/theme.doc.zh.mjs +9 -8
- package/assets/docs/tokens.doc.dense.mjs +2 -2
- package/assets/docs/tokens.doc.mjs +389 -8
- package/assets/docs/tokens.doc.zh.mjs +2 -2
- package/assets/docs/tree/add-a-component.doc.mjs +75 -0
- package/assets/docs/tree/add-a-theme.doc.mjs +85 -0
- package/assets/docs/tree/add-a-topic.doc.mjs +144 -0
- package/assets/docs/tree/agent-guidance.doc.mjs +138 -0
- package/assets/docs/tree/block-template.doc.mjs +130 -0
- package/assets/docs/tree/build-the-template.doc.mjs +28 -0
- package/assets/docs/tree/building-blocks.doc.mjs +46 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +137 -0
- package/assets/docs/tree/checks.doc.mjs +119 -0
- package/assets/docs/tree/codemods.doc.mjs +147 -0
- package/assets/docs/tree/component-family.doc.mjs +113 -0
- package/assets/docs/tree/component-imports.doc.mjs +69 -0
- package/assets/docs/tree/component-lookups.doc.mjs +149 -0
- package/assets/docs/tree/components.doc.mjs +23 -0
- package/assets/docs/tree/configuration.doc.mjs +23 -0
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +182 -0
- package/assets/docs/tree/define-the-theme.doc.mjs +118 -0
- package/assets/docs/tree/describe-the-component.doc.mjs +57 -0
- package/assets/docs/tree/docs.doc.mjs +21 -0
- package/assets/docs/tree/document-the-template.doc.mjs +28 -0
- package/assets/docs/tree/document-the-theme.doc.mjs +68 -0
- package/assets/docs/tree/export-template-assets.doc.mjs +147 -0
- package/assets/docs/tree/extend-or-replace.doc.mjs +103 -0
- package/assets/docs/tree/fonts-and-assets.doc.mjs +106 -0
- package/assets/docs/tree/generate-a-palette.doc.mjs +66 -0
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +105 -0
- package/assets/docs/tree/help.doc.mjs +16 -0
- package/assets/docs/tree/integrations.doc.mjs +25 -451
- package/assets/docs/tree/links.doc.mjs +98 -0
- package/assets/docs/tree/package-and-test.doc.mjs +32 -0
- package/assets/docs/tree/page-template.doc.mjs +71 -0
- package/assets/docs/tree/publishing.doc.mjs +111 -0
- package/assets/docs/tree/quick-start.doc.mjs +272 -0
- package/assets/docs/tree/replace-a-core-component.doc.mjs +104 -0
- package/assets/docs/tree/replace-a-core-template.doc.mjs +172 -0
- package/assets/docs/tree/sections-and-placement.doc.mjs +108 -0
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +59 -0
- package/assets/docs/tree/ship.doc.mjs +16 -0
- package/assets/docs/tree/short-and-findable.doc.mjs +108 -0
- package/assets/docs/tree/single-component.doc.mjs +165 -0
- package/assets/docs/tree/start-a-template.doc.mjs +143 -0
- package/assets/docs/tree/subcomponent.doc.mjs +115 -0
- package/assets/docs/tree/template-assets.doc.mjs +64 -0
- package/assets/docs/tree/template-doc-overview.doc.mjs +121 -0
- package/assets/docs/tree/template-fonts.doc.mjs +102 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +452 -0
- package/assets/docs/tree/template-icons.doc.mjs +97 -0
- package/assets/docs/tree/template-images-media.doc.mjs +127 -0
- package/assets/docs/tree/template-styles.doc.mjs +93 -0
- package/assets/docs/tree/templates.doc.mjs +34 -0
- package/assets/docs/tree/test-in-an-app.doc.mjs +115 -0
- package/assets/docs/tree/test-template-in-app.doc.mjs +128 -0
- package/assets/docs/tree/themes.doc.mjs +39 -0
- package/assets/docs/tree/troubleshooting.doc.mjs +153 -0
- package/assets/docs/tree/upgrading.doc.mjs +103 -0
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +51 -0
- package/assets/docs/tree/verify-packed-template.doc.mjs +77 -0
- package/assets/docs/tree/versioning.doc.mjs +162 -0
- package/assets/docs/tree/write-good-templates.doc.mjs +64 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +154 -0
- package/assets/docs/typography.doc.mjs +24 -4
- package/assets/docs/working-with-ai.doc.mjs +30 -22
- package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
- package/assets/templates/pages/ai-chat/template.doc.mjs +16 -1
- package/assets/templates/pages/ai-chat-landing/template.doc.mjs +9 -1
- package/assets/templates/pages/blank/template.doc.mjs +3 -1
- package/assets/templates/pages/canvas-editor/template.doc.mjs +9 -1
- package/assets/templates/pages/centered-hero/template.doc.mjs +3 -1
- package/assets/templates/pages/checkout-wizard/template.doc.mjs +1 -0
- package/assets/templates/pages/classic-gallery/template.doc.mjs +3 -1
- package/assets/templates/pages/contact-form/template.doc.mjs +16 -1
- package/assets/templates/pages/dashboard/template.doc.mjs +15 -1
- package/assets/templates/pages/dashboard-alert-rail/template.doc.mjs +23 -1
- package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +10 -1
- package/assets/templates/pages/dashboard-comparison/template.doc.mjs +9 -1
- package/assets/templates/pages/dashboard-composition/template.doc.mjs +10 -1
- package/assets/templates/pages/dashboard-progress/template.doc.mjs +17 -1
- package/assets/templates/pages/dashboard-scorecard/template.doc.mjs +2 -1
- package/assets/templates/pages/detail-page/template.doc.mjs +8 -0
- package/assets/templates/pages/documentation/template.doc.mjs +10 -1
- package/assets/templates/pages/documentation-design/template.doc.mjs +10 -1
- package/assets/templates/pages/documentation-technical/template.doc.mjs +10 -1
- package/assets/templates/pages/editor/template.doc.mjs +9 -1
- package/assets/templates/pages/file-explorer/template.doc.mjs +3 -1
- package/assets/templates/pages/form-two-column/template.doc.mjs +17 -1
- package/assets/templates/pages/form-wizard/template.doc.mjs +14 -1
- package/assets/templates/pages/form-wizard-dialog/template.doc.mjs +1 -0
- package/assets/templates/pages/gallery-hero/template.doc.mjs +10 -1
- package/assets/templates/pages/ide/template.doc.mjs +9 -1
- package/assets/templates/pages/incident-console/template.doc.mjs +10 -1
- package/assets/templates/pages/kanban-board/template.doc.mjs +11 -1
- package/assets/templates/pages/library/template.doc.mjs +15 -1
- package/assets/templates/pages/login/template.doc.mjs +10 -1
- package/assets/templates/pages/login-card/template.doc.mjs +11 -1
- package/assets/templates/pages/login-split/template.doc.mjs +10 -1
- package/assets/templates/pages/login-sso/template.doc.mjs +10 -1
- package/assets/templates/pages/messaging-shell/template.doc.mjs +11 -1
- package/assets/templates/pages/mixed-gallery/template.doc.mjs +10 -1
- package/assets/templates/pages/payment-form/template.doc.mjs +3 -1
- package/assets/templates/pages/product-detail/template.doc.mjs +9 -1
- package/assets/templates/pages/product-gallery/template.doc.mjs +10 -1
- package/assets/templates/pages/settings/template.doc.mjs +3 -1
- package/assets/templates/pages/settings-dialog/template.doc.mjs +9 -1
- package/assets/templates/pages/settings-sidebar/template.doc.mjs +9 -1
- package/assets/templates/pages/shell-nav/template.doc.mjs +10 -1
- package/assets/templates/pages/shell-side-nav/template.doc.mjs +14 -1
- package/assets/templates/pages/shell-top-nav/template.doc.mjs +11 -1
- package/assets/templates/pages/side-gallery/template.doc.mjs +3 -1
- package/assets/templates/pages/table/template.doc.mjs +11 -1
- package/assets/templates/pages/table-filter/template.doc.mjs +21 -1
- package/assets/templates/pages/table-grouped/template.doc.mjs +15 -1
- package/assets/templates/pages/table-inbox/template.doc.mjs +18 -6
- package/assets/templates/pages/table-page/template.doc.mjs +20 -1
- package/assets/templates/pages/table-tree/template.doc.mjs +14 -1
- package/assets/templates/pages/theme-showcase/template.doc.mjs +10 -1
- package/assets/templates/pages/work-item-detail/template.doc.mjs +10 -0
- package/assets/templates/themes/butter/icons.tsx +2 -0
- package/assets/templates/themes/chocolate/icons.tsx +2 -0
- package/assets/templates/themes/gothic/icons.tsx +2 -0
- package/assets/templates/themes/matcha/icons.tsx +2 -0
- package/assets/templates/themes/neutral/icons.tsx +2 -0
- package/assets/templates/themes/stone/icons.tsx +2 -0
- package/assets/templates/themes/y2k/icons.tsx +2 -0
- package/authoring/config/config.doc.mjs +9 -1
- package/authoring/config/parse.d.mts +2 -0
- package/authoring/config/parse.mjs +19 -0
- package/authoring/config/parse.test.mjs +8 -0
- package/authoring/config/type.ts +11 -0
- 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 +3 -2
- package/authoring/doctypes/_schema.mjs +6 -0
- package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
- package/authoring/doctypes/base/type.ts +4 -2
- package/authoring/doctypes/component/component.doc.mjs +6 -0
- package/authoring/doctypes/component/type.ts +8 -0
- package/authoring/doctypes/reference/reference.doc.mjs +7 -0
- package/authoring/doctypes/reference/type.ts +5 -0
- package/authoring/doctypes/schema/schema.doc.mjs +2 -2
- package/authoring/doctypes/template/parse.d.mts +2 -0
- package/authoring/doctypes/template/parse.mjs +1 -0
- package/authoring/doctypes/template/parse.test.mjs +21 -0
- package/authoring/doctypes/template/template.doc.mjs +7 -1
- package/authoring/doctypes/template/type.ts +12 -2
- package/authoring/index.d.mts +1 -0
- package/authoring/index.d.ts +10 -0
- package/authoring/index.mjs +1 -0
- package/authoring/integration/integration.doc.mjs +12 -10
- package/clients/cli/commands/component/index.mjs +152 -55
- package/clients/cli/commands/component-batch.test.mjs +341 -0
- package/clients/cli/commands/component-ownership.test.mjs +89 -0
- package/clients/cli/commands/component.doc.mjs +27 -9
- package/clients/cli/commands/discover.doc.mjs +53 -9
- package/clients/cli/commands/discover.mjs +393 -118
- package/clients/cli/commands/discover.sources.test.mjs +267 -0
- package/clients/cli/commands/docs.doc.mjs +1 -1
- package/clients/cli/commands/docs.mjs +60 -17
- package/clients/cli/commands/docs.test.mjs +113 -24
- package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -2
- package/clients/cli/commands/doctor-integration.test.mjs +53 -0
- package/clients/cli/commands/doctor.doc.mjs +3 -1
- package/clients/cli/commands/doctor.mjs +53 -9
- package/clients/cli/commands/gap-report.doc.mjs +10 -9
- package/clients/cli/commands/init.doc.mjs +9 -6
- package/clients/cli/commands/integration-add.doc.mjs +17 -7
- package/clients/cli/commands/integration-authoring.test.mjs +70 -9
- package/clients/cli/commands/integration-pack.doc.mjs +5 -9
- package/clients/cli/commands/integration-real-world.test.mjs +1 -1
- package/clients/cli/commands/integration-verify.doc.mjs +22 -0
- package/clients/cli/commands/integration.doc.mjs +4 -4
- package/clients/cli/commands/integration.mjs +76 -43
- package/clients/cli/commands/layout-expand.doc.mjs +3 -1
- package/clients/cli/commands/layout.expand-receipt.test.mjs +94 -0
- package/clients/cli/commands/layout.mjs +16 -0
- package/clients/cli/commands/manifest.doc.mjs +1 -1
- package/clients/cli/commands/search.doc.mjs +10 -3
- package/clients/cli/commands/search.mjs +21 -2
- package/clients/cli/commands/search.test.mjs +21 -4
- package/clients/cli/commands/swizzle.doc.mjs +1 -1
- package/clients/cli/commands/template.copy-receipt.test.mjs +60 -0
- package/clients/cli/commands/template.doc.mjs +1 -1
- package/clients/cli/commands/template.mjs +19 -5
- package/clients/cli/commands/template.show-media.test.mjs +62 -0
- package/clients/cli/commands/text-json-parity.test.mjs +7 -1
- package/clients/cli/commands/theme-add.doc.mjs +1 -1
- package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -2
- package/clients/cli/commands/theme-palette.doc.mjs +1 -2
- package/clients/cli/commands/theme-targets.doc.mjs +2 -2
- package/clients/cli/commands/theme.doc.mjs +2 -1
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +14 -0
- package/clients/cli/commands/upgrade.doc.mjs +62 -3
- package/clients/cli/commands/write-failure.test.mjs +175 -0
- package/clients/cli/index.mjs +28 -6
- package/clients/cli/lib/define-command.mjs +28 -4
- package/clients/cli/lib/define-command.test.mjs +54 -0
- package/clients/cli/lib/exit-codes.test.mjs +17 -1
- package/clients/cli/lib/json-shim.mjs +24 -14
- package/clients/cli/lib/manifest.mjs +48 -156
- package/clients/cli/lib/manifest.test.mjs +103 -9
- package/clients/cli/lib/parse-error-format.test.mjs +81 -0
- package/foundation/agent-docs/agent-docs.mjs +1 -1
- package/foundation/agent-docs/agent-docs.test.mjs +3 -2
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/authoring-self-docs.test.mjs +6 -2
- package/foundation/discovery/cli-self-docs.mjs +16 -2
- package/foundation/discovery/cli-self-docs.test.mjs +20 -0
- package/foundation/discovery/docs-discovery.mjs +5 -1
- package/foundation/discovery/docs-discovery.test.mjs +21 -0
- package/foundation/discovery/docs-section-key.d.mts +1 -1
- package/foundation/discovery/docs-section-key.mjs +1 -1
- package/foundation/discovery/template-adapter.d.mts +14 -0
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +37 -1
- package/foundation/discovery/template-adapter.mjs +21 -1
- package/foundation/doc-compiler/doc-loads.test.mjs +5 -4
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/doc-compiler/tree.d.mts +4 -0
- package/foundation/doc-compiler/tree.mjs +6 -1
- package/foundation/doc-compiler/tree.test.mjs +65 -14
- package/foundation/integrations/cli-requirement.d.mts +75 -11
- package/foundation/integrations/cli-requirement.mjs +120 -23
- package/foundation/integrations/cli-requirement.test.mjs +141 -9
- package/foundation/integrations/contribution-inventory.mjs +1 -1
- package/foundation/integrations/integrations.d.mts +14 -1
- package/foundation/integrations/integrations.mjs +41 -1
- package/foundation/integrations/integrations.test.mjs +31 -0
- package/foundation/response/batch.type.d.mts +33 -0
- package/foundation/response/batch.type.mjs +34 -0
- package/foundation/response/error-codes.doc.mjs +6 -8
- package/foundation/response/error-codes.test.mjs +30 -5
- package/foundation/response/response-types.doc.d.mts +5 -4
- package/foundation/response/response-types.doc.mjs +49 -19
- package/foundation/response/response-types.doc.test.mjs +23 -0
- package/foundation/response/response.doc.mjs +11 -10
- package/package.json +9 -9
- package/assets/docs/tree/integrations.test.mjs +0 -62
- package/assets/docs/tree/writing-docs.doc.mjs +0 -286
|
@@ -24,7 +24,10 @@ import * as fs from 'node:fs';
|
|
|
24
24
|
import * as os from 'node:os';
|
|
25
25
|
import * as path from 'node:path';
|
|
26
26
|
import {fileURLToPath} from 'node:url';
|
|
27
|
+
import {docs} from '../docs/docs.mjs';
|
|
27
28
|
import {
|
|
29
|
+
headingWithPhrase,
|
|
30
|
+
titleInQuery,
|
|
28
31
|
search,
|
|
29
32
|
scoreCandidate,
|
|
30
33
|
scoreQuery,
|
|
@@ -98,17 +101,18 @@ describe('search leaf — docs at the grain a reader reads them', () => {
|
|
|
98
101
|
it(
|
|
99
102
|
'finds one section of a guide, and a docs-tree leaf by its own name',
|
|
100
103
|
async () => {
|
|
101
|
-
const guide = await search('codemod
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
104
|
+
const guide = await search('when a codemod runs', {cwd, type: 'doc'});
|
|
105
|
+
// Found by the section's own title, wherever the guide sits in the tree.
|
|
106
|
+
const hit = guide.data.results
|
|
107
|
+
.slice(0, 3)
|
|
108
|
+
.find(result => result.section === 'which-codemods-run');
|
|
109
|
+
expect(hit).toMatchObject({
|
|
110
|
+
domain: 'doc',
|
|
111
|
+
name: expect.stringMatching(/^cli\/integrations\/(?:.+\/)?codemods$/),
|
|
112
|
+
title: expect.stringMatching(/ › Codemods › Choose when a codemod runs$/),
|
|
113
|
+
});
|
|
114
|
+
expect(hit.parent).toBe(`astryx docs ${hit.name} --index`);
|
|
115
|
+
expect(hit.command).toBe(`astryx docs ${hit.name} which-codemods-run`);
|
|
112
116
|
const block = await search('token-ref', {cwd});
|
|
113
117
|
expect(block.data.results[0]).toMatchObject({
|
|
114
118
|
name: 'authoring',
|
|
@@ -127,6 +131,30 @@ describe('search leaf — docs at the grain a reader reads them', () => {
|
|
|
127
131
|
SLOW,
|
|
128
132
|
);
|
|
129
133
|
|
|
134
|
+
it(
|
|
135
|
+
'finds the integration guides for the ways people ask to make one',
|
|
136
|
+
async () => {
|
|
137
|
+
// "make", "build", and "an" are stopwords, so each of the first three
|
|
138
|
+
// tokenizes to `integration` alone; the phrase still matches the
|
|
139
|
+
// keywords and the title the guides declare. A namespace's own
|
|
140
|
+
// keywords count, and a plural name is the name.
|
|
141
|
+
for (const [query, route] of [
|
|
142
|
+
['make an integration', /^cli\/integrations$/],
|
|
143
|
+
['build an integration', /^cli\/integrations$/],
|
|
144
|
+
['create an integration', /^cli\/integrations$/],
|
|
145
|
+
['integration', /^cli\/integrations$/],
|
|
146
|
+
['publish an integration', /^cli\/integrations$/],
|
|
147
|
+
// The troubleshooting guide, wherever the tree places it.
|
|
148
|
+
['troubleshoot integration', /^cli\/integrations\/(?:.+\/)?troubleshooting$/],
|
|
149
|
+
]) {
|
|
150
|
+
const r = await search(query, {cwd, type: 'doc'});
|
|
151
|
+
const names = r.data.results.slice(0, 3).map(result => result.name);
|
|
152
|
+
expect(names.some(name => route.test(name)), `${query}: ${names.join(', ')}`).toBe(true);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
SLOW,
|
|
156
|
+
);
|
|
157
|
+
|
|
130
158
|
it(
|
|
131
159
|
'gives a top-level namespace hit the topic list as its parent',
|
|
132
160
|
async () => {
|
|
@@ -147,10 +175,15 @@ describe('search leaf — docs at the grain a reader reads them', () => {
|
|
|
147
175
|
it(
|
|
148
176
|
'points a topic hit at its index, never a whole-topic read',
|
|
149
177
|
async () => {
|
|
150
|
-
|
|
178
|
+
// A guide the tree places, read from the tree rather than named.
|
|
179
|
+
const {data: integrations} = await docs('cli/integrations');
|
|
180
|
+
const guide = integrations.slots
|
|
181
|
+
.flatMap(slot => slot.children)
|
|
182
|
+
.find(child => child.kind === 'generic').route;
|
|
183
|
+
const r = await search(guide, {cwd, type: 'doc'});
|
|
151
184
|
expect(r.data.results[0]).toMatchObject({
|
|
152
|
-
name:
|
|
153
|
-
command:
|
|
185
|
+
name: guide,
|
|
186
|
+
command: `astryx docs ${guide} --index`,
|
|
154
187
|
});
|
|
155
188
|
expect(r.data.results[0]).not.toHaveProperty('section');
|
|
156
189
|
},
|
|
@@ -253,6 +286,163 @@ describe('search leaf — exact keyword phrase outranks incidental token matches
|
|
|
253
286
|
}, SLOW);
|
|
254
287
|
});
|
|
255
288
|
|
|
289
|
+
describe('search leaf — a whole-query phrase in a title or heading is top tier', () => {
|
|
290
|
+
/**
|
|
291
|
+
* @param {string} q
|
|
292
|
+
* @param {object} candidate
|
|
293
|
+
* @returns {number}
|
|
294
|
+
*/
|
|
295
|
+
const score = (q, candidate) => scoreQuery(q, tokenizeQuery(q), candidate)?.score ?? 0;
|
|
296
|
+
|
|
297
|
+
it('finds the whole query, in order, inside a title or heading', () => {
|
|
298
|
+
expect(headingWithPhrase('dark mode', ['Light/Dark Mode'])).toBe('Light/Dark Mode');
|
|
299
|
+
expect(headingWithPhrase('nested theme', ['Theme Props', 'Nested themes'])).toBe(
|
|
300
|
+
'Nested themes',
|
|
301
|
+
);
|
|
302
|
+
// A plural on either side is the same word.
|
|
303
|
+
expect(headingWithPhrase('data attributes selector', ['Data attribute selectors'])).toBe(
|
|
304
|
+
'Data attribute selectors',
|
|
305
|
+
);
|
|
306
|
+
// Out of order, split up, or one word: not a phrase.
|
|
307
|
+
expect(headingWithPhrase('mode dark', ['Light/Dark Mode'])).toBeNull();
|
|
308
|
+
expect(headingWithPhrase('dark mode', ['Dark sidebar and mode toggle'])).toBeNull();
|
|
309
|
+
expect(headingWithPhrase('dark', ['Light/Dark Mode'])).toBeNull();
|
|
310
|
+
expect(headingWithPhrase('dark mode', undefined)).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('ranks a section titled with the phrase above an exact code-tick match of one word', () => {
|
|
314
|
+
// The reported miss: `search "dark mode"` put "Light/Dark Mode" at #28,
|
|
315
|
+
// under API enum docs that name `mode` in code ticks.
|
|
316
|
+
const section = {name: 'light-dark-mode', titles: ['Light/Dark Mode'], keywords: ['Light/Dark Mode']};
|
|
317
|
+
const enumDoc = {name: 'response-types', keywords: ['mode', 'dark'], description: 'mode'};
|
|
318
|
+
expect(score('dark mode', section)).toBe(170);
|
|
319
|
+
expect(score('dark mode', section)).toBeGreaterThan(score('dark mode', enumDoc));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('ranks a question that names a whole title just below that', () => {
|
|
323
|
+
expect(titleInQuery('how do i add dark mode', ['Dark mode'])).toBe('Dark mode');
|
|
324
|
+
expect(titleInQuery('how do nested themes work', ['Nested themes'])).toBe('Nested themes');
|
|
325
|
+
// One-word titles are too common to count, and order still matters.
|
|
326
|
+
expect(titleInQuery('how do i theme my app', ['Theme'])).toBeNull();
|
|
327
|
+
expect(titleInQuery('mode dark please', ['Dark mode'])).toBeNull();
|
|
328
|
+
const section = {name: 'light-dark-mode', titles: ['Dark mode'], keywords: ['Dark mode']};
|
|
329
|
+
const named = score('how do i add dark mode', section);
|
|
330
|
+
expect(named).toBeGreaterThanOrEqual(160);
|
|
331
|
+
expect(named).toBeLessThan(170);
|
|
332
|
+
// Sections that share a title are ordered by how much of the rest of the
|
|
333
|
+
// question they answer.
|
|
334
|
+
const spacing = {name: 'best-practices', titles: ['Best Practices'], prose: ['Use spacing tokens']};
|
|
335
|
+
const color = {name: 'best-practices', titles: ['Best Practices'], prose: ['Use color tokens']};
|
|
336
|
+
expect(score('best practices for spacing', spacing)).toBeGreaterThan(
|
|
337
|
+
score('best practices for spacing', color),
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('reads a plural of a name as the name, and only a real plural', () => {
|
|
342
|
+
// One point under the exact spelling, so the doc named `tokens` outranks
|
|
343
|
+
// the Token component for `tokens`.
|
|
344
|
+
expect(scoreCandidate('integration', {name: 'integrations'})?.score).toBe(99);
|
|
345
|
+
expect(scoreCandidate('box', {name: 'boxes'})?.score).toBe(99);
|
|
346
|
+
expect(scoreCandidate('tabs', {name: 'tab'})?.score).toBe(99);
|
|
347
|
+
expect(scoreCandidate('tokens', {name: 'tokens'})?.score).toBe(100);
|
|
348
|
+
// `es` only follows s, x, z, ch, or sh.
|
|
349
|
+
expect(scoreCandidate('not', {name: 'notes'})?.score ?? 0).toBeLessThan(100);
|
|
350
|
+
expect(scoreCandidate('mod', {name: 'modes'})?.score ?? 0).toBeLessThan(100);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('keeps an exact name or keyword above a title phrase', () => {
|
|
354
|
+
const titled = {name: 'x', titles: ['Table of contents for long pages']};
|
|
355
|
+
const keyword = {name: 'Outline', keywords: ['table of contents']};
|
|
356
|
+
expect(score('table of contents', keyword)).toBeGreaterThan(score('table of contents', titled));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('puts the dark mode section first for a docs search', async () => {
|
|
360
|
+
for (const query of ['dark mode', 'how do I add dark mode']) {
|
|
361
|
+
const r = await search(query, {cwd, type: 'doc'});
|
|
362
|
+
expect(r.data.results[0]).toMatchObject({name: 'theme', section: 'light-dark-mode'});
|
|
363
|
+
}
|
|
364
|
+
}, SLOW);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('search leaf — a candidate that matches every word outranks a partial match', () => {
|
|
368
|
+
/**
|
|
369
|
+
* @param {string} q
|
|
370
|
+
* @param {object} candidate
|
|
371
|
+
* @returns {number}
|
|
372
|
+
*/
|
|
373
|
+
const score = (q, candidate) => scoreQuery(q, tokenizeQuery(q), candidate)?.score ?? 0;
|
|
374
|
+
|
|
375
|
+
it('ranks a doc with both words above a doc named after one of them', () => {
|
|
376
|
+
// The reported regression: `search troubleshoot integration` put the
|
|
377
|
+
// troubleshooting guide 30th, under docs that each match `integration`
|
|
378
|
+
// alone (by name, 108; in a code tick, 98).
|
|
379
|
+
const guide = {
|
|
380
|
+
name: 'troubleshooting',
|
|
381
|
+
keywords: ['Troubleshooting'],
|
|
382
|
+
description: 'What to check when an integration does not load.',
|
|
383
|
+
};
|
|
384
|
+
const byName = {name: 'integration', keywords: ['integration-add']};
|
|
385
|
+
const byCodeTick = {name: 'integration-add', keywords: ['integration']};
|
|
386
|
+
const q = 'troubleshoot integration';
|
|
387
|
+
expect(score(q, guide)).toBeGreaterThan(score(q, byName));
|
|
388
|
+
expect(score(q, guide)).toBeGreaterThan(score(q, byCodeTick));
|
|
389
|
+
expect(scoreQuery(q, tokenizeQuery(q), guide)).toMatchObject({matched: 2, total: 2});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('holds for longer queries too, and stays below the title tiers', () => {
|
|
393
|
+
const all = {name: 'x', keywords: ['alphas'], description: 'alpha beta gamma delta'};
|
|
394
|
+
const threeOfFour = {name: 'alpha', keywords: ['beta', 'gamma']};
|
|
395
|
+
const q = 'alpha beta gamma delta';
|
|
396
|
+
expect(score(q, all)).toBeGreaterThan(score(q, threeOfFour));
|
|
397
|
+
expect(score(q, all)).toBeLessThan(160);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Among candidates that match every word, the stronger match should come
|
|
401
|
+
// first. Today every all-word match with a keyword hit gets the same score,
|
|
402
|
+
// so this records the order without enforcing it: it fails, as expected,
|
|
403
|
+
// until the scoring tells the two apart.
|
|
404
|
+
it.fails('ranks the stronger of two all-word matches first', () => {
|
|
405
|
+
const all = {name: 'x', keywords: ['alphas'], description: 'alpha beta gamma delta'};
|
|
406
|
+
const q = 'alpha beta gamma delta';
|
|
407
|
+
expect(score(q, {name: 'y', keywords: ['alpha'], description: 'beta gamma delta'})).toBeGreaterThan(
|
|
408
|
+
score(q, all),
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('keeps passing mentions of every word below an exact hit on one word', () => {
|
|
413
|
+
// Mentions in prose, or the components a page happens to render, are
|
|
414
|
+
// breadth: a page that says "empty state" is not the EmptyState answer.
|
|
415
|
+
const mentions = {name: 'ai-chat-landing', description: 'A landing page with an empty state.'};
|
|
416
|
+
const keyword = {name: 'x', keywords: ['empty']};
|
|
417
|
+
expect(score('empty state', mentions)).toBeLessThan(score('empty state', keyword));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('finds a component by its name typed as words, and a guide by its route', async () => {
|
|
421
|
+
for (const [query, name] of [
|
|
422
|
+
['command palette', 'CommandPalette'],
|
|
423
|
+
['empty state', 'EmptyState'],
|
|
424
|
+
]) {
|
|
425
|
+
const r = await search(query, {cwd});
|
|
426
|
+
expect(r.data.results[0], query).toMatchObject({domain: 'component', name});
|
|
427
|
+
}
|
|
428
|
+
const tokens = await search('tokens', {cwd});
|
|
429
|
+
expect(tokens.data.results[0]).toMatchObject({domain: 'doc', name: 'tokens'});
|
|
430
|
+
for (const [query, route] of [
|
|
431
|
+
['codemods', /^cli\/integrations\/(?:.+\/)?codemods$/],
|
|
432
|
+
['quick start', /^cli\/integrations\/(?:.+\/)?quick-start$/],
|
|
433
|
+
['test in an app', /^cli\/integrations\/(?:.+\/)?test-in-an-app$/],
|
|
434
|
+
]) {
|
|
435
|
+
const r = await search(query, {cwd, type: 'doc'});
|
|
436
|
+
// The guide is among the hits that share the top score: two guides
|
|
437
|
+
// that both declare the phrase tie, and the tie's order is not pinned.
|
|
438
|
+
const top = r.data.results
|
|
439
|
+
.filter(result => result.score === r.data.results[0].score)
|
|
440
|
+
.map(result => result.name);
|
|
441
|
+
expect(top.some(name => route.test(name)), `${query}: ${top.join(', ')}`).toBe(true);
|
|
442
|
+
}
|
|
443
|
+
}, SLOW);
|
|
444
|
+
});
|
|
445
|
+
|
|
256
446
|
describe('search leaf — error paths (pinned)', () => {
|
|
257
447
|
it('throws ERR_INVALID_ARGUMENT when the query is empty/whitespace', async () => {
|
|
258
448
|
await expect(search(' ', {cwd})).rejects.toMatchObject({
|
|
@@ -267,12 +457,20 @@ describe('search leaf — error paths (pinned)', () => {
|
|
|
267
457
|
).rejects.toMatchObject({code: 'ERR_INVALID_ARGUMENT'});
|
|
268
458
|
}, SLOW);
|
|
269
459
|
|
|
270
|
-
it('
|
|
460
|
+
it('searches the docs without @astryxdesign/core, and throws for a domain that needs it', async () => {
|
|
271
461
|
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-search-no-core-'));
|
|
272
462
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
463
|
+
// An open search outside an app covers the docs, as `astryx docs` does.
|
|
464
|
+
const open = await search('make an integration', {cwd: empty});
|
|
465
|
+
expect(open.data.results.length).toBeGreaterThan(0);
|
|
466
|
+
expect(new Set(open.data.results.map(r => r.domain))).toEqual(
|
|
467
|
+
new Set(['doc']),
|
|
468
|
+
);
|
|
469
|
+
for (const type of ['component', 'hook', 'template']) {
|
|
470
|
+
await expect(
|
|
471
|
+
search('button', {cwd: empty, type: /** @type {any} */ (type)}),
|
|
472
|
+
).rejects.toMatchObject({code: 'ERR_CORE_NOT_FOUND'});
|
|
473
|
+
}
|
|
276
474
|
} finally {
|
|
277
475
|
fs.rmSync(empty, {recursive: true, force: true});
|
|
278
476
|
}
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
findIntegrationComponentSource,
|
|
26
26
|
} from '../../../foundation/discovery/component-discovery.mjs';
|
|
27
27
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
28
|
-
import {AstryxError} from '../../error.mjs';
|
|
28
|
+
import {AstryxError, writeFailed} from '../../error.mjs';
|
|
29
29
|
|
|
30
30
|
/** Default issue tracker for maintainer feedback after swizzling. */
|
|
31
31
|
const DEFAULT_ISSUES_URL = 'https://github.com/facebook/astryx/issues/new';
|
|
@@ -290,11 +290,21 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
290
290
|
);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
fs.
|
|
293
|
+
const outputDirExisted = fs.existsSync(outputDir);
|
|
294
|
+
try {
|
|
295
|
+
fs.mkdirSync(outputDir, {recursive: true});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
throw writeFailed(outputDir, cwd, err);
|
|
298
|
+
}
|
|
294
299
|
|
|
295
300
|
const files = fs.readdirSync(componentDir);
|
|
296
301
|
let copied = 0;
|
|
297
302
|
let usesStyleX = false;
|
|
303
|
+
// A copy that fails part-way undoes what it already wrote, so the report is
|
|
304
|
+
// true and a retry does not trip over half a component. Each entry keeps
|
|
305
|
+
// the bytes the file had before this run (null when the copy created it).
|
|
306
|
+
/** @type {Array<{dest: string, original: Buffer|null}>} */
|
|
307
|
+
const written = [];
|
|
298
308
|
for (const file of files) {
|
|
299
309
|
if (isExcludedFromCopy(file)) continue;
|
|
300
310
|
const srcPath = path.join(componentDir, file);
|
|
@@ -309,7 +319,25 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
309
319
|
) {
|
|
310
320
|
usesStyleX = true;
|
|
311
321
|
}
|
|
312
|
-
|
|
322
|
+
const dest = path.join(outputDir, file);
|
|
323
|
+
try {
|
|
324
|
+
// The snapshot read is guarded too: a destination that cannot be read
|
|
325
|
+
// back (no permission, or a directory with this name) must still undo
|
|
326
|
+
// the earlier writes and report ERR_WRITE_FAILED.
|
|
327
|
+
const original = fs.existsSync(dest) ? fs.readFileSync(dest) : null;
|
|
328
|
+
written.push({dest, original});
|
|
329
|
+
fs.writeFileSync(dest, content);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const unrestored = undoCopy(written);
|
|
332
|
+
if (!outputDirExisted) {
|
|
333
|
+
try {
|
|
334
|
+
fs.rmdirSync(outputDir);
|
|
335
|
+
} catch {
|
|
336
|
+
// Not empty (something could not be undone), or already gone.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw writeFailed(dest, cwd, err, unrestored);
|
|
340
|
+
}
|
|
313
341
|
copied++;
|
|
314
342
|
}
|
|
315
343
|
|
|
@@ -333,3 +361,38 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
333
361
|
if (feedback) data.feedback = feedback;
|
|
334
362
|
return {type: 'swizzle.copy', data};
|
|
335
363
|
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Undo the writes of a copy that failed part-way, newest first: delete the
|
|
367
|
+
* files the copy created and put back the bytes of the files it replaced. A
|
|
368
|
+
* file whose bytes are already the original ones is left alone, so a write
|
|
369
|
+
* that failed before changing anything is not reported as unrestored.
|
|
370
|
+
*
|
|
371
|
+
* @param {Array<{dest: string, original: Buffer|null}>} written
|
|
372
|
+
* @returns {string[]} the files it could not restore
|
|
373
|
+
*/
|
|
374
|
+
function undoCopy(written) {
|
|
375
|
+
/** @type {string[]} */
|
|
376
|
+
const unrestored = [];
|
|
377
|
+
for (const {dest, original} of [...written].reverse()) {
|
|
378
|
+
try {
|
|
379
|
+
if (original == null) {
|
|
380
|
+
fs.rmSync(dest, {force: true});
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
/** @type {Buffer|null} */
|
|
384
|
+
let current = null;
|
|
385
|
+
try {
|
|
386
|
+
current = fs.readFileSync(dest);
|
|
387
|
+
} catch {
|
|
388
|
+
current = null;
|
|
389
|
+
}
|
|
390
|
+
if (current == null || !current.equals(original)) {
|
|
391
|
+
fs.writeFileSync(dest, original);
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
unrestored.push(dest);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return unrestored;
|
|
398
|
+
}
|
|
@@ -29,17 +29,19 @@ export const doc = {
|
|
|
29
29
|
name: 'component',
|
|
30
30
|
type: 'string',
|
|
31
31
|
description:
|
|
32
|
-
|
|
32
|
+
"Component name to copy (e.g. 'Button'). Omit to list the swizzlable components.",
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
name: 'options.cwd',
|
|
36
36
|
type: 'string',
|
|
37
37
|
description: 'Directory to resolve @astryxdesign/core from.',
|
|
38
|
+
default: 'process.cwd()',
|
|
38
39
|
},
|
|
39
40
|
{
|
|
40
41
|
name: 'options.output',
|
|
41
42
|
type: 'string',
|
|
42
|
-
description:
|
|
43
|
+
description:
|
|
44
|
+
'Output directory, relative to cwd. An absolute path, or one that resolves outside cwd, throws ERR_PATH_TRAVERSAL.',
|
|
43
45
|
default: "'./components/astryx'",
|
|
44
46
|
},
|
|
45
47
|
{
|
|
@@ -71,7 +73,7 @@ export const doc = {
|
|
|
71
73
|
{
|
|
72
74
|
type: 'swizzle.copy',
|
|
73
75
|
description:
|
|
74
|
-
'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an
|
|
76
|
+
'A receipt after copying the component into the project: the component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and, when the owner has an issues URL, feedback ({issuesUrl, ghCommand?}): where to report the gap that led to swizzling.',
|
|
75
77
|
},
|
|
76
78
|
],
|
|
77
79
|
throws: [
|
|
@@ -81,7 +83,7 @@ export const doc = {
|
|
|
81
83
|
},
|
|
82
84
|
{
|
|
83
85
|
code: 'ERR_PATH_TRAVERSAL',
|
|
84
|
-
when: 'the component name contains a path separator or traversal, output resolves outside cwd, or an existing output file or directory is a symlink that resolves outside cwd',
|
|
86
|
+
when: 'the component name contains a path separator or traversal, output is absolute or resolves outside cwd, or an existing output file or directory is a symlink that resolves outside cwd',
|
|
85
87
|
},
|
|
86
88
|
{
|
|
87
89
|
code: 'ERR_UNKNOWN_COMPONENT',
|
|
@@ -99,6 +101,10 @@ export const doc = {
|
|
|
99
101
|
code: 'ERR_FILE_EXISTS',
|
|
100
102
|
when: 'copying would overwrite existing files and overwrite is not set',
|
|
101
103
|
},
|
|
104
|
+
{
|
|
105
|
+
code: 'ERR_WRITE_FAILED',
|
|
106
|
+
when: 'the output directory or a copied file could not be written (no permission, read-only mount, full disk)',
|
|
107
|
+
},
|
|
102
108
|
],
|
|
103
109
|
examples: [
|
|
104
110
|
{
|
|
@@ -108,7 +114,7 @@ export const doc = {
|
|
|
108
114
|
{label: 'Eject a component', code: "await swizzle('Button');"},
|
|
109
115
|
{
|
|
110
116
|
label: 'Disambiguate by package',
|
|
111
|
-
code: "await swizzle('Button', {package: '@astryxdesign/core'});",
|
|
117
|
+
code: "await swizzle('Button', {package: '@astryxdesign/core', overwrite: true});",
|
|
112
118
|
},
|
|
113
119
|
{
|
|
114
120
|
label: 'Custom output directory',
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
isFilePathArg,
|
|
17
17
|
PathSafetyError,
|
|
18
18
|
} from '../../../foundation/fs/path-safety.mjs';
|
|
19
|
-
import {AstryxError} from '../../error.mjs';
|
|
19
|
+
import {AstryxError, writeFailed} from '../../error.mjs';
|
|
20
20
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
21
|
-
import {
|
|
21
|
+
import {replaceDemoMedia} from '../../../foundation/discovery/template-adapter.mjs';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Scaffold an already-resolved template to `targetPath` (relative to `cwd`) and
|
|
@@ -74,19 +74,24 @@ export function templateCopy(match, {targetPath, cwd, overwrite = false}) {
|
|
|
74
74
|
if (!overwrite && fs.existsSync(outputFilePath)) {
|
|
75
75
|
const rel = path.relative(cwd, outputFilePath) || outputFilePath;
|
|
76
76
|
throw new AstryxError(
|
|
77
|
-
`Refusing to overwrite existing file ${rel}. Re-run with overwrite to replace it.`,
|
|
77
|
+
`Refusing to overwrite existing file ${rel}. Re-run with --overwrite (or -f) to replace it.`,
|
|
78
78
|
undefined,
|
|
79
79
|
ERROR_CODES.ERR_FILE_EXISTS,
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
fs.mkdirSync(outputDir, {recursive: true});
|
|
84
|
-
|
|
85
83
|
// Strip demo image references so the scaffolded file renders without a
|
|
86
|
-
// Meta-only network dependency.
|
|
87
|
-
|
|
88
|
-
const outputSource =
|
|
89
|
-
|
|
84
|
+
// Meta-only network dependency. Read before any write, so a failure below
|
|
85
|
+
// leaves nothing behind.
|
|
86
|
+
const {source: outputSource, demoMediaReplaced} = replaceDemoMedia(
|
|
87
|
+
fs.readFileSync(match.filePath, 'utf-8'),
|
|
88
|
+
);
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(outputDir, {recursive: true});
|
|
91
|
+
fs.writeFileSync(outputFilePath, outputSource);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw writeFailed(outputFilePath, cwd, err);
|
|
94
|
+
}
|
|
90
95
|
|
|
91
96
|
const relOutput = path.relative(cwd, outputDir) || '.';
|
|
92
97
|
return {
|
|
@@ -96,6 +101,7 @@ export function templateCopy(match, {targetPath, cwd, overwrite = false}) {
|
|
|
96
101
|
outputDir: relOutput,
|
|
97
102
|
fileName: outputFileName,
|
|
98
103
|
filesCopied: 1,
|
|
104
|
+
demoMediaReplaced,
|
|
99
105
|
},
|
|
100
106
|
};
|
|
101
107
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The template.copy receipt discloses replaced demo media. Templates are
|
|
5
|
+
* picked by what their source contains, not by slug, so catalog renames do not
|
|
6
|
+
* break the suite.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import {template, discoverTemplates} from '../template.mjs';
|
|
14
|
+
|
|
15
|
+
const SLOW = 60_000;
|
|
16
|
+
const FIXTURE_REF = /\/template-assets\/[\w.-]+\.(\w+)/g;
|
|
17
|
+
const VIDEO = new Set(['mp4', 'webm', 'mov', 'ogv', 'm4v']);
|
|
18
|
+
|
|
19
|
+
/** @param {string} filePath */
|
|
20
|
+
const fixtureRefs = filePath =>
|
|
21
|
+
[...fs.readFileSync(filePath, 'utf-8').matchAll(FIXTURE_REF)].map(m =>
|
|
22
|
+
m[1].toLowerCase(),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {(refs: string[]) => boolean} predicate
|
|
27
|
+
* @param {'page' | 'block'} type
|
|
28
|
+
*/
|
|
29
|
+
async function findTemplate(type, predicate) {
|
|
30
|
+
const all = /** @type {Array<{dirName: string, type: string, filePath: string}>} */ (
|
|
31
|
+
await discoverTemplates()
|
|
32
|
+
);
|
|
33
|
+
const ids = new Map();
|
|
34
|
+
for (const t of all) ids.set(t.dirName, (ids.get(t.dirName) ?? 0) + 1);
|
|
35
|
+
const found = all.find(
|
|
36
|
+
t =>
|
|
37
|
+
t.type === type &&
|
|
38
|
+
ids.get(t.dirName) === 1 &&
|
|
39
|
+
fs.existsSync(t.filePath) &&
|
|
40
|
+
predicate(fixtureRefs(t.filePath)),
|
|
41
|
+
);
|
|
42
|
+
if (!found) throw new Error(`no ${type} template matches`);
|
|
43
|
+
return {id: found.dirName, refs: fixtureRefs(found.filePath)};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('template.copy receipt — replaced demo media', () => {
|
|
47
|
+
let dir;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmpl-receipt-'));
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => fs.rmSync(dir, {recursive: true, force: true}));
|
|
52
|
+
|
|
53
|
+
it('counts every demo image a page template carried, and leaves none behind', async () => {
|
|
54
|
+
const {id, refs} = await findTemplate(
|
|
55
|
+
'page',
|
|
56
|
+
r => r.length > 0 && r.every(ext => !VIDEO.has(ext)),
|
|
57
|
+
);
|
|
58
|
+
const res = await template(id, {targetPath: './dest', cwd: dir});
|
|
59
|
+
expect(res.type).toBe('template.copy');
|
|
60
|
+
expect(res.data.demoMediaReplaced).toBe(refs.length);
|
|
61
|
+
expect(
|
|
62
|
+
fs.readFileSync(path.join(dir, 'dest', 'page.tsx'), 'utf-8'),
|
|
63
|
+
).not.toContain('/template-assets/');
|
|
64
|
+
}, SLOW);
|
|
65
|
+
|
|
66
|
+
it('counts a demo video in a block template', async () => {
|
|
67
|
+
const {id, refs} = await findTemplate('block', r => r.some(ext => VIDEO.has(ext)));
|
|
68
|
+
const res = await template(id, {targetPath: './dest', cwd: dir});
|
|
69
|
+
expect(res.data.demoMediaReplaced).toBe(refs.length);
|
|
70
|
+
}, SLOW);
|
|
71
|
+
|
|
72
|
+
it('reports 0 when the template carries no demo media', async () => {
|
|
73
|
+
const {id} = await findTemplate('page', r => r.length === 0);
|
|
74
|
+
const res = await template(id, {targetPath: './dest', cwd: dir});
|
|
75
|
+
expect(res.data.demoMediaReplaced).toBe(0);
|
|
76
|
+
}, SLOW);
|
|
77
|
+
});
|
|
@@ -35,6 +35,15 @@ describe('template.copy — overwrite + path safety', () => {
|
|
|
35
35
|
expect(fs.readFileSync(path.join(dir, 'mine.tsx'), 'utf-8')).toBe('USER CODE');
|
|
36
36
|
}, SLOW);
|
|
37
37
|
|
|
38
|
+
it('names the flag that replaces the file, as swizzle and theme add do', async () => {
|
|
39
|
+
fs.writeFileSync(path.join(dir, 'mine.tsx'), 'USER CODE');
|
|
40
|
+
await expect(
|
|
41
|
+
template('blank', {targetPath: './mine.tsx', cwd: dir}),
|
|
42
|
+
).rejects.toThrow(
|
|
43
|
+
'Refusing to overwrite existing file mine.tsx. Re-run with --overwrite (or -f) to replace it.',
|
|
44
|
+
);
|
|
45
|
+
}, SLOW);
|
|
46
|
+
|
|
38
47
|
it('overwrites when overwrite:true is passed', async () => {
|
|
39
48
|
fs.writeFileSync(path.join(dir, 'mine.tsx'), 'USER CODE');
|
|
40
49
|
const res = await template('blank', {targetPath: './mine.tsx', overwrite: true, cwd: dir});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @file `template.show` leaf — return a resolved template's
|
|
5
|
-
* components it composes.
|
|
4
|
+
* @file `template.show` leaf — return a resolved template's source, exactly as
|
|
5
|
+
* `template.copy` writes it, plus the components it composes.
|
|
6
6
|
*
|
|
7
7
|
* @position api/template/show — reads the resolved match's source file; the
|
|
8
8
|
* template dispatcher routes `show` (and the no-target-path default) here.
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import {AstryxError} from '../../error.mjs';
|
|
13
13
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
extractComponents,
|
|
16
|
+
replaceDemoMedia,
|
|
17
|
+
} from '../../../foundation/discovery/template-adapter.mjs';
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Build the `template.show` envelope for an already-resolved template.
|
|
@@ -27,6 +30,13 @@ export function templateShow(match) {
|
|
|
27
30
|
);
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
// The source template.copy writes (spec:AST-028 FR7): a template printed and
|
|
34
|
+
// pasted must not keep a media path only Astryx's previews serve, and the
|
|
35
|
+
// caller is told how many it replaced, the way the copy receipt tells it.
|
|
36
|
+
const {source, demoMediaReplaced} = replaceDemoMedia(
|
|
37
|
+
fs.readFileSync(match.filePath, 'utf-8'),
|
|
38
|
+
);
|
|
39
|
+
|
|
30
40
|
return {
|
|
31
41
|
type: 'template.show',
|
|
32
42
|
data: {
|
|
@@ -34,7 +44,8 @@ export function templateShow(match) {
|
|
|
34
44
|
description: match.description,
|
|
35
45
|
type: match.type,
|
|
36
46
|
components: extractComponents(match.filePath),
|
|
37
|
-
source
|
|
47
|
+
source,
|
|
48
|
+
demoMediaReplaced,
|
|
38
49
|
},
|
|
39
50
|
};
|
|
40
51
|
}
|