create-projx 1.5.5 → 1.5.6

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.
@@ -0,0 +1,27 @@
1
+ import {
2
+ BASELINE_REF,
3
+ applyTemplate,
4
+ buildDisplayNames,
5
+ buildPathsUpper,
6
+ collectAllFiles,
7
+ detectPackageNameOverrides,
8
+ getBaselineRef,
9
+ getFileAtRef,
10
+ matchesSkip,
11
+ saveBaselineRef,
12
+ writeTemplateToDir
13
+ } from "./chunk-G74HYIE4.js";
14
+ import "./chunk-FTHX7ILT.js";
15
+ export {
16
+ BASELINE_REF,
17
+ applyTemplate,
18
+ buildDisplayNames,
19
+ buildPathsUpper,
20
+ collectAllFiles,
21
+ detectPackageNameOverrides,
22
+ getBaselineRef,
23
+ getFileAtRef,
24
+ matchesSkip,
25
+ saveBaselineRef,
26
+ writeTemplateToDir
27
+ };
@@ -0,0 +1,424 @@
1
+ // src/utils.ts
2
+ import { execSync } from "child_process";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { cp, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
5
+ import { join, resolve } from "path";
6
+ import { tmpdir } from "os";
7
+ import { fileURLToPath } from "url";
8
+ var REPO = "ukanhaupa/projx";
9
+ var REPO_URL = `https://github.com/${REPO}`;
10
+ var COMPONENTS = [
11
+ "fastapi",
12
+ "fastify",
13
+ "frontend",
14
+ "mobile",
15
+ "e2e",
16
+ "infra"
17
+ ];
18
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
19
+ function pmCommands(pm) {
20
+ switch (pm) {
21
+ case "npm":
22
+ return { name: "npm", install: "npm install", ci: "npm ci", run: "npm run", exec: "npx", dlx: "npx", lockfile: "package-lock.json", prismaExec: "npx prisma", runDev: "npm run dev" };
23
+ case "pnpm":
24
+ return { name: "pnpm", install: "pnpm install", ci: "pnpm install --frozen-lockfile", run: "pnpm", exec: "pnpm exec", dlx: "pnpm dlx", lockfile: "pnpm-lock.yaml", prismaExec: "pnpm prisma", runDev: "pnpm dev" };
25
+ case "yarn":
26
+ return { name: "yarn", install: "yarn", ci: "yarn --frozen-lockfile", run: "yarn", exec: "yarn", dlx: "yarn dlx", lockfile: "yarn.lock", prismaExec: "yarn prisma", runDev: "yarn dev" };
27
+ case "bun":
28
+ return { name: "bun", install: "bun install", ci: "bun install --frozen-lockfile", run: "bun run", exec: "bunx", dlx: "bunx", lockfile: "bun.lockb", prismaExec: "bunx prisma", runDev: "bun run dev" };
29
+ }
30
+ }
31
+ function detectPackageManager(cwd) {
32
+ if (existsSync(join(cwd, "bun.lockb"))) return "bun";
33
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
34
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
35
+ if (existsSync(join(cwd, "package-lock.json"))) return "npm";
36
+ return null;
37
+ }
38
+ function detectPackageManagerFromComponents(cwd, componentPaths) {
39
+ const jsComponents = ["fastify", "frontend", "e2e"];
40
+ for (const component of jsComponents) {
41
+ const dir = componentPaths[component];
42
+ if (!dir) continue;
43
+ const fullDir = join(cwd, dir);
44
+ if (!existsSync(fullDir)) continue;
45
+ const detected = detectPackageManager(fullDir);
46
+ if (detected) return detected;
47
+ }
48
+ return detectPackageManager(cwd);
49
+ }
50
+ function toKebab(s) {
51
+ return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
52
+ }
53
+ function toSnake(s) {
54
+ return toKebab(s).replace(/-/g, "_");
55
+ }
56
+ function toTitle(s) {
57
+ return s.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
58
+ }
59
+ function hasCommand(cmd) {
60
+ try {
61
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ function exec(cmd, cwd) {
68
+ execSync(cmd, { cwd, stdio: "pipe" });
69
+ }
70
+ function sharedTemplateDir() {
71
+ const thisFile = fileURLToPath(import.meta.url);
72
+ return join(thisFile, "../../src/templates");
73
+ }
74
+ async function downloadRepo(localPath) {
75
+ if (localPath) {
76
+ return localPath;
77
+ }
78
+ const dest = join(tmpdir(), `projx-${Date.now()}`);
79
+ await mkdir(dest, { recursive: true });
80
+ if (hasCommand("git")) {
81
+ execSync(
82
+ `git clone --depth 1 ${REPO_URL}.git "${dest}/repo"`,
83
+ { stdio: "pipe" }
84
+ );
85
+ return join(dest, "repo");
86
+ }
87
+ const tarUrl = `${REPO_URL}/archive/refs/heads/main.tar.gz`;
88
+ execSync(
89
+ `curl -sL "${tarUrl}" | tar xz -C "${dest}"`,
90
+ { stdio: "pipe" }
91
+ );
92
+ const entries = await readdir(dest);
93
+ const extracted = entries.find((e) => e.startsWith("projx-"));
94
+ if (!extracted) throw new Error("Failed to extract repo archive.");
95
+ return join(dest, extracted);
96
+ }
97
+ async function cleanupRepo(repoDir, isLocal) {
98
+ if (isLocal) return;
99
+ const parent = resolve(repoDir, "..");
100
+ if (parent.startsWith(tmpdir())) {
101
+ await rm(parent, { recursive: true, force: true });
102
+ }
103
+ }
104
+ var EXCLUDE = /* @__PURE__ */ new Set([
105
+ "node_modules",
106
+ "dist",
107
+ "build",
108
+ "coverage",
109
+ "__pycache__",
110
+ ".dart_tool",
111
+ ".flutter-plugins",
112
+ ".flutter-plugins-dependencies",
113
+ ".venv",
114
+ ".pytest_cache",
115
+ ".ruff_cache",
116
+ ".mypy_cache",
117
+ "playwright-report",
118
+ "test-results",
119
+ ".terraform",
120
+ "cli"
121
+ ]);
122
+ var EXCLUDE_FILES = /* @__PURE__ */ new Set([
123
+ "uv.lock",
124
+ "pnpm-lock.yaml",
125
+ "package-lock.json",
126
+ "pubspec.lock",
127
+ ".env",
128
+ ".env.dev",
129
+ ".env.staging",
130
+ ".env.prod",
131
+ "dev.tfplan",
132
+ ".coverage"
133
+ ]);
134
+ async function copyComponent(repoDir, component, dest) {
135
+ const src = join(repoDir, component);
136
+ const out = join(dest, component);
137
+ const files = [];
138
+ await cp(src, out, {
139
+ recursive: true,
140
+ filter: (source) => {
141
+ const base = source.split("/").pop();
142
+ if (EXCLUDE.has(base)) return false;
143
+ if (EXCLUDE_FILES.has(base)) return false;
144
+ if (base.endsWith(".pyc")) return false;
145
+ return true;
146
+ }
147
+ });
148
+ await collectFiles(out, out, files);
149
+ return files;
150
+ }
151
+ async function copyStaticFiles(repoDir, dest) {
152
+ const manifest = [];
153
+ const tpl = repoDir;
154
+ const statics = [".editorconfig"];
155
+ for (const file of statics) {
156
+ const src = join(tpl, file);
157
+ if (existsSync(src)) {
158
+ await cp(src, join(dest, file));
159
+ manifest.push(file);
160
+ }
161
+ }
162
+ const extensionsJson = join(tpl, ".vscode/extensions.json");
163
+ if (existsSync(extensionsJson)) {
164
+ await mkdir(join(dest, ".vscode"), { recursive: true });
165
+ await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
166
+ manifest.push(".vscode/extensions.json");
167
+ }
168
+ const scripts = join(tpl, "scripts");
169
+ if (existsSync(scripts)) {
170
+ await cp(scripts, join(dest, "scripts"), { recursive: true });
171
+ manifest.push("scripts/setup-ssl.sh");
172
+ }
173
+ return manifest;
174
+ }
175
+ async function collectFiles(dir, root, files) {
176
+ const entries = await readdir(dir, { withFileTypes: true });
177
+ for (const entry of entries) {
178
+ const full = join(dir, entry.name);
179
+ if (entry.isDirectory()) {
180
+ await collectFiles(full, root, files);
181
+ } else {
182
+ files.push(full.slice(root.length + 1));
183
+ }
184
+ }
185
+ }
186
+ async function replaceInFile(filePath, find, replace) {
187
+ if (!existsSync(filePath)) return;
188
+ const content = await readFile(filePath, "utf-8");
189
+ if (!content.includes(find)) return;
190
+ await writeFile(filePath, content.replaceAll(find, replace));
191
+ }
192
+ async function replaceInDir(dir, find, replace, ext) {
193
+ if (!existsSync(dir)) return;
194
+ const entries = await readdir(dir, { withFileTypes: true });
195
+ for (const entry of entries) {
196
+ const full = join(dir, entry.name);
197
+ if (entry.isDirectory()) {
198
+ await replaceInDir(full, find, replace, ext);
199
+ } else if (entry.name.endsWith(ext)) {
200
+ await replaceInFile(full, find, replace);
201
+ }
202
+ }
203
+ }
204
+ var COMPONENT_MARKER = ".projx-component";
205
+ async function readFileOrNull(path) {
206
+ try {
207
+ return await readFile(path, "utf-8");
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+ function parseMarker(raw) {
213
+ try {
214
+ const data = JSON.parse(raw);
215
+ let component;
216
+ if (typeof data.component === "string" && COMPONENTS.includes(data.component)) {
217
+ component = data.component;
218
+ } else if (Array.isArray(data.components) && data.components.length > 0) {
219
+ const first = data.components[0];
220
+ if (typeof first === "string" && COMPONENTS.includes(first)) {
221
+ component = first;
222
+ }
223
+ }
224
+ if (!component) return null;
225
+ return {
226
+ component,
227
+ skip: Array.isArray(data.skip) ? data.skip : []
228
+ };
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+ async function readComponentMarker(dir) {
234
+ const raw = await readFileOrNull(join(dir, COMPONENT_MARKER));
235
+ if (!raw) return null;
236
+ return parseMarker(raw);
237
+ }
238
+ async function writeComponentMarker(dir, data) {
239
+ const markerPath = join(dir, COMPONENT_MARKER);
240
+ const out = {
241
+ component: data.component,
242
+ skip: Array.isArray(data.skip) ? data.skip : []
243
+ };
244
+ await writeFile(markerPath, JSON.stringify(out, null, 2) + "\n");
245
+ }
246
+ async function upsertComponentMarker(dir, component, skip) {
247
+ const existing = await readComponentMarker(dir);
248
+ await writeComponentMarker(dir, {
249
+ component,
250
+ skip: skip ?? existing?.skip ?? []
251
+ });
252
+ }
253
+ async function readProjxConfig(cwd) {
254
+ const path = join(cwd, ".projx");
255
+ if (!existsSync(path)) return {};
256
+ try {
257
+ return JSON.parse(await readFile(path, "utf-8"));
258
+ } catch {
259
+ return {};
260
+ }
261
+ }
262
+ async function writeProjxConfig(cwd, data) {
263
+ const path = join(cwd, ".projx");
264
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
265
+ const out = { ...data };
266
+ if (typeof out.createdAt !== "string") out.createdAt = today;
267
+ if (!Array.isArray(out.skip)) out.skip = [];
268
+ await writeFile(path, JSON.stringify(out, null, 2) + "\n");
269
+ }
270
+ var DEFAULT_ROOT_SKIP_PATTERNS = [
271
+ "docker-compose.yml",
272
+ "docker-compose.dev.yml",
273
+ "README.md",
274
+ ".githooks/pre-commit",
275
+ ".github/workflows/ci.yml",
276
+ "setup.sh"
277
+ ];
278
+ var DEFAULT_COMPONENT_SKIP_PATTERNS = {
279
+ fastapi: ["pyproject.toml"],
280
+ fastify: ["package.json"],
281
+ frontend: ["package.json"],
282
+ e2e: ["package.json"],
283
+ mobile: ["pubspec.yaml"]
284
+ };
285
+ async function discoverComponentPaths(cwd, components) {
286
+ const { paths: discovered } = await discoverComponentsFromMarkers(cwd);
287
+ const paths = { ...discovered };
288
+ for (const c of components) {
289
+ if (!paths[c]) paths[c] = c;
290
+ }
291
+ return paths;
292
+ }
293
+ async function discoverComponentsFromMarkers(cwd) {
294
+ const components = [];
295
+ const paths = {};
296
+ if (!existsSync(cwd)) return { components, paths };
297
+ const entries = await readdir(cwd, { withFileTypes: true });
298
+ for (const entry of entries) {
299
+ if (!entry.isDirectory()) continue;
300
+ if (EXCLUDE.has(entry.name)) continue;
301
+ if (entry.name.startsWith(".")) continue;
302
+ const marker = await readComponentMarker(join(cwd, entry.name));
303
+ if (!marker) continue;
304
+ if (!components.includes(marker.component)) {
305
+ components.push(marker.component);
306
+ paths[marker.component] = entry.name;
307
+ }
308
+ }
309
+ for (const c of components) {
310
+ if (!paths[c]) paths[c] = c;
311
+ }
312
+ return { components, paths };
313
+ }
314
+ function render(template, vars) {
315
+ const components = vars.components;
316
+ const projectName = vars.projectName;
317
+ const lines = template.split("\n");
318
+ const output = [];
319
+ const stack = [];
320
+ for (const line of lines) {
321
+ const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
322
+ if (ifMatch) {
323
+ const pmName = vars.pm?.name ?? "npm";
324
+ const fn = new Function("components", "projectName", "pm", `return ${ifMatch[1]}`);
325
+ const result = fn(components, projectName, pmName);
326
+ stack.push({ active: result, matched: result });
327
+ continue;
328
+ }
329
+ const elseIfMatch = line.match(/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/);
330
+ if (elseIfMatch) {
331
+ if (stack.length > 0) {
332
+ const top = stack[stack.length - 1];
333
+ if (top.matched) {
334
+ top.active = false;
335
+ } else {
336
+ const pmN = vars.pm?.name ?? "npm";
337
+ const fn = new Function("components", "projectName", "pm", `return ${elseIfMatch[1]}`);
338
+ const result = fn(components, projectName, pmN);
339
+ top.active = result;
340
+ if (result) top.matched = true;
341
+ }
342
+ }
343
+ continue;
344
+ }
345
+ if (/^<%\s*\}\s*else\s*\{?\s*%>$/.test(line)) {
346
+ if (stack.length > 0) {
347
+ const top = stack[stack.length - 1];
348
+ top.active = !top.matched;
349
+ }
350
+ continue;
351
+ }
352
+ if (/^<%\s*\}?\s*%>$/.test(line)) {
353
+ stack.pop();
354
+ continue;
355
+ }
356
+ if (stack.length > 0 && stack.some((v) => !v.active)) continue;
357
+ const replaced = line.replace(
358
+ /<%=\s*([\w.]+)\s*%>/g,
359
+ (_, expr) => {
360
+ const parts = expr.split(".");
361
+ let val = vars;
362
+ for (const p of parts) {
363
+ val = val?.[p];
364
+ }
365
+ return String(val ?? "");
366
+ }
367
+ );
368
+ output.push(replaced);
369
+ }
370
+ return output.join("\n").replace(/\n{3,}/g, "\n\n");
371
+ }
372
+ function detectProjectName(cwd, components, componentPaths) {
373
+ for (const component of components) {
374
+ const dir = componentPaths[component] ?? component;
375
+ const pkgPath = join(cwd, dir, "package.json");
376
+ if (existsSync(pkgPath)) {
377
+ try {
378
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
379
+ const n = pkg.name;
380
+ if (n && n.includes("-")) {
381
+ return n.substring(0, n.lastIndexOf("-"));
382
+ }
383
+ } catch {
384
+ }
385
+ }
386
+ }
387
+ return toKebab(cwd.split("/").pop());
388
+ }
389
+
390
+ export {
391
+ REPO,
392
+ REPO_URL,
393
+ COMPONENTS,
394
+ PACKAGE_MANAGERS,
395
+ pmCommands,
396
+ detectPackageManager,
397
+ detectPackageManagerFromComponents,
398
+ toKebab,
399
+ toSnake,
400
+ toTitle,
401
+ hasCommand,
402
+ exec,
403
+ sharedTemplateDir,
404
+ downloadRepo,
405
+ cleanupRepo,
406
+ EXCLUDE,
407
+ copyComponent,
408
+ copyStaticFiles,
409
+ replaceInFile,
410
+ replaceInDir,
411
+ COMPONENT_MARKER,
412
+ readFileOrNull,
413
+ readComponentMarker,
414
+ writeComponentMarker,
415
+ upsertComponentMarker,
416
+ readProjxConfig,
417
+ writeProjxConfig,
418
+ DEFAULT_ROOT_SKIP_PATTERNS,
419
+ DEFAULT_COMPONENT_SKIP_PATTERNS,
420
+ discoverComponentPaths,
421
+ discoverComponentsFromMarkers,
422
+ render,
423
+ detectProjectName
424
+ };