@astryxdesign/cli 0.1.1-canary.4b92876 → 0.1.1-canary.7e591d0
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/package.json +13 -8
- package/src/api/discover.mjs +78 -26
- package/src/api/template.mjs +134 -43
- package/src/commands/gap-report.mjs +17 -9
- package/src/commands/gap-report.test.mjs +21 -16
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/swizzle.mjs +51 -23
- package/src/commands/upgrade.mjs +1 -70
- package/src/config.mjs +31 -0
- package/src/config.test.mjs +24 -0
- package/src/lib/config-schema.mjs +119 -0
- package/src/lib/config.mjs +34 -7
- package/src/lib/config.test.mjs +51 -2
- package/src/lib/integrations.mjs +155 -0
- package/src/lib/integrations.test.mjs +154 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/types/config.d.ts +99 -0
- package/src/utils/github.mjs +12 -27
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.1-canary.
|
|
3
|
+
"version": "0.1.1-canary.7e591d0",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"./api": {
|
|
39
39
|
"types": "./src/types/api.d.ts",
|
|
40
40
|
"import": "./src/api/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./config": {
|
|
43
|
+
"types": "./src/types/config.d.ts",
|
|
44
|
+
"import": "./src/config.mjs"
|
|
41
45
|
}
|
|
42
46
|
},
|
|
43
47
|
"files": [
|
|
@@ -51,12 +55,13 @@
|
|
|
51
55
|
"@clack/prompts": "^1.5.1",
|
|
52
56
|
"commander": "^12.1.0",
|
|
53
57
|
"jiti": "^2.7.0",
|
|
54
|
-
"jscodeshift": "^17.3.0"
|
|
58
|
+
"jscodeshift": "^17.3.0",
|
|
59
|
+
"zod": "^4.4.3"
|
|
55
60
|
},
|
|
56
61
|
"peerDependencies": {
|
|
57
|
-
"@astryxdesign/core": "0.1.1-canary.
|
|
58
|
-
"@astryxdesign/lab": "0.1.1-canary.
|
|
59
|
-
"@astryxdesign/theme-neutral": "0.1.1-canary.
|
|
62
|
+
"@astryxdesign/core": "0.1.1-canary.7e591d0",
|
|
63
|
+
"@astryxdesign/lab": "0.1.1-canary.7e591d0",
|
|
64
|
+
"@astryxdesign/theme-neutral": "0.1.1-canary.7e591d0"
|
|
60
65
|
},
|
|
61
66
|
"peerDependenciesMeta": {
|
|
62
67
|
"@astryxdesign/core": {
|
|
@@ -70,9 +75,9 @@
|
|
|
70
75
|
}
|
|
71
76
|
},
|
|
72
77
|
"devDependencies": {
|
|
73
|
-
"@astryxdesign/core": "0.1.1-canary.
|
|
74
|
-
"@astryxdesign/lab": "0.1.1-canary.
|
|
75
|
-
"@astryxdesign/theme-neutral": "0.1.1-canary.
|
|
78
|
+
"@astryxdesign/core": "0.1.1-canary.7e591d0",
|
|
79
|
+
"@astryxdesign/lab": "0.1.1-canary.7e591d0",
|
|
80
|
+
"@astryxdesign/theme-neutral": "0.1.1-canary.7e591d0"
|
|
76
81
|
},
|
|
77
82
|
"scripts": {
|
|
78
83
|
"astryx": "node bin/astryx.mjs",
|
package/src/api/discover.mjs
CHANGED
|
@@ -5,19 +5,28 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {loadConfig} from '../lib/config.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
scanAllPackages,
|
|
10
|
+
findComponentInPackages,
|
|
11
|
+
} from '../lib/package-scanner.mjs';
|
|
9
12
|
import {loadDocs} from '../lib/component-loader.mjs';
|
|
10
13
|
import {levenshteinDistance} from '../lib/string-utils.mjs';
|
|
11
14
|
import {AstryxError} from './error.mjs';
|
|
12
15
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
13
16
|
|
|
14
17
|
function validateDocs(docs) {
|
|
15
|
-
if (!docs || typeof docs !== 'object')
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
if (docs.
|
|
20
|
-
|
|
18
|
+
if (!docs || typeof docs !== 'object')
|
|
19
|
+
return 'docs export is missing or not an object';
|
|
20
|
+
if (typeof docs.name !== 'string' || !docs.name)
|
|
21
|
+
return 'docs.name is missing or not a string';
|
|
22
|
+
if (!docs.usage || typeof docs.usage.description !== 'string')
|
|
23
|
+
return 'docs.usage.description is missing or not a string';
|
|
24
|
+
if (docs.props && !Array.isArray(docs.props))
|
|
25
|
+
return 'docs.props must be an array';
|
|
26
|
+
if (docs.components && !Array.isArray(docs.components))
|
|
27
|
+
return 'docs.components must be an array';
|
|
28
|
+
if (docs.usage?.bestPractices && !Array.isArray(docs.usage.bestPractices))
|
|
29
|
+
return 'docs.usage.bestPractices must be an array';
|
|
21
30
|
return null;
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -32,7 +41,7 @@ function validateDocs(docs) {
|
|
|
32
41
|
export async function discover(query, options = {}) {
|
|
33
42
|
const {lang = null, zh = false} = options;
|
|
34
43
|
const config = await loadConfig();
|
|
35
|
-
const toEntry =
|
|
44
|
+
const toEntry = pkg => ({
|
|
36
45
|
name: pkg.name,
|
|
37
46
|
category: pkg.category,
|
|
38
47
|
components: pkg.components,
|
|
@@ -41,11 +50,14 @@ export async function discover(query, options = {}) {
|
|
|
41
50
|
displayName: pkg.displayName,
|
|
42
51
|
});
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
const explicitPackages = (config.loadedIntegrations ?? [])
|
|
54
|
+
.map(integration => integration.package)
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
if (config.packages.length === 0 && explicitPackages.length === 0) {
|
|
45
57
|
return {type: 'discover.list', data: [], meta: {configured: false}};
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
const packages = scanAllPackages(config.packages);
|
|
60
|
+
const packages = scanAllPackages(config.packages, explicitPackages);
|
|
49
61
|
|
|
50
62
|
if (packages.length === 0) {
|
|
51
63
|
return {type: 'discover.list', data: [], meta: {configured: true}};
|
|
@@ -61,7 +73,10 @@ export async function discover(query, options = {}) {
|
|
|
61
73
|
if (slashIdx > 0) {
|
|
62
74
|
const pkgName = query.slice(0, slashIdx);
|
|
63
75
|
const compName = query.slice(slashIdx + 1);
|
|
64
|
-
return await resolveComponentDocs(packages, compName, pkgName, {
|
|
76
|
+
return await resolveComponentDocs(packages, compName, pkgName, {
|
|
77
|
+
lang,
|
|
78
|
+
zh,
|
|
79
|
+
});
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
const pkg = packages.find(p => p.name === query);
|
|
@@ -99,7 +114,16 @@ export async function discover(query, options = {}) {
|
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
if (substringMatches.length > 1) {
|
|
102
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
type: 'discover.search',
|
|
119
|
+
data: {
|
|
120
|
+
query,
|
|
121
|
+
matches: substringMatches.map(m => ({
|
|
122
|
+
package: m.pkg.name,
|
|
123
|
+
component: m.comp,
|
|
124
|
+
})),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
// Fuzzy fallback
|
|
@@ -110,7 +134,10 @@ export async function discover(query, options = {}) {
|
|
|
110
134
|
}
|
|
111
135
|
}
|
|
112
136
|
const fuzzyMatches = allComponents
|
|
113
|
-
.map(item => ({
|
|
137
|
+
.map(item => ({
|
|
138
|
+
...item,
|
|
139
|
+
distance: levenshteinDistance(lower, item.comp.toLowerCase()),
|
|
140
|
+
}))
|
|
114
141
|
.filter(m => m.distance <= 3)
|
|
115
142
|
.sort((a, b) => a.distance - b.distance)
|
|
116
143
|
.slice(0, 5);
|
|
@@ -118,30 +145,46 @@ export async function discover(query, options = {}) {
|
|
|
118
145
|
if (fuzzyMatches.length > 0) {
|
|
119
146
|
throw new AstryxError(
|
|
120
147
|
`"${query}" not found`,
|
|
121
|
-
fuzzyMatches.map(m => ({
|
|
148
|
+
fuzzyMatches.map(m => ({
|
|
149
|
+
name: m.pkg.name + '/' + m.comp,
|
|
150
|
+
reason: 'similar name',
|
|
151
|
+
})),
|
|
122
152
|
ERROR_CODES.ERR_NOT_FOUND,
|
|
123
153
|
);
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
throw new AstryxError(
|
|
156
|
+
throw new AstryxError(
|
|
157
|
+
`"${query}" not found in any package`,
|
|
158
|
+
undefined,
|
|
159
|
+
ERROR_CODES.ERR_NOT_FOUND,
|
|
160
|
+
);
|
|
127
161
|
}
|
|
128
162
|
|
|
129
163
|
async function resolveComponentDocs(packages, compName, pkgName, {lang, zh}) {
|
|
130
164
|
const pkg = packages.find(p => p.name === pkgName);
|
|
131
|
-
if (!pkg)
|
|
165
|
+
if (!pkg)
|
|
166
|
+
throw new AstryxError(
|
|
167
|
+
`Package "${pkgName}" not found`,
|
|
168
|
+
undefined,
|
|
169
|
+
ERROR_CODES.ERR_UNKNOWN_PACKAGE,
|
|
170
|
+
);
|
|
132
171
|
|
|
133
172
|
const result = findComponentInPackages([pkg], compName);
|
|
134
173
|
if (!result) {
|
|
135
174
|
const lower = compName.toLowerCase();
|
|
136
175
|
const hits = pkg.components.filter(c => c.toLowerCase().includes(lower));
|
|
137
|
-
const suggestions =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
176
|
+
const suggestions =
|
|
177
|
+
hits.length > 0
|
|
178
|
+
? hits
|
|
179
|
+
: pkg.components
|
|
180
|
+
.map(c => ({
|
|
181
|
+
name: c,
|
|
182
|
+
distance: levenshteinDistance(lower, c.toLowerCase()),
|
|
183
|
+
}))
|
|
184
|
+
.filter(m => m.distance <= 3)
|
|
185
|
+
.sort((a, b) => a.distance - b.distance)
|
|
186
|
+
.slice(0, 5)
|
|
187
|
+
.map(m => m.name);
|
|
145
188
|
throw new AstryxError(
|
|
146
189
|
`Component "${compName}" not found in ${pkgName}`,
|
|
147
190
|
suggestions.map(s => ({name: s, reason: 'similar name'})),
|
|
@@ -157,9 +200,18 @@ async function loadAndValidate(result, {lang, zh}) {
|
|
|
157
200
|
try {
|
|
158
201
|
docs = await loadDocs(result.docPath, {zh, lang});
|
|
159
202
|
} catch (e) {
|
|
160
|
-
throw new AstryxError(
|
|
203
|
+
throw new AstryxError(
|
|
204
|
+
`Failed to load docs for ${result.componentName}: ${e.message}`,
|
|
205
|
+
undefined,
|
|
206
|
+
ERROR_CODES.ERR_INVALID_DOC,
|
|
207
|
+
);
|
|
161
208
|
}
|
|
162
209
|
const err = validateDocs(docs);
|
|
163
|
-
if (err)
|
|
210
|
+
if (err)
|
|
211
|
+
throw new AstryxError(
|
|
212
|
+
`Invalid docs for ${result.componentName}: ${err}`,
|
|
213
|
+
undefined,
|
|
214
|
+
ERROR_CODES.ERR_INVALID_DOC,
|
|
215
|
+
);
|
|
164
216
|
return {type: 'discover.detail.doc', data: docs};
|
|
165
217
|
}
|
package/src/api/template.mjs
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import {CLI_ROOT, discoverExternalPackages} from '../utils/paths.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
assertWithin,
|
|
12
|
+
isFilePathArg,
|
|
13
|
+
PathSafetyError,
|
|
14
|
+
} from '../utils/path-safety.mjs';
|
|
11
15
|
import {AstryxError} from './error.mjs';
|
|
12
16
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
13
17
|
import {loadConfig} from '../lib/config.mjs';
|
|
@@ -23,7 +27,7 @@ const BLOCKS_DIR = path.join(TEMPLATES_DIR, 'blocks');
|
|
|
23
27
|
* or not. Mirrors apps/docsite/public/template-assets/placeholder.svg.
|
|
24
28
|
*/
|
|
25
29
|
const PLACEHOLDER_IMAGE =
|
|
26
|
-
|
|
30
|
+
'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20400%20300%22%20preserveAspectRatio%3D%22xMidYMid%20slice%22%3E%3Crect%20width%3D%22400%22%20height%3D%22300%22%20fill%3D%22%23f5f6f8%22%2F%3E%3Cg%20transform%3D%22translate%28200%20150%29%22%20fill%3D%22none%22%20stroke%3D%22%23c2cad6%22%20stroke-width%3D%225%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%22-44%22%20y%3D%22-44%22%20width%3D%2288%22%20height%3D%2288%22%20rx%3D%2216%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%22-18%22%20r%3D%222.5%22%20fill%3D%22%23c2cad6%22%20stroke%3D%22none%22%2F%3E%3Cpath%20d%3D%22M-34%2030%20L-8%200%20L10%2018%20L20%208%20L34%2024%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E';
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Demo-image sources to strip from scaffolded projects — Meta's lookaside CDN
|
|
@@ -60,9 +64,12 @@ export {discoverAll as discoverTemplates};
|
|
|
60
64
|
export function listTemplates() {
|
|
61
65
|
const all = [];
|
|
62
66
|
if (fs.existsSync(PAGES_DIR)) {
|
|
63
|
-
all.push(
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
all.push(
|
|
68
|
+
...fs
|
|
69
|
+
.readdirSync(PAGES_DIR, {withFileTypes: true})
|
|
70
|
+
.filter(e => e.isDirectory())
|
|
71
|
+
.map(e => e.name),
|
|
72
|
+
);
|
|
66
73
|
}
|
|
67
74
|
return all.sort();
|
|
68
75
|
}
|
|
@@ -83,7 +90,8 @@ function findDocFiles(dir, pattern) {
|
|
|
83
90
|
|
|
84
91
|
async function discoverPages() {
|
|
85
92
|
if (!fs.existsSync(PAGES_DIR)) return [];
|
|
86
|
-
const dirs = fs
|
|
93
|
+
const dirs = fs
|
|
94
|
+
.readdirSync(PAGES_DIR, {withFileTypes: true})
|
|
87
95
|
.filter(e => e.isDirectory());
|
|
88
96
|
|
|
89
97
|
const templates = [];
|
|
@@ -138,7 +146,11 @@ async function discoverBlocks() {
|
|
|
138
146
|
* @param {string} [cwd]
|
|
139
147
|
*/
|
|
140
148
|
async function discoverExternalBlocks(cwd = process.cwd()) {
|
|
141
|
-
const
|
|
149
|
+
const config = await loadConfig(cwd);
|
|
150
|
+
const integrationPackages = (config.loadedIntegrations ?? [])
|
|
151
|
+
.map(integration => integration.package)
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
const externals = [...discoverExternalPackages(cwd), ...integrationPackages];
|
|
142
154
|
const blocks = [];
|
|
143
155
|
|
|
144
156
|
for (const ext of externals) {
|
|
@@ -193,9 +205,7 @@ async function discoverAll() {
|
|
|
193
205
|
export async function findRelatedBlocks(componentName, cwd) {
|
|
194
206
|
const blocks = await discoverAllBlocks(cwd);
|
|
195
207
|
return blocks.filter(b =>
|
|
196
|
-
b.componentsUsed.some(c =>
|
|
197
|
-
c.toLowerCase() === componentName.toLowerCase(),
|
|
198
|
-
),
|
|
208
|
+
b.componentsUsed.some(c => c.toLowerCase() === componentName.toLowerCase()),
|
|
199
209
|
);
|
|
200
210
|
}
|
|
201
211
|
|
|
@@ -218,7 +228,7 @@ export async function findShowcase(componentName, cwd, options) {
|
|
|
218
228
|
return true;
|
|
219
229
|
});
|
|
220
230
|
|
|
221
|
-
const toResult =
|
|
231
|
+
const toResult = b => ({
|
|
222
232
|
name: b.name,
|
|
223
233
|
aspectRatio: b.aspectRatio,
|
|
224
234
|
filePath: b.filePath,
|
|
@@ -242,8 +252,14 @@ export async function findShowcase(componentName, cwd, options) {
|
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
const UBIQUITOUS = new Set([
|
|
245
|
-
'Text',
|
|
246
|
-
'
|
|
255
|
+
'Text',
|
|
256
|
+
'Heading',
|
|
257
|
+
'Button',
|
|
258
|
+
'HStack',
|
|
259
|
+
'VStack',
|
|
260
|
+
'Link',
|
|
261
|
+
'StackItem',
|
|
262
|
+
'Icon',
|
|
247
263
|
]);
|
|
248
264
|
|
|
249
265
|
export function extractComponents(pagePath) {
|
|
@@ -259,26 +275,60 @@ export function extractComponents(pagePath) {
|
|
|
259
275
|
while ((m = tagRegex.exec(src)) !== null) {
|
|
260
276
|
matches.push(m[2]);
|
|
261
277
|
}
|
|
262
|
-
return [
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
return [
|
|
279
|
+
...new Set(
|
|
280
|
+
matches
|
|
281
|
+
.filter(n => !['Theme', 'ThemeProvider'].includes(n))
|
|
282
|
+
.filter(n => !UBIQUITOUS.has(n))
|
|
283
|
+
.map(n =>
|
|
284
|
+
n.replace(
|
|
285
|
+
/(Item|Section|Header|Content|Footer|Panel|Heading|CollapseButton|Column|Sortable|Selection|Group|Source)$/,
|
|
286
|
+
'',
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
.filter(Boolean),
|
|
290
|
+
),
|
|
291
|
+
].sort();
|
|
269
292
|
}
|
|
270
293
|
|
|
271
294
|
const STRUCTURAL = new Set([
|
|
272
|
-
'AppShell',
|
|
273
|
-
'
|
|
274
|
-
'
|
|
275
|
-
'
|
|
295
|
+
'AppShell',
|
|
296
|
+
'Layout',
|
|
297
|
+
'LayoutHeader',
|
|
298
|
+
'LayoutContent',
|
|
299
|
+
'LayoutPanel',
|
|
300
|
+
'LayoutFooter',
|
|
301
|
+
'Card',
|
|
302
|
+
'Section',
|
|
303
|
+
'Grid',
|
|
304
|
+
'GridSpan',
|
|
305
|
+
'List',
|
|
306
|
+
'Table',
|
|
307
|
+
'TabList',
|
|
308
|
+
'Toolbar',
|
|
309
|
+
'SideNav',
|
|
310
|
+
'TopNav',
|
|
311
|
+
'Dialog',
|
|
312
|
+
'FormLayout',
|
|
313
|
+
'Center',
|
|
276
314
|
]);
|
|
277
315
|
|
|
278
316
|
const SPATIAL_PROPS = [
|
|
279
|
-
'padding',
|
|
280
|
-
'
|
|
281
|
-
'
|
|
317
|
+
'padding',
|
|
318
|
+
'contentPadding',
|
|
319
|
+
'gap',
|
|
320
|
+
'rowGap',
|
|
321
|
+
'columnGap',
|
|
322
|
+
'columns',
|
|
323
|
+
'minChildWidth',
|
|
324
|
+
'hasDivider',
|
|
325
|
+
'defaultHasDividers',
|
|
326
|
+
'variant',
|
|
327
|
+
'density',
|
|
328
|
+
'role',
|
|
329
|
+
'height',
|
|
330
|
+
'width',
|
|
331
|
+
'maxWidth',
|
|
282
332
|
];
|
|
283
333
|
|
|
284
334
|
/**
|
|
@@ -346,11 +396,18 @@ function extractSkeleton(source) {
|
|
|
346
396
|
for (let i = 0; i < lines.length; i++) {
|
|
347
397
|
const t = lines[i].trim();
|
|
348
398
|
|
|
349
|
-
if (t.match(/^export\s+default\s+function/)) {
|
|
350
|
-
|
|
399
|
+
if (t.match(/^export\s+default\s+function/)) {
|
|
400
|
+
inDefaultExport = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (inDefaultExport && t.match(/^return\s*\(/)) {
|
|
404
|
+
capturing = true;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
351
407
|
if (!capturing) continue;
|
|
352
408
|
if (out.length >= MAX_LINES) {
|
|
353
|
-
if (!out[out.length - 1]?.includes('...'))
|
|
409
|
+
if (!out[out.length - 1]?.includes('...'))
|
|
410
|
+
out.push(' '.repeat(depth) + '...');
|
|
354
411
|
continue;
|
|
355
412
|
}
|
|
356
413
|
|
|
@@ -372,7 +429,9 @@ function extractSkeleton(source) {
|
|
|
372
429
|
const hasSpatialProps = props.length > 0;
|
|
373
430
|
const propStr = hasSpatialProps ? ' ' + props.join(' ') : '';
|
|
374
431
|
const isVStack = comp === 'VStack' || comp === 'HStack';
|
|
375
|
-
const isSelfClosing = tagText.match(
|
|
432
|
+
const isSelfClosing = tagText.match(
|
|
433
|
+
new RegExp('<' + tagName + '[^>]*/>', 's'),
|
|
434
|
+
);
|
|
376
435
|
|
|
377
436
|
if (isVStack && !hasSpatialProps) continue;
|
|
378
437
|
|
|
@@ -399,13 +458,18 @@ function extractSkeleton(source) {
|
|
|
399
458
|
continue;
|
|
400
459
|
}
|
|
401
460
|
|
|
402
|
-
const slotMatch = t.match(
|
|
461
|
+
const slotMatch = t.match(
|
|
462
|
+
/^(header|content|footer|start|end|sideNav|topNav)\s*=\s*\{/,
|
|
463
|
+
);
|
|
403
464
|
if (slotMatch) {
|
|
404
465
|
out.push(' '.repeat(depth) + `/* ${slotMatch[1]}: */`);
|
|
405
466
|
continue;
|
|
406
467
|
}
|
|
407
468
|
|
|
408
|
-
if (
|
|
469
|
+
if (
|
|
470
|
+
t.startsWith('<div') &&
|
|
471
|
+
(t.includes('padding') || t.includes('maxWidth') || t.includes('gap:'))
|
|
472
|
+
) {
|
|
409
473
|
const styleProps = [];
|
|
410
474
|
const divText = lines.slice(i, Math.min(i + 5, lines.length)).join(' ');
|
|
411
475
|
const pp = divText.match(/padding[^:]*:\s*['"]?([^'"},)]+)/);
|
|
@@ -445,7 +509,7 @@ export async function getTemplateById(id, options = {}) {
|
|
|
445
509
|
'Add a template.get function to astryx.config.mjs:\n\n' +
|
|
446
510
|
' export default {\n' +
|
|
447
511
|
' template: {\n' +
|
|
448
|
-
|
|
512
|
+
' get: async (id) => { /* return template source string */ },\n' +
|
|
449
513
|
' },\n' +
|
|
450
514
|
' };',
|
|
451
515
|
undefined,
|
|
@@ -458,7 +522,11 @@ export async function getTemplateById(id, options = {}) {
|
|
|
458
522
|
source = await getter(id);
|
|
459
523
|
} catch (err) {
|
|
460
524
|
const detail = err instanceof Error ? err.message : String(err);
|
|
461
|
-
throw new AstryxError(
|
|
525
|
+
throw new AstryxError(
|
|
526
|
+
`template.get("${id}") threw an error: ${detail}`,
|
|
527
|
+
undefined,
|
|
528
|
+
ERROR_CODES.ERR_TEMPLATE_GET,
|
|
529
|
+
);
|
|
462
530
|
}
|
|
463
531
|
|
|
464
532
|
if (source == null) {
|
|
@@ -500,7 +568,14 @@ export async function getTemplateById(id, options = {}) {
|
|
|
500
568
|
* @returns {Promise<{type: string, data: unknown}>}
|
|
501
569
|
*/
|
|
502
570
|
export async function template(name, options = {}) {
|
|
503
|
-
const {
|
|
571
|
+
const {
|
|
572
|
+
list = false,
|
|
573
|
+
skeleton = false,
|
|
574
|
+
show = false,
|
|
575
|
+
targetPath,
|
|
576
|
+
type,
|
|
577
|
+
cwd = process.cwd(),
|
|
578
|
+
} = options;
|
|
504
579
|
const templates = await discoverAll();
|
|
505
580
|
|
|
506
581
|
if (list || (!name && !skeleton)) {
|
|
@@ -537,7 +612,11 @@ export async function template(name, options = {}) {
|
|
|
537
612
|
);
|
|
538
613
|
}
|
|
539
614
|
if (!fs.existsSync(match.filePath)) {
|
|
540
|
-
throw new AstryxError(
|
|
615
|
+
throw new AstryxError(
|
|
616
|
+
`No source file found for template "${name}"`,
|
|
617
|
+
undefined,
|
|
618
|
+
ERROR_CODES.ERR_NO_SOURCE,
|
|
619
|
+
);
|
|
541
620
|
}
|
|
542
621
|
const src = fs.readFileSync(match.filePath, 'utf-8');
|
|
543
622
|
return {
|
|
@@ -552,7 +631,11 @@ export async function template(name, options = {}) {
|
|
|
552
631
|
}
|
|
553
632
|
|
|
554
633
|
if (!fs.existsSync(match.filePath)) {
|
|
555
|
-
throw new AstryxError(
|
|
634
|
+
throw new AstryxError(
|
|
635
|
+
`No source file found for template "${name}"`,
|
|
636
|
+
undefined,
|
|
637
|
+
ERROR_CODES.ERR_NO_SOURCE,
|
|
638
|
+
);
|
|
556
639
|
}
|
|
557
640
|
|
|
558
641
|
if (show || !targetPath) {
|
|
@@ -579,7 +662,11 @@ export async function template(name, options = {}) {
|
|
|
579
662
|
});
|
|
580
663
|
} catch (err) {
|
|
581
664
|
if (err instanceof PathSafetyError) {
|
|
582
|
-
throw new AstryxError(
|
|
665
|
+
throw new AstryxError(
|
|
666
|
+
err.message,
|
|
667
|
+
undefined,
|
|
668
|
+
ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
669
|
+
);
|
|
583
670
|
}
|
|
584
671
|
throw err;
|
|
585
672
|
}
|
|
@@ -596,9 +683,8 @@ export async function template(name, options = {}) {
|
|
|
596
683
|
outputFilePath = resolvedTarget;
|
|
597
684
|
} else {
|
|
598
685
|
outputDir = resolvedTarget;
|
|
599
|
-
outputFileName =
|
|
600
|
-
? path.basename(match.filePath)
|
|
601
|
-
: 'page.tsx';
|
|
686
|
+
outputFileName =
|
|
687
|
+
match.type === 'block' ? path.basename(match.filePath) : 'page.tsx';
|
|
602
688
|
outputFilePath = path.join(outputDir, outputFileName);
|
|
603
689
|
}
|
|
604
690
|
|
|
@@ -613,6 +699,11 @@ export async function template(name, options = {}) {
|
|
|
613
699
|
const relOutput = path.relative(cwd, outputDir) || '.';
|
|
614
700
|
return {
|
|
615
701
|
type: 'template.copy',
|
|
616
|
-
data: {
|
|
702
|
+
data: {
|
|
703
|
+
template: name,
|
|
704
|
+
outputDir: relOutput,
|
|
705
|
+
fileName: outputFileName,
|
|
706
|
+
filesCopied: 1,
|
|
707
|
+
},
|
|
617
708
|
};
|
|
618
709
|
}
|
|
@@ -132,7 +132,7 @@ export function registerGapReport(program) {
|
|
|
132
132
|
.action(async () => {
|
|
133
133
|
p.intro('Gap report setup');
|
|
134
134
|
|
|
135
|
-
const config = loadGapReportConfig();
|
|
135
|
+
const config = await loadGapReportConfig();
|
|
136
136
|
const currentMode = !config.enabled
|
|
137
137
|
? 'disabled'
|
|
138
138
|
: config.command
|
|
@@ -252,9 +252,14 @@ export function registerGapReport(program) {
|
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
const config = loadGapReportConfig();
|
|
255
|
+
const config = await loadGapReportConfig();
|
|
256
256
|
if (!config.enabled) {
|
|
257
|
-
if (json)
|
|
257
|
+
if (json)
|
|
258
|
+
return jsonError(
|
|
259
|
+
'Gap reporting is disabled',
|
|
260
|
+
undefined,
|
|
261
|
+
ERROR_CODES.ERR_GAP_REPORT_FAILED,
|
|
262
|
+
);
|
|
258
263
|
humanLog(
|
|
259
264
|
`Gap reporting is disabled (ASTRYX_GAP_REPORT=off or astryx.config.mjs).\n` +
|
|
260
265
|
`Run \`${getRunPrefix()} astryx gap-report setup\` to configure.`,
|
|
@@ -295,7 +300,7 @@ export function registerGapReport(program) {
|
|
|
295
300
|
return;
|
|
296
301
|
}
|
|
297
302
|
|
|
298
|
-
const preview = buildGapReportPreview({
|
|
303
|
+
const preview = await buildGapReportPreview({
|
|
299
304
|
component: options.component,
|
|
300
305
|
category: options.category,
|
|
301
306
|
intention: options.reason,
|
|
@@ -326,7 +331,7 @@ export function registerGapReport(program) {
|
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
try {
|
|
329
|
-
const url = createGapReport({
|
|
334
|
+
const url = await createGapReport({
|
|
330
335
|
component: options.component,
|
|
331
336
|
category: options.category,
|
|
332
337
|
intention: options.reason,
|
|
@@ -343,7 +348,9 @@ export function registerGapReport(program) {
|
|
|
343
348
|
humanLog('\nGap reporting is disabled via configuration.\n');
|
|
344
349
|
}
|
|
345
350
|
} catch (err) {
|
|
346
|
-
cliError(`Filing gap report failed: ${err.message}`, {
|
|
351
|
+
cliError(`Filing gap report failed: ${err.message}`, {
|
|
352
|
+
code: ERROR_CODES.ERR_GAP_REPORT_FAILED,
|
|
353
|
+
});
|
|
347
354
|
return;
|
|
348
355
|
}
|
|
349
356
|
return;
|
|
@@ -386,7 +393,8 @@ export function registerGapReport(program) {
|
|
|
386
393
|
placeholder:
|
|
387
394
|
'e.g. "Need a compact variant for use in dense data tables"',
|
|
388
395
|
validate: val => {
|
|
389
|
-
if (!val.trim())
|
|
396
|
+
if (!val.trim())
|
|
397
|
+
return 'Please describe what you were trying to do';
|
|
390
398
|
},
|
|
391
399
|
}),
|
|
392
400
|
);
|
|
@@ -406,7 +414,7 @@ export function registerGapReport(program) {
|
|
|
406
414
|
source: 'interactive',
|
|
407
415
|
};
|
|
408
416
|
|
|
409
|
-
const preview = buildGapReportPreview(previewArgs);
|
|
417
|
+
const preview = await buildGapReportPreview(previewArgs);
|
|
410
418
|
|
|
411
419
|
// Always show the user exactly what would be filed before sending.
|
|
412
420
|
p.note(
|
|
@@ -440,7 +448,7 @@ export function registerGapReport(program) {
|
|
|
440
448
|
s.start('Filing gap report');
|
|
441
449
|
|
|
442
450
|
try {
|
|
443
|
-
const url = createGapReport(previewArgs);
|
|
451
|
+
const url = await createGapReport(previewArgs);
|
|
444
452
|
s.stop(url ? 'Gap report filed' : 'Gap reporting is disabled');
|
|
445
453
|
if (url) {
|
|
446
454
|
p.log.success(url);
|