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
@@ -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",
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';
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';