@usequark/quark-create-app 1.23.11 → 1.23.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -26
- package/package.json +67 -55
- package/src/index.js +17 -17
- package/src/utils.js +1 -1
- package/templates/base-project/.cursor/rules/quark.mdc +17 -17
- package/templates/base-project/.github/copilot-instructions.md +5 -5
- package/templates/base-project/.github/skills/project-context/SKILL.md +6 -6
- package/templates/base-project/CLAUDE.md +12 -12
- package/templates/base-project/README.md +3 -3
- package/templates/base-project/apps/web/next.config.js +5 -5
- package/templates/base-project/apps/web/package.json +5 -5
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +1 -1
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +3 -3
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/error-handler.js +1 -1
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/files/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/health/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/users/route.js +2 -2
- package/templates/base-project/apps/web/src/app/auth/error/page.js +1 -1
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +1 -1
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +1 -1
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +1 -1
- package/templates/base-project/apps/web/src/app/layout.js +1 -1
- package/templates/base-project/apps/web/src/app/manifest.js +1 -1
- package/templates/base-project/apps/web/src/app/page.js +19 -19
- package/templates/base-project/apps/web/src/app/robots.js +1 -1
- package/templates/base-project/apps/web/src/app/sitemap.js +1 -1
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth-signup.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth.js +2 -2
- package/templates/base-project/apps/web/src/lib/jwt.js +1 -1
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +1 -1
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +1 -1
- package/templates/base-project/apps/web/src/preflight.js +3 -3
- package/templates/base-project/apps/web/src/proxy.js +1 -1
- package/templates/base-project/docs/START_HERE.md +2 -2
- package/templates/base-project/packages/db/package.json +2 -2
- package/templates/base-project/packages/db/src/client.js +3 -3
- package/templates/base-project/packages/db/src/umami.js +1 -1
- package/templates/base-project/scripts/check-loading.mjs +1 -1
- package/templates/base-project/scripts/doctor.js +1 -1
- package/templates/base-project/skills/add-endpoint/SKILL.md +1 -1
- package/templates/base-project/skills/ecommerce/SKILL.md +2 -2
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +3 -3
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +3 -3
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +2 -2
- package/templates/base-project/skills/i18n/SKILL.md +6 -6
- package/templates/base-project/skills/payment/SKILL.md +8 -8
- package/templates/config/package.json +1 -1
- package/templates/config/src/app-url.js +1 -1
- package/templates/config/src/environment.js +1 -1
- package/templates/config/src/load-config.js +2 -2
- package/templates/config/src/redis.js +1 -1
- package/templates/config/src/validate-env.js +2 -2
- package/templates/jobs/README.md +2 -2
- package/templates/mobile/lib/errors.ts +1 -1
- package/templates/ui/src/theme-constants.js +1 -1
- package/templates/worker/README.md +2 -2
- package/templates/worker/package.json +4 -4
- package/templates/worker/src/handlers/email-job-validation.js +1 -1
- package/templates/worker/src/handlers/email.js +5 -5
- package/templates/worker/src/handlers/files.js +4 -4
- package/templates/worker/src/handlers/index.js +1 -1
- package/templates/worker/src/handlers/push-notification.js +3 -3
- package/templates/worker/src/index.js +6 -6
- package/templates/worker/src/lib/railway.js +2 -2
- package/templates/worker/src/lib/tokens.js +1 -1
- 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 -228
- package/src/template-scripts.test.js +0 -160
- package/src/utils.test.js +0 -95
- package/src/worker-dockerfile.test.js +0 -85
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Tests for the scaffolded project's scripts/prepare.js git hook setup.
|
|
4
|
-
* Run with: node --test packages/cli/src/prepare-hooks.test.js
|
|
5
|
-
*/
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
|
-
import crypto from "node:crypto";
|
|
9
|
-
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { after, describe, it } from "node:test";
|
|
13
|
-
import {
|
|
14
|
-
getGitRoot,
|
|
15
|
-
getHooksDir,
|
|
16
|
-
installScopedHooks,
|
|
17
|
-
main,
|
|
18
|
-
readHookConfig,
|
|
19
|
-
} from "../templates/base-project/scripts/prepare.js";
|
|
20
|
-
|
|
21
|
-
const tempDirs = [];
|
|
22
|
-
|
|
23
|
-
function makeTempDir() {
|
|
24
|
-
const dir = mkdtempSync(path.join(tmpdir(), "quark-prepare-hooks-"));
|
|
25
|
-
tempDirs.push(dir);
|
|
26
|
-
return dir;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
after(() => {
|
|
30
|
-
for (const dir of tempDirs) {
|
|
31
|
-
try {
|
|
32
|
-
rmSync(dir, { recursive: true, force: true });
|
|
33
|
-
} catch {
|
|
34
|
-
// best-effort cleanup
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
function git(args, cwd, opts = {}) {
|
|
40
|
-
return execFileSync("git", args, {
|
|
41
|
-
cwd,
|
|
42
|
-
encoding: "utf8",
|
|
43
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
44
|
-
...opts,
|
|
45
|
-
}).trim();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function initRepo(dir) {
|
|
49
|
-
git(["init", "-q"], dir);
|
|
50
|
-
git(["config", "user.email", "test@quark.local"], dir);
|
|
51
|
-
git(["config", "user.name", "Quark Test"], dir);
|
|
52
|
-
return dir;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function makeProject(repoRoot, name, hookConfig) {
|
|
56
|
-
const projectDir = path.join(repoRoot, name);
|
|
57
|
-
fs.mkdirSync(path.join(projectDir, "scripts"), { recursive: true });
|
|
58
|
-
fs.writeFileSync(
|
|
59
|
-
path.join(projectDir, "package.json"),
|
|
60
|
-
`${JSON.stringify(
|
|
61
|
-
{
|
|
62
|
-
name: `@test/${name}`,
|
|
63
|
-
private: true,
|
|
64
|
-
scripts: { prepare: "node scripts/prepare.js" },
|
|
65
|
-
"simple-git-hooks": hookConfig,
|
|
66
|
-
},
|
|
67
|
-
null,
|
|
68
|
-
"\t",
|
|
69
|
-
)}\n`,
|
|
70
|
-
);
|
|
71
|
-
return projectDir;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function hooksDirFor(repoRoot) {
|
|
75
|
-
return path.join(repoRoot, ".git", "hooks");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function scopedHooksDirFor(repoRoot) {
|
|
79
|
-
return path.join(hooksDirFor(repoRoot), "quark-scoped-hooks");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function entriesDirFor(repoRoot) {
|
|
83
|
-
return path.join(scopedHooksDirFor(repoRoot), "entries");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function entryName(relativeProjectPath, hook) {
|
|
87
|
-
const hash = crypto
|
|
88
|
-
.createHash("sha256")
|
|
89
|
-
.update(relativeProjectPath)
|
|
90
|
-
.digest("hex")
|
|
91
|
-
.slice(0, 16);
|
|
92
|
-
return `${hash}.${hook}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function readEntry(repoRoot, relativeProjectPath, hook) {
|
|
96
|
-
return JSON.parse(
|
|
97
|
-
fs.readFileSync(
|
|
98
|
-
path.join(entriesDirFor(repoRoot), entryName(relativeProjectPath, hook)),
|
|
99
|
-
"utf8",
|
|
100
|
-
),
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function writeHook(dir, name, contents) {
|
|
105
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
106
|
-
fs.writeFileSync(path.join(dir, name), contents);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function readHook(dir, name) {
|
|
110
|
-
return fs.readFileSync(path.join(dir, name), "utf8");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function silentLogger() {
|
|
114
|
-
return { log() {}, warn() {} };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const isExecutable = (file) => (fs.statSync(file).mode & 0o111) !== 0;
|
|
118
|
-
|
|
119
|
-
describe("prepare.js git context detection", () => {
|
|
120
|
-
it("returns null outside a git repository", () => {
|
|
121
|
-
const dir = makeTempDir();
|
|
122
|
-
assert.equal(getGitRoot(dir), null);
|
|
123
|
-
assert.equal(getHooksDir(dir), null);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("finds the enclosing repository for a nested project", () => {
|
|
127
|
-
const repoRoot = initRepo(makeTempDir());
|
|
128
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
129
|
-
"pre-commit": "pnpm nano-staged",
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
assert.equal(
|
|
133
|
-
fs.realpathSync(getGitRoot(projectDir)),
|
|
134
|
-
fs.realpathSync(repoRoot),
|
|
135
|
-
);
|
|
136
|
-
assert.equal(
|
|
137
|
-
fs.realpathSync(getHooksDir(projectDir)),
|
|
138
|
-
fs.realpathSync(hooksDirFor(repoRoot)),
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("respects a relative core.hooksPath", () => {
|
|
143
|
-
const repoRoot = initRepo(makeTempDir());
|
|
144
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
145
|
-
"pre-commit": "pnpm nano-staged",
|
|
146
|
-
});
|
|
147
|
-
git(["config", "core.hooksPath", ".githooks"], repoRoot);
|
|
148
|
-
|
|
149
|
-
assert.equal(
|
|
150
|
-
getHooksDir(projectDir),
|
|
151
|
-
path.join(fs.realpathSync(repoRoot), ".githooks"),
|
|
152
|
-
);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe("scoped hook installation for nested projects", () => {
|
|
157
|
-
it("installs an executable shim, a dispatcher, and a project entry", () => {
|
|
158
|
-
const repoRoot = initRepo(makeTempDir());
|
|
159
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
160
|
-
"pre-commit": "pnpm nano-staged",
|
|
161
|
-
"pre-push": "pnpm check:loading && pnpm test",
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const result = installScopedHooks({ projectDir, logger: silentLogger() });
|
|
165
|
-
assert.deepEqual(result.installed.sort(), ["pre-commit", "pre-push"]);
|
|
166
|
-
assert.deepEqual(result.skipped, []);
|
|
167
|
-
|
|
168
|
-
const shimPath = path.join(hooksDirFor(repoRoot), "pre-commit");
|
|
169
|
-
const shim = readHook(hooksDirFor(repoRoot), "pre-commit");
|
|
170
|
-
assert.ok(isExecutable(shimPath));
|
|
171
|
-
assert.ok(shim.includes("quark:scoped-git-hook"));
|
|
172
|
-
assert.ok(shim.includes("quark-scoped-hooks/dispatch.js"));
|
|
173
|
-
assert.ok(shim.includes('" pre-commit'));
|
|
174
|
-
assert.ok(
|
|
175
|
-
fs.existsSync(path.join(scopedHooksDirFor(repoRoot), "dispatch.js")),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
assert.deepEqual(readEntry(repoRoot, "my-app", "pre-commit"), {
|
|
179
|
-
project: "my-app",
|
|
180
|
-
command: "pnpm nano-staged",
|
|
181
|
-
});
|
|
182
|
-
assert.deepEqual(readEntry(repoRoot, "my-app", "pre-push"), {
|
|
183
|
-
project: "my-app",
|
|
184
|
-
command: "pnpm check:loading && pnpm test",
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("is idempotent", () => {
|
|
189
|
-
const repoRoot = initRepo(makeTempDir());
|
|
190
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
191
|
-
"pre-commit": "pnpm nano-staged",
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
installScopedHooks({ projectDir, logger: silentLogger() });
|
|
195
|
-
const firstShim = readHook(hooksDirFor(repoRoot), "pre-commit");
|
|
196
|
-
const firstDispatcher = fs.readFileSync(
|
|
197
|
-
path.join(scopedHooksDirFor(repoRoot), "dispatch.js"),
|
|
198
|
-
"utf8",
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const result = installScopedHooks({ projectDir, logger: silentLogger() });
|
|
202
|
-
|
|
203
|
-
assert.deepEqual(result.installed, ["pre-commit"]);
|
|
204
|
-
assert.equal(readHook(hooksDirFor(repoRoot), "pre-commit"), firstShim);
|
|
205
|
-
assert.equal(
|
|
206
|
-
fs.readFileSync(
|
|
207
|
-
path.join(scopedHooksDirFor(repoRoot), "dispatch.js"),
|
|
208
|
-
"utf8",
|
|
209
|
-
),
|
|
210
|
-
firstDispatcher,
|
|
211
|
-
);
|
|
212
|
-
assert.equal(fs.readdirSync(entriesDirFor(repoRoot)).length, 1);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("does not overwrite foreign hooks or remove unrelated hooks", () => {
|
|
216
|
-
const repoRoot = initRepo(makeTempDir());
|
|
217
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
218
|
-
"pre-commit": "pnpm nano-staged",
|
|
219
|
-
"pre-push": "pnpm test",
|
|
220
|
-
});
|
|
221
|
-
const hooksDir = hooksDirFor(repoRoot);
|
|
222
|
-
writeHook(hooksDir, "pre-commit", "#!/bin/sh\necho custom hook\n");
|
|
223
|
-
writeHook(hooksDir, "commit-msg", "#!/bin/sh\necho commit-msg\n");
|
|
224
|
-
|
|
225
|
-
const result = installScopedHooks({ projectDir, logger: silentLogger() });
|
|
226
|
-
|
|
227
|
-
assert.deepEqual(result.installed, ["pre-push"]);
|
|
228
|
-
assert.deepEqual(result.skipped, ["pre-commit"]);
|
|
229
|
-
assert.equal(
|
|
230
|
-
readHook(hooksDir, "pre-commit"),
|
|
231
|
-
"#!/bin/sh\necho custom hook\n",
|
|
232
|
-
);
|
|
233
|
-
assert.equal(
|
|
234
|
-
readHook(hooksDir, "commit-msg"),
|
|
235
|
-
"#!/bin/sh\necho commit-msg\n",
|
|
236
|
-
);
|
|
237
|
-
// The skipped hook must not leave a stale entry behind either.
|
|
238
|
-
assert.equal(
|
|
239
|
-
fs.existsSync(
|
|
240
|
-
path.join(entriesDirFor(repoRoot), entryName("my-app", "pre-commit")),
|
|
241
|
-
),
|
|
242
|
-
false,
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("replaces hooks previously installed by simple-git-hooks", () => {
|
|
247
|
-
const repoRoot = initRepo(makeTempDir());
|
|
248
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
249
|
-
"pre-commit": "pnpm nano-staged",
|
|
250
|
-
});
|
|
251
|
-
const hooksDir = hooksDirFor(repoRoot);
|
|
252
|
-
writeHook(
|
|
253
|
-
hooksDir,
|
|
254
|
-
"pre-commit",
|
|
255
|
-
'#!/bin/sh\nif [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then\n exit 0\nfi\npnpm nano-staged',
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
const result = installScopedHooks({ projectDir, logger: silentLogger() });
|
|
259
|
-
|
|
260
|
-
assert.deepEqual(result.installed, ["pre-commit"]);
|
|
261
|
-
assert.ok(
|
|
262
|
-
readHook(hooksDir, "pre-commit").includes("quark:scoped-git-hook"),
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("skips installation when the project is at the repository root", () => {
|
|
267
|
-
const repoRoot = initRepo(makeTempDir());
|
|
268
|
-
const hooksDir = hooksDirFor(repoRoot);
|
|
269
|
-
writeHook(hooksDir, "pre-commit", "#!/bin/sh\necho root project\n");
|
|
270
|
-
fs.writeFileSync(
|
|
271
|
-
path.join(repoRoot, "package.json"),
|
|
272
|
-
JSON.stringify({
|
|
273
|
-
"simple-git-hooks": { "pre-commit": "pnpm nano-staged" },
|
|
274
|
-
}),
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const result = installScopedHooks({
|
|
278
|
-
projectDir: repoRoot,
|
|
279
|
-
logger: silentLogger(),
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
assert.deepEqual(result.installed, []);
|
|
283
|
-
assert.equal(result.reason, "project-is-git-root");
|
|
284
|
-
assert.equal(
|
|
285
|
-
readHook(hooksDir, "pre-commit"),
|
|
286
|
-
"#!/bin/sh\necho root project\n",
|
|
287
|
-
);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("composes hooks from multiple nested projects instead of overwriting", () => {
|
|
291
|
-
const repoRoot = initRepo(makeTempDir());
|
|
292
|
-
const alpha = makeProject(repoRoot, "alpha", {
|
|
293
|
-
"pre-commit": "pwd > .hook-cwd",
|
|
294
|
-
});
|
|
295
|
-
const beta = makeProject(repoRoot, "beta", {
|
|
296
|
-
"pre-commit": "pwd > .hook-cwd",
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
installScopedHooks({ projectDir: alpha, logger: silentLogger() });
|
|
300
|
-
const shimAfterAlpha = readHook(hooksDirFor(repoRoot), "pre-commit");
|
|
301
|
-
installScopedHooks({ projectDir: beta, logger: silentLogger() });
|
|
302
|
-
|
|
303
|
-
// Installing beta must not replace alpha's registration or the shim.
|
|
304
|
-
assert.equal(readHook(hooksDirFor(repoRoot), "pre-commit"), shimAfterAlpha);
|
|
305
|
-
assert.deepEqual(readEntry(repoRoot, "alpha", "pre-commit"), {
|
|
306
|
-
project: "alpha",
|
|
307
|
-
command: "pwd > .hook-cwd",
|
|
308
|
-
});
|
|
309
|
-
assert.deepEqual(readEntry(repoRoot, "beta", "pre-commit"), {
|
|
310
|
-
project: "beta",
|
|
311
|
-
command: "pwd > .hook-cwd",
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// A commit staged in alpha runs alpha's command only.
|
|
315
|
-
fs.writeFileSync(path.join(alpha, "file.txt"), "alpha\n");
|
|
316
|
-
git(["add", "file.txt"], alpha);
|
|
317
|
-
git(["commit", "-m", "alpha change"], alpha);
|
|
318
|
-
assert.ok(fs.existsSync(path.join(alpha, ".hook-cwd")));
|
|
319
|
-
assert.equal(fs.existsSync(path.join(beta, ".hook-cwd")), false);
|
|
320
|
-
|
|
321
|
-
// And a commit staged in beta runs beta's command.
|
|
322
|
-
fs.writeFileSync(path.join(beta, "file.txt"), "beta\n");
|
|
323
|
-
git(["add", "file.txt"], beta);
|
|
324
|
-
git(["commit", "-m", "beta change"], beta);
|
|
325
|
-
assert.ok(fs.existsSync(path.join(beta, ".hook-cwd")));
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("drops stale entries when a hook is removed from the config", () => {
|
|
329
|
-
const repoRoot = initRepo(makeTempDir());
|
|
330
|
-
const alpha = makeProject(repoRoot, "alpha", {
|
|
331
|
-
"pre-commit": "pwd > .hook-cwd",
|
|
332
|
-
"pre-push": "pwd > .hook-cwd",
|
|
333
|
-
});
|
|
334
|
-
const beta = makeProject(repoRoot, "beta", {
|
|
335
|
-
"pre-commit": "pwd > .hook-cwd",
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
installScopedHooks({ projectDir: alpha, logger: silentLogger() });
|
|
339
|
-
installScopedHooks({ projectDir: beta, logger: silentLogger() });
|
|
340
|
-
|
|
341
|
-
// alpha no longer configures pre-push.
|
|
342
|
-
fs.writeFileSync(
|
|
343
|
-
path.join(alpha, "package.json"),
|
|
344
|
-
JSON.stringify({
|
|
345
|
-
"simple-git-hooks": { "pre-commit": "pwd > .hook-cwd" },
|
|
346
|
-
}),
|
|
347
|
-
);
|
|
348
|
-
installScopedHooks({ projectDir: alpha, logger: silentLogger() });
|
|
349
|
-
|
|
350
|
-
assert.equal(
|
|
351
|
-
fs.existsSync(
|
|
352
|
-
path.join(entriesDirFor(repoRoot), entryName("alpha", "pre-push")),
|
|
353
|
-
),
|
|
354
|
-
false,
|
|
355
|
-
);
|
|
356
|
-
assert.ok(
|
|
357
|
-
fs.existsSync(
|
|
358
|
-
path.join(entriesDirFor(repoRoot), entryName("alpha", "pre-commit")),
|
|
359
|
-
),
|
|
360
|
-
);
|
|
361
|
-
assert.ok(
|
|
362
|
-
fs.existsSync(
|
|
363
|
-
path.join(entriesDirFor(repoRoot), entryName("beta", "pre-commit")),
|
|
364
|
-
),
|
|
365
|
-
);
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe("prepare.js main() in a nested repository", () => {
|
|
370
|
-
it("installs scoped hooks when the enclosing repo has no hook config", () => {
|
|
371
|
-
const repoRoot = initRepo(makeTempDir());
|
|
372
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
373
|
-
"pre-commit": "pnpm nano-staged",
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
main({ projectDir, logger: silentLogger() });
|
|
377
|
-
|
|
378
|
-
assert.ok(
|
|
379
|
-
readHook(hooksDirFor(repoRoot), "pre-commit").includes(
|
|
380
|
-
"quark:scoped-git-hook",
|
|
381
|
-
),
|
|
382
|
-
);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("does not touch hooks when the enclosing repo manages its own", () => {
|
|
386
|
-
const repoRoot = initRepo(makeTempDir());
|
|
387
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
388
|
-
"pre-commit": "pnpm nano-staged",
|
|
389
|
-
});
|
|
390
|
-
fs.writeFileSync(
|
|
391
|
-
path.join(repoRoot, "package.json"),
|
|
392
|
-
JSON.stringify({
|
|
393
|
-
"simple-git-hooks": { "pre-commit": "pnpm nano-staged" },
|
|
394
|
-
}),
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
main({ projectDir, logger: silentLogger() });
|
|
398
|
-
|
|
399
|
-
assert.equal(
|
|
400
|
-
fs.existsSync(path.join(hooksDirFor(repoRoot), "pre-commit")),
|
|
401
|
-
false,
|
|
402
|
-
);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("does nothing when the project is not in a git repository", () => {
|
|
406
|
-
const projectDir = makeProject(makeTempDir(), "my-app", {
|
|
407
|
-
"pre-commit": "pnpm nano-staged",
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
main({ projectDir, logger: silentLogger() });
|
|
411
|
-
|
|
412
|
-
assert.equal(fs.existsSync(path.join(projectDir, ".git")), false);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it("delegates to simple-git-hooks when the project is the git root", () => {
|
|
416
|
-
const repoRoot = initRepo(makeTempDir());
|
|
417
|
-
fs.writeFileSync(
|
|
418
|
-
path.join(repoRoot, "package.json"),
|
|
419
|
-
JSON.stringify({
|
|
420
|
-
"simple-git-hooks": { "pre-commit": "pnpm nano-staged" },
|
|
421
|
-
}),
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
const binDir = makeTempDir();
|
|
425
|
-
const marker = path.join(binDir, "npx-called");
|
|
426
|
-
fs.writeFileSync(
|
|
427
|
-
path.join(binDir, "npx"),
|
|
428
|
-
`#!/bin/sh\necho "$@" > ${JSON.stringify(marker)}\n`,
|
|
429
|
-
);
|
|
430
|
-
fs.chmodSync(path.join(binDir, "npx"), 0o755);
|
|
431
|
-
|
|
432
|
-
const originalPath = process.env.PATH;
|
|
433
|
-
process.env.PATH = `${binDir}${path.delimiter}${originalPath}`;
|
|
434
|
-
try {
|
|
435
|
-
main({ projectDir: repoRoot, logger: silentLogger() });
|
|
436
|
-
} finally {
|
|
437
|
-
process.env.PATH = originalPath;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
assert.equal(fs.readFileSync(marker, "utf8").trim(), "simple-git-hooks");
|
|
441
|
-
assert.equal(
|
|
442
|
-
fs.existsSync(path.join(hooksDirFor(repoRoot), "pre-commit")),
|
|
443
|
-
false,
|
|
444
|
-
);
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
describe("scoped hook execution", () => {
|
|
449
|
-
it("runs the hook command from the project directory", () => {
|
|
450
|
-
const repoRoot = initRepo(makeTempDir());
|
|
451
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
452
|
-
"pre-commit": "pwd > .hook-cwd",
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
installScopedHooks({ projectDir, logger: silentLogger() });
|
|
456
|
-
|
|
457
|
-
fs.writeFileSync(path.join(projectDir, "file.txt"), "hello\n");
|
|
458
|
-
git(["add", "file.txt"], projectDir);
|
|
459
|
-
git(["commit", "-m", "test commit"], projectDir);
|
|
460
|
-
|
|
461
|
-
const hookCwd = fs.readFileSync(path.join(projectDir, ".hook-cwd"), "utf8");
|
|
462
|
-
assert.equal(fs.realpathSync(hookCwd.trim()), fs.realpathSync(projectDir));
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it("is a no-op in a checkout without the project directory", () => {
|
|
466
|
-
const repoRoot = initRepo(makeTempDir());
|
|
467
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
468
|
-
"pre-commit": "pwd > .hook-cwd",
|
|
469
|
-
});
|
|
470
|
-
installScopedHooks({ projectDir, logger: silentLogger() });
|
|
471
|
-
|
|
472
|
-
// Simulate a worktree where the project is not checked out.
|
|
473
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
474
|
-
|
|
475
|
-
fs.writeFileSync(path.join(repoRoot, "README.md"), "root\n");
|
|
476
|
-
git(["add", "README.md"], repoRoot);
|
|
477
|
-
git(["commit", "-m", "root change"], repoRoot);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it("scopes pre-push to projects affected by the pushed commits", () => {
|
|
481
|
-
const repoRoot = initRepo(makeTempDir());
|
|
482
|
-
const alpha = makeProject(repoRoot, "alpha", {
|
|
483
|
-
"pre-push": "pwd > .hook-cwd",
|
|
484
|
-
});
|
|
485
|
-
const beta = makeProject(repoRoot, "beta", {
|
|
486
|
-
"pre-push": "pwd > .hook-cwd",
|
|
487
|
-
});
|
|
488
|
-
installScopedHooks({ projectDir: alpha, logger: silentLogger() });
|
|
489
|
-
installScopedHooks({ projectDir: beta, logger: silentLogger() });
|
|
490
|
-
|
|
491
|
-
git(["add", "-A"], repoRoot);
|
|
492
|
-
git(["commit", "-m", "base"], repoRoot);
|
|
493
|
-
const baseOid = git(["rev-parse", "HEAD"], repoRoot);
|
|
494
|
-
|
|
495
|
-
fs.writeFileSync(path.join(alpha, "change.txt"), "alpha\n");
|
|
496
|
-
git(["add", "change.txt"], alpha);
|
|
497
|
-
git(["commit", "-m", "alpha change"], alpha);
|
|
498
|
-
const headOid = git(["rev-parse", "HEAD"], repoRoot);
|
|
499
|
-
|
|
500
|
-
const dispatcher = path.join(scopedHooksDirFor(repoRoot), "dispatch.js");
|
|
501
|
-
execFileSync(process.execPath, [dispatcher, "pre-push"], {
|
|
502
|
-
cwd: repoRoot,
|
|
503
|
-
input: `refs/heads/main ${headOid} refs/heads/main ${baseOid}\n`,
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
assert.ok(fs.existsSync(path.join(alpha, ".hook-cwd")));
|
|
507
|
-
assert.equal(fs.existsSync(path.join(beta, ".hook-cwd")), false);
|
|
508
|
-
|
|
509
|
-
// Pushing a new branch has no remote commit to diff, so every project runs.
|
|
510
|
-
execFileSync(process.execPath, [dispatcher, "pre-push"], {
|
|
511
|
-
cwd: repoRoot,
|
|
512
|
-
input: `refs/heads/new ${headOid} refs/heads/new ${"0".repeat(40)}\n`,
|
|
513
|
-
});
|
|
514
|
-
assert.ok(fs.existsSync(path.join(beta, ".hook-cwd")));
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
describe("readHookConfig", () => {
|
|
519
|
-
it("reads the simple-git-hooks config from package.json", () => {
|
|
520
|
-
const repoRoot = initRepo(makeTempDir());
|
|
521
|
-
const projectDir = makeProject(repoRoot, "my-app", {
|
|
522
|
-
"pre-commit": "pnpm nano-staged",
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
assert.deepEqual(readHookConfig(projectDir), {
|
|
526
|
-
"pre-commit": "pnpm nano-staged",
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it("returns null when no config is present", () => {
|
|
531
|
-
const dir = makeTempDir();
|
|
532
|
-
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({}));
|
|
533
|
-
assert.equal(readHookConfig(dir), null);
|
|
534
|
-
});
|
|
535
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tests for the uncommitted-changes guard in `quark add` / `quark update`.
|
|
5
|
-
*
|
|
6
|
-
* The guard must be scoped to the Quark project directory so that a project
|
|
7
|
-
* nested inside a larger repository is not blocked by dirty sibling files
|
|
8
|
-
* elsewhere in that repository.
|
|
9
|
-
*
|
|
10
|
-
* Run with: node --test packages/cli/src/scaffold-guards.test.js
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { execFileSync, 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, before, describe, it } from "node:test";
|
|
19
|
-
|
|
20
|
-
const CLI = path.join(import.meta.dirname, "index.js");
|
|
21
|
-
const PROJECT_NAME = "guard-app";
|
|
22
|
-
|
|
23
|
-
let tempRoot;
|
|
24
|
-
let fixtureRepo;
|
|
25
|
-
let scaffoldResult;
|
|
26
|
-
|
|
27
|
-
function git(args, cwd) {
|
|
28
|
-
return execFileSync("git", args, {
|
|
29
|
-
cwd,
|
|
30
|
-
encoding: "utf8",
|
|
31
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
-
}).trim();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function runCLI(args, cwd) {
|
|
36
|
-
return spawnSync(process.execPath, [CLI, ...args], {
|
|
37
|
-
cwd,
|
|
38
|
-
encoding: "utf8",
|
|
39
|
-
timeout: 120_000,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function copyFixture() {
|
|
44
|
-
const dir = mkdtempSync(path.join(tempRoot, "case-"));
|
|
45
|
-
fs.cpSync(fixtureRepo, dir, { recursive: true });
|
|
46
|
-
return dir;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
before(() => {
|
|
50
|
-
tempRoot = mkdtempSync(path.join(tmpdir(), "quark-scaffold-guards-"));
|
|
51
|
-
fixtureRepo = path.join(tempRoot, "fixture");
|
|
52
|
-
fs.mkdirSync(fixtureRepo, { recursive: true });
|
|
53
|
-
|
|
54
|
-
git(["init", "-q"], fixtureRepo);
|
|
55
|
-
git(["config", "user.email", "test@quark.local"], fixtureRepo);
|
|
56
|
-
git(["config", "user.name", "Quark Test"], fixtureRepo);
|
|
57
|
-
fs.writeFileSync(path.join(fixtureRepo, "sibling.txt"), "sibling\n");
|
|
58
|
-
git(["add", "."], fixtureRepo);
|
|
59
|
-
git(["commit", "-q", "-m", "fixture"], fixtureRepo);
|
|
60
|
-
|
|
61
|
-
const result = runCLI(
|
|
62
|
-
[
|
|
63
|
-
PROJECT_NAME,
|
|
64
|
-
"--no-prompts",
|
|
65
|
-
"--packages",
|
|
66
|
-
"",
|
|
67
|
-
"--skip-install",
|
|
68
|
-
"--skip-docker",
|
|
69
|
-
],
|
|
70
|
-
fixtureRepo,
|
|
71
|
-
);
|
|
72
|
-
scaffoldResult = result;
|
|
73
|
-
assert.strictEqual(
|
|
74
|
-
result.status,
|
|
75
|
-
0,
|
|
76
|
-
`scaffold failed\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
git(["add", PROJECT_NAME], fixtureRepo);
|
|
80
|
-
git(["commit", "-q", "-m", "scaffold"], fixtureRepo);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
after(() => {
|
|
84
|
-
if (tempRoot) {
|
|
85
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("uncommitted-changes guard", () => {
|
|
90
|
-
it("warns that nested workflows are ignored by GitHub", () => {
|
|
91
|
-
assert.match(
|
|
92
|
-
scaffoldResult.stdout + scaffoldResult.stderr,
|
|
93
|
-
/GitHub only runs workflows stored at the repository root/,
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("ignores dirty sibling files in the enclosing repository", () => {
|
|
98
|
-
const repo = copyFixture();
|
|
99
|
-
fs.appendFileSync(path.join(repo, "sibling.txt"), "dirty sibling\n");
|
|
100
|
-
|
|
101
|
-
const result = runCLI(
|
|
102
|
-
["add", "jobs", "--skip-install"],
|
|
103
|
-
path.join(repo, PROJECT_NAME),
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
assert.strictEqual(
|
|
107
|
-
result.status,
|
|
108
|
-
0,
|
|
109
|
-
`dirty sibling must not block the nested project\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("blocks when the project has unstaged changes", () => {
|
|
114
|
-
const repo = copyFixture();
|
|
115
|
-
fs.appendFileSync(
|
|
116
|
-
path.join(repo, PROJECT_NAME, "README.md"),
|
|
117
|
-
"\nlocal edit\n",
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const result = runCLI(
|
|
121
|
-
["add", "jobs", "--skip-install"],
|
|
122
|
-
path.join(repo, PROJECT_NAME),
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
assert.strictEqual(result.status, 1);
|
|
126
|
-
assert.match(result.stdout + result.stderr, /uncommitted changes/);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("blocks when the project has staged changes", () => {
|
|
130
|
-
const repo = copyFixture();
|
|
131
|
-
const projectDir = path.join(repo, PROJECT_NAME);
|
|
132
|
-
fs.appendFileSync(path.join(projectDir, "README.md"), "\nstaged edit\n");
|
|
133
|
-
git(["add", "README.md"], projectDir);
|
|
134
|
-
|
|
135
|
-
const result = runCLI(["add", "jobs", "--skip-install"], projectDir);
|
|
136
|
-
|
|
137
|
-
assert.strictEqual(result.status, 1);
|
|
138
|
-
assert.match(result.stdout + result.stderr, /uncommitted changes/);
|
|
139
|
-
});
|
|
140
|
-
});
|