@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4
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 +99 -66
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +115 -3
- package/api/build/blocks/blocks.d.mts +42 -0
- package/api/build/blocks/blocks.mjs +169 -0
- package/api/build/blocks/blocks.test.mjs +108 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/kit/answer.d.mts +14 -0
- package/api/build/kit/answer.mjs +85 -0
- package/api/build/kit/answer.test.mjs +38 -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 +60 -0
- package/api/build/kit/signals.mjs +284 -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 +52 -0
- package/api/build/tree.test.mjs +177 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/component.mjs +16 -2
- package/api/component/component.test.mjs +15 -0
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.mjs +17 -5
- package/api/docs/docs.test.mjs +7 -1
- package/api/doctor/doctor.mjs +86 -12
- package/api/doctor/doctor.test.mjs +28 -3
- 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/authoring-checks.mjs +83 -9
- 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-gaps.test.mjs +1964 -0
- package/api/integration/validate-integration.d.mts +11 -2
- package/api/integration/validate-integration.mjs +226 -32
- package/api/integration/validate-integration.type.d.mts +58 -1
- package/api/integration/validate-integration.type.mjs +45 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +66 -2
- package/api/search/_adapter.mjs +118 -10
- 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 +52 -0
- package/api/search/_entries.mjs +158 -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 +7 -5
- package/api/search/search.mjs +53 -2483
- 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/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/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- 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/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- 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/outside-project.test.mjs +2 -1
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- 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 +19 -1
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
- package/foundation/agent-docs/agent-doc-state.mjs +35 -8
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1010 -99
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +678 -35
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- 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/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 +8 -0
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- 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/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- 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/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- 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/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
package/api/init/run/run.mjs
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* @file init.run leaf — non-interactive project setup + feature installer.
|
|
5
5
|
*
|
|
6
6
|
* `run(options, ctx)` is the default / `--features` / `--all` path behind
|
|
7
|
-
* `astryx init`. It installs the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* `astryx init`. It installs the agent-docs cheat sheet at the project root so
|
|
8
|
+
* every agent reads it (AGENTS.md, a copy in the project's Claude Code file, and
|
|
9
|
+
* a GEMINI.md import), points at the theme + page-building workflows, and
|
|
10
|
+
* (optionally) scaffolds a starter template — with NO prompts, so it behaves
|
|
11
|
+
* identically for humans, agents, CI, and piped I/O. It performs the side
|
|
12
|
+
* effects and returns an
|
|
11
13
|
* `init.run` receipt. Hard errors (unknown feature/template) throw AstryxError
|
|
12
14
|
* with a stable code. Human output is emitted through the shared `logger`
|
|
13
15
|
* (silent by default) so the CLI keeps its exact plain output and a
|
|
@@ -21,8 +23,11 @@ import {PathSafetyError, assertWithin} from '../../../foundation/fs/path-safety.
|
|
|
21
23
|
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
22
24
|
import {isOutsideProject} from '../../../foundation/env/project-scope.mjs';
|
|
23
25
|
import {
|
|
26
|
+
AgentDocsHomeError,
|
|
27
|
+
assertAgentDocsTarget,
|
|
24
28
|
installAgentDocs,
|
|
25
29
|
renderAgentDocsBlock,
|
|
30
|
+
agentDocsTargetDir,
|
|
26
31
|
} from '../../../foundation/agent-docs/agent-docs.mjs';
|
|
27
32
|
import {themeTemplate} from '../../theme/template/template.mjs';
|
|
28
33
|
import {listTemplates} from '../../template/template.mjs';
|
|
@@ -64,6 +69,24 @@ export function getNextSteps(invocation) {
|
|
|
64
69
|
];
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Whether two paths name one folder, links resolved.
|
|
74
|
+
* @param {string} a
|
|
75
|
+
* @param {string} b
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function sameFolder(a, b) {
|
|
79
|
+
/** @param {string} p */
|
|
80
|
+
const real = p => {
|
|
81
|
+
try {
|
|
82
|
+
return fs.realpathSync(p);
|
|
83
|
+
} catch {
|
|
84
|
+
return path.resolve(p);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return real(a) === real(b);
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
/**
|
|
68
91
|
* Install the agent-docs block; records the outcome (or soft error) on `data`.
|
|
69
92
|
* Mirrors the CLI's historical soft-error policy: a PathSafetyError surfaces its
|
|
@@ -92,14 +115,46 @@ async function applyAgents(cwd, options, invocation, data) {
|
|
|
92
115
|
? options.agentDocsPath
|
|
93
116
|
: [options.agentDocsPath]
|
|
94
117
|
: undefined;
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
// The docs belong at the project root, not in the subfolder init ran in;
|
|
119
|
+
// with no project above, they go where init ran, as before. Explicit
|
|
120
|
+
// --agent-docs-path entries stay relative to where init ran.
|
|
121
|
+
const root = agentDocsTargetDir(cwd);
|
|
122
|
+
const targetDir = paths ? cwd : root;
|
|
123
|
+
// With explicit paths, installAgentDocs checks each file's own folder.
|
|
124
|
+
if (!paths) assertAgentDocsTarget(targetDir);
|
|
125
|
+
const renderedBlock = await renderAgentDocsBlock(root);
|
|
126
|
+
/** @type {string[]} */
|
|
127
|
+
const skipped = [];
|
|
128
|
+
const files = installAgentDocs(targetDir, {
|
|
97
129
|
agent: options.agent,
|
|
98
130
|
paths,
|
|
99
131
|
renderedBlock,
|
|
132
|
+
onSkipped: (rel, reason) => skipped.push(`${rel} (${reason})`),
|
|
133
|
+
// Below the project root, 0.7.1 wrote in the folder init ran in, so a
|
|
134
|
+
// project-root file that escapes the project is skipped and reported,
|
|
135
|
+
// not refused.
|
|
136
|
+
refuseEscapes: sameFolder(targetDir, cwd),
|
|
100
137
|
});
|
|
138
|
+
// Each file is reported from where init ran, as before.
|
|
139
|
+
const written =
|
|
140
|
+
path.resolve(targetDir) === path.resolve(cwd)
|
|
141
|
+
? files
|
|
142
|
+
: files.map(rel =>
|
|
143
|
+
path.relative(cwd, path.join(targetDir, rel)).split(path.sep).join('/'),
|
|
144
|
+
);
|
|
101
145
|
data.docsWritten = written;
|
|
102
146
|
logger.log(`[ok] AI agent docs installed -> ${written.join(', ')}`);
|
|
147
|
+
// A file 0.7.1 did not write (GEMINI.md, or a project-root file when init
|
|
148
|
+
// runs below the root) that resolves outside the project is left as it
|
|
149
|
+
// is, reported in docsError with one note; the exit code stays 0. A file
|
|
150
|
+
// 0.7.1 wrote whose path escapes the project still fails the run (below).
|
|
151
|
+
if (skipped.length > 0) {
|
|
152
|
+
const message =
|
|
153
|
+
`Left as ${skipped.length === 1 ? 'it is' : 'they are'}: ${skipped.join('; ')}. ` +
|
|
154
|
+
'To have init write the Astryx block there, make it a regular file inside the project, or copy the block from AGENTS.md by hand.';
|
|
155
|
+
logger.warn(`Note: ${message}`);
|
|
156
|
+
data.docsError = {kind: 'install-failed', message};
|
|
157
|
+
}
|
|
103
158
|
} catch (err) {
|
|
104
159
|
// PathSafetyError carries a precise, user-actionable message — surface it
|
|
105
160
|
// (and flag exit 1) instead of the generic "could not install" warning so
|
|
@@ -109,6 +164,14 @@ async function applyAgents(cwd, options, invocation, data) {
|
|
|
109
164
|
data.docsError = {kind: 'path-safety', message: err.message};
|
|
110
165
|
return;
|
|
111
166
|
}
|
|
167
|
+
// The home folder, and a settings folder in it outside a project, are
|
|
168
|
+
// refused: nothing is written, the reason is printed as a note, and the
|
|
169
|
+
// exit code stays 0.
|
|
170
|
+
if (err instanceof AgentDocsHomeError) {
|
|
171
|
+
logger.warn(`Note: ${err.message}`);
|
|
172
|
+
data.docsError = {kind: 'install-failed', message: err.message};
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
112
175
|
logger.error(
|
|
113
176
|
`Could not install agent docs. Try again with \`${invocation} init --features agents\`.`,
|
|
114
177
|
);
|
|
@@ -51,6 +51,8 @@ import {
|
|
|
51
51
|
validateInstalledIntegration,
|
|
52
52
|
validateLocalIntegration,
|
|
53
53
|
} from './validate-integration.mjs';
|
|
54
|
+
import {rootProblem} from '../../foundation/integrations/validate-contributions.mjs';
|
|
55
|
+
import {normalizeProviderId} from '../../foundation/identity/provider-identity.mjs';
|
|
54
56
|
|
|
55
57
|
/** @param {string} value */
|
|
56
58
|
export function shellArg(value) {
|
|
@@ -58,15 +60,61 @@ export function shellArg(value) {
|
|
|
58
60
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The provider ID a package without a name is checked under: a valid bare
|
|
65
|
+
* package name no published package can hold (npm names are lowercase).
|
|
66
|
+
*/
|
|
67
|
+
const UNNAMED_PROVIDER = 'astryx-unnamed-local-package';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A message as the package sees it: the stand-in provider ID gone, its docs
|
|
71
|
+
* named by their path in the package.
|
|
72
|
+
* @param {string} message
|
|
73
|
+
*/
|
|
74
|
+
function unnamedShown(message) {
|
|
75
|
+
return message
|
|
76
|
+
.split(`${UNNAMED_PROVIDER}/`)
|
|
77
|
+
.join('')
|
|
78
|
+
.split(UNNAMED_PROVIDER)
|
|
79
|
+
.join('this package');
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
/**
|
|
62
83
|
* Resolve one local or installed integration for an authoring check.
|
|
63
84
|
* @param {string | undefined} pkg
|
|
64
85
|
* @param {string} cwd
|
|
65
86
|
*/
|
|
66
87
|
async function resolveIntegration(pkg, cwd) {
|
|
67
|
-
|
|
68
|
-
? validateInstalledIntegration(pkg, cwd)
|
|
69
|
-
: validateLocalIntegration(cwd);
|
|
88
|
+
const resolved = pkg
|
|
89
|
+
? await validateInstalledIntegration(pkg, cwd)
|
|
90
|
+
: await validateLocalIntegration(cwd);
|
|
91
|
+
const integration = resolved.found ? resolved.integration : undefined;
|
|
92
|
+
if (!integration) return resolved;
|
|
93
|
+
// A root that is not a readable folder is reported in the issues; the
|
|
94
|
+
// checks read nothing from it rather than fail on it.
|
|
95
|
+
/** @type {typeof integration & {__unnamed?: boolean}} */
|
|
96
|
+
const usable = {...integration};
|
|
97
|
+
// The provider ID readers key the package on: the manifest's providerId,
|
|
98
|
+
// else its name. A package with neither (validate warned about it) is
|
|
99
|
+
// still checked; the docs check names its docs under a stand-in ID.
|
|
100
|
+
try {
|
|
101
|
+
usable.providerId = normalizeProviderId(
|
|
102
|
+
integration.__declaredProviderId ?? integration.name,
|
|
103
|
+
);
|
|
104
|
+
} catch {
|
|
105
|
+
usable.__unnamed = true;
|
|
106
|
+
}
|
|
107
|
+
for (const kind of /** @type {const} */ ([
|
|
108
|
+
'components',
|
|
109
|
+
'templates',
|
|
110
|
+
'codemods',
|
|
111
|
+
'docs',
|
|
112
|
+
'themes',
|
|
113
|
+
])) {
|
|
114
|
+
const root = usable[kind];
|
|
115
|
+
if (root != null && rootProblem(root)) usable[kind] = undefined;
|
|
116
|
+
}
|
|
117
|
+
return {...resolved, integration: usable};
|
|
70
118
|
}
|
|
71
119
|
|
|
72
120
|
/**
|
|
@@ -387,8 +435,20 @@ export async function integrationDocConflicts(pkg, options = {}) {
|
|
|
387
435
|
};
|
|
388
436
|
}
|
|
389
437
|
|
|
438
|
+
// The docs tree names a package's docs by its provider ID, which a
|
|
439
|
+
// package.json without a name does not give: its docs are checked under a
|
|
440
|
+
// stand-in ID, which no message names.
|
|
441
|
+
const unnamed = /** @type {{__unnamed?: boolean}} */ (resolved.integration)
|
|
442
|
+
.__unnamed;
|
|
443
|
+
const docsIntegration = unnamed
|
|
444
|
+
? {
|
|
445
|
+
...resolved.integration,
|
|
446
|
+
name: UNNAMED_PROVIDER,
|
|
447
|
+
providerId: UNNAMED_PROVIDER,
|
|
448
|
+
}
|
|
449
|
+
: resolved.integration;
|
|
390
450
|
const [discovered, builtinTopics] = await Promise.all([
|
|
391
|
-
discoverIntegrationDocs(
|
|
451
|
+
discoverIntegrationDocs(docsIntegration),
|
|
392
452
|
Promise.resolve(discoverBuiltinTopics()),
|
|
393
453
|
]);
|
|
394
454
|
const {records, errors} = discovered;
|
|
@@ -458,27 +518,40 @@ export async function integrationDocConflicts(pkg, options = {}) {
|
|
|
458
518
|
// its docs (spec:AST-046, spec:AST-047).
|
|
459
519
|
if (errors.length === 0) {
|
|
460
520
|
for (const {severity, message} of await packageDocsProblems(
|
|
461
|
-
/** @type {{name: string}} */ (
|
|
521
|
+
/** @type {{name: string}} */ (docsIntegration),
|
|
462
522
|
discovered,
|
|
463
523
|
)) {
|
|
464
|
-
issues.push({
|
|
524
|
+
issues.push({
|
|
525
|
+
code: 'invalid_doc_graph',
|
|
526
|
+
severity,
|
|
527
|
+
message: unnamedShown(message),
|
|
528
|
+
});
|
|
465
529
|
}
|
|
466
530
|
|
|
467
531
|
// A reference block includes content rather than linking to it, so one
|
|
468
532
|
// that cannot include what it names loses that content for every reader:
|
|
469
533
|
// an error, where a link that names no doc still prints as written.
|
|
470
534
|
for (const message of await packageReferenceProblems(
|
|
471
|
-
/** @type {{name: string}} */ (
|
|
535
|
+
/** @type {{name: string}} */ (docsIntegration),
|
|
472
536
|
discovered,
|
|
473
537
|
)) {
|
|
474
|
-
issues.push({
|
|
538
|
+
issues.push({
|
|
539
|
+
code: 'invalid_doc_reference',
|
|
540
|
+
severity: 'error',
|
|
541
|
+
message: unnamedShown(message),
|
|
542
|
+
});
|
|
475
543
|
}
|
|
476
544
|
|
|
477
545
|
// One fact has one owner: a passage two topics repeat goes stale in one
|
|
478
546
|
// of them. Each is listed and is a warning (architecture:cli-surface
|
|
479
547
|
// DEC-8), so `integration check --strict` fails until it is fixed;
|
|
480
548
|
// `doctor integration docs` keeps its exit code, as it fails on errors.
|
|
481
|
-
copiedPassages.push(
|
|
549
|
+
copiedPassages.push(
|
|
550
|
+
...(await integrationCopiedPassages(discovered)).map(passage => ({
|
|
551
|
+
...passage,
|
|
552
|
+
message: unnamedShown(passage.message),
|
|
553
|
+
})),
|
|
554
|
+
);
|
|
482
555
|
for (const {message} of copiedPassages) {
|
|
483
556
|
issues.push({code: 'copied_passage', severity: 'warning', message});
|
|
484
557
|
}
|
|
@@ -488,6 +561,7 @@ export async function integrationDocConflicts(pkg, options = {}) {
|
|
|
488
561
|
...(await integrationFluff(discovered)).map(each => ({
|
|
489
562
|
...each,
|
|
490
563
|
file: fromPackage(each.file),
|
|
564
|
+
message: unnamedShown(each.message),
|
|
491
565
|
})),
|
|
492
566
|
);
|
|
493
567
|
for (const {message} of fluff) {
|
|
@@ -26,6 +26,11 @@ import {fileURLToPath} from 'node:url';
|
|
|
26
26
|
import {spawnSync} from 'node:child_process';
|
|
27
27
|
import {assertWithin} from '../../foundation/fs/path-safety.mjs';
|
|
28
28
|
import {resolvePackageDir} from '../../foundation/integrations/integrations.mjs';
|
|
29
|
+
import {
|
|
30
|
+
literalThemeName,
|
|
31
|
+
rootProblem,
|
|
32
|
+
symlinkedFolder,
|
|
33
|
+
} from '../../foundation/integrations/validate-contributions.mjs';
|
|
29
34
|
import {
|
|
30
35
|
componentReplacesCliProblem,
|
|
31
36
|
docsTreeCliProblem,
|
|
@@ -953,6 +958,10 @@ async function validatePackedThemeExports(
|
|
|
953
958
|
);
|
|
954
959
|
}
|
|
955
960
|
|
|
961
|
+
// The name the source gives defineTheme, when the file says it: the
|
|
962
|
+
// build names its files after it.
|
|
963
|
+
const builtName = literalThemeName(source, localTheme.exportName);
|
|
964
|
+
|
|
956
965
|
const declaredOutputs = declaredThemeBuildOutputs(
|
|
957
966
|
localIntegration,
|
|
958
967
|
localTheme,
|
|
@@ -1097,7 +1106,15 @@ async function validatePackedThemeExports(
|
|
|
1097
1106
|
? `Theme "${localTheme.slug}" module is not the module \`${formatCliCommand(`theme build ${sourceRelative}`, localIntegration.__packageDir)}\` writes and does not only wrap it: ${localWrapper.reason}. A module that wraps the built module may only import the theme's own stylesheet and font stylesheet for their side effects and re-export from the built module either everything or only its theme, as ${localTheme.exportName}.`
|
|
1098
1107
|
: localWrapper && !localWrapper.ok
|
|
1099
1108
|
? `Theme "${localTheme.slug}" module re-exports ${localWrapper.reason}, which does not match ${sourceRelative}. Run \`${formatCliCommand(`theme build ${sourceRelative}`, localIntegration.__packageDir)}\` before packing.`
|
|
1100
|
-
:
|
|
1109
|
+
: (renamedOutputMessage(
|
|
1110
|
+
localTheme,
|
|
1111
|
+
output.kind,
|
|
1112
|
+
output.local,
|
|
1113
|
+
source,
|
|
1114
|
+
builtName,
|
|
1115
|
+
localIntegration.__packageDir,
|
|
1116
|
+
) ??
|
|
1117
|
+
`Theme "${localTheme.slug}" ${output.kind} does not match ${sourceRelative}. Run \`${formatCliCommand(`theme build ${sourceRelative}`, localIntegration.__packageDir)}\` before packing.`),
|
|
1101
1118
|
),
|
|
1102
1119
|
);
|
|
1103
1120
|
}
|
|
@@ -1265,7 +1282,17 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1265
1282
|
// ── Phase 1: structure validation ──
|
|
1266
1283
|
const localResult = await validateLocalIntegration(cwd);
|
|
1267
1284
|
/** @type {Issue[]} */
|
|
1268
|
-
const issues = [
|
|
1285
|
+
const issues = [
|
|
1286
|
+
...localResult.issues,
|
|
1287
|
+
// Validate lists these layout findings without counting them. A
|
|
1288
|
+
// package.json npm cannot pack is what fails the pack here, as it did
|
|
1289
|
+
// before they were named; the rest stay warnings, which fail nothing.
|
|
1290
|
+
...(localResult.found ? (localResult.listed ?? []) : []).map(issue =>
|
|
1291
|
+
issue.code === 'invalid_package_json'
|
|
1292
|
+
? {...issue, severity: /** @type {const} */ ('error')}
|
|
1293
|
+
: issue,
|
|
1294
|
+
),
|
|
1295
|
+
];
|
|
1269
1296
|
|
|
1270
1297
|
if (!localResult.found || !localResult.integration) {
|
|
1271
1298
|
return emptyReceipt(pkgName, [
|
|
@@ -1314,6 +1341,8 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1314
1341
|
);
|
|
1315
1342
|
}
|
|
1316
1343
|
for (const root of fileInv.roots) {
|
|
1344
|
+
// A root that is not a readable folder has its own issue.
|
|
1345
|
+
if (rootProblem(/** @type {string} */ (loaded[root.kind]))) continue;
|
|
1317
1346
|
if (root.files.length === 0) {
|
|
1318
1347
|
issues.push(
|
|
1319
1348
|
error(
|
|
@@ -1403,6 +1432,9 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1403
1432
|
);
|
|
1404
1433
|
/** @type {string|null} */
|
|
1405
1434
|
let scratchBase = null;
|
|
1435
|
+
/** @type {string|undefined} the packed copy of the package */
|
|
1436
|
+
let extractDir;
|
|
1437
|
+
const issuesBeforePack = issues.length;
|
|
1406
1438
|
|
|
1407
1439
|
try {
|
|
1408
1440
|
// npm pack
|
|
@@ -1451,11 +1483,16 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1451
1483
|
for (const root of fileInv.roots) {
|
|
1452
1484
|
const missing = root.files.filter(f => !packResult.packedPaths.has(f));
|
|
1453
1485
|
if (missing.length === root.files.length && root.files.length > 0) {
|
|
1486
|
+
const absolute = /** @type {string} */ (loaded[root.kind]);
|
|
1487
|
+
const link = symlinkedFolder(absolute, packageDir);
|
|
1454
1488
|
issues.push(
|
|
1455
1489
|
error(
|
|
1456
1490
|
'root_not_packed',
|
|
1457
1491
|
`Declared ${root.kind} root "${root.path}" has 0 of ${root.files.length} expected ` +
|
|
1458
|
-
`files in the pack list.
|
|
1492
|
+
`files in the pack list. ` +
|
|
1493
|
+
(link
|
|
1494
|
+
? `It is reached through the symlink "${path.relative(packageDir, link.path).split(path.sep).join('/')}", and npm pack does not follow a symlinked folder. Fix: replace the link with the folder itself, or point "${root.kind}" at the folder it links to.`
|
|
1495
|
+
: `Add "${root.path.replace(/^\.\//, '')}" to "files" in package.json.`),
|
|
1459
1496
|
),
|
|
1460
1497
|
);
|
|
1461
1498
|
} else {
|
|
@@ -1494,7 +1531,6 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1494
1531
|
}
|
|
1495
1532
|
|
|
1496
1533
|
// ── Phase 3: extract + identity comparison ──
|
|
1497
|
-
let extractDir;
|
|
1498
1534
|
try {
|
|
1499
1535
|
extractDir = resolvePackageDir(pkgName, scratchBase);
|
|
1500
1536
|
} catch (err) {
|
|
@@ -1616,11 +1652,94 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1616
1652
|
issues,
|
|
1617
1653
|
);
|
|
1618
1654
|
} finally {
|
|
1655
|
+
// The packed copy lives in a scratch folder that is gone once this
|
|
1656
|
+
// returns: name its files the way the package does.
|
|
1657
|
+
if (extractDir && scratchBase) {
|
|
1658
|
+
packageRelative(issues.slice(issuesBeforePack), extractDir, scratchBase);
|
|
1659
|
+
}
|
|
1619
1660
|
removeTreeBestEffort(tgzTmpDir, 'temporary tarball directory', issues);
|
|
1620
1661
|
removeTreeBestEffort(scratchBase, 'scratch consumer', issues);
|
|
1621
1662
|
}
|
|
1622
1663
|
}
|
|
1623
1664
|
|
|
1665
|
+
/**
|
|
1666
|
+
* When a theme's source names it something other than the file the package
|
|
1667
|
+
* exports for it, a rebuild writes new files under that name and leaves the
|
|
1668
|
+
* exported one stale forever: say so, with both ways out. Null otherwise.
|
|
1669
|
+
* @param {{slug: string}} theme
|
|
1670
|
+
* @param {string} kind 'module' or 'stylesheet'
|
|
1671
|
+
* @param {string} exported the file the package exports for it
|
|
1672
|
+
* @param {string} source the theme's source
|
|
1673
|
+
* @param {string | null} builtName the literal defineTheme name, if any
|
|
1674
|
+
* @param {string} packageDir
|
|
1675
|
+
* @returns {string | null}
|
|
1676
|
+
*/
|
|
1677
|
+
function renamedOutputMessage(
|
|
1678
|
+
theme,
|
|
1679
|
+
kind,
|
|
1680
|
+
exported,
|
|
1681
|
+
source,
|
|
1682
|
+
builtName,
|
|
1683
|
+
packageDir,
|
|
1684
|
+
) {
|
|
1685
|
+
if (builtName == null || builtName === theme.slug) return null;
|
|
1686
|
+
const extension = kind === 'module' ? '.js' : '.css';
|
|
1687
|
+
if (path.basename(exported) === `${builtName}${extension}`) return null;
|
|
1688
|
+
const rel = (/** @type {string} */ file) =>
|
|
1689
|
+
path.relative(packageDir, file).split(path.sep).join('/');
|
|
1690
|
+
const built = rel(
|
|
1691
|
+
path.join(path.dirname(source), `${builtName}${extension}`),
|
|
1692
|
+
);
|
|
1693
|
+
return `Theme "${theme.slug}" ${kind} is ${rel(exported)}, but ${path.basename(source)} calls defineTheme with name: '${builtName}', so \`${formatCliCommand(`theme build ${rel(source)}`, packageDir)}\` writes ${built} instead and rebuilding never updates ${path.basename(exported)}. Fix: point the package export that serves it at ${built} after building, or set name: '${theme.slug}' in ${path.basename(source)} and rebuild.`;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Rewrite the scratch copy's absolute paths in each issue's message: a file
|
|
1698
|
+
* of the packed package as its path in the package, and any other path in the
|
|
1699
|
+
* scratch consumer (such as the module that resolved an import) as that
|
|
1700
|
+
* consumer. Both folders are gone once verify returns.
|
|
1701
|
+
* @param {Issue[]} issues
|
|
1702
|
+
* @param {string} packedDir the packed package inside the scratch consumer
|
|
1703
|
+
* @param {string} consumerDir the scratch consumer
|
|
1704
|
+
*/
|
|
1705
|
+
function packageRelative(issues, packedDir, consumerDir) {
|
|
1706
|
+
/** @param {string} dir */
|
|
1707
|
+
const spellings = dir => {
|
|
1708
|
+
const out = [dir];
|
|
1709
|
+
try {
|
|
1710
|
+
const real = fs.realpathSync(dir);
|
|
1711
|
+
if (real !== dir) out.push(real);
|
|
1712
|
+
} catch {
|
|
1713
|
+
// Already removed: the plain path is all there is.
|
|
1714
|
+
}
|
|
1715
|
+
return out;
|
|
1716
|
+
};
|
|
1717
|
+
/** @param {string} text */
|
|
1718
|
+
const escape = text => text.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
1719
|
+
const packed = spellings(packedDir);
|
|
1720
|
+
const consumer = spellings(consumerDir);
|
|
1721
|
+
for (const issue of issues) {
|
|
1722
|
+
if (typeof issue.message !== 'string') continue;
|
|
1723
|
+
let message = issue.message;
|
|
1724
|
+
for (const prefix of packed) {
|
|
1725
|
+
message = message
|
|
1726
|
+
.split(`${prefix}${path.sep}`)
|
|
1727
|
+
.join('')
|
|
1728
|
+
.replace(
|
|
1729
|
+
new RegExp(`${escape(prefix)}(?![\\w-]|\\.\\w)`, 'gu'),
|
|
1730
|
+
'the package root',
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
for (const prefix of consumer) {
|
|
1734
|
+
message = message.replace(
|
|
1735
|
+
new RegExp(`${escape(prefix)}[^\\s"'\`]*`, 'gu'),
|
|
1736
|
+
'a consumer project',
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
issue.message = message;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1624
1743
|
/**
|
|
1625
1744
|
* Assemble the receipt.
|
|
1626
1745
|
* @param {string} name
|
|
@@ -580,6 +580,24 @@ describe('a theme module that loads its own styles', () => {
|
|
|
580
580
|
expect(cli.status, cli.stderr).toBe(0);
|
|
581
581
|
});
|
|
582
582
|
|
|
583
|
+
it('says a rebuild cannot fix exports that name the theme after its folder once its source renames it', async () => {
|
|
584
|
+
writePackage({
|
|
585
|
+
files: ['astryx.integration.mjs', 'themes'],
|
|
586
|
+
themeExports: true,
|
|
587
|
+
});
|
|
588
|
+
fs.writeFileSync(
|
|
589
|
+
path.join(tmpDir, 'themes', 'ocean', 'oceanTheme.ts'),
|
|
590
|
+
"import {defineTheme} from '@astryxdesign/core/theme';\n\nexport const oceanTheme = defineTheme({name: 'sea'});\n",
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
594
|
+
|
|
595
|
+
expect(result.data.issues.map(issue => issue.message)).toEqual([
|
|
596
|
+
`Theme "ocean" module is themes/ocean/ocean.js, but oceanTheme.ts calls defineTheme with name: 'sea', so \`${formatCliCommand('theme build themes/ocean/oceanTheme.ts', tmpDir)}\` writes themes/ocean/sea.js instead and rebuilding never updates ocean.js. Fix: point the package export that serves it at themes/ocean/sea.js after building, or set name: 'ocean' in oceanTheme.ts and rebuild.`,
|
|
597
|
+
`Theme "ocean" stylesheet is themes/ocean/ocean.css, but oceanTheme.ts calls defineTheme with name: 'sea', so \`${formatCliCommand('theme build themes/ocean/oceanTheme.ts', tmpDir)}\` writes themes/ocean/sea.css instead and rebuilding never updates ocean.css. Fix: point the package export that serves it at themes/ocean/sea.css after building, or set name: 'ocean' in oceanTheme.ts and rebuild.`,
|
|
598
|
+
]);
|
|
599
|
+
});
|
|
600
|
+
|
|
583
601
|
it('compares a partial export set with the module named after the theme', async () => {
|
|
584
602
|
writeRenamedOcean();
|
|
585
603
|
setThemeExports({'./themes/ocean': './themes/ocean/deep-sea.js'});
|
|
@@ -1919,6 +1937,75 @@ describe('integrationPackCheck', () => {
|
|
|
1919
1937
|
expect(sourceWarnings[0].message).toContain('stray.tsx');
|
|
1920
1938
|
});
|
|
1921
1939
|
|
|
1940
|
+
it('says npm pack skips a symlinked root instead of asking for a "files" entry', async () => {
|
|
1941
|
+
writePackage({
|
|
1942
|
+
manifest:
|
|
1943
|
+
"export default {themes: './themes', components: './components'};\n",
|
|
1944
|
+
});
|
|
1945
|
+
const real = path.join(tmpDir, 'real-components');
|
|
1946
|
+
fs.mkdirSync(real, {recursive: true});
|
|
1947
|
+
fs.writeFileSync(
|
|
1948
|
+
path.join(real, 'Card.doc.mjs'),
|
|
1949
|
+
"export default {type: 'component', name: 'Card', displayName: 'Card', usage: {description: 'Card.'}, props: []};\n",
|
|
1950
|
+
);
|
|
1951
|
+
fs.writeFileSync(
|
|
1952
|
+
path.join(real, 'Card.tsx'),
|
|
1953
|
+
'export function Card() { return null; }\n',
|
|
1954
|
+
);
|
|
1955
|
+
fs.symlinkSync('real-components', path.join(tmpDir, 'components'));
|
|
1956
|
+
|
|
1957
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
1958
|
+
const codes = result.data.issues.map(issue => issue.code);
|
|
1959
|
+
expect(codes).toContain('symlinked_root');
|
|
1960
|
+
const notPacked = result.data.issues.find(
|
|
1961
|
+
issue => issue.code === 'root_not_packed',
|
|
1962
|
+
);
|
|
1963
|
+
expect(notPacked?.message).toBe(
|
|
1964
|
+
'Declared components root "components" has 0 of 2 expected files in the pack list. It is reached through the symlink "components", and npm pack does not follow a symlinked folder. Fix: replace the link with the folder itself, or point "components" at the folder it links to.',
|
|
1965
|
+
);
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
it('names packed files by their path in the package, never the scratch copy', async () => {
|
|
1969
|
+
writePackage({
|
|
1970
|
+
manifest:
|
|
1971
|
+
"export default {themes: './themes', docs: './docs/a.doc.mjs'};\n",
|
|
1972
|
+
});
|
|
1973
|
+
fs.mkdirSync(path.join(tmpDir, 'docs'), {recursive: true});
|
|
1974
|
+
fs.writeFileSync(path.join(tmpDir, 'docs', 'a.doc.mjs'), '');
|
|
1975
|
+
|
|
1976
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
1977
|
+
const messages = result.data.issues.map(issue => issue.message);
|
|
1978
|
+
expect(messages.join('\n')).not.toContain(
|
|
1979
|
+
path.join(tmpDir, '.astryx-pack-check-'),
|
|
1980
|
+
);
|
|
1981
|
+
expect(messages).toContain(
|
|
1982
|
+
'Packed: Declared docs root is a file, not a folder: docs/a.doc.mjs. Fix: point "docs" in the manifest at the folder that holds them.',
|
|
1983
|
+
);
|
|
1984
|
+
// The root's own issue covers it: no empty-root error, no raw errno.
|
|
1985
|
+
const codes = result.data.issues.map(issue => issue.code);
|
|
1986
|
+
expect(codes).not.toContain('empty_contribution_root');
|
|
1987
|
+
expect(codes).not.toContain('local_discovery_failed');
|
|
1988
|
+
expect(codes).not.toContain('packed_discovery_failed');
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
it('fails on a package.json npm cannot pack, which validate only warns about', async () => {
|
|
1992
|
+
writePackage({files: ['astryx.integration.mjs', 'themes']});
|
|
1993
|
+
const packageFile = path.join(tmpDir, 'package.json');
|
|
1994
|
+
const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf-8'));
|
|
1995
|
+
delete pkg.name;
|
|
1996
|
+
fs.writeFileSync(packageFile, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
1997
|
+
|
|
1998
|
+
const result = await integrationPackCheck({cwd: tmpDir});
|
|
1999
|
+
|
|
2000
|
+
expect(result.data.packable).toBe(false);
|
|
2001
|
+
expect(
|
|
2002
|
+
result.data.issues.map(issue => [issue.code, issue.severity]),
|
|
2003
|
+
).toEqual([
|
|
2004
|
+
['invalid_package_json', 'error'],
|
|
2005
|
+
['pack_failed', 'error'],
|
|
2006
|
+
]);
|
|
2007
|
+
});
|
|
2008
|
+
|
|
1922
2009
|
it('cleans up its scratch consumer when npm pack fails', async () => {
|
|
1923
2010
|
writePackage({
|
|
1924
2011
|
files: ['astryx.integration.mjs', 'themes'],
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A component doc whose file name differs from its source only in
|
|
5
|
+
* letter case, on a case-insensitive disk (macOS, Windows), where discovery
|
|
6
|
+
* still pairs the two: a warning naming the rename, not an error. The disk is
|
|
7
|
+
* simulated by finding the doc's source case-insensitively.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
vi.mock(
|
|
15
|
+
'../../foundation/discovery/component-discovery.mjs',
|
|
16
|
+
async importOriginal => {
|
|
17
|
+
const original =
|
|
18
|
+
/** @type {typeof import('../../foundation/discovery/component-discovery.mjs')} */ (
|
|
19
|
+
await importOriginal()
|
|
20
|
+
);
|
|
21
|
+
return {
|
|
22
|
+
...original,
|
|
23
|
+
/** @param {string} docPath */
|
|
24
|
+
integrationSourceForDoc: docPath => {
|
|
25
|
+
const dir = path.dirname(docPath);
|
|
26
|
+
const want = `${path
|
|
27
|
+
.basename(docPath)
|
|
28
|
+
.replace(/\.doc\.(ts|mjs|js)$/u, '')}.tsx`.toLowerCase();
|
|
29
|
+
const found = fs
|
|
30
|
+
.readdirSync(dir)
|
|
31
|
+
.find(name => name.toLowerCase() === want);
|
|
32
|
+
return found ? path.join(dir, found) : null;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const {validateIntegration} = await import('./validate-integration.mjs');
|
|
39
|
+
|
|
40
|
+
/** @type {string} */
|
|
41
|
+
let tmpDir;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-case-disk-'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('a doc whose name differs from its source only in case, on a case-insensitive disk', () => {
|
|
52
|
+
it('lists source_without_component_doc with the rename, without an issue', async () => {
|
|
53
|
+
fs.writeFileSync(
|
|
54
|
+
path.join(tmpDir, 'package.json'),
|
|
55
|
+
JSON.stringify({name: '@acme/kit', version: '1.0.0'}),
|
|
56
|
+
);
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
59
|
+
"export default {components: './components'};\n",
|
|
60
|
+
);
|
|
61
|
+
fs.mkdirSync(path.join(tmpDir, 'components'));
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(tmpDir, 'components', 'Acmewidget.doc.mjs'),
|
|
64
|
+
"export default {type: 'component', name: 'AcmeWidget', displayName: 'AcmeWidget', usage: {description: 'A widget.'}, props: []};\n",
|
|
65
|
+
);
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
path.join(tmpDir, 'components', 'AcmeWidget.tsx'),
|
|
68
|
+
'export function AcmeWidget() { return null; }\n',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const {data} = await validateIntegration(undefined, {cwd: tmpDir});
|
|
72
|
+
expect(data.issues).toEqual([]);
|
|
73
|
+
expect(data.notes).toEqual([
|
|
74
|
+
{
|
|
75
|
+
code: 'source_without_component_doc',
|
|
76
|
+
message:
|
|
77
|
+
'Component doc "Acmewidget.doc.mjs" and its source "AcmeWidget.tsx" differ only in letter case. They pair on a case-insensitive disk (macOS, Windows) but not on Linux, where CI and most installs run, so there the component is missing. Fix: rename Acmewidget.doc.mjs to AcmeWidget.doc.mjs.',
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('lists a hidden doc that pairs with its source, without an issue', async () => {
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(tmpDir, 'package.json'),
|
|
85
|
+
JSON.stringify({name: '@acme/kit', version: '1.0.0'}),
|
|
86
|
+
);
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
89
|
+
"export default {components: './components'};\n",
|
|
90
|
+
);
|
|
91
|
+
fs.mkdirSync(path.join(tmpDir, 'components'));
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
path.join(tmpDir, 'components', 'Acmewidget.doc.mjs'),
|
|
94
|
+
"export default {type: 'component',\n hidden: true,\n name: 'AcmeWidget', displayName: 'AcmeWidget', usage: {description: 'A widget.'}, props: []};\n",
|
|
95
|
+
);
|
|
96
|
+
fs.writeFileSync(
|
|
97
|
+
path.join(tmpDir, 'components', 'AcmeWidget.tsx'),
|
|
98
|
+
'export function AcmeWidget() { return null; }\n',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const {data} = await validateIntegration(undefined, {cwd: tmpDir});
|
|
102
|
+
expect(data.issues).toEqual([]);
|
|
103
|
+
expect(data.notes).toEqual([
|
|
104
|
+
{
|
|
105
|
+
code: 'source_without_component_doc',
|
|
106
|
+
message:
|
|
107
|
+
'Hidden component doc "Acmewidget.doc.mjs" and its source "AcmeWidget.tsx" differ only in letter case. They pair on a case-insensitive disk (macOS, Windows) but not on Linux, where CI and most installs run. Fix: rename Acmewidget.doc.mjs to AcmeWidget.doc.mjs.',
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
});
|