design-embed 0.1.0 → 0.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 (45) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +98 -2
  3. package/dist/cli.d.mts +1 -0
  4. package/dist/cli.mjs +273 -0
  5. package/dist/core-BLV62TaX.mjs +907 -0
  6. package/dist/index.d.mts +273 -0
  7. package/dist/index.mjs +2 -0
  8. package/package.json +6 -19
  9. package/src/cli.ts +8 -16
  10. package/src/commands/compile.ts +25 -110
  11. package/src/commands/generateTests.ts +14 -96
  12. package/src/commands/init.ts +52 -55
  13. package/src/commands/plugin.ts +6 -21
  14. package/src/config/index.ts +302 -0
  15. package/{node_modules/@design-embed/core/src → src/core}/index.ts +151 -163
  16. package/src/core/nodes.ts +74 -0
  17. package/src/core/plugins/pluginApi.ts +44 -0
  18. package/src/core/types.ts +120 -0
  19. package/src/index.ts +48 -2
  20. package/src/targets/html.ts +621 -0
  21. package/dist/args.js +0 -36
  22. package/dist/cli.js +0 -35
  23. package/dist/commands/check.js +0 -4
  24. package/dist/commands/compile.js +0 -157
  25. package/dist/commands/generateTests.js +0 -113
  26. package/dist/commands/init.js +0 -102
  27. package/dist/commands/plugin.js +0 -68
  28. package/dist/index.js +0 -2
  29. package/node_modules/@design-embed/config/README.md +0 -5
  30. package/node_modules/@design-embed/config/dist/index.js +0 -283
  31. package/node_modules/@design-embed/config/package.json +0 -19
  32. package/node_modules/@design-embed/config/src/index.ts +0 -518
  33. package/node_modules/@design-embed/core/README.md +0 -5
  34. package/node_modules/@design-embed/core/dist/diagnostics/diagnostic.js +0 -3
  35. package/node_modules/@design-embed/core/dist/diagnostics/jsonDiagnostic.js +0 -35
  36. package/node_modules/@design-embed/core/dist/index.js +0 -351
  37. package/node_modules/@design-embed/core/dist/pipeline/checkMode.js +0 -29
  38. package/node_modules/@design-embed/core/dist/plugins/pluginApi.js +0 -1
  39. package/node_modules/@design-embed/core/dist/plugins/pluginRegistry.js +0 -25
  40. package/node_modules/@design-embed/core/package.json +0 -19
  41. package/node_modules/@design-embed/core/src/plugins/pluginApi.ts +0 -78
  42. package/node_modules/@design-embed/core/src/plugins/pluginRegistry.ts +0 -37
  43. /package/{node_modules/@design-embed/core/src → src/core}/diagnostics/diagnostic.ts +0 -0
  44. /package/{node_modules/@design-embed/core/src → src/core}/diagnostics/jsonDiagnostic.ts +0 -0
  45. /package/{node_modules/@design-embed/core/src → src/core}/pipeline/checkMode.ts +0 -0
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Jin-Woo Lee
3
+ Copyright (c) 2026 Jin Woo Lee
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
package/README.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # design-embed
2
2
 
3
- The command-line interface for design-embed.
3
+ `design-embed` is the command-line workflow for compiling exported design into files that can live inside an application repository.
4
4
 
5
- It runs local HTML/CSS compilation, writes generated output, checks whether generated files are current, and invokes explicit source plugins such as `figma-html`. The CLI is the user-facing workflow layer around the core compiler: it loads config, reads design input, and formats diagnostics for humans or CI.
5
+ The package provides:
6
+
7
+ - a built-in deterministic HTML output for quick inspection
8
+ - config loading and validation
9
+ - generated-file writing and check mode
10
+ - explicit source-plugin orchestration
11
+ - target-adapter orchestration for framework output such as React
12
+
13
+ Without a target adapter, `design-embed` only emits HTML. Framework packages
14
+ such as `@design-embed/target-react` are separate adapters that must be wired
15
+ into your config.
16
+
17
+ ## Quick Start
18
+
19
+ Create a design HTML file:
20
+
21
+ ```html
22
+ <!-- target.html -->
23
+ <section style="padding: 24px; background: #f8fafc;">
24
+ <h1 style="font-size: 32px;">Welcome</h1>
25
+ <button data-role="primary">Get started</button>
26
+ </section>
27
+ ```
28
+
29
+ Run the compiler:
30
+
31
+ ```bash
32
+ npx design-embed target.html
33
+ ```
34
+
35
+ By default this uses the built-in HTML target and writes:
36
+
37
+ ```text
38
+ src/generated/views/debug.html
39
+ ```
40
+
41
+ When `components` are configured with the HTML target, two additional files are generated for each view alongside the HTML:
42
+
43
+ - `ViewName.ts` — a native web component scaffold (`HTMLElement` subclass with `observedAttributes`, lifecycle hooks, and a `render()` method; no React dependency)
44
+ - `ViewName.html` — includes `<script type="module" src="./ViewName.js"></script>` at the bottom
45
+
46
+ ## React Adapter Example
47
+
48
+ Install the React target adapter alongside `design-embed`:
49
+
50
+ ```bash
51
+ pnpm add design-embed @design-embed/target-react
52
+ ```
53
+
54
+ Create a config file:
55
+
56
+ ```ts
57
+ // design-embed.config.ts
58
+ import { defineConfig } from "design-embed";
59
+ import { ReactTarget } from "@design-embed/target-react";
60
+
61
+ export default defineConfig({
62
+ output: {
63
+ target: new ReactTarget(),
64
+ viewName: "WelcomeHero",
65
+ viewsDir: "src/components",
66
+ assembliesDir: "src/pages",
67
+ styleMode: "inline",
68
+ },
69
+ tests: {
70
+ outputDir: "tests",
71
+ },
72
+ });
73
+ ```
74
+
75
+ Run the compiler with the config:
76
+
77
+ ```bash
78
+ npx design-embed target.html --config design-embed.config.ts
79
+ ```
80
+
81
+ This writes React output through the adapter:
82
+
83
+ ```text
84
+ src/components/WelcomeHero.view.tsx
85
+ src/pages/WelcomeHeroPage.tsx
86
+ tests/WelcomeHero.reference.html
87
+ tests/WelcomeHero.visual.spec.tsx
88
+ ```
89
+
90
+ Skip adapter-provided test generation with:
91
+
92
+ ```bash
93
+ npx design-embed target.html --config design-embed.config.ts --no-test
94
+ ```
95
+
96
+ ## How It Fits Together
97
+
98
+ `design-embed` owns the user-facing CLI flow: it loads config, reads input,
99
+ selects the built-in HTML target or a configured target adapter, writes files,
100
+ and formats diagnostics. The framework-specific behavior stays in target
101
+ packages that implement the shared target interface.
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ import { a as formatDiagnosticText, d as loadConfig, i as checkGeneratedFiles, n as embed, o as toJsonDiagnostics } from "./core-BLV62TaX.mjs";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ //#region packages/design-embed/src/args.ts
6
+ function parseArgs(args) {
7
+ const positionals = [];
8
+ const flags = {};
9
+ for (let index = 0; index < args.length; index += 1) {
10
+ const value = args[index];
11
+ if (!value?.startsWith("--")) {
12
+ if (value) positionals.push(value);
13
+ continue;
14
+ }
15
+ const next = args[index + 1];
16
+ if (!next || next.startsWith("--")) {
17
+ flags[value] = true;
18
+ continue;
19
+ }
20
+ flags[value] = next;
21
+ index += 1;
22
+ }
23
+ const [command = "compile", ...rest] = positionals;
24
+ return {
25
+ command,
26
+ positionals: rest,
27
+ flags
28
+ };
29
+ }
30
+ function getStringFlag(flags, name) {
31
+ const value = flags[name];
32
+ return typeof value === "string" ? value : void 0;
33
+ }
34
+ function getBooleanFlag(flags, name) {
35
+ return flags[name] === true;
36
+ }
37
+ function getFormat(flags) {
38
+ return flags["--format"] === "json" ? "json" : "text";
39
+ }
40
+ //#endregion
41
+ //#region packages/design-embed/src/commands/compile.ts
42
+ async function runCompileCommand(flags, options = {}) {
43
+ const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
44
+ const explicitConfigPath = getStringFlag(flags, "--config");
45
+ const defaultConfigPath = resolve(cwd, "design-embed.config.ts");
46
+ const configPath = explicitConfigPath ?? (existsSync(defaultConfigPath) ? "design-embed.config.ts" : void 0);
47
+ const quiet = getBooleanFlag(flags, "--quiet");
48
+ const format = getFormat(flags);
49
+ const diagnostics = [];
50
+ if (!configPath) {
51
+ diagnostics.push({
52
+ code: "CONFIG_REQUIRED",
53
+ message: "No config file found. Create design-embed.config.ts or use --config.",
54
+ severity: "error"
55
+ });
56
+ printDiagnostics(diagnostics, format, quiet);
57
+ return 2;
58
+ }
59
+ const configResult = await loadConfig(configPath, cwd);
60
+ diagnostics.push(...configResult.diagnostics);
61
+ const config = configResult.config;
62
+ if (hasErrors$1(diagnostics)) {
63
+ printDiagnostics(diagnostics, format, quiet);
64
+ return 2;
65
+ }
66
+ const isCheckMode = options.check && !getBooleanFlag(flags, "--write");
67
+ const result = await embed({
68
+ config,
69
+ cwd,
70
+ dryRun: isCheckMode,
71
+ generateTests: !getBooleanFlag(flags, "--no-test")
72
+ });
73
+ diagnostics.push(...result.diagnostics);
74
+ if (hasErrors$1(diagnostics)) {
75
+ printDiagnostics(diagnostics, format, quiet);
76
+ return 2;
77
+ }
78
+ if (isCheckMode) {
79
+ const checkResult = checkGeneratedFiles({
80
+ cwd,
81
+ files: result.files,
82
+ readFile(path) {
83
+ return existsSync(path) ? readFileSync(path, "utf-8") : void 0;
84
+ }
85
+ });
86
+ diagnostics.push(...checkResult.diagnostics);
87
+ printDiagnostics(diagnostics, format, quiet);
88
+ return checkResult.ok ? 0 : 3;
89
+ }
90
+ printDiagnostics(diagnostics, format, quiet);
91
+ if (!quiet && format === "text") console.log(`Success. Generated ${result.files.length} file(s).`);
92
+ return 0;
93
+ }
94
+ function printDiagnostics(diagnostics, format, quiet) {
95
+ if (format === "json") {
96
+ console.log(JSON.stringify({ diagnostics: toJsonDiagnostics(diagnostics) }, null, 2));
97
+ return;
98
+ }
99
+ if (quiet) return;
100
+ for (const diagnostic of diagnostics) {
101
+ const output = formatDiagnosticText(diagnostic);
102
+ if (diagnostic.severity === "error") console.error(output);
103
+ else console.warn(output);
104
+ }
105
+ }
106
+ function hasErrors$1(diagnostics) {
107
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
108
+ }
109
+ //#endregion
110
+ //#region packages/design-embed/src/commands/check.ts
111
+ async function runCheckCommand(flags) {
112
+ return runCompileCommand(flags, { check: true });
113
+ }
114
+ //#endregion
115
+ //#region packages/design-embed/src/commands/generateTests.ts
116
+ async function runGenerateTestsCommand(flags) {
117
+ const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
118
+ const configPath = getStringFlag(flags, "--config") ?? "design-embed.config.ts";
119
+ const quiet = getBooleanFlag(flags, "--quiet");
120
+ const format = getFormat(flags);
121
+ const diagnostics = [];
122
+ const configResult = await loadConfig(configPath, cwd);
123
+ diagnostics.push(...configResult.diagnostics);
124
+ const config = configResult.config;
125
+ if (!config || hasErrors(diagnostics)) {
126
+ printDiagnostics(diagnostics, format, quiet);
127
+ return 2;
128
+ }
129
+ if (!getTestGenerator(config)) {
130
+ diagnostics.push({
131
+ code: "TEST_TARGET_UNSUPPORTED",
132
+ message: "generate-tests requires output.target to be a target adapter with generateTests().",
133
+ severity: "error"
134
+ });
135
+ printDiagnostics(diagnostics, format, quiet);
136
+ return 2;
137
+ }
138
+ const result = await embed({
139
+ config,
140
+ cwd,
141
+ generateTests: true
142
+ });
143
+ diagnostics.push(...result.diagnostics);
144
+ if (hasErrors(diagnostics)) {
145
+ printDiagnostics(diagnostics, format, quiet);
146
+ return 2;
147
+ }
148
+ printDiagnostics(diagnostics, format, quiet);
149
+ if (!quiet && format === "text") console.log(`Success. Generated ${result.files.length} test file(s).`);
150
+ return 0;
151
+ }
152
+ function getTestGenerator(config) {
153
+ const target = config.output?.target;
154
+ return !!(target && target !== "html" && "generateTests" in target);
155
+ }
156
+ function hasErrors(diagnostics) {
157
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
158
+ }
159
+ //#endregion
160
+ //#region packages/design-embed/src/commands/init.ts
161
+ async function runInitCommand(flags) {
162
+ const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
163
+ const quiet = getBooleanFlag(flags, "--quiet");
164
+ const force = getBooleanFlag(flags, "--force");
165
+ const format = getFormat(flags);
166
+ const viewName = getStringFlag(flags, "--view-name") ?? "WelcomeHero";
167
+ const diagnostics = [];
168
+ const files = [{
169
+ path: "design-embed.config.ts",
170
+ contents: configTemplate(viewName)
171
+ }];
172
+ let written = 0;
173
+ for (const file of files) {
174
+ const outPath = resolve(cwd, file.path);
175
+ if (existsSync(outPath) && !force) {
176
+ diagnostics.push({
177
+ code: "INIT_FILE_EXISTS",
178
+ message: `Skipped existing file: ${file.path}. Pass --force to overwrite it.`,
179
+ severity: "warning",
180
+ file: file.path
181
+ });
182
+ continue;
183
+ }
184
+ mkdirSync(dirname(outPath), { recursive: true });
185
+ writeFileSync(outPath, file.contents, "utf-8");
186
+ written += 1;
187
+ if (!quiet && format === "text") console.log(`Wrote ${file.path}`);
188
+ }
189
+ printDiagnostics(diagnostics, format, quiet);
190
+ if (!quiet && format === "text") {
191
+ console.log(`Success. Initialized design-embed with ${written} file(s).`);
192
+ console.log("Next: pnpm exec design-embed --out ./design.html");
193
+ }
194
+ return 0;
195
+ }
196
+ function configTemplate(viewName) {
197
+ return `import {
198
+ \tdefineConfig,
199
+ \ttype SourcePlugin,
200
+ \ttype SourcePluginInput,
201
+ \ttype SourcePluginResult,
202
+ } from "design-embed";
203
+
204
+ class HtmlFetcherPlugin implements SourcePlugin {
205
+ \treadonly name = "html-fetcher";
206
+ \tprivate readonly options: { url: string };
207
+
208
+ \tconstructor(options: { url: string }) {
209
+ \t\tthis.options = options;
210
+ \t}
211
+
212
+ \tasync run(_input: SourcePluginInput): Promise<SourcePluginResult> {
213
+ \t\ttry {
214
+ \t\t\tconst response = await fetch(this.options.url);
215
+ \t\t\tif (!response.ok) {
216
+ \t\t\t\treturn {
217
+ \t\t\t\t\tdiagnostics: [
218
+ \t\t\t\t\t\t{
219
+ \t\t\t\t\t\t\tcode: "HTML_FETCH_FAILED",
220
+ \t\t\t\t\t\t\tmessage: \`Failed to fetch HTML: \${response.status} \${response.statusText}\`,
221
+ \t\t\t\t\t\t\tseverity: "error",
222
+ \t\t\t\t\t\t},
223
+ \t\t\t\t\t],
224
+ \t\t\t\t};
225
+ \t\t\t}
226
+
227
+ \t\t\treturn {
228
+ \t\t\t\thtml: await response.text(),
229
+ \t\t\t\tdiagnostics: [],
230
+ \t\t\t};
231
+ \t\t} catch (error) {
232
+ \t\t\treturn {
233
+ \t\t\t\tdiagnostics: [
234
+ \t\t\t\t\t{
235
+ \t\t\t\t\t\tcode: "HTML_FETCH_FAILED",
236
+ \t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),
237
+ \t\t\t\t\t\tseverity: "error",
238
+ \t\t\t\t\t},
239
+ \t\t\t\t],
240
+ \t\t\t};
241
+ \t\t}
242
+ \t}
243
+ }
244
+
245
+ export default defineConfig({
246
+ \tsource: new HtmlFetcherPlugin({
247
+ \t\turl: "https://www.scrapethissite.com/pages/",
248
+ \t}),
249
+ \toutput: {
250
+ \t\tviewName: "${viewName}",
251
+ \t\tviewsDir: "src/generated/views",
252
+ \t},
253
+ });
254
+ `;
255
+ }
256
+ //#endregion
257
+ //#region packages/design-embed/src/cli.ts
258
+ async function main() {
259
+ const { command, flags } = parseArgs(process.argv.slice(2));
260
+ if (command === "check") return runCheckCommand(flags);
261
+ if (command === "generate-tests") return runGenerateTestsCommand(flags);
262
+ if (command === "init") return runInitCommand(flags);
263
+ return runCompileCommand(flags);
264
+ }
265
+ main().then((code) => {
266
+ if (code !== 0) process.exit(code);
267
+ }).catch((error) => {
268
+ const message = error instanceof Error ? error.message : String(error);
269
+ console.error(`Pipeline failed: ${message}`);
270
+ process.exit(1);
271
+ });
272
+ //#endregion
273
+ export {};