@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
package/src/api/template.mjs
CHANGED
|
@@ -6,11 +6,38 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
+
import {loadModuleWithSchema} from '../lib/module-loader.mjs';
|
|
10
|
+
import {TemplateEnvelopeSchema} from '../template.mjs';
|
|
9
11
|
import {CLI_ROOT, discoverExternalPackages} from '../utils/paths.mjs';
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
assertWithin,
|
|
14
|
+
isFilePathArg,
|
|
15
|
+
PathSafetyError,
|
|
16
|
+
} from '../utils/path-safety.mjs';
|
|
11
17
|
import {AstryxError} from './error.mjs';
|
|
12
18
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
13
|
-
import {
|
|
19
|
+
import {Project} from '../lib/project.mjs';
|
|
20
|
+
|
|
21
|
+
/** Identity used for core (built-in) templates in package-scoped listings. */
|
|
22
|
+
const CORE_PACKAGE = '@astryxdesign/core';
|
|
23
|
+
|
|
24
|
+
/** Doc-file basename suffixes for integration templates, in precedence order. */
|
|
25
|
+
const DOC_SUFFIXES = ['.doc.ts', '.doc.mjs', '.doc.js'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load an integration template doc module and validate it against the template
|
|
29
|
+
* envelope at the load boundary. Default export only — `.ts` via jiti,
|
|
30
|
+
* `.mjs`/`.js` via dynamic import. Throws (caught by discovery) if the default
|
|
31
|
+
* export is missing or fails {@link TemplateEnvelopeSchema}. NOTE: the built-in
|
|
32
|
+
* core templates use `export const doc = {...}` and are loaded by a different
|
|
33
|
+
* function ({@link loadDocModule}) — this path is for INTEGRATION templates.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} file
|
|
36
|
+
* @param {string} [label]
|
|
37
|
+
*/
|
|
38
|
+
async function loadIntegrationDoc(file, label) {
|
|
39
|
+
return loadModuleWithSchema(file, TemplateEnvelopeSchema, {label});
|
|
40
|
+
}
|
|
14
41
|
|
|
15
42
|
const TEMPLATES_DIR = path.join(CLI_ROOT, 'templates');
|
|
16
43
|
const PAGES_DIR = path.join(TEMPLATES_DIR, 'pages');
|
|
@@ -23,7 +50,7 @@ const BLOCKS_DIR = path.join(TEMPLATES_DIR, 'blocks');
|
|
|
23
50
|
* or not. Mirrors apps/docsite/public/template-assets/placeholder.svg.
|
|
24
51
|
*/
|
|
25
52
|
const PLACEHOLDER_IMAGE =
|
|
26
|
-
|
|
53
|
+
'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
54
|
|
|
28
55
|
/**
|
|
29
56
|
* Demo-image sources to strip from scaffolded projects — Meta's lookaside CDN
|
|
@@ -60,9 +87,12 @@ export {discoverAll as discoverTemplates};
|
|
|
60
87
|
export function listTemplates() {
|
|
61
88
|
const all = [];
|
|
62
89
|
if (fs.existsSync(PAGES_DIR)) {
|
|
63
|
-
all.push(
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
all.push(
|
|
91
|
+
...fs
|
|
92
|
+
.readdirSync(PAGES_DIR, {withFileTypes: true})
|
|
93
|
+
.filter(e => e.isDirectory())
|
|
94
|
+
.map(e => e.name),
|
|
95
|
+
);
|
|
66
96
|
}
|
|
67
97
|
return all.sort();
|
|
68
98
|
}
|
|
@@ -83,7 +113,8 @@ function findDocFiles(dir, pattern) {
|
|
|
83
113
|
|
|
84
114
|
async function discoverPages() {
|
|
85
115
|
if (!fs.existsSync(PAGES_DIR)) return [];
|
|
86
|
-
const dirs = fs
|
|
116
|
+
const dirs = fs
|
|
117
|
+
.readdirSync(PAGES_DIR, {withFileTypes: true})
|
|
87
118
|
.filter(e => e.isDirectory());
|
|
88
119
|
|
|
89
120
|
const templates = [];
|
|
@@ -132,8 +163,8 @@ async function discoverBlocks() {
|
|
|
132
163
|
}
|
|
133
164
|
|
|
134
165
|
/**
|
|
135
|
-
* Discover blocks from external packages that declare `
|
|
136
|
-
* Same shape as discoverBlocks() output.
|
|
166
|
+
* Discover blocks from external packages that declare `astryx.blocks` in
|
|
167
|
+
* their package.json. Same shape as discoverBlocks() output.
|
|
137
168
|
*
|
|
138
169
|
* @param {string} [cwd]
|
|
139
170
|
*/
|
|
@@ -182,20 +213,190 @@ async function discoverAllBlocks(cwd = process.cwd()) {
|
|
|
182
213
|
return [...core, ...external];
|
|
183
214
|
}
|
|
184
215
|
|
|
185
|
-
async function discoverAll() {
|
|
186
|
-
const [pages, blocks] = await Promise.all([
|
|
216
|
+
async function discoverAll(cwd = process.cwd()) {
|
|
217
|
+
const [pages, blocks, integration] = await Promise.all([
|
|
187
218
|
discoverPages(),
|
|
188
|
-
discoverAllBlocks(),
|
|
219
|
+
discoverAllBlocks(cwd),
|
|
220
|
+
discoverIntegrationTemplates(cwd),
|
|
189
221
|
]);
|
|
190
|
-
return [...pages, ...blocks].sort((a, b) =>
|
|
222
|
+
return [...pages, ...blocks, ...integration.templates].sort((a, b) =>
|
|
223
|
+
a.name.localeCompare(b.name),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Like {@link discoverAll} but also returns integration-template discovery
|
|
229
|
+
* errors (missing same-stem source, missing `type`, load failure). Use this
|
|
230
|
+
* when the caller wants to warn about malformed integration templates.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} [cwd]
|
|
233
|
+
* @returns {Promise<{templates: object[], errors: {package: string, template?: string, message: string}[]}>}
|
|
234
|
+
*/
|
|
235
|
+
async function discoverAllWithErrors(cwd = process.cwd()) {
|
|
236
|
+
const [pages, blocks, integration] = await Promise.all([
|
|
237
|
+
discoverPages(),
|
|
238
|
+
discoverAllBlocks(cwd),
|
|
239
|
+
discoverIntegrationTemplates(cwd),
|
|
240
|
+
]);
|
|
241
|
+
const templates = [...pages, ...blocks, ...integration.templates].sort(
|
|
242
|
+
(a, b) => a.name.localeCompare(b.name),
|
|
243
|
+
);
|
|
244
|
+
return {templates, errors: integration.errors};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export {discoverAllWithErrors};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Recursively collect integration template doc files under `root`.
|
|
251
|
+
* Returns absolute paths to files ending in one of DOC_SUFFIXES.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} root
|
|
254
|
+
* @returns {string[]}
|
|
255
|
+
*/
|
|
256
|
+
function findIntegrationDocFiles(root) {
|
|
257
|
+
const results = [];
|
|
258
|
+
if (!fs.existsSync(root)) return results;
|
|
259
|
+
const walk = dir => {
|
|
260
|
+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
261
|
+
const full = path.join(dir, entry.name);
|
|
262
|
+
if (entry.isDirectory()) {
|
|
263
|
+
walk(full);
|
|
264
|
+
} else if (DOC_SUFFIXES.some(suffix => entry.name.endsWith(suffix))) {
|
|
265
|
+
results.push(full);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
walk(root);
|
|
270
|
+
return results;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The doc-file suffix present on `file`, or null if none matches.
|
|
275
|
+
* @param {string} file
|
|
276
|
+
*/
|
|
277
|
+
function matchedDocSuffix(file) {
|
|
278
|
+
return DOC_SUFFIXES.find(suffix => file.endsWith(suffix)) ?? null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Discover templates contributed by configured integrations.
|
|
283
|
+
*
|
|
284
|
+
* For each integration with a resolved `templates` root, every
|
|
285
|
+
* `<id>.doc.{ts,mjs,js}` file is a template whose id is its path relative to
|
|
286
|
+
* the templates root with the `.doc.*` suffix stripped (kebab-case, may be
|
|
287
|
+
* nested). The doc's `type` (page|block) decides scaffolding — there is no
|
|
288
|
+
* `/pages` vs `/blocks` requirement. A same-stem sibling source file
|
|
289
|
+
* (`<id>.tsx`) is required; a doc missing its source, or missing `type`, is
|
|
290
|
+
* an integration error and that template is skipped (recorded in `errors`).
|
|
291
|
+
*
|
|
292
|
+
* @param {string} [cwd]
|
|
293
|
+
* @returns {Promise<{templates: object[], errors: {package: string, template?: string, message: string}[]}>}
|
|
294
|
+
*/
|
|
295
|
+
async function discoverIntegrationTemplates(cwd = process.cwd()) {
|
|
296
|
+
const templates = [];
|
|
297
|
+
const errors = [];
|
|
298
|
+
|
|
299
|
+
let loadedIntegrations;
|
|
300
|
+
try {
|
|
301
|
+
const project = await Project.load(cwd);
|
|
302
|
+
loadedIntegrations = project.loadedIntegrations;
|
|
303
|
+
} catch {
|
|
304
|
+
// Config load failures are surfaced elsewhere (discover/doctor); here we
|
|
305
|
+
// simply contribute no integration templates.
|
|
306
|
+
return {templates, errors};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const integration of loadedIntegrations) {
|
|
310
|
+
const result = await discoverIntegrationTemplatesForOne(integration);
|
|
311
|
+
templates.push(...result.templates);
|
|
312
|
+
errors.push(...result.errors);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {templates, errors};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Discover the templates contributed by a SINGLE integration. Same per-template
|
|
320
|
+
* rules as {@link discoverIntegrationTemplates} (same-stem source required,
|
|
321
|
+
* page|block type required); broken templates are recorded in `errors` rather
|
|
322
|
+
* than thrown. Exposed for `validate-integration`.
|
|
323
|
+
*
|
|
324
|
+
* @param {{name?: string, __spec?: string, templates?: string}} integration
|
|
325
|
+
* @returns {Promise<{templates: object[], errors: {package: string, template?: string, message: string}[]}>}
|
|
326
|
+
*/
|
|
327
|
+
export async function discoverIntegrationTemplatesForOne(integration) {
|
|
328
|
+
const templates = [];
|
|
329
|
+
const errors = [];
|
|
330
|
+
|
|
331
|
+
const root = integration?.templates;
|
|
332
|
+
const pkgLabel = integration?.name ?? integration?.__spec ?? 'integration';
|
|
333
|
+
if (!root || !fs.existsSync(root)) return {templates, errors};
|
|
334
|
+
|
|
335
|
+
for (const docPath of findIntegrationDocFiles(root)) {
|
|
336
|
+
const suffix = matchedDocSuffix(docPath);
|
|
337
|
+
const id = path
|
|
338
|
+
.relative(root, docPath)
|
|
339
|
+
.slice(0, -suffix.length)
|
|
340
|
+
.split(path.sep)
|
|
341
|
+
.join('/');
|
|
342
|
+
|
|
343
|
+
const sourcePath = docPath.slice(0, -suffix.length) + '.tsx';
|
|
344
|
+
if (!fs.existsSync(sourcePath)) {
|
|
345
|
+
errors.push({
|
|
346
|
+
package: pkgLabel,
|
|
347
|
+
template: id,
|
|
348
|
+
message: `Template "${id}" is missing its same-stem source file ${path.basename(sourcePath)}.`,
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let doc;
|
|
354
|
+
try {
|
|
355
|
+
doc = await loadIntegrationDoc(docPath, `Template "${id}"`);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
errors.push({
|
|
358
|
+
package: pkgLabel,
|
|
359
|
+
template: id,
|
|
360
|
+
message: `Template "${id}" failed to load: ${err.message}`,
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// loadIntegrationDoc validates against the envelope (incl. a 'page'|'block'
|
|
366
|
+
// type) at the load boundary, so a valid doc always has a type. This guard
|
|
367
|
+
// stays as defense-in-depth.
|
|
368
|
+
const type = doc?.type;
|
|
369
|
+
if (type !== 'page' && type !== 'block') {
|
|
370
|
+
errors.push({
|
|
371
|
+
package: pkgLabel,
|
|
372
|
+
template: id,
|
|
373
|
+
message: `Template "${id}" is missing a "type" of "page" or "block". Author it with createPageTemplate/createBlockTemplate.`,
|
|
374
|
+
});
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
templates.push({
|
|
379
|
+
type,
|
|
380
|
+
dirName: id,
|
|
381
|
+
name: doc?.name || id,
|
|
382
|
+
description: doc?.description || '',
|
|
383
|
+
category: doc?.category || '',
|
|
384
|
+
isReady: true,
|
|
385
|
+
scaffold: false,
|
|
386
|
+
componentsUsed: doc?.componentsUsed ?? [],
|
|
387
|
+
filePath: sourcePath,
|
|
388
|
+
docPath,
|
|
389
|
+
package: pkgLabel,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {templates, errors};
|
|
191
394
|
}
|
|
192
395
|
|
|
193
396
|
export async function findRelatedBlocks(componentName, cwd) {
|
|
194
397
|
const blocks = await discoverAllBlocks(cwd);
|
|
195
398
|
return blocks.filter(b =>
|
|
196
|
-
b.componentsUsed.some(c =>
|
|
197
|
-
c.toLowerCase() === componentName.toLowerCase(),
|
|
198
|
-
),
|
|
399
|
+
b.componentsUsed.some(c => c.toLowerCase() === componentName.toLowerCase()),
|
|
199
400
|
);
|
|
200
401
|
}
|
|
201
402
|
|
|
@@ -218,7 +419,7 @@ export async function findShowcase(componentName, cwd, options) {
|
|
|
218
419
|
return true;
|
|
219
420
|
});
|
|
220
421
|
|
|
221
|
-
const toResult =
|
|
422
|
+
const toResult = b => ({
|
|
222
423
|
name: b.name,
|
|
223
424
|
aspectRatio: b.aspectRatio,
|
|
224
425
|
filePath: b.filePath,
|
|
@@ -242,8 +443,14 @@ export async function findShowcase(componentName, cwd, options) {
|
|
|
242
443
|
}
|
|
243
444
|
|
|
244
445
|
const UBIQUITOUS = new Set([
|
|
245
|
-
'Text',
|
|
246
|
-
'
|
|
446
|
+
'Text',
|
|
447
|
+
'Heading',
|
|
448
|
+
'Button',
|
|
449
|
+
'HStack',
|
|
450
|
+
'VStack',
|
|
451
|
+
'Link',
|
|
452
|
+
'StackItem',
|
|
453
|
+
'Icon',
|
|
247
454
|
]);
|
|
248
455
|
|
|
249
456
|
export function extractComponents(pagePath) {
|
|
@@ -259,22 +466,115 @@ export function extractComponents(pagePath) {
|
|
|
259
466
|
while ((m = tagRegex.exec(src)) !== null) {
|
|
260
467
|
matches.push(m[2]);
|
|
261
468
|
}
|
|
262
|
-
return [
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
469
|
+
return [
|
|
470
|
+
...new Set(
|
|
471
|
+
matches
|
|
472
|
+
.filter(n => !['Theme', 'ThemeProvider'].includes(n))
|
|
473
|
+
.filter(n => !UBIQUITOUS.has(n))
|
|
474
|
+
.map(n =>
|
|
475
|
+
n.replace(
|
|
476
|
+
/(Item|Section|Header|Content|Footer|Panel|Heading|CollapseButton|Column|Sortable|Selection|Group|Source)$/,
|
|
477
|
+
'',
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
.filter(Boolean),
|
|
481
|
+
),
|
|
482
|
+
].sort();
|
|
269
483
|
}
|
|
270
484
|
|
|
271
485
|
const STRUCTURAL = new Set([
|
|
272
|
-
'AppShell',
|
|
273
|
-
'
|
|
274
|
-
'
|
|
275
|
-
'
|
|
486
|
+
'AppShell',
|
|
487
|
+
'Layout',
|
|
488
|
+
'LayoutHeader',
|
|
489
|
+
'LayoutContent',
|
|
490
|
+
'LayoutPanel',
|
|
491
|
+
'LayoutFooter',
|
|
492
|
+
'Card',
|
|
493
|
+
'Section',
|
|
494
|
+
'Grid',
|
|
495
|
+
'GridSpan',
|
|
496
|
+
'List',
|
|
497
|
+
'Table',
|
|
498
|
+
'TabList',
|
|
499
|
+
'Toolbar',
|
|
500
|
+
'SideNav',
|
|
501
|
+
'TopNav',
|
|
502
|
+
'Dialog',
|
|
503
|
+
'FormLayout',
|
|
504
|
+
'Center',
|
|
276
505
|
]);
|
|
277
506
|
|
|
507
|
+
const SPATIAL_PROPS = [
|
|
508
|
+
'padding',
|
|
509
|
+
'contentPadding',
|
|
510
|
+
'gap',
|
|
511
|
+
'rowGap',
|
|
512
|
+
'columnGap',
|
|
513
|
+
'columns',
|
|
514
|
+
'minChildWidth',
|
|
515
|
+
'hasDivider',
|
|
516
|
+
'defaultHasDividers',
|
|
517
|
+
'variant',
|
|
518
|
+
'density',
|
|
519
|
+
'role',
|
|
520
|
+
'height',
|
|
521
|
+
'width',
|
|
522
|
+
'maxWidth',
|
|
523
|
+
];
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Copy allowlisted layout props verbatim from a JSX opening-tag fragment.
|
|
527
|
+
* Uses quote/brace matching so object literals and spaced strings stay intact.
|
|
528
|
+
*
|
|
529
|
+
* @param {string} tagText
|
|
530
|
+
* @returns {string[]}
|
|
531
|
+
*/
|
|
532
|
+
function extractSpatialAttrs(tagText) {
|
|
533
|
+
const attrs = [];
|
|
534
|
+
for (const name of SPATIAL_PROPS) {
|
|
535
|
+
const eqMatch = tagText.match(new RegExp(`\\b${name}\\s*=\\s*`));
|
|
536
|
+
if (eqMatch) {
|
|
537
|
+
const start = eqMatch.index;
|
|
538
|
+
let i = eqMatch.index + eqMatch[0].length;
|
|
539
|
+
const rest = tagText.slice(i);
|
|
540
|
+
|
|
541
|
+
if (rest[0] === '"' || rest[0] === "'") {
|
|
542
|
+
const q = rest[0];
|
|
543
|
+
i += 1;
|
|
544
|
+
while (i < tagText.length && tagText[i] !== q) {
|
|
545
|
+
if (tagText[i] === '\\') i += 1;
|
|
546
|
+
i += 1;
|
|
547
|
+
}
|
|
548
|
+
if (i < tagText.length) i += 1;
|
|
549
|
+
} else if (rest[0] === '{') {
|
|
550
|
+
let depth = 0;
|
|
551
|
+
while (i < tagText.length) {
|
|
552
|
+
if (tagText[i] === '{') depth += 1;
|
|
553
|
+
else if (tagText[i] === '}') {
|
|
554
|
+
depth -= 1;
|
|
555
|
+
if (depth === 0) {
|
|
556
|
+
i += 1;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
i += 1;
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
const bare = rest.match(/^[^\s/>]+/);
|
|
564
|
+
i += bare ? bare[0].length : 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
attrs.push(tagText.slice(start, i).trim());
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (new RegExp(`\\b${name}(?=[\\s/>])`).test(tagText)) {
|
|
572
|
+
attrs.push(name);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return attrs;
|
|
576
|
+
}
|
|
577
|
+
|
|
278
578
|
function extractSkeleton(source) {
|
|
279
579
|
const lines = source.split('\n');
|
|
280
580
|
const out = [];
|
|
@@ -287,11 +587,18 @@ function extractSkeleton(source) {
|
|
|
287
587
|
for (let i = 0; i < lines.length; i++) {
|
|
288
588
|
const t = lines[i].trim();
|
|
289
589
|
|
|
290
|
-
if (t.match(/^export\s+default\s+function/)) {
|
|
291
|
-
|
|
590
|
+
if (t.match(/^export\s+default\s+function/)) {
|
|
591
|
+
inDefaultExport = true;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (inDefaultExport && t.match(/^return\s*\(/)) {
|
|
595
|
+
capturing = true;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
292
598
|
if (!capturing) continue;
|
|
293
599
|
if (out.length >= MAX_LINES) {
|
|
294
|
-
if (!out[out.length - 1]?.includes('...'))
|
|
600
|
+
if (!out[out.length - 1]?.includes('...'))
|
|
601
|
+
out.push(' '.repeat(depth) + '...');
|
|
295
602
|
continue;
|
|
296
603
|
}
|
|
297
604
|
|
|
@@ -309,20 +616,13 @@ function extractSkeleton(source) {
|
|
|
309
616
|
if (lines[j].includes('>')) break;
|
|
310
617
|
}
|
|
311
618
|
|
|
312
|
-
const props =
|
|
313
|
-
const propRegex = /\b(padding|contentPadding|gap|rowGap|columnGap|columns|minChildWidth|hasDivider|defaultHasDividers|variant|density|role|height|width|maxWidth)\s*[=]\s*\{?\s*['"]?([^}'"\s,/>]+)/g;
|
|
314
|
-
let m;
|
|
315
|
-
while ((m = propRegex.exec(tagText)) !== null) {
|
|
316
|
-
const val = m[2];
|
|
317
|
-
if (val === 'true') props.push(m[1]);
|
|
318
|
-
else if (/^\d+$/.test(val)) props.push(`${m[1]}={${val}}`);
|
|
319
|
-
else props.push(`${m[1]}="${val}"`);
|
|
320
|
-
}
|
|
321
|
-
|
|
619
|
+
const props = extractSpatialAttrs(tagText);
|
|
322
620
|
const hasSpatialProps = props.length > 0;
|
|
323
621
|
const propStr = hasSpatialProps ? ' ' + props.join(' ') : '';
|
|
324
622
|
const isVStack = comp === 'VStack' || comp === 'HStack';
|
|
325
|
-
const isSelfClosing = tagText.match(
|
|
623
|
+
const isSelfClosing = tagText.match(
|
|
624
|
+
new RegExp('<' + tagName + '[^>]*/>', 's'),
|
|
625
|
+
);
|
|
326
626
|
|
|
327
627
|
if (isVStack && !hasSpatialProps) continue;
|
|
328
628
|
|
|
@@ -349,13 +649,18 @@ function extractSkeleton(source) {
|
|
|
349
649
|
continue;
|
|
350
650
|
}
|
|
351
651
|
|
|
352
|
-
const slotMatch = t.match(
|
|
652
|
+
const slotMatch = t.match(
|
|
653
|
+
/^(header|content|footer|start|end|sideNav|topNav)\s*=\s*\{/,
|
|
654
|
+
);
|
|
353
655
|
if (slotMatch) {
|
|
354
656
|
out.push(' '.repeat(depth) + `/* ${slotMatch[1]}: */`);
|
|
355
657
|
continue;
|
|
356
658
|
}
|
|
357
659
|
|
|
358
|
-
if (
|
|
660
|
+
if (
|
|
661
|
+
t.startsWith('<div') &&
|
|
662
|
+
(t.includes('padding') || t.includes('maxWidth') || t.includes('gap:'))
|
|
663
|
+
) {
|
|
359
664
|
const styleProps = [];
|
|
360
665
|
const divText = lines.slice(i, Math.min(i + 5, lines.length)).join(' ');
|
|
361
666
|
const pp = divText.match(/padding[^:]*:\s*['"]?([^'"},)]+)/);
|
|
@@ -377,67 +682,6 @@ function extractSkeleton(source) {
|
|
|
377
682
|
return out.filter(l => l.trim()).join('\n');
|
|
378
683
|
}
|
|
379
684
|
|
|
380
|
-
/**
|
|
381
|
-
* Fetch a template by ID using the `template.get` hook in astryx.config.mjs.
|
|
382
|
-
* @param {string} id
|
|
383
|
-
* @param {object} [options]
|
|
384
|
-
* @param {string} [options.cwd]
|
|
385
|
-
* @returns {Promise<{type: 'template.get', data: {id: string, source: string}}>}
|
|
386
|
-
*/
|
|
387
|
-
export async function getTemplateById(id, options = {}) {
|
|
388
|
-
const {cwd = process.cwd()} = options;
|
|
389
|
-
const config = await loadConfig(cwd);
|
|
390
|
-
|
|
391
|
-
const getter = config.template?.get;
|
|
392
|
-
if (typeof getter !== 'function') {
|
|
393
|
-
throw new AstryxError(
|
|
394
|
-
'Template fetching by ID is not configured.\n' +
|
|
395
|
-
'Add a template.get function to astryx.config.mjs:\n\n' +
|
|
396
|
-
' export default {\n' +
|
|
397
|
-
' template: {\n' +
|
|
398
|
-
" get: async (id) => { /* return template source string */ },\n" +
|
|
399
|
-
' },\n' +
|
|
400
|
-
' };',
|
|
401
|
-
undefined,
|
|
402
|
-
ERROR_CODES.ERR_TEMPLATE_CONFIG,
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
let source;
|
|
407
|
-
try {
|
|
408
|
-
source = await getter(id);
|
|
409
|
-
} catch (err) {
|
|
410
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
411
|
-
throw new AstryxError(`template.get("${id}") threw an error: ${detail}`, undefined, ERROR_CODES.ERR_TEMPLATE_GET);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (source == null) {
|
|
415
|
-
throw new AstryxError(
|
|
416
|
-
`template.get("${id}") returned ${source} — no template found for that ID`,
|
|
417
|
-
undefined,
|
|
418
|
-
ERROR_CODES.ERR_TEMPLATE_GET,
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (typeof source !== 'string') {
|
|
423
|
-
throw new AstryxError(
|
|
424
|
-
`template.get("${id}") must return a string, got ${typeof source}`,
|
|
425
|
-
undefined,
|
|
426
|
-
ERROR_CODES.ERR_TEMPLATE_GET,
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (source.trim() === '') {
|
|
431
|
-
throw new AstryxError(
|
|
432
|
-
`template.get("${id}") returned an empty string`,
|
|
433
|
-
undefined,
|
|
434
|
-
ERROR_CODES.ERR_TEMPLATE_GET,
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return {type: 'template.get', data: {id, source}};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
685
|
/**
|
|
442
686
|
* @param {string} [name]
|
|
443
687
|
* @param {object} [options]
|
|
@@ -445,38 +689,77 @@ export async function getTemplateById(id, options = {}) {
|
|
|
445
689
|
* @param {boolean} [options.list]
|
|
446
690
|
* @param {boolean} [options.skeleton]
|
|
447
691
|
* @param {boolean} [options.show]
|
|
448
|
-
* @param {'page'|'block'} [options.type] - Filter list views by template kind.
|
|
692
|
+
* @param {'page'|'block'} [options.type] - Filter list views / narrow lookups by template kind.
|
|
693
|
+
* @param {string} [options.package] - Narrow lookups to a specific package (id-only matches across packages are ambiguous).
|
|
449
694
|
* @param {string} [options.cwd]
|
|
450
695
|
* @returns {Promise<{type: string, data: unknown}>}
|
|
451
696
|
*/
|
|
452
697
|
export async function template(name, options = {}) {
|
|
453
|
-
const {
|
|
454
|
-
|
|
698
|
+
const {
|
|
699
|
+
list = false,
|
|
700
|
+
skeleton = false,
|
|
701
|
+
show = false,
|
|
702
|
+
targetPath,
|
|
703
|
+
type,
|
|
704
|
+
package: packageFilter,
|
|
705
|
+
cwd = process.cwd(),
|
|
706
|
+
} = options;
|
|
707
|
+
const templates = await discoverAll(cwd);
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Identity for a template in package-scoped views. Core (built-in)
|
|
711
|
+
* templates have no `package` field; report them under @astryxdesign/core.
|
|
712
|
+
* @param {{package?: string}} t
|
|
713
|
+
*/
|
|
714
|
+
const pkgOf = t => t.package ?? CORE_PACKAGE;
|
|
455
715
|
|
|
456
716
|
if (list || (!name && !skeleton)) {
|
|
457
717
|
let filtered = templates;
|
|
458
|
-
if (type) filtered =
|
|
718
|
+
if (type) filtered = filtered.filter(t => t.type === type);
|
|
719
|
+
if (packageFilter) filtered = filtered.filter(t => pkgOf(t) === packageFilter);
|
|
459
720
|
return {
|
|
460
721
|
type: 'template.list',
|
|
461
722
|
data: filtered.map(t => ({
|
|
462
|
-
|
|
723
|
+
id: t.dirName,
|
|
724
|
+
name: t.name,
|
|
725
|
+
// `displayName` retained for back-compat with existing consumers.
|
|
463
726
|
displayName: t.name,
|
|
464
727
|
description: t.description,
|
|
728
|
+
type: t.type,
|
|
729
|
+
package: pkgOf(t),
|
|
730
|
+
category: t.category || undefined,
|
|
731
|
+
componentsUsed: t.componentsUsed ?? undefined,
|
|
465
732
|
isReady: t.isReady,
|
|
466
733
|
scaffold: t.scaffold ?? false,
|
|
467
|
-
type: t.type,
|
|
468
734
|
})),
|
|
469
735
|
};
|
|
470
736
|
}
|
|
471
737
|
|
|
472
|
-
|
|
473
|
-
|
|
738
|
+
// Resolve `name` to a single template. The same id can appear across types
|
|
739
|
+
// and/or packages (e.g. a core "hero" page and an integration "hero"
|
|
740
|
+
// block); narrow with --type / --package.
|
|
741
|
+
let candidates = templates.filter(t => t.dirName === name);
|
|
742
|
+
if (type) candidates = candidates.filter(t => t.type === type);
|
|
743
|
+
if (packageFilter) candidates = candidates.filter(t => pkgOf(t) === packageFilter);
|
|
744
|
+
|
|
745
|
+
if (name && candidates.length === 0) {
|
|
474
746
|
throw new AstryxError(
|
|
475
747
|
`Unknown template "${name}"`,
|
|
476
748
|
templates.map(t => ({name: t.dirName, reason: `${t.type} template`})),
|
|
477
749
|
ERROR_CODES.ERR_UNKNOWN_TEMPLATE,
|
|
478
750
|
);
|
|
479
751
|
}
|
|
752
|
+
if (name && candidates.length > 1) {
|
|
753
|
+
throw new AstryxError(
|
|
754
|
+
`Template "${name}" is ambiguous — narrow it with --type and/or --package.`,
|
|
755
|
+
candidates.map(t => ({
|
|
756
|
+
name: t.dirName,
|
|
757
|
+
reason: `${t.type} template in ${pkgOf(t)}`,
|
|
758
|
+
})),
|
|
759
|
+
ERROR_CODES.ERR_AMBIGUOUS_TEMPLATE,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const match = candidates[0];
|
|
480
763
|
|
|
481
764
|
if (skeleton) {
|
|
482
765
|
if (!match) {
|
|
@@ -487,7 +770,11 @@ export async function template(name, options = {}) {
|
|
|
487
770
|
);
|
|
488
771
|
}
|
|
489
772
|
if (!fs.existsSync(match.filePath)) {
|
|
490
|
-
throw new AstryxError(
|
|
773
|
+
throw new AstryxError(
|
|
774
|
+
`No source file found for template "${name}"`,
|
|
775
|
+
undefined,
|
|
776
|
+
ERROR_CODES.ERR_NO_SOURCE,
|
|
777
|
+
);
|
|
491
778
|
}
|
|
492
779
|
const src = fs.readFileSync(match.filePath, 'utf-8');
|
|
493
780
|
return {
|
|
@@ -502,7 +789,11 @@ export async function template(name, options = {}) {
|
|
|
502
789
|
}
|
|
503
790
|
|
|
504
791
|
if (!fs.existsSync(match.filePath)) {
|
|
505
|
-
throw new AstryxError(
|
|
792
|
+
throw new AstryxError(
|
|
793
|
+
`No source file found for template "${name}"`,
|
|
794
|
+
undefined,
|
|
795
|
+
ERROR_CODES.ERR_NO_SOURCE,
|
|
796
|
+
);
|
|
506
797
|
}
|
|
507
798
|
|
|
508
799
|
if (show || !targetPath) {
|
|
@@ -529,7 +820,11 @@ export async function template(name, options = {}) {
|
|
|
529
820
|
});
|
|
530
821
|
} catch (err) {
|
|
531
822
|
if (err instanceof PathSafetyError) {
|
|
532
|
-
throw new AstryxError(
|
|
823
|
+
throw new AstryxError(
|
|
824
|
+
err.message,
|
|
825
|
+
undefined,
|
|
826
|
+
ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
827
|
+
);
|
|
533
828
|
}
|
|
534
829
|
throw err;
|
|
535
830
|
}
|
|
@@ -546,9 +841,8 @@ export async function template(name, options = {}) {
|
|
|
546
841
|
outputFilePath = resolvedTarget;
|
|
547
842
|
} else {
|
|
548
843
|
outputDir = resolvedTarget;
|
|
549
|
-
outputFileName =
|
|
550
|
-
? path.basename(match.filePath)
|
|
551
|
-
: 'page.tsx';
|
|
844
|
+
outputFileName =
|
|
845
|
+
match.type === 'block' ? path.basename(match.filePath) : 'page.tsx';
|
|
552
846
|
outputFilePath = path.join(outputDir, outputFileName);
|
|
553
847
|
}
|
|
554
848
|
|
|
@@ -563,6 +857,11 @@ export async function template(name, options = {}) {
|
|
|
563
857
|
const relOutput = path.relative(cwd, outputDir) || '.';
|
|
564
858
|
return {
|
|
565
859
|
type: 'template.copy',
|
|
566
|
-
data: {
|
|
860
|
+
data: {
|
|
861
|
+
template: name,
|
|
862
|
+
outputDir: relOutput,
|
|
863
|
+
fileName: outputFileName,
|
|
864
|
+
filesCopied: 1,
|
|
865
|
+
},
|
|
567
866
|
};
|
|
568
867
|
}
|