design-embed 0.1.0 → 0.1.1
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/LICENSE +1 -1
- package/README.md +93 -2
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +469 -0
- package/dist/index.d.mts +400 -0
- package/dist/index.mjs +2 -0
- package/dist/src-D3fnqGCq.mjs +511 -0
- package/package.json +8 -13
- package/src/cli.ts +18 -1
- package/src/commands/compile.ts +46 -63
- package/src/commands/generateTests.ts +17 -17
- package/src/commands/init.ts +54 -54
- package/src/commands/plugin.ts +3 -7
- package/src/targets/html.ts +68 -0
- package/dist/args.js +0 -36
- package/dist/cli.js +0 -35
- package/dist/commands/check.js +0 -4
- package/dist/commands/compile.js +0 -157
- package/dist/commands/generateTests.js +0 -113
- package/dist/commands/init.js +0 -102
- package/dist/commands/plugin.js +0 -68
- package/dist/index.js +0 -2
- package/node_modules/@design-embed/config/README.md +0 -5
- package/node_modules/@design-embed/config/dist/index.js +0 -283
- package/node_modules/@design-embed/config/package.json +0 -19
- package/node_modules/@design-embed/config/src/index.ts +0 -518
- package/node_modules/@design-embed/core/README.md +0 -5
- package/node_modules/@design-embed/core/dist/diagnostics/diagnostic.js +0 -3
- package/node_modules/@design-embed/core/dist/diagnostics/jsonDiagnostic.js +0 -35
- package/node_modules/@design-embed/core/dist/index.js +0 -351
- package/node_modules/@design-embed/core/dist/pipeline/checkMode.js +0 -29
- package/node_modules/@design-embed/core/dist/plugins/pluginApi.js +0 -1
- package/node_modules/@design-embed/core/dist/plugins/pluginRegistry.js +0 -25
- package/node_modules/@design-embed/core/package.json +0 -19
- package/node_modules/@design-embed/core/src/diagnostics/diagnostic.ts +0 -18
- package/node_modules/@design-embed/core/src/diagnostics/jsonDiagnostic.ts +0 -51
- package/node_modules/@design-embed/core/src/index.ts +0 -591
- package/node_modules/@design-embed/core/src/pipeline/checkMode.ts +0 -46
- package/node_modules/@design-embed/core/src/plugins/pluginApi.ts +0 -78
- package/node_modules/@design-embed/core/src/plugins/pluginRegistry.ts +0 -37
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Jin
|
|
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,96 @@
|
|
|
1
1
|
# design-embed
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`design-embed` is the command-line workflow for compiling exported design into files that can live inside an application repository.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
## React Adapter Example
|
|
42
|
+
|
|
43
|
+
Install the React target adapter alongside `design-embed`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm add design-embed @design-embed/target-react
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Create a config file:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// design-embed.config.ts
|
|
53
|
+
import { defineConfig } from "design-embed";
|
|
54
|
+
import { reactTarget } from "@design-embed/target-react";
|
|
55
|
+
|
|
56
|
+
export default defineConfig({
|
|
57
|
+
output: {
|
|
58
|
+
target: reactTarget,
|
|
59
|
+
viewName: "WelcomeHero",
|
|
60
|
+
viewsDir: "src/components",
|
|
61
|
+
assembliesDir: "src/pages",
|
|
62
|
+
styleMode: "inline",
|
|
63
|
+
},
|
|
64
|
+
tests: {
|
|
65
|
+
outputDir: "tests",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Run the compiler with the config:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx design-embed target.html --config design-embed.config.ts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This writes React output through the adapter:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
src/components/WelcomeHero.view.tsx
|
|
80
|
+
src/pages/WelcomeHeroPage.tsx
|
|
81
|
+
tests/WelcomeHero.reference.html
|
|
82
|
+
tests/WelcomeHero.visual.spec.tsx
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Skip adapter-provided test generation with:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx design-embed target.html --config design-embed.config.ts --no-test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How It Fits Together
|
|
92
|
+
|
|
93
|
+
`design-embed` owns the user-facing CLI flow: it loads config, reads input,
|
|
94
|
+
selects the built-in HTML target or a configured target adapter, writes files,
|
|
95
|
+
and formats diagnostics. The framework-specific behavior stays in target
|
|
96
|
+
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,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { c as checkGeneratedFiles, d as toJsonDiagnostics, l as formatDiagnosticText, n as embed, p as loadConfig } from "./src-D3fnqGCq.mjs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, 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/targets/html.ts
|
|
42
|
+
function emitHtmlDebug(nodes, css) {
|
|
43
|
+
const body = nodes.map((node) => emitNode(node, 0)).join("");
|
|
44
|
+
if (!css?.trim()) return body;
|
|
45
|
+
return `<style>\n${css.trim()}\n</style>\n${body}\n`;
|
|
46
|
+
}
|
|
47
|
+
const htmlEmitter = { emit({ nodes, css, config }) {
|
|
48
|
+
return { files: [{
|
|
49
|
+
path: `${config?.output?.viewsDir ?? "src/generated/views"}/debug.html`,
|
|
50
|
+
contents: emitHtmlDebug(nodes, css)
|
|
51
|
+
}] };
|
|
52
|
+
} };
|
|
53
|
+
function emitNode(node, depth) {
|
|
54
|
+
const indent = " ".repeat(depth);
|
|
55
|
+
if (node.kind === "text") return `${indent}${escapeHtml(node.text ?? "")}\n`;
|
|
56
|
+
if (node.kind === "component") return `${indent}<${node.component}></${node.component}>\n`;
|
|
57
|
+
const attributes = Object.entries(node.attributes ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => value === "" ? name : `${name}="${escapeAttribute(value)}"`).join(" ");
|
|
58
|
+
const openTag = attributes ? `<${node.tagName} ${attributes}>` : `<${node.tagName}>`;
|
|
59
|
+
const children = node.children ?? [];
|
|
60
|
+
if (children.length === 0) return `${indent}${openTag}</${node.tagName}>\n`;
|
|
61
|
+
return `${indent}${openTag}\n${children.map((child) => emitNode(child, depth + 1)).join("")}${indent}</${node.tagName}>\n`;
|
|
62
|
+
}
|
|
63
|
+
function escapeHtml(value) {
|
|
64
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
65
|
+
}
|
|
66
|
+
function escapeAttribute(value) {
|
|
67
|
+
return escapeHtml(value).replace(/"/g, """);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region packages/design-embed/src/commands/compile.ts
|
|
71
|
+
async function runCompileCommand(flags, options = {}) {
|
|
72
|
+
const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
|
|
73
|
+
const inputPath = getStringFlag(flags, "--input") ?? getStringFlag(flags, "--");
|
|
74
|
+
const explicitConfigPath = getStringFlag(flags, "--config");
|
|
75
|
+
const defaultConfigPath = resolve(cwd, "design-embed.config.ts");
|
|
76
|
+
const configPath = explicitConfigPath ?? (existsSync(defaultConfigPath) ? "design-embed.config.ts" : void 0);
|
|
77
|
+
const quiet = getBooleanFlag(flags, "--quiet");
|
|
78
|
+
const format = getFormat(flags);
|
|
79
|
+
const generateTests = !getBooleanFlag(flags, "--no-test");
|
|
80
|
+
const diagnostics = [];
|
|
81
|
+
if (!inputPath) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
code: "INPUT_REQUIRED",
|
|
84
|
+
message: "--input is required.",
|
|
85
|
+
severity: "error"
|
|
86
|
+
});
|
|
87
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
const resolvedInputPath = resolve(cwd, inputPath);
|
|
91
|
+
if (!existsSync(resolvedInputPath)) {
|
|
92
|
+
diagnostics.push({
|
|
93
|
+
code: "INPUT_NOT_FOUND",
|
|
94
|
+
message: `Input file not found: ${resolvedInputPath}`,
|
|
95
|
+
severity: "error",
|
|
96
|
+
file: inputPath
|
|
97
|
+
});
|
|
98
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
let config;
|
|
102
|
+
if (configPath) {
|
|
103
|
+
const configResult = await loadConfig(configPath, cwd);
|
|
104
|
+
diagnostics.push(...configResult.diagnostics);
|
|
105
|
+
config = configResult.config;
|
|
106
|
+
if (hasErrors$1(diagnostics)) {
|
|
107
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const targetAdapter = getTargetAdapter(config);
|
|
112
|
+
const cssPath = getStringFlag(flags, "--css");
|
|
113
|
+
const html = readFileSync(resolvedInputPath, "utf-8");
|
|
114
|
+
const css = cssPath ? readFileSync(resolve(cwd, cssPath), "utf-8") : void 0;
|
|
115
|
+
const result = await embed({
|
|
116
|
+
html,
|
|
117
|
+
css,
|
|
118
|
+
configPath,
|
|
119
|
+
config,
|
|
120
|
+
cwd,
|
|
121
|
+
targetEmitter: targetAdapter.emitter
|
|
122
|
+
});
|
|
123
|
+
diagnostics.push(...result.diagnostics);
|
|
124
|
+
if (generateTests && targetAdapter.testGenerator) {
|
|
125
|
+
const testResult = targetAdapter.testGenerator.generateTests({
|
|
126
|
+
html,
|
|
127
|
+
css,
|
|
128
|
+
config: config ?? {},
|
|
129
|
+
diagnostics,
|
|
130
|
+
generatedFiles: result.files
|
|
131
|
+
});
|
|
132
|
+
result.files.push(...testResult.files);
|
|
133
|
+
}
|
|
134
|
+
if (hasErrors$1(diagnostics)) {
|
|
135
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
if (options.check && !getBooleanFlag(flags, "--write")) {
|
|
139
|
+
const checkResult = checkGeneratedFiles({
|
|
140
|
+
cwd,
|
|
141
|
+
files: result.files,
|
|
142
|
+
readFile(path) {
|
|
143
|
+
return existsSync(path) ? readFileSync(path, "utf-8") : void 0;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
const checkDiagnostics = checkResult.diagnostics;
|
|
147
|
+
diagnostics.push(...checkDiagnostics);
|
|
148
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
149
|
+
return checkResult.ok ? 0 : 3;
|
|
150
|
+
}
|
|
151
|
+
for (const file of result.files) {
|
|
152
|
+
const outPath = resolve(cwd, file.path);
|
|
153
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
154
|
+
writeFileSync(outPath, file.contents, "utf-8");
|
|
155
|
+
if (!quiet && format === "text") console.log(`Wrote ${file.path}`);
|
|
156
|
+
}
|
|
157
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
158
|
+
if (!quiet && format === "text") console.log(`Success. Generated ${result.files.length} file(s).`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
function printDiagnostics(diagnostics, format, quiet) {
|
|
162
|
+
if (format === "json") {
|
|
163
|
+
console.log(JSON.stringify({ diagnostics: toJsonDiagnostics(diagnostics) }, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (quiet) return;
|
|
167
|
+
for (const diagnostic of diagnostics) {
|
|
168
|
+
const output = formatDiagnosticText(diagnostic);
|
|
169
|
+
if (diagnostic.severity === "error") console.error(output);
|
|
170
|
+
else console.warn(output);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function getTargetAdapter(config) {
|
|
174
|
+
const target = config?.output?.target;
|
|
175
|
+
if (!target || target === "html") return { emitter: htmlEmitter };
|
|
176
|
+
return {
|
|
177
|
+
emitter: target,
|
|
178
|
+
testGenerator: "generateTests" in target ? target : void 0
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function hasErrors$1(diagnostics) {
|
|
182
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region packages/design-embed/src/commands/check.ts
|
|
186
|
+
async function runCheckCommand(flags) {
|
|
187
|
+
return runCompileCommand(flags, { check: true });
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region packages/design-embed/src/commands/generateTests.ts
|
|
191
|
+
async function runGenerateTestsCommand(flags) {
|
|
192
|
+
const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
|
|
193
|
+
const configPath = getStringFlag(flags, "--config") ?? "design-embed.config.ts";
|
|
194
|
+
const quiet = getBooleanFlag(flags, "--quiet");
|
|
195
|
+
const format = getFormat(flags);
|
|
196
|
+
const diagnostics = [];
|
|
197
|
+
const configResult = await loadConfig(configPath, cwd);
|
|
198
|
+
diagnostics.push(...configResult.diagnostics);
|
|
199
|
+
const config = configResult.config;
|
|
200
|
+
if (!config || hasErrors(diagnostics)) {
|
|
201
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
202
|
+
return 2;
|
|
203
|
+
}
|
|
204
|
+
const source = readConfiguredSource(config, configPath, cwd, diagnostics);
|
|
205
|
+
if (!source || hasErrors(diagnostics)) {
|
|
206
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
const testGenerator = getTestGenerator(config);
|
|
210
|
+
if (!testGenerator) {
|
|
211
|
+
diagnostics.push({
|
|
212
|
+
code: "TEST_TARGET_UNSUPPORTED",
|
|
213
|
+
message: "generate-tests requires output.target to be a target adapter with generateTests().",
|
|
214
|
+
severity: "error"
|
|
215
|
+
});
|
|
216
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
const result = testGenerator.generateTests({
|
|
220
|
+
html: source.html,
|
|
221
|
+
css: source.css,
|
|
222
|
+
config,
|
|
223
|
+
diagnostics
|
|
224
|
+
});
|
|
225
|
+
if (hasErrors(diagnostics)) {
|
|
226
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
for (const file of result.files) {
|
|
230
|
+
const outPath = resolve(cwd, file.path);
|
|
231
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
232
|
+
writeFileSync(outPath, file.contents, "utf-8");
|
|
233
|
+
if (!quiet && format === "text") console.log(`Wrote ${file.path}`);
|
|
234
|
+
}
|
|
235
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
236
|
+
if (!quiet && format === "text") console.log(`Success. Generated ${result.files.length} test file(s).`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
function readConfiguredSource(config, configPath, cwd, diagnostics) {
|
|
240
|
+
const source = config.tests?.source;
|
|
241
|
+
if (!source?.html) {
|
|
242
|
+
diagnostics.push({
|
|
243
|
+
code: "TEST_SOURCE_HTML_REQUIRED",
|
|
244
|
+
message: "tests.source.html is required for generate-tests.",
|
|
245
|
+
severity: "error"
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const configDir = dirname(resolve(cwd, configPath));
|
|
250
|
+
const htmlPath = resolveConfigPath(source.html, configDir);
|
|
251
|
+
if (!existsSync(htmlPath)) {
|
|
252
|
+
diagnostics.push({
|
|
253
|
+
code: "TEST_SOURCE_HTML_NOT_FOUND",
|
|
254
|
+
message: `Test source HTML not found: ${htmlPath}`,
|
|
255
|
+
severity: "error",
|
|
256
|
+
file: source.html
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
let css;
|
|
261
|
+
if (source.css) {
|
|
262
|
+
const cssPath = resolveConfigPath(source.css, configDir);
|
|
263
|
+
if (!existsSync(cssPath)) {
|
|
264
|
+
diagnostics.push({
|
|
265
|
+
code: "TEST_SOURCE_CSS_NOT_FOUND",
|
|
266
|
+
message: `Test source CSS not found: ${cssPath}`,
|
|
267
|
+
severity: "error",
|
|
268
|
+
file: source.css
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
css = readFileSync(cssPath, "utf-8");
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
html: readFileSync(htmlPath, "utf-8"),
|
|
276
|
+
css
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function resolveConfigPath(path, configDir) {
|
|
280
|
+
return isAbsolute(path) ? path : resolve(configDir, path);
|
|
281
|
+
}
|
|
282
|
+
function getTestGenerator(config) {
|
|
283
|
+
const target = config.output?.target;
|
|
284
|
+
return target && target !== "html" && "generateTests" in target ? target : void 0;
|
|
285
|
+
}
|
|
286
|
+
function hasErrors(diagnostics) {
|
|
287
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region packages/design-embed/src/commands/init.ts
|
|
291
|
+
async function runInitCommand(flags) {
|
|
292
|
+
const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
|
|
293
|
+
const quiet = getBooleanFlag(flags, "--quiet");
|
|
294
|
+
const force = getBooleanFlag(flags, "--force");
|
|
295
|
+
const format = getFormat(flags);
|
|
296
|
+
const viewName = getStringFlag(flags, "--view-name") ?? "WelcomeHero";
|
|
297
|
+
const diagnostics = [];
|
|
298
|
+
const files = [{
|
|
299
|
+
path: "design-embed.config.ts",
|
|
300
|
+
contents: configTemplate(viewName)
|
|
301
|
+
}];
|
|
302
|
+
let written = 0;
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
const outPath = resolve(cwd, file.path);
|
|
305
|
+
if (existsSync(outPath) && !force) {
|
|
306
|
+
diagnostics.push({
|
|
307
|
+
code: "INIT_FILE_EXISTS",
|
|
308
|
+
message: `Skipped existing file: ${file.path}. Pass --force to overwrite it.`,
|
|
309
|
+
severity: "warning",
|
|
310
|
+
file: file.path
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
315
|
+
writeFileSync(outPath, file.contents, "utf-8");
|
|
316
|
+
written += 1;
|
|
317
|
+
if (!quiet && format === "text") console.log(`Wrote ${file.path}`);
|
|
318
|
+
}
|
|
319
|
+
printDiagnostics(diagnostics, format, quiet);
|
|
320
|
+
if (!quiet && format === "text") {
|
|
321
|
+
console.log(`Success. Initialized design-embed with ${written} file(s).`);
|
|
322
|
+
console.log("Next: pnpm exec design-embed --out ./design.html");
|
|
323
|
+
}
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
function configTemplate(viewName) {
|
|
327
|
+
return `import {
|
|
328
|
+
\tdefineConfig,
|
|
329
|
+
\ttype PluginDefinition,
|
|
330
|
+
\ttype SourcePlugin,
|
|
331
|
+
\ttype SourcePluginInput,
|
|
332
|
+
\ttype SourcePluginResult,
|
|
333
|
+
} from "design-embed";
|
|
334
|
+
|
|
335
|
+
class HtmlFetcherPlugin implements PluginDefinition, SourcePlugin {
|
|
336
|
+
\treadonly name = "html-fetcher";
|
|
337
|
+
\tprivate readonly options: { url: string };
|
|
338
|
+
|
|
339
|
+
\tconstructor(options: { url: string }) {
|
|
340
|
+
\t\tthis.options = options;
|
|
341
|
+
\t}
|
|
342
|
+
|
|
343
|
+
\tasync run(_input: SourcePluginInput): Promise<SourcePluginResult> {
|
|
344
|
+
\t\ttry {
|
|
345
|
+
\t\t\tconst response = await fetch(this.options.url);
|
|
346
|
+
\t\t\tif (!response.ok) {
|
|
347
|
+
\t\t\t\treturn {
|
|
348
|
+
\t\t\t\t\tdiagnostics: [
|
|
349
|
+
\t\t\t\t\t\t{
|
|
350
|
+
\t\t\t\t\t\t\tcode: "HTML_FETCH_FAILED",
|
|
351
|
+
\t\t\t\t\t\t\tmessage: \`Failed to fetch HTML: \${response.status} \${response.statusText}\`,
|
|
352
|
+
\t\t\t\t\t\t\tseverity: "error",
|
|
353
|
+
\t\t\t\t\t\t},
|
|
354
|
+
\t\t\t\t\t],
|
|
355
|
+
\t\t\t\t};
|
|
356
|
+
\t\t\t}
|
|
357
|
+
|
|
358
|
+
\t\t\treturn {
|
|
359
|
+
\t\t\t\thtml: await response.text(),
|
|
360
|
+
\t\t\t\tdiagnostics: [],
|
|
361
|
+
\t\t\t};
|
|
362
|
+
\t\t} catch (error) {
|
|
363
|
+
\t\t\treturn {
|
|
364
|
+
\t\t\t\tdiagnostics: [
|
|
365
|
+
\t\t\t\t\t{
|
|
366
|
+
\t\t\t\t\t\tcode: "HTML_FETCH_FAILED",
|
|
367
|
+
\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),
|
|
368
|
+
\t\t\t\t\t\tseverity: "error",
|
|
369
|
+
\t\t\t\t\t},
|
|
370
|
+
\t\t\t\t],
|
|
371
|
+
\t\t\t};
|
|
372
|
+
\t\t}
|
|
373
|
+
\t}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export default defineConfig({
|
|
377
|
+
\tplugins: [
|
|
378
|
+
\t\tnew HtmlFetcherPlugin({
|
|
379
|
+
\t\t\turl: "https://www.scrapethissite.com/pages/",
|
|
380
|
+
\t\t}),
|
|
381
|
+
\t],
|
|
382
|
+
\toutput: {
|
|
383
|
+
\t\tviewName: "${viewName}",
|
|
384
|
+
\t\tviewsDir: "src/generated/views",
|
|
385
|
+
\t},
|
|
386
|
+
});
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region packages/design-embed/src/commands/plugin.ts
|
|
391
|
+
async function runPluginCommand(_name, flags) {
|
|
392
|
+
const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
|
|
393
|
+
const configResult = await loadConfig(getStringFlag(flags, "--config") ?? "design-embed.config.ts", cwd);
|
|
394
|
+
for (const diagnostic of configResult.diagnostics) if (diagnostic.severity === "error") console.error(`error: ${diagnostic.code}: ${diagnostic.message}`);
|
|
395
|
+
else console.warn(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`);
|
|
396
|
+
if (configResult.diagnostics.some((d) => d.severity === "error")) return 2;
|
|
397
|
+
const outPath = getStringFlag(flags, "--out");
|
|
398
|
+
if (!outPath) {
|
|
399
|
+
console.error("Error: --out is required.");
|
|
400
|
+
return 2;
|
|
401
|
+
}
|
|
402
|
+
const plugin = findSourcePlugin(configResult.config?.plugins);
|
|
403
|
+
if (!plugin) {
|
|
404
|
+
console.error("Error: config must include a source plugin instance in the plugins array (e.g. new FigmaHtmlPlugin({ ... })).");
|
|
405
|
+
return 2;
|
|
406
|
+
}
|
|
407
|
+
const result = await plugin.run({
|
|
408
|
+
cwd,
|
|
409
|
+
args: {}
|
|
410
|
+
});
|
|
411
|
+
for (const diagnostic of result.diagnostics) {
|
|
412
|
+
const output = `${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`;
|
|
413
|
+
if (diagnostic.severity === "error") console.error(output);
|
|
414
|
+
else console.warn(output);
|
|
415
|
+
}
|
|
416
|
+
if (result.diagnostics.some((d) => d.severity === "error")) return 2;
|
|
417
|
+
if (!result.html) {
|
|
418
|
+
console.error("Error: source plugin produced no HTML.");
|
|
419
|
+
return 2;
|
|
420
|
+
}
|
|
421
|
+
const resolvedOutPath = resolve(cwd, outPath);
|
|
422
|
+
mkdirSync(dirname(resolvedOutPath), { recursive: true });
|
|
423
|
+
writeFileSync(resolvedOutPath, result.html, "utf-8");
|
|
424
|
+
console.log(`Wrote ${outPath}`);
|
|
425
|
+
for (const file of result.files ?? []) {
|
|
426
|
+
const resolvedPath = resolve(cwd, file.path);
|
|
427
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
428
|
+
writeFileSync(resolvedPath, file.contents, "utf-8");
|
|
429
|
+
console.log(`Wrote ${file.path}`);
|
|
430
|
+
}
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
function isSourcePlugin(plugin) {
|
|
434
|
+
return typeof plugin.run === "function";
|
|
435
|
+
}
|
|
436
|
+
function findSourcePlugin(plugins) {
|
|
437
|
+
return plugins?.find(isSourcePlugin);
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region packages/design-embed/src/cli.ts
|
|
441
|
+
async function main() {
|
|
442
|
+
const args = process.argv.slice(2);
|
|
443
|
+
const parsed = parseArgs(args);
|
|
444
|
+
if (args[0] === "check") return runCheckCommand(parsed.flags);
|
|
445
|
+
if (args[0] === "plugin") return runPluginCommand(parsed.positionals[0], parsed.flags);
|
|
446
|
+
if (args[0] === "generate-tests") return runGenerateTestsCommand(parsed.flags);
|
|
447
|
+
if (args[0] === "init") return runInitCommand(parsed.flags);
|
|
448
|
+
const flags = args[0] && !args[0].startsWith("--") ? {
|
|
449
|
+
...parsed.flags,
|
|
450
|
+
"--": args[0]
|
|
451
|
+
} : parsed.flags;
|
|
452
|
+
if (getOutPath(flags) && !hasInput(flags)) return runPluginCommand(void 0, flags);
|
|
453
|
+
return runCompileCommand(flags);
|
|
454
|
+
}
|
|
455
|
+
function getOutPath(flags) {
|
|
456
|
+
return typeof flags["--out"] === "string";
|
|
457
|
+
}
|
|
458
|
+
function hasInput(flags) {
|
|
459
|
+
return typeof flags["--input"] === "string" || typeof flags["--"] === "string";
|
|
460
|
+
}
|
|
461
|
+
main().then((code) => {
|
|
462
|
+
if (code !== 0) process.exit(code);
|
|
463
|
+
}).catch((error) => {
|
|
464
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
465
|
+
console.error(`Pipeline failed: ${message}`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
});
|
|
468
|
+
//#endregion
|
|
469
|
+
export {};
|