@django-stack/cli 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +28 -3
- package/dist/index.d.ts +13 -5
- package/dist/index.js +1 -1
- package/dist/{production-M2SsuuBb.js → production-Ct_qoeXk.js} +154 -9
- package/package.json +1 -1
- package/templates/backend/config/settings.py +1 -0
- package/templates/backend/content/models.py +2 -2
- package/templates/backend/content/tests/test_api.py +15 -3
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/AGENTS.md +8 -8
- package/templates/base/README.md +3 -3
- package/templates/base/compose.yaml +7 -0
- package/templates/base/env.example +12 -0
- package/templates/frontend-astro/AGENTS.md +3 -3
- package/templates/frontend-astro/Dockerfile +6 -3
- package/templates/frontend-astro/astro.config.mjs +16 -5
- package/templates/frontend-astro/package.json +1 -1
- package/templates/frontend-astro/pnpm-workspace.yaml +17 -0
- package/templates/frontend-astro/src/components/ContactForm.astro +19 -3
- package/templates/frontend-astro/src/middleware.ts +9 -1
- package/templates/frontend-next/AGENTS.md +3 -3
- package/templates/frontend-next/Dockerfile +5 -2
- package/templates/frontend-next/package.json +3 -3
- package/templates/frontend-next/pnpm-workspace.yaml +11 -0
- package/templates/frontend-next/src/app/[...path]/page.tsx +2 -1
- package/templates/frontend-next/src/components/ContactForm.tsx +14 -3
- package/templates/frontend-next/src/instrumentation.ts +12 -0
- package/templates/frontend-next/src/proxy.ts +4 -1
- package/templates/frontend-nuxt/AGENTS.md +3 -3
- package/templates/frontend-nuxt/Dockerfile +5 -2
- package/templates/frontend-nuxt/nuxt.config.ts +7 -3
- package/templates/frontend-nuxt/package.json +2 -2
- package/templates/frontend-nuxt/pnpm-workspace.yaml +13 -0
- package/templates/frontend-nuxt/server/plugins/errors.ts +14 -0
- package/templates/frontend-nuxt/server/routes/_content/layout.get.ts +4 -0
- package/templates/frontend-nuxt/src/components/ContactForm.vue +15 -2
- package/templates/frontend-sveltekit/AGENTS.md +3 -3
- package/templates/frontend-sveltekit/Dockerfile +6 -3
- package/templates/frontend-sveltekit/package.json +2 -2
- package/templates/frontend-sveltekit/pnpm-workspace.yaml +11 -0
- package/templates/frontend-sveltekit/src/hooks.server.ts +13 -1
- package/templates/frontend-sveltekit/src/lib/components/ContactForm.svelte +15 -2
- package/templates/frontend-sveltekit/src/routes/+layout.server.ts +3 -0
- package/templates/frontend-sveltekit/src/routes/[...path]/+page.svelte +1 -1
- package/templates/frontend-sveltekit/vite.config.ts +10 -3
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as envPorts, A as compose, B as NAMED_PRESETS, C as PACKAGE_NAME, D as RUN_DOMAIN, E as CAPTCHA_APP, F as projectUrls, H as PROMPT_ORDER, I as readLocalRun, J as resolvePreset, K as encodePreset, L as readProject, M as findProjectDirectory, N as parseProfiles, O as RUN_ENV, P as projectStack, Q as describeMove, R as runningServices, S as CLI_VERSION, T as generateSecret, U as STACK_OPTIONS, V as NODE_FRONTENDS, W as applies, X as DEV_PORTS, Y as stackOption, Z as chooseFreePorts, _ as umamiEnv, a as LOCAL_S3, at as isDockerDown, b as validateProjectName, c as localS3, ct as setEnvValues, et as portValues, f as relativeDirectory, g as toProjectName, h as timeZones, it as dockerRunning, j as composeOutput, k as activeProject, l as lockBackend, m as setNotificationsApp, n as productionEnv$1, nt as checkTools, o as cacheEnv, ot as parseEnv, p as scaffold, q as pendingChoices, r as validateDomain, rt as dockerNotRunning, s as frontendPackageManager, st as quoteEnvValue, t as missingS3, tt as runDoctor, u as lockFrontend, v as updateFrontendLock, w as generateProjectSecrets, x as validateTimeZone, y as usesValkey, z as DEFAULT_STACK } from "./production-Ct_qoeXk.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, delimiter, dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
@@ -4070,6 +4070,8 @@ async function init(directoryArgument, options) {
|
|
|
4070
4070
|
analytics: stack.analytics === "umami",
|
|
4071
4071
|
cache: stack.cache,
|
|
4072
4072
|
notifications: stack.notifications === "on",
|
|
4073
|
+
captcha: stack.captcha === "on",
|
|
4074
|
+
packageManager: stack.packageManager,
|
|
4073
4075
|
errors: stack.errors,
|
|
4074
4076
|
tasks: stack.tasks,
|
|
4075
4077
|
preset: encodePreset(stack),
|
|
@@ -4087,6 +4089,16 @@ async function init(directoryArgument, options) {
|
|
|
4087
4089
|
if (lockBackend(target)) spinner.stop(`Locked the backend dependencies in ${pc.bold("backend/uv.lock")}`);
|
|
4088
4090
|
else spinner.error("Could not lock the backend dependencies. The image build resolves them instead.");
|
|
4089
4091
|
}
|
|
4092
|
+
if (NODE_FRONTENDS.includes(stack.frontend)) {
|
|
4093
|
+
const lockfile = {
|
|
4094
|
+
pnpm: "pnpm-lock.yaml",
|
|
4095
|
+
npm: "package-lock.json",
|
|
4096
|
+
bun: "bun.lock"
|
|
4097
|
+
}[stack.packageManager];
|
|
4098
|
+
spinner.start(`Locking the frontend dependencies with ${stack.packageManager}`);
|
|
4099
|
+
if (lockFrontend(target, stack.packageManager)) spinner.stop(`Locked the frontend dependencies in ${pc.bold(`frontend/${lockfile}`)}`);
|
|
4100
|
+
else spinner.error("Could not lock the frontend dependencies. The image build resolves them instead.");
|
|
4101
|
+
}
|
|
4090
4102
|
if (options.git && has("git")) try {
|
|
4091
4103
|
execFileSync("git", ["init", "--quiet"], {
|
|
4092
4104
|
cwd: target,
|
|
@@ -4512,7 +4524,8 @@ const SWITCHABLE = [
|
|
|
4512
4524
|
"media",
|
|
4513
4525
|
"cache",
|
|
4514
4526
|
"tasks",
|
|
4515
|
-
"notifications"
|
|
4527
|
+
"notifications",
|
|
4528
|
+
"captcha"
|
|
4516
4529
|
];
|
|
4517
4530
|
const EMPTY_S3 = Object.fromEntries(Object.keys(LOCAL_S3).map((key) => [key, key === "S3_CREATE_BUCKET" ? "0" : ""]));
|
|
4518
4531
|
/**
|
|
@@ -4580,6 +4593,10 @@ function applyStack(envText, env, from, to) {
|
|
|
4580
4593
|
}
|
|
4581
4594
|
}
|
|
4582
4595
|
if (key === "notifications") changes.push(to.notifications === "on" ? "Notifications are on: content_studio.notifications is in INSTALLED_APPS." : "Notifications are off: content_studio.notifications is out of INSTALLED_APPS; its tables stay.");
|
|
4596
|
+
if (key === "captcha") {
|
|
4597
|
+
text = setEnvValues(text, { CAPTCHA: to.captcha });
|
|
4598
|
+
changes.push(to.captcha === "on" ? "The captcha of the contact form is on." : "The captcha is off: public forms need no captcha token.");
|
|
4599
|
+
}
|
|
4583
4600
|
if (key === "media") changes.push(to.media === "s3" ? "Uploads in production go to S3; dstack settings asks for the S3 settings." : "Uploads in production stay on the server.");
|
|
4584
4601
|
}
|
|
4585
4602
|
return {
|
|
@@ -4998,6 +5015,11 @@ async function update(options) {
|
|
|
4998
5015
|
}).status !== 0) fail("uv could not update backend/uv.lock.");
|
|
4999
5016
|
p.log.success(options.all ? "Updated backend/uv.lock" : "Updated django-stack in backend/uv.lock");
|
|
5000
5017
|
} else if (existsSync(lock)) p.log.warn("uv is not installed, so backend/uv.lock stays as it is: https://docs.astral.sh/uv/");
|
|
5018
|
+
if (frontendPackageManager(project.directory)) {
|
|
5019
|
+
const lockfile = updateFrontendLock(project.directory, options.all);
|
|
5020
|
+
if (!lockfile) fail("The package manager could not update the frontend's lockfile.");
|
|
5021
|
+
p.log.success(options.all ? `Updated frontend/${lockfile}` : `Updated the @django-stack packages in frontend/${lockfile}`);
|
|
5022
|
+
}
|
|
5001
5023
|
for (const target of targets) {
|
|
5002
5024
|
if (compose(target, ["pull", "--ignore-buildable"]) !== 0) fail("Pulling the images failed.");
|
|
5003
5025
|
const build = [
|
|
@@ -5058,6 +5080,9 @@ async function config(options) {
|
|
|
5058
5080
|
if (!existsSync(compose) || !/^ {2}umami:/m.test(readFileSync(compose, "utf8"))) fail("compose.yaml has no umami service. Add it as the analytics docs describe, then run this again.");
|
|
5059
5081
|
}
|
|
5060
5082
|
const settingsPath = join(project.directory, "backend", "config", "settings.py");
|
|
5083
|
+
if (next.captcha === "on" && current.captcha !== "on") {
|
|
5084
|
+
if (!existsSync(settingsPath) || !CAPTCHA_APP.test(readFileSync(settingsPath, "utf8"))) fail("The backend has no django_stack.contrib.captcha. Add it and the widget as the captcha docs describe, then run this again.");
|
|
5085
|
+
}
|
|
5061
5086
|
let settingsText;
|
|
5062
5087
|
if (next.notifications !== current.notifications) {
|
|
5063
5088
|
if (!existsSync(settingsPath)) fail("This project has no backend/config/settings.py.");
|
|
@@ -5301,7 +5326,7 @@ function createProgram() {
|
|
|
5301
5326
|
program.command("logs").description("Show the logs of the services").argument("[services...]", "Only these services, such as backend").addOption(dirOption()).option("--no-follow", "Print the logs and exit").option("-n, --tail <lines>", "Only the last lines of each service").action(logs);
|
|
5302
5327
|
program.command("status").description("Show the containers and whether the database, cache, worker and backups are healthy").addOption(dirOption()).action(status);
|
|
5303
5328
|
program.command("check").description("Check the project: setup, Django checks, migrations and linters").addOption(dirOption()).option("--tests", "Run the backend and frontend tests too").action(check);
|
|
5304
|
-
program.command("update").description("Pull and rebuild the images and update the DjangoStack packages").addOption(dirOption()).option("--all", "Update every
|
|
5329
|
+
program.command("update").description("Pull and rebuild the images and update the DjangoStack packages").addOption(dirOption()).option("--all", "Update every package in the lockfiles, not only DjangoStack's").option("--no-cache", "Rebuild the images from scratch").option("--no-restart", "Don't restart what was running").action(update);
|
|
5305
5330
|
const configCommand = program.command("config").description("Change the stack options of a project").addOption(dirOption()).option("-y, --yes", "Don't ask; change only what the flags set").action((options, cmd) => config({
|
|
5306
5331
|
...options,
|
|
5307
5332
|
fromCli: (option) => cmd.getOptionValueSource(option) === "cli"
|
package/dist/index.d.ts
CHANGED
|
@@ -174,7 +174,7 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
174
174
|
readonly value: "on";
|
|
175
175
|
readonly label: "On";
|
|
176
176
|
readonly hint: "proof of work, no tracking";
|
|
177
|
-
readonly generated:
|
|
177
|
+
readonly generated: true;
|
|
178
178
|
}, {
|
|
179
179
|
readonly value: "off";
|
|
180
180
|
readonly label: "Off";
|
|
@@ -206,7 +206,8 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
206
206
|
readonly values: readonly [{
|
|
207
207
|
readonly value: "pnpm";
|
|
208
208
|
readonly label: "pnpm";
|
|
209
|
-
readonly
|
|
209
|
+
readonly hint: "with a day's delay on new releases";
|
|
210
|
+
readonly generated: true;
|
|
210
211
|
}, {
|
|
211
212
|
readonly value: "npm";
|
|
212
213
|
readonly label: "npm";
|
|
@@ -214,7 +215,8 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
214
215
|
}, {
|
|
215
216
|
readonly value: "bun";
|
|
216
217
|
readonly label: "Bun";
|
|
217
|
-
readonly
|
|
218
|
+
readonly hint: "for installs; the frontend runs on Node.js";
|
|
219
|
+
readonly generated: true;
|
|
218
220
|
}];
|
|
219
221
|
}, {
|
|
220
222
|
readonly key: "media";
|
|
@@ -247,8 +249,8 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
247
249
|
}, {
|
|
248
250
|
readonly value: "sentry";
|
|
249
251
|
readonly label: "Sentry";
|
|
250
|
-
readonly hint: "
|
|
251
|
-
readonly generated:
|
|
252
|
+
readonly hint: "sentry.io, with SENTRY_DSN";
|
|
253
|
+
readonly generated: true;
|
|
252
254
|
}, {
|
|
253
255
|
readonly value: "off";
|
|
254
256
|
readonly label: "Off";
|
|
@@ -296,6 +298,10 @@ interface ScaffoldOptions {
|
|
|
296
298
|
analytics?: boolean;
|
|
297
299
|
/** Content Studio's notifications app; on by default. */
|
|
298
300
|
notifications?: boolean;
|
|
301
|
+
/** The captcha of the contact form; on by default. */
|
|
302
|
+
captcha?: boolean;
|
|
303
|
+
/** The frontend's package manager; pnpm by default. */
|
|
304
|
+
packageManager?: PackageManager;
|
|
299
305
|
/** How errors are reported in production; by email by default. */
|
|
300
306
|
errors?: Stack["errors"];
|
|
301
307
|
/** The cache backend; the database by default. */
|
|
@@ -313,6 +319,8 @@ interface ScaffoldOptions {
|
|
|
313
319
|
/** Where the templates are; the ones in the package by default. */
|
|
314
320
|
templatesDir?: string;
|
|
315
321
|
}
|
|
322
|
+
/** A package manager of the frontend. */
|
|
323
|
+
type PackageManager = Stack["packageManager"];
|
|
316
324
|
interface ScaffoldResult {
|
|
317
325
|
directory: string;
|
|
318
326
|
adminUsername: string;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { z as DEFAULT_STACK, i as FRONTENDS, B as NAMED_PRESETS, U as STACK_OPTIONS, S as VERSION, o as cacheEnv, G as decodePreset, K as encodePreset, w as generateProjectSecrets, T as generateSecret, d as normalizeDatabaseUrl, ot as parseEnv, q as pendingChoices, n as productionEnv, J as resolvePreset, tt as runDoctor, p as scaffold, ct as setEnvValues, m as setNotificationsApp, g as toProjectName, y as usesValkey, r as validateDomain, b as validateProjectName } from "./production-Ct_qoeXk.js";
|
|
@@ -599,7 +599,7 @@ const STACK_OPTIONS = [
|
|
|
599
599
|
value: "on",
|
|
600
600
|
label: "On",
|
|
601
601
|
hint: "proof of work, no tracking",
|
|
602
|
-
generated:
|
|
602
|
+
generated: true
|
|
603
603
|
}, {
|
|
604
604
|
value: "off",
|
|
605
605
|
label: "Off",
|
|
@@ -634,7 +634,8 @@ const STACK_OPTIONS = [
|
|
|
634
634
|
{
|
|
635
635
|
value: "pnpm",
|
|
636
636
|
label: "pnpm",
|
|
637
|
-
|
|
637
|
+
hint: "with a day's delay on new releases",
|
|
638
|
+
generated: true
|
|
638
639
|
},
|
|
639
640
|
{
|
|
640
641
|
value: "npm",
|
|
@@ -644,7 +645,8 @@ const STACK_OPTIONS = [
|
|
|
644
645
|
{
|
|
645
646
|
value: "bun",
|
|
646
647
|
label: "Bun",
|
|
647
|
-
|
|
648
|
+
hint: "for installs; the frontend runs on Node.js",
|
|
649
|
+
generated: true
|
|
648
650
|
}
|
|
649
651
|
]
|
|
650
652
|
},
|
|
@@ -682,8 +684,8 @@ const STACK_OPTIONS = [
|
|
|
682
684
|
{
|
|
683
685
|
value: "sentry",
|
|
684
686
|
label: "Sentry",
|
|
685
|
-
hint: "
|
|
686
|
-
generated:
|
|
687
|
+
hint: "sentry.io, with SENTRY_DSN",
|
|
688
|
+
generated: true
|
|
687
689
|
},
|
|
688
690
|
{
|
|
689
691
|
value: "off",
|
|
@@ -973,7 +975,8 @@ function projectStack(project) {
|
|
|
973
975
|
const fromEnv = {
|
|
974
976
|
analytics: project.profiles.includes("analytics") ? "umami" : "off",
|
|
975
977
|
...project.env.CACHE_URL === void 0 ? {} : cacheStack(project.env),
|
|
976
|
-
...notificationsStack(project)
|
|
978
|
+
...notificationsStack(project),
|
|
979
|
+
...captchaStack(project)
|
|
977
980
|
};
|
|
978
981
|
const { preset } = projectManifest(project);
|
|
979
982
|
if (preset) try {
|
|
@@ -994,6 +997,17 @@ function notificationsStack(project) {
|
|
|
994
997
|
if (!existsSync(settings)) return {};
|
|
995
998
|
return { notifications: /^ *["']content_studio\.notifications["'],/m.test(readFileSync(settings, "utf8")) ? "on" : "off" };
|
|
996
999
|
}
|
|
1000
|
+
/** The line of DjangoStack's captcha app in the backend's INSTALLED_APPS. */
|
|
1001
|
+
const CAPTCHA_APP = /^ *["']django_stack\.contrib\.captcha["'],/m;
|
|
1002
|
+
/**
|
|
1003
|
+
* The captcha is on when the backend has DjangoStack's captcha app and
|
|
1004
|
+
* CAPTCHA is not off. Projects from before the captcha have no app.
|
|
1005
|
+
*/
|
|
1006
|
+
function captchaStack(project) {
|
|
1007
|
+
const settings = join(project.directory, "backend", "config", "settings.py");
|
|
1008
|
+
if (!existsSync(settings)) return {};
|
|
1009
|
+
return { captcha: CAPTCHA_APP.test(readFileSync(settings, "utf8")) && project.env.CAPTCHA !== "off" ? "on" : "off" };
|
|
1010
|
+
}
|
|
997
1011
|
const REDIS = /^(redis|rediss|valkey|valkeys):/;
|
|
998
1012
|
/**
|
|
999
1013
|
* The cache and the tasks a project's `.env` runs. Projects from before 0.7
|
|
@@ -1147,6 +1161,56 @@ const PACKAGE_NAME = "@django-stack/cli";
|
|
|
1147
1161
|
// src/scaffold.ts
|
|
1148
1162
|
/** The frontends this version of the CLI generates. */
|
|
1149
1163
|
const FRONTENDS = stackOption("frontend").values.filter((value) => value.generated).map((value) => value.value);
|
|
1164
|
+
/** The pnpm and Bun versions the frontend image installs with. */
|
|
1165
|
+
const PNPM_VERSION = "12.18.0";
|
|
1166
|
+
const BUN_VERSION = "1.4.4";
|
|
1167
|
+
const LOCKFILES = {
|
|
1168
|
+
pnpm: "pnpm-lock.yaml",
|
|
1169
|
+
npm: "package-lock.json",
|
|
1170
|
+
bun: "bun.lock"
|
|
1171
|
+
};
|
|
1172
|
+
/** The install command of a package manager: frozen to the lockfile when there is one. */
|
|
1173
|
+
function installCommand(manager, flags = "") {
|
|
1174
|
+
const frozen = manager === "npm" ? "npm ci" : `${manager} install --frozen-lockfile`;
|
|
1175
|
+
return `if [ -f ${LOCKFILES[manager]} ]; then ${frozen}${flags}; else ${manager} install${flags}; fi`;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* The template values for a package manager: the commands in the README and
|
|
1179
|
+
* AGENTS.md, and the install steps of the frontend's Dockerfile. Scripts run
|
|
1180
|
+
* with npm in the image whatever installed them; it comes with Node.js.
|
|
1181
|
+
*/
|
|
1182
|
+
function packageManagerValues(manager) {
|
|
1183
|
+
return {
|
|
1184
|
+
package_manager: manager,
|
|
1185
|
+
pm_run: {
|
|
1186
|
+
pnpm: "pnpm",
|
|
1187
|
+
npm: "npm run",
|
|
1188
|
+
bun: "bun run"
|
|
1189
|
+
}[manager],
|
|
1190
|
+
pm_backend: {
|
|
1191
|
+
pnpm: "pnpm backend",
|
|
1192
|
+
npm: "npm run backend --",
|
|
1193
|
+
bun: "bun run backend"
|
|
1194
|
+
}[manager],
|
|
1195
|
+
pm_setup: {
|
|
1196
|
+
pnpm: `RUN npm install --global pnpm@${PNPM_VERSION}`,
|
|
1197
|
+
npm: "# npm comes with Node.js.",
|
|
1198
|
+
bun: `COPY --from=oven/bun:${BUN_VERSION}-alpine /usr/local/bin/bun /usr/local/bin/bun`
|
|
1199
|
+
}[manager],
|
|
1200
|
+
pm_manifests: {
|
|
1201
|
+
pnpm: "package.json pnpm-lock.yaml* pnpm-workspace.yaml",
|
|
1202
|
+
npm: "package.json package-lock.json*",
|
|
1203
|
+
bun: "package.json bun.lock*"
|
|
1204
|
+
}[manager],
|
|
1205
|
+
pm_install: installCommand(manager),
|
|
1206
|
+
pm_install_no_scripts: installCommand(manager, " --ignore-scripts"),
|
|
1207
|
+
pm_prune: {
|
|
1208
|
+
pnpm: "pnpm prune --prod",
|
|
1209
|
+
npm: "npm prune --omit=dev",
|
|
1210
|
+
bun: "rm -rf node_modules && bun install --production"
|
|
1211
|
+
}[manager]
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1150
1214
|
/** Umami's database URL: the project's database, with Umami's tables in the `umami` schema. */
|
|
1151
1215
|
function umamiDatabaseUrl(databaseUrl) {
|
|
1152
1216
|
const url = new URL(databaseUrl);
|
|
@@ -1291,6 +1355,7 @@ function scaffold(options) {
|
|
|
1291
1355
|
const about = join(target, "frontend", "template.json");
|
|
1292
1356
|
const frontend = JSON.parse(readFileSync(about, "utf8"));
|
|
1293
1357
|
rmSync(about);
|
|
1358
|
+
const packageManager = options.packageManager ?? "pnpm";
|
|
1294
1359
|
const values = {
|
|
1295
1360
|
project_name: options.name,
|
|
1296
1361
|
frontend: options.frontend,
|
|
@@ -1300,7 +1365,8 @@ function scaffold(options) {
|
|
|
1300
1365
|
cli_version: CLI_VERSION,
|
|
1301
1366
|
preset: options.preset ?? encodePreset(DEFAULT_STACK),
|
|
1302
1367
|
site_port: String(options.ports?.SITE_PORT ?? 3e3),
|
|
1303
|
-
mail_port: String(options.ports?.MAIL_PORT ?? 8025)
|
|
1368
|
+
mail_port: String(options.ports?.MAIL_PORT ?? 8025),
|
|
1369
|
+
...packageManagerValues(packageManager)
|
|
1304
1370
|
};
|
|
1305
1371
|
const files = walk(target);
|
|
1306
1372
|
for (const file of files) {
|
|
@@ -1309,6 +1375,16 @@ function scaffold(options) {
|
|
|
1309
1375
|
const rendered = render(text, values);
|
|
1310
1376
|
if (rendered !== text) writeFileSync(file, rendered);
|
|
1311
1377
|
}
|
|
1378
|
+
if (packageManager !== "pnpm") rmSync(join(target, "frontend", "pnpm-workspace.yaml"), { force: true });
|
|
1379
|
+
if (packageManager !== "npm") {
|
|
1380
|
+
const packageJson = join(target, "frontend", "package.json");
|
|
1381
|
+
const manifest = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
1382
|
+
const version = packageManager === "pnpm" ? PNPM_VERSION : BUN_VERSION;
|
|
1383
|
+
writeFileSync(packageJson, `${JSON.stringify({
|
|
1384
|
+
...manifest,
|
|
1385
|
+
packageManager: `${packageManager}@${version}`
|
|
1386
|
+
}, null, 2)}\n`);
|
|
1387
|
+
}
|
|
1312
1388
|
if (options.notifications === false) {
|
|
1313
1389
|
const settings = join(target, "backend", "config", "settings.py");
|
|
1314
1390
|
writeFileSync(settings, setNotificationsApp(readFileSync(settings, "utf8"), false));
|
|
@@ -1342,7 +1418,8 @@ function scaffold(options) {
|
|
|
1342
1418
|
env = setEnvValues(env, { COMPOSE_PROFILES: profiles.join(",") });
|
|
1343
1419
|
if (options.ports) env = setEnvValues(env, portValues(parseEnv(env), options.ports));
|
|
1344
1420
|
if (!options.demoContent) env = setEnvValues(env, { SEED_DEMO_CONTENT: "0" });
|
|
1345
|
-
if (options.errors
|
|
1421
|
+
if (options.errors && options.errors !== "email") env = setEnvValues(env, { ERRORS: options.errors });
|
|
1422
|
+
if (options.captcha === false) env = setEnvValues(env, { CAPTCHA: "off" });
|
|
1346
1423
|
writeFileSync(join(target, ".env"), env, { mode: 384 });
|
|
1347
1424
|
writeFileSync(join(target, ".env.example"), envExample);
|
|
1348
1425
|
rmSync(join(target, "env.example"));
|
|
@@ -1407,6 +1484,74 @@ function linkPackages(target, monorepo) {
|
|
|
1407
1484
|
writeFileSync(packageJson, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1408
1485
|
}
|
|
1409
1486
|
/** Locks the backend's dependencies with uv, when uv is installed. */
|
|
1487
|
+
/** Runs a package manager in the frontend directory: pnpm and Bun through npx, at the image's version. */
|
|
1488
|
+
function runPackageManager(directory, manager, args) {
|
|
1489
|
+
const [file, commandArgs] = manager === "npm" ? ["npm", args] : ["npx", [
|
|
1490
|
+
"--yes",
|
|
1491
|
+
`${manager}@${manager === "pnpm" ? PNPM_VERSION : BUN_VERSION}`,
|
|
1492
|
+
...args
|
|
1493
|
+
]];
|
|
1494
|
+
try {
|
|
1495
|
+
execFileSync(file, commandArgs, {
|
|
1496
|
+
cwd: directory,
|
|
1497
|
+
stdio: "ignore",
|
|
1498
|
+
shell: process.platform === "win32"
|
|
1499
|
+
});
|
|
1500
|
+
return true;
|
|
1501
|
+
} catch {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
/** Writes the frontend's lockfile with the chosen package manager, without installing anything. */
|
|
1506
|
+
function lockFrontend(target, manager) {
|
|
1507
|
+
const directory = join(target, "frontend");
|
|
1508
|
+
return runPackageManager(directory, manager, manager === "npm" ? [
|
|
1509
|
+
"install",
|
|
1510
|
+
"--package-lock-only",
|
|
1511
|
+
"--ignore-scripts",
|
|
1512
|
+
"--no-audit",
|
|
1513
|
+
"--no-fund"
|
|
1514
|
+
] : ["install", "--lockfile-only"]) && existsSync(join(directory, LOCKFILES[manager]));
|
|
1515
|
+
}
|
|
1516
|
+
/** The package manager of a project's frontend, from its lockfile. */
|
|
1517
|
+
function frontendPackageManager(directory) {
|
|
1518
|
+
return Object.keys(LOCKFILES).find((manager) => existsSync(join(directory, "frontend", LOCKFILES[manager])));
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Updates the frontend's lockfile, without installing: the @django-stack
|
|
1522
|
+
* packages, or every package with `all`, within the ranges of package.json.
|
|
1523
|
+
* Returns the lockfile, or undefined without one or on failure.
|
|
1524
|
+
*/
|
|
1525
|
+
function updateFrontendLock(target, all = false) {
|
|
1526
|
+
const manager = frontendPackageManager(target);
|
|
1527
|
+
if (!manager) return void 0;
|
|
1528
|
+
const directory = join(target, "frontend");
|
|
1529
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
1530
|
+
const names = Object.keys(manifest.dependencies ?? {}).filter((name) => name.startsWith("@django-stack/"));
|
|
1531
|
+
const packages = all ? [] : names;
|
|
1532
|
+
const args = {
|
|
1533
|
+
pnpm: [
|
|
1534
|
+
"update",
|
|
1535
|
+
...packages,
|
|
1536
|
+
"--lockfile-only"
|
|
1537
|
+
],
|
|
1538
|
+
npm: [
|
|
1539
|
+
"update",
|
|
1540
|
+
...packages,
|
|
1541
|
+
"--package-lock-only",
|
|
1542
|
+
"--ignore-scripts",
|
|
1543
|
+
"--no-audit",
|
|
1544
|
+
"--no-fund"
|
|
1545
|
+
],
|
|
1546
|
+
bun: [
|
|
1547
|
+
"update",
|
|
1548
|
+
...packages,
|
|
1549
|
+
"--lockfile-only"
|
|
1550
|
+
]
|
|
1551
|
+
}[manager];
|
|
1552
|
+
if (!all && !names.length) return LOCKFILES[manager];
|
|
1553
|
+
return runPackageManager(directory, manager, args) ? LOCKFILES[manager] : void 0;
|
|
1554
|
+
}
|
|
1410
1555
|
function lockBackend(target) {
|
|
1411
1556
|
try {
|
|
1412
1557
|
execFileSync("uv", ["lock", "--quiet"], {
|
|
@@ -1499,4 +1644,4 @@ function productionEnv(devEnv, options) {
|
|
|
1499
1644
|
todo
|
|
1500
1645
|
};
|
|
1501
1646
|
}
|
|
1502
|
-
export {
|
|
1647
|
+
export { envPorts as $, compose as A, NAMED_PRESETS as B, PACKAGE_NAME as C, RUN_DOMAIN as D, CAPTCHA_APP as E, projectUrls as F, decodePreset as G, PROMPT_ORDER as H, readLocalRun as I, resolvePreset as J, encodePreset as K, readProject as L, findProjectDirectory as M, parseProfiles as N, RUN_ENV as O, projectStack as P, describeMove as Q, runningServices as R, CLI_VERSION as S, generateSecret as T, STACK_OPTIONS as U, NODE_FRONTENDS as V, applies as W, DEV_PORTS as X, stackOption as Y, chooseFreePorts as Z, umamiEnv as _, LOCAL_S3 as a, isDockerDown as at, validateProjectName as b, localS3 as c, setEnvValues as ct, normalizeDatabaseUrl as d, portValues as et, relativeDirectory as f, toProjectName as g, timeZones as h, FRONTENDS as i, dockerRunning as it, composeOutput as j, activeProject as k, lockBackend as l, setNotificationsApp as m, productionEnv as n, checkTools as nt, cacheEnv as o, parseEnv as ot, scaffold as p, pendingChoices as q, validateDomain as r, dockerNotRunning as rt, frontendPackageManager as s, quoteEnvValue as st, missingS3 as t, runDoctor as tt, lockFrontend as u, updateFrontendLock as v, generateProjectSecrets as w, validateTimeZone as x, usesValkey as y, DEFAULT_STACK as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@django-stack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The production-ready Django, batteries included stack. Create and manage DjangoStack projects: a Django backend, a frontend, Postgres and Docker Compose in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Leon van der Grient",
|
|
@@ -40,6 +40,7 @@ INSTALLED_APPS = [
|
|
|
40
40
|
"content_studio.contrib.password_reset",
|
|
41
41
|
"content_studio.notifications", # notifications in the admin and by email
|
|
42
42
|
"django_stack",
|
|
43
|
+
"django_stack.contrib.captcha", # the captcha of the contact form (CAPTCHA=off turns it off)
|
|
43
44
|
"content",
|
|
44
45
|
# Last: builds the API routes once every model is exposed.
|
|
45
46
|
"headless.rest",
|
|
@@ -5,8 +5,8 @@ site settings, menus, redirects and form submissions.
|
|
|
5
5
|
These models are yours: add fields, remove models, create new ones. Every
|
|
6
6
|
model exposed with ``@expose()`` gets routes under /api/ (see
|
|
7
7
|
https://djangoheadless.org/docs/models); register it in ``admin.py`` to edit
|
|
8
|
-
it in Content Studio. After a change, run ``
|
|
9
|
-
and ``
|
|
8
|
+
it in Content Studio. After a change, run ``{{pm_run}} backend makemigrations``
|
|
9
|
+
and ``{{pm_run}} generate:types``.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from content_studio.blocks import Block, BlockField
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Smoke tests of the content API: every exposed model answers the read-only
|
|
3
3
|
key, drafts stay hidden, and the contact form takes submissions.
|
|
4
|
-
Run them with `
|
|
4
|
+
Run them with `{{pm_run}} test` at the project root.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
from django.apps import apps
|
|
9
9
|
from django.conf import settings
|
|
10
|
+
from django_stack.contrib.captcha.testing import captcha_token
|
|
10
11
|
from rest_framework.test import APIClient
|
|
11
12
|
|
|
12
13
|
from content.models import FormSubmission, Page, Post
|
|
@@ -63,13 +64,24 @@ def test_post_by_slug(reader, seeded):
|
|
|
63
64
|
def test_contact_form(seeded):
|
|
64
65
|
response = APIClient().post(
|
|
65
66
|
"/api/content.formsubmission",
|
|
66
|
-
{"name": "Ada", "email": "ada@example.com", "message": "Hello"},
|
|
67
|
+
{"name": "Ada", "email": "ada@example.com", "message": "Hello", "captcha": captcha_token()},
|
|
67
68
|
format="json",
|
|
68
69
|
)
|
|
69
70
|
assert response.status_code == 201
|
|
70
71
|
assert FormSubmission.objects.filter(email="ada@example.com").exists()
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
@pytest.mark.skipif(not settings.DJANGO_STACK["CAPTCHA"], reason="the captcha is off")
|
|
75
|
+
def test_contact_form_needs_a_captcha(seeded):
|
|
76
|
+
response = APIClient().post(
|
|
77
|
+
"/api/content.formsubmission",
|
|
78
|
+
{"name": "Ada", "email": "ada@example.com", "message": "Hello"},
|
|
79
|
+
format="json",
|
|
80
|
+
)
|
|
81
|
+
assert response.status_code == 400
|
|
82
|
+
assert "captcha" in response.json()
|
|
83
|
+
|
|
84
|
+
|
|
73
85
|
@pytest.mark.skipif(not apps.is_installed("content_studio.notifications"), reason="notifications are off")
|
|
74
86
|
def test_contact_form_notifies_the_editors(seeded, django_capture_on_commit_callbacks):
|
|
75
87
|
from content_studio.notifications.models import Notification
|
|
@@ -79,7 +91,7 @@ def test_contact_form_notifies_the_editors(seeded, django_capture_on_commit_call
|
|
|
79
91
|
with django_capture_on_commit_callbacks(execute=True):
|
|
80
92
|
APIClient().post(
|
|
81
93
|
"/api/content.formsubmission",
|
|
82
|
-
{"name": "Ada", "email": "ada@example.com", "message": "100% sure"},
|
|
94
|
+
{"name": "Ada", "email": "ada@example.com", "message": "100% sure", "captcha": captcha_token()},
|
|
83
95
|
format="json",
|
|
84
96
|
)
|
|
85
97
|
notification = Notification.objects.get(recipient=editor, event="site.form_submitted")
|
|
@@ -5,7 +5,7 @@ description = "The Django backend of {{project_name}}."
|
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"Django>=6.1,<6.2",
|
|
8
|
-
"django-stack[server,s3]>=0.
|
|
8
|
+
"django-stack[server,s3]>=0.10.0",
|
|
9
9
|
"django-headless[schema]>=1.1.1",
|
|
10
10
|
"django-content-studio[images,huey]>=1.11.0",
|
|
11
11
|
]
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@ backend/ Django project
|
|
|
17
17
|
content/seed.py demo content
|
|
18
18
|
content/tests/ pytest tests of the API
|
|
19
19
|
frontend/ the {{frontend_label}} site; frontend/AGENTS.md lists its files
|
|
20
|
-
src/lib/headless.ts generated API types; do not edit, run `
|
|
20
|
+
src/lib/headless.ts generated API types; do not edit, run `{{pm_run}} generate:types`
|
|
21
21
|
compose.yaml every service; COMPOSE_PROFILES in .env picks dev or prod
|
|
22
22
|
Caddyfile the prod profile's reverse proxy and HTTPS
|
|
23
23
|
.github/workflows/deploy.yml optional deploy to a server over SSH
|
|
@@ -26,16 +26,16 @@ Caddyfile the prod profile's reverse proxy and HTTPS
|
|
|
26
26
|
|
|
27
27
|
## Commands
|
|
28
28
|
|
|
29
|
-
Run them at the project root. They need Docker; Node is only needed for the `
|
|
29
|
+
Run them at the project root. They need Docker; Node is only needed for the `{{pm_run}}` shortcuts.
|
|
30
30
|
|
|
31
31
|
| Task | Command |
|
|
32
32
|
|------|---------|
|
|
33
|
-
| Start everything | `docker compose up --build` (or `
|
|
34
|
-
| Run a manage.py command | `docker compose exec backend python manage.py <command>` (or `
|
|
33
|
+
| Start everything | `docker compose up --build` (or `{{pm_run}} dev`) |
|
|
34
|
+
| Run a manage.py command | `docker compose exec backend python manage.py <command>` (or `{{pm_backend}} <command>`) |
|
|
35
35
|
| Make migrations after a model change | `docker compose exec backend python manage.py makemigrations` |
|
|
36
|
-
| Regenerate the API types after a model change | `
|
|
37
|
-
| Run the tests | `
|
|
38
|
-
| Lint and type-check | `
|
|
36
|
+
| Regenerate the API types after a model change | `{{pm_run}} generate:types` |
|
|
37
|
+
| Run the tests | `{{pm_run}} test` |
|
|
38
|
+
| Lint and type-check | `{{pm_run}} lint` |
|
|
39
39
|
| Logs | `docker compose logs --follow backend` |
|
|
40
40
|
| Production settings for a server | `npx @django-stack/cli production --domain example.com` |
|
|
41
41
|
|
|
@@ -54,7 +54,7 @@ in a running container.
|
|
|
54
54
|
`ModelStudio`.
|
|
55
55
|
3. Run `makemigrations`; the backend migrates on its next start, or run
|
|
56
56
|
`migrate`.
|
|
57
|
-
4. Run `
|
|
57
|
+
4. Run `{{pm_run}} generate:types`, then read it in the frontend as
|
|
58
58
|
`frontend/AGENTS.md` shows.
|
|
59
59
|
5. Add an API smoke test to `backend/content/tests/test_api.py`.
|
|
60
60
|
|
package/templates/base/README.md
CHANGED
|
@@ -38,9 +38,9 @@ the frontend {{frontend_label}}'s, with the source mounted from `backend/` and
|
|
|
38
38
|
|------|---------|
|
|
39
39
|
| Run a manage.py command | `docker compose exec backend python manage.py <command>` |
|
|
40
40
|
| Make migrations | `docker compose exec backend python manage.py makemigrations` |
|
|
41
|
-
| Regenerate the API types | `
|
|
42
|
-
| Run the tests | `
|
|
43
|
-
| Check the setup | `
|
|
41
|
+
| Regenerate the API types | `{{pm_run}} generate:types` |
|
|
42
|
+
| Run the tests | `{{pm_run}} test` |
|
|
43
|
+
| Check the setup | `{{pm_run}} doctor` |
|
|
44
44
|
|
|
45
45
|
With the `dstack` command installed (`npm i -g @django-stack/cli`),
|
|
46
46
|
`dstack dev`, `dstack check`, `dstack manage <command>` and `dstack types` do
|
|
@@ -45,6 +45,13 @@ x-frontend-env: &frontend-env
|
|
|
45
45
|
# Page analytics: the tracker loads from /_a/ on the site when set.
|
|
46
46
|
UMAMI_WEBSITE_ID: ${UMAMI_WEBSITE_ID:-}
|
|
47
47
|
UMAMI_URL: ${UMAMI_URL:-}
|
|
48
|
+
# The captcha widget of the contact form, unless CAPTCHA=off.
|
|
49
|
+
CAPTCHA: ${CAPTCHA:-on}
|
|
50
|
+
# Server errors to Sentry with ERRORS=sentry, as the backend's.
|
|
51
|
+
ERRORS: ${ERRORS:-email}
|
|
52
|
+
SENTRY_DSN: ${SENTRY_DSN:-}
|
|
53
|
+
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
|
|
54
|
+
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
|
|
48
55
|
|
|
49
56
|
x-backend-depends: &backend-depends
|
|
50
57
|
db:
|
|
@@ -118,12 +118,24 @@ BACKUP_S3_BUCKET=
|
|
|
118
118
|
# an Uptime Kuma push monitor, which alerts you when the pings stop.
|
|
119
119
|
BACKUP_PING_URL=
|
|
120
120
|
|
|
121
|
+
# The captcha of the contact form: a proof of work the visitor's browser
|
|
122
|
+
# solves, served by Django at /api/cap/, with no third party. CAPTCHA=off
|
|
123
|
+
# turns it off for the backend and the frontend.
|
|
124
|
+
CAPTCHA=on
|
|
125
|
+
|
|
121
126
|
# Error reports (with DJANGO_DEBUG=0): each new error of the backend, the
|
|
122
127
|
# worker or a backup is mailed to ERROR_EMAIL (comma separated) or, when it
|
|
123
128
|
# is empty, to the superusers, and shows in the admin's notifications. The
|
|
124
129
|
# same error is mailed once an hour at most. ERRORS=off turns it off.
|
|
125
130
|
ERRORS=email
|
|
126
131
|
ERROR_EMAIL=
|
|
132
|
+
# ERRORS=sentry sends the errors of the backend, the worker and the
|
|
133
|
+
# frontend's server to Sentry instead: create a Django project on sentry.io
|
|
134
|
+
# and paste its DSN here. SENTRY_TRACES_SAMPLE_RATE (0 to 1) also sends that
|
|
135
|
+
# share of the backend's requests as performance traces.
|
|
136
|
+
SENTRY_DSN=
|
|
137
|
+
SENTRY_ENVIRONMENT=production
|
|
138
|
+
SENTRY_TRACES_SAMPLE_RATE=0
|
|
127
139
|
|
|
128
140
|
# Logs: one JSON object per line in production, plain text in development
|
|
129
141
|
# (LOG_FORMAT=json or plain). LOG_ACCESS=0 turns off Django's access log.
|
|
@@ -12,7 +12,7 @@ src/layouts/Base.astro the page shell: head, menus, footer, preview banner
|
|
|
12
12
|
src/components/ Blocks.astro maps block types to components in components/blocks/
|
|
13
13
|
src/lib/api.ts getContent(): content helpers with the read-only key, drafts in preview
|
|
14
14
|
src/lib/cache.ts the in-memory page cache that /revalidate empties
|
|
15
|
-
src/lib/headless.ts generated API types; do not edit, run `
|
|
15
|
+
src/lib/headless.ts generated API types; do not edit, run `{{pm_run}} generate:types`
|
|
16
16
|
src/middleware.ts page cache, editor-managed redirects, security headers
|
|
17
17
|
astro.config.mjs server output, Content-Security-Policy, the dev proxy to Django
|
|
18
18
|
```
|
|
@@ -39,5 +39,5 @@ Run them in the `frontend` container (`docker compose exec frontend <command>`):
|
|
|
39
39
|
|
|
40
40
|
| Task | Command |
|
|
41
41
|
|------|---------|
|
|
42
|
-
| Type-check | `
|
|
43
|
-
| Test | `
|
|
42
|
+
| Type-check | `{{pm_run}} check` |
|
|
43
|
+
| Test | `{{pm_run}} test` |
|
|
@@ -7,10 +7,13 @@ WORKDIR /app
|
|
|
7
7
|
ENV ASTRO_TELEMETRY_DISABLED=1
|
|
8
8
|
|
|
9
9
|
FROM base AS deps
|
|
10
|
-
|
|
10
|
+
{{pm_setup}}
|
|
11
|
+
COPY {{pm_manifests}} ./
|
|
11
12
|
COPY vendor/ vendor/
|
|
12
13
|
# No BuildKit cache mounts: Railway rejects them, and the layer is cached anyway.
|
|
13
|
-
RUN
|
|
14
|
+
RUN {{pm_install}}
|
|
15
|
+
# The package manager only installs: npm, which comes with Node.js, runs
|
|
16
|
+
# the scripts of package.json below.
|
|
14
17
|
|
|
15
18
|
FROM deps AS dev
|
|
16
19
|
EXPOSE 3000
|
|
@@ -21,7 +24,7 @@ CMD ["sh", "-c", "rm -f .astro/dev.json && exec npm run dev"]
|
|
|
21
24
|
|
|
22
25
|
FROM deps AS build
|
|
23
26
|
COPY . .
|
|
24
|
-
RUN npm run build &&
|
|
27
|
+
RUN npm run build && {{pm_prune}}
|
|
25
28
|
|
|
26
29
|
FROM base AS prod
|
|
27
30
|
ENV NODE_ENV=production HOST=0.0.0.0 PORT=3000
|