@astryxdesign/cli 0.7.2 → 0.7.3-canary.02f5fe4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -71
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +127 -3
- package/api/build/blocks/blocks.d.mts +92 -0
- package/api/build/blocks/blocks.mjs +528 -0
- package/api/build/blocks/blocks.test.mjs +238 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/build.type.mjs +1 -1
- package/api/build/kit/answer.d.mts +18 -0
- package/api/build/kit/answer.mjs +195 -0
- package/api/build/kit/answer.test.mjs +108 -0
- package/api/build/kit/kind.d.mts +25 -0
- package/api/build/kit/kind.mjs +170 -0
- package/api/build/kit/kit.d.mts +2 -4
- package/api/build/kit/kit.mjs +8 -398
- package/api/build/kit/rank.d.mts +3 -32
- package/api/build/kit/rank.mjs +25 -342
- package/api/build/kit/rank.test.mjs +2 -1
- package/api/build/kit/signals.d.mts +79 -0
- package/api/build/kit/signals.mjs +295 -0
- package/api/build/kit/start.d.mts +32 -0
- package/api/build/kit/start.mjs +163 -0
- package/api/build/kit/words.d.mts +72 -0
- package/api/build/kit/words.mjs +202 -0
- package/api/build/tree.d.mts +20 -0
- package/api/build/tree.mjs +53 -0
- package/api/build/tree.test.mjs +185 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/_adapter.mjs +39 -5
- package/api/component/component-replacement.test.mjs +1 -0
- package/api/component/component.mjs +17 -3
- package/api/component/component.test.mjs +15 -0
- package/api/component/doc-links.test.mjs +2 -2
- package/api/discover/_adapter.d.mts +1 -2
- package/api/discover/_adapter.mjs +42 -17
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/discover/discover.mjs +0 -1
- package/api/docs/_adapter.d.mts +2 -2
- package/api/docs/_adapter.mjs +82 -23
- package/api/docs/docs-homes.test.mjs +127 -0
- package/api/docs/docs.doc.mjs +3 -3
- package/api/docs/docs.test.mjs +35 -19
- package/api/docs/integration-tree.test.mjs +20 -20
- package/api/docs/integrationDocs.test.mjs +1 -1
- package/api/docs/namespace-section.test.mjs +6 -6
- package/api/docs/reference-blocks.test.mjs +2 -2
- package/api/doctor/_adapter.d.mts +33 -13
- package/api/doctor/_adapter.mjs +55 -25
- package/api/doctor/doctor.d.mts +23 -5
- package/api/doctor/doctor.mjs +183 -40
- package/api/doctor/doctor.test.mjs +95 -12
- package/api/hook/hook.mjs +1 -1
- package/api/init/init.doc.mjs +20 -6
- package/api/init/init.test.mjs +18 -2
- package/api/init/init.type.d.mts +3 -3
- package/api/init/init.type.mjs +3 -3
- package/api/init/remove/remove.mjs +17 -4
- package/api/init/remove/remove.test.mjs +114 -2
- package/api/init/run/run.mjs +69 -6
- package/api/integration/_adapter.d.mts +8 -0
- package/api/integration/_adapter.mjs +21 -1
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/authoring-checks.test.mjs +220 -0
- package/api/integration/pack-check.mjs +123 -4
- package/api/integration/pack-check.test.mjs +87 -0
- package/api/integration/validate-integration-case-disk.test.mjs +111 -0
- package/api/integration/validate-integration-fixes.test.mjs +6 -5
- package/api/integration/validate-integration-gaps.test.mjs +1975 -0
- package/api/integration/validate-integration.d.mts +15 -2
- package/api/integration/validate-integration.mjs +298 -39
- package/api/integration/validate-integration.type.d.mts +90 -1
- package/api/integration/validate-integration.type.mjs +67 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +82 -2
- package/api/search/_adapter.mjs +157 -10
- package/api/search/_adapter.test.mjs +173 -0
- package/api/search/_candidate-score.d.mts +40 -0
- package/api/search/_candidate-score.mjs +238 -0
- package/api/search/_cards.json +6098 -0
- package/api/search/_compound.d.mts +50 -0
- package/api/search/_compound.mjs +170 -0
- package/api/search/_doc-searcher.d.mts +4 -0
- package/api/search/_doc-searcher.mjs +23 -0
- package/api/search/_doc-searcher.test.mjs +36 -0
- package/api/search/_doc-searcher.weights.json +1 -0
- package/api/search/_doc-text.d.mts +30 -0
- package/api/search/_doc-text.mjs +87 -0
- package/api/search/_entries.d.mts +63 -0
- package/api/search/_entries.mjs +232 -0
- package/api/search/_entries.test.mjs +48 -0
- package/api/search/_features.d.mts +35 -0
- package/api/search/_features.mjs +155 -0
- package/api/search/_gather-components.d.mts +26 -0
- package/api/search/_gather-components.mjs +284 -0
- package/api/search/_gather-docs.d.mts +20 -0
- package/api/search/_gather-docs.mjs +254 -0
- package/api/search/_gather-hooks.d.mts +12 -0
- package/api/search/_gather-hooks.mjs +52 -0
- package/api/search/_gather-templates.d.mts +25 -0
- package/api/search/_gather-templates.mjs +107 -0
- package/api/search/_lexical.d.mts +135 -0
- package/api/search/_lexical.mjs +351 -0
- package/api/search/_main-searcher.d.mts +19 -0
- package/api/search/_main-searcher.mjs +115 -0
- package/api/search/_main-searcher.test.mjs +106 -0
- package/api/search/_main-searcher.weights.json +1 -0
- package/api/search/_module-doc.d.mts +32 -0
- package/api/search/_module-doc.mjs +78 -0
- package/api/search/_order.d.mts +32 -0
- package/api/search/_order.mjs +162 -0
- package/api/search/_order.test.mjs +72 -0
- package/api/search/_priority.d.mts +50 -0
- package/api/search/_priority.mjs +139 -0
- package/api/search/_query-words.d.mts +25 -0
- package/api/search/_query-words.mjs +92 -0
- package/api/search/_query.d.mts +65 -0
- package/api/search/_query.mjs +176 -0
- package/api/search/_result.d.mts +97 -0
- package/api/search/_result.mjs +150 -0
- package/api/search/_score.d.mts +60 -0
- package/api/search/_score.mjs +283 -0
- package/api/search/_synonyms.d.mts +21 -0
- package/api/search/_synonyms.mjs +67 -0
- package/api/search/_template-searcher.d.mts +4 -0
- package/api/search/_template-searcher.mjs +23 -0
- package/api/search/_template-searcher.test.mjs +40 -0
- package/api/search/_template-searcher.weights.json +1 -0
- package/api/search/_titles.d.mts +33 -0
- package/api/search/_titles.mjs +65 -0
- package/api/search/_words.d.mts +64 -0
- package/api/search/_words.mjs +157 -0
- package/api/search/search.d.mts +22 -305
- package/api/search/search.doc.mjs +8 -6
- package/api/search/search.mjs +56 -2482
- package/api/search/search.test.mjs +14 -14
- package/api/search/search.type.d.mts +6 -1
- package/api/search/search.type.mjs +6 -1
- package/api/search/tree.d.mts +15 -0
- package/api/search/tree.mjs +40 -0
- package/api/search/tree.test.mjs +66 -0
- package/api/search/tree.weights.json +1 -0
- package/api/template/template.mjs +5 -1
- package/api/theme/build/build.packed-old-core.test.mjs +1 -0
- package/api/upgrade/_adapter.d.mts +57 -9
- package/api/upgrade/_adapter.mjs +198 -28
- package/api/upgrade/registry/registry.d.mts +10 -4
- package/api/upgrade/registry/registry.mjs +33 -16
- package/api/upgrade/registry/registry.test.mjs +85 -0
- package/api/upgrade/run/run.d.mts +4 -2
- package/api/upgrade/run/run.mjs +13 -2
- package/api/upgrade/run/run.test.mjs +417 -3
- package/api/upgrade/upgrade.doc.mjs +10 -5
- package/api/upgrade/upgrade.mjs +13 -2
- package/api/upgrade/upgrade.test.mjs +194 -0
- package/api/upgrade/upgrade.type.d.mts +5 -3
- package/api/upgrade/upgrade.type.mjs +8 -6
- package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
- package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
- package/assets/docs/author-a-theme.doc.mjs +2 -2
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/add-a-component.doc.mjs +1 -1
- package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
- package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
- package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
- package/assets/docs/tree/block-template.doc.mjs +1 -1
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/building-blocks.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +1 -1
- package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
- package/assets/docs/tree/checks.doc.mjs +3 -3
- package/assets/docs/tree/ci-and-containers.doc.mjs +13 -0
- package/assets/docs/tree/cli-command.doc.mjs +1 -1
- package/assets/docs/tree/cli.doc.mjs +1 -1
- package/assets/docs/tree/codemods.doc.mjs +1 -1
- package/assets/docs/tree/component-family.doc.mjs +1 -1
- package/assets/docs/tree/component-imports.doc.mjs +1 -1
- package/assets/docs/tree/components.doc.mjs +1 -1
- package/assets/docs/tree/configuration.doc.mjs +1 -1
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
- package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/docs.doc.mjs +1 -1
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
- package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
- package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
- package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
- package/assets/docs/tree/global-integrations.doc.mjs +2 -2
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
- package/assets/docs/tree/help.doc.mjs +2 -2
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/links.doc.mjs +1 -1
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/publishing.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/ship.doc.mjs +2 -2
- package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
- package/assets/docs/tree/single-component.doc.mjs +1 -1
- package/assets/docs/tree/start-a-template.doc.mjs +1 -1
- package/assets/docs/tree/subcomponent.doc.mjs +1 -1
- package/assets/docs/tree/template-assets.doc.mjs +1 -1
- package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
- package/assets/docs/tree/template-icons.doc.mjs +1 -1
- package/assets/docs/tree/template-images-media.doc.mjs +1 -1
- package/assets/docs/tree/template-styles.doc.mjs +1 -1
- package/assets/docs/tree/templates.doc.mjs +1 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
- package/assets/docs/tree/themes.doc.mjs +1 -1
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
- package/assets/docs/tree/upgrading.doc.mjs +1 -1
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
- package/assets/docs/tree/versioning.doc.mjs +5 -1
- package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
- package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
- package/assets/docs/use-integrations.doc.mjs +120 -0
- package/assets/docs/working-with-ai.doc.mjs +6 -2
- package/assets/templates/themes/butter/butterTheme.ts +4 -2
- package/assets/templates/themes/stone/stoneTheme.ts +4 -2
- package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
- package/authoring/debug/parse.d.mts +1 -1
- package/authoring/doctypes/schema/schema.doc.mjs +1 -1
- package/authoring/integration/integration.doc.mjs +1 -1
- package/authoring/integration/type.ts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/command-result-coverage.test.mjs +6 -5
- package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
- package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
- package/clients/cli/commands/component.test.mjs +6 -6
- package/clients/cli/commands/docs.doc.mjs +2 -2
- package/clients/cli/commands/docs.mjs +1 -1
- package/clients/cli/commands/docs.test.mjs +14 -14
- package/clients/cli/commands/doctor-integration.test.mjs +49 -1
- package/clients/cli/commands/doctor.doc.mjs +1 -1
- package/clients/cli/commands/doctor.mjs +40 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
- package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
- package/clients/cli/commands/init.behavior.test.mjs +230 -1
- package/clients/cli/commands/init.doc.mjs +28 -9
- package/clients/cli/commands/init.flag-help.test.mjs +1 -0
- package/clients/cli/commands/init.mjs +2 -2
- package/clients/cli/commands/integration-check.test.mjs +48 -0
- package/clients/cli/commands/integration.doc.mjs +1 -1
- package/clients/cli/commands/manifest.doc.mjs +1 -1
- package/clients/cli/commands/manifest.mjs +67 -0
- package/clients/cli/commands/outside-project.test.mjs +101 -1
- package/clients/cli/commands/postinstall.doc.mjs +23 -0
- package/clients/cli/commands/postinstall.mjs +31 -0
- package/clients/cli/commands/setup-nudge.test.mjs +78 -0
- package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
- package/clients/cli/commands/upgrade.doc.mjs +4 -2
- package/clients/cli/commands/upgrade.mjs +9 -2
- package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
- package/clients/cli/index.mjs +54 -94
- package/clients/cli/lib/component-format.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +12 -7
- package/clients/cli/lib/exit-codes.test.mjs +5 -0
- package/clients/cli/lib/manifest.test.mjs +12 -0
- package/clients/cli/lib/welcome-box.mjs +50 -0
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +32 -7
- package/foundation/agent-docs/agent-doc-state.mjs +58 -9
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1017 -105
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +683 -39
- package/foundation/agent-docs/computer-agent-text.d.mts +39 -2
- package/foundation/agent-docs/computer-agent-text.mjs +135 -8
- package/foundation/agent-docs/computer-agent-text.test.mjs +99 -2
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/authoring-self-docs.d.mts +7 -0
- package/foundation/discovery/authoring-self-docs.mjs +16 -4
- package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
- package/foundation/discovery/authoring-surface.d.mts +1 -1
- package/foundation/discovery/authoring-surface.mjs +2 -2
- package/foundation/discovery/authoring-surface.test.mjs +1 -1
- package/foundation/discovery/cli-self-docs.d.mts +2 -1
- package/foundation/discovery/cli-self-docs.mjs +4 -3
- package/foundation/discovery/cli-self-docs.test.mjs +2 -2
- package/foundation/discovery/component-discovery.d.mts +37 -0
- package/foundation/discovery/component-discovery.mjs +80 -5
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/docs-discovery.mjs +14 -1
- package/foundation/discovery/module-exports.d.mts +41 -0
- package/foundation/discovery/module-exports.mjs +253 -0
- package/foundation/discovery/runner-allowlist.d.mts +2 -4
- package/foundation/discovery/runner-allowlist.mjs +2 -4
- package/foundation/discovery/runner-commands.d.mts +77 -0
- package/foundation/discovery/runner-commands.mjs +294 -0
- package/foundation/discovery/runner-commands.test.mjs +128 -0
- package/foundation/discovery/stray-pointers.test.mjs +1 -1
- package/foundation/discovery/template-adapter.mjs +18 -1
- package/foundation/discovery/template-needs.mjs +1 -44
- package/foundation/discovery/theme-discovery.mjs +147 -3
- package/foundation/doc-compiler/doc-loads.test.mjs +33 -9
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
- package/foundation/doc-compiler/tree.d.mts +32 -6
- package/foundation/doc-compiler/tree.mjs +77 -43
- package/foundation/doc-compiler/tree.test.mjs +7 -7
- package/foundation/fs/paths.d.mts +70 -8
- package/foundation/fs/paths.mjs +556 -38
- package/foundation/fs/paths.test.mjs +430 -54
- package/foundation/fs/read-record.d.mts +18 -0
- package/foundation/fs/read-record.mjs +131 -0
- package/foundation/fs/read-record.test.mjs +77 -0
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/cli-peer-version.d.mts +36 -0
- package/foundation/integrations/cli-peer-version.mjs +132 -0
- package/foundation/integrations/cli-peer-version.test.mjs +354 -0
- package/foundation/integrations/computer-integrations.d.mts +6 -3
- package/foundation/integrations/computer-integrations.mjs +38 -9
- package/foundation/integrations/contribution-inventory.d.mts +15 -20
- package/foundation/integrations/contribution-inventory.mjs +41 -5
- package/foundation/integrations/exports-subpaths.d.mts +57 -0
- package/foundation/integrations/exports-subpaths.mjs +113 -0
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/public-imports.d.mts +118 -0
- package/foundation/integrations/public-imports.mjs +1769 -0
- package/foundation/integrations/public-imports.test.mjs +1793 -0
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/cache.test.mjs +24 -21
- package/foundation/release/global-state.d.mts +8 -4
- package/foundation/release/global-state.mjs +4 -2
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/notices.d.mts +62 -8
- package/foundation/release/notices.mjs +136 -25
- package/foundation/release/notices.test.mjs +21 -0
- package/foundation/release/run-context.mjs +14 -0
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/route/route.d.mts +285 -0
- package/foundation/route/route.mjs +324 -0
- package/foundation/route/route.test.mjs +153 -0
- package/foundation/runtime/hosted-runtime.test.mjs +403 -0
- package/foundation/runtime/locked-versions.d.mts +32 -0
- package/foundation/runtime/locked-versions.mjs +65 -0
- package/foundation/runtime/package-root.d.mts +91 -0
- package/foundation/runtime/package-root.mjs +174 -0
- package/foundation/runtime/package-set.d.mts +62 -0
- package/foundation/runtime/package-set.mjs +130 -0
- package/foundation/runtime/package-set.test.mjs +432 -0
- package/foundation/text/string-utils.d.mts +2 -1
- package/foundation/text/string-utils.mjs +16 -5
- package/foundation/text/suggest-order.d.mts +72 -0
- package/foundation/text/suggest-order.mjs +363 -0
- package/foundation/text/suggest-order.test.mjs +259 -0
- package/host/door-conformance.test.mjs +274 -0
- package/host/index.d.ts +116 -0
- package/host/index.mjs +295 -0
- package/host/index.test.mjs +302 -0
- package/package.json +14 -9
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Order the suggestions a failed lookup prints, best first.
|
|
5
|
+
*
|
|
6
|
+
* A failed `component`, `template` or `docs` lookup already knows which
|
|
7
|
+
* entries to suggest; this module decides their order, so the entry the
|
|
8
|
+
* caller most likely meant comes first. It never makes a list longer.
|
|
9
|
+
*
|
|
10
|
+
* Names are compared as words: `DropdownMenuItem`, `dropdown-menu-item` and
|
|
11
|
+
* `Dropdown Menu Items` all read as `dropdown menu item`.
|
|
12
|
+
*
|
|
13
|
+
* @input the name the caller typed and the candidate entries
|
|
14
|
+
* @output the entries, reordered
|
|
15
|
+
* @position lib: shared by the component, template and docs lookup errors
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {levenshteinDistance} from './levenshtein.mjs';
|
|
19
|
+
|
|
20
|
+
/** The score a synonym match gets: an exact keyword's. */
|
|
21
|
+
const SYNONYM_SCORE = 90;
|
|
22
|
+
/** The score a name with exactly the typed words gets: an exact name's. */
|
|
23
|
+
const SAME_WORDS_SCORE = 100;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A word with one trailing plural `s` removed (`tabs` → `tab`), leaving short
|
|
27
|
+
* words and `ss` endings alone.
|
|
28
|
+
* @param {string} word
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function singular(word) {
|
|
32
|
+
return word.length > 3 && word.endsWith('s') && !word.endsWith('ss')
|
|
33
|
+
? word.slice(0, -1)
|
|
34
|
+
: word;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A name's words, lowercase and singular: camelCase, PascalCase, kebab-case,
|
|
39
|
+
* snake_case, slashes and spaces all split.
|
|
40
|
+
* @param {string} text
|
|
41
|
+
* @returns {string[]}
|
|
42
|
+
*/
|
|
43
|
+
export function nameWords(text) {
|
|
44
|
+
/** @type {string[]} */
|
|
45
|
+
const words = [];
|
|
46
|
+
for (const part of String(text ?? '').split(/[^A-Za-z0-9]+/)) {
|
|
47
|
+
if (!part) continue;
|
|
48
|
+
const found =
|
|
49
|
+
part.match(/[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+/g) ?? [];
|
|
50
|
+
for (const w of found) words.push(singular(w.toLowerCase()));
|
|
51
|
+
}
|
|
52
|
+
return words;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How closely one candidate name matches what the caller typed; higher is
|
|
57
|
+
* closer. Tiers, best first: the same words; every typed word present; some
|
|
58
|
+
* typed word present; none. Within a tier, more of the typed words and more of
|
|
59
|
+
* the candidate's own words matched rank higher, and a name that starts with
|
|
60
|
+
* the typed name (or the reverse) gets a small lift.
|
|
61
|
+
* @param {string[]} typed - {@link nameWords} of the typed name
|
|
62
|
+
* @param {string} name
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
function affinityOf(typed, name) {
|
|
66
|
+
const words = nameWords(name);
|
|
67
|
+
if (typed.length === 0 || words.length === 0) return 0;
|
|
68
|
+
const typedJoined = typed.join('');
|
|
69
|
+
const joined = words.join('');
|
|
70
|
+
if (joined === typedJoined) return 40;
|
|
71
|
+
const have = new Set(words);
|
|
72
|
+
const want = new Set(typed);
|
|
73
|
+
const covered = typed.filter(w => have.has(w)).length / typed.length;
|
|
74
|
+
const precise = words.filter(w => want.has(w)).length / words.length;
|
|
75
|
+
const prefix =
|
|
76
|
+
joined.startsWith(typedJoined) || typedJoined.startsWith(joined) ? 1 : 0;
|
|
77
|
+
const tier = covered === 1 ? 2 : covered > 0 ? 1 : 0;
|
|
78
|
+
return tier * 10 + covered * 4 + precise * 2 + prefix + precise * 0.01;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The same items, closest to the typed name first; each item is as close as
|
|
83
|
+
* its closest name. Equal matches keep their original order.
|
|
84
|
+
* @template T
|
|
85
|
+
* @param {string} typedName
|
|
86
|
+
* @param {T[]} items
|
|
87
|
+
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
88
|
+
* @returns {T[]}
|
|
89
|
+
*/
|
|
90
|
+
export function orderByAffinity(typedName, items, namesOf) {
|
|
91
|
+
const typed = nameWords(typedName);
|
|
92
|
+
const scored = items.map((item, index) => {
|
|
93
|
+
let best = 0;
|
|
94
|
+
for (const name of namesOf(item)) {
|
|
95
|
+
if (name) best = Math.max(best, affinityOf(typed, name));
|
|
96
|
+
}
|
|
97
|
+
return {item, index, best};
|
|
98
|
+
});
|
|
99
|
+
scored.sort((a, b) => b.best - a.best || a.index - b.index);
|
|
100
|
+
return scored.map(s => s.item);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Docs route suggestions, closest to the typed route first. When every
|
|
105
|
+
* suggestion sits under one namespace the typed route also starts with, that
|
|
106
|
+
* shared part is left out of the comparison: it says nothing about which page
|
|
107
|
+
* was meant. Equal matches keep their order.
|
|
108
|
+
* @template {{name: string}} T
|
|
109
|
+
* @param {string} topic - the typed route
|
|
110
|
+
* @param {T[]} suggestions
|
|
111
|
+
* @returns {T[]}
|
|
112
|
+
*/
|
|
113
|
+
export function orderRouteSuggestions(topic, suggestions) {
|
|
114
|
+
let shared = suggestions.length > 0 ? suggestions[0].name : '';
|
|
115
|
+
for (const {name} of suggestions) {
|
|
116
|
+
while (shared && !name.startsWith(shared)) shared = shared.slice(0, -1);
|
|
117
|
+
}
|
|
118
|
+
const namespace = shared.slice(0, shared.lastIndexOf('/') + 1);
|
|
119
|
+
return namespace && topic.startsWith(namespace)
|
|
120
|
+
? orderByAffinity(topic.slice(namespace.length), suggestions, s => [
|
|
121
|
+
s.name.slice(namespace.length),
|
|
122
|
+
])
|
|
123
|
+
: orderByAffinity(topic, suggestions, s => [s.name]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Library names a synonym may start with (`MUI TextField`). */
|
|
127
|
+
const LIBRARY_WORDS = new Set([
|
|
128
|
+
'mui',
|
|
129
|
+
'material',
|
|
130
|
+
'chakra',
|
|
131
|
+
'ant',
|
|
132
|
+
'antd',
|
|
133
|
+
'radix',
|
|
134
|
+
'bootstrap',
|
|
135
|
+
'mantine',
|
|
136
|
+
'shadcn',
|
|
137
|
+
'headless',
|
|
138
|
+
'aria',
|
|
139
|
+
'react',
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The components whose synonyms name what was typed. A synonym matches when
|
|
144
|
+
* its words, with or without a leading library name, are the typed words
|
|
145
|
+
* (`MUI TextField` matches `MUI TextField`, `TextField` and `text-field`).
|
|
146
|
+
* @param {string} needle
|
|
147
|
+
* @param {Record<string, {synonyms?: string[]}>} cards - component name → its search card
|
|
148
|
+
* @returns {Set<string>}
|
|
149
|
+
*/
|
|
150
|
+
export function synonymMatches(needle, cards) {
|
|
151
|
+
const typed = nameWords(needle).join('');
|
|
152
|
+
/** @type {Set<string>} */
|
|
153
|
+
const found = new Set();
|
|
154
|
+
if (!typed) return found;
|
|
155
|
+
for (const [name, card] of Object.entries(cards ?? {})) {
|
|
156
|
+
for (const synonym of card?.synonyms ?? []) {
|
|
157
|
+
const words = nameWords(synonym);
|
|
158
|
+
const whole = words.join('');
|
|
159
|
+
while (words.length > 1 && LIBRARY_WORDS.has(words[0])) words.shift();
|
|
160
|
+
if (whole === typed || words.join('') === typed) {
|
|
161
|
+
found.add(name);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return found;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Words that name a structural part of a component (`<Family>Footer`). */
|
|
170
|
+
const PART_WORDS = new Set([
|
|
171
|
+
'header',
|
|
172
|
+
'footer',
|
|
173
|
+
'body',
|
|
174
|
+
'content',
|
|
175
|
+
'title',
|
|
176
|
+
'heading',
|
|
177
|
+
'description',
|
|
178
|
+
'item',
|
|
179
|
+
'trigger',
|
|
180
|
+
'action',
|
|
181
|
+
'close',
|
|
182
|
+
'group',
|
|
183
|
+
'cell',
|
|
184
|
+
'row',
|
|
185
|
+
'column',
|
|
186
|
+
'label',
|
|
187
|
+
'icon',
|
|
188
|
+
'root',
|
|
189
|
+
'wrapper',
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
/** Part words that name the same slot of a component family. */
|
|
193
|
+
const SAME_PART = /** @type {Record<string, string[]>} */ ({
|
|
194
|
+
item: ['item', 'option'],
|
|
195
|
+
option: ['option', 'item'],
|
|
196
|
+
header: ['header', 'heading', 'title'],
|
|
197
|
+
heading: ['heading', 'header', 'title'],
|
|
198
|
+
title: ['title', 'heading', 'header'],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A suggestion score for a component whose name shares the typed compound
|
|
203
|
+
* name's words, on the same scale as `searchComponents`, or null. It covers
|
|
204
|
+
* part names that do not exist: the family part named with other words
|
|
205
|
+
* (`PickerItem` → `PickerOption`), the part named without its family's
|
|
206
|
+
* first word (`RailItem` → `SideRailItem`), the part named without its family's
|
|
207
|
+
* middle word (`FactItem` → `FactListItem`), and the parent of a structural
|
|
208
|
+
* part that is not its own component (`SheetFooter` → `Sheet`, but not
|
|
209
|
+
* `SheetMusic`).
|
|
210
|
+
* @param {string[]} typed - {@link nameWords} of the typed name
|
|
211
|
+
* @param {string} name
|
|
212
|
+
* @returns {{score: number, reason: string} | null}
|
|
213
|
+
*/
|
|
214
|
+
function compoundMatch(typed, name) {
|
|
215
|
+
if (typed.length < 2) return null;
|
|
216
|
+
const words = nameWords(name);
|
|
217
|
+
const n = words.length;
|
|
218
|
+
const t = typed.length;
|
|
219
|
+
const same = (/** @type {string[]} */ a, /** @type {string[]} */ b) =>
|
|
220
|
+
a.length === b.length && a.every((w, i) => w === b[i]);
|
|
221
|
+
if (
|
|
222
|
+
n >= 2 &&
|
|
223
|
+
same(words.slice(0, -1), typed.slice(0, -1)) &&
|
|
224
|
+
(SAME_PART[typed[t - 1]] ?? []).includes(words[n - 1])
|
|
225
|
+
) {
|
|
226
|
+
return {score: 79, reason: 'same part of the family'};
|
|
227
|
+
}
|
|
228
|
+
if (n > t && same(words.slice(n - t), typed)) {
|
|
229
|
+
return {score: 78, reason: 'name ends with the words typed'};
|
|
230
|
+
}
|
|
231
|
+
if (n > t && words[0] === typed[0] && words[n - 1] === typed[t - 1]) {
|
|
232
|
+
return {score: 77, reason: 'same part of the family'};
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
n < t &&
|
|
236
|
+
same(typed.slice(0, n), words) &&
|
|
237
|
+
typed.slice(n).every(w => PART_WORDS.has(w))
|
|
238
|
+
) {
|
|
239
|
+
return {score: 72, reason: 'the family this part belongs to'};
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Rank component suggestions for a name that resolved to nothing.
|
|
246
|
+
*
|
|
247
|
+
* Every component `searchComponents` scored keeps its score. A component whose
|
|
248
|
+
* name has exactly the typed words (`chat-messages` for `ChatMessage`) scores
|
|
249
|
+
* like an exact name; one of whose synonyms is the typed name (see
|
|
250
|
+
* {@link synonymMatches}) like an exact keyword; one whose name shares the
|
|
251
|
+
* typed compound name's words (see {@link compoundMatch}) just below a
|
|
252
|
+
* one-letter typo of a name. Equal scores put synonym matches first, then order by how many of the typed words the
|
|
253
|
+
* name carries, then by how early the keyword that matched sits in the
|
|
254
|
+
* component's keywords, then by edit distance, then by shared leading letters,
|
|
255
|
+
* then by name. The caller decides how many to print.
|
|
256
|
+
* @param {string} needle - the typed component name (no `XDS` prefix)
|
|
257
|
+
* @param {Array<{name: string, score: number, reason: string, keywordAt?: number}>} results - `searchComponents` output
|
|
258
|
+
* @param {string[]} allNames - every component name
|
|
259
|
+
* @param {Set<string>} [synonyms] - {@link synonymMatches} for the typed name
|
|
260
|
+
* @returns {Array<{name: string, score: number, reason: string}>}
|
|
261
|
+
*/
|
|
262
|
+
export function rankComponentSuggestions(
|
|
263
|
+
needle,
|
|
264
|
+
results,
|
|
265
|
+
allNames,
|
|
266
|
+
synonyms = new Set(),
|
|
267
|
+
) {
|
|
268
|
+
const term = needle.toLowerCase();
|
|
269
|
+
const typed = nameWords(needle);
|
|
270
|
+
/** @type {Map<string, {name: string, score: number, reason: string, keywordAt: number}>} */
|
|
271
|
+
const byName = new Map();
|
|
272
|
+
for (const r of results) {
|
|
273
|
+
byName.set(r.name, {
|
|
274
|
+
name: r.name,
|
|
275
|
+
score: r.score,
|
|
276
|
+
reason: r.reason,
|
|
277
|
+
keywordAt: r.keywordAt ?? Infinity,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// The typed words are a name's words (`chat-messages` is `ChatMessage`):
|
|
281
|
+
// that name is meant, ahead of every rule below.
|
|
282
|
+
const joined = typed.join('');
|
|
283
|
+
for (const name of allNames) {
|
|
284
|
+
if (joined && nameWords(name).join('') === joined) {
|
|
285
|
+
byName.set(name, {
|
|
286
|
+
name,
|
|
287
|
+
score: SAME_WORDS_SCORE,
|
|
288
|
+
reason: 'same name',
|
|
289
|
+
keywordAt: -1,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const name of synonyms) {
|
|
294
|
+
if (!allNames.includes(name)) continue;
|
|
295
|
+
const prior = byName.get(name);
|
|
296
|
+
if (!prior || prior.score < SYNONYM_SCORE) {
|
|
297
|
+
byName.set(name, {
|
|
298
|
+
name,
|
|
299
|
+
score: SYNONYM_SCORE,
|
|
300
|
+
reason: 'another name for it',
|
|
301
|
+
keywordAt: Infinity,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const name of allNames) {
|
|
306
|
+
const match = compoundMatch(typed, name);
|
|
307
|
+
if (!match) continue;
|
|
308
|
+
const prior = byName.get(name);
|
|
309
|
+
if (!prior || match.score > prior.score) {
|
|
310
|
+
byName.set(name, {
|
|
311
|
+
name,
|
|
312
|
+
score: match.score,
|
|
313
|
+
reason: match.reason,
|
|
314
|
+
keywordAt: Infinity,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** @param {string} name */
|
|
319
|
+
const lead = name => {
|
|
320
|
+
const lower = name.toLowerCase();
|
|
321
|
+
let i = 0;
|
|
322
|
+
while (i < term.length && i < lower.length && term[i] === lower[i]) i++;
|
|
323
|
+
return i / Math.max(term.length, lower.length);
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* How many typed words the name carries, whole or as a word's start; the
|
|
327
|
+
* last typed word, the one a compound name is a kind of (`PlaceInput` is an
|
|
328
|
+
* input), counts a little more.
|
|
329
|
+
*/
|
|
330
|
+
const shared = (/** @type {string} */ name) => {
|
|
331
|
+
const words = nameWords(name);
|
|
332
|
+
const has = (/** @type {string} */ t) =>
|
|
333
|
+
words.some(
|
|
334
|
+
w =>
|
|
335
|
+
w === t ||
|
|
336
|
+
(Math.min(w.length, t.length) >= 3 &&
|
|
337
|
+
(w.startsWith(t) || t.startsWith(w))),
|
|
338
|
+
);
|
|
339
|
+
const count = typed.filter(has).length;
|
|
340
|
+
return typed.length > 1 && has(typed[typed.length - 1])
|
|
341
|
+
? count + 0.5
|
|
342
|
+
: count;
|
|
343
|
+
};
|
|
344
|
+
return [...byName.values()]
|
|
345
|
+
.map(r => ({
|
|
346
|
+
...r,
|
|
347
|
+
synonym: synonyms.has(r.name) ? 1 : 0,
|
|
348
|
+
shared: shared(r.name),
|
|
349
|
+
distance: levenshteinDistance(term, r.name.toLowerCase()),
|
|
350
|
+
lead: lead(r.name),
|
|
351
|
+
}))
|
|
352
|
+
.sort(
|
|
353
|
+
(a, b) =>
|
|
354
|
+
b.score - a.score ||
|
|
355
|
+
b.synonym - a.synonym ||
|
|
356
|
+
b.shared - a.shared ||
|
|
357
|
+
a.keywordAt - b.keywordAt ||
|
|
358
|
+
a.distance - b.distance ||
|
|
359
|
+
b.lead - a.lead ||
|
|
360
|
+
a.name.localeCompare(b.name),
|
|
361
|
+
)
|
|
362
|
+
.map(({name, score, reason}) => ({name, score, reason}));
|
|
363
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
nameWords,
|
|
6
|
+
orderByAffinity,
|
|
7
|
+
orderRouteSuggestions,
|
|
8
|
+
rankComponentSuggestions,
|
|
9
|
+
synonymMatches,
|
|
10
|
+
} from './suggest-order.mjs';
|
|
11
|
+
|
|
12
|
+
describe('nameWords', () => {
|
|
13
|
+
it('reads every spelling of a name as the same words', () => {
|
|
14
|
+
expect(nameWords('DropdownMenuItems')).toEqual([
|
|
15
|
+
'dropdown',
|
|
16
|
+
'menu',
|
|
17
|
+
'item',
|
|
18
|
+
]);
|
|
19
|
+
expect(nameWords('dropdown-menu-item')).toEqual([
|
|
20
|
+
'dropdown',
|
|
21
|
+
'menu',
|
|
22
|
+
'item',
|
|
23
|
+
]);
|
|
24
|
+
expect(nameWords('Dropdown Menu Item')).toEqual([
|
|
25
|
+
'dropdown',
|
|
26
|
+
'menu',
|
|
27
|
+
'item',
|
|
28
|
+
]);
|
|
29
|
+
expect(nameWords('cli/api_HTMLParser')).toEqual([
|
|
30
|
+
'cli',
|
|
31
|
+
'api',
|
|
32
|
+
'html',
|
|
33
|
+
'parser',
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('keeps short words and double-s endings', () => {
|
|
38
|
+
expect(nameWords('Gas')).toEqual(['gas']);
|
|
39
|
+
expect(nameWords('Glass')).toEqual(['glass']);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('orderByAffinity', () => {
|
|
44
|
+
const names = (/** @type {string[]} */ list, /** @type {string} */ typed) =>
|
|
45
|
+
orderByAffinity(typed, list, n => [n]);
|
|
46
|
+
|
|
47
|
+
it('puts the same words first, then every typed word, then some', () => {
|
|
48
|
+
expect(
|
|
49
|
+
names(
|
|
50
|
+
['zeta-panel', 'wide-panel-grid', 'panel-grid', 'grid-panel-x'],
|
|
51
|
+
'PanelGrid',
|
|
52
|
+
),
|
|
53
|
+
).toEqual(['panel-grid', 'wide-panel-grid', 'grid-panel-x', 'zeta-panel']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('matches a title as well as an id', () => {
|
|
57
|
+
const items = [
|
|
58
|
+
{id: 'alpha', title: 'Something Else'},
|
|
59
|
+
{id: 'beta', title: 'Wave Editor'},
|
|
60
|
+
];
|
|
61
|
+
expect(
|
|
62
|
+
orderByAffinity('Wave Editor', items, i => [i.id, i.title]).map(
|
|
63
|
+
i => i.id,
|
|
64
|
+
),
|
|
65
|
+
).toEqual(['beta', 'alpha']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('keeps the original order between equal matches and never changes the list', () => {
|
|
69
|
+
const list = ['c', 'b', 'a', 'route/b-thing', 'route/a-thing'];
|
|
70
|
+
const out = names(list, 'thing');
|
|
71
|
+
expect(out).toEqual(['route/b-thing', 'route/a-thing', 'c', 'b', 'a']);
|
|
72
|
+
expect([...out].sort()).toEqual([...list].sort());
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('ranks a route by its words', () => {
|
|
76
|
+
expect(
|
|
77
|
+
names(
|
|
78
|
+
['basics/basics-color', 'basics/basics-size', 'basics/basics-usage'],
|
|
79
|
+
'basics/usage',
|
|
80
|
+
),
|
|
81
|
+
).toEqual([
|
|
82
|
+
'basics/basics-usage',
|
|
83
|
+
'basics/basics-color',
|
|
84
|
+
'basics/basics-size',
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('rankComponentSuggestions', () => {
|
|
90
|
+
const all = [
|
|
91
|
+
'Zebra',
|
|
92
|
+
'Picker',
|
|
93
|
+
'MultiPicker',
|
|
94
|
+
'PickerOption',
|
|
95
|
+
'Gadget',
|
|
96
|
+
'SideRailItem',
|
|
97
|
+
'Sheet',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
it('breaks a tie by where the typed name sits in the keywords', () => {
|
|
101
|
+
const results = [
|
|
102
|
+
{name: 'MultiPicker', score: 90, reason: 'keyword', keywordAt: 3},
|
|
103
|
+
{name: 'Picker', score: 90, reason: 'keyword', keywordAt: 0},
|
|
104
|
+
{name: 'Gadget', score: 90, reason: 'keyword', keywordAt: 5},
|
|
105
|
+
];
|
|
106
|
+
expect(
|
|
107
|
+
rankComponentSuggestions('Pick', results, all).map(r => r.name),
|
|
108
|
+
).toEqual(['Picker', 'MultiPicker', 'Gadget']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('breaks a remaining tie by edit distance, not by the alphabet', () => {
|
|
112
|
+
const results = [
|
|
113
|
+
{name: 'Gadget', score: 70, reason: 'keyword'},
|
|
114
|
+
{name: 'Sheet', score: 70, reason: 'keyword'},
|
|
115
|
+
];
|
|
116
|
+
expect(
|
|
117
|
+
rankComponentSuggestions('Sheets', results, all).map(r => r.name),
|
|
118
|
+
).toEqual(['Sheet', 'Gadget']);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('finds the family part named with other words', () => {
|
|
122
|
+
const results = [{name: 'Picker', score: 70, reason: 'keyword'}];
|
|
123
|
+
expect(rankComponentSuggestions('PickerItem', results, all)[0].name).toBe(
|
|
124
|
+
'PickerOption',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('finds the part named without its first word', () => {
|
|
129
|
+
expect(rankComponentSuggestions('RailItem', [], all)[0].name).toBe(
|
|
130
|
+
'SideRailItem',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('finds the family of a part that is not its own component', () => {
|
|
135
|
+
expect(rankComponentSuggestions('SheetFooter', [], all)[0].name).toBe(
|
|
136
|
+
'Sheet',
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('puts a name with exactly the typed words first, ahead of every rule', () => {
|
|
141
|
+
const results = [{name: 'Picker', score: 80, reason: 'similar name'}];
|
|
142
|
+
const ranked = rankComponentSuggestions(
|
|
143
|
+
'pickers',
|
|
144
|
+
results,
|
|
145
|
+
all,
|
|
146
|
+
new Set(['Gadget']),
|
|
147
|
+
);
|
|
148
|
+
expect(ranked[0].name).toBe('Picker');
|
|
149
|
+
expect(rankComponentSuggestions('side-rail-item', [], all)[0].name).toBe(
|
|
150
|
+
'SideRailItem',
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('names a family only for a structural part', () => {
|
|
155
|
+
expect(
|
|
156
|
+
rankComponentSuggestions('SheetMusic', [], all).map(r => r.name),
|
|
157
|
+
).not.toContain('Sheet');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('does not let a lifted entry borrow a weaker keyword position', () => {
|
|
161
|
+
const results = [
|
|
162
|
+
{name: 'Picker', score: 90, reason: 'keyword', keywordAt: 2},
|
|
163
|
+
{name: 'MultiPicker', score: 70, reason: 'keyword', keywordAt: 0},
|
|
164
|
+
];
|
|
165
|
+
const ranked = rankComponentSuggestions(
|
|
166
|
+
'Pick',
|
|
167
|
+
results,
|
|
168
|
+
all,
|
|
169
|
+
new Set(['Picker', 'MultiPicker']),
|
|
170
|
+
);
|
|
171
|
+
expect(ranked.map(r => r.name).slice(0, 2)).toEqual([
|
|
172
|
+
'Picker',
|
|
173
|
+
'MultiPicker',
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('never lowers a score search gave', () => {
|
|
178
|
+
const results = [{name: 'Picker', score: 100, reason: 'exact name'}];
|
|
179
|
+
const ranked = rankComponentSuggestions('PickerItem', results, all);
|
|
180
|
+
expect(ranked[0]).toEqual({
|
|
181
|
+
name: 'Picker',
|
|
182
|
+
score: 100,
|
|
183
|
+
reason: 'exact name',
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('suggests only catalog names', () => {
|
|
188
|
+
const ranked = rankComponentSuggestions('PickerWidgetItem', [], all);
|
|
189
|
+
for (const r of ranked) expect(all).toContain(r.name);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('synonymMatches', () => {
|
|
194
|
+
const cards = {
|
|
195
|
+
Picker: {synonyms: ['choice box', 'Acme ChoiceBox', 'MUI Choice Field']},
|
|
196
|
+
Sheet: {synonyms: ['side sheet', 'choice']},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
it('matches a synonym in any spelling, without a leading library name', () => {
|
|
200
|
+
expect([...synonymMatches('ChoiceField', cards)]).toEqual(['Picker']);
|
|
201
|
+
expect([...synonymMatches('choice-box', cards)]).toEqual(['Picker']);
|
|
202
|
+
expect([...synonymMatches('SideSheets', cards)]).toEqual(['Sheet']);
|
|
203
|
+
expect([...synonymMatches('MUI Choice Field', cards)]).toEqual(['Picker']);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('needs every word to match', () => {
|
|
207
|
+
expect([...synonymMatches('Side', cards)]).toEqual([]);
|
|
208
|
+
expect([...synonymMatches('', cards)]).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('puts a synonym match first among equal scores, and only real names', () => {
|
|
212
|
+
const all = ['Gadget', 'Picker'];
|
|
213
|
+
const results = [
|
|
214
|
+
{name: 'Gadget', score: 90, reason: 'keyword', keywordAt: 0},
|
|
215
|
+
];
|
|
216
|
+
const ranked = rankComponentSuggestions(
|
|
217
|
+
'ChoiceField',
|
|
218
|
+
results,
|
|
219
|
+
all,
|
|
220
|
+
new Set(['Picker', 'NotInCatalog']),
|
|
221
|
+
);
|
|
222
|
+
expect(ranked.map(r => r.name)).toEqual(['Picker', 'Gadget']);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('orderRouteSuggestions', () => {
|
|
227
|
+
const kids = [
|
|
228
|
+
'guide/start',
|
|
229
|
+
'guide/building-blocks',
|
|
230
|
+
'guide/ship',
|
|
231
|
+
'guide/help',
|
|
232
|
+
].map(name => ({name}));
|
|
233
|
+
const names = (/** @type {{name: string}[]} */ list) => list.map(s => s.name);
|
|
234
|
+
|
|
235
|
+
it('keeps tree order when no child matches, at any depth', () => {
|
|
236
|
+
expect(names(orderRouteSuggestions('guide/theme', kids))).toEqual(
|
|
237
|
+
names(kids),
|
|
238
|
+
);
|
|
239
|
+
expect(names(orderRouteSuggestions('guide/theme/colors', kids))).toEqual(
|
|
240
|
+
names(kids),
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('puts the child the route names first', () => {
|
|
245
|
+
expect(names(orderRouteSuggestions('guide/ship-it', kids))[0]).toBe(
|
|
246
|
+
'guide/ship',
|
|
247
|
+
);
|
|
248
|
+
expect(names(orderRouteSuggestions('guide/help/extra', kids))[0]).toBe(
|
|
249
|
+
'guide/help',
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('compares whole routes when the suggestions share no namespace', () => {
|
|
254
|
+
const roots = [{name: 'cli'}, {name: 'guide'}, {name: 'theme'}];
|
|
255
|
+
expect(names(orderRouteSuggestions('theme/quick-start', roots))[0]).toBe(
|
|
256
|
+
'theme',
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
});
|