handoff-app 1.2.0-0 → 1.2.0-2
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/dist/app-builder/build.js +24 -10
- package/dist/app-builder/config-diff/index.d.ts +1 -1
- package/dist/app-builder/config-diff/registry.d.ts +2 -2
- package/dist/app-builder/config-diff/registry.js +2 -2
- package/dist/app-builder/config-diff/strategies/pattern.js +14 -1
- package/dist/app-builder/config-diff/types.d.ts +15 -1
- package/dist/app-builder/paths.js +18 -1
- package/dist/app-builder/watchers.js +35 -3
- package/dist/app-builder/websocket.js +10 -4
- package/dist/cli.js +1 -1
- package/dist/config/normalizers/pattern.d.ts +0 -1
- package/dist/config/normalizers/pattern.js +0 -3
- package/dist/config/runtime.js +10 -4
- package/dist/pipeline/patterns.d.ts +3 -1
- package/dist/pipeline/patterns.js +2 -2
- package/dist/transformers/plugins/csf-render.js +2 -1
- package/dist/transformers/plugins/ssr-render.js +2 -1
- package/dist/transformers/preview/component/api.d.ts +2 -13
- package/dist/transformers/preview/component/api.js +7 -46
- package/dist/transformers/preview/component/artifacts.d.ts +4 -0
- package/dist/transformers/preview/component/artifacts.js +126 -0
- package/dist/transformers/preview/component/builder.js +62 -6
- package/dist/transformers/preview/component/css.d.ts +2 -0
- package/dist/transformers/preview/component/css.js +25 -9
- package/dist/transformers/preview/component/javascript.d.ts +1 -1
- package/dist/transformers/preview/component/javascript.js +45 -19
- package/dist/transformers/preview/component/summary.d.ts +6 -0
- package/dist/transformers/preview/component/summary.js +55 -0
- package/dist/transformers/preview/component.js +3 -25
- package/dist/transformers/preview/pattern/api.d.ts +4 -0
- package/dist/transformers/preview/pattern/api.js +19 -1
- package/dist/transformers/preview/pattern/builder.d.ts +9 -1
- package/dist/transformers/preview/pattern/builder.js +123 -42
- package/dist/transformers/utils/vite-logger.js +4 -4
- package/dist/transformers/vite-config.js +2 -0
- package/dist/utils/duration.d.ts +2 -0
- package/dist/utils/duration.js +6 -0
- package/dist/utils/logger.d.ts +42 -5
- package/dist/utils/logger.js +135 -15
- package/package.json +1 -1
- package/src/app/pages/system/pattern/[pattern]/index.tsx +178 -55
- package/src/app-builder/build.ts +26 -10
- package/src/app-builder/config-diff/index.ts +1 -1
- package/src/app-builder/config-diff/registry.ts +3 -3
- package/src/app-builder/config-diff/strategies/pattern.ts +17 -2
- package/src/app-builder/config-diff/types.ts +16 -1
- package/src/app-builder/paths.ts +22 -1
- package/src/app-builder/watchers.ts +15 -3
- package/src/app-builder/websocket.ts +16 -5
- package/src/cli.ts +1 -1
- package/src/config/normalizers/pattern.ts +0 -7
- package/src/config/runtime.ts +11 -4
- package/src/pipeline/patterns.ts +5 -3
- package/src/transformers/plugins/csf-render.ts +2 -1
- package/src/transformers/plugins/ssr-render.ts +2 -1
- package/src/transformers/preview/component/api.ts +2 -50
- package/src/transformers/preview/component/artifacts.ts +131 -0
- package/src/transformers/preview/component/builder.ts +72 -6
- package/src/transformers/preview/component/css.ts +26 -10
- package/src/transformers/preview/component/javascript.ts +50 -23
- package/src/transformers/preview/component/summary.ts +54 -0
- package/src/transformers/preview/component.ts +2 -1
- package/src/transformers/preview/pattern/api.ts +17 -0
- package/src/transformers/preview/pattern/builder.ts +156 -58
- package/src/transformers/utils/vite-logger.ts +4 -4
- package/src/transformers/vite-config.ts +2 -0
- package/src/utils/duration.ts +3 -0
- package/src/utils/logger.ts +143 -16
package/src/pipeline/patterns.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import Handoff from '..';
|
|
2
|
-
import { processPatterns } from '../transformers/preview/pattern/builder';
|
|
2
|
+
import { ProcessPatternsOptions, processPatterns } from '../transformers/preview/pattern/builder';
|
|
3
|
+
|
|
4
|
+
export type { ProcessPatternsOptions };
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Builds pattern previews by composing pre-built component preview HTML.
|
|
6
8
|
* Must be called AFTER buildComponents.
|
|
7
9
|
*/
|
|
8
|
-
export const buildPatterns = async (handoff: Handoff) => {
|
|
9
|
-
await processPatterns(handoff);
|
|
10
|
+
export const buildPatterns = async (handoff: Handoff, options?: ProcessPatternsOptions) => {
|
|
11
|
+
await processPatterns(handoff, options);
|
|
10
12
|
};
|
|
@@ -10,6 +10,7 @@ import Handoff from '../..';
|
|
|
10
10
|
import { Logger } from '../../utils/logger';
|
|
11
11
|
import { generateDocsArtifact, getPropertiesForComponentFromDocs } from '../docgen';
|
|
12
12
|
import { SlotMetadata, SlotType } from '../preview/component';
|
|
13
|
+
import { MAIN_COMPONENT_CSS_FILE } from '../preview/component/css';
|
|
13
14
|
import { TransformComponentTokensResult } from '../preview/types';
|
|
14
15
|
import { formatHtml, trimPreview } from '../utils/html';
|
|
15
16
|
import { buildAndEvaluateModule } from '../utils/module';
|
|
@@ -270,7 +271,7 @@ function createHtmlDocument(componentId: string, previewTitle: string, renderedH
|
|
|
270
271
|
<html lang="en">
|
|
271
272
|
<head>
|
|
272
273
|
<meta charset="UTF-8" />
|
|
273
|
-
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component
|
|
274
|
+
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component/${MAIN_COMPONENT_CSS_FILE}" />
|
|
274
275
|
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component/${componentId}.css" />
|
|
275
276
|
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/assets/css/preview.css" />
|
|
276
277
|
<title>${previewTitle}</title>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getPropertiesFromGeneratedDocs,
|
|
15
15
|
} from '../docgen';
|
|
16
16
|
import { SlotMetadata } from '../preview/component';
|
|
17
|
+
import { MAIN_COMPONENT_CSS_FILE } from '../preview/component/css';
|
|
17
18
|
import { TransformComponentTokensResult } from '../preview/types';
|
|
18
19
|
import { DEFAULT_CLIENT_BUILD_CONFIG, createReactResolvePlugin } from '../utils/build';
|
|
19
20
|
import { formatHtml, trimPreview } from '../utils/html';
|
|
@@ -124,7 +125,7 @@ function generateHtmlDocument(componentId: string, previewTitle: string, rendere
|
|
|
124
125
|
<html lang="en">
|
|
125
126
|
<head>
|
|
126
127
|
<meta charset="UTF-8" />
|
|
127
|
-
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component
|
|
128
|
+
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component/${MAIN_COMPONENT_CSS_FILE}" />
|
|
128
129
|
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/api/component/${componentId}.css" />
|
|
129
130
|
<link rel="stylesheet" href="${process.env.HANDOFF_APP_BASE_PATH ?? ''}/assets/css/preview.css" />
|
|
130
131
|
<script id="${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}" type="application/json">${JSON.stringify(props)}</script>
|
|
@@ -4,7 +4,7 @@ import Handoff from '../../../index';
|
|
|
4
4
|
import { ComponentListObject, TransformComponentTokensResult } from '../types';
|
|
5
5
|
import { getDocumentedPreviews } from './previews';
|
|
6
6
|
|
|
7
|
-
const sanitizeComponentApiData = (
|
|
7
|
+
export const sanitizeComponentApiData = (
|
|
8
8
|
component: TransformComponentTokensResult
|
|
9
9
|
): TransformComponentTokensResult => {
|
|
10
10
|
if (!component) return component;
|
|
@@ -64,16 +64,6 @@ export const getAPIPath = (handoff: Handoff) => {
|
|
|
64
64
|
return apiPath;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* Build the preview API from the component data
|
|
69
|
-
* @param handoff
|
|
70
|
-
* @param componentData
|
|
71
|
-
*/
|
|
72
|
-
const writeComponentSummaryAPI = async (handoff: Handoff, componentData: ComponentListObject[]) => {
|
|
73
|
-
componentData.sort((a, b) => a.title.localeCompare(b.title));
|
|
74
|
-
await fs.writeFile(path.resolve(getAPIPath(handoff), 'components.json'), JSON.stringify(componentData, null, 2));
|
|
75
|
-
};
|
|
76
|
-
|
|
77
67
|
export const writeComponentApi = async (
|
|
78
68
|
id: string,
|
|
79
69
|
component: TransformComponentTokensResult,
|
|
@@ -110,43 +100,6 @@ export const writeComponentApi = async (
|
|
|
110
100
|
await fs.writeFile(outputFilePath, JSON.stringify(sanitizedComponent, null, 2));
|
|
111
101
|
};
|
|
112
102
|
|
|
113
|
-
/**
|
|
114
|
-
* Update the main component summary API with the new component data
|
|
115
|
-
* @param handoff
|
|
116
|
-
* @param componentData
|
|
117
|
-
*/
|
|
118
|
-
export const updateComponentSummaryApi = async (handoff: Handoff, componentData: ComponentListObject[], isFullRebuild: boolean = false) => {
|
|
119
|
-
if (isFullRebuild) {
|
|
120
|
-
// Full rebuild: replace the entire file
|
|
121
|
-
await writeComponentSummaryAPI(handoff, componentData);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Partial update: merge with existing data
|
|
126
|
-
const apiPath = path.resolve(getAPIPath(handoff), 'components.json');
|
|
127
|
-
let existingData: ComponentListObject[] = [];
|
|
128
|
-
|
|
129
|
-
if (fs.existsSync(apiPath)) {
|
|
130
|
-
try {
|
|
131
|
-
const existing = await fs.readFile(apiPath, 'utf8');
|
|
132
|
-
existingData = (JSON.parse(existing) as ComponentListObject[]).map((component) => ({
|
|
133
|
-
...component,
|
|
134
|
-
previews: getDocumentedPreviews(component.previews),
|
|
135
|
-
}));
|
|
136
|
-
} catch {
|
|
137
|
-
// Corrupt or missing JSON — treat as empty
|
|
138
|
-
existingData = [];
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Replace existing entries with same ID
|
|
143
|
-
const incomingIds = new Set(componentData.map((c) => c.id));
|
|
144
|
-
const merged = [...componentData, ...existingData.filter((c) => !incomingIds.has(c.id))];
|
|
145
|
-
|
|
146
|
-
// Always write the file (even if merged is empty)
|
|
147
|
-
await writeComponentSummaryAPI(handoff, merged);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
103
|
/**
|
|
151
104
|
* Read the component API data
|
|
152
105
|
* @param handoff
|
|
@@ -198,5 +151,4 @@ export const readComponentMetadataApi = async (handoff: Handoff, id: string): Pr
|
|
|
198
151
|
path: `/api/component/${id}.json`,
|
|
199
152
|
};
|
|
200
153
|
};
|
|
201
|
-
|
|
202
|
-
export default writeComponentSummaryAPI;
|
|
154
|
+
export default writeComponentApi;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Handoff from '../../../index';
|
|
4
|
+
import { TransformComponentTokensResult } from '../types';
|
|
5
|
+
import { getDocumentedPreviews } from './previews';
|
|
6
|
+
import { getAPIPath, sanitizeComponentApiData } from './api';
|
|
7
|
+
import { MAIN_COMPONENT_CSS_FILE, SHARED_COMPONENT_CSS_FILE } from './css';
|
|
8
|
+
import { MAIN_COMPONENT_JS_FILE } from './javascript';
|
|
9
|
+
|
|
10
|
+
const COMPONENT_JS_RESERVED_FILES = new Set([MAIN_COMPONENT_JS_FILE]);
|
|
11
|
+
const COMPONENT_CSS_RESERVED_FILES = new Set([MAIN_COMPONENT_CSS_FILE, SHARED_COMPONENT_CSS_FILE]);
|
|
12
|
+
|
|
13
|
+
export const getComponentApiPath = (handoff: Handoff) => path.resolve(getAPIPath(handoff), 'component');
|
|
14
|
+
|
|
15
|
+
const getComponentPreviewKeys = async (handoff: Handoff, componentId: string): Promise<Set<string>> => {
|
|
16
|
+
const runtimeComponent = handoff.runtimeConfig?.entries?.components?.[componentId];
|
|
17
|
+
const previewKeys = new Set<string>();
|
|
18
|
+
|
|
19
|
+
const outputFilePath = path.resolve(getComponentApiPath(handoff), `${componentId}.json`);
|
|
20
|
+
if (fs.existsSync(outputFilePath)) {
|
|
21
|
+
try {
|
|
22
|
+
const existingJson = await fs.readFile(outputFilePath, 'utf8');
|
|
23
|
+
if (existingJson) {
|
|
24
|
+
const existingData = sanitizeComponentApiData(JSON.parse(existingJson) as TransformComponentTokensResult);
|
|
25
|
+
for (const previewKey of Object.keys(existingData?.previews || {})) {
|
|
26
|
+
previewKeys.add(previewKey);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Fall back to runtime config below.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const previewKey of Object.keys(getDocumentedPreviews(runtimeComponent?.previews) || {})) {
|
|
35
|
+
previewKeys.add(previewKey);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const previewKey of Object.keys(runtimeComponent?.internalPatternPreviews || {})) {
|
|
39
|
+
previewKeys.add(previewKey);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return previewKeys;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getArtifactComponentId = (fileName: string, componentIds: Iterable<string>): string | null => {
|
|
46
|
+
let match: string | null = null;
|
|
47
|
+
|
|
48
|
+
for (const componentId of componentIds) {
|
|
49
|
+
if (!fileName.startsWith(`${componentId}-`)) continue;
|
|
50
|
+
if (!match || componentId.length > match.length) {
|
|
51
|
+
match = componentId;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return match;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const removeComponentApi = async (handoff: Handoff, id: string): Promise<void> => {
|
|
59
|
+
const outputDirPath = getComponentApiPath(handoff);
|
|
60
|
+
const outputFilePath = path.resolve(outputDirPath, `${id}.json`);
|
|
61
|
+
|
|
62
|
+
if (await fs.pathExists(outputFilePath)) {
|
|
63
|
+
await fs.remove(outputFilePath);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const syncComponentArtifacts = async (handoff: Handoff): Promise<void> => {
|
|
68
|
+
const componentPath = getComponentApiPath(handoff);
|
|
69
|
+
await fs.ensureDir(componentPath);
|
|
70
|
+
|
|
71
|
+
const runtimeComponents = handoff.runtimeConfig?.entries?.components ?? {};
|
|
72
|
+
const runtimeIds = Object.keys(runtimeComponents);
|
|
73
|
+
const entries = await fs.readdir(componentPath);
|
|
74
|
+
|
|
75
|
+
const discoveredArtifactIds = new Set<string>(runtimeIds);
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const parsed = path.parse(entry);
|
|
78
|
+
if (parsed.ext === '.json' || parsed.ext === '.js' || parsed.ext === '.css') {
|
|
79
|
+
if (!COMPONENT_JS_RESERVED_FILES.has(entry) && !COMPONENT_CSS_RESERVED_FILES.has(entry)) {
|
|
80
|
+
discoveredArtifactIds.add(parsed.name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const validFiles = new Set<string>();
|
|
86
|
+
for (const componentId of runtimeIds) {
|
|
87
|
+
validFiles.add(`${componentId}.json`);
|
|
88
|
+
|
|
89
|
+
if (await fs.pathExists(path.resolve(componentPath, `${componentId}.js`))) {
|
|
90
|
+
validFiles.add(`${componentId}.js`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (await fs.pathExists(path.resolve(componentPath, `${componentId}.css`))) {
|
|
94
|
+
validFiles.add(`${componentId}.css`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const previewKeys = await getComponentPreviewKeys(handoff, componentId);
|
|
98
|
+
for (const previewKey of previewKeys) {
|
|
99
|
+
validFiles.add(`${componentId}-${previewKey}.html`);
|
|
100
|
+
validFiles.add(`${componentId}-${previewKey}-inspect.html`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (COMPONENT_JS_RESERVED_FILES.has(entry) || COMPONENT_CSS_RESERVED_FILES.has(entry)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parsed = path.parse(entry);
|
|
110
|
+
|
|
111
|
+
if (parsed.ext === '.json' || parsed.ext === '.js' || parsed.ext === '.css') {
|
|
112
|
+
if (!validFiles.has(entry)) {
|
|
113
|
+
await fs.remove(path.resolve(componentPath, entry));
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (parsed.ext !== '.html') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ownerId = getArtifactComponentId(entry, discoveredArtifactIds);
|
|
123
|
+
if (!ownerId) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!validFiles.has(entry)) {
|
|
128
|
+
await fs.remove(path.resolve(componentPath, entry));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
@@ -14,11 +14,14 @@ import {
|
|
|
14
14
|
updateComponentCacheEntry,
|
|
15
15
|
} from '../../../cache';
|
|
16
16
|
import Handoff from '../../../index';
|
|
17
|
+
import { formatDurationMs } from '../../../utils/duration';
|
|
17
18
|
import { Logger } from '../../../utils/logger';
|
|
18
19
|
import { ensureIds } from '../../utils/schema';
|
|
19
20
|
import { ComponentListObject, ComponentType, OptionalPreviewRender, TransformComponentTokensResult } from '../types';
|
|
20
|
-
import { readComponentApi, readComponentMetadataApi,
|
|
21
|
+
import { readComponentApi, readComponentMetadataApi, writeComponentApi } from './api';
|
|
22
|
+
import { removeComponentApi, syncComponentArtifacts } from './artifacts';
|
|
21
23
|
import { getDocumentedPreviews } from './previews';
|
|
24
|
+
import { removeComponentFromSummaryApi, updateComponentSummaryApi } from './summary';
|
|
22
25
|
import buildComponentCss from './css';
|
|
23
26
|
import buildPreviews from './html';
|
|
24
27
|
import buildComponentJs from './javascript';
|
|
@@ -83,6 +86,44 @@ const getBuildPreviews = (data: TransformComponentTokensResult): { [key: string]
|
|
|
83
86
|
};
|
|
84
87
|
};
|
|
85
88
|
|
|
89
|
+
/** Vite-backed segments in execution order; extend here if new steps are added. */
|
|
90
|
+
const VITE_BUILD_SEGMENTS: {
|
|
91
|
+
key: keyof Pick<ComponentBuildPlan, 'js' | 'css' | 'previews'>;
|
|
92
|
+
label: string;
|
|
93
|
+
}[] = [
|
|
94
|
+
{ key: 'js', label: 'script' },
|
|
95
|
+
{ key: 'css', label: 'styles' },
|
|
96
|
+
{ key: 'previews', label: 'previews' },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const isFullViteComponentBuild = (plan: ComponentBuildPlan): boolean => plan.js && plan.css && plan.previews;
|
|
100
|
+
|
|
101
|
+
const getActiveViteBuildLabels = (plan: ComponentBuildPlan): string[] =>
|
|
102
|
+
VITE_BUILD_SEGMENTS.filter(({ key }) => plan[key]).map(({ label }) => label);
|
|
103
|
+
|
|
104
|
+
/** e.g. "a", "a and b", "a, b, and c" — common CLI copy style. */
|
|
105
|
+
const formatEnglishList = (items: string[]): string => {
|
|
106
|
+
if (items.length === 0) return '';
|
|
107
|
+
if (items.length === 1) return items[0];
|
|
108
|
+
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
|
109
|
+
return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const viteBuildStartMessage = (componentId: string, plan: ComponentBuildPlan): string => {
|
|
113
|
+
if (isFullViteComponentBuild(plan)) {
|
|
114
|
+
return `Building component "${componentId}"…`;
|
|
115
|
+
}
|
|
116
|
+
return `Building ${formatEnglishList(getActiveViteBuildLabels(plan))} for component "${componentId}"…`;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const viteBuildFinishMessage = (componentId: string, plan: ComponentBuildPlan, elapsedMs: number): string => {
|
|
120
|
+
const duration = formatDurationMs(elapsedMs);
|
|
121
|
+
if (isFullViteComponentBuild(plan)) {
|
|
122
|
+
return `Finished building component "${componentId}" in ${duration}`;
|
|
123
|
+
}
|
|
124
|
+
return `Finished building ${formatEnglishList(getActiveViteBuildLabels(plan))} for component "${componentId}" in ${duration}`;
|
|
125
|
+
};
|
|
126
|
+
|
|
86
127
|
/**
|
|
87
128
|
* Returns a normalized build plan describing which component segments need rebuilding.
|
|
88
129
|
*
|
|
@@ -140,6 +181,13 @@ export async function processComponents(
|
|
|
140
181
|
const runtimeComponents = handoff.runtimeConfig?.entries?.components ?? {};
|
|
141
182
|
const allComponentIds = Object.keys(runtimeComponents);
|
|
142
183
|
|
|
184
|
+
if (id && !runtimeComponents[id]) {
|
|
185
|
+
await removeComponentApi(handoff, id);
|
|
186
|
+
await removeComponentFromSummaryApi(handoff, id);
|
|
187
|
+
await syncComponentArtifacts(handoff);
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
143
191
|
// Determine which components need building based on cache (when enabled)
|
|
144
192
|
let componentsToBuild: Set<string>;
|
|
145
193
|
let cache: BuildCache | null = null;
|
|
@@ -193,11 +241,16 @@ export async function processComponents(
|
|
|
193
241
|
pruneRemovedComponents(cache, allComponentIds);
|
|
194
242
|
}
|
|
195
243
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
244
|
+
const total = allComponentIds.length;
|
|
245
|
+
const toBuildCount = componentsToBuild.size;
|
|
246
|
+
const skippedCount = total - toBuildCount;
|
|
247
|
+
|
|
248
|
+
if (total > 0 && toBuildCount === 0) {
|
|
249
|
+
Logger.info(`All ${total} components unchanged; skipping build`);
|
|
250
|
+
} else if (skippedCount > 0 && toBuildCount > 0) {
|
|
251
|
+
Logger.info(`Building ${toBuildCount} of ${total} components (${skippedCount} unchanged)`);
|
|
252
|
+
} else if (toBuildCount > 0) {
|
|
253
|
+
Logger.info(`Building all ${toBuildCount} components`);
|
|
201
254
|
} else {
|
|
202
255
|
Logger.info('All components up to date, nothing to build');
|
|
203
256
|
}
|
|
@@ -301,6 +354,13 @@ export async function processComponents(
|
|
|
301
354
|
// Components should always have at least one preview variation.
|
|
302
355
|
ensureDefaultPreview(data);
|
|
303
356
|
|
|
357
|
+
const runsViteBuildSteps = buildPlan.js || buildPlan.css || buildPlan.previews;
|
|
358
|
+
let viteBuildStartedAtMs = 0;
|
|
359
|
+
if (runsViteBuildSteps) {
|
|
360
|
+
viteBuildStartedAtMs = Date.now();
|
|
361
|
+
Logger.info(viteBuildStartMessage(runtimeComponentId, buildPlan));
|
|
362
|
+
}
|
|
363
|
+
|
|
304
364
|
// Build JS if needed (new build, validation missing, or explicit segment request).
|
|
305
365
|
if (buildPlan.js) {
|
|
306
366
|
data = await buildComponentJs(data, handoff);
|
|
@@ -321,6 +381,11 @@ export async function processComponents(
|
|
|
321
381
|
);
|
|
322
382
|
}
|
|
323
383
|
|
|
384
|
+
if (runsViteBuildSteps) {
|
|
385
|
+
const elapsedMs = Date.now() - viteBuildStartedAtMs;
|
|
386
|
+
Logger.info(viteBuildFinishMessage(runtimeComponentId, buildPlan, elapsedMs));
|
|
387
|
+
}
|
|
388
|
+
|
|
324
389
|
/**
|
|
325
390
|
* Run validation if explicitly requested and a hook is configured.
|
|
326
391
|
* This allows custom logic to assess the validity of the generated component data.
|
|
@@ -369,6 +434,7 @@ export async function processComponents(
|
|
|
369
434
|
// Always merge and write summary file, even if no components processed
|
|
370
435
|
const isFullRebuild = !id;
|
|
371
436
|
await updateComponentSummaryApi(handoff, result, isFullRebuild);
|
|
437
|
+
await syncComponentArtifacts(handoff);
|
|
372
438
|
|
|
373
439
|
return result;
|
|
374
440
|
}
|
|
@@ -3,12 +3,16 @@ import path from 'path';
|
|
|
3
3
|
import { InlineConfig, build as viteBuild } from 'vite';
|
|
4
4
|
import { initRuntimeConfig } from '../../../config';
|
|
5
5
|
import Handoff from '../../../index';
|
|
6
|
+
import { formatDurationMs } from '../../../utils/duration';
|
|
6
7
|
import { Logger } from '../../../utils/logger';
|
|
7
8
|
import viteBaseConfig from '../../vite-config';
|
|
8
9
|
import { getComponentOutputPath } from '../component';
|
|
9
10
|
import { TransformComponentTokensResult } from '../types';
|
|
10
11
|
const { pathToFileURL } = require('url');
|
|
11
12
|
|
|
13
|
+
export const MAIN_COMPONENT_CSS_FILE = 'main.css';
|
|
14
|
+
export const SHARED_COMPONENT_CSS_FILE = 'shared.css';
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Builds a CSS bundle using Vite
|
|
14
18
|
*
|
|
@@ -98,8 +102,16 @@ const buildCssBundle = async ({
|
|
|
98
102
|
const buildComponentCss = async (data: TransformComponentTokensResult, handoff: Handoff) => {
|
|
99
103
|
const id = data.id;
|
|
100
104
|
Logger.debug(`buildComponentCss`, id);
|
|
105
|
+
const outputPath = getComponentOutputPath(handoff);
|
|
106
|
+
const builtCssPath = path.resolve(outputPath, `${id}.css`);
|
|
101
107
|
const entry = data.entries?.scss;
|
|
108
|
+
|
|
102
109
|
if (!entry) {
|
|
110
|
+
// Keep generated output aligned with the current component declaration.
|
|
111
|
+
await fs.remove(builtCssPath);
|
|
112
|
+
delete data['css'];
|
|
113
|
+
delete data['sass'];
|
|
114
|
+
delete data['sharedStyles'];
|
|
103
115
|
return data;
|
|
104
116
|
}
|
|
105
117
|
|
|
@@ -109,10 +121,12 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
109
121
|
return data;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
const outputPath = getComponentOutputPath(handoff);
|
|
113
|
-
|
|
114
124
|
try {
|
|
115
125
|
if (extension === '.scss' || extension === '.css') {
|
|
126
|
+
// Remove the previous artifact before rebuilding so "no emitted CSS"
|
|
127
|
+
// doesn't silently preserve stale output from an earlier build.
|
|
128
|
+
await fs.remove(builtCssPath);
|
|
129
|
+
|
|
116
130
|
// Read the original source
|
|
117
131
|
const sourceContent = await fs.readFile(entry, 'utf8');
|
|
118
132
|
if (extension === '.scss') {
|
|
@@ -139,7 +153,6 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
139
153
|
});
|
|
140
154
|
|
|
141
155
|
// Read the built CSS
|
|
142
|
-
const builtCssPath = path.resolve(outputPath, `${id}.css`);
|
|
143
156
|
if (fs.existsSync(builtCssPath)) {
|
|
144
157
|
const builtCss = await fs.readFile(builtCssPath, 'utf8');
|
|
145
158
|
data['css'] = builtCss;
|
|
@@ -150,8 +163,11 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
150
163
|
if (splitCSS && splitCSS.length > 1) {
|
|
151
164
|
data['css'] = splitCSS[1];
|
|
152
165
|
data['sharedStyles'] = splitCSS[0];
|
|
153
|
-
await fs.writeFile(path.resolve(outputPath,
|
|
166
|
+
await fs.writeFile(path.resolve(outputPath, SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
|
|
154
167
|
}
|
|
168
|
+
} else {
|
|
169
|
+
delete data['css'];
|
|
170
|
+
delete data['sharedStyles'];
|
|
155
171
|
}
|
|
156
172
|
}
|
|
157
173
|
} catch (e) {
|
|
@@ -174,10 +190,9 @@ export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
|
174
190
|
const entryPath = stat.isDirectory() ? path.resolve(runtimeConfig.entries.scss, 'main.scss') : runtimeConfig.entries.scss;
|
|
175
191
|
|
|
176
192
|
if (entryPath === runtimeConfig.entries.scss || fs.existsSync(entryPath)) {
|
|
177
|
-
Logger.
|
|
178
|
-
|
|
193
|
+
Logger.info(`Building styles for global entry (${MAIN_COMPONENT_CSS_FILE})…`);
|
|
194
|
+
const startedAt = Date.now();
|
|
179
195
|
try {
|
|
180
|
-
// Setup SASS load paths
|
|
181
196
|
const loadPaths = [
|
|
182
197
|
path.resolve(handoff.workingPath),
|
|
183
198
|
path.resolve(handoff.workingPath, handoff.exportsDirectory, handoff.getProjectId()),
|
|
@@ -191,12 +206,13 @@ export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
|
191
206
|
await buildCssBundle({
|
|
192
207
|
entry: entryPath,
|
|
193
208
|
outputPath,
|
|
194
|
-
outputFilename:
|
|
209
|
+
outputFilename: MAIN_COMPONENT_CSS_FILE,
|
|
195
210
|
loadPaths,
|
|
196
211
|
handoff,
|
|
197
212
|
});
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
Logger.info(`Finished building styles for global entry (${MAIN_COMPONENT_CSS_FILE}) in ${formatDurationMs(Date.now() - startedAt)}`);
|
|
214
|
+
} catch {
|
|
215
|
+
// buildCssBundle already logs failure for the entry path
|
|
200
216
|
}
|
|
201
217
|
}
|
|
202
218
|
}
|
|
@@ -3,11 +3,14 @@ import path from 'path';
|
|
|
3
3
|
import { InlineConfig, build as viteBuild } from 'vite';
|
|
4
4
|
import { initRuntimeConfig } from '../../../config';
|
|
5
5
|
import Handoff from '../../../index';
|
|
6
|
+
import { formatDurationMs } from '../../../utils/duration';
|
|
6
7
|
import { Logger } from '../../../utils/logger';
|
|
7
8
|
import viteBaseConfig from '../../vite-config';
|
|
8
9
|
import { getComponentOutputPath } from '../component';
|
|
9
10
|
import { TransformComponentTokensResult } from '../types';
|
|
10
11
|
|
|
12
|
+
export const MAIN_COMPONENT_JS_FILE = 'main.js';
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Builds a JavaScript bundle using Vite
|
|
13
16
|
*
|
|
@@ -54,8 +57,6 @@ const buildJsBundle = async (
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
await viteBuild(viteConfig);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
Logger.error(`Failed to build JS for "${outputFilename}":`, e);
|
|
59
60
|
} finally {
|
|
60
61
|
// Restore the original NODE_ENV value after vite build completes
|
|
61
62
|
// This prevents interference with Next.js app building/running processes
|
|
@@ -79,25 +80,45 @@ const buildJsBundle = async (
|
|
|
79
80
|
*/
|
|
80
81
|
export const buildComponentJs = async (data: TransformComponentTokensResult, handoff: Handoff): Promise<TransformComponentTokensResult> => {
|
|
81
82
|
const id = data.id;
|
|
83
|
+
const outputPath = getComponentOutputPath(handoff);
|
|
84
|
+
const builtJsPath = path.resolve(outputPath, `${id}.js`);
|
|
82
85
|
const entry = data.entries?.js;
|
|
83
|
-
if (!entry) return data;
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
if (!entry) {
|
|
88
|
+
// Keep generated output aligned with the current component declaration.
|
|
89
|
+
await fs.remove(builtJsPath);
|
|
90
|
+
delete data.js;
|
|
91
|
+
delete data['jsCompiled'];
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
86
94
|
|
|
87
95
|
try {
|
|
96
|
+
// Remove the previous artifact before rebuilding so a no-output build
|
|
97
|
+
// cannot accidentally preserve stale compiled JS.
|
|
98
|
+
await fs.remove(builtJsPath);
|
|
99
|
+
|
|
88
100
|
const js = await fs.readFile(path.resolve(entry), 'utf8');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
try {
|
|
102
|
+
await buildJsBundle(
|
|
103
|
+
{
|
|
104
|
+
entry,
|
|
105
|
+
outputPath,
|
|
106
|
+
outputFilename: `${id}.js`,
|
|
107
|
+
},
|
|
108
|
+
handoff
|
|
109
|
+
);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
Logger.error(`Failed to bundle JS for component "${id}" (${id}.js):`, e);
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
97
114
|
|
|
98
115
|
data.js = js;
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
if (await fs.pathExists(builtJsPath)) {
|
|
117
|
+
const compiled = await fs.readFile(builtJsPath, 'utf8');
|
|
118
|
+
data['jsCompiled'] = compiled;
|
|
119
|
+
} else {
|
|
120
|
+
delete data['jsCompiled'];
|
|
121
|
+
}
|
|
101
122
|
} catch (e) {
|
|
102
123
|
Logger.error(`JS build failed for component "${id}":`, e);
|
|
103
124
|
}
|
|
@@ -113,21 +134,27 @@ export const buildComponentJs = async (data: TransformComponentTokensResult, han
|
|
|
113
134
|
*
|
|
114
135
|
* @param handoff - The Handoff configuration object containing build settings
|
|
115
136
|
* @returns A Promise that resolves when the build process is complete
|
|
116
|
-
* @throws May throw an error if the build process fails
|
|
117
137
|
*/
|
|
118
138
|
export const buildMainJS = async (handoff: Handoff): Promise<void> => {
|
|
119
139
|
const outputPath = getComponentOutputPath(handoff);
|
|
120
140
|
const runtimeConfig = initRuntimeConfig(handoff)[0];
|
|
121
141
|
|
|
122
142
|
if (runtimeConfig && runtimeConfig.entries.js && fs.existsSync(path.resolve(runtimeConfig.entries.js))) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
Logger.info(`Building script for global entry (${MAIN_COMPONENT_JS_FILE})…`);
|
|
144
|
+
const startedAt = Date.now();
|
|
145
|
+
try {
|
|
146
|
+
await buildJsBundle(
|
|
147
|
+
{
|
|
148
|
+
entry: runtimeConfig.entries.js,
|
|
149
|
+
outputPath,
|
|
150
|
+
outputFilename: MAIN_COMPONENT_JS_FILE,
|
|
151
|
+
},
|
|
152
|
+
handoff
|
|
153
|
+
);
|
|
154
|
+
Logger.info(`Finished building script for global entry (${MAIN_COMPONENT_JS_FILE}) in ${formatDurationMs(Date.now() - startedAt)}`);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
Logger.error(`Failed to build global script (${MAIN_COMPONENT_JS_FILE}):`, e);
|
|
157
|
+
}
|
|
131
158
|
}
|
|
132
159
|
};
|
|
133
160
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Handoff from '../../../index';
|
|
4
|
+
import { ComponentListObject } from '../types';
|
|
5
|
+
import { getDocumentedPreviews } from './previews';
|
|
6
|
+
import { getAPIPath } from './api';
|
|
7
|
+
|
|
8
|
+
const getComponentSummaryPath = (handoff: Handoff): string => path.resolve(getAPIPath(handoff), 'components.json');
|
|
9
|
+
|
|
10
|
+
const writeComponentSummaryAPI = async (handoff: Handoff, componentData: ComponentListObject[]) => {
|
|
11
|
+
componentData.sort((a, b) => a.title.localeCompare(b.title));
|
|
12
|
+
await fs.writeFile(getComponentSummaryPath(handoff), JSON.stringify(componentData, null, 2));
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const readExistingComponentSummaryApi = async (handoff: Handoff): Promise<ComponentListObject[]> => {
|
|
16
|
+
const apiPath = getComponentSummaryPath(handoff);
|
|
17
|
+
if (!fs.existsSync(apiPath)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const existing = await fs.readFile(apiPath, 'utf8');
|
|
23
|
+
return (JSON.parse(existing) as ComponentListObject[]).map((component) => ({
|
|
24
|
+
...component,
|
|
25
|
+
previews: getDocumentedPreviews(component.previews),
|
|
26
|
+
}));
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const updateComponentSummaryApi = async (
|
|
33
|
+
handoff: Handoff,
|
|
34
|
+
componentData: ComponentListObject[],
|
|
35
|
+
isFullRebuild: boolean = false
|
|
36
|
+
) => {
|
|
37
|
+
if (isFullRebuild) {
|
|
38
|
+
await writeComponentSummaryAPI(handoff, componentData);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const existingData = await readExistingComponentSummaryApi(handoff);
|
|
43
|
+
const incomingIds = new Set(componentData.map((c) => c.id));
|
|
44
|
+
const merged = [...componentData, ...existingData.filter((c) => !incomingIds.has(c.id))];
|
|
45
|
+
await writeComponentSummaryAPI(handoff, merged);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const removeComponentFromSummaryApi = async (handoff: Handoff, id: string): Promise<void> => {
|
|
49
|
+
const existingData = await readExistingComponentSummaryApi(handoff);
|
|
50
|
+
const filtered = existingData.filter((component) => component.id !== id);
|
|
51
|
+
await writeComponentSummaryAPI(handoff, filtered);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default writeComponentSummaryAPI;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { DocAnnotation, TypeNode } from 'handoff-docgen';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import Handoff from '../../index';
|
|
4
|
-
import
|
|
4
|
+
import { getAPIPath } from './component/api';
|
|
5
|
+
import writeComponentSummaryAPI from './component/summary';
|
|
5
6
|
import processComponents from './component/builder';
|
|
6
7
|
import { buildMainCss } from './component/css';
|
|
7
8
|
import { buildMainJS } from './component/javascript';
|