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.
- package/LICENSE +1 -1
- package/README.md +98 -2
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +273 -0
- package/dist/core-BLV62TaX.mjs +907 -0
- package/dist/index.d.mts +273 -0
- package/dist/index.mjs +2 -0
- package/package.json +6 -19
- package/src/cli.ts +8 -16
- package/src/commands/compile.ts +25 -110
- package/src/commands/generateTests.ts +14 -96
- package/src/commands/init.ts +52 -55
- package/src/commands/plugin.ts +6 -21
- package/src/config/index.ts +302 -0
- package/{node_modules/@design-embed/core/src → src/core}/index.ts +151 -163
- package/src/core/nodes.ts +74 -0
- package/src/core/plugins/pluginApi.ts +44 -0
- package/src/core/types.ts +120 -0
- package/src/index.ts +48 -2
- package/src/targets/html.ts +621 -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/plugins/pluginApi.ts +0 -78
- package/node_modules/@design-embed/core/src/plugins/pluginRegistry.ts +0 -37
- /package/{node_modules/@design-embed/core/src → src/core}/diagnostics/diagnostic.ts +0 -0
- /package/{node_modules/@design-embed/core/src → src/core}/diagnostics/jsonDiagnostic.ts +0 -0
- /package/{node_modules/@design-embed/core/src → src/core}/pipeline/checkMode.ts +0 -0
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
-
import { type DesignEmbedConfig, loadConfig } from "@design-embed/config";
|
|
4
|
-
import type { Diagnostic } from "@design-embed/core";
|
|
5
|
-
import { reactTestGenerator } from "@design-embed/target-react";
|
|
1
|
+
import { resolve } from "node:path";
|
|
6
2
|
import { getBooleanFlag, getFormat, getStringFlag } from "../args.ts";
|
|
3
|
+
import { loadConfig } from "../config/index.ts";
|
|
4
|
+
import { type Diagnostic, embed } from "../core/index.ts";
|
|
5
|
+
import type { DesignEmbedConfig } from "../core/types.ts";
|
|
7
6
|
import { printDiagnostics } from "./compile.ts";
|
|
8
7
|
|
|
9
8
|
export async function runGenerateTestsCommand(
|
|
10
9
|
flags: Record<string, string | boolean>,
|
|
11
10
|
): Promise<number> {
|
|
12
11
|
const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
|
|
13
|
-
const configPath =
|
|
12
|
+
const configPath =
|
|
13
|
+
getStringFlag(flags, "--config") ?? "design-embed.config.ts";
|
|
14
14
|
const quiet = getBooleanFlag(flags, "--quiet");
|
|
15
15
|
const format = getFormat(flags);
|
|
16
16
|
const diagnostics: Diagnostic[] = [];
|
|
17
17
|
|
|
18
|
-
if (!configPath) {
|
|
19
|
-
diagnostics.push({
|
|
20
|
-
code: "CONFIG_REQUIRED",
|
|
21
|
-
message: "--config is required for generate-tests.",
|
|
22
|
-
severity: "error",
|
|
23
|
-
});
|
|
24
|
-
printDiagnostics(diagnostics, format, quiet);
|
|
25
|
-
return 2;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
18
|
const configResult = await loadConfig(configPath, cwd);
|
|
29
19
|
diagnostics.push(...configResult.diagnostics);
|
|
30
20
|
const config = configResult.config;
|
|
@@ -33,43 +23,24 @@ export async function runGenerateTestsCommand(
|
|
|
33
23
|
return 2;
|
|
34
24
|
}
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
if (!source || hasErrors(diagnostics)) {
|
|
38
|
-
printDiagnostics(diagnostics, format, quiet);
|
|
39
|
-
return 2;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const target = config.output?.target ?? "html";
|
|
43
|
-
if (target !== "react") {
|
|
26
|
+
if (!getTestGenerator(config)) {
|
|
44
27
|
diagnostics.push({
|
|
45
28
|
code: "TEST_TARGET_UNSUPPORTED",
|
|
46
|
-
message:
|
|
29
|
+
message:
|
|
30
|
+
"generate-tests requires output.target to be a target adapter with generateTests().",
|
|
47
31
|
severity: "error",
|
|
48
32
|
});
|
|
49
33
|
printDiagnostics(diagnostics, format, quiet);
|
|
50
34
|
return 2;
|
|
51
35
|
}
|
|
52
36
|
|
|
53
|
-
const result =
|
|
54
|
-
|
|
55
|
-
css: source.css,
|
|
56
|
-
config,
|
|
57
|
-
diagnostics,
|
|
58
|
-
});
|
|
37
|
+
const result = await embed({ config, cwd, generateTests: true });
|
|
38
|
+
diagnostics.push(...result.diagnostics);
|
|
59
39
|
if (hasErrors(diagnostics)) {
|
|
60
40
|
printDiagnostics(diagnostics, format, quiet);
|
|
61
41
|
return 2;
|
|
62
42
|
}
|
|
63
43
|
|
|
64
|
-
for (const file of result.files) {
|
|
65
|
-
const outPath = resolve(cwd, file.path);
|
|
66
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
67
|
-
writeFileSync(outPath, file.contents, "utf-8");
|
|
68
|
-
if (!quiet && format === "text") {
|
|
69
|
-
console.log(`Wrote ${file.path}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
44
|
printDiagnostics(diagnostics, format, quiet);
|
|
74
45
|
if (!quiet && format === "text") {
|
|
75
46
|
console.log(`Success. Generated ${result.files.length} test file(s).`);
|
|
@@ -77,62 +48,9 @@ export async function runGenerateTestsCommand(
|
|
|
77
48
|
return 0;
|
|
78
49
|
}
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function readConfiguredSource(
|
|
86
|
-
config: DesignEmbedConfig,
|
|
87
|
-
configPath: string,
|
|
88
|
-
cwd: string,
|
|
89
|
-
diagnostics: Diagnostic[],
|
|
90
|
-
): SourceContents | undefined {
|
|
91
|
-
const source = config.tests?.source;
|
|
92
|
-
if (!source?.html) {
|
|
93
|
-
diagnostics.push({
|
|
94
|
-
code: "TEST_SOURCE_HTML_REQUIRED",
|
|
95
|
-
message: "tests.source.html is required for generate-tests.",
|
|
96
|
-
severity: "error",
|
|
97
|
-
});
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const configDir = dirname(resolve(cwd, configPath));
|
|
102
|
-
const htmlPath = resolveConfigPath(source.html, configDir);
|
|
103
|
-
if (!existsSync(htmlPath)) {
|
|
104
|
-
diagnostics.push({
|
|
105
|
-
code: "TEST_SOURCE_HTML_NOT_FOUND",
|
|
106
|
-
message: `Test source HTML not found: ${htmlPath}`,
|
|
107
|
-
severity: "error",
|
|
108
|
-
file: source.html,
|
|
109
|
-
});
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let css: string | undefined;
|
|
114
|
-
if (source.css) {
|
|
115
|
-
const cssPath = resolveConfigPath(source.css, configDir);
|
|
116
|
-
if (!existsSync(cssPath)) {
|
|
117
|
-
diagnostics.push({
|
|
118
|
-
code: "TEST_SOURCE_CSS_NOT_FOUND",
|
|
119
|
-
message: `Test source CSS not found: ${cssPath}`,
|
|
120
|
-
severity: "error",
|
|
121
|
-
file: source.css,
|
|
122
|
-
});
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
css = readFileSync(cssPath, "utf-8");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
html: readFileSync(htmlPath, "utf-8"),
|
|
130
|
-
css,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function resolveConfigPath(path: string, configDir: string): string {
|
|
135
|
-
return isAbsolute(path) ? path : resolve(configDir, path);
|
|
51
|
+
function getTestGenerator(config: DesignEmbedConfig): boolean {
|
|
52
|
+
const target = config.output?.target;
|
|
53
|
+
return !!(target && target !== "html" && "generateTests" in target);
|
|
136
54
|
}
|
|
137
55
|
|
|
138
56
|
function hasErrors(diagnostics: Diagnostic[]): boolean {
|
package/src/commands/init.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import type { Diagnostic } from "@design-embed/core";
|
|
4
3
|
import { getBooleanFlag, getFormat, getStringFlag } from "../args.ts";
|
|
4
|
+
import type { Diagnostic } from "../core/index.ts";
|
|
5
5
|
import { printDiagnostics } from "./compile.ts";
|
|
6
6
|
|
|
7
7
|
export async function runInitCommand(
|
|
@@ -19,14 +19,6 @@ export async function runInitCommand(
|
|
|
19
19
|
path: "design-embed.config.ts",
|
|
20
20
|
contents: configTemplate(viewName),
|
|
21
21
|
},
|
|
22
|
-
{
|
|
23
|
-
path: "design.html",
|
|
24
|
-
contents: designHtmlTemplate(),
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
path: "playwright-ct.config.ts",
|
|
28
|
-
contents: playwrightConfigTemplate(),
|
|
29
|
-
},
|
|
30
22
|
];
|
|
31
23
|
|
|
32
24
|
let written = 0;
|
|
@@ -52,62 +44,67 @@ export async function runInitCommand(
|
|
|
52
44
|
printDiagnostics(diagnostics, format, quiet);
|
|
53
45
|
if (!quiet && format === "text") {
|
|
54
46
|
console.log(`Success. Initialized design-embed with ${written} file(s).`);
|
|
55
|
-
console.log(
|
|
56
|
-
"Next: pnpm exec design-embed --input ./design.html --config ./design-embed.config.ts",
|
|
57
|
-
);
|
|
47
|
+
console.log("Next: pnpm exec design-embed --out ./design.html");
|
|
58
48
|
}
|
|
59
49
|
return 0;
|
|
60
50
|
}
|
|
61
51
|
|
|
62
52
|
function configTemplate(viewName: string): string {
|
|
63
|
-
return `import {
|
|
53
|
+
return `import {
|
|
54
|
+
\tdefineConfig,
|
|
55
|
+
\ttype SourcePlugin,
|
|
56
|
+
\ttype SourcePluginInput,
|
|
57
|
+
\ttype SourcePluginResult,
|
|
58
|
+
} from "design-embed";
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
\
|
|
67
|
-
\
|
|
68
|
-
\t\tviewName: "${viewName}",
|
|
69
|
-
\t\tviewsDir: "src/generated/views",
|
|
70
|
-
\t\tstyleMode: "inline",
|
|
71
|
-
\t},
|
|
72
|
-
\ttests: {
|
|
73
|
-
\t\toutputDir: "tests/generated/design-embed",
|
|
74
|
-
\t\trunner: "playwright",
|
|
75
|
-
\t\tsource: {
|
|
76
|
-
\t\t\thtml: "./design.html",
|
|
77
|
-
\t\t},
|
|
78
|
-
\t\tviewports: [
|
|
79
|
-
\t\t\t{ name: "mobile", width: 390, height: 844 },
|
|
80
|
-
\t\t\t{ name: "desktop", width: 1440, height: 900 },
|
|
81
|
-
\t\t],
|
|
82
|
-
\t\tstates: [{ name: "default" }],
|
|
83
|
-
\t\tassertions: {
|
|
84
|
-
\t\t\tscreenshot: true,
|
|
85
|
-
\t\t\tlayout: true,
|
|
86
|
-
\t\t\tlayoutTolerance: 1,
|
|
87
|
-
\t\t\tselectors: [":scope", ":scope *"],
|
|
88
|
-
\t\t},
|
|
89
|
-
\t},
|
|
90
|
-
});
|
|
91
|
-
`;
|
|
92
|
-
}
|
|
60
|
+
class HtmlFetcherPlugin implements SourcePlugin {
|
|
61
|
+
\treadonly name = "html-fetcher";
|
|
62
|
+
\tprivate readonly options: { url: string };
|
|
93
63
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
\t
|
|
97
|
-
\t<h1 style="margin: 0 0 12px; font-size: 32px; line-height: 1.1;">Welcome hero</h1>
|
|
98
|
-
\t<p style="margin: 0 0 20px; font-size: 16px; line-height: 1.5;">Replace this file with HTML exported from your design source.</p>
|
|
99
|
-
\t<button data-role="primary" style="border: 0; border-radius: 999px; padding: 12px 18px; background: #2563eb; color: white; font-size: 14px; font-weight: 700;">Get started</button>
|
|
100
|
-
</section>
|
|
101
|
-
`;
|
|
102
|
-
}
|
|
64
|
+
\tconstructor(options: { url: string }) {
|
|
65
|
+
\t\tthis.options = options;
|
|
66
|
+
\t}
|
|
103
67
|
|
|
104
|
-
|
|
105
|
-
|
|
68
|
+
\tasync run(_input: SourcePluginInput): Promise<SourcePluginResult> {
|
|
69
|
+
\t\ttry {
|
|
70
|
+
\t\t\tconst response = await fetch(this.options.url);
|
|
71
|
+
\t\t\tif (!response.ok) {
|
|
72
|
+
\t\t\t\treturn {
|
|
73
|
+
\t\t\t\t\tdiagnostics: [
|
|
74
|
+
\t\t\t\t\t\t{
|
|
75
|
+
\t\t\t\t\t\t\tcode: "HTML_FETCH_FAILED",
|
|
76
|
+
\t\t\t\t\t\t\tmessage: \`Failed to fetch HTML: \${response.status} \${response.statusText}\`,
|
|
77
|
+
\t\t\t\t\t\t\tseverity: "error",
|
|
78
|
+
\t\t\t\t\t\t},
|
|
79
|
+
\t\t\t\t\t],
|
|
80
|
+
\t\t\t\t};
|
|
81
|
+
\t\t\t}
|
|
82
|
+
|
|
83
|
+
\t\t\treturn {
|
|
84
|
+
\t\t\t\thtml: await response.text(),
|
|
85
|
+
\t\t\t\tdiagnostics: [],
|
|
86
|
+
\t\t\t};
|
|
87
|
+
\t\t} catch (error) {
|
|
88
|
+
\t\t\treturn {
|
|
89
|
+
\t\t\t\tdiagnostics: [
|
|
90
|
+
\t\t\t\t\t{
|
|
91
|
+
\t\t\t\t\t\tcode: "HTML_FETCH_FAILED",
|
|
92
|
+
\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),
|
|
93
|
+
\t\t\t\t\t\tseverity: "error",
|
|
94
|
+
\t\t\t\t\t},
|
|
95
|
+
\t\t\t\t],
|
|
96
|
+
\t\t\t};
|
|
97
|
+
\t\t}
|
|
98
|
+
\t}
|
|
99
|
+
}
|
|
106
100
|
|
|
107
101
|
export default defineConfig({
|
|
108
|
-
\
|
|
109
|
-
\
|
|
110
|
-
\t
|
|
102
|
+
\tsource: new HtmlFetcherPlugin({
|
|
103
|
+
\t\turl: "https://www.scrapethissite.com/pages/",
|
|
104
|
+
\t}),
|
|
105
|
+
\toutput: {
|
|
106
|
+
\t\tviewName: "${viewName}",
|
|
107
|
+
\t\tviewsDir: "src/generated/views",
|
|
111
108
|
\t},
|
|
112
109
|
});
|
|
113
110
|
`;
|
package/src/commands/plugin.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { loadConfig, type PluginDefinition } from "@design-embed/config";
|
|
4
|
-
import type { SourcePlugin } from "@design-embed/core";
|
|
5
3
|
import { getStringFlag } from "../args.ts";
|
|
4
|
+
import { loadConfig } from "../config/index.ts";
|
|
6
5
|
|
|
7
6
|
export async function runPluginCommand(
|
|
8
7
|
_name: string | undefined,
|
|
9
8
|
flags: Record<string, string | boolean>,
|
|
10
9
|
): Promise<number> {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return 2;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const cwd = process.cwd();
|
|
10
|
+
const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
|
|
11
|
+
const configPath =
|
|
12
|
+
getStringFlag(flags, "--config") ?? "design-embed.config.ts";
|
|
18
13
|
const configResult = await loadConfig(configPath, cwd);
|
|
19
14
|
for (const diagnostic of configResult.diagnostics) {
|
|
20
15
|
if (diagnostic.severity === "error") {
|
|
@@ -35,7 +30,7 @@ export async function runPluginCommand(
|
|
|
35
30
|
return 2;
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
const plugin =
|
|
33
|
+
const plugin = configResult.config?.source;
|
|
39
34
|
if (!plugin) {
|
|
40
35
|
console.error(
|
|
41
36
|
"Error: config must include a source plugin instance in the plugins array (e.g. new FigmaHtmlPlugin({ ... })).",
|
|
@@ -43,7 +38,7 @@ export async function runPluginCommand(
|
|
|
43
38
|
return 2;
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
const result = await plugin.run({ cwd
|
|
41
|
+
const result = await plugin.run({ cwd });
|
|
47
42
|
|
|
48
43
|
for (const diagnostic of result.diagnostics) {
|
|
49
44
|
const output = `${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`;
|
|
@@ -77,13 +72,3 @@ export async function runPluginCommand(
|
|
|
77
72
|
|
|
78
73
|
return 0;
|
|
79
74
|
}
|
|
80
|
-
|
|
81
|
-
function isSourcePlugin(plugin: PluginDefinition): plugin is SourcePlugin {
|
|
82
|
-
return typeof (plugin as SourcePlugin).run === "function";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function findSourcePlugin(
|
|
86
|
-
plugins: PluginDefinition[] | undefined,
|
|
87
|
-
): SourcePlugin | undefined {
|
|
88
|
-
return plugins?.find(isSourcePlugin);
|
|
89
|
-
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import type { Diagnostic } from "../core/diagnostics/diagnostic.ts";
|
|
5
|
+
import type {
|
|
6
|
+
SourcePlugin,
|
|
7
|
+
SourcePluginInput,
|
|
8
|
+
SourcePluginResult,
|
|
9
|
+
} from "../core/plugins/pluginApi.ts";
|
|
10
|
+
import type { DesignEmbedConfig, TestGenerationConfig } from "../core/types.ts";
|
|
11
|
+
|
|
12
|
+
export type { Diagnostic } from "../core/diagnostics/diagnostic.ts";
|
|
13
|
+
|
|
14
|
+
export interface LoadConfigResult {
|
|
15
|
+
config?: DesignEmbedConfig;
|
|
16
|
+
configPath?: string;
|
|
17
|
+
diagnostics: Diagnostic[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defineConfig(config: DesignEmbedConfig): DesignEmbedConfig {
|
|
21
|
+
return config;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fromFile(
|
|
25
|
+
htmlPath: string | URL,
|
|
26
|
+
cssPath?: string | URL,
|
|
27
|
+
): SourcePlugin {
|
|
28
|
+
const resolvedHtml = htmlPath instanceof URL ? fileURLToPath(htmlPath) : null;
|
|
29
|
+
const resolvedCss = cssPath
|
|
30
|
+
? cssPath instanceof URL
|
|
31
|
+
? fileURLToPath(cssPath)
|
|
32
|
+
: null
|
|
33
|
+
: null;
|
|
34
|
+
return {
|
|
35
|
+
name: "html-file",
|
|
36
|
+
async run({ cwd }: SourcePluginInput): Promise<SourcePluginResult> {
|
|
37
|
+
const html = readFileSync(
|
|
38
|
+
resolvedHtml ?? resolve(cwd, htmlPath as string),
|
|
39
|
+
"utf-8",
|
|
40
|
+
);
|
|
41
|
+
const css = cssPath
|
|
42
|
+
? readFileSync(resolvedCss ?? resolve(cwd, cssPath as string), "utf-8")
|
|
43
|
+
: undefined;
|
|
44
|
+
return { html, css, diagnostics: [] };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function loadConfig(
|
|
50
|
+
configPath: string,
|
|
51
|
+
cwd = process.cwd(),
|
|
52
|
+
): Promise<LoadConfigResult> {
|
|
53
|
+
const diagnostics: Diagnostic[] = [];
|
|
54
|
+
const resolvedPath = isAbsolute(configPath)
|
|
55
|
+
? configPath
|
|
56
|
+
: resolve(cwd, configPath);
|
|
57
|
+
|
|
58
|
+
if (!existsSync(resolvedPath)) {
|
|
59
|
+
return {
|
|
60
|
+
diagnostics: [
|
|
61
|
+
{
|
|
62
|
+
code: "CONFIG_NOT_FOUND",
|
|
63
|
+
message: `Config file not found: ${resolvedPath}`,
|
|
64
|
+
severity: "error",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!/\.(ts|js|mjs)$/.test(resolvedPath)) {
|
|
71
|
+
return {
|
|
72
|
+
diagnostics: [
|
|
73
|
+
{
|
|
74
|
+
code: "CONFIG_UNSUPPORTED_FORMAT",
|
|
75
|
+
message: `Unsupported config format: ${resolvedPath}. Only .ts, .js, and .mjs are supported.`,
|
|
76
|
+
severity: "error",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const module = await import(pathToFileURL(resolvedPath).href);
|
|
84
|
+
const config = module.default ?? module.config;
|
|
85
|
+
|
|
86
|
+
if (!config) {
|
|
87
|
+
return {
|
|
88
|
+
diagnostics: [
|
|
89
|
+
{
|
|
90
|
+
code: "CONFIG_INVALID",
|
|
91
|
+
message: `Config file must export a default object or a named 'config' object: ${resolvedPath}`,
|
|
92
|
+
severity: "error",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
diagnostics.push(...validateConfig(config));
|
|
99
|
+
return { config, configPath: resolvedPath, diagnostics };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
return {
|
|
103
|
+
diagnostics: [
|
|
104
|
+
{
|
|
105
|
+
code: "CONFIG_INVALID",
|
|
106
|
+
message: `Failed to load config file: ${message}`,
|
|
107
|
+
severity: "error",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function validateConfig(config: DesignEmbedConfig): Diagnostic[] {
|
|
115
|
+
const diagnostics: Diagnostic[] = [];
|
|
116
|
+
const target = config.output?.target;
|
|
117
|
+
const styleMode = config.output?.styleMode;
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
target &&
|
|
121
|
+
target !== "html" &&
|
|
122
|
+
(typeof target !== "object" || typeof target.emit !== "function")
|
|
123
|
+
) {
|
|
124
|
+
diagnostics.push({
|
|
125
|
+
code: "TARGET_ADAPTER_INVALID",
|
|
126
|
+
message: "output.target must be a target adapter with emit().",
|
|
127
|
+
severity: "error",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
styleMode &&
|
|
133
|
+
styleMode !== "inline" &&
|
|
134
|
+
styleMode !== "css-modules" &&
|
|
135
|
+
styleMode !== "tailwind"
|
|
136
|
+
) {
|
|
137
|
+
diagnostics.push({
|
|
138
|
+
code: "STYLE_MODE_UNSUPPORTED",
|
|
139
|
+
message: `Unsupported style mode: ${styleMode}`,
|
|
140
|
+
severity: "error",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const [index, component] of (config.components ?? []).entries()) {
|
|
145
|
+
if (!component.selector || typeof component.selector !== "string") {
|
|
146
|
+
diagnostics.push({
|
|
147
|
+
code: "COMPONENT_SELECTOR_INVALID",
|
|
148
|
+
message: `Component mapping ${index} must include a selector.`,
|
|
149
|
+
severity: "error",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!component.component || typeof component.component !== "string") {
|
|
154
|
+
diagnostics.push({
|
|
155
|
+
code: "COMPONENT_IMPORT_INVALID",
|
|
156
|
+
message: `Component mapping ${index} must include a component name.`,
|
|
157
|
+
severity: "error",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const spacing = config.tokens?.spacing;
|
|
163
|
+
if (spacing?.unit && spacing.unit !== "px" && spacing.unit !== "rem") {
|
|
164
|
+
diagnostics.push({
|
|
165
|
+
code: "TOKEN_SPACING_UNIT_INVALID",
|
|
166
|
+
message: `Unsupported spacing unit: ${spacing.unit}`,
|
|
167
|
+
severity: "error",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (spacing?.threshold !== undefined && !Number.isFinite(spacing.threshold)) {
|
|
172
|
+
diagnostics.push({
|
|
173
|
+
code: "TOKEN_SPACING_THRESHOLD_INVALID",
|
|
174
|
+
message: "Spacing threshold must be a finite number.",
|
|
175
|
+
severity: "error",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const [name, value] of Object.entries(spacing?.values ?? {})) {
|
|
180
|
+
if (!Number.isFinite(value)) {
|
|
181
|
+
diagnostics.push({
|
|
182
|
+
code: "TOKEN_SPACING_VALUE_INVALID",
|
|
183
|
+
message: `Spacing token ${name} must be a finite number.`,
|
|
184
|
+
severity: "error",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const [name, value] of Object.entries(config.tokens?.colors ?? {})) {
|
|
190
|
+
if (!/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(value)) {
|
|
191
|
+
diagnostics.push({
|
|
192
|
+
code: "TOKEN_COLOR_INVALID",
|
|
193
|
+
message: `Color token ${name} must be a hex color.`,
|
|
194
|
+
severity: "error",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
config.tokens?.colorThreshold !== undefined &&
|
|
201
|
+
!Number.isFinite(config.tokens.colorThreshold)
|
|
202
|
+
) {
|
|
203
|
+
diagnostics.push({
|
|
204
|
+
code: "TOKEN_COLOR_THRESHOLD_INVALID",
|
|
205
|
+
message: "Color threshold must be a finite number.",
|
|
206
|
+
severity: "error",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const [groupName, group] of Object.entries({
|
|
211
|
+
sizing: config.tokens?.sizing,
|
|
212
|
+
typography: config.tokens?.typography,
|
|
213
|
+
})) {
|
|
214
|
+
if (!group) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (group.unit && group.unit !== "px" && group.unit !== "rem") {
|
|
218
|
+
diagnostics.push({
|
|
219
|
+
code: "TOKEN_NUMERIC_UNIT_INVALID",
|
|
220
|
+
message: `Unsupported ${groupName} unit: ${group.unit}`,
|
|
221
|
+
severity: "error",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (group.threshold !== undefined && !Number.isFinite(group.threshold)) {
|
|
225
|
+
diagnostics.push({
|
|
226
|
+
code: "TOKEN_NUMERIC_THRESHOLD_INVALID",
|
|
227
|
+
message: `${groupName} threshold must be a finite number.`,
|
|
228
|
+
severity: "error",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
for (const [name, value] of Object.entries(group.values ?? {})) {
|
|
232
|
+
if (!Number.isFinite(value)) {
|
|
233
|
+
diagnostics.push({
|
|
234
|
+
code: "TOKEN_NUMERIC_VALUE_INVALID",
|
|
235
|
+
message: `${groupName} token ${name} must be a finite number.`,
|
|
236
|
+
severity: "error",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
validateTestGeneration(config.tests, diagnostics);
|
|
243
|
+
|
|
244
|
+
return diagnostics;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function validateTestGeneration(
|
|
248
|
+
tests: TestGenerationConfig | undefined,
|
|
249
|
+
diagnostics: Diagnostic[],
|
|
250
|
+
): void {
|
|
251
|
+
if (!tests) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (tests.runner && tests.runner !== "playwright") {
|
|
256
|
+
diagnostics.push({
|
|
257
|
+
code: "TEST_RUNNER_UNSUPPORTED",
|
|
258
|
+
message: `Unsupported test runner: ${tests.runner}`,
|
|
259
|
+
severity: "error",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const [index, viewport] of (tests.viewports ?? []).entries()) {
|
|
264
|
+
if (!Number.isFinite(viewport.width) || viewport.width <= 0) {
|
|
265
|
+
diagnostics.push({
|
|
266
|
+
code: "TEST_VIEWPORT_WIDTH_INVALID",
|
|
267
|
+
message: `Test viewport ${index} width must be a positive finite number.`,
|
|
268
|
+
severity: "error",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (!Number.isFinite(viewport.height) || viewport.height <= 0) {
|
|
272
|
+
diagnostics.push({
|
|
273
|
+
code: "TEST_VIEWPORT_HEIGHT_INVALID",
|
|
274
|
+
message: `Test viewport ${index} height must be a positive finite number.`,
|
|
275
|
+
severity: "error",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const [index, state] of (tests.states ?? []).entries()) {
|
|
281
|
+
if (!state.name || typeof state.name !== "string") {
|
|
282
|
+
diagnostics.push({
|
|
283
|
+
code: "TEST_STATE_NAME_INVALID",
|
|
284
|
+
message: `Test state ${index} must include a name.`,
|
|
285
|
+
severity: "error",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
tests.assertions?.layoutTolerance !== undefined &&
|
|
292
|
+
(!Number.isFinite(tests.assertions.layoutTolerance) ||
|
|
293
|
+
tests.assertions.layoutTolerance < 0)
|
|
294
|
+
) {
|
|
295
|
+
diagnostics.push({
|
|
296
|
+
code: "TEST_LAYOUT_TOLERANCE_INVALID",
|
|
297
|
+
message:
|
|
298
|
+
"Test layout tolerance must be a finite number greater than or equal to 0.",
|
|
299
|
+
severity: "error",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|