handoff-app 2.0.0-8 → 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/{Changelog.md → CHANGELOG.md} +85 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +816 -393
- package/RECIPES.md +975 -0
- package/UPGRADE.md +57 -9
- package/components.json +20 -20
- package/config/docs/foundations/colors/guidelines.md +0 -1
- package/config/docs/foundations/colors.md +0 -1
- package/config/docs/foundations/effects/guidelines.md +0 -1
- package/config/docs/foundations/effects/resources.md +0 -1
- package/config/docs/foundations/effects.md +0 -1
- package/config/docs/foundations/fonts.md +0 -1
- package/config/docs/foundations/grid.md +0 -1
- package/config/docs/foundations/icon.md +0 -1
- package/config/docs/foundations/icons/guidelines.md +0 -1
- package/config/docs/foundations/icons.md +0 -1
- package/config/docs/foundations/images/guidelines.md +0 -1
- package/config/docs/foundations/images/resources.md +0 -1
- package/config/docs/foundations/images.md +0 -1
- package/config/docs/foundations/logo/resources.md +0 -1
- package/config/docs/foundations/logo.md +0 -1
- package/config/docs/foundations/typography/guidelines.md +0 -1
- package/config/docs/foundations/typography/resources.md +0 -1
- package/config/docs/foundations/typography.md +0 -1
- package/config/docs/foundations.md +0 -1
- package/config/docs/guidelines.md +0 -1
- package/config/docs/index.md +0 -1
- package/config/docs/system/tokens/foundations/colors.md +0 -4
- package/config/docs/system/tokens/foundations/effects.md +0 -4
- package/config/docs/system/tokens/foundations/typography.md +0 -2
- package/config/docs/system.md +0 -2
- package/config/templates/page.md +2 -1
- package/config/templates/page.mdx +2 -1
- package/dist/ai/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +195 -66
- package/dist/app-builder/client-config.js +3 -2
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/cli/create.js +1 -2
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +28 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +9 -11
- package/dist/handlebars.d.ts +10 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.js +18 -22
- package/dist/nav/index.d.ts +2 -2
- package/dist/nav/registry.d.ts +3 -3
- package/dist/nav/registry.js +1 -1
- package/dist/nav/workspace.d.ts +3 -3
- package/dist/nav/workspace.js +1 -1
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +13 -0
- package/dist/registry/asset-storage/define.d.ts +5 -5
- package/dist/registry/asset-storage/define.js +4 -4
- package/dist/registry/asset-storage/resolve.d.ts +9 -19
- package/dist/registry/asset-storage/resolve.js +33 -21
- package/dist/registry/asset-storage/types.d.ts +37 -17
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +17 -0
- package/dist/registry/auth/crypto.js +61 -1
- package/dist/registry/auth/index.d.ts +2 -1
- package/dist/registry/auth/index.js +11 -2
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +261 -3
- package/dist/registry/db/schema.js +29 -4
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/providers/smtp.d.ts +10 -0
- package/dist/registry/email/providers/smtp.js +73 -0
- package/dist/registry/email/resolve.d.ts +28 -0
- package/dist/registry/email/resolve.js +74 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/package.js +8 -0
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +3 -1
- package/dist/transformers/preview/component/css.js +92 -51
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +24 -0
- package/dist/transformers/utils/build.js +100 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +135 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +275 -54
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +19 -8
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +58 -19
- package/src/app/components/Auth/AuthShell.tsx +27 -33
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +383 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Header.tsx +21 -15
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +2 -2
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +7 -2
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +3 -12
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +35 -36
- package/src/app/lib/auth/api.ts +4 -12
- package/src/app/lib/auth/email.ts +96 -23
- package/src/app/lib/docs-api/page-rendering.ts +1 -1
- package/src/app/lib/docs-api/registry.ts +2 -3
- package/src/app/lib/docs-api/runtime-config.ts +47 -21
- package/src/app/lib/mcp/components.ts +523 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +39 -318
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +119 -11
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +20 -0
- package/src/app/next.config.mjs +26 -24
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/_app.tsx +6 -2
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +68 -68
- package/src/app/pages/account/tokens.tsx +37 -7
- package/src/app/pages/account/users.tsx +75 -15
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +10 -5
- package/src/app/pages/foundations/colors.tsx +9 -8
- package/src/app/pages/foundations/index.tsx +3 -3
- package/src/app/pages/index.tsx +2 -2
- package/src/app/pages/install.tsx +7 -2
- package/src/app/pages/login.tsx +32 -27
- package/src/app/pages/reset-password.tsx +21 -20
- package/src/app/pages/system/component/[component]/index.tsx +2 -5
- package/src/app/pages/system/index.tsx +11 -11
- package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
- package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +218 -73
- package/src/app-builder/client-config.ts +3 -2
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/cli/create.ts +1 -2
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +10 -9
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +27 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +10 -5
- package/src/handlebars.ts +8 -2
- package/src/index.ts +33 -29
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/providers.ts +18 -0
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +38 -37
- package/src/registry/asset-storage/types.ts +39 -17
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +63 -1
- package/src/registry/auth/index.ts +11 -1
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +49 -22
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/providers/smtp.ts +39 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/package.ts +9 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +101 -53
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +93 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +148 -20
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- package/.editorconfig +0 -19
- package/.eslintrc.json +0 -4
- package/.prettierignore +0 -0
- package/.prettierrc.json +0 -8
- package/.vscode/settings.json +0 -17
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/lib/mcp/shape.ts +0 -596
- package/src/app/pages/assets/fonts.tsx +0 -129
- package/src/app/pages/assets/index.tsx +0 -91
- package/src/app/pages/assets/logos.tsx +0 -75
- package/src/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { InlineConfig, build as viteBuild } from 'vite';
|
|
4
|
+
import { BuildInputs, withBuildInputs } from '../../../cache/build-inputs';
|
|
4
5
|
import Handoff from '../../../index';
|
|
5
6
|
import { formatDurationMs } from '../../../utils/duration';
|
|
6
7
|
import { Logger } from '../../../utils/logger';
|
|
8
|
+
import { collectStyleImports } from '../../utils/build';
|
|
7
9
|
import viteBaseConfig from '../../vite-config';
|
|
8
10
|
import { getComponentOutputPath } from '../component';
|
|
9
11
|
import { TransformComponentTokensResult } from '../types';
|
|
@@ -21,6 +23,7 @@ export const SHARED_COMPONENT_CSS_FILE = 'shared.css';
|
|
|
21
23
|
* @param options.outputFilename - The name of the output file
|
|
22
24
|
* @param options.loadPaths - Array of paths for SASS to look for imports
|
|
23
25
|
* @param options.handoff - The Handoff configuration object
|
|
26
|
+
* @param options.inputs - Collects the files that the build reads, for the build cache
|
|
24
27
|
*/
|
|
25
28
|
const buildCssBundle = async ({
|
|
26
29
|
entry,
|
|
@@ -28,12 +31,14 @@ const buildCssBundle = async ({
|
|
|
28
31
|
outputFilename,
|
|
29
32
|
loadPaths,
|
|
30
33
|
handoff,
|
|
34
|
+
inputs,
|
|
31
35
|
}: {
|
|
32
36
|
entry: string;
|
|
33
37
|
outputPath: string;
|
|
34
38
|
outputFilename: string;
|
|
35
39
|
loadPaths: string[];
|
|
36
40
|
handoff: Handoff;
|
|
41
|
+
inputs?: BuildInputs;
|
|
37
42
|
}): Promise<void> => {
|
|
38
43
|
// Store the current NODE_ENV value
|
|
39
44
|
const oldNodeEnv = process.env.NODE_ENV;
|
|
@@ -84,7 +89,7 @@ const buildCssBundle = async ({
|
|
|
84
89
|
viteConfig = handoff.config.hooks.cssBuildConfig(viteConfig);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
await viteBuild(viteConfig);
|
|
92
|
+
await viteBuild(withBuildInputs(viteConfig, inputs));
|
|
88
93
|
} catch (e) {
|
|
89
94
|
Logger.error(`Failed to build CSS for "${entry}"`);
|
|
90
95
|
throw e;
|
|
@@ -98,14 +103,56 @@ const buildCssBundle = async ({
|
|
|
98
103
|
}
|
|
99
104
|
};
|
|
100
105
|
|
|
101
|
-
const
|
|
106
|
+
const STYLE_SOURCE_EXTENSIONS = new Set(['.scss', '.sass', '.css']);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The stylesheets of one item, in cascade order and deduplicated by resolved path.
|
|
110
|
+
*
|
|
111
|
+
* Discovered imports come first and the declared entry last, so a declared entry can override what
|
|
112
|
+
* the component imports. A project that both declares and imports one file must build it once.
|
|
113
|
+
*/
|
|
114
|
+
const resolveStyleSources = async (data: TransformComponentTokensResult, handoff: Handoff): Promise<string[]> => {
|
|
115
|
+
const template = data.entries?.template;
|
|
116
|
+
const discovered = template ? await collectStyleImports(path.resolve(template), handoff) : [];
|
|
117
|
+
const declared = data.entries?.scss ? [path.resolve(data.entries.scss)] : [];
|
|
118
|
+
|
|
119
|
+
const ordered: string[] = [];
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
|
|
122
|
+
for (const source of [...discovered, ...declared]) {
|
|
123
|
+
if (!seen.has(source) && STYLE_SOURCE_EXTENSIONS.has(path.extname(source)) && fs.existsSync(source)) {
|
|
124
|
+
seen.add(source);
|
|
125
|
+
ordered.push(source);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return ordered;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A single Sass entry that `@use`s every source in order.
|
|
134
|
+
*
|
|
135
|
+
* `@use` emits a repeated file once and inlines a plain `.css` source, which `@import` would leave
|
|
136
|
+
* as an unresolvable URL. Namespaces are explicit because two sources sharing a basename would
|
|
137
|
+
* claim the same default namespace and fail the compile.
|
|
138
|
+
*/
|
|
139
|
+
const writeCombinedStyleEntry = async (sources: string[], outputPath: string, id: string): Promise<string> => {
|
|
140
|
+
const entryPath = path.resolve(outputPath, `.${id}.entry.scss`);
|
|
141
|
+
const rules = sources.map((source, index) => `@use '${source.split(path.sep).join('/')}' as ns${index};`);
|
|
142
|
+
|
|
143
|
+
await fs.outputFile(entryPath, `${rules.join('\n')}\n`);
|
|
144
|
+
|
|
145
|
+
return entryPath;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const buildComponentCss = async (data: TransformComponentTokensResult, handoff: Handoff, inputs?: BuildInputs) => {
|
|
102
149
|
const id = data.id;
|
|
103
150
|
Logger.debug(`buildComponentCss`, id);
|
|
104
151
|
const outputPath = getComponentOutputPath(handoff);
|
|
105
152
|
const builtCssPath = path.resolve(outputPath, `${id}.css`);
|
|
106
|
-
const
|
|
153
|
+
const sources = await resolveStyleSources(data, handoff);
|
|
107
154
|
|
|
108
|
-
if (
|
|
155
|
+
if (sources.length === 0) {
|
|
109
156
|
// Keep generated output aligned with the current component declaration.
|
|
110
157
|
await fs.remove(builtCssPath);
|
|
111
158
|
delete data['css'];
|
|
@@ -114,64 +161,65 @@ const buildComponentCss = async (data: TransformComponentTokensResult, handoff:
|
|
|
114
161
|
return data;
|
|
115
162
|
}
|
|
116
163
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return data;
|
|
121
|
-
}
|
|
164
|
+
// A single source compiles directly, so its relative asset URLs and documented Sass stay as authored.
|
|
165
|
+
const combinedEntry = sources.length > 1 ? await writeCombinedStyleEntry(sources, outputPath, id) : undefined;
|
|
166
|
+
const entry = combinedEntry ?? sources[0];
|
|
122
167
|
|
|
123
168
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
169
|
+
// Remove the previous artifact before rebuilding so "no emitted CSS"
|
|
170
|
+
// doesn't silently preserve stale output from an earlier build.
|
|
171
|
+
await fs.remove(builtCssPath);
|
|
172
|
+
|
|
173
|
+
// Read the original source
|
|
174
|
+
const primarySource = data.entries?.scss ? path.resolve(data.entries.scss) : sources[0];
|
|
175
|
+
if (path.extname(primarySource) === '.scss') {
|
|
176
|
+
data['sass'] = await fs.readFile(primarySource, 'utf8');
|
|
177
|
+
}
|
|
134
178
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
179
|
+
// Setup SASS load paths
|
|
180
|
+
const loadPaths = [
|
|
181
|
+
path.resolve(handoff.workingPath),
|
|
182
|
+
handoff.getOutputPath(),
|
|
183
|
+
path.resolve(handoff.workingPath, 'node_modules'),
|
|
184
|
+
];
|
|
141
185
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
186
|
+
if (handoff.runtimeConfig?.entries?.scss) {
|
|
187
|
+
loadPaths.unshift(path.dirname(handoff.runtimeConfig.entries.scss));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await buildCssBundle({
|
|
191
|
+
entry,
|
|
192
|
+
outputPath,
|
|
193
|
+
outputFilename: `${id}.css`,
|
|
194
|
+
loadPaths,
|
|
195
|
+
handoff,
|
|
196
|
+
inputs,
|
|
197
|
+
});
|
|
145
198
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
data['
|
|
158
|
-
|
|
159
|
-
// Handle shared styles — if the component SCSS uses the split marker,
|
|
160
|
-
// extract shared styles from the portion above it
|
|
161
|
-
const splitCSS = builtCss.split('/* COMPONENT STYLES*/');
|
|
162
|
-
if (splitCSS && splitCSS.length > 1) {
|
|
163
|
-
data['css'] = splitCSS[1];
|
|
164
|
-
data['sharedStyles'] = splitCSS[0];
|
|
165
|
-
await fs.writeFile(path.resolve(outputPath, SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
delete data['css'];
|
|
169
|
-
delete data['sharedStyles'];
|
|
199
|
+
// Read the built CSS
|
|
200
|
+
if (fs.existsSync(builtCssPath)) {
|
|
201
|
+
const builtCss = await fs.readFile(builtCssPath, 'utf8');
|
|
202
|
+
data['css'] = builtCss;
|
|
203
|
+
|
|
204
|
+
// Handle shared styles — if the component SCSS uses the split marker,
|
|
205
|
+
// extract shared styles from the portion above it
|
|
206
|
+
const splitCSS = builtCss.split('/* COMPONENT STYLES*/');
|
|
207
|
+
if (splitCSS && splitCSS.length > 1) {
|
|
208
|
+
data['css'] = splitCSS[1];
|
|
209
|
+
data['sharedStyles'] = splitCSS[0];
|
|
210
|
+
await fs.writeFile(path.resolve(outputPath, SHARED_COMPONENT_CSS_FILE), data['sharedStyles']);
|
|
170
211
|
}
|
|
212
|
+
} else {
|
|
213
|
+
delete data['css'];
|
|
214
|
+
delete data['sharedStyles'];
|
|
171
215
|
}
|
|
172
216
|
} catch (e) {
|
|
173
217
|
Logger.error(`Failed to build CSS for "${id}"`);
|
|
174
218
|
throw e;
|
|
219
|
+
} finally {
|
|
220
|
+
if (combinedEntry) {
|
|
221
|
+
await fs.remove(combinedEntry);
|
|
222
|
+
}
|
|
175
223
|
}
|
|
176
224
|
|
|
177
225
|
return data;
|
|
@@ -199,7 +247,7 @@ export const buildMainCss = async (handoff: Handoff): Promise<void> => {
|
|
|
199
247
|
const loadPaths = [
|
|
200
248
|
path.dirname(scssEntry),
|
|
201
249
|
path.resolve(handoff.workingPath),
|
|
202
|
-
|
|
250
|
+
handoff.getOutputPath(),
|
|
203
251
|
path.resolve(handoff.workingPath, 'node_modules'),
|
|
204
252
|
];
|
|
205
253
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import react from '@vitejs/plugin-react';
|
|
2
2
|
import { Types as CoreTypes } from 'handoff-core';
|
|
3
3
|
import { InlineConfig, PluginOption, build as viteBuild } from 'vite';
|
|
4
|
+
import { BuildInputs, withBuildInputs } from '../../../cache/build-inputs';
|
|
4
5
|
import type { RendererKind, SourceFormat } from '../../../catalog/renderers';
|
|
5
6
|
import Handoff from '../../../index';
|
|
6
7
|
import { Logger } from '../../../utils/logger';
|
|
@@ -35,6 +36,7 @@ const RENDERER_PLUGINS: Record<RendererKind, PreviewPlugins> = {
|
|
|
35
36
|
* @param data - The result of transforming component tokens.
|
|
36
37
|
* @param handoff - The Handoff configuration object.
|
|
37
38
|
* @param components - Optional file components object.
|
|
39
|
+
* @param inputs - Collects the files that the build reads, for the build cache.
|
|
38
40
|
* @returns A promise that resolves to the transformed component tokens result.
|
|
39
41
|
* @throws Will throw an error if the Vite build process fails.
|
|
40
42
|
*
|
|
@@ -46,7 +48,8 @@ const RENDERER_PLUGINS: Record<RendererKind, PreviewPlugins> = {
|
|
|
46
48
|
export const buildPreviews = async (
|
|
47
49
|
data: TransformComponentTokensResult,
|
|
48
50
|
handoff: Handoff,
|
|
49
|
-
components?: CoreTypes.IDocumentationObject['components']
|
|
51
|
+
components?: CoreTypes.IDocumentationObject['components'],
|
|
52
|
+
inputs?: BuildInputs
|
|
50
53
|
): Promise<TransformComponentTokensResult> => {
|
|
51
54
|
if (!data.entries?.template) return data;
|
|
52
55
|
|
|
@@ -87,8 +90,9 @@ export const buildPreviews = async (
|
|
|
87
90
|
viteConfig = handoff.config.hooks.htmlBuildConfig(viteConfig);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
await viteBuild(viteConfig);
|
|
93
|
+
await viteBuild(withBuildInputs(viteConfig, inputs));
|
|
91
94
|
} catch (error) {
|
|
95
|
+
if (inputs) inputs.failed = true;
|
|
92
96
|
Logger.error(`Error building component previews: ${data.entries.template}`, error);
|
|
93
97
|
} finally {
|
|
94
98
|
// Restore the original NODE_ENV value after vite build completes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { InlineConfig, build as viteBuild } from 'vite';
|
|
4
|
+
import { BuildInputs, withBuildInputs } from '../../../cache/build-inputs';
|
|
4
5
|
import Handoff from '../../../index';
|
|
5
6
|
import { formatDurationMs } from '../../../utils/duration';
|
|
6
7
|
import { Logger } from '../../../utils/logger';
|
|
@@ -17,6 +18,7 @@ export const MAIN_COMPONENT_JS_FILE = 'main.js';
|
|
|
17
18
|
* @param options.entry - The entry file path for the bundle
|
|
18
19
|
* @param options.outputPath - The directory where the bundle will be output
|
|
19
20
|
* @param options.outputFilename - The name of the output file
|
|
21
|
+
* @param inputs - Collects the files that the build reads, for the build cache
|
|
20
22
|
*/
|
|
21
23
|
const buildJsBundle = async (
|
|
22
24
|
{
|
|
@@ -25,7 +27,8 @@ const buildJsBundle = async (
|
|
|
25
27
|
outputFilename,
|
|
26
28
|
format = 'cjs',
|
|
27
29
|
}: { entry: string; outputPath: string; outputFilename: string; format?: 'cjs' | 'iife' },
|
|
28
|
-
handoff: Handoff
|
|
30
|
+
handoff: Handoff,
|
|
31
|
+
inputs?: BuildInputs
|
|
29
32
|
) => {
|
|
30
33
|
const absEntryPath = path.resolve(entry);
|
|
31
34
|
|
|
@@ -62,7 +65,7 @@ const buildJsBundle = async (
|
|
|
62
65
|
viteConfig = handoff.config.hooks.jsBuildConfig(viteConfig);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
await viteBuild(viteConfig);
|
|
68
|
+
await viteBuild(withBuildInputs(viteConfig, inputs));
|
|
66
69
|
} finally {
|
|
67
70
|
// Restore the original NODE_ENV value after vite build completes
|
|
68
71
|
// This prevents interference with Next.js app building/running processes
|
|
@@ -84,7 +87,11 @@ const buildJsBundle = async (
|
|
|
84
87
|
* @param handoff - The Handoff configuration object
|
|
85
88
|
* @returns The updated component transformation result with JavaScript data
|
|
86
89
|
*/
|
|
87
|
-
export const buildComponentJs = async (
|
|
90
|
+
export const buildComponentJs = async (
|
|
91
|
+
data: TransformComponentTokensResult,
|
|
92
|
+
handoff: Handoff,
|
|
93
|
+
inputs?: BuildInputs
|
|
94
|
+
): Promise<TransformComponentTokensResult> => {
|
|
88
95
|
const id = data.id;
|
|
89
96
|
const outputPath = getComponentOutputPath(handoff);
|
|
90
97
|
const builtJsPath = path.resolve(outputPath, `${id}.js`);
|
|
@@ -111,9 +118,11 @@ export const buildComponentJs = async (data: TransformComponentTokensResult, han
|
|
|
111
118
|
outputPath,
|
|
112
119
|
outputFilename: `${id}.js`,
|
|
113
120
|
},
|
|
114
|
-
handoff
|
|
121
|
+
handoff,
|
|
122
|
+
inputs
|
|
115
123
|
);
|
|
116
124
|
} catch (e) {
|
|
125
|
+
if (inputs) inputs.failed = true;
|
|
117
126
|
Logger.error(`Failed to bundle JS for component "${id}" (${id}.js):`, e);
|
|
118
127
|
return data;
|
|
119
128
|
}
|
|
@@ -126,6 +135,7 @@ export const buildComponentJs = async (data: TransformComponentTokensResult, han
|
|
|
126
135
|
delete data['jsCompiled'];
|
|
127
136
|
}
|
|
128
137
|
} catch (e) {
|
|
138
|
+
if (inputs) inputs.failed = true;
|
|
129
139
|
Logger.error(`JS build failed for component "${id}":`, e);
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -56,7 +56,9 @@ export const composePatternHtml = (
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// React SSR separates adjacent text children with `<!-- -->`. The parser drops comments by
|
|
60
|
+
// default, which would make every composed React fragment fail to hydrate.
|
|
61
|
+
const doc = parse(html, { comment: true });
|
|
60
62
|
const suffix = `_p${i}`;
|
|
61
63
|
const namespacedRootId = `root${suffix}`;
|
|
62
64
|
const namespacedPropsId = `__APP_PROPS__${suffix}`;
|
|
@@ -18,6 +18,9 @@ export interface BasePageSlice {
|
|
|
18
18
|
type: PageSliceType;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** A fluid preview renders at the width the reader's stage has, so it is never scaled down. */
|
|
22
|
+
export type PreviewWidth = 'fluid' | number;
|
|
23
|
+
|
|
21
24
|
export interface BestPracticesPageSlice extends BasePageSlice {
|
|
22
25
|
type: 'BEST_PRACTICES';
|
|
23
26
|
}
|
|
@@ -27,6 +30,8 @@ export interface ComponentDisplayPageSlice extends BasePageSlice {
|
|
|
27
30
|
showPreview?: boolean;
|
|
28
31
|
showCodeHighlight?: boolean;
|
|
29
32
|
defaultHeight?: string;
|
|
33
|
+
/** Viewport width for the preview, in pixels. Defaults to 'fluid'. */
|
|
34
|
+
defaultWidth?: PreviewWidth;
|
|
30
35
|
filterBy?: Filter;
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -115,6 +120,8 @@ export type ComponentObject = {
|
|
|
115
120
|
story?: string;
|
|
116
121
|
/** Optional path to schema file (if available) */
|
|
117
122
|
schema?: string;
|
|
123
|
+
/** Declaration file path; a preview `render` is read from it at build and hydration time. */
|
|
124
|
+
declaration?: string;
|
|
118
125
|
/** Optional path to templates directory */
|
|
119
126
|
templates?: string;
|
|
120
127
|
};
|
|
@@ -192,6 +199,8 @@ export type TransformComponentTokensResult = {
|
|
|
192
199
|
story?: string;
|
|
193
200
|
schema?: string;
|
|
194
201
|
templates?: string;
|
|
202
|
+
/** See `ComponentObject.entries.declaration`. */
|
|
203
|
+
declaration?: string;
|
|
195
204
|
};
|
|
196
205
|
renderer?: RendererKind;
|
|
197
206
|
/** See `ComponentObject.sourceFormat`. Distinct from `format`, the preview's code language. */
|
|
@@ -212,6 +221,11 @@ export type OptionalPreviewRender = {
|
|
|
212
221
|
values: { [key: string]: string | string[] | any };
|
|
213
222
|
url: string;
|
|
214
223
|
usage?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Build-time only: the server renders it and the client re-reads it from the declaration module.
|
|
226
|
+
* `JSON.stringify` drops it from published artifacts.
|
|
227
|
+
*/
|
|
228
|
+
render?: (args: Record<string, any>) => unknown;
|
|
215
229
|
/** Optional source preview/story key used to derive this preview */
|
|
216
230
|
sourcePreview?: string;
|
|
217
231
|
};
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import esbuild from 'esbuild';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
+
const STYLE_EXTENSIONS = ['.css', '.scss', '.sass'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keeps co-located stylesheet imports out of the JavaScript graph; the CSS pipeline compiles them.
|
|
8
|
+
*
|
|
9
|
+
* esbuild has no `.scss` loader, and a `.css` import fails too because these builds run with
|
|
10
|
+
* `write: false` and no output path. `empty` rather than a plugin: `esbuild.buildSync` rejects
|
|
11
|
+
* plugins, and the declaration and config loaders are synchronous.
|
|
12
|
+
*
|
|
13
|
+
* CSS Modules are not covered — the empty module leaves the class map undefined.
|
|
14
|
+
*/
|
|
15
|
+
export const STYLE_IMPORT_LOADERS: Record<string, esbuild.Loader> = Object.fromEntries(
|
|
16
|
+
STYLE_EXTENSIONS.map((extension) => [extension, 'empty' as esbuild.Loader])
|
|
17
|
+
);
|
|
18
|
+
|
|
4
19
|
/**
|
|
5
20
|
* Default esbuild configuration for SSR builds
|
|
6
21
|
*/
|
|
@@ -11,6 +26,7 @@ export const DEFAULT_SSR_BUILD_CONFIG: esbuild.BuildOptions = {
|
|
|
11
26
|
platform: 'node',
|
|
12
27
|
jsx: 'automatic',
|
|
13
28
|
external: ['react', 'react-dom', '@opentelemetry/api'],
|
|
29
|
+
loader: STYLE_IMPORT_LOADERS,
|
|
14
30
|
};
|
|
15
31
|
|
|
16
32
|
/**
|
|
@@ -24,8 +40,85 @@ export const DEFAULT_CLIENT_BUILD_CONFIG: esbuild.BuildOptions = {
|
|
|
24
40
|
jsx: 'automatic',
|
|
25
41
|
sourcemap: false,
|
|
26
42
|
minify: false,
|
|
43
|
+
loader: STYLE_IMPORT_LOADERS,
|
|
27
44
|
};
|
|
28
45
|
|
|
46
|
+
/** Apply after a consumer hook: a hook that replaces `loader` would otherwise drop these. */
|
|
47
|
+
export function withStyleLoaders<T extends esbuild.BuildOptions>(config: T): T {
|
|
48
|
+
return { ...config, loader: { ...config.loader, ...STYLE_IMPORT_LOADERS } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stylesheets of the module graph in cascade order: every import of a module before the module
|
|
53
|
+
* itself, siblings in source order.
|
|
54
|
+
*
|
|
55
|
+
* This is the order a bundler emits CSS in, and the cascade depends on it. A component that
|
|
56
|
+
* overrides a rule of a component it imports ties that rule on specificity, so the later of the
|
|
57
|
+
* two wins. esbuild's `onLoad` callbacks do not give this order: they fire roughly breadth-first,
|
|
58
|
+
* which puts a component's own stylesheet ahead of the ones it imports and reverses every such
|
|
59
|
+
* override.
|
|
60
|
+
*/
|
|
61
|
+
function orderStyleImports(metafile: esbuild.Metafile, entryPath: string): string[] {
|
|
62
|
+
const entryKey = Object.keys(metafile.inputs).find((key) => path.resolve(key) === entryPath);
|
|
63
|
+
if (!entryKey) return [];
|
|
64
|
+
|
|
65
|
+
const ordered: string[] = [];
|
|
66
|
+
const visited = new Set<string>();
|
|
67
|
+
|
|
68
|
+
const visit = (key: string) => {
|
|
69
|
+
if (visited.has(key)) return;
|
|
70
|
+
visited.add(key);
|
|
71
|
+
|
|
72
|
+
for (const imported of metafile.inputs[key]?.imports ?? []) {
|
|
73
|
+
if (!imported.external && metafile.inputs[imported.path]) visit(imported.path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (STYLE_EXTENSIONS.includes(path.extname(key))) ordered.push(path.resolve(key));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
visit(entryKey);
|
|
80
|
+
|
|
81
|
+
return ordered;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stylesheet paths imported anywhere in a component's module graph, in cascade order.
|
|
86
|
+
*
|
|
87
|
+
* An unbuildable graph yields an empty list rather than throwing, leaving the CSS step with the
|
|
88
|
+
* declared entry.
|
|
89
|
+
*/
|
|
90
|
+
export async function collectStyleImports(entryPath: string, handoff: any): Promise<string[]> {
|
|
91
|
+
const metafile = await buildModuleGraph(entryPath, handoff);
|
|
92
|
+
return metafile ? orderStyleImports(metafile, entryPath) : [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Every file in the module graph of an entry, stylesheets included, or `null` when the graph does
|
|
97
|
+
* not build.
|
|
98
|
+
*/
|
|
99
|
+
export async function collectModuleInputs(entryPath: string, handoff: any): Promise<string[] | null> {
|
|
100
|
+
const metafile = await buildModuleGraph(entryPath, handoff);
|
|
101
|
+
return metafile ? Object.keys(metafile.inputs).map((input) => path.resolve(input)) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function buildModuleGraph(entryPath: string, handoff: any): Promise<esbuild.Metafile | null> {
|
|
105
|
+
const baseConfig: esbuild.BuildOptions = {
|
|
106
|
+
...DEFAULT_SSR_BUILD_CONFIG,
|
|
107
|
+
entryPoints: [entryPath],
|
|
108
|
+
logLevel: 'silent',
|
|
109
|
+
};
|
|
110
|
+
const hookedConfig = handoff?.config?.hooks?.ssrBuildConfig ? handoff.config.hooks.ssrBuildConfig(baseConfig) : baseConfig;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// The `empty` loader keeps stylesheets out of the JavaScript, and the metafile still records
|
|
114
|
+
// them as inputs, so the graph carries the order.
|
|
115
|
+
const result = await esbuild.build({ ...withStyleLoaders(hookedConfig), metafile: true });
|
|
116
|
+
return result.metafile ?? null;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
29
122
|
/**
|
|
30
123
|
* Resolves a module from multiple search directories
|
|
31
124
|
* @param id - Module ID to resolve
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import esbuild from 'esbuild';
|
|
2
2
|
import { ModuleEvaluationResult } from '../types';
|
|
3
|
-
import { DEFAULT_SSR_BUILD_CONFIG } from './build';
|
|
3
|
+
import { DEFAULT_SSR_BUILD_CONFIG, withStyleLoaders } from './build';
|
|
4
4
|
|
|
5
5
|
function createModuleBuildConfig(entryPath: string, handoff: any): esbuild.BuildOptions {
|
|
6
6
|
const defaultBuildConfig: esbuild.BuildOptions = {
|
|
@@ -8,9 +8,11 @@ function createModuleBuildConfig(entryPath: string, handoff: any): esbuild.Build
|
|
|
8
8
|
entryPoints: [entryPath],
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const hookedBuildConfig = handoff.config?.hooks?.ssrBuildConfig
|
|
12
12
|
? handoff.config.hooks.ssrBuildConfig(defaultBuildConfig)
|
|
13
13
|
: defaultBuildConfig;
|
|
14
|
+
|
|
15
|
+
return withStyleLoaders(hookedBuildConfig);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function evaluateBuiltModule(code: string): ModuleEvaluationResult {
|