@astryxdesign/cli 0.1.1 → 0.1.2-canary.043f3fd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +31 -8
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +87 -28
- package/src/api/doctor.mjs +7 -22
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +302 -0
- package/src/api/layout.test.mjs +241 -0
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +421 -122
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/api/validate-integration.mjs +370 -0
- package/src/api/validate-integration.test.mjs +222 -0
- package/src/codemod.mjs +93 -0
- package/src/codemod.test.mjs +134 -0
- package/src/codemods/__tests__/registry.test.mjs +2 -0
- package/src/codemods/__tests__/runner.test.mjs +103 -0
- package/src/codemods/integration-discovery.mjs +168 -0
- package/src/codemods/integration-discovery.test.mjs +234 -0
- package/src/codemods/integration-runner.mjs +109 -0
- package/src/codemods/registry.mjs +2 -0
- package/src/codemods/run-codemod.mjs +207 -0
- package/src/codemods/runner.mjs +73 -96
- package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +81 -0
- package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
- package/src/codemods/transforms/v0.1.0/index.mjs +15 -6
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
- package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
- package/src/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/component/index.mjs +44 -10
- package/src/commands/component-ownership.test.mjs +227 -0
- package/src/commands/discover.mjs +5 -16
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +241 -224
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/swizzle.routing.test.mjs +279 -0
- package/src/commands/template.mjs +33 -37
- package/src/commands/upgrade.config-ordering.test.mjs +273 -0
- package/src/commands/upgrade.integration-policy.test.mjs +204 -0
- package/src/commands/upgrade.mjs +349 -239
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +18 -0
- package/src/config.test.mjs +100 -0
- package/src/index.mjs +13 -3
- package/src/integration.mjs +19 -0
- package/src/lib/component-discovery.mjs +177 -0
- package/src/lib/config-cache.mjs +91 -0
- package/src/lib/config-cache.test.mjs +83 -0
- package/src/lib/config-schema.mjs +105 -0
- package/src/lib/error-codes.mjs +18 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +132 -0
- package/src/lib/integrations.test.mjs +135 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +15 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/template.mjs +73 -0
- package/src/template.test.mjs +127 -0
- package/src/types/api.d.ts +2 -6
- package/src/types/base.d.ts +3 -9
- package/src/types/codemod.d.ts +81 -0
- package/src/types/component.d.ts +27 -2
- package/src/types/config.d.ts +70 -0
- package/src/types/error-codes.d.ts +5 -3
- package/src/types/index.d.ts +0 -1
- package/src/types/integration.d.ts +29 -0
- package/src/types/swizzle.d.ts +9 -2
- package/src/types/template-api.d.ts +54 -0
- package/src/types/template.d.ts +10 -7
- package/src/types/upgrade.d.ts +29 -0
- package/src/types/validate-integration.d.ts +24 -0
- package/src/utils/github.mjs +0 -252
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
- package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +242 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/commands/gap-report.mjs +0 -456
- package/src/commands/gap-report.test.mjs +0 -163
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -86
- package/src/lib/config.test.mjs +0 -42
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import {Project} from './project.mjs';
|
|
7
|
+
import {loadIntegrations} from './integrations.mjs';
|
|
8
|
+
import {discover} from '../api/discover.mjs';
|
|
9
|
+
|
|
10
|
+
let tmpDir;
|
|
11
|
+
let originalCwd;
|
|
12
|
+
|
|
13
|
+
function writeManifestPackage(dir, {basename = 'astryx.integration.mjs', body}) {
|
|
14
|
+
const pkgDir = path.join(dir, 'node_modules', '@acme', 'widgets');
|
|
15
|
+
fs.mkdirSync(pkgDir, {recursive: true});
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
path.join(pkgDir, 'package.json'),
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
name: '@acme/widgets',
|
|
20
|
+
version: '1.2.3',
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
fs.writeFileSync(path.join(pkgDir, basename), body);
|
|
24
|
+
return pkgDir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
originalCwd = process.cwd();
|
|
29
|
+
tmpDir = fs.mkdtempSync(
|
|
30
|
+
path.join(process.cwd(), '.astryx-integration-test-'),
|
|
31
|
+
);
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(tmpDir, 'package.json'),
|
|
34
|
+
JSON.stringify({name: 'consumer'}),
|
|
35
|
+
);
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
38
|
+
`export default { integrations: ['@acme/widgets'] };\n`,
|
|
39
|
+
);
|
|
40
|
+
process.chdir(tmpDir);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
process.chdir(originalCwd);
|
|
45
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('configured integrations', () => {
|
|
49
|
+
it('resolves identity from package.json and contribution roots to absolute paths', async () => {
|
|
50
|
+
const pkgDir = writeManifestPackage(tmpDir, {
|
|
51
|
+
body: `export default {
|
|
52
|
+
components: './docs',
|
|
53
|
+
templates: './blocks',
|
|
54
|
+
codemods: './codemods',
|
|
55
|
+
issuesUrl: 'https://example.com/issues',
|
|
56
|
+
};\n`,
|
|
57
|
+
});
|
|
58
|
+
fs.mkdirSync(path.join(pkgDir, 'docs'));
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
path.join(pkgDir, 'docs', 'Widget.doc.mjs'),
|
|
61
|
+
`export const doc = {
|
|
62
|
+
name: 'Widget',
|
|
63
|
+
usage: {description: 'Acme widget'},
|
|
64
|
+
props: [],
|
|
65
|
+
};\n`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const project = await Project.load(tmpDir);
|
|
69
|
+
expect(project.integrations).toEqual(['@acme/widgets']);
|
|
70
|
+
const loaded = project.loadedIntegrations[0];
|
|
71
|
+
// Identity comes from package.json, not the manifest.
|
|
72
|
+
expect(loaded.name).toBe('@acme/widgets');
|
|
73
|
+
expect(loaded.version).toBe('1.2.3');
|
|
74
|
+
expect(loaded.components).toBe(path.join(pkgDir, 'docs'));
|
|
75
|
+
expect(loaded.templates).toBe(path.join(pkgDir, 'blocks'));
|
|
76
|
+
expect(loaded.codemods).toBe(path.join(pkgDir, 'codemods'));
|
|
77
|
+
expect(loaded.issuesUrl).toBe('https://example.com/issues');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('makes integration components discoverable', async () => {
|
|
81
|
+
const pkgDir = writeManifestPackage(tmpDir, {
|
|
82
|
+
body: `export default { components: './docs' };\n`,
|
|
83
|
+
});
|
|
84
|
+
fs.mkdirSync(path.join(pkgDir, 'docs'));
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
path.join(pkgDir, 'docs', 'Widget.doc.mjs'),
|
|
87
|
+
`export const doc = {
|
|
88
|
+
name: 'Widget',
|
|
89
|
+
usage: {description: 'Acme widget'},
|
|
90
|
+
props: [],
|
|
91
|
+
};\n`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const result = await discover(undefined, {});
|
|
95
|
+
expect(result.type).toBe('discover.list');
|
|
96
|
+
expect(result.data).toEqual([
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
name: '@acme/widgets',
|
|
99
|
+
category: '@acme/widgets',
|
|
100
|
+
components: ['Widget'],
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('errors when the package has no conventional root manifest', async () => {
|
|
106
|
+
const pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
|
|
107
|
+
fs.mkdirSync(pkgDir, {recursive: true});
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(pkgDir, 'package.json'),
|
|
110
|
+
JSON.stringify({name: '@acme/widgets'}),
|
|
111
|
+
);
|
|
112
|
+
await expect(loadIntegrations(['@acme/widgets'], {cwd: tmpDir})).rejects.toThrow(
|
|
113
|
+
/no conventional root manifest/,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('errors when the package has multiple root manifests', async () => {
|
|
118
|
+
const pkgDir = writeManifestPackage(tmpDir, {
|
|
119
|
+
body: `export default {};\n`,
|
|
120
|
+
});
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
path.join(pkgDir, 'astryx.integration.js'),
|
|
123
|
+
`module.exports = {};\n`,
|
|
124
|
+
);
|
|
125
|
+
await expect(loadIntegrations(['@acme/widgets'], {cwd: tmpDir})).rejects.toThrow(
|
|
126
|
+
/multiple root manifests/,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('errors when the package is not installed', async () => {
|
|
131
|
+
await expect(loadIntegrations(['@acme/missing'], {cwd: tmpDir})).rejects.toThrow(
|
|
132
|
+
/Could not find installed integration package/,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Levenshtein edit distance — pure, dependency-free.
|
|
5
|
+
*
|
|
6
|
+
* Lives apart from string-utils.mjs (which dynamically imports node:fs/path
|
|
7
|
+
* for component search) so browser-bundled code — the XLE/XLO layout
|
|
8
|
+
* language — can use fuzzy matching without dragging node: schemes into the
|
|
9
|
+
* webpack graph.
|
|
10
|
+
*
|
|
11
|
+
* @input two strings
|
|
12
|
+
* @output edit distance (number)
|
|
13
|
+
* @position lib — shared by string-utils.mjs and lib/xle/validate.mjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function levenshteinDistance(a, b) {
|
|
17
|
+
const m = a.length, n = b.length;
|
|
18
|
+
const dp = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
|
|
19
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
20
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
21
|
+
for (let i = 1; i <= m; i++) {
|
|
22
|
+
for (let j = 1; j <= n; j++) {
|
|
23
|
+
dp[i][j] = a[i-1] === b[j-1]
|
|
24
|
+
? dp[i-1][j-1]
|
|
25
|
+
: 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return dp[m][n];
|
|
29
|
+
}
|
package/src/lib/manifest.mjs
CHANGED
|
@@ -56,14 +56,18 @@ export const RESPONSE_TYPES = {
|
|
|
56
56
|
'template.show',
|
|
57
57
|
'template.skeleton',
|
|
58
58
|
'template.copy',
|
|
59
|
-
'template.get',
|
|
60
59
|
],
|
|
61
60
|
hook: ['hook.list', 'hook.brief', 'hook.full', 'hook.detail', 'hook.detail.params'],
|
|
62
61
|
'theme build': ['theme.build'],
|
|
62
|
+
'theme list': ['theme.list'],
|
|
63
|
+
'theme add': ['theme.list', 'theme.add'],
|
|
63
64
|
upgrade: ['upgrade.list', 'upgrade.status', 'upgrade.run'],
|
|
64
|
-
'gap-report': ['gap-report.categories', 'gap-report.dryRun', 'gap-report.file'],
|
|
65
65
|
manifest: ['manifest'],
|
|
66
66
|
doctor: ['doctor'],
|
|
67
|
+
'validate-integration': ['integration.validate'],
|
|
68
|
+
'layout expand': ['layout.expand'],
|
|
69
|
+
'layout check': ['layout.check'],
|
|
70
|
+
'layout grammar': ['layout.grammar'],
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
/**
|
|
@@ -79,10 +83,19 @@ const EXAMPLES = {
|
|
|
79
83
|
template: ['astryx template --json', 'astryx template dashboard ./src/app'],
|
|
80
84
|
hook: ['astryx hook', 'astryx hook useToggle --json'],
|
|
81
85
|
'theme build': ['astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css'],
|
|
86
|
+
'theme list': ['astryx theme list --json'],
|
|
87
|
+
'theme add': ['astryx theme add matcha', 'astryx theme add matcha ./src/themes/matcha'],
|
|
82
88
|
upgrade: ['astryx upgrade --json'],
|
|
83
89
|
manifest: ['astryx manifest --json', 'astryx --json'],
|
|
84
90
|
doctor: ['astryx doctor', 'astryx doctor --json'],
|
|
91
|
+
'validate-integration': [
|
|
92
|
+
'astryx validate-integration',
|
|
93
|
+
'astryx validate-integration @acme/widgets --json',
|
|
94
|
+
],
|
|
85
95
|
init: ['astryx init'],
|
|
96
|
+
'layout expand': [`astryx layout expand 'V[g6] > C{card-callout}*4' ./src/Page.tsx`],
|
|
97
|
+
'layout check': [`astryx layout check 'A[cp6] > L > LC > S[p6]' --json`],
|
|
98
|
+
'layout grammar': ['astryx layout grammar'],
|
|
86
99
|
};
|
|
87
100
|
|
|
88
101
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Shared module-loading + conventional-file-discovery primitives.
|
|
5
|
+
*
|
|
6
|
+
* Both config loading and integration loading need to (a) import a
|
|
7
|
+
* user-authored module (`.ts` via jiti, `.mjs`/`.js` via native dynamic
|
|
8
|
+
* import) and (b) find conventional files by basename in a fixed
|
|
9
|
+
* load-precedence order. These helpers centralize that so the two callers stay
|
|
10
|
+
* in lockstep.
|
|
11
|
+
*
|
|
12
|
+
* `loadModuleWithSchema` builds on these primitives to provide the single
|
|
13
|
+
* load/validation boundary shared by config, integration, codemod, and
|
|
14
|
+
* template discovery: import the module, take its default export, and validate
|
|
15
|
+
* it against the expected zod schema.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import {pathToFileURL} from 'node:url';
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import {createJiti} from 'jiti';
|
|
22
|
+
import {formatZodError} from './config-schema.mjs';
|
|
23
|
+
|
|
24
|
+
let jitiInstance;
|
|
25
|
+
function getJiti() {
|
|
26
|
+
if (!jitiInstance) {
|
|
27
|
+
jitiInstance = createJiti(import.meta.url);
|
|
28
|
+
}
|
|
29
|
+
return jitiInstance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Import a user-authored module. `.ts` is loaded via jiti; `.mjs`/`.js` via
|
|
34
|
+
* native dynamic import (file:// URL). Returns the full module namespace.
|
|
35
|
+
* @param {string} file absolute path
|
|
36
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
37
|
+
*/
|
|
38
|
+
export async function importUserModule(file) {
|
|
39
|
+
if (file.endsWith('.ts')) {
|
|
40
|
+
return await getJiti().import(file);
|
|
41
|
+
}
|
|
42
|
+
return await import(pathToFileURL(file).href);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return the conventional files (by basename, in the given precedence order)
|
|
47
|
+
* that exist directly in `dir`, as absolute paths. Never throws.
|
|
48
|
+
* @param {string} dir
|
|
49
|
+
* @param {string[]} basenames precedence-ordered
|
|
50
|
+
* @returns {string[]} absolute paths of present files, in basenames order
|
|
51
|
+
*/
|
|
52
|
+
export function findPresentFiles(dir, basenames) {
|
|
53
|
+
return basenames
|
|
54
|
+
.filter(name => fs.existsSync(path.join(dir, name)))
|
|
55
|
+
.map(name => path.join(dir, name));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Import a user-authored module, take its default export, and validate it
|
|
60
|
+
* against a zod schema. This is the single load/validation boundary for all
|
|
61
|
+
* user-authored modules (config, integration, codemod, template): execute the
|
|
62
|
+
* module, take the default export, validate it against the expected schema.
|
|
63
|
+
* Throws a clear, readable error if the default export is missing or fails
|
|
64
|
+
* validation.
|
|
65
|
+
*
|
|
66
|
+
* @template {import('zod').ZodType} S
|
|
67
|
+
* @param {string} file absolute path
|
|
68
|
+
* @param {S} schema
|
|
69
|
+
* @param {{label?: string}} [opts] label used in error messages
|
|
70
|
+
* @returns {Promise<import('zod').infer<S>>} parsed + typed value
|
|
71
|
+
*/
|
|
72
|
+
export async function loadModuleWithSchema(file, schema, {label} = {}) {
|
|
73
|
+
const mod = await importUserModule(file);
|
|
74
|
+
const exported = mod?.default;
|
|
75
|
+
const result = schema.safeParse(exported);
|
|
76
|
+
if (!result.success) {
|
|
77
|
+
throw new Error(formatZodError(label ?? file, result.error));
|
|
78
|
+
}
|
|
79
|
+
return result.data;
|
|
80
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import {z} from 'zod';
|
|
7
|
+
import {
|
|
8
|
+
findPresentFiles,
|
|
9
|
+
importUserModule,
|
|
10
|
+
loadModuleWithSchema,
|
|
11
|
+
} from './module-loader.mjs';
|
|
12
|
+
|
|
13
|
+
let tmpDir;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-module-loader-test-'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('findPresentFiles', () => {
|
|
24
|
+
const basenames = ['a.ts', 'b.mjs', 'c.js'];
|
|
25
|
+
|
|
26
|
+
it('returns [] when none of the basenames are present', () => {
|
|
27
|
+
expect(findPresentFiles(tmpDir, basenames)).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns only the present files, in basenames precedence order', () => {
|
|
31
|
+
// Create out of order to prove the result follows `basenames`, not disk order.
|
|
32
|
+
fs.writeFileSync(path.join(tmpDir, 'c.js'), '');
|
|
33
|
+
fs.writeFileSync(path.join(tmpDir, 'a.ts'), '');
|
|
34
|
+
const present = findPresentFiles(tmpDir, basenames);
|
|
35
|
+
expect(present.map(p => path.basename(p))).toEqual(['a.ts', 'c.js']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns absolute paths joined to the given directory', () => {
|
|
39
|
+
fs.writeFileSync(path.join(tmpDir, 'b.mjs'), '');
|
|
40
|
+
const present = findPresentFiles(tmpDir, basenames);
|
|
41
|
+
expect(present).toEqual([path.join(tmpDir, 'b.mjs')]);
|
|
42
|
+
expect(path.isAbsolute(present[0])).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not match files in nested subdirectories', () => {
|
|
46
|
+
const nested = path.join(tmpDir, 'nested');
|
|
47
|
+
fs.mkdirSync(nested);
|
|
48
|
+
fs.writeFileSync(path.join(nested, 'a.ts'), '');
|
|
49
|
+
expect(findPresentFiles(tmpDir, basenames)).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('importUserModule', () => {
|
|
54
|
+
it('imports a .mjs file and returns its module namespace', async () => {
|
|
55
|
+
const file = path.join(tmpDir, 'mod.mjs');
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
file,
|
|
58
|
+
`export default {answer: 42};\nexport const named = 'hi';\n`,
|
|
59
|
+
);
|
|
60
|
+
const mod = await importUserModule(file);
|
|
61
|
+
expect(mod.default).toEqual({answer: 42});
|
|
62
|
+
expect(mod.named).toBe('hi');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('loadModuleWithSchema', () => {
|
|
67
|
+
// Temp module files live under a repo-local dir (not /tmp): Vite's dynamic
|
|
68
|
+
// import blocks /tmp, so we mirror the existing repo-local temp pattern.
|
|
69
|
+
const schema = z
|
|
70
|
+
.object({
|
|
71
|
+
name: z.string(),
|
|
72
|
+
count: z.number().optional(),
|
|
73
|
+
})
|
|
74
|
+
.strict();
|
|
75
|
+
|
|
76
|
+
it('returns the parsed default export when it satisfies the schema', async () => {
|
|
77
|
+
const file = path.join(tmpDir, 'valid.mjs');
|
|
78
|
+
fs.writeFileSync(file, `export default {name: 'ok', count: 3};\n`);
|
|
79
|
+
const value = await loadModuleWithSchema(file, schema, {label: 'thing'});
|
|
80
|
+
expect(value).toEqual({name: 'ok', count: 3});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('throws a readable error when there is no default export', async () => {
|
|
84
|
+
const file = path.join(tmpDir, 'no-default.mjs');
|
|
85
|
+
fs.writeFileSync(file, `export const named = {name: 'x'};\n`);
|
|
86
|
+
await expect(
|
|
87
|
+
loadModuleWithSchema(file, schema, {label: 'thing'}),
|
|
88
|
+
).rejects.toThrow(/thing is invalid/i);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('throws a readable error when the default export fails the schema', async () => {
|
|
92
|
+
const file = path.join(tmpDir, 'invalid.mjs');
|
|
93
|
+
fs.writeFileSync(file, `export default {count: 'not-a-number'};\n`);
|
|
94
|
+
await expect(
|
|
95
|
+
loadModuleWithSchema(file, schema, {label: 'thing'}),
|
|
96
|
+
).rejects.toThrow(/thing is invalid:.*name/i);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('falls back to the file path in the message when no label is given', async () => {
|
|
100
|
+
const file = path.join(tmpDir, 'unlabeled.mjs');
|
|
101
|
+
fs.writeFileSync(file, `export default {bogus: true};\n`);
|
|
102
|
+
await expect(loadModuleWithSchema(file, schema)).rejects.toThrow(
|
|
103
|
+
new RegExp(`${path.basename(file)} is invalid`),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -15,7 +15,11 @@ export function scanDirectory(scanDir) {
|
|
|
15
15
|
const pkgPath = path.join(scanDir, entry.name, 'package.json');
|
|
16
16
|
if (!fs.existsSync(pkgPath)) continue;
|
|
17
17
|
let pkg;
|
|
18
|
-
try {
|
|
18
|
+
try {
|
|
19
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
20
|
+
} catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
19
23
|
if (!pkg.astryx || !pkg.astryx.docs) continue;
|
|
20
24
|
const pkgDir = path.join(scanDir, entry.name);
|
|
21
25
|
const docsDir = path.resolve(pkgDir, pkg.astryx.docs);
|
|
@@ -36,9 +40,18 @@ export function scanDirectory(scanDir) {
|
|
|
36
40
|
return packages;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
export function scanAllPackages(packageDirs) {
|
|
43
|
+
export function scanAllPackages(packageDirs, explicitPackages = []) {
|
|
40
44
|
const all = [];
|
|
41
45
|
const seen = new Set();
|
|
46
|
+
|
|
47
|
+
for (const pkg of explicitPackages) {
|
|
48
|
+
if (!pkg || seen.has(pkg.name)) continue;
|
|
49
|
+
const components = discoverDocComponents(pkg.docsDir);
|
|
50
|
+
if (components.length === 0) continue;
|
|
51
|
+
seen.add(pkg.name);
|
|
52
|
+
all.push({...pkg, components});
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
for (const dir of packageDirs) {
|
|
43
56
|
for (const pkg of scanDirectory(dir)) {
|
|
44
57
|
if (seen.has(pkg.name)) continue;
|
|
@@ -54,12 +67,17 @@ function discoverDocComponents(docsDir) {
|
|
|
54
67
|
const components = [];
|
|
55
68
|
function walk(dir) {
|
|
56
69
|
let entries;
|
|
57
|
-
try {
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(dir, {withFileTypes: true});
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
58
75
|
for (const entry of entries) {
|
|
59
76
|
if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
60
77
|
const full = path.join(dir, entry.name);
|
|
61
78
|
if (entry.isDirectory()) walk(full);
|
|
62
|
-
else if (entry.name.endsWith('.doc.mjs'))
|
|
79
|
+
else if (entry.name.endsWith('.doc.mjs'))
|
|
80
|
+
components.push(entry.name.replace('.doc.mjs', ''));
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
walk(docsDir);
|
|
@@ -81,12 +99,18 @@ function findDocFile(docsDir, name) {
|
|
|
81
99
|
const target = name + '.doc.mjs';
|
|
82
100
|
function walk(dir) {
|
|
83
101
|
let entries;
|
|
84
|
-
try {
|
|
102
|
+
try {
|
|
103
|
+
entries = fs.readdirSync(dir, {withFileTypes: true});
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
85
107
|
for (const entry of entries) {
|
|
86
108
|
if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
|
|
87
109
|
const full = path.join(dir, entry.name);
|
|
88
|
-
if (entry.isDirectory()) {
|
|
89
|
-
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
const f = walk(full);
|
|
112
|
+
if (f) return f;
|
|
113
|
+
} else if (entry.name === target) return full;
|
|
90
114
|
}
|
|
91
115
|
return null;
|
|
92
116
|
}
|