handoff-app 1.2.0-1 → 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.
Files changed (28) hide show
  1. package/dist/app-builder/paths.js +18 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/transformers/plugins/csf-render.js +2 -1
  4. package/dist/transformers/plugins/ssr-render.js +2 -1
  5. package/dist/transformers/preview/component/api.d.ts +2 -13
  6. package/dist/transformers/preview/component/api.js +7 -46
  7. package/dist/transformers/preview/component/artifacts.d.ts +4 -0
  8. package/dist/transformers/preview/component/artifacts.js +126 -0
  9. package/dist/transformers/preview/component/builder.js +10 -1
  10. package/dist/transformers/preview/component/css.d.ts +2 -0
  11. package/dist/transformers/preview/component/css.js +21 -7
  12. package/dist/transformers/preview/component/javascript.d.ts +1 -0
  13. package/dist/transformers/preview/component/javascript.js +24 -9
  14. package/dist/transformers/preview/component/summary.d.ts +6 -0
  15. package/dist/transformers/preview/component/summary.js +55 -0
  16. package/dist/transformers/preview/component.js +3 -25
  17. package/package.json +1 -1
  18. package/src/app-builder/paths.ts +22 -1
  19. package/src/cli.ts +1 -1
  20. package/src/transformers/plugins/csf-render.ts +2 -1
  21. package/src/transformers/plugins/ssr-render.ts +2 -1
  22. package/src/transformers/preview/component/api.ts +2 -50
  23. package/src/transformers/preview/component/artifacts.ts +131 -0
  24. package/src/transformers/preview/component/builder.ts +11 -1
  25. package/src/transformers/preview/component/css.ts +22 -7
  26. package/src/transformers/preview/component/javascript.ts +25 -8
  27. package/src/transformers/preview/component/summary.ts +54 -0
  28. package/src/transformers/preview/component.ts +2 -1
@@ -41,6 +41,15 @@ const getAppPath = (handoff) => {
41
41
  return path_1.default.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
42
42
  };
43
43
  exports.getAppPath = getAppPath;
44
+ const mirrorDirectory = (sourcePath, destinationPath) => __awaiter(void 0, void 0, void 0, function* () {
45
+ if (!(yield fs_extra_1.default.pathExists(sourcePath))) {
46
+ yield fs_extra_1.default.remove(destinationPath);
47
+ return;
48
+ }
49
+ yield fs_extra_1.default.remove(destinationPath);
50
+ yield fs_extra_1.default.ensureDir(path_1.default.dirname(destinationPath));
51
+ yield fs_extra_1.default.copy(sourcePath, destinationPath, { overwrite: true });
52
+ });
44
53
  /**
45
54
  * Copy the public dir from the working dir to the module dir.
46
55
  */
@@ -48,9 +57,17 @@ const syncPublicFiles = (handoff) => __awaiter(void 0, void 0, void 0, function*
48
57
  const appPath = (0, exports.getAppPath)(handoff);
49
58
  const workingPublicPath = (0, exports.getWorkingPublicPath)(handoff);
50
59
  if (workingPublicPath) {
51
- yield fs_extra_1.default.copy(workingPublicPath, path_1.default.resolve(appPath, 'public'), {
60
+ const destinationPublicPath = path_1.default.resolve(appPath, 'public');
61
+ const sourceApiPath = path_1.default.resolve(workingPublicPath, 'api');
62
+ const destinationApiPath = path_1.default.resolve(destinationPublicPath, 'api');
63
+ yield fs_extra_1.default.copy(workingPublicPath, destinationPublicPath, {
52
64
  overwrite: true,
65
+ filter: (file) => {
66
+ const relativePath = path_1.default.relative(workingPublicPath, file);
67
+ return relativePath !== 'api' && !relativePath.startsWith(`api${path_1.default.sep}`);
68
+ },
53
69
  });
70
+ yield mirrorDirectory(sourceApiPath, destinationApiPath);
54
71
  }
55
72
  });
56
73
  exports.syncPublicFiles = syncPublicFiles;
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ class HandoffCliError extends Error {
19
19
  * Show the help message
20
20
  */
21
21
  const showVersion = () => {
22
- return 'Handoff App - 1.2.0-1';
22
+ return 'Handoff App - 1.2.0-2';
23
23
  };
24
24
  /**
25
25
  * Define a CLI error
@@ -22,6 +22,7 @@ const react_element_to_jsx_string_1 = __importDefault(require("react-element-to-
22
22
  const logger_1 = require("../../utils/logger");
23
23
  const docgen_1 = require("../docgen");
24
24
  const component_1 = require("../preview/component");
25
+ const css_1 = require("../preview/component/css");
25
26
  const html_1 = require("../utils/html");
26
27
  const module_1 = require("../utils/module");
27
28
  const schema_1 = require("../utils/schema");
@@ -240,7 +241,7 @@ function createHtmlDocument(componentId, previewTitle, renderedHtml) {
240
241
  <html lang="en">
241
242
  <head>
242
243
  <meta charset="UTF-8" />
243
- <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/main.css" />
244
+ <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/${css_1.MAIN_COMPONENT_CSS_FILE}" />
244
245
  <link rel="stylesheet" href="${(_b = process.env.HANDOFF_APP_BASE_PATH) !== null && _b !== void 0 ? _b : ''}/api/component/${componentId}.css" />
245
246
  <link rel="stylesheet" href="${(_c = process.env.HANDOFF_APP_BASE_PATH) !== null && _c !== void 0 ? _c : ''}/assets/css/preview.css" />
246
247
  <title>${previewTitle}</title>
@@ -21,6 +21,7 @@ const server_1 = __importDefault(require("react-dom/server"));
21
21
  const vite_1 = require("vite");
22
22
  const logger_1 = require("../../utils/logger");
23
23
  const docgen_1 = require("../docgen");
24
+ const css_1 = require("../preview/component/css");
24
25
  const build_1 = require("../utils/build");
25
26
  const html_1 = require("../utils/html");
26
27
  const module_1 = require("../utils/module");
@@ -117,7 +118,7 @@ function generateHtmlDocument(componentId, previewTitle, renderedHtml, clientJs,
117
118
  <html lang="en">
118
119
  <head>
119
120
  <meta charset="UTF-8" />
120
- <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/main.css" />
121
+ <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/${css_1.MAIN_COMPONENT_CSS_FILE}" />
121
122
  <link rel="stylesheet" href="${(_b = process.env.HANDOFF_APP_BASE_PATH) !== null && _b !== void 0 ? _b : ''}/api/component/${componentId}.css" />
122
123
  <link rel="stylesheet" href="${(_c = process.env.HANDOFF_APP_BASE_PATH) !== null && _c !== void 0 ? _c : ''}/assets/css/preview.css" />
123
124
  <script id="${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}" type="application/json">${JSON.stringify(props)}</script>
@@ -1,19 +1,8 @@
1
1
  import Handoff from '../../../index';
2
2
  import { ComponentListObject, TransformComponentTokensResult } from '../types';
3
+ export declare const sanitizeComponentApiData: (component: TransformComponentTokensResult) => TransformComponentTokensResult;
3
4
  export declare const getAPIPath: (handoff: Handoff) => string;
4
- /**
5
- * Build the preview API from the component data
6
- * @param handoff
7
- * @param componentData
8
- */
9
- declare const writeComponentSummaryAPI: (handoff: Handoff, componentData: ComponentListObject[]) => Promise<void>;
10
5
  export declare const writeComponentApi: (id: string, component: TransformComponentTokensResult, handoff: Handoff, preserveKeys?: string[]) => Promise<void>;
11
- /**
12
- * Update the main component summary API with the new component data
13
- * @param handoff
14
- * @param componentData
15
- */
16
- export declare const updateComponentSummaryApi: (handoff: Handoff, componentData: ComponentListObject[], isFullRebuild?: boolean) => Promise<void>;
17
6
  /**
18
7
  * Read the component API data
19
8
  * @param handoff
@@ -28,4 +17,4 @@ export declare const readComponentApi: (handoff: Handoff, id: string) => Promise
28
17
  * @returns The component summary or null if not found
29
18
  */
30
19
  export declare const readComponentMetadataApi: (handoff: Handoff, id: string) => Promise<ComponentListObject | null>;
31
- export default writeComponentSummaryAPI;
20
+ export default writeComponentApi;
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.readComponentMetadataApi = exports.readComponentApi = exports.updateComponentSummaryApi = exports.writeComponentApi = exports.getAPIPath = void 0;
15
+ exports.readComponentMetadataApi = exports.readComponentApi = exports.writeComponentApi = exports.getAPIPath = exports.sanitizeComponentApiData = void 0;
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const previews_1 = require("./previews");
@@ -23,6 +23,7 @@ const sanitizeComponentApiData = (component) => {
23
23
  delete sanitized.internalPatternPreviews;
24
24
  return sanitized;
25
25
  };
26
+ exports.sanitizeComponentApiData = sanitizeComponentApiData;
26
27
  /**
27
28
  * Merges values from a source object into a target object, returning a new object.
28
29
  * For each key present in either object:
@@ -62,28 +63,19 @@ const getAPIPath = (handoff) => {
62
63
  return apiPath;
63
64
  };
64
65
  exports.getAPIPath = getAPIPath;
65
- /**
66
- * Build the preview API from the component data
67
- * @param handoff
68
- * @param componentData
69
- */
70
- const writeComponentSummaryAPI = (handoff, componentData) => __awaiter(void 0, void 0, void 0, function* () {
71
- componentData.sort((a, b) => a.title.localeCompare(b.title));
72
- yield fs_extra_1.default.writeFile(path_1.default.resolve((0, exports.getAPIPath)(handoff), 'components.json'), JSON.stringify(componentData, null, 2));
73
- });
74
66
  const writeComponentApi = (id_1, component_1, handoff_1, ...args_1) => __awaiter(void 0, [id_1, component_1, handoff_1, ...args_1], void 0, function* (id, component, handoff, preserveKeys = []) {
75
67
  const outputDirPath = path_1.default.resolve((0, exports.getAPIPath)(handoff), 'component');
76
68
  const outputFilePath = path_1.default.resolve(outputDirPath, `${id}.json`);
77
- const sanitizedComponent = sanitizeComponentApiData(component);
69
+ const sanitizedComponent = (0, exports.sanitizeComponentApiData)(component);
78
70
  if (fs_extra_1.default.existsSync(outputFilePath)) {
79
71
  const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
80
72
  if (existingJson) {
81
73
  try {
82
- const existingData = sanitizeComponentApiData(JSON.parse(existingJson));
74
+ const existingData = (0, exports.sanitizeComponentApiData)(JSON.parse(existingJson));
83
75
  // Special case: always allow page to be cleared when undefined
84
76
  // This handles the case where page slices are removed
85
77
  const finalPreserveKeys = sanitizedComponent.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys;
86
- const mergedData = sanitizeComponentApiData(updateObject(existingData, sanitizedComponent, finalPreserveKeys));
78
+ const mergedData = (0, exports.sanitizeComponentApiData)(updateObject(existingData, sanitizedComponent, finalPreserveKeys));
87
79
  yield fs_extra_1.default.writeFile(outputFilePath, JSON.stringify(mergedData, null, 2));
88
80
  return;
89
81
  }
@@ -98,37 +90,6 @@ const writeComponentApi = (id_1, component_1, handoff_1, ...args_1) => __awaiter
98
90
  yield fs_extra_1.default.writeFile(outputFilePath, JSON.stringify(sanitizedComponent, null, 2));
99
91
  });
100
92
  exports.writeComponentApi = writeComponentApi;
101
- /**
102
- * Update the main component summary API with the new component data
103
- * @param handoff
104
- * @param componentData
105
- */
106
- const updateComponentSummaryApi = (handoff_1, componentData_1, ...args_1) => __awaiter(void 0, [handoff_1, componentData_1, ...args_1], void 0, function* (handoff, componentData, isFullRebuild = false) {
107
- if (isFullRebuild) {
108
- // Full rebuild: replace the entire file
109
- yield writeComponentSummaryAPI(handoff, componentData);
110
- return;
111
- }
112
- // Partial update: merge with existing data
113
- const apiPath = path_1.default.resolve((0, exports.getAPIPath)(handoff), 'components.json');
114
- let existingData = [];
115
- if (fs_extra_1.default.existsSync(apiPath)) {
116
- try {
117
- const existing = yield fs_extra_1.default.readFile(apiPath, 'utf8');
118
- existingData = JSON.parse(existing).map((component) => (Object.assign(Object.assign({}, component), { previews: (0, previews_1.getDocumentedPreviews)(component.previews) })));
119
- }
120
- catch (_a) {
121
- // Corrupt or missing JSON — treat as empty
122
- existingData = [];
123
- }
124
- }
125
- // Replace existing entries with same ID
126
- const incomingIds = new Set(componentData.map((c) => c.id));
127
- const merged = [...componentData, ...existingData.filter((c) => !incomingIds.has(c.id))];
128
- // Always write the file (even if merged is empty)
129
- yield writeComponentSummaryAPI(handoff, merged);
130
- });
131
- exports.updateComponentSummaryApi = updateComponentSummaryApi;
132
93
  /**
133
94
  * Read the component API data
134
95
  * @param handoff
@@ -141,7 +102,7 @@ const readComponentApi = (handoff, id) => __awaiter(void 0, void 0, void 0, func
141
102
  try {
142
103
  const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
143
104
  if (existingJson) {
144
- return sanitizeComponentApiData(JSON.parse(existingJson));
105
+ return (0, exports.sanitizeComponentApiData)(JSON.parse(existingJson));
145
106
  }
146
107
  }
147
108
  catch (_) {
@@ -180,4 +141,4 @@ const readComponentMetadataApi = (handoff, id) => __awaiter(void 0, void 0, void
180
141
  };
181
142
  });
182
143
  exports.readComponentMetadataApi = readComponentMetadataApi;
183
- exports.default = writeComponentSummaryAPI;
144
+ exports.default = exports.writeComponentApi;
@@ -0,0 +1,4 @@
1
+ import Handoff from '../../../index';
2
+ export declare const getComponentApiPath: (handoff: Handoff) => string;
3
+ export declare const removeComponentApi: (handoff: Handoff, id: string) => Promise<void>;
4
+ export declare const syncComponentArtifacts: (handoff: Handoff) => Promise<void>;
@@ -0,0 +1,126 @@
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.syncComponentArtifacts = exports.removeComponentApi = exports.getComponentApiPath = 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 css_1 = require("./css");
21
+ const javascript_1 = require("./javascript");
22
+ const COMPONENT_JS_RESERVED_FILES = new Set([javascript_1.MAIN_COMPONENT_JS_FILE]);
23
+ const COMPONENT_CSS_RESERVED_FILES = new Set([css_1.MAIN_COMPONENT_CSS_FILE, css_1.SHARED_COMPONENT_CSS_FILE]);
24
+ const getComponentApiPath = (handoff) => path_1.default.resolve((0, api_1.getAPIPath)(handoff), 'component');
25
+ exports.getComponentApiPath = getComponentApiPath;
26
+ const getComponentPreviewKeys = (handoff, componentId) => __awaiter(void 0, void 0, void 0, function* () {
27
+ var _a, _b, _c;
28
+ const runtimeComponent = (_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) === null || _c === void 0 ? void 0 : _c[componentId];
29
+ const previewKeys = new Set();
30
+ const outputFilePath = path_1.default.resolve((0, exports.getComponentApiPath)(handoff), `${componentId}.json`);
31
+ if (fs_extra_1.default.existsSync(outputFilePath)) {
32
+ try {
33
+ const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
34
+ if (existingJson) {
35
+ const existingData = (0, api_1.sanitizeComponentApiData)(JSON.parse(existingJson));
36
+ for (const previewKey of Object.keys((existingData === null || existingData === void 0 ? void 0 : existingData.previews) || {})) {
37
+ previewKeys.add(previewKey);
38
+ }
39
+ }
40
+ }
41
+ catch (_d) {
42
+ // Fall back to runtime config below.
43
+ }
44
+ }
45
+ for (const previewKey of Object.keys((0, previews_1.getDocumentedPreviews)(runtimeComponent === null || runtimeComponent === void 0 ? void 0 : runtimeComponent.previews) || {})) {
46
+ previewKeys.add(previewKey);
47
+ }
48
+ for (const previewKey of Object.keys((runtimeComponent === null || runtimeComponent === void 0 ? void 0 : runtimeComponent.internalPatternPreviews) || {})) {
49
+ previewKeys.add(previewKey);
50
+ }
51
+ return previewKeys;
52
+ });
53
+ const getArtifactComponentId = (fileName, componentIds) => {
54
+ let match = null;
55
+ for (const componentId of componentIds) {
56
+ if (!fileName.startsWith(`${componentId}-`))
57
+ continue;
58
+ if (!match || componentId.length > match.length) {
59
+ match = componentId;
60
+ }
61
+ }
62
+ return match;
63
+ };
64
+ const removeComponentApi = (handoff, id) => __awaiter(void 0, void 0, void 0, function* () {
65
+ const outputDirPath = (0, exports.getComponentApiPath)(handoff);
66
+ const outputFilePath = path_1.default.resolve(outputDirPath, `${id}.json`);
67
+ if (yield fs_extra_1.default.pathExists(outputFilePath)) {
68
+ yield fs_extra_1.default.remove(outputFilePath);
69
+ }
70
+ });
71
+ exports.removeComponentApi = removeComponentApi;
72
+ const syncComponentArtifacts = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
73
+ var _a, _b, _c;
74
+ const componentPath = (0, exports.getComponentApiPath)(handoff);
75
+ yield fs_extra_1.default.ensureDir(componentPath);
76
+ const runtimeComponents = (_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) !== null && _c !== void 0 ? _c : {};
77
+ const runtimeIds = Object.keys(runtimeComponents);
78
+ const entries = yield fs_extra_1.default.readdir(componentPath);
79
+ const discoveredArtifactIds = new Set(runtimeIds);
80
+ for (const entry of entries) {
81
+ const parsed = path_1.default.parse(entry);
82
+ if (parsed.ext === '.json' || parsed.ext === '.js' || parsed.ext === '.css') {
83
+ if (!COMPONENT_JS_RESERVED_FILES.has(entry) && !COMPONENT_CSS_RESERVED_FILES.has(entry)) {
84
+ discoveredArtifactIds.add(parsed.name);
85
+ }
86
+ }
87
+ }
88
+ const validFiles = new Set();
89
+ for (const componentId of runtimeIds) {
90
+ validFiles.add(`${componentId}.json`);
91
+ if (yield fs_extra_1.default.pathExists(path_1.default.resolve(componentPath, `${componentId}.js`))) {
92
+ validFiles.add(`${componentId}.js`);
93
+ }
94
+ if (yield fs_extra_1.default.pathExists(path_1.default.resolve(componentPath, `${componentId}.css`))) {
95
+ validFiles.add(`${componentId}.css`);
96
+ }
97
+ const previewKeys = yield 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
+ for (const entry of entries) {
104
+ if (COMPONENT_JS_RESERVED_FILES.has(entry) || COMPONENT_CSS_RESERVED_FILES.has(entry)) {
105
+ continue;
106
+ }
107
+ const parsed = path_1.default.parse(entry);
108
+ if (parsed.ext === '.json' || parsed.ext === '.js' || parsed.ext === '.css') {
109
+ if (!validFiles.has(entry)) {
110
+ yield fs_extra_1.default.remove(path_1.default.resolve(componentPath, entry));
111
+ }
112
+ continue;
113
+ }
114
+ if (parsed.ext !== '.html') {
115
+ continue;
116
+ }
117
+ const ownerId = getArtifactComponentId(entry, discoveredArtifactIds);
118
+ if (!ownerId) {
119
+ continue;
120
+ }
121
+ if (!validFiles.has(entry)) {
122
+ yield fs_extra_1.default.remove(path_1.default.resolve(componentPath, entry));
123
+ }
124
+ }
125
+ });
126
+ exports.syncComponentArtifacts = syncComponentArtifacts;
@@ -32,7 +32,9 @@ const logger_1 = require("../../../utils/logger");
32
32
  const schema_1 = require("../../utils/schema");
33
33
  const types_1 = require("../types");
34
34
  const api_1 = require("./api");
35
+ const artifacts_1 = require("./artifacts");
35
36
  const previews_1 = require("./previews");
37
+ const summary_1 = require("./summary");
36
38
  const css_1 = __importDefault(require("./css"));
37
39
  const html_1 = __importDefault(require("./html"));
38
40
  const javascript_1 = __importDefault(require("./javascript"));
@@ -153,6 +155,12 @@ function processComponents(handoff, id, segmentToProcess, options) {
153
155
  const components = (_a = documentationObject === null || documentationObject === void 0 ? void 0 : documentationObject.components) !== null && _a !== void 0 ? _a : {};
154
156
  const runtimeComponents = (_d = (_c = (_b = handoff.runtimeConfig) === null || _b === void 0 ? void 0 : _b.entries) === null || _c === void 0 ? void 0 : _c.components) !== null && _d !== void 0 ? _d : {};
155
157
  const allComponentIds = Object.keys(runtimeComponents);
158
+ if (id && !runtimeComponents[id]) {
159
+ yield (0, artifacts_1.removeComponentApi)(handoff, id);
160
+ yield (0, summary_1.removeComponentFromSummaryApi)(handoff, id);
161
+ yield (0, artifacts_1.syncComponentArtifacts)(handoff);
162
+ return [];
163
+ }
156
164
  // Determine which components need building based on cache (when enabled)
157
165
  let componentsToBuild;
158
166
  let cache = null;
@@ -367,7 +375,8 @@ function processComponents(handoff, id, segmentToProcess, options) {
367
375
  }
368
376
  // Always merge and write summary file, even if no components processed
369
377
  const isFullRebuild = !id;
370
- yield (0, api_1.updateComponentSummaryApi)(handoff, result, isFullRebuild);
378
+ yield (0, summary_1.updateComponentSummaryApi)(handoff, result, isFullRebuild);
379
+ yield (0, artifacts_1.syncComponentArtifacts)(handoff);
371
380
  return result;
372
381
  });
373
382
  }
@@ -1,5 +1,7 @@
1
1
  import Handoff from '../../../index';
2
2
  import { TransformComponentTokensResult } from '../types';
3
+ export declare const MAIN_COMPONENT_CSS_FILE = "main.css";
4
+ export declare const SHARED_COMPONENT_CSS_FILE = "shared.css";
3
5
  declare const buildComponentCss: (data: TransformComponentTokensResult, handoff: Handoff) => Promise<{
4
6
  id: string;
5
7
  type?: import("../types").ComponentType;
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.buildMainCss = void 0;
15
+ exports.buildMainCss = exports.SHARED_COMPONENT_CSS_FILE = exports.MAIN_COMPONENT_CSS_FILE = void 0;
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const vite_1 = require("vite");
@@ -22,6 +22,8 @@ const logger_1 = require("../../../utils/logger");
22
22
  const vite_config_1 = __importDefault(require("../../vite-config"));
23
23
  const component_1 = require("../component");
24
24
  const { pathToFileURL } = require('url');
25
+ exports.MAIN_COMPONENT_CSS_FILE = 'main.css';
26
+ exports.SHARED_COMPONENT_CSS_FILE = 'shared.css';
25
27
  /**
26
28
  * Builds a CSS bundle using Vite
27
29
  *
@@ -91,17 +93,26 @@ const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, f
91
93
  var _a, _b, _c;
92
94
  const id = data.id;
93
95
  logger_1.Logger.debug(`buildComponentCss`, id);
96
+ const outputPath = (0, component_1.getComponentOutputPath)(handoff);
97
+ const builtCssPath = path_1.default.resolve(outputPath, `${id}.css`);
94
98
  const entry = (_a = data.entries) === null || _a === void 0 ? void 0 : _a.scss;
95
99
  if (!entry) {
100
+ // Keep generated output aligned with the current component declaration.
101
+ yield fs_extra_1.default.remove(builtCssPath);
102
+ delete data['css'];
103
+ delete data['sass'];
104
+ delete data['sharedStyles'];
96
105
  return data;
97
106
  }
98
107
  const extension = path_1.default.extname(entry);
99
108
  if (!extension) {
100
109
  return data;
101
110
  }
102
- const outputPath = (0, component_1.getComponentOutputPath)(handoff);
103
111
  try {
104
112
  if (extension === '.scss' || extension === '.css') {
113
+ // Remove the previous artifact before rebuilding so "no emitted CSS"
114
+ // doesn't silently preserve stale output from an earlier build.
115
+ yield fs_extra_1.default.remove(builtCssPath);
105
116
  // Read the original source
106
117
  const sourceContent = yield fs_extra_1.default.readFile(entry, 'utf8');
107
118
  if (extension === '.scss') {
@@ -124,7 +135,6 @@ const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, f
124
135
  handoff,
125
136
  });
126
137
  // Read the built CSS
127
- const builtCssPath = path_1.default.resolve(outputPath, `${id}.css`);
128
138
  if (fs_extra_1.default.existsSync(builtCssPath)) {
129
139
  const builtCss = yield fs_extra_1.default.readFile(builtCssPath, 'utf8');
130
140
  data['css'] = builtCss;
@@ -134,9 +144,13 @@ const buildComponentCss = (data, handoff) => __awaiter(void 0, void 0, void 0, f
134
144
  if (splitCSS && splitCSS.length > 1) {
135
145
  data['css'] = splitCSS[1];
136
146
  data['sharedStyles'] = splitCSS[0];
137
- yield fs_extra_1.default.writeFile(path_1.default.resolve(outputPath, 'shared.css'), data['sharedStyles']);
147
+ yield fs_extra_1.default.writeFile(path_1.default.resolve(outputPath, exports.SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
138
148
  }
139
149
  }
150
+ else {
151
+ delete data['css'];
152
+ delete data['sharedStyles'];
153
+ }
140
154
  }
141
155
  }
142
156
  catch (e) {
@@ -156,7 +170,7 @@ const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
156
170
  const stat = yield fs_extra_1.default.stat(runtimeConfig.entries.scss);
157
171
  const entryPath = stat.isDirectory() ? path_1.default.resolve(runtimeConfig.entries.scss, 'main.scss') : runtimeConfig.entries.scss;
158
172
  if (entryPath === runtimeConfig.entries.scss || fs_extra_1.default.existsSync(entryPath)) {
159
- logger_1.Logger.info(`Building styles for global entry (main.css)…`);
173
+ logger_1.Logger.info(`Building styles for global entry (${exports.MAIN_COMPONENT_CSS_FILE})…`);
160
174
  const startedAt = Date.now();
161
175
  try {
162
176
  const loadPaths = [
@@ -170,11 +184,11 @@ const buildMainCss = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
170
184
  yield buildCssBundle({
171
185
  entry: entryPath,
172
186
  outputPath,
173
- outputFilename: 'main.css',
187
+ outputFilename: exports.MAIN_COMPONENT_CSS_FILE,
174
188
  loadPaths,
175
189
  handoff,
176
190
  });
177
- logger_1.Logger.info(`Finished building styles for global entry (main.css) in ${(0, duration_1.formatDurationMs)(Date.now() - startedAt)}`);
191
+ logger_1.Logger.info(`Finished building styles for global entry (${exports.MAIN_COMPONENT_CSS_FILE}) in ${(0, duration_1.formatDurationMs)(Date.now() - startedAt)}`);
178
192
  }
179
193
  catch (_d) {
180
194
  // buildCssBundle already logs failure for the entry path
@@ -1,5 +1,6 @@
1
1
  import Handoff from '../../../index';
2
2
  import { TransformComponentTokensResult } from '../types';
3
+ export declare const MAIN_COMPONENT_JS_FILE = "main.js";
3
4
  /**
4
5
  * Builds the JavaScript file for a single component if it exists.
5
6
  * Reads the component JavaScript file, bundles it using the buildJsBundle utility,
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.buildMainJS = exports.buildComponentJs = void 0;
15
+ exports.buildMainJS = exports.buildComponentJs = exports.MAIN_COMPONENT_JS_FILE = void 0;
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const vite_1 = require("vite");
@@ -21,6 +21,7 @@ const duration_1 = require("../../../utils/duration");
21
21
  const logger_1 = require("../../../utils/logger");
22
22
  const vite_config_1 = __importDefault(require("../../vite-config"));
23
23
  const component_1 = require("../component");
24
+ exports.MAIN_COMPONENT_JS_FILE = 'main.js';
24
25
  /**
25
26
  * Builds a JavaScript bundle using Vite
26
27
  *
@@ -75,11 +76,20 @@ const buildJsBundle = (_a, handoff_1) => __awaiter(void 0, [_a, handoff_1], void
75
76
  const buildComponentJs = (data, handoff) => __awaiter(void 0, void 0, void 0, function* () {
76
77
  var _a;
77
78
  const id = data.id;
79
+ const outputPath = (0, component_1.getComponentOutputPath)(handoff);
80
+ const builtJsPath = path_1.default.resolve(outputPath, `${id}.js`);
78
81
  const entry = (_a = data.entries) === null || _a === void 0 ? void 0 : _a.js;
79
- if (!entry)
82
+ if (!entry) {
83
+ // Keep generated output aligned with the current component declaration.
84
+ yield fs_extra_1.default.remove(builtJsPath);
85
+ delete data.js;
86
+ delete data['jsCompiled'];
80
87
  return data;
81
- const outputPath = (0, component_1.getComponentOutputPath)(handoff);
88
+ }
82
89
  try {
90
+ // Remove the previous artifact before rebuilding so a no-output build
91
+ // cannot accidentally preserve stale compiled JS.
92
+ yield fs_extra_1.default.remove(builtJsPath);
83
93
  const js = yield fs_extra_1.default.readFile(path_1.default.resolve(entry), 'utf8');
84
94
  try {
85
95
  yield buildJsBundle({
@@ -93,8 +103,13 @@ const buildComponentJs = (data, handoff) => __awaiter(void 0, void 0, void 0, fu
93
103
  return data;
94
104
  }
95
105
  data.js = js;
96
- const compiled = yield fs_extra_1.default.readFile(path_1.default.resolve(outputPath, `${id}.js`), 'utf8');
97
- data['jsCompiled'] = compiled;
106
+ if (yield fs_extra_1.default.pathExists(builtJsPath)) {
107
+ const compiled = yield fs_extra_1.default.readFile(builtJsPath, 'utf8');
108
+ data['jsCompiled'] = compiled;
109
+ }
110
+ else {
111
+ delete data['jsCompiled'];
112
+ }
98
113
  }
99
114
  catch (e) {
100
115
  logger_1.Logger.error(`JS build failed for component "${id}":`, e);
@@ -115,18 +130,18 @@ const buildMainJS = (handoff) => __awaiter(void 0, void 0, void 0, function* ()
115
130
  const outputPath = (0, component_1.getComponentOutputPath)(handoff);
116
131
  const runtimeConfig = (0, config_1.initRuntimeConfig)(handoff)[0];
117
132
  if (runtimeConfig && runtimeConfig.entries.js && fs_extra_1.default.existsSync(path_1.default.resolve(runtimeConfig.entries.js))) {
118
- logger_1.Logger.info(`Building script for global entry (main.js)…`);
133
+ logger_1.Logger.info(`Building script for global entry (${exports.MAIN_COMPONENT_JS_FILE})…`);
119
134
  const startedAt = Date.now();
120
135
  try {
121
136
  yield buildJsBundle({
122
137
  entry: runtimeConfig.entries.js,
123
138
  outputPath,
124
- outputFilename: 'main.js',
139
+ outputFilename: exports.MAIN_COMPONENT_JS_FILE,
125
140
  }, handoff);
126
- logger_1.Logger.info(`Finished building script for global entry (main.js) in ${(0, duration_1.formatDurationMs)(Date.now() - startedAt)}`);
141
+ logger_1.Logger.info(`Finished building script for global entry (${exports.MAIN_COMPONENT_JS_FILE}) in ${(0, duration_1.formatDurationMs)(Date.now() - startedAt)}`);
127
142
  }
128
143
  catch (e) {
129
- logger_1.Logger.error(`Failed to build global script (main.js):`, e);
144
+ logger_1.Logger.error(`Failed to build global script (${exports.MAIN_COMPONENT_JS_FILE}):`, e);
130
145
  }
131
146
  }
132
147
  });
@@ -0,0 +1,6 @@
1
+ import Handoff from '../../../index';
2
+ import { ComponentListObject } from '../types';
3
+ declare const writeComponentSummaryAPI: (handoff: Handoff, componentData: ComponentListObject[]) => Promise<void>;
4
+ export declare const updateComponentSummaryApi: (handoff: Handoff, componentData: ComponentListObject[], isFullRebuild?: boolean) => Promise<void>;
5
+ export declare const removeComponentFromSummaryApi: (handoff: Handoff, id: string) => Promise<void>;
6
+ export default writeComponentSummaryAPI;
@@ -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 = __importStar(require("./component/api"));
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, api_1.default)(handoff, componentData);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handoff-app",
3
- "version": "1.2.0-1",
3
+ "version": "1.2.0-2",
4
4
  "description": "Automated documentation toolchain for building client side documentation from figma",
5
5
  "author ": {
6
6
  "name": "Convertiv",
@@ -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
- await fs.copy(workingPublicPath, path.resolve(appPath, 'public'), {
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
@@ -19,7 +19,7 @@ class HandoffCliError extends Error {
19
19
  * Show the help message
20
20
  */
21
21
  const showVersion = () => {
22
- return 'Handoff App - 1.2.0-1';
22
+ return 'Handoff App - 1.2.0-2';
23
23
  };
24
24
 
25
25
  /**
@@ -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/main.css" />
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/main.css" />
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, updateComponentSummaryApi, writeComponentApi } from './api';
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, 'shared.css'), data['sharedStyles']);
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 (main.css)…`);
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: 'main.css',
209
+ outputFilename: MAIN_COMPONENT_CSS_FILE,
195
210
  loadPaths,
196
211
  handoff,
197
212
  });
198
- Logger.info(`Finished building styles for global entry (main.css) in ${formatDurationMs(Date.now() - startedAt)}`);
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
- const outputPath = getComponentOutputPath(handoff);
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
- const compiled = await fs.readFile(path.resolve(outputPath, `${id}.js`), 'utf8');
104
- data['jsCompiled'] = compiled;
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 (main.js)…`);
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: 'main.js',
150
+ outputFilename: MAIN_COMPONENT_JS_FILE,
134
151
  },
135
152
  handoff
136
153
  );
137
- Logger.info(`Finished building script for global entry (main.js) in ${formatDurationMs(Date.now() - startedAt)}`);
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 (main.js):`, e);
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 writeComponentSummaryAPI, { getAPIPath } from './component/api';
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';