@astryxdesign/cli 0.7.2-canary.f16dc9f → 0.7.2-canary.f2b60a0
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 +15 -14
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/component.test.mjs +420 -167
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/component/list/list.mjs +42 -4
- package/api/docs/_adapter.d.mts +11 -0
- package/api/docs/_adapter.mjs +244 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/doctor.mjs +27 -3
- package/api/doctor/doctor.test.mjs +43 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +8 -4
- package/api/integration/add-contribution.test.mjs +4 -2
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/check/check.d.mts +96 -0
- package/api/integration/check/check.mjs +357 -0
- package/api/integration/check/check.test.mjs +489 -0
- package/api/integration/integration-check.type.d.mts +118 -0
- package/api/integration/integration-check.type.mjs +84 -0
- package/api/integration/integrationCheck.doc.d.mts +10 -0
- package/api/integration/integrationCheck.doc.mjs +73 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.mjs +8 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/integration/validate-integration.type.d.mts +8 -0
- package/api/integration/validate-integration.type.mjs +12 -1
- package/api/integration/validateIntegration.doc.mjs +17 -3
- package/api/search/search.d.mts +22 -6
- package/api/search/search.domain-priority.test.mjs +108 -18
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +141 -25
- package/api/search/search.test.mjs +36 -0
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/template/template.test.mjs +120 -3
- package/api/theme/build/build.family.test.mjs +64 -0
- package/api/theme/build/build.mjs +180 -24
- package/api/theme/themeBuild.doc.mjs +4 -4
- package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
- package/assets/docs/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/styling-overview.doc.mjs +1 -1
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +11 -1
- package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
- package/assets/docs/tree/add-a-topic.doc.mjs +10 -7
- package/assets/docs/tree/agent-guidance.doc.mjs +2 -2
- package/assets/docs/tree/block-template.doc.mjs +8 -20
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/check-the-props.doc.mjs +196 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
- package/assets/docs/tree/checks.doc.mjs +24 -16
- package/assets/docs/tree/codemods.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +8 -6
- package/assets/docs/tree/component-imports.doc.mjs +10 -2
- package/assets/docs/tree/components.doc.mjs +1 -1
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +7 -18
- package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
- package/assets/docs/tree/describe-the-component.doc.mjs +2 -2
- package/assets/docs/tree/docs.doc.mjs +3 -3
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +6 -6
- package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
- package/assets/docs/tree/generate-a-palette.doc.mjs +5 -3
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +1 -1
- package/assets/docs/tree/integrations.doc.mjs +8 -0
- package/assets/docs/tree/links.doc.mjs +6 -5
- package/assets/docs/tree/migration-setup.doc.mjs +1 -0
- package/assets/docs/tree/migration-verification.doc.mjs +0 -1
- package/assets/docs/tree/migration.doc.mjs +0 -1
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +4 -8
- package/assets/docs/tree/publishing.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +37 -21
- package/assets/docs/tree/replace-a-core-component.doc.mjs +4 -6
- package/assets/docs/tree/replace-a-core-template.doc.mjs +6 -8
- package/assets/docs/tree/sections-and-placement.doc.mjs +4 -6
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +4 -2
- package/assets/docs/tree/short-and-findable.doc.mjs +6 -5
- package/assets/docs/tree/single-component.doc.mjs +15 -6
- package/assets/docs/tree/start-a-template.doc.mjs +3 -3
- package/assets/docs/tree/subcomponent.doc.mjs +6 -6
- package/assets/docs/tree/template-assets.doc.mjs +2 -2
- package/assets/docs/tree/template-doc-overview.doc.mjs +4 -15
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- package/assets/docs/tree/template-icons.doc.mjs +1 -1
- package/assets/docs/tree/template-images-media.doc.mjs +1 -1
- package/assets/docs/tree/template-styles.doc.mjs +2 -2
- package/assets/docs/tree/templates.doc.mjs +1 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +3 -3
- package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
- package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
- package/assets/docs/tree/troubleshooting.doc.mjs +13 -5
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/upgrading.doc.mjs +3 -3
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +3 -1
- package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
- package/assets/docs/tree/versioning.doc.mjs +7 -6
- package/assets/docs/tree/write-the-template-file.doc.mjs +4 -3
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +43 -4
- package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
- package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
- package/assets/templates/pages/detail-page/page.tsx +3 -3
- package/assets/templates/pages/table-inbox/page.tsx +10 -8
- package/assets/theme.template.ts +1 -1
- package/authoring/codemod/codemod.doc.mjs +7 -3
- package/authoring/config/config.doc.mjs +9 -6
- package/authoring/config/type.ts +4 -2
- package/authoring/discover/discover.doc.mjs +1 -1
- package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
- package/authoring/doctypes/base/type.ts +6 -0
- package/authoring/doctypes/command/command.doc.mjs +14 -12
- package/authoring/doctypes/component/component.doc.mjs +31 -28
- package/authoring/doctypes/component/type.ts +13 -8
- package/authoring/doctypes/enum/enum.doc.mjs +7 -4
- package/authoring/doctypes/function/function.doc.mjs +9 -8
- package/authoring/doctypes/hook/hook.doc.mjs +5 -4
- package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
- package/authoring/doctypes/reference/reference.doc.mjs +11 -10
- package/authoring/doctypes/schema/schema.doc.mjs +9 -9
- package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/template.doc.mjs +3 -3
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/doctypes/theme/theme.doc.mjs +2 -2
- package/authoring/gap-report/gap-report.doc.mjs +2 -2
- package/authoring/identity/identity.doc.mjs +3 -3
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +12 -4
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
- package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +837 -0
- package/clients/cli/commands/doctor.mjs +190 -0
- package/clients/cli/commands/integration-check.doc.mjs +85 -0
- package/clients/cli/commands/integration-check.test.mjs +267 -0
- package/clients/cli/commands/integration.doc.mjs +6 -2
- package/clients/cli/commands/integration.mjs +107 -1
- package/clients/cli/commands/setup-nudge.test.mjs +79 -1
- package/clients/cli/commands/swizzle.mjs +5 -3
- package/clients/cli/commands/swizzle.routing.test.mjs +18 -1
- package/clients/cli/commands/text-json-parity.test.mjs +5 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/index.mjs +30 -5
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/agent-docs/agent-doc-state.d.mts +14 -0
- package/foundation/agent-docs/agent-doc-state.mjs +38 -0
- package/foundation/agent-docs/agent-docs.d.mts +2 -1
- package/foundation/agent-docs/agent-docs.mjs +8 -6
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +2 -1
- package/foundation/discovery/component-discovery.d.mts +190 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +864 -56
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1224 -0
- package/foundation/discovery/doc-types.test.mjs +652 -0
- package/foundation/discovery/docs-discovery.d.mts +81 -0
- package/foundation/discovery/docs-discovery.mjs +57 -0
- package/foundation/discovery/integration-doc-files.d.mts +22 -0
- package/foundation/discovery/integration-doc-files.mjs +231 -0
- package/foundation/discovery/integration-doc-files.test.mjs +246 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +278 -0
- package/foundation/discovery/prop-drift.mjs +2263 -0
- package/foundation/discovery/prop-drift.test.mjs +2219 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +11 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/doc-compiler/links.d.mts +9 -5
- package/foundation/doc-compiler/links.mjs +4 -2
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/response/response-types.doc.mjs +7 -2
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
- package/scripts/postinstall.mjs +3 -3
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
3
6
|
import {describe, it, expect} from 'vitest';
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
stripTemplateAssetRefs,
|
|
9
|
+
template,
|
|
10
|
+
extractComponents,
|
|
11
|
+
} from './template.mjs';
|
|
5
12
|
|
|
6
13
|
describe('stripTemplateAssetRefs', () => {
|
|
7
14
|
it('replaces a /template-assets image path with an inline data URI', () => {
|
|
@@ -31,8 +38,7 @@ describe('stripTemplateAssetRefs', () => {
|
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
it('preserves surrounding source structure', () => {
|
|
34
|
-
const src =
|
|
35
|
-
"const data = [{src: '/template-assets/x.png', alt: 'X'}];";
|
|
41
|
+
const src = "const data = [{src: '/template-assets/x.png', alt: 'X'}];";
|
|
36
42
|
const out = stripTemplateAssetRefs(src);
|
|
37
43
|
expect(out).toContain("alt: 'X'");
|
|
38
44
|
expect(out).toContain('const data = [{src:');
|
|
@@ -102,4 +108,115 @@ describe('template --skeleton component extraction (prefix-agnostic)', () => {
|
|
|
102
108
|
|
|
103
109
|
expect(result.data.skeleton).toContain('columns={{minWidth: 200}}');
|
|
104
110
|
});
|
|
111
|
+
|
|
112
|
+
it('filters out local helper components declared in the same file', () => {
|
|
113
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
114
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
pagePath,
|
|
117
|
+
`
|
|
118
|
+
import {Card} from '@astryxdesign/core/Layout';
|
|
119
|
+
|
|
120
|
+
function TimelineSection() {
|
|
121
|
+
return <div />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default function Page() {
|
|
125
|
+
return (
|
|
126
|
+
<Card>
|
|
127
|
+
<TimelineSection />
|
|
128
|
+
</Card>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const components = extractComponents(pagePath);
|
|
136
|
+
expect(components).toContain('Card');
|
|
137
|
+
expect(components).not.toContain('Timeline');
|
|
138
|
+
} finally {
|
|
139
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('filters out a type-annotated local helper', () => {
|
|
144
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
145
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
pagePath,
|
|
148
|
+
`
|
|
149
|
+
import type {FC} from 'react';
|
|
150
|
+
import {Card} from '@astryxdesign/core/Layout';
|
|
151
|
+
|
|
152
|
+
const TimelineSection: FC = () => <div />;
|
|
153
|
+
|
|
154
|
+
export default function Page() {
|
|
155
|
+
return (
|
|
156
|
+
<Card>
|
|
157
|
+
<TimelineSection />
|
|
158
|
+
</Card>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
);
|
|
163
|
+
try {
|
|
164
|
+
const components = extractComponents(pagePath);
|
|
165
|
+
expect(components).toContain('Card');
|
|
166
|
+
expect(components).not.toContain('Timeline');
|
|
167
|
+
} finally {
|
|
168
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('keeps an imported component when declaration-like text appears in a comment or string', () => {
|
|
173
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
174
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
pagePath,
|
|
177
|
+
`
|
|
178
|
+
import {Card} from '@astryxdesign/core/Layout';
|
|
179
|
+
|
|
180
|
+
// const Card = () => null;
|
|
181
|
+
const note = 'function Card() {}';
|
|
182
|
+
|
|
183
|
+
export default function Page() {
|
|
184
|
+
return <Card>{note}</Card>;
|
|
185
|
+
}
|
|
186
|
+
`,
|
|
187
|
+
);
|
|
188
|
+
try {
|
|
189
|
+
expect(extractComponents(pagePath)).toContain('Card');
|
|
190
|
+
} finally {
|
|
191
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('keeps a bare imported component whose name is itself a stripped suffix', () => {
|
|
196
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-test-'));
|
|
197
|
+
const pagePath = path.join(tmpDir, 'page.tsx');
|
|
198
|
+
fs.writeFileSync(
|
|
199
|
+
pagePath,
|
|
200
|
+
`
|
|
201
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
202
|
+
|
|
203
|
+
function SectionHeading() {
|
|
204
|
+
return <div />;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default function Page() {
|
|
208
|
+
return (
|
|
209
|
+
<Section>
|
|
210
|
+
<SectionHeading />
|
|
211
|
+
</Section>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
`,
|
|
215
|
+
);
|
|
216
|
+
try {
|
|
217
|
+
expect(extractComponents(pagePath)).toContain('Section');
|
|
218
|
+
} finally {
|
|
219
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
105
222
|
});
|
|
@@ -13,6 +13,7 @@ import {themeBuild, themeBuildFamily} from './build.mjs';
|
|
|
13
13
|
import {interceptCore} from './core-interception.mjs';
|
|
14
14
|
import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
|
|
15
15
|
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
16
17
|
vi.setConfig({testTimeout: 120_000});
|
|
17
18
|
const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
|
|
18
19
|
const FAMILY_KEY = 'ocean-family';
|
|
@@ -252,6 +253,69 @@ describe('themeBuildFamily()', () => {
|
|
|
252
253
|
expect(fs.existsSync(dtsPath)).toBe(false);
|
|
253
254
|
});
|
|
254
255
|
|
|
256
|
+
it('refuses to write family outputs outside cwd, and checks them read-only', async () => {
|
|
257
|
+
const dir = makeDir();
|
|
258
|
+
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
259
|
+
const app = path.join(dir, 'app');
|
|
260
|
+
fs.mkdirSync(app);
|
|
261
|
+
|
|
262
|
+
await expect(
|
|
263
|
+
themeBuildFamily(files, {familyKey: FAMILY_KEY}, {cwd: app}),
|
|
264
|
+
).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
|
|
265
|
+
const checked = await themeBuildFamily(
|
|
266
|
+
files,
|
|
267
|
+
{familyKey: FAMILY_KEY, check: true},
|
|
268
|
+
{cwd: app},
|
|
269
|
+
);
|
|
270
|
+
expect(checked?.type).toBe('theme.build.check');
|
|
271
|
+
expect(checked?.data.upToDate).toBe(false);
|
|
272
|
+
// The `.css.d.ts` import stub may be absent in check mode.
|
|
273
|
+
expect(checked?.data.stale).toEqual(
|
|
274
|
+
OUTPUTS.filter(file => !file.endsWith('.css.d.ts')).map(file => ({
|
|
275
|
+
path: `../themes/${file}`,
|
|
276
|
+
reason: 'missing',
|
|
277
|
+
})),
|
|
278
|
+
);
|
|
279
|
+
for (const output of OUTPUTS) {
|
|
280
|
+
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(false);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('ends a stale family report for outputs outside cwd with a remedy that works', async () => {
|
|
285
|
+
const dir = makeDir();
|
|
286
|
+
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
287
|
+
const app = path.join(dir, 'app');
|
|
288
|
+
fs.mkdirSync(app);
|
|
289
|
+
const family = ['--family', ...files, '--family-key', FAMILY_KEY];
|
|
290
|
+
|
|
291
|
+
const stale = await runCli(['theme', 'build', ...family, '--check'], app);
|
|
292
|
+
expect(stale.code).toBe(1);
|
|
293
|
+
// The old remedy ran the build from cwd, which refuses these outputs.
|
|
294
|
+
expect(stale.stderr).not.toContain('Rebuild with:');
|
|
295
|
+
|
|
296
|
+
const lines = stale.stderr.split('\n').map(line => line.trim());
|
|
297
|
+
const runDir = lines.find(line => line.startsWith('cd '))?.slice(3);
|
|
298
|
+
expect(runDir).toBe(fs.realpathSync(path.join(dir, 'themes')));
|
|
299
|
+
const stem = `${getCliInvocation(runDir)} theme build `;
|
|
300
|
+
const [buildArgs, checkArgs] = lines
|
|
301
|
+
.filter(line => line.startsWith(stem))
|
|
302
|
+
.map(line => line.slice(stem.length).split(' '));
|
|
303
|
+
expect(checkArgs).toEqual([...buildArgs, '--check']);
|
|
304
|
+
|
|
305
|
+
// Following the remedy builds, and the check from there passes.
|
|
306
|
+
const built = await runCli(['theme', 'build', ...buildArgs], runDir);
|
|
307
|
+
expect(built.code).toBe(0);
|
|
308
|
+
for (const output of OUTPUTS) {
|
|
309
|
+
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(true);
|
|
310
|
+
}
|
|
311
|
+
const checked = await runCli(
|
|
312
|
+
['--json', 'theme', 'build', ...checkArgs],
|
|
313
|
+
runDir,
|
|
314
|
+
);
|
|
315
|
+
expect(checked.code).toBe(0);
|
|
316
|
+
expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
255
319
|
it('fails without publishing a mixed trio when staging a write fails', async () => {
|
|
256
320
|
const dir = makeDir();
|
|
257
321
|
const files = writeFamily(dir);
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* files; an internal callback lets the CLI make only the process status fail.
|
|
28
28
|
* Human progress is emitted through the shared `logger` (silent by default), so
|
|
29
29
|
* the CLI keeps its exact output while a programmatic caller stays quiet.
|
|
30
|
+
* Every output a build writes, explicit or beside the theme file, stays inside
|
|
31
|
+
* `cwd` (`ERR_PATH_TRAVERSAL` otherwise). Check mode writes nothing, so it
|
|
32
|
+
* compares outputs outside `cwd` read-only; only a relative `out` path that
|
|
33
|
+
* escapes `cwd` is refused there too.
|
|
30
34
|
*
|
|
31
35
|
* The installed `@astryxdesign/core` is an independently versioned optional
|
|
32
36
|
* peer, so it can be older than the CLI. A theme that uses only baseline
|
|
@@ -58,7 +62,10 @@ import {isBuiltin} from 'node:module';
|
|
|
58
62
|
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
59
63
|
import {parse} from '@babel/parser';
|
|
60
64
|
import {createJiti} from 'jiti';
|
|
61
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
formatCliCommand,
|
|
67
|
+
getCliInvocation,
|
|
68
|
+
} from '../../../foundation/env/package-manager.mjs';
|
|
62
69
|
import {
|
|
63
70
|
CLI_ROOT,
|
|
64
71
|
findCoreDir,
|
|
@@ -270,6 +277,25 @@ function realOrSelf(file) {
|
|
|
270
277
|
}
|
|
271
278
|
}
|
|
272
279
|
|
|
280
|
+
/**
|
|
281
|
+
* `target` spelled through real directories: the deepest ancestor that exists
|
|
282
|
+
* is resolved through its symlinks, and the missing rest is appended as given.
|
|
283
|
+
* @param {string} target absolute path
|
|
284
|
+
* @returns {string}
|
|
285
|
+
*/
|
|
286
|
+
function realLocation(target) {
|
|
287
|
+
/** @type {string[]} */
|
|
288
|
+
const missing = [];
|
|
289
|
+
let existing = target;
|
|
290
|
+
while (!fs.existsSync(existing)) {
|
|
291
|
+
const parent = path.dirname(existing);
|
|
292
|
+
if (parent === existing) break;
|
|
293
|
+
missing.unshift(path.basename(existing));
|
|
294
|
+
existing = parent;
|
|
295
|
+
}
|
|
296
|
+
return path.join(realOrSelf(existing), ...missing);
|
|
297
|
+
}
|
|
298
|
+
|
|
273
299
|
/**
|
|
274
300
|
* The Core the project at `cwd` installed, when it differs from the one the
|
|
275
301
|
* CLI's own import found and loads. The app's `<Theme>` runs on the app's
|
|
@@ -506,6 +532,127 @@ function checkedBuildOutputs(writes, cwd) {
|
|
|
506
532
|
.map(write => path.relative(cwd, write.dest));
|
|
507
533
|
}
|
|
508
534
|
|
|
535
|
+
/** Remedies that end a confinement error, by where the output path came from. */
|
|
536
|
+
const OUT_REMEDY = 'Pass an --out path inside it.';
|
|
537
|
+
const SOURCE_REMEDY =
|
|
538
|
+
'Without --out the outputs go beside the theme file, so run the build from a directory that contains the theme file, or pass an --out path inside the working directory.';
|
|
539
|
+
const FAMILY_REMEDY =
|
|
540
|
+
"Family outputs go beside the family's root theme file, so run the build from a directory that contains it.";
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Refuse an output outside `root`: every write is confined, whether `out` names
|
|
544
|
+
* the path or it defaults to the theme file's directory (architecture:cli-surface
|
|
545
|
+
* INV10). Both sides are compared by real location, so an absolute path that
|
|
546
|
+
* reaches the working directory through a symlink is inside it, and a symlink
|
|
547
|
+
* inside it that leads elsewhere is not.
|
|
548
|
+
* @param {string} dest absolute output path
|
|
549
|
+
* @param {string} root the directory writes stay inside: the working directory
|
|
550
|
+
* @param {string} remedy what to do instead; ends the error message
|
|
551
|
+
* @param {string} [shown] the path as the caller wrote it, for the message
|
|
552
|
+
*/
|
|
553
|
+
function confineOutput(dest, root, remedy, shown = dest) {
|
|
554
|
+
try {
|
|
555
|
+
assertWithin(realLocation(dest), realOrSelf(path.resolve(root)), {
|
|
556
|
+
allowAbsolute: true,
|
|
557
|
+
label: 'output path',
|
|
558
|
+
});
|
|
559
|
+
} catch (err) {
|
|
560
|
+
if (!(err instanceof PathSafetyError)) throw err;
|
|
561
|
+
throw new AstryxError(
|
|
562
|
+
err.code === ERROR_CODES.ERR_PATH_TRAVERSAL
|
|
563
|
+
? `Output path "${shown}" is outside the working directory (${root}). ${remedy}`
|
|
564
|
+
: err.message,
|
|
565
|
+
undefined,
|
|
566
|
+
ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** @param {Array<{dest: string}>} writes @param {string} root @param {string} remedy */
|
|
572
|
+
function confineOutputs(writes, root, remedy) {
|
|
573
|
+
for (const write of writes) confineOutput(write.dest, root, remedy);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** @param {string} dest absolute output path @param {string} root @returns {boolean} whether a build at `root` refuses to write `dest` */
|
|
577
|
+
function refusedOutput(dest, root) {
|
|
578
|
+
try {
|
|
579
|
+
confineOutput(dest, root, '');
|
|
580
|
+
return false;
|
|
581
|
+
} catch (err) {
|
|
582
|
+
if (
|
|
583
|
+
err instanceof AstryxError &&
|
|
584
|
+
err.code === ERROR_CODES.ERR_PATH_TRAVERSAL
|
|
585
|
+
)
|
|
586
|
+
return true;
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** @param {string[]} dirs absolute directories @returns {string | null} the deepest directory containing all of them, or null across roots */
|
|
592
|
+
function commonDirectory(dirs) {
|
|
593
|
+
let common = dirs[0];
|
|
594
|
+
for (const dir of dirs.slice(1)) {
|
|
595
|
+
while (
|
|
596
|
+
path.relative(common, dir).startsWith('..') ||
|
|
597
|
+
path.isAbsolute(path.relative(common, dir))
|
|
598
|
+
) {
|
|
599
|
+
const parent = path.dirname(common);
|
|
600
|
+
if (parent === common) return null;
|
|
601
|
+
common = parent;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return common;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** @param {string} value @returns {string} `value` as one shell word */
|
|
608
|
+
function shellWord(value) {
|
|
609
|
+
if (/^[A-Za-z0-9@%+=:,./_-]+$/.test(value)) return value;
|
|
610
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* How a stale check report ends when outputs sit outside the write root. A
|
|
615
|
+
* build there refuses them, and each output's `Source:` header records the
|
|
616
|
+
* directory its build ran from, so the build and the check both run from the
|
|
617
|
+
* nearest directory that contains every theme file and output.
|
|
618
|
+
* @param {Array<{dest: string}>} writes
|
|
619
|
+
* @param {string[]} sources absolute theme files the build reads
|
|
620
|
+
* @param {string} root the directory a build here keeps its writes inside
|
|
621
|
+
* @param {(relative: (file: string) => string) => string[]} args the arguments after `theme build`, given each path relative to the directory to run from
|
|
622
|
+
* @returns {string[] | null} report lines, or null when every output is inside `root`
|
|
623
|
+
*/
|
|
624
|
+
function outsideCheckRemedy(writes, sources, root, args) {
|
|
625
|
+
if (!writes.some(write => refusedOutput(write.dest, root))) return null;
|
|
626
|
+
const real = (/** @type {string} */ file) => realLocation(path.resolve(file));
|
|
627
|
+
const dir = commonDirectory([
|
|
628
|
+
...sources.map(file => path.dirname(real(file))),
|
|
629
|
+
...writes.map(write => path.dirname(real(write.dest))),
|
|
630
|
+
]);
|
|
631
|
+
if (!dir) {
|
|
632
|
+
return [
|
|
633
|
+
'These outputs are outside the working directory, so a build here refuses them. Build and check them from one directory that contains the theme files and the outputs.',
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
const command = (/** @type {string[]} */ extra) =>
|
|
637
|
+
formatCliCommand(
|
|
638
|
+
[
|
|
639
|
+
'theme',
|
|
640
|
+
'build',
|
|
641
|
+
...args(file => path.relative(dir, real(file))),
|
|
642
|
+
...extra,
|
|
643
|
+
]
|
|
644
|
+
.map(shellWord)
|
|
645
|
+
.join(' '),
|
|
646
|
+
dir,
|
|
647
|
+
);
|
|
648
|
+
return [
|
|
649
|
+
'These outputs are outside the working directory, so a build here refuses them, and each output records the directory it was built from. Build and check them from a directory that contains them:',
|
|
650
|
+
` cd ${shellWord(dir)}`,
|
|
651
|
+
` ${command([])}`,
|
|
652
|
+
` ${command(['--check'])}`,
|
|
653
|
+
];
|
|
654
|
+
}
|
|
655
|
+
|
|
509
656
|
/** @param {Array<{dest: string, content: string}>} writes */
|
|
510
657
|
function writeBuildOutputs(writes) {
|
|
511
658
|
if (writes.length === 0) return;
|
|
@@ -2344,9 +2491,10 @@ function validateHeadingTypeAugmentationSupport(themeDef) {
|
|
|
2344
2491
|
* `logger` (silent by default).
|
|
2345
2492
|
*
|
|
2346
2493
|
* @param {string} file - Theme file path, resolved against `cwd`.
|
|
2347
|
-
* @param {{out?: string, check?: boolean, iconsSpecifier?: string, __prepareFamily?: boolean, __familyLoader?: any, __familyInterception?: any, __onPrivateVarError?: () => void}} [options] -
|
|
2494
|
+
* @param {{out?: string, check?: boolean, iconsSpecifier?: string, __prepareFamily?: boolean, __familyLoader?: any, __familyInterception?: any, __onPrivateVarError?: () => void, __writeRoot?: string}} [options] -
|
|
2348
2495
|
* `out` overrides the output CSS path; `check` compares against on-disk outputs
|
|
2349
|
-
* instead of writing. `
|
|
2496
|
+
* instead of writing. `__writeRoot` replaces `cwd` as the directory every
|
|
2497
|
+
* output must stay inside, for a CLI-internal scratch build. `iconsSpecifier` overrides the icon registry import
|
|
2350
2498
|
* specifier in the generated module (e.g. `./icons.mjs`); when omitted, the
|
|
2351
2499
|
* direct registry specifiers from the theme source are emitted unchanged.
|
|
2352
2500
|
* @param {{cwd?: string}} [ctx]
|
|
@@ -2791,24 +2939,17 @@ async function themeBuildInternal(
|
|
|
2791
2939
|
// Derive the default CSS name from the theme name so .css/.js/.d.ts
|
|
2792
2940
|
// share one scheme; an explicit --out still wins.
|
|
2793
2941
|
const baseName = themeDef.name;
|
|
2942
|
+
// Every write stays inside cwd. A CLI-internal caller that renders into a
|
|
2943
|
+
// scratch directory it creates and removes names that directory instead.
|
|
2944
|
+
const writeRoot = options.__writeRoot ?? cwd;
|
|
2794
2945
|
let outPath;
|
|
2795
2946
|
if (options.out) {
|
|
2796
2947
|
outPath = path.resolve(cwd, options.out);
|
|
2797
|
-
//
|
|
2798
|
-
//
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
} catch (err) {
|
|
2803
|
-
if (err instanceof PathSafetyError) {
|
|
2804
|
-
throw new AstryxError(
|
|
2805
|
-
err.message,
|
|
2806
|
-
undefined,
|
|
2807
|
-
ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
2808
|
-
);
|
|
2809
|
-
}
|
|
2810
|
-
throw err;
|
|
2811
|
-
}
|
|
2948
|
+
// A build keeps the explicit output inside the write root, relative or
|
|
2949
|
+
// absolute. Check mode writes nothing, so it refuses only a relative path
|
|
2950
|
+
// that escapes cwd.
|
|
2951
|
+
if (!options.check || !path.isAbsolute(options.out)) {
|
|
2952
|
+
confineOutput(outPath, writeRoot, OUT_REMEDY, options.out);
|
|
2812
2953
|
}
|
|
2813
2954
|
} else {
|
|
2814
2955
|
outPath = path.join(path.dirname(filePath), `${baseName}.css`);
|
|
@@ -2905,6 +3046,13 @@ async function themeBuildInternal(
|
|
|
2905
3046
|
writes.push({dest: variantDtsPath, content: variantContent});
|
|
2906
3047
|
}
|
|
2907
3048
|
|
|
3049
|
+
// Every output a build writes stays inside cwd, including the defaults
|
|
3050
|
+
// beside the theme file. Check mode writes nothing, so it compares outputs
|
|
3051
|
+
// outside cwd read-only.
|
|
3052
|
+
if (!options.check) {
|
|
3053
|
+
confineOutputs(writes, writeRoot, options.out ? OUT_REMEDY : SOURCE_REMEDY);
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2908
3056
|
// Check mode: compare generated content against what's on disk instead of
|
|
2909
3057
|
// writing. A file is "stale" if it's missing or its content differs once the
|
|
2910
3058
|
// volatile @generated `Command:` line is ignored. Returns a
|
|
@@ -2923,7 +3071,10 @@ async function themeBuildInternal(
|
|
|
2923
3071
|
` ${s.reason === 'missing' ? 'missing' : 'stale'}: ${s.path}`,
|
|
2924
3072
|
);
|
|
2925
3073
|
}
|
|
2926
|
-
|
|
3074
|
+
// prettier-ignore
|
|
3075
|
+
const remedy = outsideCheckRemedy(writes, [filePath], writeRoot, relative => [relative(filePath), ...(options.out ? ['--out', relative(outPath)] : []), ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
|
|
3076
|
+
if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
|
|
3077
|
+
else logger.error(`\n Rebuild with: ${buildCommand}`);
|
|
2927
3078
|
}
|
|
2928
3079
|
return {
|
|
2929
3080
|
type: 'theme.build.check',
|
|
@@ -3036,7 +3187,7 @@ function printBuildTrailer(trailer) {
|
|
|
3036
3187
|
logger.log(`
|
|
3037
3188
|
Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
|
|
3038
3189
|
|
|
3039
|
-
import { Theme } from '@astryxdesign/core';
|
|
3190
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3040
3191
|
import { ${trailer.exportName} } from '${trailer.jsImport}';
|
|
3041
3192
|
import '${trailer.cssImport}';
|
|
3042
3193
|
|
|
@@ -3046,7 +3197,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
|
|
|
3046
3197
|
|
|
3047
3198
|
Or with a <link> tag:
|
|
3048
3199
|
|
|
3049
|
-
import { Theme } from '@astryxdesign/core';
|
|
3200
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3050
3201
|
import { ${trailer.exportName} } from '${trailer.jsImport}';
|
|
3051
3202
|
|
|
3052
3203
|
<link rel="stylesheet" href="${trailer.cssImport}" />
|
|
@@ -3072,7 +3223,7 @@ export function printBatchTrailer(trailers) {
|
|
|
3072
3223
|
logger.log(`
|
|
3073
3224
|
Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
|
|
3074
3225
|
|
|
3075
|
-
import { Theme } from '@astryxdesign/core';
|
|
3226
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3076
3227
|
import { ${first.exportName} } from '${first.jsImport}';
|
|
3077
3228
|
import '${first.cssImport}';
|
|
3078
3229
|
|
|
@@ -3082,7 +3233,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
|
|
|
3082
3233
|
|
|
3083
3234
|
Or with a <link> tag:
|
|
3084
3235
|
|
|
3085
|
-
import { Theme } from '@astryxdesign/core';
|
|
3236
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3086
3237
|
import { ${first.exportName} } from '${first.jsImport}';
|
|
3087
3238
|
|
|
3088
3239
|
<link rel="stylesheet" href="${first.cssImport}" />
|
|
@@ -3449,6 +3600,8 @@ export async function themeBuildFamily(
|
|
|
3449
3600
|
js: path.relative(cwd, jsPath),
|
|
3450
3601
|
dts: path.relative(cwd, dtsPath),
|
|
3451
3602
|
};
|
|
3603
|
+
// A family build's outputs stay inside cwd too; check mode only reads them.
|
|
3604
|
+
if (!options.check) confineOutputs(writes, cwd, FAMILY_REMEDY);
|
|
3452
3605
|
if (options.check) {
|
|
3453
3606
|
const stale = staleBuildOutputs(writes, cwd);
|
|
3454
3607
|
const upToDate = stale.length === 0;
|
|
@@ -3465,7 +3618,10 @@ export async function themeBuildFamily(
|
|
|
3465
3618
|
` ${entry.reason === 'missing' ? 'missing' : 'stale'}: ${entry.path}`,
|
|
3466
3619
|
);
|
|
3467
3620
|
}
|
|
3468
|
-
|
|
3621
|
+
// prettier-ignore
|
|
3622
|
+
const remedy = outsideCheckRemedy(writes, members.map(member => member.filePath), cwd, relative => ['--family', ...members.map(member => relative(member.filePath)), '--family-key', baseName, ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
|
|
3623
|
+
if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
|
|
3624
|
+
else logger.error(`\n Rebuild with: ${buildCommand}`);
|
|
3469
3625
|
}
|
|
3470
3626
|
return {
|
|
3471
3627
|
type: 'theme.build.check',
|
|
@@ -58,13 +58,13 @@ export const doc = {
|
|
|
58
58
|
name: 'options.out',
|
|
59
59
|
type: 'string',
|
|
60
60
|
description:
|
|
61
|
-
'Override the output CSS path. The .js, .d.ts and any .variants.d.ts are written in the same directory, named after the theme (<name>.js), not after the CSS file.
|
|
61
|
+
'Override the output CSS path, relative or absolute; a build refuses a path outside cwd, and check mode refuses only a relative path that escapes cwd. The .js, .d.ts and any .variants.d.ts are written in the same directory, named after the theme (<name>.js), not after the CSS file.',
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
name: 'options.check',
|
|
65
65
|
type: 'boolean',
|
|
66
66
|
description:
|
|
67
|
-
'Compile in memory and compare each output against what is on disk instead of writing: the CI drift guard.',
|
|
67
|
+
'Compile in memory and compare each output against what is on disk instead of writing: the CI drift guard. Because it writes nothing, it also compares outputs outside cwd; when those are stale, the report names the directory that contains them and the build and check commands to run from there, since a build refuses them and each output records the directory it was built from.',
|
|
68
68
|
default: 'false',
|
|
69
69
|
},
|
|
70
70
|
{
|
|
@@ -77,7 +77,7 @@ export const doc = {
|
|
|
77
77
|
name: 'ctx.cwd',
|
|
78
78
|
type: 'string',
|
|
79
79
|
description:
|
|
80
|
-
'Directory the theme file, a relative out path, and the returned output paths resolve against.',
|
|
80
|
+
'Directory the theme file, a relative out path, and the returned output paths resolve against. Every output a build writes is inside it, including the default outputs beside the theme file.',
|
|
81
81
|
default: 'process.cwd()',
|
|
82
82
|
},
|
|
83
83
|
],
|
|
@@ -105,7 +105,7 @@ export const doc = {
|
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
code: 'ERR_PATH_TRAVERSAL',
|
|
108
|
-
when: 'the theme name contains a path separator or traversal marker, or a
|
|
108
|
+
when: 'the theme name contains a path separator or traversal marker, or a written output resolves outside cwd (including through a symlink): an `out` path, relative or absolute, or the default outputs beside a theme file outside cwd. Check mode writes nothing, so it refuses only a relative `out` path that escapes cwd',
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
code: 'ERR_CORE_NOT_FOUND',
|
package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
import transform from '../migrate-list-edge-compensation-to-edge.mjs';
|
|
5
|
+
|
|
6
|
+
async function applyTransform(source) {
|
|
7
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
8
|
+
const j = jscodeshift.withParser('tsx');
|
|
9
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
10
|
+
return transform({source, path: 'test.tsx'}, api) ?? source;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('migrate-list-edge-compensation-to-edge', () => {
|
|
14
|
+
it("renames edgeCompensation='inline' to edge='outset'", async () => {
|
|
15
|
+
const output =
|
|
16
|
+
await applyTransform(`import {List, ListItem} from '@astryxdesign/core/List';
|
|
17
|
+
const a = <List edgeCompensation="inline"><ListItem label="a" /></List>;
|
|
18
|
+
const b = <List hasDividers edgeCompensation={'inline'} />;`);
|
|
19
|
+
|
|
20
|
+
expect(output).not.toContain('edgeCompensation');
|
|
21
|
+
expect(output.match(/edge='outset'/g)).toHaveLength(2);
|
|
22
|
+
expect(output).toContain('hasDividers');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('follows aliases and the package root import', async () => {
|
|
26
|
+
const output =
|
|
27
|
+
await applyTransform(`import {List as Rows} from '@astryxdesign/core';
|
|
28
|
+
const a = <Rows edgeCompensation="inline" />;`);
|
|
29
|
+
|
|
30
|
+
expect(output).toContain("<Rows edge='outset' />");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('drops edgeCompensation beside an edge string, whatever its value', async () => {
|
|
34
|
+
const output =
|
|
35
|
+
await applyTransform(`import {List} from '@astryxdesign/core/List';
|
|
36
|
+
const a = <List edge="bleed" edgeCompensation="inline" />;
|
|
37
|
+
const b = <List edge={'flush'} edgeCompensation={mode} />;
|
|
38
|
+
const c = <List {...rest} edge="padded" edgeCompensation="inline" />;`);
|
|
39
|
+
|
|
40
|
+
// c: the spread comes before edge, so it cannot override it.
|
|
41
|
+
expect(output).toContain('edge="bleed"');
|
|
42
|
+
expect(output).toContain("edge={'flush'}");
|
|
43
|
+
expect(output).toContain('edge="padded"');
|
|
44
|
+
expect(output).not.toContain('edgeCompensation');
|
|
45
|
+
expect(output).not.toContain('TODO');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('leaves a TODO where edge may be undefined at runtime', async () => {
|
|
49
|
+
const output =
|
|
50
|
+
await applyTransform(`import {List} from '@astryxdesign/core/List';
|
|
51
|
+
const a = <List edge={edge} edgeCompensation="inline" />;
|
|
52
|
+
const b = <List edge="flush" {...rest} edgeCompensation="inline" />;
|
|
53
|
+
const c = <List edge={props.edge} edgeCompensation={mode} />;`);
|
|
54
|
+
|
|
55
|
+
// a and c: a computed edge falls back to the old prop when undefined.
|
|
56
|
+
// b: the spread after edge may set it to undefined.
|
|
57
|
+
expect(output.match(/TODO\(astryx upgrade\)/g)).toHaveLength(3);
|
|
58
|
+
expect(output.match(/edgeCompensation=/g)).toHaveLength(3);
|
|
59
|
+
expect(output).toContain('edge={edge}');
|
|
60
|
+
expect(output).toContain('edge={props.edge}');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('leaves a TODO on a dynamic value and keeps it working', async () => {
|
|
64
|
+
const output =
|
|
65
|
+
await applyTransform(`import {List} from '@astryxdesign/core/List';
|
|
66
|
+
const a = <List edgeCompensation={mode} />;
|
|
67
|
+
const b = <List edgeCompensation={flag ? 'inline' : undefined} />;`);
|
|
68
|
+
|
|
69
|
+
expect(output.match(/TODO\(astryx upgrade\)/g)).toHaveLength(2);
|
|
70
|
+
expect(output).toContain('edgeCompensation={mode}');
|
|
71
|
+
expect(output).toContain("edgeCompensation={flag ? 'inline' : undefined}");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('keeps edgeCompensation with a TODO when the List also spreads props', async () => {
|
|
75
|
+
const output =
|
|
76
|
+
await applyTransform(`import {List} from '@astryxdesign/core/List';
|
|
77
|
+
const a = <List {...rest} edgeCompensation="inline" />;
|
|
78
|
+
const b = <List edgeCompensation="inline" {...rest} />;
|
|
79
|
+
const c = <List {...rest} edgeCompensation={mode} />;
|
|
80
|
+
const d = <List {...props} />;
|
|
81
|
+
const e = <List {...rest} edge="padded" />;`);
|
|
82
|
+
|
|
83
|
+
// a: an edge in the spread wins today, and a new edge would override
|
|
84
|
+
// it. b: the spread may override either prop. c: dynamic value and a
|
|
85
|
+
// spread, one TODO. d: no evidence of the old prop, so unchanged; its
|
|
86
|
+
// @deprecated type reports a typed use where the props are written.
|
|
87
|
+
// e: no old prop.
|
|
88
|
+
expect(output.match(/TODO\(astryx upgrade\)/g)).toHaveLength(3);
|
|
89
|
+
const code = output.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
90
|
+
expect(code).not.toContain("edge='outset'");
|
|
91
|
+
expect(code.match(/edgeCompensation="inline"/g)).toHaveLength(2);
|
|
92
|
+
expect(output).toContain('edgeCompensation={mode}');
|
|
93
|
+
expect(output).toContain('const d = <List {...props} />;');
|
|
94
|
+
expect(output).toContain('edge="padded"');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('leaves other components, other packages, and unrelated spreads alone', async () => {
|
|
98
|
+
const source = `import {List} from '@astryxdesign/core/List';
|
|
99
|
+
import {TabList} from '@astryxdesign/core/TabList';
|
|
100
|
+
import {List as Other} from 'some-other-package';
|
|
101
|
+
const a = <TabList edgeCompensation="inline" {...props} />;
|
|
102
|
+
const b = <Other edgeCompensation="inline" {...props} />;
|
|
103
|
+
const c = <div {...props} />;`;
|
|
104
|
+
expect(await applyTransform(source)).toBe(source);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('changes nothing on a second run', async () => {
|
|
108
|
+
const once =
|
|
109
|
+
await applyTransform(`import {List} from '@astryxdesign/core/List';
|
|
110
|
+
const a = <List edgeCompensation="inline" />;
|
|
111
|
+
const b = <List edgeCompensation={mode} />;
|
|
112
|
+
const c = <List {...props} edgeCompensation="inline" />;
|
|
113
|
+
const d = <List edge="flush" edgeCompensation="inline" />;
|
|
114
|
+
const e = <List edge={edge} edgeCompensation="inline" />;`);
|
|
115
|
+
const twice = await applyTransform(once);
|
|
116
|
+
|
|
117
|
+
expect(twice).toBe(once);
|
|
118
|
+
expect(twice.match(/TODO\(astryx upgrade\)/g)).toHaveLength(3);
|
|
119
|
+
});
|
|
120
|
+
});
|