@astryxdesign/cli 0.6.4-canary.f0355e3 → 0.6.4
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 +96 -99
- package/api/build/build.doc.mjs +1 -6
- package/api/build/build.test.mjs +0 -22
- package/api/build/kit/kit.mjs +5 -44
- package/api/component/_adapter.d.mts +0 -25
- package/api/component/_adapter.mjs +5 -59
- package/api/component/component.d.mts +3 -6
- package/api/component/component.doc.mjs +17 -37
- package/api/component/component.mjs +9 -249
- package/api/component/component.type.d.mts +0 -25
- package/api/component/component.type.mjs +0 -44
- package/api/discover/_adapter.d.mts +6 -114
- package/api/discover/_adapter.mjs +17 -372
- package/api/discover/detail/detail.d.mts +6 -18
- package/api/discover/detail/detail.mjs +13 -67
- package/api/discover/detail/detail.test.mjs +0 -85
- package/api/discover/discover.d.mts +9 -3
- package/api/discover/discover.doc.mjs +18 -61
- package/api/discover/discover.mjs +36 -220
- package/api/discover/discover.test.mjs +2 -11
- package/api/discover/discover.type.d.mts +8 -147
- package/api/discover/discover.type.mjs +12 -102
- package/api/discover/list/list.d.mts +6 -20
- package/api/discover/list/list.mjs +12 -45
- package/api/discover/list/list.test.mjs +0 -46
- package/api/discover/search/search.d.mts +16 -18
- package/api/discover/search/search.mjs +56 -102
- package/api/discover/search/search.test.mjs +10 -144
- package/api/docs/_adapter.d.mts +3 -8
- package/api/docs/_adapter.mjs +6 -14
- package/api/docs/docOverlays.test.mjs +1 -27
- package/api/docs/docs.doc.mjs +2 -2
- package/api/docs/docs.test.mjs +243 -0
- package/api/docs/integration-tree.test.mjs +555 -0
- package/api/docs/integrationDocs.test.mjs +314 -0
- package/api/doctor/doctor.d.mts +3 -8
- package/api/doctor/doctor.doc.mjs +8 -17
- package/api/doctor/doctor.mjs +9 -90
- package/api/doctor/doctor.test.mjs +10 -122
- package/api/doctor/doctor.type.d.mts +1 -1
- package/api/doctor/doctor.type.mjs +1 -1
- package/api/gap-report/gap-report.doc.mjs +10 -19
- package/api/hook/hook.doc.mjs +3 -6
- package/api/index.d.mts +2 -1
- package/api/index.mjs +5 -5
- package/api/init/init.doc.mjs +12 -17
- package/api/integration/add-helpers.d.mts +2 -5
- package/api/integration/add-helpers.mjs +9 -36
- package/api/integration/add-theme.mjs +1 -22
- package/api/integration/add-theme.test.mjs +0 -34
- package/api/integration/authoring-checks.mjs +2 -2
- package/api/integration/integrationPackCheck.doc.mjs +3 -3
- package/api/integration/pack-check.mjs +9 -82
- package/api/integration/pack-check.test.mjs +0 -90
- package/api/integration/pack-check.type.mjs +1 -1
- package/api/json/assertResponse.doc.mjs +1 -1
- package/api/json/index.ts +1 -0
- package/api/json/isError.doc.mjs +1 -1
- package/api/layout/_adapter.d.mts +34 -0
- package/api/layout/_adapter.mjs +148 -0
- package/api/layout/check/check.d.mts +16 -0
- package/api/layout/check/check.mjs +40 -0
- package/api/layout/expand/expand.d.mts +22 -0
- package/api/layout/expand/expand.mjs +155 -0
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.d.mts +13 -0
- package/api/layout/grammar/grammar.mjs +87 -0
- package/api/layout/layout.d.mts +6 -0
- package/api/layout/layout.mjs +17 -0
- package/api/layout/layout.test.mjs +297 -0
- package/api/layout/layout.type.d.mts +89 -0
- package/api/layout/layout.type.mjs +103 -0
- package/api/layout/layoutCheck.doc.d.mts +11 -0
- package/api/layout/layoutCheck.doc.mjs +85 -0
- package/api/layout/layoutExpand.doc.d.mts +11 -0
- package/api/layout/layoutExpand.doc.mjs +107 -0
- package/api/layout/layoutGrammar.doc.d.mts +11 -0
- package/api/layout/layoutGrammar.doc.mjs +57 -0
- package/api/search/search.d.mts +1 -27
- package/api/search/search.doc.mjs +2 -2
- package/api/search/search.mjs +16 -228
- package/api/search/search.test.mjs +512 -0
- package/api/swizzle/swizzle.doc.mjs +5 -7
- package/api/template/copy/copy.mjs +1 -1
- package/api/template/copy/copy.test.mjs +0 -9
- package/api/template/template-integration.test.mjs +65 -1
- package/api/template/template.doc.mjs +1 -2
- package/api/template/template.mjs +1 -1
- package/api/theme/add/add.mjs +25 -17
- package/api/theme/add/add.staging.test.mjs +23 -40
- package/api/theme/build/build.family.test.mjs +12 -7
- package/api/theme/build/build.mjs +18 -8
- package/api/theme/generateTonalPalette.doc.mjs +2 -1
- package/api/theme/listThemes.doc.mjs +1 -1
- package/api/theme/themeAdd.doc.mjs +10 -9
- package/api/theme/themeBuild.doc.mjs +13 -13
- package/api/theme/themeList.doc.mjs +1 -1
- package/api/theme/themeListAvailable.doc.mjs +1 -2
- package/api/theme/themePaletteGenerate.doc.mjs +8 -15
- package/api/theme/themeTargets.doc.mjs +2 -3
- package/api/theme/themeTemplate.doc.mjs +1 -2
- package/api/upgrade/run/run.mjs +4 -6
- package/api/upgrade/upgrade.doc.mjs +22 -24
- package/api/upgrade/upgrade.type.mjs +2 -2
- package/assets/codemods/__tests__/runner.test.mjs +1 -3
- package/assets/codemods/integration-runner.mjs +3 -3
- package/assets/codemods/runner.mjs +4 -5
- package/assets/docs/README.md +2 -4
- package/assets/docs/browser-support.doc.mjs +11 -11
- package/assets/docs/color.doc.mjs +2 -8
- package/assets/docs/elevation.doc.mjs +4 -6
- package/assets/docs/getting-started.doc.mjs +16 -5
- package/assets/docs/icons.doc.mjs +21 -2
- package/assets/docs/illustrations.doc.mjs +15 -7
- package/assets/docs/internationalization.doc.mjs +5 -7
- package/assets/docs/layout.doc.dense.mjs +82 -130
- package/assets/docs/layout.doc.mjs +77 -133
- package/assets/docs/migration.doc.mjs +21 -19
- package/assets/docs/motion.doc.mjs +3 -16
- package/assets/docs/principles.doc.dense.mjs +5 -5
- package/assets/docs/principles.doc.mjs +0 -8
- package/assets/docs/principles.doc.zh.mjs +6 -6
- package/assets/docs/shape.doc.mjs +3 -8
- package/assets/docs/spacing.doc.mjs +2 -7
- package/assets/docs/styling-libraries.doc.mjs +2 -6
- package/assets/docs/styling.doc.mjs +23 -19
- package/assets/docs/theme.doc.dense.mjs +18 -58
- package/assets/docs/theme.doc.mjs +46 -56
- package/assets/docs/theme.doc.zh.mjs +8 -9
- package/assets/docs/tokens.doc.dense.mjs +2 -2
- package/assets/docs/tokens.doc.mjs +8 -389
- package/assets/docs/tokens.doc.zh.mjs +2 -2
- package/assets/docs/tree/integrations.doc.mjs +451 -25
- package/assets/docs/tree/integrations.test.mjs +62 -0
- package/assets/docs/tree/writing-docs.doc.mjs +286 -0
- package/assets/docs/typography.doc.mjs +4 -24
- package/assets/docs/working-with-ai.doc.mjs +22 -30
- package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
- package/authoring/config/config.doc.mjs +2 -10
- package/authoring/config/parse.d.mts +0 -2
- package/authoring/config/parse.mjs +0 -19
- package/authoring/config/parse.test.mjs +0 -8
- package/authoring/config/type.ts +2 -13
- package/authoring/doctypes/_schema.d.mts +2 -3
- package/authoring/doctypes/_schema.mjs +0 -6
- package/authoring/doctypes/base/graph-fields.doc.mjs +3 -3
- package/authoring/doctypes/base/type.ts +2 -4
- package/authoring/doctypes/command/command.doc.mjs +1 -1
- package/authoring/doctypes/command/type.ts +1 -1
- package/authoring/doctypes/component/component.doc.mjs +0 -6
- package/authoring/doctypes/component/type.ts +0 -8
- package/authoring/doctypes/reference/reference.doc.mjs +0 -7
- package/authoring/doctypes/reference/type.ts +0 -5
- package/authoring/doctypes/schema/schema.doc.mjs +2 -2
- package/authoring/doctypes/template/template.doc.mjs +1 -1
- package/authoring/doctypes/template/type.ts +2 -2
- package/authoring/index.d.mts +0 -1
- package/authoring/index.d.ts +0 -10
- package/authoring/index.mjs +0 -1
- package/authoring/integration/integration.doc.mjs +10 -12
- package/clients/cli/command-result-coverage.test.mjs +7 -7
- package/clients/cli/commands/component/index.mjs +55 -152
- package/clients/cli/commands/component-ownership.test.mjs +0 -89
- package/clients/cli/commands/component.doc.mjs +9 -27
- package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
- package/clients/cli/commands/discover.doc.mjs +9 -53
- package/clients/cli/commands/discover.mjs +118 -393
- package/clients/cli/commands/docs.doc.mjs +1 -1
- package/clients/cli/commands/docs.mjs +17 -60
- package/clients/cli/commands/docs.test.mjs +294 -0
- package/clients/cli/commands/doctor-integration-docs.doc.mjs +2 -3
- package/clients/cli/commands/doctor-integration.test.mjs +0 -53
- package/clients/cli/commands/doctor.doc.mjs +1 -3
- package/clients/cli/commands/doctor.mjs +5 -49
- package/clients/cli/commands/gap-report.doc.mjs +9 -10
- package/clients/cli/commands/init.doc.mjs +6 -9
- package/clients/cli/commands/integration-add.doc.mjs +9 -9
- package/clients/cli/commands/integration-authoring.test.mjs +10 -61
- package/clients/cli/commands/integration-pack.doc.mjs +9 -5
- package/clients/cli/commands/integration-real-world.test.mjs +1 -1
- package/clients/cli/commands/integration.doc.mjs +4 -4
- package/clients/cli/commands/integration.mjs +43 -74
- package/clients/cli/commands/layout-check.doc.mjs +65 -0
- package/clients/cli/commands/layout-expand.doc.mjs +83 -0
- package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
- package/clients/cli/commands/layout.doc.mjs +34 -0
- package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
- package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
- package/clients/cli/commands/layout.mjs +275 -0
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/manifest.doc.mjs +1 -1
- package/clients/cli/commands/search.doc.mjs +3 -10
- package/clients/cli/commands/search.mjs +2 -21
- package/clients/cli/commands/search.test.mjs +4 -21
- package/clients/cli/commands/swizzle.doc.mjs +1 -1
- package/clients/cli/commands/template.doc.mjs +1 -1
- package/clients/cli/commands/text-json-parity.test.mjs +16 -5
- package/clients/cli/commands/theme-add.doc.mjs +1 -1
- package/clients/cli/commands/theme-palette-generate.doc.mjs +2 -3
- package/clients/cli/commands/theme-palette.doc.mjs +2 -1
- package/clients/cli/commands/theme-targets.doc.mjs +2 -2
- package/clients/cli/commands/theme.doc.mjs +1 -2
- package/clients/cli/commands/upgrade.doc.mjs +3 -62
- package/clients/cli/index.mjs +10 -28
- package/clients/cli/lib/define-command.mjs +4 -28
- package/clients/cli/lib/define-command.test.mjs +0 -54
- package/clients/cli/lib/exit-codes.test.mjs +9 -18
- package/clients/cli/lib/json-shim.mjs +14 -24
- package/clients/cli/lib/json-shim.test.mjs +20 -6
- package/clients/cli/lib/manifest.mjs +13 -18
- package/clients/cli/lib/manifest.test.mjs +2 -5
- package/foundation/agent-docs/agent-docs.mjs +1 -1
- package/foundation/agent-docs/agent-docs.test.mjs +1159 -0
- package/foundation/discovery/authoring-self-docs.mjs +0 -1
- package/foundation/discovery/authoring-self-docs.test.mjs +2 -6
- package/foundation/discovery/cli-self-docs.mjs +2 -16
- package/foundation/discovery/cli-self-docs.test.mjs +0 -20
- package/foundation/discovery/docs-discovery.mjs +1 -5
- package/foundation/discovery/docs-discovery.test.mjs +0 -21
- package/foundation/discovery/docs-section-key.d.mts +1 -1
- package/foundation/discovery/docs-section-key.mjs +1 -1
- package/foundation/discovery/template-adapter.mjs +1 -1
- package/foundation/doc-compiler/doc-loads.test.mjs +14 -3
- package/foundation/doc-compiler/tree.d.mts +0 -4
- package/foundation/doc-compiler/tree.mjs +1 -6
- package/foundation/doc-compiler/tree.test.mjs +598 -0
- package/foundation/integrations/cli-requirement.d.mts +6 -26
- package/foundation/integrations/cli-requirement.mjs +11 -46
- package/foundation/integrations/cli-requirement.test.mjs +2 -7
- package/foundation/integrations/contribution-inventory.mjs +1 -1
- package/foundation/integrations/integrations.d.mts +1 -14
- package/foundation/integrations/integrations.mjs +1 -41
- package/foundation/integrations/integrations.test.mjs +0 -31
- package/foundation/response/error-codes.doc.mjs +8 -6
- package/foundation/response/error-codes.test.mjs +5 -30
- package/foundation/response/response-types.doc.d.mts +3 -4
- package/foundation/response/response-types.doc.mjs +27 -40
- package/foundation/response/response-types.doc.test.mjs +0 -23
- package/foundation/response/response.doc.mjs +10 -11
- package/foundation/xle/browser.d.mts +3 -3
- package/foundation/xle/browser.mjs +3 -3
- package/foundation/xle/expand.mjs +2 -2
- package/foundation/xle/parse.mjs +1 -1
- package/foundation/xle/print.mjs +2 -2
- package/foundation/xle/splice.mjs +1 -1
- package/package.json +9 -9
- package/api/discover/_adapter.test.mjs +0 -215
- package/api/discover/_catalog-view.d.mts +0 -115
- package/api/discover/_catalog-view.mjs +0 -203
- package/api/discover/_catalog-view.test.mjs +0 -128
- package/api/discover/detail/item/item.d.mts +0 -26
- package/api/discover/detail/item/item.mjs +0 -78
- package/api/discover/detail/item/item.test.mjs +0 -73
- package/api/integration/pack-check.lifecycle-output.test.mjs +0 -107
- package/api/theme/add/add.rollback.test.mjs +0 -158
- package/api/theme/build/build.rollback.test.mjs +0 -148
- package/api/upgrade/run/files-changed.test.mjs +0 -111
- package/assets/codemods/file-count.test.mjs +0 -163
- package/assets/docs/tree/add-a-component.doc.mjs +0 -75
- package/assets/docs/tree/add-a-theme.doc.mjs +0 -85
- package/assets/docs/tree/add-a-topic.doc.mjs +0 -144
- package/assets/docs/tree/agent-guidance.doc.mjs +0 -138
- package/assets/docs/tree/block-template.doc.mjs +0 -130
- package/assets/docs/tree/build-the-template.doc.mjs +0 -28
- package/assets/docs/tree/building-blocks.doc.mjs +0 -46
- package/assets/docs/tree/check-your-docs.doc.mjs +0 -137
- package/assets/docs/tree/checks.doc.mjs +0 -119
- package/assets/docs/tree/codemods.doc.mjs +0 -147
- package/assets/docs/tree/component-family.doc.mjs +0 -113
- package/assets/docs/tree/component-imports.doc.mjs +0 -69
- package/assets/docs/tree/component-lookups.doc.mjs +0 -149
- package/assets/docs/tree/components.doc.mjs +0 -23
- package/assets/docs/tree/configuration.doc.mjs +0 -23
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +0 -182
- package/assets/docs/tree/define-the-theme.doc.mjs +0 -118
- package/assets/docs/tree/describe-the-component.doc.mjs +0 -57
- package/assets/docs/tree/docs.doc.mjs +0 -21
- package/assets/docs/tree/document-the-template.doc.mjs +0 -28
- package/assets/docs/tree/document-the-theme.doc.mjs +0 -68
- package/assets/docs/tree/export-template-assets.doc.mjs +0 -147
- package/assets/docs/tree/extend-or-replace.doc.mjs +0 -103
- package/assets/docs/tree/fonts-and-assets.doc.mjs +0 -106
- package/assets/docs/tree/generate-a-palette.doc.mjs +0 -66
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +0 -105
- package/assets/docs/tree/help.doc.mjs +0 -16
- package/assets/docs/tree/links.doc.mjs +0 -98
- package/assets/docs/tree/package-and-test.doc.mjs +0 -32
- package/assets/docs/tree/page-template.doc.mjs +0 -71
- package/assets/docs/tree/publishing.doc.mjs +0 -111
- package/assets/docs/tree/quick-start.doc.mjs +0 -272
- package/assets/docs/tree/replace-a-core-component.doc.mjs +0 -104
- package/assets/docs/tree/replace-a-core-template.doc.mjs +0 -172
- package/assets/docs/tree/sections-and-placement.doc.mjs +0 -108
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +0 -59
- package/assets/docs/tree/ship.doc.mjs +0 -16
- package/assets/docs/tree/short-and-findable.doc.mjs +0 -108
- package/assets/docs/tree/single-component.doc.mjs +0 -165
- package/assets/docs/tree/start-a-template.doc.mjs +0 -143
- package/assets/docs/tree/subcomponent.doc.mjs +0 -115
- package/assets/docs/tree/template-assets.doc.mjs +0 -64
- package/assets/docs/tree/template-doc-overview.doc.mjs +0 -109
- package/assets/docs/tree/template-fonts.doc.mjs +0 -102
- package/assets/docs/tree/template-grading-rubric.doc.mjs +0 -452
- package/assets/docs/tree/template-icons.doc.mjs +0 -97
- package/assets/docs/tree/template-images-media.doc.mjs +0 -127
- package/assets/docs/tree/template-styles.doc.mjs +0 -93
- package/assets/docs/tree/templates.doc.mjs +0 -34
- package/assets/docs/tree/test-in-an-app.doc.mjs +0 -115
- package/assets/docs/tree/test-template-in-app.doc.mjs +0 -128
- package/assets/docs/tree/themes.doc.mjs +0 -39
- package/assets/docs/tree/troubleshooting.doc.mjs +0 -149
- package/assets/docs/tree/upgrading.doc.mjs +0 -103
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +0 -51
- package/assets/docs/tree/verify-packed-template.doc.mjs +0 -77
- package/assets/docs/tree/versioning.doc.mjs +0 -161
- package/assets/docs/tree/write-good-templates.doc.mjs +0 -64
- package/assets/docs/tree/write-the-template-file.doc.mjs +0 -154
- package/authoring/discover/discover.doc.d.mts +0 -13
- package/authoring/discover/discover.doc.mjs +0 -138
- package/authoring/discover/parse.d.mts +0 -24
- package/authoring/discover/parse.mjs +0 -128
- package/authoring/discover/parse.test.mjs +0 -124
- package/authoring/discover/type.ts +0 -87
- package/clients/cli/commands/component-batch.test.mjs +0 -341
- package/clients/cli/commands/discover.sources.test.mjs +0 -267
- package/clients/cli/commands/integration-verify.doc.mjs +0 -22
- package/clients/cli/lib/parse-error-format.test.mjs +0 -81
- package/foundation/response/batch.type.d.mts +0 -33
- package/foundation/response/batch.type.mjs +0 -34
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Colocated tests for the `search` leaf (api/search/search.mjs), run
|
|
5
|
+
* against the real @astryxdesign/core registry. `search` had no api-level tests;
|
|
6
|
+
* this locks the envelope, ranking invariants, the `--type`/limit handling, and
|
|
7
|
+
* the error paths.
|
|
8
|
+
*
|
|
9
|
+
* The API validates its own inputs (not just the CLI): a non-positive/non-integer
|
|
10
|
+
* `limit`, an empty query, and a bad `--type` all throw AstryxError with the
|
|
11
|
+
* ERR_INVALID_ARGUMENT code, so a direct `@astryxdesign/cli/api` caller gets the
|
|
12
|
+
* same contract as `astryx search` on the command line.
|
|
13
|
+
*
|
|
14
|
+
* The last describe block covers integration-contributed components, using the
|
|
15
|
+
* same temp-consumer harness as template-integration.test.mjs. Before this,
|
|
16
|
+
* `search`/`build` only ever scanned @astryxdesign/core — an integration's own
|
|
17
|
+
* components were invisible to both, even though `component --list` and
|
|
18
|
+
* `component <Name>` already resolved them. The two discovery paths silently
|
|
19
|
+
* disagreed.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {describe, it, expect} from 'vitest';
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as os from 'node:os';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import {fileURLToPath} from 'node:url';
|
|
27
|
+
import {
|
|
28
|
+
search,
|
|
29
|
+
scoreCandidate,
|
|
30
|
+
scoreQuery,
|
|
31
|
+
tokenizeQuery,
|
|
32
|
+
SEARCH_DOMAINS,
|
|
33
|
+
} from './search.mjs';
|
|
34
|
+
|
|
35
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
36
|
+
const cwd = REPO;
|
|
37
|
+
const SLOW = 30_000;
|
|
38
|
+
|
|
39
|
+
describe('search leaf — envelope + ranking', () => {
|
|
40
|
+
it('returns a `search` envelope with query + results', async () => {
|
|
41
|
+
const r = await search('button', {cwd});
|
|
42
|
+
expect(r.type).toBe('search');
|
|
43
|
+
expect(r.data.query).toBe('button');
|
|
44
|
+
expect(Array.isArray(r.data.results)).toBe(true);
|
|
45
|
+
expect(r.data.results.length).toBeGreaterThan(0);
|
|
46
|
+
}, SLOW);
|
|
47
|
+
|
|
48
|
+
it('keeps tokenizer coverage out of the public result shape', async () => {
|
|
49
|
+
const r = await search('table of contents', {cwd});
|
|
50
|
+
expect(r.data.results.length).toBeGreaterThan(0);
|
|
51
|
+
for (const result of r.data.results) {
|
|
52
|
+
expect(result).not.toHaveProperty('matchedTerms');
|
|
53
|
+
expect(result).not.toHaveProperty('queryTerms');
|
|
54
|
+
}
|
|
55
|
+
}, SLOW);
|
|
56
|
+
|
|
57
|
+
it('returns an empty result set (not an error) for a no-match query', async () => {
|
|
58
|
+
const r = await search('zzznomatch99', {cwd});
|
|
59
|
+
expect(r.type).toBe('search');
|
|
60
|
+
expect(r.data.results).toEqual([]);
|
|
61
|
+
}, SLOW);
|
|
62
|
+
|
|
63
|
+
it('defaults to at most 20 results', async () => {
|
|
64
|
+
const r = await search('button', {cwd});
|
|
65
|
+
expect(r.data.results.length).toBeLessThanOrEqual(20);
|
|
66
|
+
}, SLOW);
|
|
67
|
+
|
|
68
|
+
it('caps results to a positive limit', async () => {
|
|
69
|
+
const r = await search('button', {cwd, limit: 2});
|
|
70
|
+
expect(r.data.results.length).toBeLessThanOrEqual(2);
|
|
71
|
+
}, SLOW);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('search leaf — per-domain result fields', () => {
|
|
75
|
+
it('carries import for components and hooks, title for docs, displayName and kind for templates', async () => {
|
|
76
|
+
const r = await search('theme', {cwd, limit: 60});
|
|
77
|
+
expect(new Set(r.data.results.map(res => res.domain))).toEqual(
|
|
78
|
+
new Set(SEARCH_DOMAINS),
|
|
79
|
+
);
|
|
80
|
+
for (const res of r.data.results) {
|
|
81
|
+
expect(typeof res.command).toBe('string');
|
|
82
|
+
expect(typeof res.description).toBe('string');
|
|
83
|
+
if (res.domain === 'component' || res.domain === 'hook') {
|
|
84
|
+
expect(res.import).toMatch(/\S/);
|
|
85
|
+
} else if (res.domain === 'doc') {
|
|
86
|
+
expect(res.title).toMatch(/\S/);
|
|
87
|
+
const read = `astryx docs ${res.name}`;
|
|
88
|
+
expect([read, `${read} --index`, `${read} ${res.section}`]).toContain(res.command);
|
|
89
|
+
} else {
|
|
90
|
+
expect(res.displayName).toMatch(/\S/);
|
|
91
|
+
expect(['page', 'block']).toContain(res.kind);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, SLOW);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('search leaf — docs at the grain a reader reads them', () => {
|
|
98
|
+
it(
|
|
99
|
+
'finds one section of a guide, and a docs-tree leaf by its own name',
|
|
100
|
+
async () => {
|
|
101
|
+
const guide = await search('codemod protected files', {cwd, type: 'doc'});
|
|
102
|
+
expect(guide.data.results.slice(0, 3)).toContainEqual(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
domain: 'doc',
|
|
105
|
+
name: 'cli/integrations',
|
|
106
|
+
section: 'codemods',
|
|
107
|
+
title: 'Astryx CLI › CLI Integrations › Codemods',
|
|
108
|
+
parent: 'astryx docs cli/integrations --index',
|
|
109
|
+
command: 'astryx docs cli/integrations codemods',
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
const block = await search('token-ref', {cwd});
|
|
113
|
+
expect(block.data.results[0]).toMatchObject({
|
|
114
|
+
name: 'authoring',
|
|
115
|
+
section: 'reference-doc',
|
|
116
|
+
command: 'astryx docs authoring reference-doc',
|
|
117
|
+
});
|
|
118
|
+
const fn = await search('assertResponse', {cwd, type: 'doc'});
|
|
119
|
+
expect(fn.data.results[0]).toMatchObject({
|
|
120
|
+
name: 'cli/api/functions/assert-response',
|
|
121
|
+
title: 'Astryx CLI › API › Functions › assertResponse()',
|
|
122
|
+
parent: 'astryx docs cli/api/functions',
|
|
123
|
+
command: 'astryx docs cli/api/functions/assert-response',
|
|
124
|
+
});
|
|
125
|
+
expect(fn.data.results[0]).not.toHaveProperty('section');
|
|
126
|
+
},
|
|
127
|
+
SLOW,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
it(
|
|
131
|
+
'gives a top-level namespace hit the topic list as its parent',
|
|
132
|
+
async () => {
|
|
133
|
+
for (const [query, route] of [['unorganized', 'unorganized'], ['Astryx CLI', 'cli']]) {
|
|
134
|
+
const r = await search(query, {cwd, type: 'doc'});
|
|
135
|
+
expect(r.data.results).toContainEqual(
|
|
136
|
+
expect.objectContaining({
|
|
137
|
+
name: route,
|
|
138
|
+
command: `astryx docs ${route}`,
|
|
139
|
+
parent: 'astryx docs',
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
SLOW,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
it(
|
|
148
|
+
'points a topic hit at its index, never a whole-topic read',
|
|
149
|
+
async () => {
|
|
150
|
+
const r = await search('cli/integrations', {cwd, type: 'doc'});
|
|
151
|
+
expect(r.data.results[0]).toMatchObject({
|
|
152
|
+
name: 'cli/integrations',
|
|
153
|
+
command: 'astryx docs cli/integrations --index',
|
|
154
|
+
});
|
|
155
|
+
expect(r.data.results[0]).not.toHaveProperty('section');
|
|
156
|
+
},
|
|
157
|
+
SLOW,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
it(
|
|
161
|
+
'searches docs where @astryxdesign/core is not installed',
|
|
162
|
+
async () => {
|
|
163
|
+
const bare = fs.mkdtempSync(
|
|
164
|
+
path.join(os.tmpdir(), 'astryx-search-bare-'),
|
|
165
|
+
);
|
|
166
|
+
try {
|
|
167
|
+
const r = await search('assertResponse', {cwd: bare, type: 'doc'});
|
|
168
|
+
expect(r.data.results[0].name).toBe(
|
|
169
|
+
'cli/api/functions/assert-response',
|
|
170
|
+
);
|
|
171
|
+
} finally {
|
|
172
|
+
fs.rmSync(bare, {recursive: true, force: true});
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
SLOW,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('search leaf — matchCount is the total, not the cap', () => {
|
|
180
|
+
it('reports every match while `results` stays bounded by the limit', async () => {
|
|
181
|
+
// The regression: `matchCount` used to be `results.length`, so a query
|
|
182
|
+
// matching 57 things reported 20 — the cap read back as the answer. Any
|
|
183
|
+
// consumer counting matches (the recorded run, a caller paginating) then
|
|
184
|
+
// could not tell a capped answer from an exactly-cap-sized one.
|
|
185
|
+
const capped = await search('button', {cwd, limit: 2});
|
|
186
|
+
expect(capped.data.results.length).toBe(2);
|
|
187
|
+
expect(capped.data.matchCount).toBeGreaterThan(2);
|
|
188
|
+
|
|
189
|
+
// Same query, no meaningful cap: the count is stable across limits, which
|
|
190
|
+
// is what makes it a count of MATCHES rather than of what was returned.
|
|
191
|
+
const full = await search('button', {cwd, limit: 500});
|
|
192
|
+
expect(full.data.matchCount).toBe(capped.data.matchCount);
|
|
193
|
+
expect(full.data.results.length).toBe(full.data.matchCount);
|
|
194
|
+
}, SLOW);
|
|
195
|
+
|
|
196
|
+
it('reports 0 for a no-match query', async () => {
|
|
197
|
+
const r = await search('zzznomatch99', {cwd});
|
|
198
|
+
expect(r.data.matchCount).toBe(0);
|
|
199
|
+
}, SLOW);
|
|
200
|
+
|
|
201
|
+
it('counts only the requested domain under --type', async () => {
|
|
202
|
+
const all = await search('button', {cwd, limit: 500});
|
|
203
|
+
const components = await search('button', {
|
|
204
|
+
cwd,
|
|
205
|
+
type: 'component',
|
|
206
|
+
limit: 500,
|
|
207
|
+
});
|
|
208
|
+
expect(components.data.matchCount).toBe(components.data.results.length);
|
|
209
|
+
expect(components.data.matchCount).toBeLessThanOrEqual(all.data.matchCount);
|
|
210
|
+
}, SLOW);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('search leaf — --type filter', () => {
|
|
214
|
+
it('restricts results to the requested domain', async () => {
|
|
215
|
+
const r = await search('button', {cwd, type: 'component'});
|
|
216
|
+
expect(r.data.results.every(x => x.domain === 'component')).toBe(true);
|
|
217
|
+
}, SLOW);
|
|
218
|
+
|
|
219
|
+
it('exposes the valid domain list', () => {
|
|
220
|
+
expect(SEARCH_DOMAINS).toEqual(expect.arrayContaining(['component', 'hook', 'doc', 'template']));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('search leaf — exact keyword phrase outranks incidental token matches (issue #5239)', () => {
|
|
225
|
+
it('surfaces Outline for its own declared keyword "table of contents", ranked first', async () => {
|
|
226
|
+
// Before the fix, "table" and "contents" each separately matched dozens
|
|
227
|
+
// of unrelated Table-related templates by coincidence, and their
|
|
228
|
+
// combined token-sum score outranked Outline's single exact match,
|
|
229
|
+
// pushing it out of the results entirely at the default limit.
|
|
230
|
+
const r = await search('table of contents', {cwd});
|
|
231
|
+
expect(r.data.results[0]?.name).toBe('Outline');
|
|
232
|
+
}, SLOW);
|
|
233
|
+
|
|
234
|
+
it('surfaces Outline for its own declared keyword "heading navigation", ranked first', async () => {
|
|
235
|
+
const r = await search('heading navigation', {cwd});
|
|
236
|
+
expect(r.data.results[0]?.name).toBe('Outline');
|
|
237
|
+
}, SLOW);
|
|
238
|
+
|
|
239
|
+
it('preserves query coverage metadata on the promoted exact phrase', () => {
|
|
240
|
+
const query = 'table of contents';
|
|
241
|
+
const tokens = tokenizeQuery(query);
|
|
242
|
+
expect(
|
|
243
|
+
scoreQuery(query, tokens, {
|
|
244
|
+
name: 'Outline',
|
|
245
|
+
keywords: [query],
|
|
246
|
+
}),
|
|
247
|
+
).toMatchObject({matched: tokens.length, total: tokens.length});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('still returns no results for a nonsense query (the fix does not loosen matching)', async () => {
|
|
251
|
+
const r = await search('zzzzqqqx', {cwd});
|
|
252
|
+
expect(r.data.results).toEqual([]);
|
|
253
|
+
}, SLOW);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('search leaf — error paths (pinned)', () => {
|
|
257
|
+
it('throws ERR_INVALID_ARGUMENT when the query is empty/whitespace', async () => {
|
|
258
|
+
await expect(search(' ', {cwd})).rejects.toMatchObject({
|
|
259
|
+
code: 'ERR_INVALID_ARGUMENT',
|
|
260
|
+
message: expect.stringMatching(/query is required/i),
|
|
261
|
+
});
|
|
262
|
+
}, SLOW);
|
|
263
|
+
|
|
264
|
+
it('throws ERR_INVALID_ARGUMENT for an unknown --type', async () => {
|
|
265
|
+
await expect(
|
|
266
|
+
search('button', {cwd, type: /** @type {any} */ ('bogus')}),
|
|
267
|
+
).rejects.toMatchObject({code: 'ERR_INVALID_ARGUMENT'});
|
|
268
|
+
}, SLOW);
|
|
269
|
+
|
|
270
|
+
it('throws ERR_CORE_NOT_FOUND when @astryxdesign/core cannot be found', async () => {
|
|
271
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-search-no-core-'));
|
|
272
|
+
try {
|
|
273
|
+
await expect(search('button', {cwd: empty})).rejects.toMatchObject({
|
|
274
|
+
code: 'ERR_CORE_NOT_FOUND',
|
|
275
|
+
});
|
|
276
|
+
} finally {
|
|
277
|
+
fs.rmSync(empty, {recursive: true, force: true});
|
|
278
|
+
}
|
|
279
|
+
}, SLOW);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('search leaf — limit validation (API matches the CLI contract)', () => {
|
|
283
|
+
it('throws ERR_INVALID_ARGUMENT for a limit of 0 (no longer returns everything)', async () => {
|
|
284
|
+
await expect(search('button', {cwd, limit: 0})).rejects.toMatchObject({
|
|
285
|
+
code: 'ERR_INVALID_ARGUMENT',
|
|
286
|
+
});
|
|
287
|
+
}, SLOW);
|
|
288
|
+
|
|
289
|
+
it('throws ERR_INVALID_ARGUMENT for a negative limit', async () => {
|
|
290
|
+
await expect(search('button', {cwd, limit: -5})).rejects.toMatchObject({
|
|
291
|
+
code: 'ERR_INVALID_ARGUMENT',
|
|
292
|
+
});
|
|
293
|
+
}, SLOW);
|
|
294
|
+
|
|
295
|
+
it('throws ERR_INVALID_ARGUMENT for a non-integer limit', async () => {
|
|
296
|
+
await expect(search('button', {cwd, limit: 2.5})).rejects.toMatchObject({
|
|
297
|
+
code: 'ERR_INVALID_ARGUMENT',
|
|
298
|
+
});
|
|
299
|
+
}, SLOW);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('search leaf — integration components', () => {
|
|
303
|
+
/**
|
|
304
|
+
* A minimal consumer project: a stub `@astryxdesign/core` (so `findCoreDir`
|
|
305
|
+
* resolves without needing the real package) plus an installed
|
|
306
|
+
* `@acme/widgets` integration that contributes one component.
|
|
307
|
+
*/
|
|
308
|
+
function makeConsumerWithIntegrationComponent() {
|
|
309
|
+
const dir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-search-it-'));
|
|
310
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({name: 'consumer'}));
|
|
311
|
+
fs.writeFileSync(
|
|
312
|
+
path.join(dir, 'astryx.config.mjs'),
|
|
313
|
+
`export default { integrations: ['@acme/widgets'] };\n`,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Stub core: just needs to exist with an (empty) src/ so discoverComponents
|
|
317
|
+
// doesn't throw. Its own component list is irrelevant to this test.
|
|
318
|
+
const coreDir = path.join(dir, 'node_modules', '@astryxdesign', 'core');
|
|
319
|
+
fs.mkdirSync(path.join(coreDir, 'src'), {recursive: true});
|
|
320
|
+
|
|
321
|
+
const widgetsDir = path.join(dir, 'node_modules', '@acme', 'widgets');
|
|
322
|
+
fs.mkdirSync(path.join(widgetsDir, 'components'), {recursive: true});
|
|
323
|
+
fs.writeFileSync(
|
|
324
|
+
path.join(widgetsDir, 'package.json'),
|
|
325
|
+
JSON.stringify({name: '@acme/widgets', version: '1.0.0'}),
|
|
326
|
+
);
|
|
327
|
+
fs.writeFileSync(
|
|
328
|
+
path.join(widgetsDir, 'astryx.integration.mjs'),
|
|
329
|
+
`export default { components: './components' };\n`,
|
|
330
|
+
);
|
|
331
|
+
fs.writeFileSync(
|
|
332
|
+
path.join(widgetsDir, 'components', 'FancyGizmo.doc.mjs'),
|
|
333
|
+
`export default {
|
|
334
|
+
type: 'component',
|
|
335
|
+
name: 'FancyGizmo',
|
|
336
|
+
keywords: ['gizmo', 'widget'],
|
|
337
|
+
usage: {description: 'A fancy gizmo widget.'},
|
|
338
|
+
props: [],
|
|
339
|
+
};\n`,
|
|
340
|
+
);
|
|
341
|
+
fs.writeFileSync(
|
|
342
|
+
path.join(widgetsDir, 'components', 'FancyGizmo.tsx'),
|
|
343
|
+
`export function FancyGizmo() { return null; }\n`,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
return dir;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
it('includes a component contributed by a configured integration', async () => {
|
|
350
|
+
const dir = makeConsumerWithIntegrationComponent();
|
|
351
|
+
try {
|
|
352
|
+
const r = await search('gizmo', {cwd: dir, type: 'component'});
|
|
353
|
+
expect(r.data.results.some(x => x.name === 'FancyGizmo')).toBe(true);
|
|
354
|
+
} finally {
|
|
355
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
356
|
+
}
|
|
357
|
+
}, SLOW);
|
|
358
|
+
|
|
359
|
+
it('reports the contributing package as the import hint', async () => {
|
|
360
|
+
const dir = makeConsumerWithIntegrationComponent();
|
|
361
|
+
try {
|
|
362
|
+
const r = await search('FancyGizmo', {cwd: dir, type: 'component'});
|
|
363
|
+
const hit = r.data.results.find(x => x.name === 'FancyGizmo');
|
|
364
|
+
expect(hit?.import).toBe('@acme/widgets');
|
|
365
|
+
} finally {
|
|
366
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
367
|
+
}
|
|
368
|
+
}, SLOW);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('search scoring — multi-token aggregation is monotonic', () => {
|
|
372
|
+
/**
|
|
373
|
+
* @param {string} q
|
|
374
|
+
* @param {object} candidate
|
|
375
|
+
* @returns {number}
|
|
376
|
+
*/
|
|
377
|
+
const score = (q, candidate) => scoreQuery(q, tokenizeQuery(q), candidate)?.score ?? 0;
|
|
378
|
+
|
|
379
|
+
it('ranks matching both terms above matching only the stronger one', () => {
|
|
380
|
+
// The shipped regression: scoring averaged over MATCHED tokens, so the
|
|
381
|
+
// weaker second hit pulled the mean down further than the coverage bonus
|
|
382
|
+
// pushed it up. `build "file browser"` put two form wizards that matched
|
|
383
|
+
// only "file" (98) above the actual file browser that matched both (97),
|
|
384
|
+
// and 98 clears the confident-match gate.
|
|
385
|
+
const partial = {name: 'form-wizard-vertical', keywords: ['file']};
|
|
386
|
+
const complete = {
|
|
387
|
+
name: 'file-explorer',
|
|
388
|
+
keywords: ['file'],
|
|
389
|
+
description: 'Column browser for nested folders',
|
|
390
|
+
};
|
|
391
|
+
expect(score('file browser', complete)).toBeGreaterThan(score('file browser', partial));
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('never scores a superset of matched terms below a subset', () => {
|
|
395
|
+
const subset = {name: 'zzz-none', keywords: ['alpha']};
|
|
396
|
+
const supersets = [
|
|
397
|
+
{name: 'zzz-none', keywords: ['alpha'], description: 'beta things'},
|
|
398
|
+
{name: 'zzz-none', keywords: ['alpha', 'beta']},
|
|
399
|
+
{name: 'zzz-none', keywords: ['alpha'], weakKeywords: ['beta']},
|
|
400
|
+
];
|
|
401
|
+
for (const superset of supersets) {
|
|
402
|
+
expect(score('alpha beta', superset)).toBeGreaterThanOrEqual(score('alpha beta', subset));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('still scores a verbose prompt on the concepts it did hit', () => {
|
|
407
|
+
// Guards the reason the mean was used: a long prompt matching one concept
|
|
408
|
+
// strongly must not be crushed by dividing across every query token.
|
|
409
|
+
const candidate = {name: 'kanban', keywords: ['kanban']};
|
|
410
|
+
expect(score('i need a kanban somewhere in this rambling request', candidate))
|
|
411
|
+
.toBeGreaterThanOrEqual(90);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('search scoring — derived keywords rank below authored ones', () => {
|
|
416
|
+
it('scores an authored keyword above a derived one', () => {
|
|
417
|
+
const authored = scoreCandidate('dialog', {name: 'x', keywords: ['Dialog']});
|
|
418
|
+
const derived = scoreCandidate('dialog', {name: 'x', weakKeywords: ['Dialog']});
|
|
419
|
+
expect(authored?.score).toBe(90);
|
|
420
|
+
expect(derived?.score).toBe(60);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('keeps one incidental derived match below the confident-match gate', () => {
|
|
424
|
+
// PAGE_DIRECT in api/build/kit is 95: at full keyword strength a page that
|
|
425
|
+
// renders one of everything got a 90-point shot per component and was
|
|
426
|
+
// returned as a confident match for queries it had nothing to do with.
|
|
427
|
+
const derived = scoreCandidate('dialog', {name: 'x', weakKeywords: ['Dialog']});
|
|
428
|
+
expect(derived?.score).toBeLessThan(95);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('still surfaces a derived match above the page floor', () => {
|
|
432
|
+
// PAGE_FLOOR is 50 — weakening the signal must not make it invisible.
|
|
433
|
+
const derived = scoreCandidate('dialog', {name: 'x', weakKeywords: ['Dialog']});
|
|
434
|
+
expect(derived?.score).toBeGreaterThanOrEqual(50);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('explains a derived hit as something the template renders', () => {
|
|
438
|
+
const derived = scoreCandidate('dialog', {name: 'x', weakKeywords: ['Dialog']});
|
|
439
|
+
expect(derived?.reason).toMatch(/renders Dialog/);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('search — usage guidance is indexed, a tier below description', () => {
|
|
444
|
+
// The vocabulary a reader types usually lives in a component's guidance, not
|
|
445
|
+
// in its one-line description. Banner calls itself "a persistent message";
|
|
446
|
+
// only its best practices name "caution", "problems", "form errors". Before
|
|
447
|
+
// this, none of those words found it.
|
|
448
|
+
const banner = {
|
|
449
|
+
name: 'Banner',
|
|
450
|
+
keywords: ['alert', 'notification'],
|
|
451
|
+
description: 'A persistent message shown above content.',
|
|
452
|
+
guidance: [
|
|
453
|
+
'Pick a status that matches the message: info for updates, warning for caution.',
|
|
454
|
+
'Use error for problems the reader must resolve before continuing.',
|
|
455
|
+
],
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
it('finds a term that appears ONLY in guidance', () => {
|
|
459
|
+
// Red before this change: guidance was never read, so this scored null.
|
|
460
|
+
const hit = scoreCandidate('caution', banner);
|
|
461
|
+
expect(hit).not.toBeNull();
|
|
462
|
+
expect(hit?.reason).toMatch(/guidance mentions "caution"/);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('scores guidance BELOW description, so a component about X outranks one that merely mentions X', () => {
|
|
466
|
+
const own = scoreCandidate('persistent', banner);
|
|
467
|
+
const mention = scoreCandidate('caution', banner);
|
|
468
|
+
expect(own?.score).toBe(50);
|
|
469
|
+
expect(mention?.score).toBe(45);
|
|
470
|
+
expect(mention.score).toBeLessThan(own.score);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('never lets guidance outrank a name or keyword hit', () => {
|
|
474
|
+
expect(scoreCandidate('banner', banner)?.score).toBe(100);
|
|
475
|
+
expect(scoreCandidate('notification', banner)?.score).toBe(90);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('prefers the stronger signal when a term is in both description and guidance', () => {
|
|
479
|
+
const both = scoreCandidate('message', banner);
|
|
480
|
+
expect(both?.score).toBe(50);
|
|
481
|
+
expect(both?.reason).toMatch(/description mentions/);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('stays below MIN_TOKEN_SCORE, so guidance never counts as a matched CONCEPT', () => {
|
|
485
|
+
// Measured regression this prevents: counting guidance as a matched term
|
|
486
|
+
// moved `nested menu` from SideNav to List, and `explain why a field is
|
|
487
|
+
// required` from Field to TextInput — in both cases a component whose
|
|
488
|
+
// guidance happens to mention the other word displaced the real answer.
|
|
489
|
+
// Guidance decides single-word queries; it must not win multi-word ones on
|
|
490
|
+
// breadth. Same reason weakKeywords are capped.
|
|
491
|
+
expect(scoreCandidate('caution', banner)?.score).toBeLessThan(50);
|
|
492
|
+
// Both words are in this candidate's guidance and nowhere else, so if
|
|
493
|
+
// guidance counted as a concept this would come back as a 2/2 match.
|
|
494
|
+
const multi = scoreQuery('caution problems', tokenizeQuery('caution problems'), banner);
|
|
495
|
+
expect(multi).toBeNull();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('lets a real description hit still win the multi-word pass', () => {
|
|
499
|
+
// The floor must exclude guidance without muting the tiers above it.
|
|
500
|
+
const hit = scoreQuery('persistent message', tokenizeQuery('persistent message'), banner);
|
|
501
|
+
expect(hit?.reason).toMatch(/matches 2\/2 terms/);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('tolerates the object-shaped bestPractices entries core actually ships', () => {
|
|
505
|
+
// Core writes `{guidance: true, description: '...'}`, not plain strings.
|
|
506
|
+
const hit = scoreCandidate('resolve', {
|
|
507
|
+
name: 'X',
|
|
508
|
+
guidance: ['Use error for problems the reader must resolve.'],
|
|
509
|
+
});
|
|
510
|
+
expect(hit?.score).toBe(45);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
@@ -29,19 +29,17 @@ export const doc = {
|
|
|
29
29
|
name: 'component',
|
|
30
30
|
type: 'string',
|
|
31
31
|
description:
|
|
32
|
-
|
|
32
|
+
'Bare or XDS-prefixed component name to copy. 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()',
|
|
39
38
|
},
|
|
40
39
|
{
|
|
41
40
|
name: 'options.output',
|
|
42
41
|
type: 'string',
|
|
43
|
-
description:
|
|
44
|
-
'Output directory, relative to cwd. An absolute path, or one that resolves outside cwd, throws ERR_PATH_TRAVERSAL.',
|
|
42
|
+
description: 'Output directory; must resolve inside cwd.',
|
|
45
43
|
default: "'./components/astryx'",
|
|
46
44
|
},
|
|
47
45
|
{
|
|
@@ -73,7 +71,7 @@ export const doc = {
|
|
|
73
71
|
{
|
|
74
72
|
type: 'swizzle.copy',
|
|
75
73
|
description:
|
|
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
|
|
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 optional maintainer-feedback note.',
|
|
77
75
|
},
|
|
78
76
|
],
|
|
79
77
|
throws: [
|
|
@@ -83,7 +81,7 @@ export const doc = {
|
|
|
83
81
|
},
|
|
84
82
|
{
|
|
85
83
|
code: 'ERR_PATH_TRAVERSAL',
|
|
86
|
-
when: 'the component name contains a path separator or traversal, output
|
|
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',
|
|
87
85
|
},
|
|
88
86
|
{
|
|
89
87
|
code: 'ERR_UNKNOWN_COMPONENT',
|
|
@@ -110,7 +108,7 @@ export const doc = {
|
|
|
110
108
|
{label: 'Eject a component', code: "await swizzle('Button');"},
|
|
111
109
|
{
|
|
112
110
|
label: 'Disambiguate by package',
|
|
113
|
-
code: "await swizzle('Button', {package: '@astryxdesign/core'
|
|
111
|
+
code: "await swizzle('Button', {package: '@astryxdesign/core'});",
|
|
114
112
|
},
|
|
115
113
|
{
|
|
116
114
|
label: 'Custom output directory',
|
|
@@ -74,7 +74,7 @@ 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
|
|
77
|
+
`Refusing to overwrite existing file ${rel}. Re-run with overwrite to replace it.`,
|
|
78
78
|
undefined,
|
|
79
79
|
ERROR_CODES.ERR_FILE_EXISTS,
|
|
80
80
|
);
|
|
@@ -35,15 +35,6 @@ 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
|
-
|
|
47
38
|
it('overwrites when overwrite:true is passed', async () => {
|
|
48
39
|
fs.writeFileSync(path.join(dir, 'mine.tsx'), 'USER CODE');
|
|
49
40
|
const res = await template('blank', {targetPath: './mine.tsx', overwrite: true, cwd: dir});
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from './template.mjs';
|
|
20
20
|
import {search} from '../search/search.mjs';
|
|
21
21
|
import {build} from '../build/build.mjs';
|
|
22
|
+
import {layoutExpand} from '../layout/layout.mjs';
|
|
22
23
|
import {runCli} from '../../test-utils/run-cli.mjs';
|
|
23
24
|
|
|
24
25
|
let tmpDir;
|
|
@@ -517,7 +518,53 @@ describe('integration template discovery', () => {
|
|
|
517
518
|
});
|
|
518
519
|
});
|
|
519
520
|
|
|
520
|
-
it('resolves
|
|
521
|
+
it('resolves a replacement block through the layout alias', async () => {
|
|
522
|
+
const pkgDir = installWidgets(tmpDir);
|
|
523
|
+
writeTemplate(pkgDir, 'acme-card-callout', {
|
|
524
|
+
kind: 'block',
|
|
525
|
+
source:
|
|
526
|
+
'export default function AcmeCardCallout() { return <span>Acme replacement block</span>; }\n',
|
|
527
|
+
});
|
|
528
|
+
declareReplaces(pkgDir, {
|
|
529
|
+
'acme-card-callout': 'CardCallout',
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const accidental = path.join(tmpDir, 'node_modules', '@acme', 'extra');
|
|
533
|
+
fs.mkdirSync(path.join(accidental, 'templates'), {recursive: true});
|
|
534
|
+
fs.writeFileSync(
|
|
535
|
+
path.join(accidental, 'package.json'),
|
|
536
|
+
JSON.stringify({name: '@acme/extra', version: '1.0.0'}),
|
|
537
|
+
);
|
|
538
|
+
fs.writeFileSync(
|
|
539
|
+
path.join(accidental, 'astryx.integration.mjs'),
|
|
540
|
+
`export default {templates: './templates'};\n`,
|
|
541
|
+
);
|
|
542
|
+
writeTemplate(accidental, 'CardCallout', {
|
|
543
|
+
kind: 'block',
|
|
544
|
+
body: `export default {type: 'block', name: 'ZZZ accidental block', description: 'collision'};\n`,
|
|
545
|
+
source:
|
|
546
|
+
'export default function Accidental() { return <span>Accidental block</span>; }\n',
|
|
547
|
+
});
|
|
548
|
+
fs.writeFileSync(
|
|
549
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
550
|
+
`export default { integrations: ['@acme/widgets', '@acme/extra'] };\n`,
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const result = await layoutExpand('C{card-callout}', {
|
|
554
|
+
name: 'ReplacementLayout',
|
|
555
|
+
cwd: tmpDir,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(result.data.code).toContain('Acme replacement block');
|
|
559
|
+
expect(result.data.code).not.toContain('Accidental block');
|
|
560
|
+
expect(result.data.blocksReferenced).toEqual(
|
|
561
|
+
expect.arrayContaining([
|
|
562
|
+
expect.objectContaining({name: 'CardCallout', mode: 'splice'}),
|
|
563
|
+
]),
|
|
564
|
+
);
|
|
565
|
+
}, 30_000);
|
|
566
|
+
|
|
567
|
+
it('resolves chained block aliases consistently in template and layout', async () => {
|
|
521
568
|
const [firstTarget, secondTarget] = (await discoverCoreTemplates()).filter(
|
|
522
569
|
candidate => candidate.type === 'block',
|
|
523
570
|
);
|
|
@@ -545,12 +592,29 @@ describe('integration template discovery', () => {
|
|
|
545
592
|
cwd: tmpDir,
|
|
546
593
|
});
|
|
547
594
|
expect(firstTemplate.data.source).toContain('First chain replacement');
|
|
595
|
+
const layoutId = id =>
|
|
596
|
+
id.replace(/([a-z0-9])([A-Z])/gu, '$1-$2').toLowerCase();
|
|
597
|
+
const firstLayout = await layoutExpand(
|
|
598
|
+
`C{${layoutId(firstTarget.dirName)}}`,
|
|
599
|
+
{
|
|
600
|
+
cwd: tmpDir,
|
|
601
|
+
},
|
|
602
|
+
);
|
|
603
|
+
expect(firstLayout.data.code).toContain('First chain replacement');
|
|
604
|
+
expect(firstLayout.data.code).not.toContain('Final chain replacement');
|
|
548
605
|
|
|
549
606
|
const secondTemplate = await template(secondTarget.dirName, {
|
|
550
607
|
show: true,
|
|
551
608
|
cwd: tmpDir,
|
|
552
609
|
});
|
|
553
610
|
expect(secondTemplate.data.source).toContain('Final chain replacement');
|
|
611
|
+
const secondLayout = await layoutExpand(
|
|
612
|
+
`C{${layoutId(secondTarget.dirName)}}`,
|
|
613
|
+
{
|
|
614
|
+
cwd: tmpDir,
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
expect(secondLayout.data.code).toContain('Final chain replacement');
|
|
554
618
|
}, 30_000);
|
|
555
619
|
|
|
556
620
|
it('keeps the old discovery behavior when no replacement is declared', async () => {
|