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.
Files changed (29) hide show
  1. package/Changelog.md +65 -0
  2. package/dist/app-builder/paths.js +18 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/transformers/plugins/csf-render.js +2 -1
  5. package/dist/transformers/plugins/ssr-render.js +2 -1
  6. package/dist/transformers/preview/component/api.d.ts +2 -13
  7. package/dist/transformers/preview/component/api.js +7 -46
  8. package/dist/transformers/preview/component/artifacts.d.ts +4 -0
  9. package/dist/transformers/preview/component/artifacts.js +126 -0
  10. package/dist/transformers/preview/component/builder.js +10 -1
  11. package/dist/transformers/preview/component/css.d.ts +2 -0
  12. package/dist/transformers/preview/component/css.js +21 -7
  13. package/dist/transformers/preview/component/javascript.d.ts +1 -0
  14. package/dist/transformers/preview/component/javascript.js +24 -9
  15. package/dist/transformers/preview/component/summary.d.ts +6 -0
  16. package/dist/transformers/preview/component/summary.js +55 -0
  17. package/dist/transformers/preview/component.js +3 -25
  18. package/package.json +1 -1
  19. package/src/app-builder/paths.ts +22 -1
  20. package/src/cli.ts +1 -1
  21. package/src/transformers/plugins/csf-render.ts +2 -1
  22. package/src/transformers/plugins/ssr-render.ts +2 -1
  23. package/src/transformers/preview/component/api.ts +2 -50
  24. package/src/transformers/preview/component/artifacts.ts +131 -0
  25. package/src/transformers/preview/component/builder.ts +11 -1
  26. package/src/transformers/preview/component/css.ts +22 -7
  27. package/src/transformers/preview/component/javascript.ts +25 -8
  28. package/src/transformers/preview/component/summary.ts +54 -0
  29. package/src/transformers/preview/component.ts +2 -1
package/Changelog.md CHANGED
@@ -6,6 +6,71 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [1.2.0] - 2026-04-03
10
+
11
+ This release introduces support for patterns and significantly
12
+ modernizes the configuration and declaration workflow. It expands Handoff from
13
+ documenting individual components to documenting larger design-system
14
+ compositions, while also improving TypeScript support, preview usability, and
15
+ overall build and development reliability.
16
+
17
+ ### Patterns
18
+
19
+ * Added support for design system patterns as a new documentation
20
+ layer above individual components.
21
+ * Patterns support composing multiple component previews into a single page.
22
+ * Pattern definitions can reference existing named component previews or
23
+ provide inline args for custom pattern-specific compositions.
24
+ * Added support for `entries.patterns`, allowing projects to register composed
25
+ design-system patterns alongside components.
26
+ * Added pattern summary and detail artifacts so patterns behave as first-class
27
+ generated output alongside components.
28
+
29
+ ### Typed Config and Declaration Workflow
30
+
31
+ * Added support for typed TypeScript and JavaScript config and declaration
32
+ files across the project workflow.
33
+ * Established TypeScript-based config and declaration files as the preferred
34
+ convention, while continuing to support JavaScript, CJS, and JSON where
35
+ applicable.
36
+ * Added clear config resolution precedence when multiple root config files are
37
+ present.
38
+ * Introduced the `.handoff.ts` and `.handoff.js` declaration convention for
39
+ components and patterns.
40
+ * Introduced structured helper APIs including `defineConfig`,
41
+ `defineReactComponent`, `defineHandlebarsComponent`,
42
+ `defineCsfComponent`, and `definePattern`.
43
+ * Updated runtime loading and normalization so typed declarations and modern
44
+ config shapes work consistently across the pipeline.
45
+ * Updated scaffolding and CLI flows to support both TypeScript and JavaScript
46
+ project setup.
47
+ * Updated generated examples and component scaffolds to follow the new
48
+ `.handoff.*` declaration convention.
49
+
50
+ ### Usage Snippets and Preview Improvements
51
+
52
+ * Added usage snippets as a new documentation feature, with output that
53
+ reflects the currently selected preview variant.
54
+ * Expanded CSF preview handling to produce more useful implementation examples
55
+ for React and Storybook-style components.
56
+ * Improved story and preview resolution so generated documentation is more
57
+ actionable for engineers consuming the design system.
58
+ * Enhanced code display behavior to better support usage-oriented
59
+ documentation rather than only static rendered output.
60
+
61
+ ### General Improvements
62
+
63
+ * Improved incremental build and watch behavior so changes trigger more
64
+ selective and reliable rebuilds.
65
+ * Added better cleanup of stale generated artifacts to keep component and
66
+ pattern output in sync as files change or are removed.
67
+ * Improved summary artifact maintenance during rebuilds so generated API output
68
+ stays aligned with the current source of truth.
69
+ * Enhanced logging with clearer scoped output across Handoff, Vite, and Next.js
70
+ processes.
71
+ * Improved child-process logging and development server reporting to make local
72
+ debugging and build visibility better.
73
+
9
74
  ## \[1.1.3] - 2026-03-27
10
75
 
11
76
  This release reworks the markdown page rendering system with four improvements:
@@ -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';
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;