handoff-app 1.1.3 → 1.2.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/config.template.js +21 -14
- package/config/config.template.ts +54 -0
- package/config/docs/system.md +2 -0
- package/dist/app-builder/build.js +7 -2
- package/dist/app-builder/config-diff/index.d.ts +4 -0
- package/dist/app-builder/config-diff/index.js +12 -0
- package/dist/app-builder/config-diff/registry.d.ts +9 -0
- package/dist/app-builder/config-diff/registry.js +30 -0
- package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
- package/dist/app-builder/config-diff/snapshot.js +43 -0
- package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
- package/dist/app-builder/config-diff/strategies/pattern.js +93 -0
- package/dist/app-builder/config-diff/types.d.ts +19 -0
- package/dist/app-builder/config-diff/types.js +2 -0
- package/dist/app-builder/watchers/component.d.ts +15 -0
- package/dist/app-builder/watchers/component.js +69 -0
- package/dist/app-builder/watchers/utils.d.ts +14 -0
- package/dist/app-builder/watchers/utils.js +44 -0
- package/dist/app-builder/watchers.d.ts +10 -19
- package/dist/app-builder/watchers.js +56 -102
- package/dist/cache/build-cache.js +14 -5
- package/dist/cli/create.js +67 -23
- package/dist/cli/eject.js +48 -7
- package/dist/cli/make.js +29 -4
- package/dist/cli/scaffold.js +60 -19
- package/dist/cli.js +1 -1
- package/dist/config/helpers.d.ts +7 -0
- package/dist/config/helpers.js +31 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +5 -1
- package/dist/config/loader.d.ts +10 -1
- package/dist/config/loader.js +92 -28
- package/dist/config/normalizers/declaration.d.ts +11 -0
- package/dist/config/normalizers/declaration.js +119 -0
- package/dist/config/normalizers/pattern.d.ts +9 -0
- package/dist/config/normalizers/pattern.js +38 -0
- package/dist/config/runtime.d.ts +9 -3
- package/dist/config/runtime.js +254 -33
- package/dist/declarations/helpers.d.ts +6 -0
- package/dist/declarations/helpers.js +19 -0
- package/dist/declarations/index.d.ts +2 -0
- package/dist/declarations/index.js +9 -0
- package/dist/declarations/types.d.ts +60 -0
- package/dist/declarations/types.js +2 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +34 -5
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +3 -1
- package/dist/pipeline/patterns.d.ts +6 -0
- package/dist/pipeline/patterns.js +21 -0
- package/dist/transformers/docgen/index.js +12 -2
- package/dist/transformers/plugins/csf-render.js +170 -34
- package/dist/transformers/plugins/ssr-render.js +18 -2
- package/dist/transformers/preview/component/api.js +16 -7
- package/dist/transformers/preview/component/builder.js +9 -3
- package/dist/transformers/preview/component/css.d.ts +8 -0
- package/dist/transformers/preview/component/html.js +14 -5
- package/dist/transformers/preview/component/previews.d.ts +9 -0
- package/dist/transformers/preview/component/previews.js +10 -0
- package/dist/transformers/preview/pattern/api.d.ts +7 -0
- package/dist/transformers/preview/pattern/api.js +61 -0
- package/dist/transformers/preview/pattern/builder.d.ts +9 -0
- package/dist/transformers/preview/pattern/builder.js +86 -0
- package/dist/transformers/preview/pattern/html.d.ts +19 -0
- package/dist/transformers/preview/pattern/html.js +94 -0
- package/dist/transformers/preview/types.d.ts +54 -0
- package/dist/transformers/utils/csf.d.ts +16 -0
- package/dist/transformers/utils/csf.js +30 -0
- package/dist/transformers/utils/index.d.ts +1 -0
- package/dist/transformers/utils/index.js +2 -0
- package/dist/transformers/utils/module.d.ts +5 -0
- package/dist/transformers/utils/module.js +26 -13
- package/dist/transformers/utils/usage.d.ts +23 -0
- package/dist/transformers/utils/usage.js +294 -0
- package/dist/types/config.d.ts +32 -7
- package/dist/types/preview.d.ts +2 -0
- package/docs/cli.md +3 -3
- package/package.json +3 -1
- package/src/app/components/Component/PageSliceResolver.tsx +6 -6
- package/src/app/components/Component/Preview.tsx +17 -12
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
- package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
- package/src/app/components/util/index.ts +71 -2
- package/src/app/pages/system/pattern/[pattern]/index.tsx +223 -0
- package/src/app/pages/system/pattern/index.tsx +14 -0
- package/src/app-builder/README.md +37 -9
- package/src/app-builder/build.ts +8 -2
- package/src/app-builder/config-diff/index.ts +4 -0
- package/src/app-builder/config-diff/registry.ts +23 -0
- package/src/app-builder/config-diff/snapshot.ts +51 -0
- package/src/app-builder/config-diff/strategies/pattern.ts +65 -0
- package/src/app-builder/config-diff/types.ts +21 -0
- package/src/app-builder/watchers/component.ts +72 -0
- package/src/app-builder/watchers/utils.ts +44 -0
- package/src/app-builder/watchers.ts +62 -80
- package/src/cache/build-cache.ts +14 -5
- package/src/cli/create.ts +86 -25
- package/src/cli/eject.ts +29 -7
- package/src/cli/make.ts +32 -5
- package/src/cli/scaffold.ts +68 -21
- package/src/cli.ts +1 -1
- package/src/config/README.md +10 -1
- package/src/config/helpers.ts +43 -0
- package/src/config/index.ts +4 -1
- package/src/config/loader.ts +110 -31
- package/src/config/normalizers/declaration.ts +176 -0
- package/src/config/normalizers/pattern.ts +63 -0
- package/src/config/runtime.ts +310 -35
- package/src/declarations/helpers.ts +36 -0
- package/src/declarations/index.ts +19 -0
- package/src/declarations/types.ts +70 -0
- package/src/index.ts +47 -6
- package/src/pipeline/index.ts +1 -0
- package/src/pipeline/patterns.ts +10 -0
- package/src/transformers/README.md +2 -2
- package/src/transformers/docgen/index.ts +13 -2
- package/src/transformers/plugins/csf-render.ts +216 -49
- package/src/transformers/plugins/ssr-render.ts +23 -7
- package/src/transformers/preview/component/api.ts +27 -7
- package/src/transformers/preview/component/builder.ts +21 -4
- package/src/transformers/preview/component/html.ts +18 -5
- package/src/transformers/preview/component/previews.ts +12 -0
- package/src/transformers/preview/pattern/api.ts +66 -0
- package/src/transformers/preview/pattern/builder.ts +94 -0
- package/src/transformers/preview/pattern/html.ts +107 -0
- package/src/transformers/preview/types.ts +58 -1
- package/src/transformers/utils/csf.ts +48 -0
- package/src/transformers/utils/index.ts +3 -0
- package/src/transformers/utils/module.ts +33 -18
- package/src/transformers/utils/usage.ts +319 -0
- package/src/types/README.md +1 -1
- package/src/types/config.ts +33 -7
- package/src/types/preview.ts +2 -0
- package/templates/default/components/button/Button.jsx.tpl +64 -0
- package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
- package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
- package/templates/default/handoff.config.js.tpl +29 -16
- package/templates/default/handoff.config.ts.tpl +196 -0
- package/templates/default/handoff.config.with-examples.js.tpl +28 -17
- package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
- package/templates/default/package.json.tpl +1 -3
- package/handoff.config.js +0 -90
- package/templates/default/components/button/button.js.tpl +0 -62
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
defineComponent,
|
|
3
|
+
defineCsfComponent,
|
|
4
|
+
defineHandlebarsComponent,
|
|
5
|
+
definePattern,
|
|
6
|
+
defineReactComponent
|
|
7
|
+
} from './helpers';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
CsfDeclarationConfig,
|
|
11
|
+
DeclarationPreview,
|
|
12
|
+
GenericDeclarationConfig,
|
|
13
|
+
GenericPatternDeclarationConfig,
|
|
14
|
+
HandlebarsDeclarationConfig,
|
|
15
|
+
PatternComponentRef,
|
|
16
|
+
ReactDeclarationConfig,
|
|
17
|
+
RendererKind
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { ComponentObject } from '../transformers/preview/types';
|
|
3
|
+
|
|
4
|
+
export type RendererKind = 'react' | 'handlebars' | 'csf';
|
|
5
|
+
|
|
6
|
+
export type DeclarationPreview<TArgs = Record<string, any>> = {
|
|
7
|
+
title: string;
|
|
8
|
+
args?: TArgs;
|
|
9
|
+
values?: TArgs;
|
|
10
|
+
url?: string;
|
|
11
|
+
usage?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type BaseDeclarationEntries = NonNullable<ComponentObject['entries']> & {
|
|
15
|
+
component?: string;
|
|
16
|
+
story?: string;
|
|
17
|
+
templates?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type OptionalComponentMetadata = Partial<Omit<ComponentObject, 'previews' | 'entries' | 'title' | 'should_do' | 'should_not_do'>>;
|
|
21
|
+
|
|
22
|
+
export type BaseDeclarationConfig = OptionalComponentMetadata & {
|
|
23
|
+
id?: string;
|
|
24
|
+
name: string;
|
|
25
|
+
renderer?: RendererKind;
|
|
26
|
+
entries?: BaseDeclarationEntries;
|
|
27
|
+
previews?: Record<string, DeclarationPreview>;
|
|
28
|
+
shouldDo?: string[];
|
|
29
|
+
shouldNotDo?: string[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ReactDeclarationConfig<TProps> = Omit<BaseDeclarationConfig, 'renderer' | 'entries' | 'previews'> & {
|
|
33
|
+
entries: BaseDeclarationEntries & { component: string };
|
|
34
|
+
previews: Record<string, DeclarationPreview<Partial<TProps>>>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type HandlebarsDeclarationConfig = Omit<BaseDeclarationConfig, 'renderer' | 'entries'> & {
|
|
38
|
+
entries: BaseDeclarationEntries & { template: string };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type CsfDeclarationConfig = Omit<BaseDeclarationConfig, 'renderer' | 'entries'> & {
|
|
42
|
+
entries: BaseDeclarationEntries & { story: string };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type GenericDeclarationConfig = Omit<BaseDeclarationConfig, 'renderer'> & {
|
|
46
|
+
renderer: RendererKind;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ReactComponentType<TProps = any> = React.ComponentType<TProps>;
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Pattern declarations
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export type PatternComponentRef = {
|
|
56
|
+
id: string;
|
|
57
|
+
preview?: string;
|
|
58
|
+
args?: Record<string, any>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type BasePatternDeclarationConfig = {
|
|
62
|
+
id?: string;
|
|
63
|
+
name: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
group?: string;
|
|
66
|
+
tags?: string[];
|
|
67
|
+
components: PatternComponentRef[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type GenericPatternDeclarationConfig = BasePatternDeclarationConfig;
|
package/src/index.ts
CHANGED
|
@@ -5,11 +5,12 @@ import path from 'path';
|
|
|
5
5
|
import buildApp, { devApp, watchApp } from './app-builder';
|
|
6
6
|
import { ejectConfig, ejectPages, ejectTheme } from './cli/eject';
|
|
7
7
|
import { makeComponent, makePage, makeTemplate } from './cli/make';
|
|
8
|
-
import {
|
|
9
|
-
import pipeline, { buildComponents } from './pipeline';
|
|
8
|
+
import { initConfigWithMetadata, initRuntimeConfig, validateConfig } from './config';
|
|
9
|
+
import pipeline, { buildComponents, buildPatterns } from './pipeline';
|
|
10
10
|
import processComponents, { ComponentSegment } from './transformers/preview/component/builder';
|
|
11
|
-
import { Config, RuntimeConfig } from './types/config';
|
|
11
|
+
import { Config, ConfigFileEntry, RuntimeConfig } from './types/config';
|
|
12
12
|
import { Logger } from './utils/logger';
|
|
13
|
+
import { normalizePathForCompare } from './utils/path';
|
|
13
14
|
import { generateFilesystemSafeId } from './utils/path';
|
|
14
15
|
|
|
15
16
|
class Handoff {
|
|
@@ -29,6 +30,8 @@ class Handoff {
|
|
|
29
30
|
|
|
30
31
|
private _initialArgs: { debug?: boolean; force?: boolean; config?: Partial<Config> } = {};
|
|
31
32
|
private _configFilePaths: string[] = [];
|
|
33
|
+
private _configFileIndex: Map<string, ConfigFileEntry> = new Map();
|
|
34
|
+
private _mainConfigFilePath?: string;
|
|
32
35
|
private _documentationObjectCache?: CoreTypes.IDocumentationObject;
|
|
33
36
|
private _handoffRunner?: ReturnType<typeof HandoffRunner> | null;
|
|
34
37
|
|
|
@@ -47,11 +50,13 @@ class Handoff {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
init(configOverride?: Partial<Config>): Handoff {
|
|
50
|
-
const
|
|
53
|
+
const configResult = initConfigWithMetadata(configOverride ?? {});
|
|
54
|
+
const config = configResult.config;
|
|
51
55
|
this.config = config;
|
|
56
|
+
this._mainConfigFilePath = configResult.configPath;
|
|
52
57
|
this.exportsDirectory = config.exportsOutputDirectory ?? this.exportsDirectory;
|
|
53
58
|
this.sitesDirectory = config.sitesOutputDirectory ?? this.exportsDirectory;
|
|
54
|
-
[this.runtimeConfig, this._configFilePaths] = initRuntimeConfig(this);
|
|
59
|
+
[this.runtimeConfig, this._configFilePaths, this._configFileIndex] = initRuntimeConfig(this);
|
|
55
60
|
if(this.config.app.base_path && !process.env.HANDOFF_APP_BASE_PATH) {
|
|
56
61
|
process.env.HANDOFF_APP_BASE_PATH = this.config.app.base_path ?? '';
|
|
57
62
|
}
|
|
@@ -92,6 +97,12 @@ class Handoff {
|
|
|
92
97
|
return this;
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
async pattern(): Promise<Handoff> {
|
|
101
|
+
this.preRunner();
|
|
102
|
+
await buildPatterns(this);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
async build(skipComponents?: boolean): Promise<Handoff> {
|
|
96
107
|
this.preRunner();
|
|
97
108
|
await buildApp(this, skipComponents);
|
|
@@ -271,7 +282,19 @@ class Handoff {
|
|
|
271
282
|
* @returns {string[]} Array of absolute paths to config files
|
|
272
283
|
*/
|
|
273
284
|
getConfigFilePaths(): string[] {
|
|
274
|
-
|
|
285
|
+
const combined = this._mainConfigFilePath ? [this._mainConfigFilePath, ...this._configFilePaths] : this._configFilePaths;
|
|
286
|
+
return Array.from(new Set(combined));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Gets the selected main config file path if one exists.
|
|
291
|
+
*/
|
|
292
|
+
getMainConfigFilePath(): string | undefined {
|
|
293
|
+
return this._mainConfigFilePath;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
getConfigFileEntry(configPath: string): ConfigFileEntry | undefined {
|
|
297
|
+
return this._configFileIndex.get(normalizePathForCompare(configPath));
|
|
275
298
|
}
|
|
276
299
|
|
|
277
300
|
/**
|
|
@@ -298,6 +321,24 @@ class Handoff {
|
|
|
298
321
|
|
|
299
322
|
export type { ComponentObject as Component } from './transformers/preview/types';
|
|
300
323
|
export type { Config, RegisterHandlebarsHelpersContext } from './types/config';
|
|
324
|
+
export { defineConfig } from './config';
|
|
325
|
+
export {
|
|
326
|
+
defineComponent,
|
|
327
|
+
defineCsfComponent,
|
|
328
|
+
defineHandlebarsComponent,
|
|
329
|
+
definePattern,
|
|
330
|
+
defineReactComponent,
|
|
331
|
+
} from './declarations';
|
|
332
|
+
export type {
|
|
333
|
+
CsfDeclarationConfig,
|
|
334
|
+
DeclarationPreview,
|
|
335
|
+
GenericDeclarationConfig,
|
|
336
|
+
GenericPatternDeclarationConfig,
|
|
337
|
+
HandlebarsDeclarationConfig,
|
|
338
|
+
PatternComponentRef,
|
|
339
|
+
ReactDeclarationConfig,
|
|
340
|
+
RendererKind,
|
|
341
|
+
} from './declarations';
|
|
301
342
|
|
|
302
343
|
// Export transformers and types from handoff-core
|
|
303
344
|
export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core';
|
package/src/pipeline/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { validateHandoffRequirements } from './validation';
|
|
|
9
9
|
// Re-exports used by other modules
|
|
10
10
|
export { readPrevJSONFile, zip, zipAssets } from './archive';
|
|
11
11
|
export { buildComponents } from './components';
|
|
12
|
+
export { buildPatterns } from './patterns';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Run the entire Figma data pipeline:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Handoff from '..';
|
|
2
|
+
import { processPatterns } from '../transformers/preview/pattern/builder';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds pattern previews by composing pre-built component preview HTML.
|
|
6
|
+
* Must be called AFTER buildComponents.
|
|
7
|
+
*/
|
|
8
|
+
export const buildPatterns = async (handoff: Handoff) => {
|
|
9
|
+
await processPatterns(handoff);
|
|
10
|
+
};
|
|
@@ -10,9 +10,9 @@ transformers/
|
|
|
10
10
|
│ ├── component/ # Component builder, CSS/JS bundling, SSR rendering, validation
|
|
11
11
|
│ ├── component.ts # Component transformer entry point, shared styles processing
|
|
12
12
|
│ └── types.ts # Preview/component type definitions
|
|
13
|
-
├── plugins/ # Build plugins (SSR render, Handlebars previews, CSF render)
|
|
13
|
+
├── plugins/ # Build plugins (SSR render, Handlebars previews, CSF render with per-preview/story usage generation)
|
|
14
14
|
├── docgen/ # React component documentation generation
|
|
15
|
-
├── utils/ # Build utilities (handlebars, schema, HTML, SVG sprites, Vite logger)
|
|
15
|
+
├── utils/ # Build utilities (handlebars, schema, HTML, SVG sprites, Vite logger, usage snippet helpers)
|
|
16
16
|
├── plugins.ts # Barrel re-exports for plugins
|
|
17
17
|
├── vite-config.ts # Shared Vite configuration
|
|
18
18
|
└── types.ts # Transformer-level type definitions
|
|
@@ -261,8 +261,19 @@ export const enrichPropertiesWithDocgen = (
|
|
|
261
261
|
export const generateDocsArtifact = async (entry: string, handoff: any): Promise<GeneratedDocs | null> => {
|
|
262
262
|
try {
|
|
263
263
|
const tsconfigPath = path.resolve(handoff.workingPath, 'tsconfig.json');
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
const hasTsconfig = fs.existsSync(tsconfigPath);
|
|
265
|
+
const entryExtension = path.extname(entry).toLowerCase();
|
|
266
|
+
const isJavaScriptEntry = ['.js', '.jsx', '.cjs', '.mjs'].includes(entryExtension);
|
|
267
|
+
|
|
268
|
+
if (!hasTsconfig) {
|
|
269
|
+
// For JS-only projects, handoff-docgen currently expects a tsconfig and will fail noisily.
|
|
270
|
+
// Skip docgen gracefully in this case.
|
|
271
|
+
if (isJavaScriptEntry) {
|
|
272
|
+
Logger.debug(`Skipping handoff-docgen for ${entry}: no tsconfig.json found in ${handoff.workingPath}`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
Logger.warn(`TypeScript config not found at ${tsconfigPath}; skipping docs generation for ${entry}.`);
|
|
276
|
+
return null;
|
|
266
277
|
}
|
|
267
278
|
|
|
268
279
|
const componentDirectory = path.dirname(path.resolve(entry));
|
|
@@ -4,32 +4,24 @@ import { startCase } from 'lodash';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import ReactDOMServer from 'react-dom/server';
|
|
7
|
+
import reactElementToJSXString from 'react-element-to-jsx-string';
|
|
7
8
|
import { Plugin } from 'vite';
|
|
8
9
|
import Handoff from '../..';
|
|
9
10
|
import { Logger } from '../../utils/logger';
|
|
11
|
+
import { generateDocsArtifact, getPropertiesForComponentFromDocs } from '../docgen';
|
|
10
12
|
import { SlotMetadata, SlotType } from '../preview/component';
|
|
11
13
|
import { TransformComponentTokensResult } from '../preview/types';
|
|
12
14
|
import { formatHtml, trimPreview } from '../utils/html';
|
|
13
15
|
import { buildAndEvaluateModule } from '../utils/module';
|
|
14
16
|
import { ensureIds } from '../utils/schema';
|
|
17
|
+
import { extractComponentName, generateUsageSnippet } from '../utils/usage';
|
|
18
|
+
import { createCsfStoryPreviews, CsfMeta, getCsfStoryEntries, StoryObject } from '../utils/csf';
|
|
15
19
|
import { createViteLogger } from '../utils/vite-logger';
|
|
16
|
-
import { generateDocsArtifact, getPropertiesForComponentFromDocs } from '../docgen';
|
|
17
20
|
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
argTypes?: Record<string, any>;
|
|
22
|
-
render?: (args: Record<string, any>) => any;
|
|
21
|
+
type ComponentImportInfo = {
|
|
22
|
+
componentName: string;
|
|
23
|
+
importStatement: string;
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
-
type CsfMeta = {
|
|
26
|
-
title?: string;
|
|
27
|
-
component?: React.ComponentType<any>;
|
|
28
|
-
args?: Record<string, any>;
|
|
29
|
-
argTypes?: Record<string, any>;
|
|
30
|
-
render?: (args: Record<string, any>) => any;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
25
|
const PLUGIN_CONSTANTS = {
|
|
34
26
|
PLUGIN_NAME: 'vite-plugin-csf-render',
|
|
35
27
|
SCRIPT_ID: 'script',
|
|
@@ -78,15 +70,6 @@ function createSlotMetadata(
|
|
|
78
70
|
};
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
function getStoryEntries(moduleExports: Record<string, any>): Array<[string, StoryObject]> {
|
|
82
|
-
return Object.entries(moduleExports).filter(([key, value]) => {
|
|
83
|
-
if (key === 'default' || key === '__esModule') return false;
|
|
84
|
-
if (typeof value === 'function') return true;
|
|
85
|
-
if (value && typeof value === 'object') return true;
|
|
86
|
-
return false;
|
|
87
|
-
}) as Array<[string, StoryObject]>;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
73
|
function toReactElement(
|
|
91
74
|
renderResult: any,
|
|
92
75
|
meta: CsfMeta,
|
|
@@ -107,28 +90,179 @@ function toReactElement(
|
|
|
107
90
|
return React.createElement('pre', null, JSON.stringify(args, null, 2));
|
|
108
91
|
}
|
|
109
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Produces the React element for a story, mirroring Storybook's resolution order:
|
|
95
|
+
* story render > meta render > meta.component > JSON fallback.
|
|
96
|
+
*/
|
|
97
|
+
function getStoryElement(
|
|
98
|
+
meta: CsfMeta,
|
|
99
|
+
story: StoryObject | ((args: Record<string, any>) => any) | undefined,
|
|
100
|
+
args: Record<string, any>
|
|
101
|
+
): React.ReactElement {
|
|
102
|
+
const storyRender = typeof story === 'function' ? story : story?.render;
|
|
103
|
+
const render = storyRender || meta.render;
|
|
104
|
+
|
|
105
|
+
if (render) {
|
|
106
|
+
return toReactElement(render(args), meta, args);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (meta.component) {
|
|
110
|
+
return React.createElement(meta.component, args);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return React.createElement('pre', null, JSON.stringify(args, null, 2));
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
function safeRenderToHtml(
|
|
111
117
|
meta: CsfMeta,
|
|
112
|
-
story: StoryObject | ((args: Record<string, any>) => any),
|
|
118
|
+
story: StoryObject | ((args: Record<string, any>) => any) | undefined,
|
|
113
119
|
args: Record<string, any>
|
|
114
120
|
): string {
|
|
115
121
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
return ReactDOMServer.renderToString(getStoryElement(meta, story, args));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
Logger.warn(`SSR render failed for story, falling back to static placeholder: ${error}`);
|
|
125
|
+
return `<div style="padding:1rem;color:#b91c1c;font-family:monospace;font-size:13px">Render error: ${String(error)}</div>`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Converts a story's React element into a JSX source string using the same
|
|
131
|
+
* approach as Storybook's Docs addon.
|
|
132
|
+
*/
|
|
133
|
+
function reactElementToUsage(element: React.ReactElement): string | null {
|
|
134
|
+
try {
|
|
135
|
+
return reactElementToJSXString(element, {
|
|
136
|
+
sortProps: false,
|
|
137
|
+
useBooleanShorthandSyntax: true,
|
|
138
|
+
useFragmentShortSyntax: true,
|
|
139
|
+
maxInlineAttributesLineLength: 80,
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
Logger.warn(`Failed to convert React element to JSX string: ${error}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function findMatchingBrace(source: string, openIndex: number): number {
|
|
148
|
+
let depth = 0;
|
|
118
149
|
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
151
|
+
const char = source[i];
|
|
152
|
+
if (char === '{') depth += 1;
|
|
153
|
+
if (char === '}') {
|
|
154
|
+
depth -= 1;
|
|
155
|
+
if (depth === 0) {
|
|
156
|
+
return i;
|
|
157
|
+
}
|
|
121
158
|
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return -1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function extractMetaObjectBody(sourceCode: string): string | null {
|
|
165
|
+
// TODO: Regex-based CSF meta parsing can miss complex inline `satisfies` cases.
|
|
166
|
+
// Move this to a TS-AST resolver in handoff-docgen and consume the normalized import info here.
|
|
167
|
+
const inlineMetaMatch = sourceCode.match(/export\s+default\s+({[\s\S]*?})\s*(?:satisfies\s+[^{;\n]+|as\s+[^{;\n]+)?\s*;/m);
|
|
168
|
+
if (inlineMetaMatch?.[1]) {
|
|
169
|
+
return inlineMetaMatch[1];
|
|
170
|
+
}
|
|
122
171
|
|
|
123
|
-
|
|
124
|
-
|
|
172
|
+
const exportedMetaRef = sourceCode.match(/export\s+default\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*;/m);
|
|
173
|
+
if (!exportedMetaRef?.[1]) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const metaVarName = exportedMetaRef[1];
|
|
178
|
+
const declarationPatterns = [
|
|
179
|
+
new RegExp(`(?:const|let|var)\\s+${metaVarName}\\s*=\\s*`, 'm'),
|
|
180
|
+
new RegExp(`(?:const|let|var)\\s+${metaVarName}\\s*=\\s*[^\\n;]*?\\b(?:satisfies|as)\\b[^\\n;]*?`, 'm'),
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
for (const pattern of declarationPatterns) {
|
|
184
|
+
const match = pattern.exec(sourceCode);
|
|
185
|
+
if (!match) continue;
|
|
186
|
+
const openIndex = sourceCode.indexOf('{', match.index);
|
|
187
|
+
if (openIndex === -1) continue;
|
|
188
|
+
const closeIndex = findMatchingBrace(sourceCode, openIndex);
|
|
189
|
+
if (closeIndex === -1) continue;
|
|
190
|
+
return sourceCode.slice(openIndex, closeIndex + 1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveComponentImportInfo(sourceCode: string): ComponentImportInfo | null {
|
|
197
|
+
const metaBody = extractMetaObjectBody(sourceCode);
|
|
198
|
+
if (!metaBody) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const componentMatch = metaBody.match(/\bcomponent\s*:\s*([A-Za-z_$][A-Za-z0-9_$]*)\b/m);
|
|
203
|
+
if (!componentMatch?.[1]) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const localName = componentMatch[1];
|
|
208
|
+
|
|
209
|
+
const defaultImportMatch = sourceCode.match(
|
|
210
|
+
new RegExp(`import\\s+${localName}\\s+from\\s+['"]([^'"]+)['"]`, 'm')
|
|
211
|
+
);
|
|
212
|
+
if (defaultImportMatch?.[1]) {
|
|
213
|
+
return {
|
|
214
|
+
componentName: localName,
|
|
215
|
+
importStatement: `import ${localName} from '${defaultImportMatch[1]}';`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const namedImportRegex = /import\s*{\s*([^}]+)\s*}\s*from\s*['"]([^'"]+)['"]/gm;
|
|
220
|
+
for (const match of sourceCode.matchAll(namedImportRegex)) {
|
|
221
|
+
const specifiers = match[1]
|
|
222
|
+
.split(',')
|
|
223
|
+
.map((specifier) => specifier.trim())
|
|
224
|
+
.filter(Boolean);
|
|
225
|
+
const moduleSpecifier = match[2];
|
|
226
|
+
|
|
227
|
+
for (const specifier of specifiers) {
|
|
228
|
+
const aliasMatch = specifier.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
|
|
229
|
+
if (aliasMatch && aliasMatch[2] === localName) {
|
|
230
|
+
return {
|
|
231
|
+
componentName: localName,
|
|
232
|
+
importStatement: `import { ${aliasMatch[1]} as ${localName} } from '${moduleSpecifier}';`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (specifier === localName) {
|
|
237
|
+
return {
|
|
238
|
+
componentName: localName,
|
|
239
|
+
importStatement: `import { ${localName} } from '${moduleSpecifier}';`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
125
242
|
}
|
|
243
|
+
}
|
|
126
244
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildUsageSnippet(
|
|
249
|
+
componentName: string,
|
|
250
|
+
importStatement: string,
|
|
251
|
+
element: React.ReactElement | null,
|
|
252
|
+
properties: Record<string, SlotMetadata>,
|
|
253
|
+
previewValues: Record<string, any>
|
|
254
|
+
): string {
|
|
255
|
+
const usageBody = element ? reactElementToUsage(element) : null;
|
|
256
|
+
if (usageBody) {
|
|
257
|
+
return `${importStatement}\n\n${usageBody}`;
|
|
131
258
|
}
|
|
259
|
+
|
|
260
|
+
return generateUsageSnippet({
|
|
261
|
+
componentName,
|
|
262
|
+
properties,
|
|
263
|
+
previewValues,
|
|
264
|
+
importStatement,
|
|
265
|
+
});
|
|
132
266
|
}
|
|
133
267
|
|
|
134
268
|
function createHtmlDocument(componentId: string, previewTitle: string, renderedHtml: string): string {
|
|
@@ -189,7 +323,8 @@ export function csfRenderPlugin(
|
|
|
189
323
|
}
|
|
190
324
|
|
|
191
325
|
const meta = (moduleExports.default || {}) as CsfMeta;
|
|
192
|
-
const stories =
|
|
326
|
+
const stories = getCsfStoryEntries(moduleExports);
|
|
327
|
+
const storyMap = Object.fromEntries(stories);
|
|
193
328
|
|
|
194
329
|
if (stories.length === 0) {
|
|
195
330
|
Logger.warn(`No named stories found in CSF file for ${componentId}: ${templatePath}`);
|
|
@@ -198,9 +333,16 @@ export function csfRenderPlugin(
|
|
|
198
333
|
|
|
199
334
|
const explicitProperties = componentData.properties || {};
|
|
200
335
|
const generatedProperties: Record<string, SlotMetadata> = { ...explicitProperties };
|
|
336
|
+
const incomingPreviews = { ...(componentData.previews || {}) };
|
|
337
|
+
const patternPreviews = Object.fromEntries(
|
|
338
|
+
Object.entries(incomingPreviews).filter(([key]) => key.startsWith('__pattern_'))
|
|
339
|
+
);
|
|
201
340
|
|
|
202
|
-
// Include all named stories as previews and
|
|
203
|
-
componentData.previews = {
|
|
341
|
+
// Include all named stories as previews and preserve injected pattern previews.
|
|
342
|
+
componentData.previews = {
|
|
343
|
+
...createCsfStoryPreviews(moduleExports),
|
|
344
|
+
...patternPreviews,
|
|
345
|
+
};
|
|
204
346
|
|
|
205
347
|
for (const [storyKey, storyValue] of stories) {
|
|
206
348
|
const storyObj = (typeof storyValue === 'function' ? {} : storyValue) as StoryObject;
|
|
@@ -213,12 +355,6 @@ export function csfRenderPlugin(
|
|
|
213
355
|
...(storyObj.argTypes || {}),
|
|
214
356
|
};
|
|
215
357
|
|
|
216
|
-
componentData.previews[storyKey] = {
|
|
217
|
-
title: storyObj.name || startCase(storyKey),
|
|
218
|
-
values: mergedArgs,
|
|
219
|
-
url: '',
|
|
220
|
-
};
|
|
221
|
-
|
|
222
358
|
for (const [argKey, argValue] of Object.entries(mergedArgs)) {
|
|
223
359
|
if (!generatedProperties[argKey]) {
|
|
224
360
|
generatedProperties[argKey] = createSlotMetadata(argKey, mergedArgTypes[argKey], argValue);
|
|
@@ -236,6 +372,10 @@ export function csfRenderPlugin(
|
|
|
236
372
|
const generatedDocs = await generateDocsArtifact(templatePath, handoff);
|
|
237
373
|
const componentName =
|
|
238
374
|
(meta.component as any)?.displayName || (meta.component as any)?.name;
|
|
375
|
+
const componentImportInfo = resolveComponentImportInfo(sourceCode);
|
|
376
|
+
const usageComponentName = componentImportInfo?.componentName || componentName || extractComponentName(templatePath);
|
|
377
|
+
const usageImportStatement =
|
|
378
|
+
componentImportInfo?.importStatement || `import ${usageComponentName} from './${usageComponentName}';`;
|
|
239
379
|
if (generatedDocs && componentName) {
|
|
240
380
|
const docgenProperties = getPropertiesForComponentFromDocs(
|
|
241
381
|
generatedDocs,
|
|
@@ -285,11 +425,15 @@ export function csfRenderPlugin(
|
|
|
285
425
|
componentData.properties = ensureIds(generatedProperties);
|
|
286
426
|
|
|
287
427
|
let lastHtml = '';
|
|
288
|
-
for (const [
|
|
289
|
-
const
|
|
290
|
-
|
|
428
|
+
for (const [previewKey, preview] of Object.entries(componentData.previews)) {
|
|
429
|
+
const storyKey = preview.sourcePreview && storyMap[preview.sourcePreview]
|
|
430
|
+
? preview.sourcePreview
|
|
431
|
+
: (storyMap[previewKey] ? previewKey : undefined);
|
|
432
|
+
const storyValue = storyKey ? storyMap[storyKey] : undefined;
|
|
433
|
+
const storyArgs = preview.values || {};
|
|
434
|
+
const rendered = safeRenderToHtml(meta, storyValue, storyArgs);
|
|
291
435
|
const html = await formatHtml(createHtmlDocument(componentId, preview.title, rendered));
|
|
292
|
-
const fileName = `${componentId}-${
|
|
436
|
+
const fileName = `${componentId}-${previewKey}.html`;
|
|
293
437
|
|
|
294
438
|
this.emitFile({
|
|
295
439
|
type: 'asset',
|
|
@@ -298,10 +442,26 @@ export function csfRenderPlugin(
|
|
|
298
442
|
});
|
|
299
443
|
this.emitFile({
|
|
300
444
|
type: 'asset',
|
|
301
|
-
fileName: `${componentId}-${
|
|
445
|
+
fileName: `${componentId}-${previewKey}${PLUGIN_CONSTANTS.INSPECT_SUFFIX}.html`,
|
|
302
446
|
source: html,
|
|
303
447
|
});
|
|
304
448
|
|
|
449
|
+
// Capture JSX usage for each story so docs can switch usage by selected preview.
|
|
450
|
+
let storyElement: React.ReactElement | null = null;
|
|
451
|
+
try {
|
|
452
|
+
storyElement = getStoryElement(meta, storyValue, storyArgs);
|
|
453
|
+
} catch {
|
|
454
|
+
// Fall back to schema-based generation below.
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
preview.usage = buildUsageSnippet(
|
|
458
|
+
usageComponentName,
|
|
459
|
+
usageImportStatement,
|
|
460
|
+
storyElement,
|
|
461
|
+
componentData.properties || {},
|
|
462
|
+
storyArgs
|
|
463
|
+
);
|
|
464
|
+
|
|
305
465
|
preview.url = fileName;
|
|
306
466
|
lastHtml = html;
|
|
307
467
|
}
|
|
@@ -310,6 +470,13 @@ export function csfRenderPlugin(
|
|
|
310
470
|
componentData.preview = '';
|
|
311
471
|
componentData.code = trimPreview(sourceCode);
|
|
312
472
|
componentData.html = trimPreview(lastHtml);
|
|
473
|
+
|
|
474
|
+
// Keep top-level usage for backwards compatibility (first preview usage).
|
|
475
|
+
const storyKeys = Object.keys(componentData.previews);
|
|
476
|
+
const firstStoryKey = storyKeys[0];
|
|
477
|
+
if (firstStoryKey) {
|
|
478
|
+
componentData.usage = componentData.previews[firstStoryKey].usage || '';
|
|
479
|
+
}
|
|
313
480
|
},
|
|
314
481
|
};
|
|
315
482
|
}
|
|
@@ -8,10 +8,10 @@ import { Plugin, normalizePath } from 'vite';
|
|
|
8
8
|
import Handoff from '../..';
|
|
9
9
|
import { Logger } from '../../utils/logger';
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
enrichPropertiesWithDocgen,
|
|
12
|
+
generateDocsArtifact,
|
|
13
|
+
generatePropertiesFromDocgen,
|
|
14
|
+
getPropertiesFromGeneratedDocs,
|
|
15
15
|
} from '../docgen';
|
|
16
16
|
import { SlotMetadata } from '../preview/component';
|
|
17
17
|
import { TransformComponentTokensResult } from '../preview/types';
|
|
@@ -20,6 +20,7 @@ import { formatHtml, trimPreview } from '../utils/html';
|
|
|
20
20
|
import { buildAndEvaluateModule } from '../utils/module';
|
|
21
21
|
import { loadSchemaFromComponent, loadSchemaFromFile } from '../utils/schema-loader';
|
|
22
22
|
import { slugify } from '../utils/string';
|
|
23
|
+
import { extractComponentName, generateUsageSnippet } from '../utils/usage';
|
|
23
24
|
import { createViteLogger } from '../utils/vite-logger';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -204,8 +205,6 @@ export function ssrRenderPlugin(
|
|
|
204
205
|
documentationComponents = {};
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
const generatedPreviews: { [key: string]: string } = {};
|
|
208
|
-
|
|
209
208
|
// Process component instances from documentation
|
|
210
209
|
// Use figmaComponentId if provided, otherwise skip implicit matching
|
|
211
210
|
if (componentData.figmaComponentId) {
|
|
@@ -219,6 +218,7 @@ export function ssrRenderPlugin(
|
|
|
219
218
|
title: variationId,
|
|
220
219
|
url: '',
|
|
221
220
|
values: instanceValues,
|
|
221
|
+
usage: '',
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
224
|
}
|
|
@@ -294,8 +294,13 @@ export function ssrRenderPlugin(
|
|
|
294
294
|
source: finalHtml,
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
-
generatedPreviews[previewKey] = finalHtml;
|
|
298
297
|
componentData.previews[previewKey].url = `${componentId}-${previewKey}.html`;
|
|
298
|
+
componentData.previews[previewKey].usage = generateUsageSnippet({
|
|
299
|
+
componentName: extractComponentName(componentPath),
|
|
300
|
+
properties: componentData.properties || {},
|
|
301
|
+
previewValues: previewProps || {},
|
|
302
|
+
templateFileName: path.basename(componentPath),
|
|
303
|
+
});
|
|
299
304
|
}
|
|
300
305
|
|
|
301
306
|
// Format final HTML and update component data
|
|
@@ -304,6 +309,17 @@ export function ssrRenderPlugin(
|
|
|
304
309
|
componentData.preview = '';
|
|
305
310
|
componentData.code = trimPreview(componentSourceCode);
|
|
306
311
|
componentData.html = trimPreview(finalHtml);
|
|
312
|
+
|
|
313
|
+
// Generate usage snippet from the first preview's values
|
|
314
|
+
const previewKeys = Object.keys(componentData.previews);
|
|
315
|
+
const firstPreviewValues = previewKeys.length > 0 ? componentData.previews[previewKeys[0]].values : {};
|
|
316
|
+
const componentName = extractComponentName(componentPath);
|
|
317
|
+
componentData.usage = generateUsageSnippet({
|
|
318
|
+
componentName,
|
|
319
|
+
properties: componentData.properties || {},
|
|
320
|
+
previewValues: firstPreviewValues,
|
|
321
|
+
templateFileName: path.basename(componentPath),
|
|
322
|
+
});
|
|
307
323
|
},
|
|
308
324
|
};
|
|
309
325
|
}
|