handoff-app 1.2.0-1 → 1.2.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/Changelog.md +65 -0
- package/dist/app-builder/paths.js +18 -1
- package/dist/cli.js +1 -1
- 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 +10 -1
- package/dist/transformers/preview/component/css.d.ts +2 -0
- package/dist/transformers/preview/component/css.js +21 -7
- package/dist/transformers/preview/component/javascript.d.ts +1 -0
- package/dist/transformers/preview/component/javascript.js +24 -9
- 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/package.json +1 -1
- package/src/app-builder/paths.ts +22 -1
- package/src/cli.ts +1 -1
- 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 +11 -1
- package/src/transformers/preview/component/css.ts +22 -7
- package/src/transformers/preview/component/javascript.ts +25 -8
- package/src/transformers/preview/component/summary.ts +54 -0
- package/src/transformers/preview/component.ts +2 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.removeComponentFromSummaryApi = exports.updateComponentSummaryApi = void 0;
|
|
16
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const previews_1 = require("./previews");
|
|
19
|
+
const api_1 = require("./api");
|
|
20
|
+
const getComponentSummaryPath = (handoff) => path_1.default.resolve((0, api_1.getAPIPath)(handoff), 'components.json');
|
|
21
|
+
const writeComponentSummaryAPI = (handoff, componentData) => __awaiter(void 0, void 0, void 0, function* () {
|
|
22
|
+
componentData.sort((a, b) => a.title.localeCompare(b.title));
|
|
23
|
+
yield fs_extra_1.default.writeFile(getComponentSummaryPath(handoff), JSON.stringify(componentData, null, 2));
|
|
24
|
+
});
|
|
25
|
+
const readExistingComponentSummaryApi = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
const apiPath = getComponentSummaryPath(handoff);
|
|
27
|
+
if (!fs_extra_1.default.existsSync(apiPath)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const existing = yield fs_extra_1.default.readFile(apiPath, 'utf8');
|
|
32
|
+
return JSON.parse(existing).map((component) => (Object.assign(Object.assign({}, component), { previews: (0, previews_1.getDocumentedPreviews)(component.previews) })));
|
|
33
|
+
}
|
|
34
|
+
catch (_a) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const updateComponentSummaryApi = (handoff_1, componentData_1, ...args_1) => __awaiter(void 0, [handoff_1, componentData_1, ...args_1], void 0, function* (handoff, componentData, isFullRebuild = false) {
|
|
39
|
+
if (isFullRebuild) {
|
|
40
|
+
yield writeComponentSummaryAPI(handoff, componentData);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const existingData = yield readExistingComponentSummaryApi(handoff);
|
|
44
|
+
const incomingIds = new Set(componentData.map((c) => c.id));
|
|
45
|
+
const merged = [...componentData, ...existingData.filter((c) => !incomingIds.has(c.id))];
|
|
46
|
+
yield writeComponentSummaryAPI(handoff, merged);
|
|
47
|
+
});
|
|
48
|
+
exports.updateComponentSummaryApi = updateComponentSummaryApi;
|
|
49
|
+
const removeComponentFromSummaryApi = (handoff, id) => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
const existingData = yield readExistingComponentSummaryApi(handoff);
|
|
51
|
+
const filtered = existingData.filter((component) => component.id !== id);
|
|
52
|
+
yield writeComponentSummaryAPI(handoff, filtered);
|
|
53
|
+
});
|
|
54
|
+
exports.removeComponentFromSummaryApi = removeComponentFromSummaryApi;
|
|
55
|
+
exports.default = writeComponentSummaryAPI;
|
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
2
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
3
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
4
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -38,7 +15,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
38
15
|
exports.getComponentOutputPath = exports.SlotType = void 0;
|
|
39
16
|
exports.componentTransformer = componentTransformer;
|
|
40
17
|
const path_1 = __importDefault(require("path"));
|
|
41
|
-
const api_1 =
|
|
18
|
+
const api_1 = require("./component/api");
|
|
19
|
+
const summary_1 = __importDefault(require("./component/summary"));
|
|
42
20
|
const builder_1 = __importDefault(require("./component/builder"));
|
|
43
21
|
const css_1 = require("./component/css");
|
|
44
22
|
const javascript_1 = require("./component/javascript");
|
|
@@ -66,7 +44,7 @@ exports.getComponentOutputPath = getComponentOutputPath;
|
|
|
66
44
|
function componentTransformer(handoff) {
|
|
67
45
|
return __awaiter(this, void 0, void 0, function* () {
|
|
68
46
|
const componentData = yield (0, builder_1.default)(handoff);
|
|
69
|
-
yield (0,
|
|
47
|
+
yield (0, summary_1.default)(handoff, componentData);
|
|
70
48
|
yield (0, javascript_1.buildMainJS)(handoff);
|
|
71
49
|
yield (0, css_1.buildMainCss)(handoff);
|
|
72
50
|
});
|
package/package.json
CHANGED
package/src/app-builder/paths.ts
CHANGED
|
@@ -30,6 +30,17 @@ export const getAppPath = (handoff: Handoff): string => {
|
|
|
30
30
|
return path.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
const mirrorDirectory = async (sourcePath: string, destinationPath: string): Promise<void> => {
|
|
34
|
+
if (!(await fs.pathExists(sourcePath))) {
|
|
35
|
+
await fs.remove(destinationPath);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await fs.remove(destinationPath);
|
|
40
|
+
await fs.ensureDir(path.dirname(destinationPath));
|
|
41
|
+
await fs.copy(sourcePath, destinationPath, { overwrite: true });
|
|
42
|
+
};
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Copy the public dir from the working dir to the module dir.
|
|
35
46
|
*/
|
|
@@ -37,8 +48,18 @@ export const syncPublicFiles = async (handoff: Handoff): Promise<void> => {
|
|
|
37
48
|
const appPath = getAppPath(handoff);
|
|
38
49
|
const workingPublicPath = getWorkingPublicPath(handoff);
|
|
39
50
|
if (workingPublicPath) {
|
|
40
|
-
|
|
51
|
+
const destinationPublicPath = path.resolve(appPath, 'public');
|
|
52
|
+
const sourceApiPath = path.resolve(workingPublicPath, 'api');
|
|
53
|
+
const destinationApiPath = path.resolve(destinationPublicPath, 'api');
|
|
54
|
+
|
|
55
|
+
await fs.copy(workingPublicPath, destinationPublicPath, {
|
|
41
56
|
overwrite: true,
|
|
57
|
+
filter: (file) => {
|
|
58
|
+
const relativePath = path.relative(workingPublicPath, file);
|
|
59
|
+
return relativePath !== 'api' && !relativePath.startsWith(`api${path.sep}`);
|
|
60
|
+
},
|
|
42
61
|
});
|
|
62
|
+
|
|
63
|
+
await mirrorDirectory(sourceApiPath, destinationApiPath);
|
|
43
64
|
}
|
|
44
65
|
};
|
package/src/cli.ts
CHANGED
|
@@ -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
|
+
};
|
|
@@ -18,8 +18,10 @@ import { formatDurationMs } from '../../../utils/duration';
|
|
|
18
18
|
import { Logger } from '../../../utils/logger';
|
|
19
19
|
import { ensureIds } from '../../utils/schema';
|
|
20
20
|
import { ComponentListObject, ComponentType, OptionalPreviewRender, TransformComponentTokensResult } from '../types';
|
|
21
|
-
import { readComponentApi, readComponentMetadataApi,
|
|
21
|
+
import { readComponentApi, readComponentMetadataApi, writeComponentApi } from './api';
|
|
22
|
+
import { removeComponentApi, syncComponentArtifacts } from './artifacts';
|
|
22
23
|
import { getDocumentedPreviews } from './previews';
|
|
24
|
+
import { removeComponentFromSummaryApi, updateComponentSummaryApi } from './summary';
|
|
23
25
|
import buildComponentCss from './css';
|
|
24
26
|
import buildPreviews from './html';
|
|
25
27
|
import buildComponentJs from './javascript';
|
|
@@ -179,6 +181,13 @@ export async function processComponents(
|
|
|
179
181
|
const runtimeComponents = handoff.runtimeConfig?.entries?.components ?? {};
|
|
180
182
|
const allComponentIds = Object.keys(runtimeComponents);
|
|
181
183
|
|
|
184
|
+
if (id && !runtimeComponents[id]) {
|
|
185
|
+
await removeComponentApi(handoff, id);
|
|
186
|
+
await removeComponentFromSummaryApi(handoff, id);
|
|
187
|
+
await syncComponentArtifacts(handoff);
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
182
191
|
// Determine which components need building based on cache (when enabled)
|
|
183
192
|
let componentsToBuild: Set<string>;
|
|
184
193
|
let cache: BuildCache | null = null;
|
|
@@ -425,6 +434,7 @@ export async function processComponents(
|
|
|
425
434
|
// Always merge and write summary file, even if no components processed
|
|
426
435
|
const isFullRebuild = !id;
|
|
427
436
|
await updateComponentSummaryApi(handoff, result, isFullRebuild);
|
|
437
|
+
await syncComponentArtifacts(handoff);
|
|
428
438
|
|
|
429
439
|
return result;
|
|
430
440
|
}
|
|
@@ -10,6 +10,9 @@ import { getComponentOutputPath } from '../component';
|
|
|
10
10
|
import { TransformComponentTokensResult } from '../types';
|
|
11
11
|
const { pathToFileURL } = require('url');
|
|
12
12
|
|
|
13
|
+
export const MAIN_COMPONENT_CSS_FILE = 'main.css';
|
|
14
|
+
export const SHARED_COMPONENT_CSS_FILE = 'shared.css';
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Builds a CSS bundle using Vite
|
|
15
18
|
*
|
|
@@ -99,8 +102,16 @@ const buildCssBundle = async ({
|
|
|
99
102
|
const buildComponentCss = async (data: TransformComponentTokensResult, handoff: Handoff) => {
|
|
100
103
|
const id = data.id;
|
|
101
104
|
Logger.debug(`buildComponentCss`, id);
|
|
105
|
+
const outputPath = getComponentOutputPath(handoff);
|
|
106
|
+
const builtCssPath = path.resolve(outputPath, `${id}.css`);
|
|
102
107
|
const entry = data.entries?.scss;
|
|
108
|
+
|
|
103
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'];
|
|
104
115
|
return data;
|
|
105
116
|
}
|
|
106
117
|
|
|
@@ -110,10 +121,12 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
110
121
|
return data;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
const outputPath = getComponentOutputPath(handoff);
|
|
114
|
-
|
|
115
124
|
try {
|
|
116
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
|
+
|
|
117
130
|
// Read the original source
|
|
118
131
|
const sourceContent = await fs.readFile(entry, 'utf8');
|
|
119
132
|
if (extension === '.scss') {
|
|
@@ -140,7 +153,6 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
140
153
|
});
|
|
141
154
|
|
|
142
155
|
// Read the built CSS
|
|
143
|
-
const builtCssPath = path.resolve(outputPath, `${id}.css`);
|
|
144
156
|
if (fs.existsSync(builtCssPath)) {
|
|
145
157
|
const builtCss = await fs.readFile(builtCssPath, 'utf8');
|
|
146
158
|
data['css'] = builtCss;
|
|
@@ -151,8 +163,11 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
151
163
|
if (splitCSS && splitCSS.length > 1) {
|
|
152
164
|
data['css'] = splitCSS[1];
|
|
153
165
|
data['sharedStyles'] = splitCSS[0];
|
|
154
|
-
await fs.writeFile(path.resolve(outputPath,
|
|
166
|
+
await fs.writeFile(path.resolve(outputPath, SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
|
|
155
167
|
}
|
|
168
|
+
} else {
|
|
169
|
+
delete data['css'];
|
|
170
|
+
delete data['sharedStyles'];
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
173
|
} catch (e) {
|
|
@@ -175,7 +190,7 @@ export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
|
175
190
|
const entryPath = stat.isDirectory() ? path.resolve(runtimeConfig.entries.scss, 'main.scss') : runtimeConfig.entries.scss;
|
|
176
191
|
|
|
177
192
|
if (entryPath === runtimeConfig.entries.scss || fs.existsSync(entryPath)) {
|
|
178
|
-
Logger.info(`Building styles for global entry (
|
|
193
|
+
Logger.info(`Building styles for global entry (${MAIN_COMPONENT_CSS_FILE})…`);
|
|
179
194
|
const startedAt = Date.now();
|
|
180
195
|
try {
|
|
181
196
|
const loadPaths = [
|
|
@@ -191,11 +206,11 @@ 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
|
-
Logger.info(`Finished building styles for global entry (
|
|
213
|
+
Logger.info(`Finished building styles for global entry (${MAIN_COMPONENT_CSS_FILE}) in ${formatDurationMs(Date.now() - startedAt)}`);
|
|
199
214
|
} catch {
|
|
200
215
|
// buildCssBundle already logs failure for the entry path
|
|
201
216
|
}
|
|
@@ -9,6 +9,8 @@ import viteBaseConfig from '../../vite-config';
|
|
|
9
9
|
import { getComponentOutputPath } from '../component';
|
|
10
10
|
import { TransformComponentTokensResult } from '../types';
|
|
11
11
|
|
|
12
|
+
export const MAIN_COMPONENT_JS_FILE = 'main.js';
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Builds a JavaScript bundle using Vite
|
|
14
16
|
*
|
|
@@ -78,12 +80,23 @@ const buildJsBundle = async (
|
|
|
78
80
|
*/
|
|
79
81
|
export const buildComponentJs = async (data: TransformComponentTokensResult, handoff: Handoff): Promise<TransformComponentTokensResult> => {
|
|
80
82
|
const id = data.id;
|
|
83
|
+
const outputPath = getComponentOutputPath(handoff);
|
|
84
|
+
const builtJsPath = path.resolve(outputPath, `${id}.js`);
|
|
81
85
|
const entry = data.entries?.js;
|
|
82
|
-
if (!entry) return data;
|
|
83
86
|
|
|
84
|
-
|
|
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
|
+
}
|
|
85
94
|
|
|
86
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
|
+
|
|
87
100
|
const js = await fs.readFile(path.resolve(entry), 'utf8');
|
|
88
101
|
try {
|
|
89
102
|
await buildJsBundle(
|
|
@@ -100,8 +113,12 @@ export const buildComponentJs = async (data: TransformComponentTokensResult, han
|
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
data.js = js;
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
122
|
} catch (e) {
|
|
106
123
|
Logger.error(`JS build failed for component "${id}":`, e);
|
|
107
124
|
}
|
|
@@ -123,20 +140,20 @@ export const buildMainJS = async (handoff: Handoff): Promise<void> => {
|
|
|
123
140
|
const runtimeConfig = initRuntimeConfig(handoff)[0];
|
|
124
141
|
|
|
125
142
|
if (runtimeConfig && runtimeConfig.entries.js && fs.existsSync(path.resolve(runtimeConfig.entries.js))) {
|
|
126
|
-
Logger.info(`Building script for global entry (
|
|
143
|
+
Logger.info(`Building script for global entry (${MAIN_COMPONENT_JS_FILE})…`);
|
|
127
144
|
const startedAt = Date.now();
|
|
128
145
|
try {
|
|
129
146
|
await buildJsBundle(
|
|
130
147
|
{
|
|
131
148
|
entry: runtimeConfig.entries.js,
|
|
132
149
|
outputPath,
|
|
133
|
-
outputFilename:
|
|
150
|
+
outputFilename: MAIN_COMPONENT_JS_FILE,
|
|
134
151
|
},
|
|
135
152
|
handoff
|
|
136
153
|
);
|
|
137
|
-
Logger.info(`Finished building script for global entry (
|
|
154
|
+
Logger.info(`Finished building script for global entry (${MAIN_COMPONENT_JS_FILE}) in ${formatDurationMs(Date.now() - startedAt)}`);
|
|
138
155
|
} catch (e) {
|
|
139
|
-
Logger.error(`Failed to build global script (
|
|
156
|
+
Logger.error(`Failed to build global script (${MAIN_COMPONENT_JS_FILE}):`, e);
|
|
140
157
|
}
|
|
141
158
|
}
|
|
142
159
|
};
|
|
@@ -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';
|