design-embed 0.1.1 → 0.2.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/README.md +7 -2
- package/dist/cli.mjs +28 -224
- package/dist/core-DSlWqRBj.mjs +907 -0
- package/dist/index.d.mts +185 -312
- package/dist/index.mjs +2 -2
- package/package.json +2 -10
- package/src/cli.ts +7 -32
- package/src/commands/compile.ts +20 -88
- package/src/commands/generateTests.ts +9 -91
- package/src/commands/init.ts +5 -8
- package/src/commands/plugin.ts +3 -14
- package/src/config/index.ts +302 -0
- package/src/core/diagnostics/diagnostic.ts +18 -0
- package/src/core/diagnostics/jsonDiagnostic.ts +51 -0
- package/src/core/index.ts +581 -0
- package/src/core/nodes.ts +74 -0
- package/src/core/pipeline/checkMode.ts +46 -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 +565 -12
- package/dist/src-D3fnqGCq.mjs +0 -511
package/src/commands/compile.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { getBooleanFlag, getFormat, getStringFlag } from "../args.ts";
|
|
4
|
+
import { loadConfig } from "../config/index.ts";
|
|
4
5
|
import {
|
|
5
6
|
checkGeneratedFiles,
|
|
6
7
|
type Diagnostic,
|
|
7
8
|
embed,
|
|
8
9
|
formatDiagnosticText,
|
|
9
|
-
type TargetEmitter,
|
|
10
|
-
type TargetTestGenerator,
|
|
11
10
|
toJsonDiagnostics,
|
|
12
|
-
} from "
|
|
13
|
-
import { getBooleanFlag, getFormat, getStringFlag } from "../args.ts";
|
|
14
|
-
import { htmlEmitter } from "../targets/html.ts";
|
|
11
|
+
} from "../core/index.ts";
|
|
15
12
|
|
|
16
13
|
export interface CompileCommandOptions {
|
|
17
14
|
check?: boolean;
|
|
@@ -22,8 +19,6 @@ export async function runCompileCommand(
|
|
|
22
19
|
options: CompileCommandOptions = {},
|
|
23
20
|
): Promise<number> {
|
|
24
21
|
const cwd = resolve(process.cwd(), getStringFlag(flags, "--cwd") ?? ".");
|
|
25
|
-
const inputPath =
|
|
26
|
-
getStringFlag(flags, "--input") ?? getStringFlag(flags, "--");
|
|
27
22
|
const explicitConfigPath = getStringFlag(flags, "--config");
|
|
28
23
|
const defaultConfigPath = resolve(cwd, "design-embed.config.ts");
|
|
29
24
|
const configPath =
|
|
@@ -31,77 +26,45 @@ export async function runCompileCommand(
|
|
|
31
26
|
(existsSync(defaultConfigPath) ? "design-embed.config.ts" : undefined);
|
|
32
27
|
const quiet = getBooleanFlag(flags, "--quiet");
|
|
33
28
|
const format = getFormat(flags);
|
|
34
|
-
const generateTests = !getBooleanFlag(flags, "--no-test");
|
|
35
29
|
const diagnostics: Diagnostic[] = [];
|
|
36
30
|
|
|
37
|
-
if (!
|
|
31
|
+
if (!configPath) {
|
|
38
32
|
diagnostics.push({
|
|
39
|
-
code: "
|
|
40
|
-
message:
|
|
33
|
+
code: "CONFIG_REQUIRED",
|
|
34
|
+
message:
|
|
35
|
+
"No config file found. Create design-embed.config.ts or use --config.",
|
|
41
36
|
severity: "error",
|
|
42
37
|
});
|
|
43
38
|
printDiagnostics(diagnostics, format, quiet);
|
|
44
39
|
return 2;
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
severity: "error",
|
|
53
|
-
file: inputPath,
|
|
54
|
-
});
|
|
42
|
+
const configResult = await loadConfig(configPath, cwd);
|
|
43
|
+
diagnostics.push(...configResult.diagnostics);
|
|
44
|
+
const config = configResult.config;
|
|
45
|
+
|
|
46
|
+
if (hasErrors(diagnostics)) {
|
|
55
47
|
printDiagnostics(diagnostics, format, quiet);
|
|
56
48
|
return 2;
|
|
57
49
|
}
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const configResult = await loadConfig(configPath, cwd);
|
|
62
|
-
diagnostics.push(...configResult.diagnostics);
|
|
63
|
-
config = configResult.config;
|
|
64
|
-
|
|
65
|
-
if (hasErrors(diagnostics)) {
|
|
66
|
-
printDiagnostics(diagnostics, format, quiet);
|
|
67
|
-
return 2;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const targetAdapter = getTargetAdapter(config);
|
|
51
|
+
const isCheckMode = options.check && !getBooleanFlag(flags, "--write");
|
|
52
|
+
const generateTests = !getBooleanFlag(flags, "--no-test");
|
|
72
53
|
|
|
73
|
-
const cssPath = getStringFlag(flags, "--css");
|
|
74
|
-
const html = readFileSync(resolvedInputPath, "utf-8");
|
|
75
|
-
const css = cssPath
|
|
76
|
-
? readFileSync(resolve(cwd, cssPath), "utf-8")
|
|
77
|
-
: undefined;
|
|
78
54
|
const result = await embed({
|
|
79
|
-
html,
|
|
80
|
-
css,
|
|
81
|
-
configPath,
|
|
82
55
|
config,
|
|
83
56
|
cwd,
|
|
84
|
-
|
|
57
|
+
dryRun: isCheckMode,
|
|
58
|
+
generateTests,
|
|
85
59
|
});
|
|
86
60
|
diagnostics.push(...result.diagnostics);
|
|
87
61
|
|
|
88
|
-
if (generateTests && targetAdapter.testGenerator) {
|
|
89
|
-
const testResult = targetAdapter.testGenerator.generateTests({
|
|
90
|
-
html,
|
|
91
|
-
css,
|
|
92
|
-
config: config ?? {},
|
|
93
|
-
diagnostics,
|
|
94
|
-
generatedFiles: result.files,
|
|
95
|
-
});
|
|
96
|
-
result.files.push(...testResult.files);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
62
|
if (hasErrors(diagnostics)) {
|
|
100
63
|
printDiagnostics(diagnostics, format, quiet);
|
|
101
64
|
return 2;
|
|
102
65
|
}
|
|
103
66
|
|
|
104
|
-
if (
|
|
67
|
+
if (isCheckMode) {
|
|
105
68
|
const checkResult = checkGeneratedFiles({
|
|
106
69
|
cwd,
|
|
107
70
|
files: result.files,
|
|
@@ -109,21 +72,11 @@ export async function runCompileCommand(
|
|
|
109
72
|
return existsSync(path) ? readFileSync(path, "utf-8") : undefined;
|
|
110
73
|
},
|
|
111
74
|
});
|
|
112
|
-
|
|
113
|
-
diagnostics.push(...checkDiagnostics);
|
|
75
|
+
diagnostics.push(...checkResult.diagnostics);
|
|
114
76
|
printDiagnostics(diagnostics, format, quiet);
|
|
115
77
|
return checkResult.ok ? 0 : 3;
|
|
116
78
|
}
|
|
117
79
|
|
|
118
|
-
for (const file of result.files) {
|
|
119
|
-
const outPath = resolve(cwd, file.path);
|
|
120
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
121
|
-
writeFileSync(outPath, file.contents, "utf-8");
|
|
122
|
-
if (!quiet && format === "text") {
|
|
123
|
-
console.log(`Wrote ${file.path}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
80
|
printDiagnostics(diagnostics, format, quiet);
|
|
128
81
|
if (!quiet && format === "text") {
|
|
129
82
|
console.log(`Success. Generated ${result.files.length} file(s).`);
|
|
@@ -155,27 +108,6 @@ export function printDiagnostics(
|
|
|
155
108
|
}
|
|
156
109
|
}
|
|
157
110
|
|
|
158
|
-
interface ResolvedTargetAdapter {
|
|
159
|
-
emitter: TargetEmitter;
|
|
160
|
-
testGenerator?: TargetTestGenerator;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function getTargetAdapter(
|
|
164
|
-
config: DesignEmbedConfig | undefined,
|
|
165
|
-
): ResolvedTargetAdapter {
|
|
166
|
-
const target = config?.output?.target;
|
|
167
|
-
if (!target || target === "html") {
|
|
168
|
-
return { emitter: htmlEmitter };
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
emitter: target as TargetEmitter,
|
|
172
|
-
testGenerator:
|
|
173
|
-
"generateTests" in target
|
|
174
|
-
? (target as TargetEmitter & TargetTestGenerator)
|
|
175
|
-
: undefined,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
111
|
function hasErrors(diagnostics: Diagnostic[]): boolean {
|
|
180
112
|
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
181
113
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
-
import { type DesignEmbedConfig, loadConfig } from "@design-embed/config";
|
|
4
|
-
import type { Diagnostic, TargetTestGenerator } from "@design-embed/core";
|
|
1
|
+
import { resolve } from "node:path";
|
|
5
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";
|
|
6
6
|
import { printDiagnostics } from "./compile.ts";
|
|
7
7
|
|
|
8
8
|
export async function runGenerateTestsCommand(
|
|
@@ -23,14 +23,7 @@ export async function runGenerateTestsCommand(
|
|
|
23
23
|
return 2;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
if (!source || hasErrors(diagnostics)) {
|
|
28
|
-
printDiagnostics(diagnostics, format, quiet);
|
|
29
|
-
return 2;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const testGenerator = getTestGenerator(config);
|
|
33
|
-
if (!testGenerator) {
|
|
26
|
+
if (!getTestGenerator(config)) {
|
|
34
27
|
diagnostics.push({
|
|
35
28
|
code: "TEST_TARGET_UNSUPPORTED",
|
|
36
29
|
message:
|
|
@@ -41,26 +34,13 @@ export async function runGenerateTestsCommand(
|
|
|
41
34
|
return 2;
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
const result =
|
|
45
|
-
|
|
46
|
-
css: source.css,
|
|
47
|
-
config,
|
|
48
|
-
diagnostics,
|
|
49
|
-
});
|
|
37
|
+
const result = await embed({ config, cwd, generateTests: true });
|
|
38
|
+
diagnostics.push(...result.diagnostics);
|
|
50
39
|
if (hasErrors(diagnostics)) {
|
|
51
40
|
printDiagnostics(diagnostics, format, quiet);
|
|
52
41
|
return 2;
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
for (const file of result.files) {
|
|
56
|
-
const outPath = resolve(cwd, file.path);
|
|
57
|
-
mkdirSync(dirname(outPath), { recursive: true });
|
|
58
|
-
writeFileSync(outPath, file.contents, "utf-8");
|
|
59
|
-
if (!quiet && format === "text") {
|
|
60
|
-
console.log(`Wrote ${file.path}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
44
|
printDiagnostics(diagnostics, format, quiet);
|
|
65
45
|
if (!quiet && format === "text") {
|
|
66
46
|
console.log(`Success. Generated ${result.files.length} test file(s).`);
|
|
@@ -68,71 +48,9 @@ export async function runGenerateTestsCommand(
|
|
|
68
48
|
return 0;
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
html: string;
|
|
73
|
-
css?: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function readConfiguredSource(
|
|
77
|
-
config: DesignEmbedConfig,
|
|
78
|
-
configPath: string,
|
|
79
|
-
cwd: string,
|
|
80
|
-
diagnostics: Diagnostic[],
|
|
81
|
-
): SourceContents | undefined {
|
|
82
|
-
const source = config.tests?.source;
|
|
83
|
-
if (!source?.html) {
|
|
84
|
-
diagnostics.push({
|
|
85
|
-
code: "TEST_SOURCE_HTML_REQUIRED",
|
|
86
|
-
message: "tests.source.html is required for generate-tests.",
|
|
87
|
-
severity: "error",
|
|
88
|
-
});
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const configDir = dirname(resolve(cwd, configPath));
|
|
93
|
-
const htmlPath = resolveConfigPath(source.html, configDir);
|
|
94
|
-
if (!existsSync(htmlPath)) {
|
|
95
|
-
diagnostics.push({
|
|
96
|
-
code: "TEST_SOURCE_HTML_NOT_FOUND",
|
|
97
|
-
message: `Test source HTML not found: ${htmlPath}`,
|
|
98
|
-
severity: "error",
|
|
99
|
-
file: source.html,
|
|
100
|
-
});
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let css: string | undefined;
|
|
105
|
-
if (source.css) {
|
|
106
|
-
const cssPath = resolveConfigPath(source.css, configDir);
|
|
107
|
-
if (!existsSync(cssPath)) {
|
|
108
|
-
diagnostics.push({
|
|
109
|
-
code: "TEST_SOURCE_CSS_NOT_FOUND",
|
|
110
|
-
message: `Test source CSS not found: ${cssPath}`,
|
|
111
|
-
severity: "error",
|
|
112
|
-
file: source.css,
|
|
113
|
-
});
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
css = readFileSync(cssPath, "utf-8");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
html: readFileSync(htmlPath, "utf-8"),
|
|
121
|
-
css,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function resolveConfigPath(path: string, configDir: string): string {
|
|
126
|
-
return isAbsolute(path) ? path : resolve(configDir, path);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function getTestGenerator(
|
|
130
|
-
config: DesignEmbedConfig,
|
|
131
|
-
): TargetTestGenerator | undefined {
|
|
51
|
+
function getTestGenerator(config: DesignEmbedConfig): boolean {
|
|
132
52
|
const target = config.output?.target;
|
|
133
|
-
return target && target !== "html" && "generateTests" in target
|
|
134
|
-
? (target as TargetTestGenerator)
|
|
135
|
-
: undefined;
|
|
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(
|
|
@@ -52,13 +52,12 @@ export async function runInitCommand(
|
|
|
52
52
|
function configTemplate(viewName: string): string {
|
|
53
53
|
return `import {
|
|
54
54
|
\tdefineConfig,
|
|
55
|
-
\ttype PluginDefinition,
|
|
56
55
|
\ttype SourcePlugin,
|
|
57
56
|
\ttype SourcePluginInput,
|
|
58
57
|
\ttype SourcePluginResult,
|
|
59
58
|
} from "design-embed";
|
|
60
59
|
|
|
61
|
-
class HtmlFetcherPlugin implements
|
|
60
|
+
class HtmlFetcherPlugin implements SourcePlugin {
|
|
62
61
|
\treadonly name = "html-fetcher";
|
|
63
62
|
\tprivate readonly options: { url: string };
|
|
64
63
|
|
|
@@ -100,11 +99,9 @@ class HtmlFetcherPlugin implements PluginDefinition, SourcePlugin {
|
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
export default defineConfig({
|
|
103
|
-
\
|
|
104
|
-
\t\
|
|
105
|
-
\t
|
|
106
|
-
\t\t}),
|
|
107
|
-
\t],
|
|
102
|
+
\tsource: new HtmlFetcherPlugin({
|
|
103
|
+
\t\turl: "https://www.scrapethissite.com/pages/",
|
|
104
|
+
\t}),
|
|
108
105
|
\toutput: {
|
|
109
106
|
\t\tviewName: "${viewName}",
|
|
110
107
|
\t\tviewsDir: "src/generated/views",
|
package/src/commands/plugin.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
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,
|
|
@@ -31,7 +30,7 @@ export async function runPluginCommand(
|
|
|
31
30
|
return 2;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
const plugin =
|
|
33
|
+
const plugin = configResult.config?.source;
|
|
35
34
|
if (!plugin) {
|
|
36
35
|
console.error(
|
|
37
36
|
"Error: config must include a source plugin instance in the plugins array (e.g. new FigmaHtmlPlugin({ ... })).",
|
|
@@ -39,7 +38,7 @@ export async function runPluginCommand(
|
|
|
39
38
|
return 2;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
const result = await plugin.run({ cwd
|
|
41
|
+
const result = await plugin.run({ cwd });
|
|
43
42
|
|
|
44
43
|
for (const diagnostic of result.diagnostics) {
|
|
45
44
|
const output = `${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`;
|
|
@@ -73,13 +72,3 @@ export async function runPluginCommand(
|
|
|
73
72
|
|
|
74
73
|
return 0;
|
|
75
74
|
}
|
|
76
|
-
|
|
77
|
-
function isSourcePlugin(plugin: PluginDefinition): plugin is SourcePlugin {
|
|
78
|
-
return typeof (plugin as SourcePlugin).run === "function";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function findSourcePlugin(
|
|
82
|
-
plugins: PluginDefinition[] | undefined,
|
|
83
|
-
): SourcePlugin | undefined {
|
|
84
|
-
return plugins?.find(isSourcePlugin);
|
|
85
|
-
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SourceLocation } from "../index.ts";
|
|
2
|
+
|
|
3
|
+
export type DiagnosticSeverity = "error" | "warning" | "info";
|
|
4
|
+
|
|
5
|
+
export interface Diagnostic {
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
severity: DiagnosticSeverity;
|
|
9
|
+
file?: string;
|
|
10
|
+
source?: SourceLocation;
|
|
11
|
+
selector?: string;
|
|
12
|
+
property?: string;
|
|
13
|
+
details?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasErrorDiagnostics(diagnostics: Diagnostic[]): boolean {
|
|
17
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
18
|
+
}
|