@format.dev/cli 0.0.0 → 0.1.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/README.md +29 -20
- package/bin/format.mjs +20 -0
- package/dist/config-Dq7O4e8N.mjs +63 -0
- package/dist/config.d.mts +21 -0
- package/dist/config.mjs +3 -0
- package/dist/draw-random-name-BzvSSVqO.d.mts +4 -0
- package/dist/draw-random-name-Dpt6YmUk.mjs +13 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1539 -0
- package/dist/install-BiDrmMgJ.mjs +1486 -0
- package/dist/lockstep.d.mts +27 -0
- package/dist/lockstep.mjs +30 -0
- package/dist/project-pX62Di_9.mjs +124 -0
- package/dist/random-name.d.mts +2 -0
- package/dist/random-name.mjs +3 -0
- package/dist/scaffold-shared.d.mts +2 -0
- package/dist/scaffold-shared.mjs +3 -0
- package/dist/scaffold.d.mts +191 -0
- package/dist/scaffold.mjs +6 -0
- package/dist/shared-BD62UD5L.d.mts +32 -0
- package/dist/shared-CM3vyUlL.mjs +143 -0
- package/env.d.ts +99 -0
- package/package.json +86 -34
- package/templates/html/documents/welcome/data/alternative.json +3 -0
- package/templates/html/documents/welcome/data/default.json +3 -0
- package/templates/html/documents/welcome/index.html +32 -0
- package/templates/html/documents/welcome/index.ts +4 -0
- package/templates/html/documents/welcome/pages/Close/close.html +9 -0
- package/templates/html/documents/welcome/pages/Cover/cover.html +13 -0
- package/templates/html/documents/welcome/pages/FeatureShowcase/feature-showcase.css +296 -0
- package/templates/html/documents/welcome/pages/FeatureShowcase/feature-showcase.html +296 -0
- package/templates/html/documents/welcome/pages/NextSteps/next-steps.css +107 -0
- package/templates/html/documents/welcome/pages/NextSteps/next-steps.html +398 -0
- package/templates/html/documents/welcome/styles.css +214 -0
- package/templates/html/format.config.json +4 -0
- package/templates/html/package.json +15 -0
- package/templates/html/tsconfig.json +3 -0
- package/templates/react/documents/welcome/components/ButtonLink.tsx +29 -0
- package/templates/react/documents/welcome/data/alternative.json +3 -0
- package/templates/react/documents/welcome/data/default.json +3 -0
- package/templates/react/documents/welcome/data/schema.ts +7 -0
- package/templates/react/documents/welcome/icons/propeller-asterisk.svg +1 -0
- package/templates/react/documents/welcome/index.tsx +59 -0
- package/templates/react/documents/welcome/logos/_logo-base.tsx +9 -0
- package/templates/react/documents/welcome/logos/aws-lambda.tsx +22 -0
- package/templates/react/documents/welcome/logos/bun.tsx +30 -0
- package/templates/react/documents/welcome/logos/cloudflare-workers.tsx +52 -0
- package/templates/react/documents/welcome/logos/css-modules.tsx +54 -0
- package/templates/react/documents/welcome/logos/css-official.tsx +18 -0
- package/templates/react/documents/welcome/logos/deno.tsx +17 -0
- package/templates/react/documents/welcome/logos/linaria.tsx +18 -0
- package/templates/react/documents/welcome/logos/node.tsx +47 -0
- package/templates/react/documents/welcome/logos/panda-css.tsx +18 -0
- package/templates/react/documents/welcome/logos/sass.tsx +12 -0
- package/templates/react/documents/welcome/logos/tailwind.tsx +45 -0
- package/templates/react/documents/welcome/logos/vanilla-extract.tsx +74 -0
- package/templates/react/documents/welcome/logos/vercel.tsx +17 -0
- package/templates/react/documents/welcome/pages/Close/ClosePage.tsx +13 -0
- package/templates/react/documents/welcome/pages/Cover/CoverPage.tsx +17 -0
- package/templates/react/documents/welcome/pages/FeatureShowcase/FeatureShowcasePage.tsx +221 -0
- package/templates/react/documents/welcome/pages/FeatureShowcase/feature-showcase.css +296 -0
- package/templates/react/documents/welcome/pages/FeatureShowcase/mountains.ts +27 -0
- package/templates/react/documents/welcome/pages/NextSteps/NextStepsPage.tsx +146 -0
- package/templates/react/documents/welcome/pages/NextSteps/next-steps.css +107 -0
- package/templates/react/documents/welcome/styles.css +214 -0
- package/templates/react/documents/welcome/types.ts +4 -0
- package/templates/react/format.config.json +4 -0
- package/templates/react/package.json +22 -0
- package/templates/react/tsconfig.json +6 -0
- package/templates/shared/README.md +18 -0
- package/templates/shared/_env +1 -0
- package/templates/shared/_gitignore +3 -0
- package/templates/shared/documents/welcome/assets/deploy.svg +268 -0
- package/templates/shared/documents/welcome/assets/logo.svg +22 -0
- package/templates/shared/documents/welcome/assets/mona-sans.woff2 +0 -0
- package/templates/shared/documents/welcome/assets/styling.svg +59 -0
- package/templates/vue/documents/welcome/components/ButtonLink.vue +28 -0
- package/templates/vue/documents/welcome/data/alternative.json +3 -0
- package/templates/vue/documents/welcome/data/default.json +3 -0
- package/templates/vue/documents/welcome/data/schema.ts +7 -0
- package/templates/vue/documents/welcome/index.vue +48 -0
- package/templates/vue/documents/welcome/logos/aws-lambda.vue +18 -0
- package/templates/vue/documents/welcome/logos/bun.vue +26 -0
- package/templates/vue/documents/welcome/logos/cloudflare-workers.vue +48 -0
- package/templates/vue/documents/welcome/logos/css-modules.vue +49 -0
- package/templates/vue/documents/welcome/logos/css-official.vue +14 -0
- package/templates/vue/documents/welcome/logos/deno.vue +13 -0
- package/templates/vue/documents/welcome/logos/linaria.vue +14 -0
- package/templates/vue/documents/welcome/logos/node.vue +43 -0
- package/templates/vue/documents/welcome/logos/panda-css.vue +14 -0
- package/templates/vue/documents/welcome/logos/sass.vue +8 -0
- package/templates/vue/documents/welcome/logos/tailwind.vue +41 -0
- package/templates/vue/documents/welcome/logos/vanilla-extract.vue +70 -0
- package/templates/vue/documents/welcome/logos/vercel.vue +13 -0
- package/templates/vue/documents/welcome/pages/Close/ClosePage.vue +11 -0
- package/templates/vue/documents/welcome/pages/Cover/CoverPage.vue +19 -0
- package/templates/vue/documents/welcome/pages/FeatureShowcase/FeatureShowcasePage.vue +217 -0
- package/templates/vue/documents/welcome/pages/FeatureShowcase/feature-showcase.css +296 -0
- package/templates/vue/documents/welcome/pages/FeatureShowcase/mountains.ts +27 -0
- package/templates/vue/documents/welcome/pages/NextSteps/NextStepsPage.vue +146 -0
- package/templates/vue/documents/welcome/pages/NextSteps/next-steps.css +107 -0
- package/templates/vue/documents/welcome/styles.css +214 -0
- package/templates/vue/documents/welcome/types.ts +4 -0
- package/templates/vue/format.config.json +4 -0
- package/templates/vue/package.json +19 -0
- package/templates/vue/tsconfig.json +7 -0
- package/tsconfig.recommended.json +22 -0
- package/index.d.ts +0 -2
- package/index.js +0 -3
|
@@ -0,0 +1,1486 @@
|
|
|
1
|
+
import { r as detectIndent$1, s as CliError } from "./config-Dq7O4e8N.mjs";
|
|
2
|
+
import { a as DEFAULT_STYLING, c as SCHEMA_DEPENDENCIES, d as STYLING_DEPENDENCIES, i as DEFAULT_SCHEMA_KIND, m as STYLING_SUPPORT, r as DEFAULT_PAGE_WIDTH, t as DEFAULT_PAGE_HEIGHT, y as methodsForFramework } from "./shared-CM3vyUlL.mjs";
|
|
3
|
+
import { t as drawRandomName } from "./draw-random-name-Dpt6YmUk.mjs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
9
|
+
import { applyEdits, modify } from "jsonc-parser";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
|
|
12
|
+
//#region src/package-manager.ts
|
|
13
|
+
const LOCKFILES = [
|
|
14
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
15
|
+
["yarn.lock", "yarn"],
|
|
16
|
+
["bun.lock", "bun"],
|
|
17
|
+
["bun.lockb", "bun"],
|
|
18
|
+
["package-lock.json", "npm"]
|
|
19
|
+
];
|
|
20
|
+
function findLockfileManager(startDir) {
|
|
21
|
+
let dir = resolve(startDir);
|
|
22
|
+
for (;;) {
|
|
23
|
+
const match = LOCKFILES.find(([lockfile]) => existsSync(join(dir, lockfile)));
|
|
24
|
+
if (match) return match[1];
|
|
25
|
+
const parent = dirname(dir);
|
|
26
|
+
if (parent === dir) return null;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function detectPackageManager(projectDir) {
|
|
31
|
+
const packageJsonPath = join(projectDir, "package.json");
|
|
32
|
+
if (existsSync(packageJsonPath)) {
|
|
33
|
+
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
34
|
+
const declared = JSON.parse(raw).packageManager?.split("@")[0];
|
|
35
|
+
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
36
|
+
}
|
|
37
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
38
|
+
const fromUserAgent = [
|
|
39
|
+
"pnpm",
|
|
40
|
+
"yarn",
|
|
41
|
+
"bun"
|
|
42
|
+
].find((name) => userAgent.includes(`${name}/`));
|
|
43
|
+
if (fromUserAgent) return fromUserAgent;
|
|
44
|
+
return findLockfileManager(projectDir) ?? "npm";
|
|
45
|
+
}
|
|
46
|
+
function installCommand(packageManager) {
|
|
47
|
+
if (packageManager === "npm") return {
|
|
48
|
+
command: "npm",
|
|
49
|
+
args: ["install"]
|
|
50
|
+
};
|
|
51
|
+
if (packageManager === "pnpm") return {
|
|
52
|
+
command: "pnpm",
|
|
53
|
+
args: ["install", "--no-frozen-lockfile"]
|
|
54
|
+
};
|
|
55
|
+
if (packageManager === "yarn") return {
|
|
56
|
+
command: "yarn",
|
|
57
|
+
args: ["install", "--no-immutable"]
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
command: packageManager,
|
|
61
|
+
args: ["install"]
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function commandForPlatform(command) {
|
|
65
|
+
if (process.platform !== "win32") return command;
|
|
66
|
+
if (command === "npm" || command === "pnpm" || command === "yarn") return `${command}.cmd`;
|
|
67
|
+
return command;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/scaffold/run-install.ts
|
|
72
|
+
const MAX_OUTPUT_LINES = 40;
|
|
73
|
+
function truncateForSpinner(line) {
|
|
74
|
+
const available = Math.max(20, (process.stdout.columns ?? 80) - 10);
|
|
75
|
+
if (line.length <= available) return line;
|
|
76
|
+
return `${line.slice(0, available - 1)}…`;
|
|
77
|
+
}
|
|
78
|
+
async function runInstall(args) {
|
|
79
|
+
const { command, args: commandArgs, cwd, onProgress } = args;
|
|
80
|
+
return new Promise((resolvePromise) => {
|
|
81
|
+
const child = spawn(commandForPlatform(command), commandArgs, {
|
|
82
|
+
cwd,
|
|
83
|
+
stdio: [
|
|
84
|
+
"ignore",
|
|
85
|
+
"pipe",
|
|
86
|
+
"pipe"
|
|
87
|
+
],
|
|
88
|
+
shell: false
|
|
89
|
+
});
|
|
90
|
+
const lines = [];
|
|
91
|
+
const collect = (chunk) => {
|
|
92
|
+
chunk.toString().split("\n").map((line) => line.trim()).filter(Boolean).forEach((line) => {
|
|
93
|
+
lines.push(line);
|
|
94
|
+
onProgress?.(line);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
child.stdout?.on("data", collect);
|
|
98
|
+
child.stderr?.on("data", collect);
|
|
99
|
+
const output = () => lines.slice(-MAX_OUTPUT_LINES).join("\n");
|
|
100
|
+
child.on("error", (error) => {
|
|
101
|
+
resolvePromise({
|
|
102
|
+
ok: false,
|
|
103
|
+
code: null,
|
|
104
|
+
error,
|
|
105
|
+
output: output()
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
child.on("exit", (code) => {
|
|
109
|
+
resolvePromise({
|
|
110
|
+
ok: code === 0,
|
|
111
|
+
code,
|
|
112
|
+
output: output()
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/scaffold/dependencies.ts
|
|
120
|
+
async function writeDependenciesToManifest(args) {
|
|
121
|
+
const { projectDir, dependencies } = args;
|
|
122
|
+
if (dependencies.length === 0) return [];
|
|
123
|
+
const packageJsonPath = join(projectDir, "package.json");
|
|
124
|
+
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
125
|
+
const packageJson = JSON.parse(raw);
|
|
126
|
+
const runtime = { ...packageJson.dependencies ?? {} };
|
|
127
|
+
const development = { ...packageJson.devDependencies ?? {} };
|
|
128
|
+
const present = new Set([...Object.keys(runtime), ...Object.keys(development)]);
|
|
129
|
+
const added = [];
|
|
130
|
+
for (const dependency of dependencies) {
|
|
131
|
+
if (present.has(dependency.name)) continue;
|
|
132
|
+
const target = dependency.dev ? development : runtime;
|
|
133
|
+
target[dependency.name] = dependency.version;
|
|
134
|
+
present.add(dependency.name);
|
|
135
|
+
added.push(dependency.name);
|
|
136
|
+
}
|
|
137
|
+
if (added.length === 0) return [];
|
|
138
|
+
if (Object.keys(runtime).length > 0) packageJson.dependencies = runtime;
|
|
139
|
+
if (Object.keys(development).length > 0) packageJson.devDependencies = development;
|
|
140
|
+
const indent$2 = detectIndent$1(raw);
|
|
141
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, indent$2)}\n`, "utf8");
|
|
142
|
+
return added;
|
|
143
|
+
}
|
|
144
|
+
async function installManifest(args) {
|
|
145
|
+
const { projectDir, onProgress } = args;
|
|
146
|
+
const packageManager = await detectPackageManager(projectDir);
|
|
147
|
+
const { command, args: commandArgs } = installCommand(packageManager);
|
|
148
|
+
const result = await runInstall({
|
|
149
|
+
command,
|
|
150
|
+
args: commandArgs,
|
|
151
|
+
cwd: projectDir,
|
|
152
|
+
onProgress
|
|
153
|
+
});
|
|
154
|
+
if (result.ok) return {
|
|
155
|
+
ok: true,
|
|
156
|
+
packageManager
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
packageManager,
|
|
161
|
+
message: result.error?.message ?? `${packageManager} exited with code ${result.code}`,
|
|
162
|
+
output: result.output
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function joinNames(names) {
|
|
166
|
+
if (names.length <= 1) return names[0] ?? "";
|
|
167
|
+
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/scaffold/document/types.ts
|
|
172
|
+
const DEFAULT_TEMPLATE = "blank";
|
|
173
|
+
const EMPTY_STYLESHEET_COMMENT = "/* Your styles go here */\n";
|
|
174
|
+
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/scaffold/document/document-names.ts
|
|
177
|
+
const RESERVED_WORDS = new Set([
|
|
178
|
+
"await",
|
|
179
|
+
"break",
|
|
180
|
+
"case",
|
|
181
|
+
"catch",
|
|
182
|
+
"class",
|
|
183
|
+
"const",
|
|
184
|
+
"continue",
|
|
185
|
+
"debugger",
|
|
186
|
+
"default",
|
|
187
|
+
"delete",
|
|
188
|
+
"do",
|
|
189
|
+
"else",
|
|
190
|
+
"enum",
|
|
191
|
+
"export",
|
|
192
|
+
"extends",
|
|
193
|
+
"false",
|
|
194
|
+
"finally",
|
|
195
|
+
"for",
|
|
196
|
+
"function",
|
|
197
|
+
"if",
|
|
198
|
+
"import",
|
|
199
|
+
"in",
|
|
200
|
+
"instanceof",
|
|
201
|
+
"new",
|
|
202
|
+
"null",
|
|
203
|
+
"return",
|
|
204
|
+
"super",
|
|
205
|
+
"switch",
|
|
206
|
+
"this",
|
|
207
|
+
"throw",
|
|
208
|
+
"true",
|
|
209
|
+
"try",
|
|
210
|
+
"typeof",
|
|
211
|
+
"var",
|
|
212
|
+
"void",
|
|
213
|
+
"while",
|
|
214
|
+
"with",
|
|
215
|
+
"yield",
|
|
216
|
+
"implements",
|
|
217
|
+
"interface",
|
|
218
|
+
"let",
|
|
219
|
+
"package",
|
|
220
|
+
"private",
|
|
221
|
+
"protected",
|
|
222
|
+
"public",
|
|
223
|
+
"static",
|
|
224
|
+
"arguments",
|
|
225
|
+
"eval",
|
|
226
|
+
"globalthis",
|
|
227
|
+
"infinity",
|
|
228
|
+
"nan",
|
|
229
|
+
"undefined",
|
|
230
|
+
"any",
|
|
231
|
+
"bigint",
|
|
232
|
+
"boolean",
|
|
233
|
+
"never",
|
|
234
|
+
"number",
|
|
235
|
+
"object",
|
|
236
|
+
"string",
|
|
237
|
+
"symbol",
|
|
238
|
+
"unknown"
|
|
239
|
+
]);
|
|
240
|
+
const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
241
|
+
const isReserved = (value) => RESERVED_WORDS.has(value.toLowerCase());
|
|
242
|
+
function capitalize$1(value) {
|
|
243
|
+
return value ? value[0].toUpperCase() + value.slice(1) : "";
|
|
244
|
+
}
|
|
245
|
+
function toCamelCase(input) {
|
|
246
|
+
const parts = input.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
247
|
+
if (!parts.length) return "document";
|
|
248
|
+
return `${parts[0].toLowerCase()}${parts.slice(1).map((part) => capitalize$1(part.toLowerCase())).join("")}` || "document";
|
|
249
|
+
}
|
|
250
|
+
function toSafeIdentifier(name) {
|
|
251
|
+
let safe = name;
|
|
252
|
+
if (!/^[A-Za-z_$]/.test(safe)) safe = `doc${capitalize$1(safe)}`;
|
|
253
|
+
if (!IDENTIFIER_RE.test(safe) || isReserved(safe)) safe = `doc${capitalize$1(safe.replace(/[^A-Za-z0-9_$]/g, ""))}`;
|
|
254
|
+
if (!IDENTIFIER_RE.test(safe) || isReserved(safe)) safe = "document";
|
|
255
|
+
return safe;
|
|
256
|
+
}
|
|
257
|
+
function getDocumentExportName(documentName) {
|
|
258
|
+
return toSafeIdentifier(toCamelCase(documentName));
|
|
259
|
+
}
|
|
260
|
+
function buildDocumentExportMap(documentNames) {
|
|
261
|
+
const map = /* @__PURE__ */ new Map();
|
|
262
|
+
const used = /* @__PURE__ */ new Map();
|
|
263
|
+
for (const documentName of documentNames) {
|
|
264
|
+
const exportName = getDocumentExportName(documentName);
|
|
265
|
+
const existing = used.get(exportName);
|
|
266
|
+
if (existing) throw new Error(`Document names must resolve to unique exports. "${existing}" and "${documentName}" both map to "${exportName}".`);
|
|
267
|
+
used.set(exportName, documentName);
|
|
268
|
+
map.set(documentName, exportName);
|
|
269
|
+
}
|
|
270
|
+
return map;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/scaffold/document/validate-name.ts
|
|
275
|
+
const RESERVED_DOCUMENT_NAMES = ["shared-assets", "chunks"];
|
|
276
|
+
const ALLOWED_NAME = /^[A-Za-z0-9-]+$/;
|
|
277
|
+
function validateDocumentName(args) {
|
|
278
|
+
const { name, existingNames } = args;
|
|
279
|
+
const trimmed = name.trim();
|
|
280
|
+
if (!trimmed) return {
|
|
281
|
+
ok: false,
|
|
282
|
+
reason: "Enter a document name."
|
|
283
|
+
};
|
|
284
|
+
if (!ALLOWED_NAME.test(trimmed)) return {
|
|
285
|
+
ok: false,
|
|
286
|
+
reason: "Document names shouldn't use spaces or non-alphanumeric characters. Use letters, numbers and dashes only."
|
|
287
|
+
};
|
|
288
|
+
if (RESERVED_DOCUMENT_NAMES.includes(trimmed)) return {
|
|
289
|
+
ok: false,
|
|
290
|
+
reason: `"${trimmed}" is a reserved name. Reserved names: ${RESERVED_DOCUMENT_NAMES.join(", ")}.`
|
|
291
|
+
};
|
|
292
|
+
const exportName = getDocumentExportName(trimmed);
|
|
293
|
+
if (RESERVED_WORDS.has(exportName.toLowerCase())) return {
|
|
294
|
+
ok: false,
|
|
295
|
+
reason: `"${trimmed}" maps to the reserved identifier "${exportName}". Pick a different name.`
|
|
296
|
+
};
|
|
297
|
+
if (existingNames.includes(trimmed)) return {
|
|
298
|
+
ok: false,
|
|
299
|
+
reason: `A document named "${trimmed}" already exists.`,
|
|
300
|
+
suggestion: suggestAvailableName({
|
|
301
|
+
name: trimmed,
|
|
302
|
+
existingNames
|
|
303
|
+
})
|
|
304
|
+
};
|
|
305
|
+
const exportCollision = existingNames.find((existing) => getDocumentExportName(existing) === exportName);
|
|
306
|
+
if (exportCollision) return {
|
|
307
|
+
ok: false,
|
|
308
|
+
reason: `"${trimmed}" and the existing document "${exportCollision}" both map to the export "${exportName}". Document names must resolve to unique exports.`,
|
|
309
|
+
suggestion: suggestAvailableName({
|
|
310
|
+
name: trimmed,
|
|
311
|
+
existingNames
|
|
312
|
+
})
|
|
313
|
+
};
|
|
314
|
+
return { ok: true };
|
|
315
|
+
}
|
|
316
|
+
function suggestAvailableName(args) {
|
|
317
|
+
const { name, existingNames } = args;
|
|
318
|
+
const takenNames = new Set(existingNames);
|
|
319
|
+
const takenExports = new Set(existingNames.map(getDocumentExportName));
|
|
320
|
+
const isAvailable = (candidate) => !takenNames.has(candidate) && !takenExports.has(getDocumentExportName(candidate));
|
|
321
|
+
if (isAvailable(name)) return name;
|
|
322
|
+
const base = name.replace(/-\d+$/, "");
|
|
323
|
+
let counter = 2;
|
|
324
|
+
while (!isAvailable(`${base}-${counter}`)) counter += 1;
|
|
325
|
+
return `${base}-${counter}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/scaffold/document/styling/prerequisites.ts
|
|
330
|
+
async function readProjectDependencies(projectRoot) {
|
|
331
|
+
try {
|
|
332
|
+
const raw = await fs.readFile(join(projectRoot, "package.json"), "utf8");
|
|
333
|
+
const pkg = JSON.parse(raw);
|
|
334
|
+
return new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
|
335
|
+
} catch {
|
|
336
|
+
return /* @__PURE__ */ new Set();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function buildReport(args) {
|
|
340
|
+
const { method, missingDependencies, missingConfig } = args;
|
|
341
|
+
const steps = [];
|
|
342
|
+
if (missingDependencies.length > 0) {
|
|
343
|
+
const names = missingDependencies.map((dependency) => dependency.name).join(" ");
|
|
344
|
+
const flag = missingDependencies.every((dependency) => dependency.dev) ? "-D " : "";
|
|
345
|
+
steps.push(`Install the required package: \`pnpm add ${flag}${names}\``);
|
|
346
|
+
}
|
|
347
|
+
for (const config of missingConfig) steps.push(config.description);
|
|
348
|
+
return {
|
|
349
|
+
method,
|
|
350
|
+
satisfied: missingDependencies.length === 0 && missingConfig.length === 0,
|
|
351
|
+
missingDependencies,
|
|
352
|
+
missingConfig,
|
|
353
|
+
steps
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function satisfied(method) {
|
|
357
|
+
return buildReport({
|
|
358
|
+
method,
|
|
359
|
+
missingDependencies: [],
|
|
360
|
+
missingConfig: []
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function requireDependency(args) {
|
|
364
|
+
const { method, dependency, installed, missingConfig = [] } = args;
|
|
365
|
+
return buildReport({
|
|
366
|
+
method,
|
|
367
|
+
missingDependencies: installed.has(dependency) ? [] : [{
|
|
368
|
+
name: dependency,
|
|
369
|
+
dev: true
|
|
370
|
+
}],
|
|
371
|
+
missingConfig
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/scaffold/document/panda-setup.ts
|
|
377
|
+
const PANDA_CONFIG_NAMES = [
|
|
378
|
+
"panda.config.ts",
|
|
379
|
+
"panda.config.mts",
|
|
380
|
+
"panda.config.cts",
|
|
381
|
+
"panda.config.js",
|
|
382
|
+
"panda.config.mjs",
|
|
383
|
+
"panda.config.cjs"
|
|
384
|
+
];
|
|
385
|
+
const DEFAULT_PANDA_CONFIG_NAME = "panda.config.ts";
|
|
386
|
+
const PANDA_CONFIG_TEMPLATE = (documentsDirName) => `import { defineConfig } from '@pandacss/dev'
|
|
387
|
+
|
|
388
|
+
export default defineConfig({
|
|
389
|
+
include: ['./${documentsDirName}/**/*.{js,jsx,ts,tsx,vue}'],
|
|
390
|
+
outdir: './styles/styled-system'
|
|
391
|
+
})
|
|
392
|
+
`;
|
|
393
|
+
function findExistingPandaConfig(projectRoot) {
|
|
394
|
+
let dir = resolve(projectRoot);
|
|
395
|
+
for (;;) {
|
|
396
|
+
for (const name of PANDA_CONFIG_NAMES) {
|
|
397
|
+
const candidate = join(dir, name);
|
|
398
|
+
if (existsSync(candidate)) return candidate;
|
|
399
|
+
}
|
|
400
|
+
const parent = dirname(dir);
|
|
401
|
+
if (parent === dir) return null;
|
|
402
|
+
dir = parent;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function writePandaConfig(args) {
|
|
406
|
+
const { projectRoot, documentsDirName } = args;
|
|
407
|
+
const configPath = join(projectRoot, DEFAULT_PANDA_CONFIG_NAME);
|
|
408
|
+
await fs.writeFile(configPath, PANDA_CONFIG_TEMPLATE(documentsDirName), "utf8");
|
|
409
|
+
return configPath;
|
|
410
|
+
}
|
|
411
|
+
const PANDA_LAYER_CSS = `@layer reset, base, tokens, recipes, utilities;
|
|
412
|
+
|
|
413
|
+
:host {
|
|
414
|
+
display: flex;
|
|
415
|
+
flex-direction: column;
|
|
416
|
+
justify-content: center;
|
|
417
|
+
text-align: center;
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
async function ensurePandaLayerCss(projectRoot) {
|
|
421
|
+
const stylesDir = join(projectRoot, "styles");
|
|
422
|
+
const layerPath = join(stylesDir, "styles.css");
|
|
423
|
+
if (existsSync(layerPath)) return "exists";
|
|
424
|
+
await fs.mkdir(stylesDir, { recursive: true });
|
|
425
|
+
await fs.writeFile(layerPath, PANDA_LAYER_CSS, "utf8");
|
|
426
|
+
return "created";
|
|
427
|
+
}
|
|
428
|
+
function detectIndent(raw) {
|
|
429
|
+
const match = raw.match(/^[ \t]+/m);
|
|
430
|
+
return match ? match[0] : " ";
|
|
431
|
+
}
|
|
432
|
+
async function addPrepareScript(projectRoot) {
|
|
433
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
434
|
+
let raw;
|
|
435
|
+
try {
|
|
436
|
+
raw = await fs.readFile(packageJsonPath, "utf8");
|
|
437
|
+
} catch {
|
|
438
|
+
return "no-package-json";
|
|
439
|
+
}
|
|
440
|
+
const packageJson = JSON.parse(raw);
|
|
441
|
+
if (packageJson.scripts?.prepare) return "exists";
|
|
442
|
+
packageJson.scripts = {
|
|
443
|
+
...packageJson.scripts,
|
|
444
|
+
prepare: "panda codegen"
|
|
445
|
+
};
|
|
446
|
+
const indent$2 = detectIndent(raw);
|
|
447
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, indent$2)}\n`, "utf8");
|
|
448
|
+
return "added";
|
|
449
|
+
}
|
|
450
|
+
async function enablePandaInFormatConfig(args) {
|
|
451
|
+
const { formatConfigPath, projectRoot, pandaConfigPath } = args;
|
|
452
|
+
const raw = await fs.readFile(formatConfigPath, "utf8");
|
|
453
|
+
const indent$2 = detectIndent(raw);
|
|
454
|
+
const formattingOptions = {
|
|
455
|
+
insertSpaces: !indent$2.includes(" "),
|
|
456
|
+
tabSize: indent$2.includes(" ") ? 4 : indent$2.length
|
|
457
|
+
};
|
|
458
|
+
const applyConfigEdit = (content, path, value) => {
|
|
459
|
+
return applyEdits(content, modify(content, path, value, { formattingOptions }));
|
|
460
|
+
};
|
|
461
|
+
let updated = applyConfigEdit(raw, ["pandaCss", "enabled"], true);
|
|
462
|
+
if (!(resolve(pandaConfigPath) === resolve(projectRoot, DEFAULT_PANDA_CONFIG_NAME))) {
|
|
463
|
+
const relativeConfig = `./${relative(projectRoot, pandaConfigPath)}`;
|
|
464
|
+
const relativeCwd = `./${relative(projectRoot, dirname(pandaConfigPath))}`;
|
|
465
|
+
updated = applyConfigEdit(updated, ["pandaCss", "postCssConfig"], {
|
|
466
|
+
cwd: relativeCwd,
|
|
467
|
+
configPath: relativeConfig
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
await fs.writeFile(formatConfigPath, updated, "utf8");
|
|
471
|
+
}
|
|
472
|
+
function execArgs(packageManager, binArgs) {
|
|
473
|
+
switch (packageManager) {
|
|
474
|
+
case "pnpm": return {
|
|
475
|
+
command: "pnpm",
|
|
476
|
+
args: ["exec", ...binArgs]
|
|
477
|
+
};
|
|
478
|
+
case "yarn": return {
|
|
479
|
+
command: "yarn",
|
|
480
|
+
args: ["exec", ...binArgs]
|
|
481
|
+
};
|
|
482
|
+
case "bun": return {
|
|
483
|
+
command: "bun",
|
|
484
|
+
args: ["x", ...binArgs]
|
|
485
|
+
};
|
|
486
|
+
default: return {
|
|
487
|
+
command: "npm",
|
|
488
|
+
args: [
|
|
489
|
+
"exec",
|
|
490
|
+
"--",
|
|
491
|
+
...binArgs
|
|
492
|
+
]
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function runPandaCodegen(projectRoot) {
|
|
497
|
+
const { command, args } = execArgs(await detectPackageManager(projectRoot), ["panda", "codegen"]);
|
|
498
|
+
return new Promise((resolvePromise) => {
|
|
499
|
+
const child = spawn(commandForPlatform(command), args, {
|
|
500
|
+
cwd: projectRoot,
|
|
501
|
+
stdio: "inherit",
|
|
502
|
+
shell: false
|
|
503
|
+
});
|
|
504
|
+
child.on("error", (error) => {
|
|
505
|
+
resolvePromise({
|
|
506
|
+
ok: false,
|
|
507
|
+
message: error.message
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
child.on("exit", (code) => {
|
|
511
|
+
resolvePromise(code === 0 ? { ok: true } : {
|
|
512
|
+
ok: false,
|
|
513
|
+
message: `panda codegen exited with code ${code}`
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async function setupPanda(args) {
|
|
519
|
+
const { projectRoot, documentsDirName, formatConfigPath, runCodegen = runPandaCodegen } = args;
|
|
520
|
+
const existing = findExistingPandaConfig(projectRoot);
|
|
521
|
+
const pandaConfigCreated = existing === null;
|
|
522
|
+
const pandaConfigPath = existing ?? await writePandaConfig({
|
|
523
|
+
projectRoot,
|
|
524
|
+
documentsDirName
|
|
525
|
+
});
|
|
526
|
+
const layerCss = await ensurePandaLayerCss(projectRoot);
|
|
527
|
+
const prepareScript = await addPrepareScript(projectRoot);
|
|
528
|
+
let formatConfigUpdated = false;
|
|
529
|
+
try {
|
|
530
|
+
await enablePandaInFormatConfig({
|
|
531
|
+
formatConfigPath,
|
|
532
|
+
projectRoot,
|
|
533
|
+
pandaConfigPath
|
|
534
|
+
});
|
|
535
|
+
formatConfigUpdated = true;
|
|
536
|
+
} catch {
|
|
537
|
+
formatConfigUpdated = false;
|
|
538
|
+
}
|
|
539
|
+
const codegen = await runCodegen(projectRoot);
|
|
540
|
+
return {
|
|
541
|
+
pandaConfigPath,
|
|
542
|
+
pandaConfigCreated,
|
|
543
|
+
layerCss,
|
|
544
|
+
prepareScript,
|
|
545
|
+
formatConfigUpdated,
|
|
546
|
+
codegen
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/scaffold/document/styling/strategies.ts
|
|
552
|
+
const STARTER_CSS = `:host {
|
|
553
|
+
display: flex;
|
|
554
|
+
flex-direction: column;
|
|
555
|
+
justify-content: center;
|
|
556
|
+
text-align: center;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
h1 {
|
|
560
|
+
margin-block: 0.5rem;
|
|
561
|
+
font-size: 1.5rem;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.hint {
|
|
565
|
+
margin: 0;
|
|
566
|
+
opacity: 0.5;
|
|
567
|
+
font-family: var(--font-mono);
|
|
568
|
+
}
|
|
569
|
+
`;
|
|
570
|
+
const TAILWIND_CSS = `@import 'tailwindcss';
|
|
571
|
+
|
|
572
|
+
@theme {
|
|
573
|
+
--font-sans: var(--font-sans-system);
|
|
574
|
+
--font-serif: var(--font-serif-system);
|
|
575
|
+
--font-mono: var(--font-mono-system);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
:host {
|
|
579
|
+
display: flex;
|
|
580
|
+
flex-direction: column;
|
|
581
|
+
justify-content: center;
|
|
582
|
+
text-align: center;
|
|
583
|
+
}
|
|
584
|
+
`;
|
|
585
|
+
const TAILWIND_EMPTY_CSS = `@import 'tailwindcss';
|
|
586
|
+
|
|
587
|
+
@theme {
|
|
588
|
+
--font-sans: var(--font-sans-system);
|
|
589
|
+
--font-serif: var(--font-serif-system);
|
|
590
|
+
--font-mono: var(--font-mono-system);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
${EMPTY_STYLESHEET_COMMENT}`;
|
|
594
|
+
const TAILWIND_HEADING_CLASS = "my-2 text-2xl font-bold";
|
|
595
|
+
const TAILWIND_HINT_CLASS = "m-0 opacity-50 font-mono";
|
|
596
|
+
const STARTER_VANILLA_EXTRACT = `import { globalStyle, style } from '@vanilla-extract/css'
|
|
597
|
+
|
|
598
|
+
globalStyle(':host', {
|
|
599
|
+
display: 'flex',
|
|
600
|
+
flexDirection: 'column',
|
|
601
|
+
justifyContent: 'center',
|
|
602
|
+
textAlign: 'center'
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
export const title = style({
|
|
606
|
+
marginBlock: '0.5rem',
|
|
607
|
+
fontSize: '1.5rem'
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
export const hint = style({
|
|
611
|
+
margin: 0,
|
|
612
|
+
opacity: 0.5,
|
|
613
|
+
fontFamily: 'var(--font-mono)'
|
|
614
|
+
})
|
|
615
|
+
`;
|
|
616
|
+
const LINARIA_HOST_STYLES = `export const hostStyles = css\`
|
|
617
|
+
:global() {
|
|
618
|
+
:host {
|
|
619
|
+
display: flex;
|
|
620
|
+
flex-direction: column;
|
|
621
|
+
justify-content: center;
|
|
622
|
+
text-align: center;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
\``;
|
|
626
|
+
const LINARIA_TITLE = `const Title = styled.h1\`
|
|
627
|
+
margin-block: 0.5rem;
|
|
628
|
+
font-size: 1.5rem;
|
|
629
|
+
\``;
|
|
630
|
+
const LINARIA_HINT = `const Hint = styled.p\`
|
|
631
|
+
margin: 0;
|
|
632
|
+
opacity: 0.5;
|
|
633
|
+
font-family: var(--font-mono);
|
|
634
|
+
\``;
|
|
635
|
+
function elementApplication(imports, vueStyleBlock) {
|
|
636
|
+
return {
|
|
637
|
+
imports,
|
|
638
|
+
declarations: [],
|
|
639
|
+
headingTag: "h1",
|
|
640
|
+
hintTag: "p",
|
|
641
|
+
hintClass: "hint",
|
|
642
|
+
vueStyleBlock
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function tailwindApplication(imports, vueStyleBlock) {
|
|
646
|
+
return {
|
|
647
|
+
imports,
|
|
648
|
+
declarations: [],
|
|
649
|
+
headingTag: "h1",
|
|
650
|
+
hintTag: "p",
|
|
651
|
+
headingClass: TAILWIND_HEADING_CLASS,
|
|
652
|
+
hintClass: TAILWIND_HINT_CLASS,
|
|
653
|
+
vueStyleBlock
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const cssStrategy = {
|
|
657
|
+
method: "css",
|
|
658
|
+
frameworks: STYLING_SUPPORT["css"],
|
|
659
|
+
files() {
|
|
660
|
+
return [{
|
|
661
|
+
path: "styles.css",
|
|
662
|
+
contents: STARTER_CSS
|
|
663
|
+
}];
|
|
664
|
+
},
|
|
665
|
+
apply({ framework }) {
|
|
666
|
+
if (framework === "vue") return elementApplication([], "<style>\n@import './styles.css';\n</style>");
|
|
667
|
+
return elementApplication(["import './styles.css'"]);
|
|
668
|
+
},
|
|
669
|
+
async checkPrerequisites() {
|
|
670
|
+
return satisfied("css");
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
const cssModulesStrategy = {
|
|
674
|
+
method: "css-modules",
|
|
675
|
+
frameworks: STYLING_SUPPORT["css-modules"],
|
|
676
|
+
files() {
|
|
677
|
+
return [{
|
|
678
|
+
path: "styles.module.css",
|
|
679
|
+
contents: STARTER_CSS
|
|
680
|
+
}];
|
|
681
|
+
},
|
|
682
|
+
apply() {
|
|
683
|
+
return {
|
|
684
|
+
imports: ["import styles from './styles.module.css'"],
|
|
685
|
+
declarations: [],
|
|
686
|
+
headingTag: "h1",
|
|
687
|
+
hintTag: "p",
|
|
688
|
+
hintClassExpr: "styles.hint"
|
|
689
|
+
};
|
|
690
|
+
},
|
|
691
|
+
async checkPrerequisites() {
|
|
692
|
+
return satisfied("css-modules");
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
const scopedStrategy = {
|
|
696
|
+
method: "vue-sfc-scoped",
|
|
697
|
+
frameworks: STYLING_SUPPORT["vue-sfc-scoped"],
|
|
698
|
+
files() {
|
|
699
|
+
return [];
|
|
700
|
+
},
|
|
701
|
+
apply() {
|
|
702
|
+
return elementApplication([], `<style scoped>\n${STARTER_CSS}</style>`);
|
|
703
|
+
},
|
|
704
|
+
async checkPrerequisites() {
|
|
705
|
+
return satisfied("vue-sfc-scoped");
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
const scssStrategy = {
|
|
709
|
+
method: "scss",
|
|
710
|
+
frameworks: STYLING_SUPPORT["scss"],
|
|
711
|
+
files() {
|
|
712
|
+
return [{
|
|
713
|
+
path: "styles.scss",
|
|
714
|
+
contents: STARTER_CSS
|
|
715
|
+
}];
|
|
716
|
+
},
|
|
717
|
+
apply({ framework }) {
|
|
718
|
+
if (framework === "vue") return elementApplication([], "<style lang=\"scss\">\n@use './styles.scss';\n</style>");
|
|
719
|
+
return elementApplication(["import './styles.scss'"]);
|
|
720
|
+
},
|
|
721
|
+
async checkPrerequisites() {
|
|
722
|
+
return satisfied("scss");
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
const tailwindStrategy = {
|
|
726
|
+
method: "tailwind",
|
|
727
|
+
frameworks: STYLING_SUPPORT["tailwind"],
|
|
728
|
+
files() {
|
|
729
|
+
return [{
|
|
730
|
+
path: "styles.css",
|
|
731
|
+
contents: TAILWIND_CSS
|
|
732
|
+
}];
|
|
733
|
+
},
|
|
734
|
+
emptyFiles() {
|
|
735
|
+
return [{
|
|
736
|
+
path: "styles.css",
|
|
737
|
+
contents: TAILWIND_EMPTY_CSS
|
|
738
|
+
}];
|
|
739
|
+
},
|
|
740
|
+
apply({ framework }) {
|
|
741
|
+
if (framework === "vue") return tailwindApplication([], "<style>\n@import './styles.css';\n</style>");
|
|
742
|
+
return tailwindApplication(["import './styles.css'"]);
|
|
743
|
+
},
|
|
744
|
+
async checkPrerequisites({ projectRoot }) {
|
|
745
|
+
return requireDependency({
|
|
746
|
+
method: "tailwind",
|
|
747
|
+
dependency: "tailwindcss",
|
|
748
|
+
installed: await readProjectDependencies(projectRoot)
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
const linariaStrategy = {
|
|
753
|
+
method: "linaria",
|
|
754
|
+
frameworks: STYLING_SUPPORT["linaria"],
|
|
755
|
+
files() {
|
|
756
|
+
return [];
|
|
757
|
+
},
|
|
758
|
+
apply() {
|
|
759
|
+
return {
|
|
760
|
+
imports: ["import { css } from '@linaria/core'", "import { styled } from '@linaria/react'"],
|
|
761
|
+
declarations: [
|
|
762
|
+
LINARIA_HOST_STYLES,
|
|
763
|
+
LINARIA_TITLE,
|
|
764
|
+
LINARIA_HINT
|
|
765
|
+
],
|
|
766
|
+
headingTag: "Title",
|
|
767
|
+
hintTag: "Hint"
|
|
768
|
+
};
|
|
769
|
+
},
|
|
770
|
+
async checkPrerequisites({ projectRoot }) {
|
|
771
|
+
const installed = await readProjectDependencies(projectRoot);
|
|
772
|
+
return buildReport({
|
|
773
|
+
method: "linaria",
|
|
774
|
+
missingDependencies: STYLING_DEPENDENCIES.linaria.filter((dependency) => !installed.has(dependency)).map((name) => ({
|
|
775
|
+
name,
|
|
776
|
+
dev: true
|
|
777
|
+
})),
|
|
778
|
+
missingConfig: []
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
const vanillaExtractStrategy = {
|
|
783
|
+
method: "vanilla-extract",
|
|
784
|
+
frameworks: STYLING_SUPPORT["vanilla-extract"],
|
|
785
|
+
files() {
|
|
786
|
+
return [{
|
|
787
|
+
path: "styles.css.ts",
|
|
788
|
+
contents: STARTER_VANILLA_EXTRACT
|
|
789
|
+
}];
|
|
790
|
+
},
|
|
791
|
+
apply() {
|
|
792
|
+
return {
|
|
793
|
+
imports: ["import { title, hint } from './styles.css'"],
|
|
794
|
+
declarations: [],
|
|
795
|
+
headingTag: "h1",
|
|
796
|
+
hintTag: "p",
|
|
797
|
+
headingClassExpr: "title",
|
|
798
|
+
hintClassExpr: "hint"
|
|
799
|
+
};
|
|
800
|
+
},
|
|
801
|
+
async checkPrerequisites({ projectRoot }) {
|
|
802
|
+
return requireDependency({
|
|
803
|
+
method: "vanilla-extract",
|
|
804
|
+
dependency: "@vanilla-extract/css",
|
|
805
|
+
installed: await readProjectDependencies(projectRoot)
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
const PANDA_TITLE = `const title = css({
|
|
810
|
+
marginBlock: '0.5rem',
|
|
811
|
+
fontSize: '1.5rem'
|
|
812
|
+
})`;
|
|
813
|
+
const PANDA_HINT = `const hint = css({
|
|
814
|
+
margin: 0,
|
|
815
|
+
opacity: 0.5,
|
|
816
|
+
fontFamily: 'var(--font-mono)'
|
|
817
|
+
})`;
|
|
818
|
+
const pandaStrategy = {
|
|
819
|
+
method: "panda-css",
|
|
820
|
+
frameworks: STYLING_SUPPORT["panda-css"],
|
|
821
|
+
files() {
|
|
822
|
+
return [];
|
|
823
|
+
},
|
|
824
|
+
apply() {
|
|
825
|
+
return {
|
|
826
|
+
imports: ["import { css } from '../../styles/styled-system/css'", "import '../../styles/styles.css'"],
|
|
827
|
+
declarations: [PANDA_TITLE, PANDA_HINT],
|
|
828
|
+
headingTag: "h1",
|
|
829
|
+
hintTag: "p",
|
|
830
|
+
headingClassExpr: "title",
|
|
831
|
+
hintClassExpr: "hint"
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
async checkPrerequisites({ projectRoot }) {
|
|
835
|
+
return requireDependency({
|
|
836
|
+
method: "panda-css",
|
|
837
|
+
dependency: "@pandacss/dev",
|
|
838
|
+
installed: await readProjectDependencies(projectRoot)
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
async setupProject(args) {
|
|
842
|
+
const { projectRoot, formatConfigPath, documentsGlobBase } = args;
|
|
843
|
+
const result = await setupPanda({
|
|
844
|
+
projectRoot,
|
|
845
|
+
documentsDirName: documentsGlobBase,
|
|
846
|
+
formatConfigPath
|
|
847
|
+
});
|
|
848
|
+
const summary = [
|
|
849
|
+
result.pandaConfigCreated ? "Created panda.config.ts" : "Reused existing Panda config",
|
|
850
|
+
`Panda layer stylesheet ${result.layerCss}`,
|
|
851
|
+
`prepare script ${result.prepareScript}`,
|
|
852
|
+
result.formatConfigUpdated ? "Enabled pandaCss in the Format config" : "Left the Format config unchanged",
|
|
853
|
+
result.codegen.ok ? "Ran panda codegen" : `panda codegen failed: ${result.codegen.message ?? "unknown error"}`
|
|
854
|
+
];
|
|
855
|
+
return {
|
|
856
|
+
method: "panda-css",
|
|
857
|
+
ok: result.codegen.ok,
|
|
858
|
+
summary
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
const STYLING_STRATEGIES = [
|
|
863
|
+
cssStrategy,
|
|
864
|
+
cssModulesStrategy,
|
|
865
|
+
scopedStrategy,
|
|
866
|
+
scssStrategy,
|
|
867
|
+
tailwindStrategy,
|
|
868
|
+
linariaStrategy,
|
|
869
|
+
vanillaExtractStrategy,
|
|
870
|
+
pandaStrategy
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/scaffold/document/styling/index.ts
|
|
875
|
+
function resolveStyling(args) {
|
|
876
|
+
const { framework, method } = args;
|
|
877
|
+
const strategy = STYLING_STRATEGIES.find((candidate) => candidate.method === method);
|
|
878
|
+
if (!strategy) throw new CliError(`Unknown styling method "${method}". Available methods: ${STYLING_STRATEGIES.map((candidate) => candidate.method).join(", ")}.`);
|
|
879
|
+
if (!STYLING_SUPPORT[method].includes(framework)) throw new CliError(`The "${method}" styling method isn't supported for ${framework} projects. Supported methods: ${methodsForFramework(framework).join(", ")}.`);
|
|
880
|
+
return strategy;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region src/scaffold/document/data-schema.ts
|
|
885
|
+
function kindOf(value) {
|
|
886
|
+
if (value === null) return "null";
|
|
887
|
+
if (Array.isArray(value)) return "array";
|
|
888
|
+
if (typeof value === "object") return "object";
|
|
889
|
+
if (typeof value === "number") return "number";
|
|
890
|
+
if (typeof value === "boolean") return "boolean";
|
|
891
|
+
return "string";
|
|
892
|
+
}
|
|
893
|
+
function isPlainObject(value) {
|
|
894
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
895
|
+
}
|
|
896
|
+
function indent$1(text, tabs) {
|
|
897
|
+
const pad = " ".repeat(tabs);
|
|
898
|
+
return text.split("\n").map((line) => line ? pad + line : line).join("\n");
|
|
899
|
+
}
|
|
900
|
+
function keyLiteral(key) {
|
|
901
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
902
|
+
}
|
|
903
|
+
function jsonToTsType(value, depth = 0) {
|
|
904
|
+
const kind = kindOf(value);
|
|
905
|
+
if (kind === "string") return "string";
|
|
906
|
+
if (kind === "number") return "number";
|
|
907
|
+
if (kind === "boolean") return "boolean";
|
|
908
|
+
if (kind === "null") return "null";
|
|
909
|
+
if (kind === "array") {
|
|
910
|
+
const items = value;
|
|
911
|
+
return `${items.length > 0 ? jsonToTsType(items[0], depth) : "unknown"}[]`;
|
|
912
|
+
}
|
|
913
|
+
const entries = Object.entries(value);
|
|
914
|
+
if (entries.length === 0) return "Record<string, unknown>";
|
|
915
|
+
return `{\n${indent$1(entries.map(([key, child]) => `${keyLiteral(key)}: ${jsonToTsType(child, depth + 1)}`).join("\n"), 1)}\n}`;
|
|
916
|
+
}
|
|
917
|
+
const DIALECTS = {
|
|
918
|
+
zod: {
|
|
919
|
+
string: "z.string()",
|
|
920
|
+
number: "z.number()",
|
|
921
|
+
boolean: "z.boolean()",
|
|
922
|
+
null: "z.null()",
|
|
923
|
+
unknown: "z.unknown()",
|
|
924
|
+
object: (body) => `z.object(${body})`,
|
|
925
|
+
array: (element) => `z.array(${element})`
|
|
926
|
+
},
|
|
927
|
+
valibot: {
|
|
928
|
+
string: "v.string()",
|
|
929
|
+
number: "v.number()",
|
|
930
|
+
boolean: "v.boolean()",
|
|
931
|
+
null: "v.null()",
|
|
932
|
+
unknown: "v.unknown()",
|
|
933
|
+
object: (body) => `v.object(${body})`,
|
|
934
|
+
array: (element) => `v.array(${element})`
|
|
935
|
+
},
|
|
936
|
+
yup: {
|
|
937
|
+
string: "yup.string()",
|
|
938
|
+
number: "yup.number()",
|
|
939
|
+
boolean: "yup.boolean()",
|
|
940
|
+
null: "yup.mixed()",
|
|
941
|
+
unknown: "yup.mixed()",
|
|
942
|
+
object: (body) => `yup.object(${body})`,
|
|
943
|
+
array: (element) => `yup.array(${element})`
|
|
944
|
+
},
|
|
945
|
+
joi: {
|
|
946
|
+
string: "Joi.string()",
|
|
947
|
+
number: "Joi.number()",
|
|
948
|
+
boolean: "Joi.boolean()",
|
|
949
|
+
null: "Joi.any()",
|
|
950
|
+
unknown: "Joi.any()",
|
|
951
|
+
object: (body) => `Joi.object(${body})`,
|
|
952
|
+
array: (element) => `Joi.array().items(${element})`
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
function jsonToSchemaExpr(value, dialect) {
|
|
956
|
+
const syntax = DIALECTS[dialect];
|
|
957
|
+
const kind = kindOf(value);
|
|
958
|
+
if (kind === "string") return syntax.string;
|
|
959
|
+
if (kind === "number") return syntax.number;
|
|
960
|
+
if (kind === "boolean") return syntax.boolean;
|
|
961
|
+
if (kind === "null") return syntax.null;
|
|
962
|
+
if (kind === "array") {
|
|
963
|
+
const items = value;
|
|
964
|
+
const element = items.length > 0 ? jsonToSchemaExpr(items[0], dialect) : syntax.unknown;
|
|
965
|
+
return syntax.array(element);
|
|
966
|
+
}
|
|
967
|
+
const entries = Object.entries(value);
|
|
968
|
+
if (entries.length === 0) return syntax.object("{}");
|
|
969
|
+
const lines = entries.map(([key, child]) => `${keyLiteral(key)}: ${jsonToSchemaExpr(child, dialect)}`);
|
|
970
|
+
return syntax.object(`{\n${indent$1(lines.join(",\n"), 1)}\n}`);
|
|
971
|
+
}
|
|
972
|
+
function firstStringField(value) {
|
|
973
|
+
if (!isPlainObject(value)) return null;
|
|
974
|
+
for (const [key, child] of Object.entries(value)) if (typeof child === "string") return key;
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/scaffold/document/templates/utils.ts
|
|
980
|
+
function starterHint(documentName, entryFile) {
|
|
981
|
+
return `Edit documents/${documentName}/${entryFile} to get started`;
|
|
982
|
+
}
|
|
983
|
+
function indent(text, tabs) {
|
|
984
|
+
const pad = " ".repeat(tabs);
|
|
985
|
+
return text.split("\n").map((line) => line ? pad + line : line).join("\n");
|
|
986
|
+
}
|
|
987
|
+
function capitalize(word) {
|
|
988
|
+
return word ? word[0].toUpperCase() + word.slice(1) : "";
|
|
989
|
+
}
|
|
990
|
+
function words(name) {
|
|
991
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
992
|
+
}
|
|
993
|
+
function pascalCase(name) {
|
|
994
|
+
return words(name).map((word) => capitalize(word.toLowerCase())).join("");
|
|
995
|
+
}
|
|
996
|
+
function titleize(name) {
|
|
997
|
+
return words(name).map((word) => capitalize(word.toLowerCase())).join(" ");
|
|
998
|
+
}
|
|
999
|
+
function dataTypeName(name) {
|
|
1000
|
+
return `${pascalCase(name)}Data`;
|
|
1001
|
+
}
|
|
1002
|
+
function block(lines) {
|
|
1003
|
+
return lines.filter(Boolean).join("\n");
|
|
1004
|
+
}
|
|
1005
|
+
function buildClassAttr(args) {
|
|
1006
|
+
const { literal, expr, classAttr, classExprAttr } = args;
|
|
1007
|
+
if (expr && classExprAttr) return classExprAttr(expr);
|
|
1008
|
+
if (literal) return classAttr(literal);
|
|
1009
|
+
return "";
|
|
1010
|
+
}
|
|
1011
|
+
function buildBody(args) {
|
|
1012
|
+
const { application, title, hint, classAttr, classExprAttr } = args;
|
|
1013
|
+
const headingClassAttr = buildClassAttr({
|
|
1014
|
+
literal: application.headingClass,
|
|
1015
|
+
expr: application.headingClassExpr,
|
|
1016
|
+
classAttr,
|
|
1017
|
+
classExprAttr
|
|
1018
|
+
});
|
|
1019
|
+
const hintClassAttr = buildClassAttr({
|
|
1020
|
+
literal: application.hintClass,
|
|
1021
|
+
expr: application.hintClassExpr,
|
|
1022
|
+
classAttr,
|
|
1023
|
+
classExprAttr
|
|
1024
|
+
});
|
|
1025
|
+
const lines = `${`<${application.headingTag}${headingClassAttr}>${title}</${application.headingTag}>`}\n${`<${application.hintTag}${hintClassAttr}>${hint}</${application.hintTag}>`}`;
|
|
1026
|
+
if (!application.wrapperTag) return lines;
|
|
1027
|
+
return `<${application.wrapperTag}>\n${indent(lines, 1)}\n</${application.wrapperTag}>`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
//#endregion
|
|
1031
|
+
//#region src/scaffold/document/templates/react.ts
|
|
1032
|
+
function buildReactEntry(args) {
|
|
1033
|
+
const { documentName, application, width, height, emitSchema, data, empty } = args;
|
|
1034
|
+
const component = `${pascalCase(documentName)}Document`;
|
|
1035
|
+
const dataType = dataTypeName(documentName);
|
|
1036
|
+
if (empty) return {
|
|
1037
|
+
path: "index.tsx",
|
|
1038
|
+
contents: `${block(["import { Document, Layout } from '@format.dev/react'", ...application.imports])}
|
|
1039
|
+
|
|
1040
|
+
export default function ${component}() {
|
|
1041
|
+
return (
|
|
1042
|
+
<Document title="${titleize(documentName)}">
|
|
1043
|
+
<Layout width={${width}} height={${height}}>
|
|
1044
|
+
{/* Your content goes here */}
|
|
1045
|
+
</Layout>
|
|
1046
|
+
</Document>
|
|
1047
|
+
)
|
|
1048
|
+
}
|
|
1049
|
+
`
|
|
1050
|
+
};
|
|
1051
|
+
const head = block([
|
|
1052
|
+
"import { Document, Layout, type RenderProps } from '@format.dev/react'",
|
|
1053
|
+
emitSchema ? `import type { ${dataType} } from './data/schema'` : "",
|
|
1054
|
+
...application.imports
|
|
1055
|
+
]);
|
|
1056
|
+
const field = firstStringField(data);
|
|
1057
|
+
return {
|
|
1058
|
+
path: "index.tsx",
|
|
1059
|
+
contents: `${head}
|
|
1060
|
+
${emitSchema ? "" : `\ntype ${dataType} = ${jsonToTsType(data)}\n`}${application.declarations.length > 0 ? `\n${application.declarations.join("\n\n")}\n` : ""}
|
|
1061
|
+
export default function ${component}({ data }: RenderProps<${dataType}>) {
|
|
1062
|
+
return (
|
|
1063
|
+
<Document title=${field ? `{data.${field}}` : `'${titleize(documentName)}'`}>
|
|
1064
|
+
<Layout width={${width}} height={${height}}>
|
|
1065
|
+
${indent(buildBody({
|
|
1066
|
+
application,
|
|
1067
|
+
title: field ? `{data.${field}}` : titleize(documentName),
|
|
1068
|
+
hint: starterHint(documentName, "index.tsx"),
|
|
1069
|
+
classAttr: (cls) => ` className="${cls}"`,
|
|
1070
|
+
classExprAttr: (expr) => ` className={${expr}}`
|
|
1071
|
+
}), 4)}
|
|
1072
|
+
</Layout>
|
|
1073
|
+
</Document>
|
|
1074
|
+
)
|
|
1075
|
+
}
|
|
1076
|
+
`
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region src/scaffold/document/templates/vue.ts
|
|
1082
|
+
function buildVueEntry(args) {
|
|
1083
|
+
const { documentName, application, width, height, emitSchema, data, empty } = args;
|
|
1084
|
+
const dataType = dataTypeName(documentName);
|
|
1085
|
+
const trailingStyles = application.vueStyleBlock ? `\n${application.vueStyleBlock}\n` : "\n";
|
|
1086
|
+
if (empty) return {
|
|
1087
|
+
path: "index.vue",
|
|
1088
|
+
contents: `<script setup lang="ts">
|
|
1089
|
+
${block(["import { Document, Layout } from '@format.dev/vue'", ...application.imports])}
|
|
1090
|
+
<\/script>
|
|
1091
|
+
|
|
1092
|
+
<template>
|
|
1093
|
+
<Document title="${titleize(documentName)}">
|
|
1094
|
+
<Layout :width="${width}" :height="${height}">
|
|
1095
|
+
<!-- Your content goes here -->
|
|
1096
|
+
</Layout>
|
|
1097
|
+
</Document>
|
|
1098
|
+
</template>
|
|
1099
|
+
${trailingStyles}`
|
|
1100
|
+
};
|
|
1101
|
+
const scriptImports = block([
|
|
1102
|
+
"import { Document, Layout, type RenderProps } from '@format.dev/vue'",
|
|
1103
|
+
emitSchema ? `import type { ${dataType} } from './data/schema'` : "",
|
|
1104
|
+
...application.imports
|
|
1105
|
+
]);
|
|
1106
|
+
const field = firstStringField(data);
|
|
1107
|
+
return {
|
|
1108
|
+
path: "index.vue",
|
|
1109
|
+
contents: `<script setup lang="ts">
|
|
1110
|
+
${scriptImports}
|
|
1111
|
+
${emitSchema ? "" : `\ntype ${dataType} = ${jsonToTsType(data)}\n`}${application.declarations.length > 0 ? `\n${application.declarations.join("\n\n")}\n` : ""}
|
|
1112
|
+
const props = defineProps<RenderProps<${dataType}>>()
|
|
1113
|
+
<\/script>
|
|
1114
|
+
|
|
1115
|
+
<template>
|
|
1116
|
+
<Document ${field ? `:title="props.data.${field}"` : `title="${titleize(documentName)}"`}>
|
|
1117
|
+
<Layout :width="${width}" :height="${height}">
|
|
1118
|
+
${indent(buildBody({
|
|
1119
|
+
application,
|
|
1120
|
+
title: field ? `{{ props.data.${field} }}` : titleize(documentName),
|
|
1121
|
+
hint: starterHint(documentName, "index.vue"),
|
|
1122
|
+
classAttr: (cls) => ` class="${cls}"`,
|
|
1123
|
+
classExprAttr: (expr) => ` :class="${expr}"`
|
|
1124
|
+
}), 3)}
|
|
1125
|
+
</Layout>
|
|
1126
|
+
</Document>
|
|
1127
|
+
</template>
|
|
1128
|
+
${trailingStyles}`
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/scaffold/document/templates/html.ts
|
|
1134
|
+
function buildHtmlEntry(args) {
|
|
1135
|
+
const { documentName, application, width, height, data, empty } = args;
|
|
1136
|
+
const field = firstStringField(data);
|
|
1137
|
+
const title = empty || !field ? titleize(documentName) : `<%= data.${field} %>`;
|
|
1138
|
+
const html = `<template data-type="document" data-title="${title}">
|
|
1139
|
+
<template data-type="layout" data-width="${width}px" data-height="${height}px">
|
|
1140
|
+
${indent(empty ? "<!-- Your content goes here -->" : buildBody({
|
|
1141
|
+
application,
|
|
1142
|
+
title,
|
|
1143
|
+
hint: starterHint(documentName, "index.html"),
|
|
1144
|
+
classAttr: (cls) => ` class="${cls}"`
|
|
1145
|
+
}), 2)}
|
|
1146
|
+
</template>
|
|
1147
|
+
</template>
|
|
1148
|
+
`;
|
|
1149
|
+
const indexTs = block(["export { default } from './index.html'", ...application.imports]) + "\n";
|
|
1150
|
+
return [{
|
|
1151
|
+
path: "index.html",
|
|
1152
|
+
contents: html
|
|
1153
|
+
}, {
|
|
1154
|
+
path: "index.ts",
|
|
1155
|
+
contents: indexTs
|
|
1156
|
+
}];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/scaffold/document/templates/index.ts
|
|
1161
|
+
function buildEntryFiles(args) {
|
|
1162
|
+
const { framework } = args;
|
|
1163
|
+
if (framework === "vue") return [buildVueEntry(args)];
|
|
1164
|
+
if (framework === "html") return buildHtmlEntry(args);
|
|
1165
|
+
return [buildReactEntry(args)];
|
|
1166
|
+
}
|
|
1167
|
+
function buildDataFile(data) {
|
|
1168
|
+
return {
|
|
1169
|
+
path: "data/default.json",
|
|
1170
|
+
contents: `${JSON.stringify(data, null, " ")}\n`
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
const SCHEMA_HEADER = {
|
|
1174
|
+
zod: "import { z } from 'zod'",
|
|
1175
|
+
valibot: "import * as v from 'valibot'",
|
|
1176
|
+
yup: "import * as yup from 'yup'",
|
|
1177
|
+
joi: "import Joi from 'joi'"
|
|
1178
|
+
};
|
|
1179
|
+
function schemaTypeExport(kind, typeName, data) {
|
|
1180
|
+
if (kind === "zod") return `export type ${typeName} = z.infer<typeof schema>`;
|
|
1181
|
+
if (kind === "valibot") return `export type ${typeName} = v.InferOutput<typeof schema>`;
|
|
1182
|
+
if (kind === "yup") return `export type ${typeName} = yup.InferType<typeof schema>`;
|
|
1183
|
+
return `export type ${typeName} = ${jsonToTsType(data)}`;
|
|
1184
|
+
}
|
|
1185
|
+
function buildSchemaFile(documentName, kind = DEFAULT_SCHEMA_KIND, data) {
|
|
1186
|
+
const typeName = dataTypeName(documentName);
|
|
1187
|
+
const schemaExpr = jsonToSchemaExpr(data, kind);
|
|
1188
|
+
return {
|
|
1189
|
+
path: "data/schema.ts",
|
|
1190
|
+
contents: `${SCHEMA_HEADER[kind]}
|
|
1191
|
+
|
|
1192
|
+
const schema = ${schemaExpr}
|
|
1193
|
+
|
|
1194
|
+
export default schema
|
|
1195
|
+
|
|
1196
|
+
${schemaTypeExport(kind, typeName, data)}
|
|
1197
|
+
`
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function buildGitkeepFile() {
|
|
1201
|
+
return {
|
|
1202
|
+
path: "assets/.gitkeep",
|
|
1203
|
+
contents: ""
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
//#endregion
|
|
1208
|
+
//#region src/scaffold/document/format.ts
|
|
1209
|
+
const PARSER_BY_EXTENSION = {
|
|
1210
|
+
".tsx": "typescript",
|
|
1211
|
+
".ts": "typescript",
|
|
1212
|
+
".jsx": "babel",
|
|
1213
|
+
".js": "babel",
|
|
1214
|
+
".vue": "vue",
|
|
1215
|
+
".css": "css",
|
|
1216
|
+
".scss": "scss",
|
|
1217
|
+
".json": "json"
|
|
1218
|
+
};
|
|
1219
|
+
const FALLBACK_OPTIONS = {
|
|
1220
|
+
useTabs: true,
|
|
1221
|
+
singleQuote: true,
|
|
1222
|
+
semi: false
|
|
1223
|
+
};
|
|
1224
|
+
const prettierByProject = /* @__PURE__ */ new Map();
|
|
1225
|
+
function loadProjectPrettier(projectRoot) {
|
|
1226
|
+
const cached = prettierByProject.get(projectRoot);
|
|
1227
|
+
if (cached) return cached;
|
|
1228
|
+
const loading = (async () => {
|
|
1229
|
+
try {
|
|
1230
|
+
const module = await import(pathToFileURL(createRequire(join(projectRoot, "package.json")).resolve("prettier")).href);
|
|
1231
|
+
return "format" in module ? module : module.default;
|
|
1232
|
+
} catch {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
})();
|
|
1236
|
+
prettierByProject.set(projectRoot, loading);
|
|
1237
|
+
return loading;
|
|
1238
|
+
}
|
|
1239
|
+
async function formatGeneratedFile(args) {
|
|
1240
|
+
const { contents, filePath, projectRoot } = args;
|
|
1241
|
+
const parser = PARSER_BY_EXTENSION[extname(filePath)];
|
|
1242
|
+
if (!parser || !contents.trim()) return contents;
|
|
1243
|
+
const prettier = await loadProjectPrettier(projectRoot);
|
|
1244
|
+
if (!prettier) return contents;
|
|
1245
|
+
try {
|
|
1246
|
+
const projectConfig = await prettier.resolveConfig(filePath);
|
|
1247
|
+
return await prettier.format(contents, {
|
|
1248
|
+
...FALLBACK_OPTIONS,
|
|
1249
|
+
...projectConfig,
|
|
1250
|
+
parser
|
|
1251
|
+
});
|
|
1252
|
+
} catch {
|
|
1253
|
+
return contents;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/scaffold/document/create-document.ts
|
|
1259
|
+
const DOCUMENTS_DIR_NAME = "documents";
|
|
1260
|
+
function getDocumentsDir(args) {
|
|
1261
|
+
const { projectRoot, config } = args;
|
|
1262
|
+
return join(resolve(projectRoot, config.rootDir || "./"), DOCUMENTS_DIR_NAME);
|
|
1263
|
+
}
|
|
1264
|
+
async function readExistingDocumentNames(documentsDir) {
|
|
1265
|
+
try {
|
|
1266
|
+
return (await fs.readdir(documentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => entry.name);
|
|
1267
|
+
} catch {
|
|
1268
|
+
return [];
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async function writeFiles(args) {
|
|
1272
|
+
const { documentDir, files, projectRoot } = args;
|
|
1273
|
+
await Promise.all(files.map(async (file) => {
|
|
1274
|
+
const fullPath = join(documentDir, file.path);
|
|
1275
|
+
await fs.mkdir(dirname(fullPath), { recursive: true });
|
|
1276
|
+
const contents = await formatGeneratedFile({
|
|
1277
|
+
contents: file.contents,
|
|
1278
|
+
filePath: fullPath,
|
|
1279
|
+
projectRoot
|
|
1280
|
+
});
|
|
1281
|
+
await fs.writeFile(fullPath, contents, "utf8");
|
|
1282
|
+
}));
|
|
1283
|
+
}
|
|
1284
|
+
function isSideEffectImport(statement) {
|
|
1285
|
+
return /^\s*import\s+['"]/.test(statement);
|
|
1286
|
+
}
|
|
1287
|
+
function toEmptyApplication(application) {
|
|
1288
|
+
const vueStyleBlockPullsInFile = application.vueStyleBlock ? /@(import|use)/.test(application.vueStyleBlock) : false;
|
|
1289
|
+
return {
|
|
1290
|
+
imports: application.imports.filter(isSideEffectImport),
|
|
1291
|
+
declarations: [],
|
|
1292
|
+
headingTag: application.headingTag,
|
|
1293
|
+
hintTag: application.hintTag,
|
|
1294
|
+
vueStyleBlock: vueStyleBlockPullsInFile ? application.vueStyleBlock : void 0
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function emptyStylingFiles(strategy, args) {
|
|
1298
|
+
if (strategy.emptyFiles) return strategy.emptyFiles(args);
|
|
1299
|
+
return strategy.files(args).map((file) => ({
|
|
1300
|
+
...file,
|
|
1301
|
+
contents: EMPTY_STYLESHEET_COMMENT
|
|
1302
|
+
}));
|
|
1303
|
+
}
|
|
1304
|
+
async function buildDocumentPlan(args) {
|
|
1305
|
+
const { config, projectRoot, name, template = DEFAULT_TEMPLATE, schemaKind, width = DEFAULT_PAGE_WIDTH, height = DEFAULT_PAGE_HEIGHT, empty = false } = args;
|
|
1306
|
+
const styling = args.styling ?? DEFAULT_STYLING;
|
|
1307
|
+
const emitSchema = empty ? false : args.emitSchema ?? false;
|
|
1308
|
+
const data = empty ? {} : args.data !== void 0 ? args.data : { title: name };
|
|
1309
|
+
if (template !== "blank") throw new CliError(`Unknown template "${template}". Only "blank" is supported.`);
|
|
1310
|
+
const documentsDir = getDocumentsDir({
|
|
1311
|
+
projectRoot,
|
|
1312
|
+
config
|
|
1313
|
+
});
|
|
1314
|
+
const validation = validateDocumentName({
|
|
1315
|
+
name,
|
|
1316
|
+
existingNames: await readExistingDocumentNames(documentsDir)
|
|
1317
|
+
});
|
|
1318
|
+
if (!validation.ok) {
|
|
1319
|
+
const suffix = validation.suggestion ? ` Try "${validation.suggestion}".` : "";
|
|
1320
|
+
throw new CliError(`${validation.reason}${suffix}`);
|
|
1321
|
+
}
|
|
1322
|
+
const documentDir = join(documentsDir, name);
|
|
1323
|
+
const strategy = resolveStyling({
|
|
1324
|
+
framework: config.framework,
|
|
1325
|
+
method: styling
|
|
1326
|
+
});
|
|
1327
|
+
const exportName = getDocumentExportName(name);
|
|
1328
|
+
const applyArgs = {
|
|
1329
|
+
framework: config.framework,
|
|
1330
|
+
documentName: name,
|
|
1331
|
+
exportName
|
|
1332
|
+
};
|
|
1333
|
+
const fullApplication = strategy.apply(applyArgs);
|
|
1334
|
+
const application = empty ? toEmptyApplication(fullApplication) : fullApplication;
|
|
1335
|
+
const stylingFiles = empty ? emptyStylingFiles(strategy, applyArgs) : strategy.files(applyArgs);
|
|
1336
|
+
const files = [
|
|
1337
|
+
...buildEntryFiles({
|
|
1338
|
+
framework: config.framework,
|
|
1339
|
+
documentName: name,
|
|
1340
|
+
application,
|
|
1341
|
+
width,
|
|
1342
|
+
height,
|
|
1343
|
+
emitSchema,
|
|
1344
|
+
data,
|
|
1345
|
+
empty
|
|
1346
|
+
}),
|
|
1347
|
+
...stylingFiles,
|
|
1348
|
+
buildDataFile(data),
|
|
1349
|
+
buildGitkeepFile()
|
|
1350
|
+
];
|
|
1351
|
+
if (emitSchema) files.push(buildSchemaFile(name, schemaKind, data));
|
|
1352
|
+
return {
|
|
1353
|
+
documentName: name,
|
|
1354
|
+
documentDir,
|
|
1355
|
+
files,
|
|
1356
|
+
prerequisites: await strategy.checkPrerequisites({
|
|
1357
|
+
config,
|
|
1358
|
+
projectRoot
|
|
1359
|
+
}),
|
|
1360
|
+
projectRoot
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
async function writeDocument(plan) {
|
|
1364
|
+
try {
|
|
1365
|
+
await writeFiles({
|
|
1366
|
+
documentDir: plan.documentDir,
|
|
1367
|
+
files: plan.files,
|
|
1368
|
+
projectRoot: plan.projectRoot
|
|
1369
|
+
});
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
await fs.rm(plan.documentDir, {
|
|
1372
|
+
recursive: true,
|
|
1373
|
+
force: true
|
|
1374
|
+
});
|
|
1375
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1376
|
+
throw new CliError(`Failed to write document "${plan.documentName}": ${message}`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async function createDocument(args) {
|
|
1380
|
+
const plan = await buildDocumentPlan(args);
|
|
1381
|
+
await writeDocument(plan);
|
|
1382
|
+
return {
|
|
1383
|
+
documentName: plan.documentName,
|
|
1384
|
+
documentDir: plan.documentDir,
|
|
1385
|
+
filesWritten: plan.files.map((file) => file.path),
|
|
1386
|
+
prerequisites: plan.prerequisites
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
//#endregion
|
|
1391
|
+
//#region src/scaffold/document/random-name.ts
|
|
1392
|
+
function generateRandomName(args) {
|
|
1393
|
+
const { existingNames, maxAttempts = 10 } = args;
|
|
1394
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1395
|
+
const candidate = drawRandomName();
|
|
1396
|
+
if (validateDocumentName({
|
|
1397
|
+
name: candidate,
|
|
1398
|
+
existingNames
|
|
1399
|
+
}).ok) return candidate;
|
|
1400
|
+
}
|
|
1401
|
+
return suggestAvailableName({
|
|
1402
|
+
name: drawRandomName(),
|
|
1403
|
+
existingNames
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
//#endregion
|
|
1408
|
+
//#region src/scaffold/recommended-versions.ts
|
|
1409
|
+
const RECOMMENDED_DEPENDENCY_VERSIONS = {
|
|
1410
|
+
zod: "^4.4.3",
|
|
1411
|
+
valibot: "^1.4.2",
|
|
1412
|
+
tailwindcss: "^4.3.3",
|
|
1413
|
+
"@linaria/react": "^7.0.1",
|
|
1414
|
+
"@linaria/core": "^7.0.0",
|
|
1415
|
+
"@vanilla-extract/css": "^1.21.1",
|
|
1416
|
+
"@pandacss/dev": "^1.11.4",
|
|
1417
|
+
joi: "^18.2.3",
|
|
1418
|
+
yup: "^1.7.1"
|
|
1419
|
+
};
|
|
1420
|
+
function recommendedVersion(name) {
|
|
1421
|
+
return RECOMMENDED_DEPENDENCY_VERSIONS[name] ?? "latest";
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/scaffold/document/install.ts
|
|
1426
|
+
async function writeDocumentDependencies(args) {
|
|
1427
|
+
const { projectRoot, emitSchema, schemaKind, missingStylingDependencies } = args;
|
|
1428
|
+
const installed = await readProjectDependencies(projectRoot);
|
|
1429
|
+
const candidates = [...missingStylingDependencies];
|
|
1430
|
+
if (emitSchema) candidates.push(SCHEMA_DEPENDENCIES[schemaKind ?? DEFAULT_SCHEMA_KIND]);
|
|
1431
|
+
return writeDependenciesToManifest({
|
|
1432
|
+
projectDir: projectRoot,
|
|
1433
|
+
dependencies: [...new Set(candidates)].filter((name) => !installed.has(name)).map((name) => ({
|
|
1434
|
+
name,
|
|
1435
|
+
version: recommendedVersion(name),
|
|
1436
|
+
dev: true
|
|
1437
|
+
}))
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
async function maybeInstallDependencies(args) {
|
|
1441
|
+
const { wanted, projectRoot, emitSchema, schemaKind, missingStylingDependencies, onInstallStart } = args;
|
|
1442
|
+
const packages = await writeDocumentDependencies({
|
|
1443
|
+
projectRoot,
|
|
1444
|
+
emitSchema,
|
|
1445
|
+
schemaKind,
|
|
1446
|
+
missingStylingDependencies
|
|
1447
|
+
});
|
|
1448
|
+
if (!wanted || packages.length === 0) return {
|
|
1449
|
+
requested: wanted,
|
|
1450
|
+
attempted: false,
|
|
1451
|
+
ok: true,
|
|
1452
|
+
packages
|
|
1453
|
+
};
|
|
1454
|
+
onInstallStart?.(packages);
|
|
1455
|
+
const result = await installManifest({
|
|
1456
|
+
projectDir: projectRoot,
|
|
1457
|
+
onProgress: args.onProgress
|
|
1458
|
+
});
|
|
1459
|
+
return {
|
|
1460
|
+
requested: true,
|
|
1461
|
+
attempted: true,
|
|
1462
|
+
ok: result.ok,
|
|
1463
|
+
packages,
|
|
1464
|
+
packageManager: result.packageManager,
|
|
1465
|
+
message: result.message,
|
|
1466
|
+
output: result.output
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
async function maybeSetupProject(args) {
|
|
1470
|
+
const { wanted, styling, config, projectRoot, formatConfigPath, documentsGlobBase } = args;
|
|
1471
|
+
if (!wanted || styling === void 0) return null;
|
|
1472
|
+
const strategy = resolveStyling({
|
|
1473
|
+
framework: config.framework,
|
|
1474
|
+
method: styling
|
|
1475
|
+
});
|
|
1476
|
+
if (!strategy.setupProject) return null;
|
|
1477
|
+
return strategy.setupProject({
|
|
1478
|
+
config,
|
|
1479
|
+
projectRoot,
|
|
1480
|
+
formatConfigPath,
|
|
1481
|
+
documentsGlobBase
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
//#endregion
|
|
1486
|
+
export { joinNames as C, commandForPlatform as D, truncateForSpinner as E, detectPackageManager as O, installManifest as S, runInstall as T, RESERVED_WORDS as _, DOCUMENTS_DIR_NAME as a, DEFAULT_TEMPLATE as b, getDocumentsDir as c, resolveStyling as d, STYLING_STRATEGIES as f, validateDocumentName as g, suggestAvailableName as h, generateRandomName as i, installCommand as k, readExistingDocumentNames as l, RESERVED_DOCUMENT_NAMES as m, maybeSetupProject as n, buildDocumentPlan as o, readProjectDependencies as p, writeDocumentDependencies as r, createDocument as s, maybeInstallDependencies as t, writeDocument as u, buildDocumentExportMap as v, writeDependenciesToManifest as w, EMPTY_STYLESHEET_COMMENT as x, getDocumentExportName as y };
|