@usequark/quark-create-app 1.23.11
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 +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
it("rewrites the DB import in AI prompts to the project scope", () => {
|
|
100
|
+
const page = fs.readFileSync(
|
|
101
|
+
path.join(projectDir, "apps/web/src/app/page.js"),
|
|
102
|
+
"utf-8",
|
|
103
|
+
);
|
|
104
|
+
assert.ok(
|
|
105
|
+
page.includes(`@${PROJECT_NAME}/db`),
|
|
106
|
+
"AI prompt must reference @<scope>/db",
|
|
107
|
+
);
|
|
108
|
+
assert.ok(
|
|
109
|
+
!page.includes("@__QUARK_SCOPE__"),
|
|
110
|
+
"AI prompt must not keep the scope placeholder",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("substitutes the project name in docs and Railway config", () => {
|
|
115
|
+
const mainMd = fs.readFileSync(path.join(projectDir, "MAIN.md"), "utf-8");
|
|
116
|
+
assert.ok(
|
|
117
|
+
mainMd.startsWith(`# ${PROJECT_NAME}`),
|
|
118
|
+
"MAIN.md title must use the project name",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const envExample = fs.readFileSync(
|
|
122
|
+
path.join(projectDir, ".env.railway.example"),
|
|
123
|
+
"utf-8",
|
|
124
|
+
);
|
|
125
|
+
assert.ok(
|
|
126
|
+
envExample.includes(`APP_NAME=${PROJECT_NAME}`),
|
|
127
|
+
".env.railway.example must use the project name",
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const railway = fs.readFileSync(
|
|
131
|
+
path.join(projectDir, ".railway/railway.ts"),
|
|
132
|
+
"utf-8",
|
|
133
|
+
);
|
|
134
|
+
assert.ok(
|
|
135
|
+
railway.includes(`project("${PROJECT_NAME}"`),
|
|
136
|
+
".railway/railway.ts must use the project name",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("substitutes placeholders in embedded skills", () => {
|
|
141
|
+
const skill = fs.readFileSync(
|
|
142
|
+
path.join(projectDir, ".opencode/skills/payment/SKILL.md"),
|
|
143
|
+
"utf-8",
|
|
144
|
+
);
|
|
145
|
+
assert.ok(
|
|
146
|
+
skill.includes(`@${PROJECT_NAME}/db`),
|
|
147
|
+
"embedded skills must use the project scope",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("generates a migration.sql that Postgres can execute", () => {
|
|
152
|
+
const migrationSql = fs.readFileSync(
|
|
153
|
+
path.join(
|
|
154
|
+
projectDir,
|
|
155
|
+
"packages/db/prisma/migrations/20260202061128_initial/migration.sql",
|
|
156
|
+
),
|
|
157
|
+
"utf-8",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Regression: capturing `prisma migrate diff --script` with a plain `>`
|
|
161
|
+
// also writes Prisma's stdout log line into the SQL. Postgres rejects the
|
|
162
|
+
// whole migration with `42601 syntax error at or near "Loaded"`, so every
|
|
163
|
+
// scaffolded project failed its first `db:migrate:deploy`.
|
|
164
|
+
const lines = migrationSql
|
|
165
|
+
.split("\n")
|
|
166
|
+
.map((line) => line.trim())
|
|
167
|
+
.filter((line) => line.length > 0);
|
|
168
|
+
|
|
169
|
+
assert.ok(
|
|
170
|
+
!lines.some((line) => /^Loaded Prisma config/.test(line)),
|
|
171
|
+
"migration.sql must not contain Prisma's stdout log line",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const firstStatement = lines.find((line) => !line.startsWith("--"));
|
|
175
|
+
assert.ok(
|
|
176
|
+
/^(CREATE|ALTER|DROP|INSERT|UPDATE)\b/.test(firstStatement),
|
|
177
|
+
`migration.sql must begin with a SQL statement (got: ${firstStatement})`,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("scaffolds the PWA manifest as manifest.js, not a conflicting manifest.json", () => {
|
|
182
|
+
const appDir = path.join(projectDir, "apps/web/src/app");
|
|
183
|
+
|
|
184
|
+
// Next.js serves app/manifest.js at /manifest.webmanifest. A sibling
|
|
185
|
+
// app/manifest.json (what the PWA feature used to write) makes the build
|
|
186
|
+
// fail with "Cannot find module for page: /manifest.webmanifest".
|
|
187
|
+
assert.ok(
|
|
188
|
+
!fs.existsSync(path.join(appDir, "manifest.json")),
|
|
189
|
+
"app/manifest.json must not be scaffolded alongside app/manifest.js",
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const manifest = fs.readFileSync(path.join(appDir, "manifest.js"), "utf-8");
|
|
193
|
+
assert.ok(
|
|
194
|
+
!manifest.includes("@__QUARK_SCOPE__"),
|
|
195
|
+
"PWA manifest must use the project scope, not __QUARK_SCOPE__",
|
|
196
|
+
);
|
|
197
|
+
assert.ok(
|
|
198
|
+
manifest.includes(`@${PROJECT_NAME}/config`),
|
|
199
|
+
"PWA manifest must import config from the project scope",
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("generates a valid auth section in .env.example", () => {
|
|
204
|
+
const envExample = fs.readFileSync(
|
|
205
|
+
path.join(projectDir, ".env.example"),
|
|
206
|
+
"utf-8",
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const secretLine = envExample
|
|
210
|
+
.split("\n")
|
|
211
|
+
.find((line) => line.startsWith("NEXTAUTH_SECRET="));
|
|
212
|
+
assert.ok(secretLine, ".env.example must define NEXTAUTH_SECRET");
|
|
213
|
+
const secret = secretLine.slice("NEXTAUTH_SECRET=".length);
|
|
214
|
+
assert.ok(
|
|
215
|
+
secret.length >= 32,
|
|
216
|
+
`NEXTAUTH_SECRET placeholder must be at least 32 characters (got ${secret.length}) - startup validation rejects shorter values`,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
assert.ok(
|
|
220
|
+
!/NEXTAUTH_URL=.*\/api\/auth/.test(envExample),
|
|
221
|
+
"NEXTAUTH_URL must be a bare origin, never include a path",
|
|
222
|
+
);
|
|
223
|
+
assert.ok(
|
|
224
|
+
!envExample.includes("derived automatically from PORT"),
|
|
225
|
+
"APP_URL is not derived from PORT - do not claim it is",
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
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 "@techstream/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.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared utilities for @techstream/quark-create-app
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find the root of the git work tree that contains `dir`, if any.
|
|
15
|
+
*
|
|
16
|
+
* Used to avoid scaffolding a project into a nested git repository: running
|
|
17
|
+
* `git init` inside an existing work tree creates a second, independent
|
|
18
|
+
* repository that shadows the outer one and breaks commits at the repo root.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} dir - Directory to inspect (must already exist)
|
|
21
|
+
* @returns {Promise<string|null>} Absolute path to the enclosing repository
|
|
22
|
+
* root, or null when `dir` is not inside a git work tree (or git is not
|
|
23
|
+
* available)
|
|
24
|
+
*/
|
|
25
|
+
export async function findEnclosingGitRepo(dir) {
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], {
|
|
28
|
+
cwd: dir,
|
|
29
|
+
});
|
|
30
|
+
const topLevel = stdout.trim();
|
|
31
|
+
return topLevel ? path.resolve(topLevel) : null;
|
|
32
|
+
} catch {
|
|
33
|
+
// Not inside a work tree, or git is not installed
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function generateSecret(length = 32) {
|
|
39
|
+
return crypto
|
|
40
|
+
.randomBytes(length)
|
|
41
|
+
.toString("base64")
|
|
42
|
+
.replace(/[/+=]/g, "")
|
|
43
|
+
.substring(0, length);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format a project slug into a human-friendly application name.
|
|
48
|
+
*
|
|
49
|
+
* - Hyphens, underscores, and dots are treated as word separators
|
|
50
|
+
* - Each word is title-cased
|
|
51
|
+
* - Degenerate inputs (all separators) fall back to "Quark App"
|
|
52
|
+
*
|
|
53
|
+
* @param {string} projectName - Raw project slug (e.g. "my-cool-app")
|
|
54
|
+
* @returns {string} Title-cased display name (e.g. "My Cool App")
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* formatProjectDisplayName("my-cool-app") // "My Cool App"
|
|
58
|
+
* formatProjectDisplayName("my.app") // "My App"
|
|
59
|
+
* formatProjectDisplayName("my_app_v2") // "My App V2"
|
|
60
|
+
* formatProjectDisplayName("myapp") // "Myapp"
|
|
61
|
+
* formatProjectDisplayName("---") // "Quark App"
|
|
62
|
+
*/
|
|
63
|
+
export function formatProjectDisplayName(projectName) {
|
|
64
|
+
const normalized = projectName
|
|
65
|
+
.replace(/[-_.]+/g, " ")
|
|
66
|
+
.replace(/\s+/g, " ")
|
|
67
|
+
.trim();
|
|
68
|
+
|
|
69
|
+
if (!normalized) {
|
|
70
|
+
return "Quark App";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return normalized
|
|
74
|
+
.split(" ")
|
|
75
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
76
|
+
.join(" ");
|
|
77
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
});
|