handoff-app 1.1.3 → 1.2.0-0
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/config/config.template.js +21 -14
- package/config/config.template.ts +54 -0
- package/config/docs/system.md +2 -0
- package/dist/app-builder/build.js +7 -2
- package/dist/app-builder/config-diff/index.d.ts +4 -0
- package/dist/app-builder/config-diff/index.js +12 -0
- package/dist/app-builder/config-diff/registry.d.ts +9 -0
- package/dist/app-builder/config-diff/registry.js +30 -0
- package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
- package/dist/app-builder/config-diff/snapshot.js +43 -0
- package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
- package/dist/app-builder/config-diff/strategies/pattern.js +93 -0
- package/dist/app-builder/config-diff/types.d.ts +19 -0
- package/dist/app-builder/config-diff/types.js +2 -0
- package/dist/app-builder/watchers/component.d.ts +15 -0
- package/dist/app-builder/watchers/component.js +69 -0
- package/dist/app-builder/watchers/utils.d.ts +14 -0
- package/dist/app-builder/watchers/utils.js +44 -0
- package/dist/app-builder/watchers.d.ts +10 -19
- package/dist/app-builder/watchers.js +56 -102
- package/dist/cache/build-cache.js +14 -5
- package/dist/cli/create.js +67 -23
- package/dist/cli/eject.js +48 -7
- package/dist/cli/make.js +29 -4
- package/dist/cli/scaffold.js +60 -19
- package/dist/cli.js +1 -1
- package/dist/config/helpers.d.ts +7 -0
- package/dist/config/helpers.js +31 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +5 -1
- package/dist/config/loader.d.ts +10 -1
- package/dist/config/loader.js +92 -28
- package/dist/config/normalizers/declaration.d.ts +11 -0
- package/dist/config/normalizers/declaration.js +119 -0
- package/dist/config/normalizers/pattern.d.ts +9 -0
- package/dist/config/normalizers/pattern.js +38 -0
- package/dist/config/runtime.d.ts +9 -3
- package/dist/config/runtime.js +254 -33
- package/dist/declarations/helpers.d.ts +6 -0
- package/dist/declarations/helpers.js +19 -0
- package/dist/declarations/index.d.ts +2 -0
- package/dist/declarations/index.js +9 -0
- package/dist/declarations/types.d.ts +60 -0
- package/dist/declarations/types.js +2 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +34 -5
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +3 -1
- package/dist/pipeline/patterns.d.ts +6 -0
- package/dist/pipeline/patterns.js +21 -0
- package/dist/transformers/docgen/index.js +12 -2
- package/dist/transformers/plugins/csf-render.js +170 -34
- package/dist/transformers/plugins/ssr-render.js +18 -2
- package/dist/transformers/preview/component/api.js +16 -7
- package/dist/transformers/preview/component/builder.js +9 -3
- package/dist/transformers/preview/component/css.d.ts +8 -0
- package/dist/transformers/preview/component/html.js +14 -5
- package/dist/transformers/preview/component/previews.d.ts +9 -0
- package/dist/transformers/preview/component/previews.js +10 -0
- package/dist/transformers/preview/pattern/api.d.ts +7 -0
- package/dist/transformers/preview/pattern/api.js +61 -0
- package/dist/transformers/preview/pattern/builder.d.ts +9 -0
- package/dist/transformers/preview/pattern/builder.js +86 -0
- package/dist/transformers/preview/pattern/html.d.ts +19 -0
- package/dist/transformers/preview/pattern/html.js +94 -0
- package/dist/transformers/preview/types.d.ts +54 -0
- package/dist/transformers/utils/csf.d.ts +16 -0
- package/dist/transformers/utils/csf.js +30 -0
- package/dist/transformers/utils/index.d.ts +1 -0
- package/dist/transformers/utils/index.js +2 -0
- package/dist/transformers/utils/module.d.ts +5 -0
- package/dist/transformers/utils/module.js +26 -13
- package/dist/transformers/utils/usage.d.ts +23 -0
- package/dist/transformers/utils/usage.js +294 -0
- package/dist/types/config.d.ts +32 -7
- package/dist/types/preview.d.ts +2 -0
- package/docs/cli.md +3 -3
- package/package.json +3 -1
- package/src/app/components/Component/PageSliceResolver.tsx +6 -6
- package/src/app/components/Component/Preview.tsx +17 -12
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
- package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
- package/src/app/components/util/index.ts +71 -2
- package/src/app/pages/system/pattern/[pattern]/index.tsx +223 -0
- package/src/app/pages/system/pattern/index.tsx +14 -0
- package/src/app-builder/README.md +37 -9
- package/src/app-builder/build.ts +8 -2
- package/src/app-builder/config-diff/index.ts +4 -0
- package/src/app-builder/config-diff/registry.ts +23 -0
- package/src/app-builder/config-diff/snapshot.ts +51 -0
- package/src/app-builder/config-diff/strategies/pattern.ts +65 -0
- package/src/app-builder/config-diff/types.ts +21 -0
- package/src/app-builder/watchers/component.ts +72 -0
- package/src/app-builder/watchers/utils.ts +44 -0
- package/src/app-builder/watchers.ts +62 -80
- package/src/cache/build-cache.ts +14 -5
- package/src/cli/create.ts +86 -25
- package/src/cli/eject.ts +29 -7
- package/src/cli/make.ts +32 -5
- package/src/cli/scaffold.ts +68 -21
- package/src/cli.ts +1 -1
- package/src/config/README.md +10 -1
- package/src/config/helpers.ts +43 -0
- package/src/config/index.ts +4 -1
- package/src/config/loader.ts +110 -31
- package/src/config/normalizers/declaration.ts +176 -0
- package/src/config/normalizers/pattern.ts +63 -0
- package/src/config/runtime.ts +310 -35
- package/src/declarations/helpers.ts +36 -0
- package/src/declarations/index.ts +19 -0
- package/src/declarations/types.ts +70 -0
- package/src/index.ts +47 -6
- package/src/pipeline/index.ts +1 -0
- package/src/pipeline/patterns.ts +10 -0
- package/src/transformers/README.md +2 -2
- package/src/transformers/docgen/index.ts +13 -2
- package/src/transformers/plugins/csf-render.ts +216 -49
- package/src/transformers/plugins/ssr-render.ts +23 -7
- package/src/transformers/preview/component/api.ts +27 -7
- package/src/transformers/preview/component/builder.ts +21 -4
- package/src/transformers/preview/component/html.ts +18 -5
- package/src/transformers/preview/component/previews.ts +12 -0
- package/src/transformers/preview/pattern/api.ts +66 -0
- package/src/transformers/preview/pattern/builder.ts +94 -0
- package/src/transformers/preview/pattern/html.ts +107 -0
- package/src/transformers/preview/types.ts +58 -1
- package/src/transformers/utils/csf.ts +48 -0
- package/src/transformers/utils/index.ts +3 -0
- package/src/transformers/utils/module.ts +33 -18
- package/src/transformers/utils/usage.ts +319 -0
- package/src/types/README.md +1 -1
- package/src/types/config.ts +33 -7
- package/src/types/preview.ts +2 -0
- package/templates/default/components/button/Button.jsx.tpl +64 -0
- package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
- package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
- package/templates/default/handoff.config.js.tpl +29 -16
- package/templates/default/handoff.config.ts.tpl +196 -0
- package/templates/default/handoff.config.with-examples.js.tpl +28 -17
- package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
- package/templates/default/package.json.tpl +1 -3
- package/handoff.config.js +0 -90
- package/templates/default/components/button/button.js.tpl +0 -62
|
@@ -2,6 +2,22 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import Handoff from '../../../index';
|
|
4
4
|
import { ComponentListObject, TransformComponentTokensResult } from '../types';
|
|
5
|
+
import { getDocumentedPreviews } from './previews';
|
|
6
|
+
|
|
7
|
+
const sanitizeComponentApiData = (
|
|
8
|
+
component: TransformComponentTokensResult
|
|
9
|
+
): TransformComponentTokensResult => {
|
|
10
|
+
if (!component) return component;
|
|
11
|
+
|
|
12
|
+
const sanitized = {
|
|
13
|
+
...component,
|
|
14
|
+
previews: getDocumentedPreviews(component.previews),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
delete sanitized.internalPatternPreviews;
|
|
18
|
+
|
|
19
|
+
return sanitized;
|
|
20
|
+
};
|
|
5
21
|
|
|
6
22
|
/**
|
|
7
23
|
* Merges values from a source object into a target object, returning a new object.
|
|
@@ -66,18 +82,19 @@ export const writeComponentApi = async (
|
|
|
66
82
|
) => {
|
|
67
83
|
const outputDirPath = path.resolve(getAPIPath(handoff), 'component');
|
|
68
84
|
const outputFilePath = path.resolve(outputDirPath, `${id}.json`);
|
|
85
|
+
const sanitizedComponent = sanitizeComponentApiData(component);
|
|
69
86
|
|
|
70
87
|
if (fs.existsSync(outputFilePath)) {
|
|
71
88
|
const existingJson = await fs.readFile(outputFilePath, 'utf8');
|
|
72
89
|
if (existingJson) {
|
|
73
90
|
try {
|
|
74
|
-
const existingData = JSON.parse(existingJson) as TransformComponentTokensResult;
|
|
91
|
+
const existingData = sanitizeComponentApiData(JSON.parse(existingJson) as TransformComponentTokensResult);
|
|
75
92
|
|
|
76
93
|
// Special case: always allow page to be cleared when undefined
|
|
77
94
|
// This handles the case where page slices are removed
|
|
78
|
-
const finalPreserveKeys =
|
|
95
|
+
const finalPreserveKeys = sanitizedComponent.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys;
|
|
79
96
|
|
|
80
|
-
const mergedData = updateObject(existingData,
|
|
97
|
+
const mergedData = sanitizeComponentApiData(updateObject(existingData, sanitizedComponent, finalPreserveKeys));
|
|
81
98
|
await fs.writeFile(outputFilePath, JSON.stringify(mergedData, null, 2));
|
|
82
99
|
return;
|
|
83
100
|
} catch (_) {
|
|
@@ -90,7 +107,7 @@ export const writeComponentApi = async (
|
|
|
90
107
|
fs.mkdirSync(outputDirPath, { recursive: true });
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
await fs.writeFile(outputFilePath, JSON.stringify(
|
|
110
|
+
await fs.writeFile(outputFilePath, JSON.stringify(sanitizedComponent, null, 2));
|
|
94
111
|
};
|
|
95
112
|
|
|
96
113
|
/**
|
|
@@ -112,7 +129,10 @@ export const updateComponentSummaryApi = async (handoff: Handoff, componentData:
|
|
|
112
129
|
if (fs.existsSync(apiPath)) {
|
|
113
130
|
try {
|
|
114
131
|
const existing = await fs.readFile(apiPath, 'utf8');
|
|
115
|
-
existingData = JSON.parse(existing)
|
|
132
|
+
existingData = (JSON.parse(existing) as ComponentListObject[]).map((component) => ({
|
|
133
|
+
...component,
|
|
134
|
+
previews: getDocumentedPreviews(component.previews),
|
|
135
|
+
}));
|
|
116
136
|
} catch {
|
|
117
137
|
// Corrupt or missing JSON — treat as empty
|
|
118
138
|
existingData = [];
|
|
@@ -140,7 +160,7 @@ export const readComponentApi = async (handoff: Handoff, id: string): Promise<Tr
|
|
|
140
160
|
try {
|
|
141
161
|
const existingJson = await fs.readFile(outputFilePath, 'utf8');
|
|
142
162
|
if (existingJson) {
|
|
143
|
-
return JSON.parse(existingJson) as TransformComponentTokensResult;
|
|
163
|
+
return sanitizeComponentApiData(JSON.parse(existingJson) as TransformComponentTokensResult);
|
|
144
164
|
}
|
|
145
165
|
} catch (_) {
|
|
146
166
|
// Unable to parse existing file
|
|
@@ -174,7 +194,7 @@ export const readComponentMetadataApi = async (handoff: Handoff, id: string): Pr
|
|
|
174
194
|
categories: componentData.categories ? componentData.categories : [],
|
|
175
195
|
tags: componentData.tags ? componentData.tags : [],
|
|
176
196
|
properties: componentData.properties,
|
|
177
|
-
previews: componentData.previews,
|
|
197
|
+
previews: getDocumentedPreviews(componentData.previews),
|
|
178
198
|
path: `/api/component/${id}.json`,
|
|
179
199
|
};
|
|
180
200
|
};
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
import Handoff from '../../../index';
|
|
17
17
|
import { Logger } from '../../../utils/logger';
|
|
18
18
|
import { ensureIds } from '../../utils/schema';
|
|
19
|
-
import { ComponentListObject, ComponentType, TransformComponentTokensResult } from '../types';
|
|
19
|
+
import { ComponentListObject, ComponentType, OptionalPreviewRender, TransformComponentTokensResult } from '../types';
|
|
20
20
|
import { readComponentApi, readComponentMetadataApi, updateComponentSummaryApi, writeComponentApi } from './api';
|
|
21
|
+
import { getDocumentedPreviews } from './previews';
|
|
21
22
|
import buildComponentCss from './css';
|
|
22
23
|
import buildPreviews from './html';
|
|
23
24
|
import buildComponentJs from './javascript';
|
|
@@ -75,6 +76,13 @@ const ensureDefaultPreview = (data: TransformComponentTokensResult): void => {
|
|
|
75
76
|
}
|
|
76
77
|
};
|
|
77
78
|
|
|
79
|
+
const getBuildPreviews = (data: TransformComponentTokensResult): { [key: string]: OptionalPreviewRender } => {
|
|
80
|
+
return {
|
|
81
|
+
...(data?.previews || {}),
|
|
82
|
+
...(data?.internalPatternPreviews || {}),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
78
86
|
/**
|
|
79
87
|
* Returns a normalized build plan describing which component segments need rebuilding.
|
|
80
88
|
*
|
|
@@ -269,10 +277,11 @@ export async function processComponents(
|
|
|
269
277
|
data.css = existingData.css;
|
|
270
278
|
data.sass = existingData.sass;
|
|
271
279
|
}
|
|
272
|
-
// If we're not building previews, preserve pre-existing HTML, code snippet, and previews.
|
|
280
|
+
// If we're not building previews, preserve pre-existing HTML, code snippet, usage, and previews.
|
|
273
281
|
if (!buildPlan.previews) {
|
|
274
282
|
data.html = existingData.html;
|
|
275
283
|
data.code = existingData.code;
|
|
284
|
+
data.usage = existingData.usage;
|
|
276
285
|
data.previews = existingData.previews;
|
|
277
286
|
}
|
|
278
287
|
/**
|
|
@@ -302,7 +311,14 @@ export async function processComponents(
|
|
|
302
311
|
}
|
|
303
312
|
// Build previews (HTML, snapshots, etc) if needed.
|
|
304
313
|
if (buildPlan.previews) {
|
|
305
|
-
data = await buildPreviews(
|
|
314
|
+
data = await buildPreviews(
|
|
315
|
+
{
|
|
316
|
+
...data,
|
|
317
|
+
previews: getBuildPreviews(data),
|
|
318
|
+
},
|
|
319
|
+
handoff,
|
|
320
|
+
components
|
|
321
|
+
);
|
|
306
322
|
}
|
|
307
323
|
|
|
308
324
|
/**
|
|
@@ -317,6 +333,7 @@ export async function processComponents(
|
|
|
317
333
|
// Ensure that every property within the properties array/object contains an 'id' field.
|
|
318
334
|
// This guarantees unique identification for property entries, which is useful for updates and API consumers.
|
|
319
335
|
data.properties = ensureIds(data.properties);
|
|
336
|
+
data.previews = getDocumentedPreviews(data.previews);
|
|
320
337
|
|
|
321
338
|
// Write the updated component data to the API file for external access and caching.
|
|
322
339
|
//console.log('data', data);
|
|
@@ -374,7 +391,7 @@ const buildComponentSummary = (id: string, data: TransformComponentTokensResult)
|
|
|
374
391
|
categories: data.categories ? data.categories : [],
|
|
375
392
|
tags: data.tags ? data.tags : [],
|
|
376
393
|
properties: data.properties,
|
|
377
|
-
previews: data.previews,
|
|
394
|
+
previews: getDocumentedPreviews(data.previews),
|
|
378
395
|
path: `${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component/${id}.json`,
|
|
379
396
|
};
|
|
380
397
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import react from '@vitejs/plugin-react';
|
|
2
2
|
import { Types as CoreTypes } from 'handoff-core';
|
|
3
3
|
import { InlineConfig, build as viteBuild } from 'vite';
|
|
4
|
+
import { RendererKind } from '../../../declarations/types';
|
|
4
5
|
import Handoff from '../../../index';
|
|
5
6
|
import { Logger } from '../../../utils/logger';
|
|
6
7
|
import { csfRenderPlugin, handlebarsPreviewsPlugin, ssrRenderPlugin } from '../../plugins';
|
|
@@ -8,6 +9,18 @@ import viteBaseConfig from '../../vite-config';
|
|
|
8
9
|
import { getComponentOutputPath } from '../component';
|
|
9
10
|
import { TransformComponentTokensResult } from '../types';
|
|
10
11
|
|
|
12
|
+
const resolveRenderer = (data: TransformComponentTokensResult): RendererKind | undefined => {
|
|
13
|
+
const templatePath = data.entries?.template || '';
|
|
14
|
+
return (
|
|
15
|
+
data.renderer ||
|
|
16
|
+
(data.entries?.story || templatePath.match(/\.stories\.(jsx|tsx|js|ts)$/) ? 'csf' : undefined) ||
|
|
17
|
+
(data.entries?.component || (templatePath.includes('.tsx') && !templatePath.match(/\.stories\.(jsx|tsx|js|ts)$/))
|
|
18
|
+
? 'react'
|
|
19
|
+
: undefined) ||
|
|
20
|
+
(templatePath.includes('.hbs') ? 'handlebars' : undefined)
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Builds previews for components using Vite and Handlebars.
|
|
13
26
|
*
|
|
@@ -29,13 +42,13 @@ export const buildPreviews = async (
|
|
|
29
42
|
): Promise<TransformComponentTokensResult> => {
|
|
30
43
|
if (!data.entries?.template) return data;
|
|
31
44
|
|
|
45
|
+
const resolvedRenderer = resolveRenderer(data);
|
|
46
|
+
|
|
32
47
|
const plugins = [
|
|
33
48
|
...(viteBaseConfig.plugins || []),
|
|
34
|
-
...(
|
|
35
|
-
...(
|
|
36
|
-
...(
|
|
37
|
-
? [react(), ssrRenderPlugin(data, components, handoff)]
|
|
38
|
-
: []),
|
|
49
|
+
...(resolvedRenderer === 'handlebars' ? [handlebarsPreviewsPlugin(data, components, handoff)] : []),
|
|
50
|
+
...(resolvedRenderer === 'csf' ? [csfRenderPlugin(data, components, handoff)] : []),
|
|
51
|
+
...(resolvedRenderer === 'react' ? [react(), ssrRenderPlugin(data, components, handoff)] : []),
|
|
39
52
|
];
|
|
40
53
|
|
|
41
54
|
// Store the current NODE_ENV value before vite build
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OptionalPreviewRender } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns previews exposed in docs/API; synthetic pattern previews (`__pattern_*`) are omitted.
|
|
5
|
+
*/
|
|
6
|
+
export const getDocumentedPreviews = (
|
|
7
|
+
previews?: { [key: string]: OptionalPreviewRender }
|
|
8
|
+
): { [key: string]: OptionalPreviewRender } => {
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(previews || {}).filter(([key]) => !key.startsWith('__pattern_'))
|
|
11
|
+
);
|
|
12
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Handoff from '../../../index';
|
|
4
|
+
import { PatternListObject, PatternObject } from '../types';
|
|
5
|
+
|
|
6
|
+
export const getPatternAPIPath = (handoff: Handoff): string => {
|
|
7
|
+
const patternPath = path.resolve(handoff.workingPath, 'public/api/pattern');
|
|
8
|
+
if (!fs.existsSync(patternPath)) {
|
|
9
|
+
fs.mkdirSync(patternPath, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
return patternPath;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const writePatternApi = async (
|
|
15
|
+
id: string,
|
|
16
|
+
pattern: PatternObject,
|
|
17
|
+
handoff: Handoff
|
|
18
|
+
): Promise<void> => {
|
|
19
|
+
const outputDir = getPatternAPIPath(handoff);
|
|
20
|
+
const outputFilePath = path.resolve(outputDir, `${id}.json`);
|
|
21
|
+
await fs.writeFile(outputFilePath, JSON.stringify(pattern, null, 2));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const writePatternHtml = async (
|
|
25
|
+
id: string,
|
|
26
|
+
html: string,
|
|
27
|
+
handoff: Handoff
|
|
28
|
+
): Promise<void> => {
|
|
29
|
+
const outputDir = getPatternAPIPath(handoff);
|
|
30
|
+
const outputFilePath = path.resolve(outputDir, `${id}.html`);
|
|
31
|
+
await fs.writeFile(outputFilePath, html);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const writePatternSummaryApi = async (
|
|
35
|
+
handoff: Handoff,
|
|
36
|
+
patterns: PatternListObject[]
|
|
37
|
+
): Promise<void> => {
|
|
38
|
+
const apiPath = path.resolve(handoff.workingPath, 'public/api');
|
|
39
|
+
if (!fs.existsSync(apiPath)) {
|
|
40
|
+
fs.mkdirSync(apiPath, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
patterns.sort((a, b) => a.title.localeCompare(b.title));
|
|
43
|
+
await fs.writeFile(
|
|
44
|
+
path.resolve(apiPath, 'patterns.json'),
|
|
45
|
+
JSON.stringify(patterns, null, 2)
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const syncPatternArtifacts = async (
|
|
50
|
+
handoff: Handoff,
|
|
51
|
+
validIds: Iterable<string>
|
|
52
|
+
): Promise<void> => {
|
|
53
|
+
const validIdSet = new Set(validIds);
|
|
54
|
+
const patternPath = getPatternAPIPath(handoff);
|
|
55
|
+
const entries = await fs.readdir(patternPath);
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const parsed = path.parse(entry);
|
|
59
|
+
const isPatternArtifact = parsed.ext === '.json' || parsed.ext === '.html';
|
|
60
|
+
if (!isPatternArtifact) continue;
|
|
61
|
+
|
|
62
|
+
if (!validIdSet.has(parsed.name)) {
|
|
63
|
+
await fs.remove(path.resolve(patternPath, entry));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Handoff from '../../../index';
|
|
4
|
+
import { Logger } from '../../../utils/logger';
|
|
5
|
+
import { PatternListObject } from '../types';
|
|
6
|
+
import { syncPatternArtifacts, writePatternApi, writePatternHtml, writePatternSummaryApi } from './api';
|
|
7
|
+
import { composePatternHtml } from './html';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Process all pattern declarations and compose their HTML from pre-built
|
|
11
|
+
* component preview files. This function must run AFTER buildComponents
|
|
12
|
+
* so that all component preview HTML files already exist on disk.
|
|
13
|
+
*/
|
|
14
|
+
export async function processPatterns(handoff: Handoff): Promise<PatternListObject[]> {
|
|
15
|
+
const runtimePatterns = handoff.runtimeConfig?.entries?.patterns ?? {};
|
|
16
|
+
const patternIds = Object.keys(runtimePatterns);
|
|
17
|
+
const result: PatternListObject[] = [];
|
|
18
|
+
|
|
19
|
+
if (patternIds.length === 0) {
|
|
20
|
+
await syncPatternArtifacts(handoff, []);
|
|
21
|
+
await writePatternSummaryApi(handoff, result);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Logger.info(`Building ${patternIds.length} pattern(s)...`);
|
|
26
|
+
|
|
27
|
+
const componentOutputDir = path.resolve(handoff.workingPath, 'public/api/component');
|
|
28
|
+
const basePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
|
|
29
|
+
|
|
30
|
+
for (const patternId of patternIds) {
|
|
31
|
+
const pattern = runtimePatterns[patternId];
|
|
32
|
+
const fragments: { componentId: string; html: string }[] = [];
|
|
33
|
+
let hasErrors = false;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < pattern.components.length; i++) {
|
|
36
|
+
const ref = pattern.components[i];
|
|
37
|
+
|
|
38
|
+
if (ref.resolved === false && !ref.resolvedPreview) {
|
|
39
|
+
hasErrors = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const previewKey = ref.resolvedPreview || ref.preview || 'default';
|
|
44
|
+
const htmlFileName = `${ref.id}-${previewKey}.html`;
|
|
45
|
+
const htmlFilePath = path.resolve(componentOutputDir, htmlFileName);
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(htmlFilePath)) {
|
|
48
|
+
const error =
|
|
49
|
+
`Pattern "${patternId}" component[${i}] ("${ref.id}") preview file not found: ${htmlFileName}. ` +
|
|
50
|
+
`Ensure the component has a preview named "${previewKey}".`;
|
|
51
|
+
Logger.warn(error);
|
|
52
|
+
ref.resolved = false;
|
|
53
|
+
hasErrors = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ref.resolved = true;
|
|
58
|
+
const html = await fs.readFile(htmlFilePath, 'utf8');
|
|
59
|
+
fragments.push({ componentId: ref.id, html });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (fragments.length === 0) {
|
|
63
|
+
Logger.warn(`Pattern "${patternId}" produced no fragments. Skipping.`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (hasErrors) {
|
|
68
|
+
Logger.warn(`Pattern "${patternId}" has missing components but will be composed from available fragments.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const composedHtml = composePatternHtml(patternId, pattern.title, fragments, basePath);
|
|
72
|
+
const patternUrl = `${patternId}.html`;
|
|
73
|
+
|
|
74
|
+
const patternData: PatternListObject = {
|
|
75
|
+
...pattern,
|
|
76
|
+
url: patternUrl,
|
|
77
|
+
path: `${basePath}/api/pattern/${patternId}.json`,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await writePatternHtml(patternId, composedHtml, handoff);
|
|
81
|
+
await writePatternApi(patternId, patternData, handoff);
|
|
82
|
+
|
|
83
|
+
result.push(patternData);
|
|
84
|
+
Logger.debug(`Built pattern: ${patternId} (${fragments.length} component(s))`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await syncPatternArtifacts(handoff, result.map((pattern) => pattern.id));
|
|
88
|
+
await writePatternSummaryApi(handoff, result);
|
|
89
|
+
Logger.info(`Finished building ${result.length} pattern(s).`);
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default processPatterns;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import escape from 'lodash/escape';
|
|
2
|
+
import { parse } from 'node-html-parser';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composes multiple component preview HTML documents into a single-page HTML.
|
|
6
|
+
*
|
|
7
|
+
* Each component preview may be a React SSR document (with hydration scripts
|
|
8
|
+
* and a `#root` / `#__APP_PROPS__` pair) or a static Handlebars document.
|
|
9
|
+
*
|
|
10
|
+
* This function namespaces the element ids per-fragment so multiple React
|
|
11
|
+
* components can coexist and hydrate independently on the same page.
|
|
12
|
+
*
|
|
13
|
+
* Each hydration bundle gets its own `<script type="module">` tag for full
|
|
14
|
+
* module-level isolation. This is critical because esbuild bundles React
|
|
15
|
+
* into each script using module-scoped `var` declarations, and `hydrateRoot`
|
|
16
|
+
* is asynchronous — sharing a single module scope would cause later bundles
|
|
17
|
+
* to overwrite earlier React instances before hydration completes.
|
|
18
|
+
*/
|
|
19
|
+
export const composePatternHtml = (
|
|
20
|
+
_patternId: string,
|
|
21
|
+
patternTitle: string,
|
|
22
|
+
fragments: { componentId: string; html: string }[],
|
|
23
|
+
basePath: string
|
|
24
|
+
): string => {
|
|
25
|
+
const cssHrefs = new Set<string>();
|
|
26
|
+
const bodyParts: string[] = [];
|
|
27
|
+
const scriptTags: string[] = [];
|
|
28
|
+
|
|
29
|
+
cssHrefs.add(`${basePath}/api/component/main.css`);
|
|
30
|
+
cssHrefs.add(`${basePath}/assets/css/preview.css`);
|
|
31
|
+
|
|
32
|
+
const seenComponents = new Set<string>();
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
35
|
+
const { componentId, html } = fragments[i];
|
|
36
|
+
|
|
37
|
+
if (!seenComponents.has(componentId)) {
|
|
38
|
+
seenComponents.add(componentId);
|
|
39
|
+
cssHrefs.add(`${basePath}/api/component/${componentId}.css`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const doc = parse(html);
|
|
43
|
+
const suffix = `_p${i}`;
|
|
44
|
+
const namespacedRootId = `root${suffix}`;
|
|
45
|
+
const namespacedPropsId = `__APP_PROPS__${suffix}`;
|
|
46
|
+
|
|
47
|
+
// -- Extract props JSON (React SSR) ------------------------------------
|
|
48
|
+
const propsScript = doc.querySelector('#__APP_PROPS__');
|
|
49
|
+
let propsContent = '{}';
|
|
50
|
+
if (propsScript) {
|
|
51
|
+
propsContent = propsScript.textContent || '{}';
|
|
52
|
+
propsScript.remove();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// -- Extract and rewrite hydration scripts (React SSR) -----------------
|
|
56
|
+
// Each script becomes its own <script type="module"> for full isolation.
|
|
57
|
+
const moduleScripts = doc.querySelectorAll('script[type="module"]');
|
|
58
|
+
for (const script of moduleScripts) {
|
|
59
|
+
let code = script.textContent || '';
|
|
60
|
+
if (code.trim()) {
|
|
61
|
+
code = code
|
|
62
|
+
.replace(/__APP_PROPS__/g, namespacedPropsId)
|
|
63
|
+
.replace(/getElementById\(\s*["']root["']\s*\)/g, `getElementById("${namespacedRootId}")`);
|
|
64
|
+
scriptTags.push(` <script type="module">\n${code}\n </script>`);
|
|
65
|
+
}
|
|
66
|
+
script.remove();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// -- Namespace the root element (React SSR) ----------------------------
|
|
70
|
+
const rootEl = doc.querySelector('#root');
|
|
71
|
+
if (rootEl) {
|
|
72
|
+
rootEl.setAttribute('id', namespacedRootId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -- Extract body content ----------------------------------------------
|
|
76
|
+
const bodyEl = doc.querySelector('body');
|
|
77
|
+
const bodyContent = bodyEl
|
|
78
|
+
? bodyEl.innerHTML.trim()
|
|
79
|
+
: rootEl
|
|
80
|
+
? rootEl.outerHTML
|
|
81
|
+
: doc.innerHTML || '';
|
|
82
|
+
|
|
83
|
+
bodyParts.push(
|
|
84
|
+
` <div class="handoff-pattern-block" data-component="${componentId}" data-fragment="${i}">\n` +
|
|
85
|
+
` <script id="${namespacedPropsId}" type="application/json">${propsContent}</script>\n` +
|
|
86
|
+
` ${bodyContent}\n` +
|
|
87
|
+
` </div>`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const linkTags = Array.from(cssHrefs)
|
|
92
|
+
.map((href) => ` <link rel="stylesheet" href="${href}" />`)
|
|
93
|
+
.join('\n');
|
|
94
|
+
|
|
95
|
+
return `<!DOCTYPE html>
|
|
96
|
+
<html lang="en">
|
|
97
|
+
<head>
|
|
98
|
+
<meta charset="UTF-8" />
|
|
99
|
+
${linkTags}
|
|
100
|
+
<title>${escape(patternTitle)}</title>
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
${bodyParts.join('\n')}
|
|
104
|
+
${scriptTags.join('\n')}
|
|
105
|
+
</body>
|
|
106
|
+
</html>`;
|
|
107
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GeneratedDocs } from 'handoff-docgen';
|
|
2
2
|
import { Card } from '../../app/components/Component/Cards';
|
|
3
|
+
import type { RendererKind } from '../../declarations/types';
|
|
3
4
|
import { ValidationResult } from '../../types/preview';
|
|
4
5
|
import { Filter } from '../../utils/filter';
|
|
5
6
|
import { SlotMetadata } from './component';
|
|
@@ -108,15 +109,31 @@ export type ComponentObject = {
|
|
|
108
109
|
scss?: string;
|
|
109
110
|
/** Optional path to component template file (if available) */
|
|
110
111
|
template?: string;
|
|
112
|
+
/** Optional path to React component source for preview rendering */
|
|
113
|
+
component?: string;
|
|
114
|
+
/** Optional path to CSF story file */
|
|
115
|
+
story?: string;
|
|
116
|
+
/** Optional path to schema file (if available) */
|
|
117
|
+
schema?: string;
|
|
118
|
+
/** Optional path to templates directory */
|
|
119
|
+
templates?: string;
|
|
111
120
|
};
|
|
121
|
+
/** Optional explicit renderer id */
|
|
122
|
+
renderer?: RendererKind;
|
|
112
123
|
/** Schema describing the expected properties (props/slots) for the component */
|
|
113
124
|
properties: { [key: string]: SlotMetadata };
|
|
114
125
|
/** Mapping of preview variations with values and titles for each (used to render sample states) */
|
|
115
126
|
previews: { [key: string]: OptionalPreviewRender };
|
|
127
|
+
/** Internal previews generated only for pattern composition, not public docs */
|
|
128
|
+
internalPatternPreviews?: { [key: string]: OptionalPreviewRender };
|
|
116
129
|
/** Optional array of high-level categories for search/filtering */
|
|
117
130
|
categories?: string[];
|
|
118
131
|
/** Optional array of tags for search/filtering (e.g. "primary", "interactive") */
|
|
119
132
|
tags?: string[];
|
|
133
|
+
/** Optional markdown strings describing recommended usage patterns */
|
|
134
|
+
should_do?: string[];
|
|
135
|
+
/** Optional markdown strings describing anti-patterns to avoid */
|
|
136
|
+
should_not_do?: string[];
|
|
120
137
|
/** Optional source Figma file or node URL for the component */
|
|
121
138
|
figma?: string;
|
|
122
139
|
/** Optional canonical Figma component name or ID (used for matching back to design tokens) */
|
|
@@ -125,7 +142,7 @@ export type ComponentObject = {
|
|
|
125
142
|
page?: ComponentPageDefinition;
|
|
126
143
|
/** Optional additional options for preview and transformer behaviors */
|
|
127
144
|
options?: ComponentOptions;
|
|
128
|
-
}
|
|
145
|
+
};
|
|
129
146
|
|
|
130
147
|
export type ComponentListObject = {
|
|
131
148
|
id: string;
|
|
@@ -151,17 +168,23 @@ export type TransformComponentTokensResult = {
|
|
|
151
168
|
css?: string;
|
|
152
169
|
sass?: string;
|
|
153
170
|
sharedStyles?: string;
|
|
171
|
+
usage?: string;
|
|
154
172
|
title?: string;
|
|
155
173
|
description?: string;
|
|
156
174
|
previews?: { [key: string]: OptionalPreviewRender };
|
|
175
|
+
internalPatternPreviews?: { [key: string]: OptionalPreviewRender };
|
|
157
176
|
properties?: { [key: string]: SlotMetadata };
|
|
158
177
|
variant?: Record<string, string>;
|
|
159
178
|
entries?: {
|
|
160
179
|
js?: string;
|
|
161
180
|
scss?: string;
|
|
162
181
|
template?: string;
|
|
182
|
+
component?: string;
|
|
183
|
+
story?: string;
|
|
163
184
|
schema?: string;
|
|
185
|
+
templates?: string;
|
|
164
186
|
};
|
|
187
|
+
renderer?: RendererKind;
|
|
165
188
|
options?: {
|
|
166
189
|
preview?: {
|
|
167
190
|
groupBy?: string;
|
|
@@ -176,8 +199,42 @@ export type OptionalPreviewRender = {
|
|
|
176
199
|
title: string;
|
|
177
200
|
values: { [key: string]: string | string[] | any };
|
|
178
201
|
url: string;
|
|
202
|
+
usage?: string;
|
|
203
|
+
/** Optional source preview/story key used to derive this preview */
|
|
204
|
+
sourcePreview?: string;
|
|
179
205
|
};
|
|
180
206
|
|
|
181
207
|
export interface TransformedPreviewComponents {
|
|
182
208
|
[key: string]: TransformComponentTokensResult[];
|
|
183
209
|
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Pattern types
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
export type PatternComponentEntry = {
|
|
216
|
+
/** Component id reference */
|
|
217
|
+
id: string;
|
|
218
|
+
/** Named preview variant to use (optional if args provided) */
|
|
219
|
+
preview?: string;
|
|
220
|
+
/** Arg overrides (optional if preview provided) */
|
|
221
|
+
args?: Record<string, any>;
|
|
222
|
+
/** Preview key resolved during injectPatternPreviews (set at build time) */
|
|
223
|
+
resolvedPreview?: string;
|
|
224
|
+
/** Whether the pattern entry resolved to a usable component preview */
|
|
225
|
+
resolved?: boolean;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export type PatternObject = {
|
|
229
|
+
title: string;
|
|
230
|
+
description?: string;
|
|
231
|
+
group?: string;
|
|
232
|
+
tags?: string[];
|
|
233
|
+
components: PatternComponentEntry[];
|
|
234
|
+
url?: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export type PatternListObject = {
|
|
238
|
+
id: string;
|
|
239
|
+
path: string;
|
|
240
|
+
} & PatternObject;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { startCase } from 'lodash';
|
|
2
|
+
import { OptionalPreviewRender } from '../preview/types';
|
|
3
|
+
|
|
4
|
+
export type StoryObject = {
|
|
5
|
+
name?: string;
|
|
6
|
+
args?: Record<string, any>;
|
|
7
|
+
argTypes?: Record<string, any>;
|
|
8
|
+
render?: (args: Record<string, any>) => any;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type CsfMeta = {
|
|
12
|
+
title?: string;
|
|
13
|
+
component?: any;
|
|
14
|
+
args?: Record<string, any>;
|
|
15
|
+
argTypes?: Record<string, any>;
|
|
16
|
+
render?: (args: Record<string, any>) => any;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getCsfStoryEntries(moduleExports: Record<string, any>): Array<[string, StoryObject]> {
|
|
20
|
+
return Object.entries(moduleExports).filter(([key, value]) => {
|
|
21
|
+
if (key === 'default' || key === '__esModule') return false;
|
|
22
|
+
if (typeof value === 'function') return true;
|
|
23
|
+
if (value && typeof value === 'object') return true;
|
|
24
|
+
return false;
|
|
25
|
+
}) as Array<[string, StoryObject]>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createCsfStoryPreviews(
|
|
29
|
+
moduleExports: Record<string, any>
|
|
30
|
+
): Record<string, OptionalPreviewRender> {
|
|
31
|
+
const meta = (moduleExports.default || {}) as CsfMeta;
|
|
32
|
+
const stories = getCsfStoryEntries(moduleExports);
|
|
33
|
+
const previews: Record<string, OptionalPreviewRender> = {};
|
|
34
|
+
|
|
35
|
+
for (const [storyKey, storyValue] of stories) {
|
|
36
|
+
const storyObj = (typeof storyValue === 'function' ? {} : storyValue) as StoryObject;
|
|
37
|
+
previews[storyKey] = {
|
|
38
|
+
title: storyObj.name || startCase(storyKey),
|
|
39
|
+
values: {
|
|
40
|
+
...(meta.args || {}),
|
|
41
|
+
...(storyObj.args || {}),
|
|
42
|
+
},
|
|
43
|
+
url: '',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return previews;
|
|
48
|
+
}
|