@usequark/quark-create-app 1.23.12 → 1.23.14
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/package.json +13 -1
- package/templates/base-project/.nvmrc +1 -0
- package/src/dependabot-config.test.js +0 -128
- package/src/deploy/adapters/railway.test.js +0 -318
- package/src/deploy/deploy.integration.test.js +0 -183
- package/src/deploy/deploy.test.js +0 -105
- package/src/deploy/discovery.test.js +0 -141
- package/src/mobile-template.test.js +0 -232
- package/src/prepare-hooks.test.js +0 -535
- package/src/scaffold-guards.test.js +0 -140
- package/src/scaffold-output.test.js +0 -278
- package/src/template-scripts.test.js +0 -160
- package/src/utils.test.js +0 -95
- package/src/worker-dockerfile.test.js +0 -85
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Scaffold output regression tests.
|
|
5
|
-
*
|
|
6
|
-
* A freshly scaffolded project must not contain any un-substituted
|
|
7
|
-
* __QUARK_* placeholders or @myquark package scopes. This guards the whole
|
|
8
|
-
* class of template-substitution bugs, including files outside the
|
|
9
|
-
* AI-context substitution list (docs, Railway config, embedded skills).
|
|
10
|
-
*
|
|
11
|
-
* Run with: node --test packages/cli/src/scaffold-output.test.js
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { spawnSync } from "node:child_process";
|
|
16
|
-
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
17
|
-
import { tmpdir } from "node:os";
|
|
18
|
-
import path from "node:path";
|
|
19
|
-
import { after, before, describe, it } from "node:test";
|
|
20
|
-
|
|
21
|
-
const CLI = path.join(import.meta.dirname, "index.js");
|
|
22
|
-
const PROJECT_NAME = "placeholder-app";
|
|
23
|
-
|
|
24
|
-
let tempDir;
|
|
25
|
-
let projectDir;
|
|
26
|
-
|
|
27
|
-
function runCLI(args, cwd) {
|
|
28
|
-
return spawnSync(process.execPath, [CLI, ...args], {
|
|
29
|
-
cwd,
|
|
30
|
-
encoding: "utf8",
|
|
31
|
-
timeout: 120_000,
|
|
32
|
-
env: { ...process.env, QUARK_SKIP_GIT_INIT: "true" },
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function collectFiles(dir, files = []) {
|
|
37
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
-
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
39
|
-
const fullPath = path.join(dir, entry.name);
|
|
40
|
-
if (entry.isDirectory()) {
|
|
41
|
-
collectFiles(fullPath, files);
|
|
42
|
-
} else {
|
|
43
|
-
files.push(fullPath);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return files;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
before(() => {
|
|
50
|
-
tempDir = mkdtempSync(path.join(tmpdir(), "quark-scaffold-output-"));
|
|
51
|
-
const result = runCLI(
|
|
52
|
-
[
|
|
53
|
-
PROJECT_NAME,
|
|
54
|
-
"--no-prompts",
|
|
55
|
-
"--packages",
|
|
56
|
-
"ui",
|
|
57
|
-
"--skip-install",
|
|
58
|
-
"--skip-docker",
|
|
59
|
-
],
|
|
60
|
-
tempDir,
|
|
61
|
-
);
|
|
62
|
-
assert.strictEqual(
|
|
63
|
-
result.status,
|
|
64
|
-
0,
|
|
65
|
-
`scaffold failed\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
|
66
|
-
);
|
|
67
|
-
projectDir = path.join(tempDir, PROJECT_NAME);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
after(() => {
|
|
71
|
-
if (tempDir) {
|
|
72
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("scaffold output placeholders", () => {
|
|
77
|
-
it("contains no un-substituted __QUARK_* or @myquark placeholders", () => {
|
|
78
|
-
const offenders = [];
|
|
79
|
-
|
|
80
|
-
for (const file of collectFiles(projectDir)) {
|
|
81
|
-
let content;
|
|
82
|
-
try {
|
|
83
|
-
content = fs.readFileSync(file, "utf-8");
|
|
84
|
-
} catch {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (/__QUARK_|@myquark/.test(content)) {
|
|
88
|
-
offenders.push(path.relative(projectDir, file));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
assert.deepStrictEqual(
|
|
93
|
-
offenders,
|
|
94
|
-
[],
|
|
95
|
-
`placeholder leaks found in: ${offenders.join(", ")}`,
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Guards the scope rewrite itself. replaceDepsScope and
|
|
100
|
-
// replaceImportsInSourceFiles map the framework scope onto the project's own
|
|
101
|
-
// scope for db/jobs/ui/config, and leave the published quark-core registry
|
|
102
|
-
// dependency alone on purpose.
|
|
103
|
-
//
|
|
104
|
-
// db, jobs, ui and config are local-only workspace packages and have never
|
|
105
|
-
// been published to any registry, so a framework-scoped reference surviving
|
|
106
|
-
// into generated output produces a project that cannot install. Nothing else
|
|
107
|
-
// catches that: the placeholder test above only looks for __QUARK_ and
|
|
108
|
-
// @myquark, so a rewrite rule that silently stopped matching - because a
|
|
109
|
-
// template and its rule drifted apart - left every test in this repo green
|
|
110
|
-
// while every scaffolded app was broken.
|
|
111
|
-
//
|
|
112
|
-
// Both scopes are asserted. @techstream is the pre-rename scope and must not
|
|
113
|
-
// appear at all; @usequark is the current one and must not appear for the
|
|
114
|
-
// local-only packages, though @usequark/quark-core is expected and correct.
|
|
115
|
-
//
|
|
116
|
-
// Scoped to files that participate in module resolution. Markdown and .mdc
|
|
117
|
-
// instruction files legitimately name the framework scope in order to tell an
|
|
118
|
-
// agent never to import it - .cursor/rules/quark.mdc ships exactly that
|
|
119
|
-
// prohibition, alongside deliberately-wrong import examples. Prose cannot
|
|
120
|
-
// break an install; an import or a dependency can.
|
|
121
|
-
it("never leaks a framework-scoped reference to a local-only package", () => {
|
|
122
|
-
const RESOLVABLE = /\.(js|jsx|mjs|cjs|ts|tsx)$/;
|
|
123
|
-
const localOnly = /@(techstream|usequark)\/quark-(db|jobs|ui|config)\b/;
|
|
124
|
-
const offenders = [];
|
|
125
|
-
|
|
126
|
-
for (const file of collectFiles(projectDir)) {
|
|
127
|
-
const rel = path.relative(projectDir, file);
|
|
128
|
-
const base = path.basename(file);
|
|
129
|
-
if (base !== "package.json" && !RESOLVABLE.test(base)) continue;
|
|
130
|
-
|
|
131
|
-
let content;
|
|
132
|
-
try {
|
|
133
|
-
content = fs.readFileSync(file, "utf-8");
|
|
134
|
-
} catch {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (localOnly.test(content)) {
|
|
138
|
-
offenders.push(rel);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
assert.deepStrictEqual(
|
|
143
|
-
offenders,
|
|
144
|
-
[],
|
|
145
|
-
`local-only packages must be rewritten to the project scope; leaked in: ${offenders.join(", ")}`,
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("rewrites the DB import in AI prompts to the project scope", () => {
|
|
150
|
-
const page = fs.readFileSync(
|
|
151
|
-
path.join(projectDir, "apps/web/src/app/page.js"),
|
|
152
|
-
"utf-8",
|
|
153
|
-
);
|
|
154
|
-
assert.ok(
|
|
155
|
-
page.includes(`@${PROJECT_NAME}/db`),
|
|
156
|
-
"AI prompt must reference @<scope>/db",
|
|
157
|
-
);
|
|
158
|
-
assert.ok(
|
|
159
|
-
!page.includes("@__QUARK_SCOPE__"),
|
|
160
|
-
"AI prompt must not keep the scope placeholder",
|
|
161
|
-
);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("substitutes the project name in docs and Railway config", () => {
|
|
165
|
-
const mainMd = fs.readFileSync(path.join(projectDir, "MAIN.md"), "utf-8");
|
|
166
|
-
assert.ok(
|
|
167
|
-
mainMd.startsWith(`# ${PROJECT_NAME}`),
|
|
168
|
-
"MAIN.md title must use the project name",
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const envExample = fs.readFileSync(
|
|
172
|
-
path.join(projectDir, ".env.railway.example"),
|
|
173
|
-
"utf-8",
|
|
174
|
-
);
|
|
175
|
-
assert.ok(
|
|
176
|
-
envExample.includes(`APP_NAME=${PROJECT_NAME}`),
|
|
177
|
-
".env.railway.example must use the project name",
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
const railway = fs.readFileSync(
|
|
181
|
-
path.join(projectDir, ".railway/railway.ts"),
|
|
182
|
-
"utf-8",
|
|
183
|
-
);
|
|
184
|
-
assert.ok(
|
|
185
|
-
railway.includes(`project("${PROJECT_NAME}"`),
|
|
186
|
-
".railway/railway.ts must use the project name",
|
|
187
|
-
);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("substitutes placeholders in embedded skills", () => {
|
|
191
|
-
const skill = fs.readFileSync(
|
|
192
|
-
path.join(projectDir, ".opencode/skills/payment/SKILL.md"),
|
|
193
|
-
"utf-8",
|
|
194
|
-
);
|
|
195
|
-
assert.ok(
|
|
196
|
-
skill.includes(`@${PROJECT_NAME}/db`),
|
|
197
|
-
"embedded skills must use the project scope",
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("generates a migration.sql that Postgres can execute", () => {
|
|
202
|
-
const migrationSql = fs.readFileSync(
|
|
203
|
-
path.join(
|
|
204
|
-
projectDir,
|
|
205
|
-
"packages/db/prisma/migrations/20260202061128_initial/migration.sql",
|
|
206
|
-
),
|
|
207
|
-
"utf-8",
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// Regression: capturing `prisma migrate diff --script` with a plain `>`
|
|
211
|
-
// also writes Prisma's stdout log line into the SQL. Postgres rejects the
|
|
212
|
-
// whole migration with `42601 syntax error at or near "Loaded"`, so every
|
|
213
|
-
// scaffolded project failed its first `db:migrate:deploy`.
|
|
214
|
-
const lines = migrationSql
|
|
215
|
-
.split("\n")
|
|
216
|
-
.map((line) => line.trim())
|
|
217
|
-
.filter((line) => line.length > 0);
|
|
218
|
-
|
|
219
|
-
assert.ok(
|
|
220
|
-
!lines.some((line) => /^Loaded Prisma config/.test(line)),
|
|
221
|
-
"migration.sql must not contain Prisma's stdout log line",
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const firstStatement = lines.find((line) => !line.startsWith("--"));
|
|
225
|
-
assert.ok(
|
|
226
|
-
/^(CREATE|ALTER|DROP|INSERT|UPDATE)\b/.test(firstStatement),
|
|
227
|
-
`migration.sql must begin with a SQL statement (got: ${firstStatement})`,
|
|
228
|
-
);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("scaffolds the PWA manifest as manifest.js, not a conflicting manifest.json", () => {
|
|
232
|
-
const appDir = path.join(projectDir, "apps/web/src/app");
|
|
233
|
-
|
|
234
|
-
// Next.js serves app/manifest.js at /manifest.webmanifest. A sibling
|
|
235
|
-
// app/manifest.json (what the PWA feature used to write) makes the build
|
|
236
|
-
// fail with "Cannot find module for page: /manifest.webmanifest".
|
|
237
|
-
assert.ok(
|
|
238
|
-
!fs.existsSync(path.join(appDir, "manifest.json")),
|
|
239
|
-
"app/manifest.json must not be scaffolded alongside app/manifest.js",
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const manifest = fs.readFileSync(path.join(appDir, "manifest.js"), "utf-8");
|
|
243
|
-
assert.ok(
|
|
244
|
-
!manifest.includes("@__QUARK_SCOPE__"),
|
|
245
|
-
"PWA manifest must use the project scope, not __QUARK_SCOPE__",
|
|
246
|
-
);
|
|
247
|
-
assert.ok(
|
|
248
|
-
manifest.includes(`@${PROJECT_NAME}/config`),
|
|
249
|
-
"PWA manifest must import config from the project scope",
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("generates a valid auth section in .env.example", () => {
|
|
254
|
-
const envExample = fs.readFileSync(
|
|
255
|
-
path.join(projectDir, ".env.example"),
|
|
256
|
-
"utf-8",
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
const secretLine = envExample
|
|
260
|
-
.split("\n")
|
|
261
|
-
.find((line) => line.startsWith("NEXTAUTH_SECRET="));
|
|
262
|
-
assert.ok(secretLine, ".env.example must define NEXTAUTH_SECRET");
|
|
263
|
-
const secret = secretLine.slice("NEXTAUTH_SECRET=".length);
|
|
264
|
-
assert.ok(
|
|
265
|
-
secret.length >= 32,
|
|
266
|
-
`NEXTAUTH_SECRET placeholder must be at least 32 characters (got ${secret.length}) - startup validation rejects shorter values`,
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
assert.ok(
|
|
270
|
-
!/NEXTAUTH_URL=.*\/api\/auth/.test(envExample),
|
|
271
|
-
"NEXTAUTH_URL must be a bare origin, never include a path",
|
|
272
|
-
);
|
|
273
|
-
assert.ok(
|
|
274
|
-
!envExample.includes("derived automatically from PORT"),
|
|
275
|
-
"APP_URL is not derived from PORT - do not claim it is",
|
|
276
|
-
);
|
|
277
|
-
});
|
|
278
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Behavior tests for scripts shipped in the scaffolded base project.
|
|
5
|
-
*
|
|
6
|
-
* These run the template copies directly against synthetic project layouts,
|
|
7
|
-
* so they catch drift between the template scripts and what scaffolded
|
|
8
|
-
* projects actually need.
|
|
9
|
-
*
|
|
10
|
-
* Run with: node --test packages/cli/src/template-scripts.test.js
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { spawnSync } from "node:child_process";
|
|
15
|
-
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
import { after, describe, it } from "node:test";
|
|
19
|
-
|
|
20
|
-
const TEMPLATE_DIR = path.join(
|
|
21
|
-
import.meta.dirname,
|
|
22
|
-
"../templates/base-project",
|
|
23
|
-
);
|
|
24
|
-
const tempDirs = [];
|
|
25
|
-
|
|
26
|
-
function makeTempDir() {
|
|
27
|
-
const dir = mkdtempSync(path.join(tmpdir(), "quark-template-scripts-"));
|
|
28
|
-
tempDirs.push(dir);
|
|
29
|
-
return dir;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function writeFile(root, relPath, content) {
|
|
33
|
-
const fullPath = path.join(root, relPath);
|
|
34
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
35
|
-
fs.writeFileSync(fullPath, content);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function copyScript(root, relPath) {
|
|
39
|
-
const dest = path.join(root, relPath);
|
|
40
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
41
|
-
fs.copyFileSync(path.join(TEMPLATE_DIR, relPath), dest);
|
|
42
|
-
return dest;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function runScript(root, relPath) {
|
|
46
|
-
return spawnSync(process.execPath, [path.join(root, relPath)], {
|
|
47
|
-
cwd: root,
|
|
48
|
-
encoding: "utf8",
|
|
49
|
-
timeout: 30_000,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
after(() => {
|
|
54
|
-
for (const dir of tempDirs) {
|
|
55
|
-
rmSync(dir, { recursive: true, force: true });
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("check-loading.mjs", () => {
|
|
60
|
-
it("flags a DB-backed page without a sibling loading.js", () => {
|
|
61
|
-
const dir = makeTempDir();
|
|
62
|
-
copyScript(dir, "scripts/check-loading.mjs");
|
|
63
|
-
writeFile(
|
|
64
|
-
dir,
|
|
65
|
-
"apps/web/src/app/orders/page.js",
|
|
66
|
-
'import { prisma } from "@demo/db";\n\nexport default function Page() {\n\treturn null;\n}\n',
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const result = runScript(dir, "scripts/check-loading.mjs");
|
|
70
|
-
|
|
71
|
-
assert.strictEqual(
|
|
72
|
-
result.status,
|
|
73
|
-
1,
|
|
74
|
-
"audit must exit 1 when loading.js is missing",
|
|
75
|
-
);
|
|
76
|
-
assert.match(result.stderr, /MISSING loading\.js/);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("passes when the DB-backed page has a sibling loading.js", () => {
|
|
80
|
-
const dir = makeTempDir();
|
|
81
|
-
copyScript(dir, "scripts/check-loading.mjs");
|
|
82
|
-
writeFile(
|
|
83
|
-
dir,
|
|
84
|
-
"apps/web/src/app/orders/page.js",
|
|
85
|
-
'import { prisma } from "@demo/db";\n\nexport default function Page() {\n\treturn null;\n}\n',
|
|
86
|
-
);
|
|
87
|
-
writeFile(
|
|
88
|
-
dir,
|
|
89
|
-
"apps/web/src/app/orders/loading.js",
|
|
90
|
-
"export default null;\n",
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const result = runScript(dir, "scripts/check-loading.mjs");
|
|
94
|
-
|
|
95
|
-
assert.strictEqual(result.status, 0, result.stderr);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("ignores pages that do not import a db package", () => {
|
|
99
|
-
const dir = makeTempDir();
|
|
100
|
-
copyScript(dir, "scripts/check-loading.mjs");
|
|
101
|
-
writeFile(
|
|
102
|
-
dir,
|
|
103
|
-
"apps/web/src/app/about/page.js",
|
|
104
|
-
'import { Button } from "@demo/ui";\n\nexport default function Page() {\n\treturn null;\n}\n',
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const result = runScript(dir, "scripts/check-loading.mjs");
|
|
108
|
-
|
|
109
|
-
assert.strictEqual(result.status, 0, result.stderr);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("also recognizes the monorepo db package name", () => {
|
|
113
|
-
const dir = makeTempDir();
|
|
114
|
-
copyScript(dir, "scripts/check-loading.mjs");
|
|
115
|
-
writeFile(
|
|
116
|
-
dir,
|
|
117
|
-
"apps/web/src/app/metrics/page.js",
|
|
118
|
-
'import { prisma } from "@usequark/quark-db";\n\nexport default function Page() {\n\treturn null;\n}\n',
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const result = runScript(dir, "scripts/check-loading.mjs");
|
|
122
|
-
|
|
123
|
-
assert.strictEqual(
|
|
124
|
-
result.status,
|
|
125
|
-
1,
|
|
126
|
-
"audit must exit 1 when loading.js is missing",
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("check-standards.mjs", () => {
|
|
132
|
-
it("allows TypeScript in apps/mobile after quark add mobile", () => {
|
|
133
|
-
const dir = makeTempDir();
|
|
134
|
-
copyScript(dir, "scripts/check-standards.mjs");
|
|
135
|
-
writeFile(
|
|
136
|
-
dir,
|
|
137
|
-
"apps/mobile/app/(app)/index.tsx",
|
|
138
|
-
"export default function Home() {\n\treturn null;\n}\n",
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const result = runScript(dir, "scripts/check-standards.mjs");
|
|
142
|
-
|
|
143
|
-
assert.strictEqual(result.status, 0, result.stderr);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("still flags console.* in web app runtime code", () => {
|
|
147
|
-
const dir = makeTempDir();
|
|
148
|
-
copyScript(dir, "scripts/check-standards.mjs");
|
|
149
|
-
writeFile(
|
|
150
|
-
dir,
|
|
151
|
-
"apps/web/src/app/orders/page.js",
|
|
152
|
-
'export default function Page() {\n\tconsole.log("orders");\n\treturn null;\n}\n',
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const result = runScript(dir, "scripts/check-standards.mjs");
|
|
156
|
-
|
|
157
|
-
assert.strictEqual(result.status, 1, "console.* must be flagged");
|
|
158
|
-
assert.match(result.stderr, /createLogger\(\)/);
|
|
159
|
-
});
|
|
160
|
-
});
|
package/src/utils.test.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { test } from "node:test";
|
|
6
|
-
import { execa } from "execa";
|
|
7
|
-
import { findEnclosingGitRepo, formatProjectDisplayName } from "./utils.js";
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Happy path - common slug patterns
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
test("formatProjectDisplayName - hyphen-separated slug", () => {
|
|
14
|
-
assert.strictEqual(formatProjectDisplayName("my-cool-app"), "My Cool App");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("formatProjectDisplayName - underscore-separated slug", () => {
|
|
18
|
-
assert.strictEqual(formatProjectDisplayName("my_app_v2"), "My App V2");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("formatProjectDisplayName - dot-separated slug", () => {
|
|
22
|
-
assert.strictEqual(formatProjectDisplayName("my.app"), "My App");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("formatProjectDisplayName - single word (no separators)", () => {
|
|
26
|
-
assert.strictEqual(formatProjectDisplayName("myapp"), "Myapp");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("formatProjectDisplayName - already title-cased input", () => {
|
|
30
|
-
// slice(1) preserves remaining chars as-is - only the first char is forced uppercase
|
|
31
|
-
assert.strictEqual(formatProjectDisplayName("MyApp"), "MyApp");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("formatProjectDisplayName - mixed separators", () => {
|
|
35
|
-
assert.strictEqual(formatProjectDisplayName("my-app_v2.0"), "My App V2 0");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("formatProjectDisplayName - consecutive separators collapse to single space", () => {
|
|
39
|
-
assert.strictEqual(formatProjectDisplayName("my--app"), "My App");
|
|
40
|
-
assert.strictEqual(formatProjectDisplayName("my___app"), "My App");
|
|
41
|
-
assert.strictEqual(formatProjectDisplayName("my-._app"), "My App");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("formatProjectDisplayName - numbers preserved in words", () => {
|
|
45
|
-
assert.strictEqual(formatProjectDisplayName("app-v2"), "App V2");
|
|
46
|
-
assert.strictEqual(formatProjectDisplayName("project-123"), "Project 123");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Edge cases - degenerate / boundary inputs
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
test("formatProjectDisplayName - all separators returns fallback", () => {
|
|
54
|
-
assert.strictEqual(formatProjectDisplayName("---"), "Quark App");
|
|
55
|
-
assert.strictEqual(formatProjectDisplayName("___"), "Quark App");
|
|
56
|
-
assert.strictEqual(formatProjectDisplayName("..."), "Quark App");
|
|
57
|
-
assert.strictEqual(formatProjectDisplayName("-._-"), "Quark App");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("formatProjectDisplayName - preserves casing of rest of word", () => {
|
|
61
|
-
// The function only forces the first char uppercase; the remainder is untouched
|
|
62
|
-
assert.strictEqual(formatProjectDisplayName("myApp"), "MyApp");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("formatProjectDisplayName - short single-char name", () => {
|
|
66
|
-
assert.strictEqual(formatProjectDisplayName("a"), "A");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// findEnclosingGitRepo
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
test("findEnclosingGitRepo - returns null outside a git work tree", async () => {
|
|
74
|
-
const dir = await mkdtemp(path.join(os.tmpdir(), "quark-utils-"));
|
|
75
|
-
try {
|
|
76
|
-
assert.strictEqual(await findEnclosingGitRepo(dir), null);
|
|
77
|
-
} finally {
|
|
78
|
-
await rm(dir, { recursive: true, force: true });
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("findEnclosingGitRepo - returns the repository root for any nested dir", async () => {
|
|
83
|
-
const dir = await mkdtemp(path.join(os.tmpdir(), "quark-utils-"));
|
|
84
|
-
try {
|
|
85
|
-
await execa("git", ["init"], { cwd: dir });
|
|
86
|
-
const nested = path.join(dir, "some", "nested", "dir");
|
|
87
|
-
await mkdir(nested, { recursive: true });
|
|
88
|
-
|
|
89
|
-
const expected = await realpath(dir);
|
|
90
|
-
assert.strictEqual(await findEnclosingGitRepo(dir), expected);
|
|
91
|
-
assert.strictEqual(await findEnclosingGitRepo(nested), expected);
|
|
92
|
-
} finally {
|
|
93
|
-
await rm(dir, { recursive: true, force: true });
|
|
94
|
-
}
|
|
95
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Worker Dockerfile guards.
|
|
5
|
-
*
|
|
6
|
-
* `pnpm deploy --prod` drops devDependencies but keeps the peer subtrees pnpm
|
|
7
|
-
* auto-installed *for* them. `prisma` (a devDependency of packages/db) declares
|
|
8
|
-
* `typescript` as an optional peer, so the hoisted store gains typescript@7 -
|
|
9
|
-
* the native Go build, which ships a ~100 MB Go `tsc` carrying 10 HIGH CVEs.
|
|
10
|
-
* The runtime worker image runs compiled JS and does not need it.
|
|
11
|
-
*
|
|
12
|
-
* These assertions run against the monorepo Dockerfile AND the scaffold
|
|
13
|
-
* template, so a regression cannot reappear in newly generated projects.
|
|
14
|
-
*
|
|
15
|
-
* Run with: node --test packages/cli/src/worker-dockerfile.test.js
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
|
-
import fs from "node:fs";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { describe, it } from "node:test";
|
|
22
|
-
import { fileURLToPath } from "node:url";
|
|
23
|
-
|
|
24
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const REPO_ROOT = path.resolve(__dirname, "../../..");
|
|
26
|
-
|
|
27
|
-
const DOCKERFILES = [
|
|
28
|
-
["monorepo", "apps/worker/Dockerfile"],
|
|
29
|
-
["scaffold template", "packages/cli/templates/worker/Dockerfile"],
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
describe("worker Dockerfile strips dev-only native binaries", () => {
|
|
33
|
-
for (const [label, relativePath] of DOCKERFILES) {
|
|
34
|
-
it(`${label} removes the auto-installed typescript subtree`, () => {
|
|
35
|
-
const dockerfile = fs.readFileSync(
|
|
36
|
-
path.join(REPO_ROOT, relativePath),
|
|
37
|
-
"utf-8",
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
assert.match(
|
|
41
|
-
dockerfile,
|
|
42
|
-
/\/app\/deploy\/node_modules\/\.pnpm\/typescript@\*/,
|
|
43
|
-
"must strip the auto-installed typescript subtree from the deploy output",
|
|
44
|
-
);
|
|
45
|
-
assert.match(
|
|
46
|
-
dockerfile,
|
|
47
|
-
/@typescript\+typescript-\*/,
|
|
48
|
-
"must strip the @typescript platform binaries (the Go tsc)",
|
|
49
|
-
);
|
|
50
|
-
assert.match(
|
|
51
|
-
dockerfile,
|
|
52
|
-
/\*_typescript@\*/,
|
|
53
|
-
"must strip peer-suffixed directories such as valibot@1.4.2_typescript@7.0.2",
|
|
54
|
-
);
|
|
55
|
-
assert.match(
|
|
56
|
-
dockerfile,
|
|
57
|
-
/dev-only TypeScript binary leaked/,
|
|
58
|
-
"must fail the build if a dev-only binary reappears",
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it(`${label} keeps the pruning after the deploy step`, () => {
|
|
63
|
-
const dockerfile = fs.readFileSync(
|
|
64
|
-
path.join(REPO_ROOT, relativePath),
|
|
65
|
-
"utf-8",
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const deployIndex = dockerfile.indexOf("prod deploy");
|
|
69
|
-
const pruneIndex = dockerfile.indexOf("node_modules/.pnpm/typescript@*");
|
|
70
|
-
const copyIndex = dockerfile.lastIndexOf(
|
|
71
|
-
"COPY --from=builder /app/deploy/",
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
assert.ok(
|
|
75
|
-
deployIndex > -1,
|
|
76
|
-
"Dockerfile must still use `pnpm deploy --prod`",
|
|
77
|
-
);
|
|
78
|
-
assert.ok(pruneIndex > deployIndex, "pruning must run after the deploy");
|
|
79
|
-
assert.ok(
|
|
80
|
-
pruneIndex < copyIndex,
|
|
81
|
-
"pruning must run before the deploy output is copied into the runtime stage",
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
});
|