@django-stack/cli 0.3.0 → 0.5.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 +3 -3
- package/dist/index.d.ts +52 -40
- package/dist/index.js +1 -1
- package/dist/{stack-CTyqYWCV.js → scaffold-DbgGYEsc.js} +216 -183
- package/package.json +1 -1
- package/templates/backend/pyproject.toml +1 -1
- package/templates/base/AGENTS.md +7 -11
- package/templates/base/README.md +3 -3
- package/templates/base/SECURITY.md +1 -1
- package/templates/base/gitignore +6 -0
- package/templates/base/package.json +1 -1
- package/templates/frontend-astro/AGENTS.md +43 -0
- package/templates/frontend-astro/package.json +1 -1
- package/templates/frontend-astro/template.json +5 -0
- package/templates/frontend-next/.dockerignore +3 -0
- package/templates/frontend-next/AGENTS.md +59 -0
- package/templates/frontend-next/Dockerfile +32 -0
- package/templates/frontend-next/next.config.ts +19 -0
- package/templates/frontend-next/package.json +34 -0
- package/templates/frontend-next/public/favicon.svg +1 -0
- package/templates/frontend-next/src/app/[...path]/page.tsx +28 -0
- package/templates/frontend-next/src/app/blog/[slug]/page.tsx +60 -0
- package/templates/frontend-next/src/app/blog/page.tsx +20 -0
- package/templates/frontend-next/src/app/category/[slug]/page.tsx +34 -0
- package/templates/frontend-next/src/app/error.tsx +13 -0
- package/templates/frontend-next/src/app/globals.css +250 -0
- package/templates/frontend-next/src/app/layout.tsx +70 -0
- package/templates/frontend-next/src/app/not-found.tsx +15 -0
- package/templates/frontend-next/src/app/page.tsx +35 -0
- package/templates/frontend-next/src/app/preview/exit/route.ts +3 -0
- package/templates/frontend-next/src/app/preview/route.ts +4 -0
- package/templates/frontend-next/src/app/revalidate/route.ts +8 -0
- package/templates/frontend-next/src/app/robots.txt/route.ts +13 -0
- package/templates/frontend-next/src/app/rss.xml/route.ts +19 -0
- package/templates/frontend-next/src/app/sitemap.xml/route.ts +28 -0
- package/templates/frontend-next/src/app/tag/[slug]/page.tsx +29 -0
- package/templates/frontend-next/src/components/ContactForm.tsx +55 -0
- package/templates/frontend-next/src/components/NavLink.tsx +15 -0
- package/templates/frontend-next/src/components/PostCard.tsx +25 -0
- package/templates/frontend-next/src/components/PostList.tsx +28 -0
- package/templates/frontend-next/src/lib/api.ts +71 -0
- package/templates/frontend-next/src/lib/headless.ts +344 -0
- package/templates/frontend-next/src/lib/rss.ts +44 -0
- package/templates/frontend-next/src/proxy.ts +34 -0
- package/templates/frontend-next/template.json +5 -0
- package/templates/frontend-next/test/rss.test.ts +17 -0
- package/templates/frontend-next/tsconfig.json +42 -0
- package/templates/frontend-next/vendor/.gitkeep +0 -0
- package/templates/frontend-next/vitest.config.ts +7 -0
- package/templates/frontend-nuxt/.dockerignore +4 -0
- package/templates/frontend-nuxt/AGENTS.md +64 -0
- package/templates/frontend-nuxt/Dockerfile +32 -0
- package/templates/frontend-nuxt/nuxt.config.ts +69 -0
- package/templates/frontend-nuxt/package.json +34 -0
- package/templates/frontend-nuxt/public/favicon.svg +1 -0
- package/templates/frontend-nuxt/server/plugins/media-origins.ts +27 -0
- package/templates/frontend-nuxt/server/routes/_content/category/[slug].get.ts +13 -0
- package/templates/frontend-nuxt/server/routes/_content/home.get.ts +10 -0
- package/templates/frontend-nuxt/server/routes/_content/layout.get.ts +13 -0
- package/templates/frontend-nuxt/server/routes/_content/page.get.ts +7 -0
- package/templates/frontend-nuxt/server/routes/_content/post/[slug].get.ts +7 -0
- package/templates/frontend-nuxt/server/routes/_content/posts.get.ts +4 -0
- package/templates/frontend-nuxt/server/routes/_content/redirect.get.ts +7 -0
- package/templates/frontend-nuxt/server/routes/_content/tag/[slug].get.ts +13 -0
- package/templates/frontend-nuxt/server/routes/preview/exit.get.ts +4 -0
- package/templates/frontend-nuxt/server/routes/preview/index.get.ts +26 -0
- package/templates/frontend-nuxt/server/routes/revalidate.post.ts +15 -0
- package/templates/frontend-nuxt/server/routes/robots.txt.get.ts +4 -0
- package/templates/frontend-nuxt/server/routes/rss.xml.get.ts +19 -0
- package/templates/frontend-nuxt/server/routes/sitemap.xml.get.ts +29 -0
- package/templates/frontend-nuxt/server/utils/cache.ts +48 -0
- package/templates/frontend-nuxt/server/utils/content.ts +41 -0
- package/templates/frontend-nuxt/server/utils/rss.ts +44 -0
- package/templates/frontend-nuxt/src/app.vue +5 -0
- package/templates/frontend-nuxt/src/assets/main.css +126 -0
- package/templates/frontend-nuxt/src/components/ContactForm.vue +66 -0
- package/templates/frontend-nuxt/src/components/PostCard.vue +43 -0
- package/templates/frontend-nuxt/src/components/PostList.vue +33 -0
- package/templates/frontend-nuxt/src/composables/useContent.ts +27 -0
- package/templates/frontend-nuxt/src/composables/useLayoutContent.ts +4 -0
- package/templates/frontend-nuxt/src/composables/usePageSeo.ts +33 -0
- package/templates/frontend-nuxt/src/error.vue +23 -0
- package/templates/frontend-nuxt/src/layouts/default.vue +99 -0
- package/templates/frontend-nuxt/src/lib/headless.ts +344 -0
- package/templates/frontend-nuxt/src/pages/[...path].vue +16 -0
- package/templates/frontend-nuxt/src/pages/blog/[slug].vue +43 -0
- package/templates/frontend-nuxt/src/pages/blog/index.vue +12 -0
- package/templates/frontend-nuxt/src/pages/category/[slug].vue +14 -0
- package/templates/frontend-nuxt/src/pages/index.vue +18 -0
- package/templates/frontend-nuxt/src/pages/tag/[slug].vue +13 -0
- package/templates/frontend-nuxt/template.json +5 -0
- package/templates/frontend-nuxt/test/cache.test.ts +20 -0
- package/templates/frontend-nuxt/test/rss.test.ts +17 -0
- package/templates/frontend-nuxt/tsconfig.json +9 -0
- package/templates/frontend-nuxt/vendor/.gitkeep +0 -0
- package/templates/frontend-nuxt/vitest.config.ts +7 -0
- package/templates/frontend-sveltekit/.dockerignore +4 -0
- package/templates/frontend-sveltekit/AGENTS.md +53 -0
- package/templates/frontend-sveltekit/Dockerfile +32 -0
- package/templates/frontend-sveltekit/package.json +39 -0
- package/templates/frontend-sveltekit/src/app.css +126 -0
- package/templates/frontend-sveltekit/src/app.d.ts +12 -0
- package/templates/frontend-sveltekit/src/app.html +12 -0
- package/templates/frontend-sveltekit/src/hooks.server.ts +70 -0
- package/templates/frontend-sveltekit/src/lib/components/ContactForm.svelte +65 -0
- package/templates/frontend-sveltekit/src/lib/components/PostCard.svelte +41 -0
- package/templates/frontend-sveltekit/src/lib/components/PostList.svelte +39 -0
- package/templates/frontend-sveltekit/src/lib/headless.ts +344 -0
- package/templates/frontend-sveltekit/src/lib/server/api.ts +28 -0
- package/templates/frontend-sveltekit/src/lib/server/cache.ts +58 -0
- package/templates/frontend-sveltekit/src/lib/server/rss.ts +44 -0
- package/templates/frontend-sveltekit/src/routes/+error.svelte +14 -0
- package/templates/frontend-sveltekit/src/routes/+layout.server.ts +18 -0
- package/templates/frontend-sveltekit/src/routes/+layout.svelte +112 -0
- package/templates/frontend-sveltekit/src/routes/+page.server.ts +10 -0
- package/templates/frontend-sveltekit/src/routes/+page.svelte +19 -0
- package/templates/frontend-sveltekit/src/routes/[...path]/+page.server.ts +9 -0
- package/templates/frontend-sveltekit/src/routes/[...path]/+page.svelte +12 -0
- package/templates/frontend-sveltekit/src/routes/blog/+page.server.ts +7 -0
- package/templates/frontend-sveltekit/src/routes/blog/+page.svelte +10 -0
- package/templates/frontend-sveltekit/src/routes/blog/[slug]/+page.server.ts +9 -0
- package/templates/frontend-sveltekit/src/routes/blog/[slug]/+page.svelte +41 -0
- package/templates/frontend-sveltekit/src/routes/category/[slug]/+page.server.ts +11 -0
- package/templates/frontend-sveltekit/src/routes/category/[slug]/+page.svelte +12 -0
- package/templates/frontend-sveltekit/src/routes/preview/+server.ts +27 -0
- package/templates/frontend-sveltekit/src/routes/preview/exit/+server.ts +8 -0
- package/templates/frontend-sveltekit/src/routes/revalidate/+server.ts +16 -0
- package/templates/frontend-sveltekit/src/routes/robots.txt/+server.ts +7 -0
- package/templates/frontend-sveltekit/src/routes/rss.xml/+server.ts +20 -0
- package/templates/frontend-sveltekit/src/routes/sitemap.xml/+server.ts +29 -0
- package/templates/frontend-sveltekit/src/routes/tag/[slug]/+page.server.ts +11 -0
- package/templates/frontend-sveltekit/src/routes/tag/[slug]/+page.svelte +11 -0
- package/templates/frontend-sveltekit/static/favicon.svg +1 -0
- package/templates/frontend-sveltekit/template.json +5 -0
- package/templates/frontend-sveltekit/test/cache.test.ts +22 -0
- package/templates/frontend-sveltekit/test/rss.test.ts +17 -0
- package/templates/frontend-sveltekit/tsconfig.json +5 -0
- package/templates/frontend-sveltekit/vendor/.gitkeep +0 -0
- package/templates/frontend-sveltekit/vite.config.ts +47 -0
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as parseEnv, E as checkTools, O as setEnvValues, S as validateDomain, T as runDoctor, _ as
|
|
2
|
+
import { D as parseEnv, E as checkTools, O as setEnvValues, S as validateDomain, T as runDoctor, _ as encodePreset, a as relativeDirectory, b as stackOption, c as toProjectName, d as CLI_VERSION, f as DEFAULT_STACK, g as decodePreset, h as applies, l as validateProjectName, m as STACK_OPTIONS, n as LOCAL_S3, o as scaffold, p as NAMED_PRESETS, r as lockBackend, s as timeZones, u as validateTimeZone, v as pendingChoices, w as generateSecret, x as productionEnv, y as resolvePreset } from "./scaffold-DbgGYEsc.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
@@ -2346,7 +2346,7 @@ async function init(directoryArgument, options) {
|
|
|
2346
2346
|
result = scaffold({
|
|
2347
2347
|
directory: target,
|
|
2348
2348
|
name,
|
|
2349
|
-
frontend:
|
|
2349
|
+
frontend: stack.frontend,
|
|
2350
2350
|
demoContent,
|
|
2351
2351
|
databaseUrl,
|
|
2352
2352
|
localS3: stack.storage === "s3",
|
|
@@ -2760,7 +2760,7 @@ function checkSteps(project, tests) {
|
|
|
2760
2760
|
devOnly: true
|
|
2761
2761
|
},
|
|
2762
2762
|
{
|
|
2763
|
-
name: "
|
|
2763
|
+
name: "Frontend check",
|
|
2764
2764
|
service: "frontend",
|
|
2765
2765
|
args: [
|
|
2766
2766
|
"npm",
|
package/dist/index.d.ts
CHANGED
|
@@ -32,45 +32,6 @@ export declare function validateDomain(domain: string): string | undefined;
|
|
|
32
32
|
* laptop works on the server.
|
|
33
33
|
*/
|
|
34
34
|
export declare function productionEnv(devEnv: string, options: ProductionOptions): ProductionEnv;
|
|
35
|
-
export declare const FRONTENDS: readonly ["astro"];
|
|
36
|
-
type Frontend = (typeof FRONTENDS)[number];
|
|
37
|
-
interface ScaffoldOptions {
|
|
38
|
-
/** The directory to create the project in. */
|
|
39
|
-
directory: string;
|
|
40
|
-
/** The project name: lowercase letters, digits and dashes. */
|
|
41
|
-
name: string;
|
|
42
|
-
frontend: Frontend;
|
|
43
|
-
/** Demo pages and posts on the first start. */
|
|
44
|
-
demoContent: boolean;
|
|
45
|
-
/** An external Postgres URL; the bundled Postgres when not set. */
|
|
46
|
-
databaseUrl?: string;
|
|
47
|
-
/** Store uploads in the local S3 service in development, like S3 in production. */
|
|
48
|
-
localS3?: boolean;
|
|
49
|
-
/** A DjangoStack monorepo to take the packages from instead of the registries (for development). */
|
|
50
|
-
link?: string;
|
|
51
|
-
/** The time zone of the backend, such as Europe/Amsterdam. UTC by default. */
|
|
52
|
-
timeZone?: string;
|
|
53
|
-
/** The preset code of the stack, saved in package.json for `config`. */
|
|
54
|
-
preset?: string;
|
|
55
|
-
/** Where the templates are; the ones in the package by default. */
|
|
56
|
-
templatesDir?: string;
|
|
57
|
-
}
|
|
58
|
-
interface ScaffoldResult {
|
|
59
|
-
directory: string;
|
|
60
|
-
adminUsername: string;
|
|
61
|
-
adminPassword: string;
|
|
62
|
-
files: number;
|
|
63
|
-
}
|
|
64
|
-
/** A valid project name from a directory name: `My Site` becomes `my-site`. */
|
|
65
|
-
export declare function toProjectName(value: string): string;
|
|
66
|
-
export declare function validateProjectName(value: string): string | undefined;
|
|
67
|
-
/** Postgres URLs for hosts other than this machine must use TLS. */
|
|
68
|
-
export declare function normalizeDatabaseUrl(url: string): string;
|
|
69
|
-
export declare function scaffold(options: ScaffoldOptions): ScaffoldResult;
|
|
70
|
-
/** A URL-safe random secret with `bytes` bytes of randomness. */
|
|
71
|
-
export declare function generateSecret(bytes?: number): string;
|
|
72
|
-
/** The secrets a project's .env needs, by the name used in `{{secret:<name>}}`. */
|
|
73
|
-
export declare function generateProjectSecrets(): Record<string, string>;
|
|
74
35
|
export declare const STACK_OPTIONS: readonly [{
|
|
75
36
|
readonly key: "frontend";
|
|
76
37
|
readonly flag: "--frontend";
|
|
@@ -85,7 +46,7 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
85
46
|
readonly value: "next";
|
|
86
47
|
readonly label: "Next.js";
|
|
87
48
|
readonly hint: "React, server components";
|
|
88
|
-
readonly generated:
|
|
49
|
+
readonly generated: true;
|
|
89
50
|
}, {
|
|
90
51
|
readonly value: "headless";
|
|
91
52
|
readonly label: "Headless";
|
|
@@ -96,6 +57,16 @@ export declare const STACK_OPTIONS: readonly [{
|
|
|
96
57
|
readonly label: "Django templates";
|
|
97
58
|
readonly hint: "no Node, Django renders the site";
|
|
98
59
|
readonly generated: false;
|
|
60
|
+
}, {
|
|
61
|
+
readonly value: "nuxt";
|
|
62
|
+
readonly label: "Nuxt";
|
|
63
|
+
readonly hint: "Vue, server-rendered";
|
|
64
|
+
readonly generated: true;
|
|
65
|
+
}, {
|
|
66
|
+
readonly value: "sveltekit";
|
|
67
|
+
readonly label: "SvelteKit";
|
|
68
|
+
readonly hint: "Svelte, server-rendered";
|
|
69
|
+
readonly generated: true;
|
|
99
70
|
}];
|
|
100
71
|
}, {
|
|
101
72
|
readonly key: "postgres";
|
|
@@ -248,6 +219,47 @@ export declare function pendingChoices(stack: Stack): {
|
|
|
248
219
|
choice: string;
|
|
249
220
|
isDefault: boolean;
|
|
250
221
|
}[];
|
|
222
|
+
/** A frontend option; its template is `templates/frontend-<name>`. */
|
|
223
|
+
type Frontend = Stack["frontend"];
|
|
224
|
+
/** The frontends this version of the CLI generates. */
|
|
225
|
+
export declare const FRONTENDS: readonly Frontend[];
|
|
226
|
+
interface ScaffoldOptions {
|
|
227
|
+
/** The directory to create the project in. */
|
|
228
|
+
directory: string;
|
|
229
|
+
/** The project name: lowercase letters, digits and dashes. */
|
|
230
|
+
name: string;
|
|
231
|
+
frontend: Frontend;
|
|
232
|
+
/** Demo pages and posts on the first start. */
|
|
233
|
+
demoContent: boolean;
|
|
234
|
+
/** An external Postgres URL; the bundled Postgres when not set. */
|
|
235
|
+
databaseUrl?: string;
|
|
236
|
+
/** Store uploads in the local S3 service in development, like S3 in production. */
|
|
237
|
+
localS3?: boolean;
|
|
238
|
+
/** A DjangoStack monorepo to take the packages from instead of the registries (for development). */
|
|
239
|
+
link?: string;
|
|
240
|
+
/** The time zone of the backend, such as Europe/Amsterdam. UTC by default. */
|
|
241
|
+
timeZone?: string;
|
|
242
|
+
/** The preset code of the stack, saved in package.json for `config`. */
|
|
243
|
+
preset?: string;
|
|
244
|
+
/** Where the templates are; the ones in the package by default. */
|
|
245
|
+
templatesDir?: string;
|
|
246
|
+
}
|
|
247
|
+
interface ScaffoldResult {
|
|
248
|
+
directory: string;
|
|
249
|
+
adminUsername: string;
|
|
250
|
+
adminPassword: string;
|
|
251
|
+
files: number;
|
|
252
|
+
}
|
|
253
|
+
/** A valid project name from a directory name: `My Site` becomes `my-site`. */
|
|
254
|
+
export declare function toProjectName(value: string): string;
|
|
255
|
+
export declare function validateProjectName(value: string): string | undefined;
|
|
256
|
+
/** Postgres URLs for hosts other than this machine must use TLS. */
|
|
257
|
+
export declare function normalizeDatabaseUrl(url: string): string;
|
|
258
|
+
export declare function scaffold(options: ScaffoldOptions): ScaffoldResult;
|
|
259
|
+
/** A URL-safe random secret with `bytes` bytes of randomness. */
|
|
260
|
+
export declare function generateSecret(bytes?: number): string;
|
|
261
|
+
/** The secrets a project's .env needs, by the name used in `{{secret:<name>}}`. */
|
|
262
|
+
export declare function generateProjectSecrets(): Record<string, string>;
|
|
251
263
|
/** The version of this package, the same for every @django-stack package. */
|
|
252
264
|
declare const CLI_VERSION: string;
|
|
253
265
|
export { type Check, type Frontend, type ProductionEnv, type ProductionOptions, type ScaffoldOptions, type ScaffoldResult, type Stack, CLI_VERSION as VERSION };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { f as DEFAULT_STACK, t as FRONTENDS, p as NAMED_PRESETS, m as STACK_OPTIONS, d as VERSION, g as decodePreset, _ as encodePreset, C as generateProjectSecrets, w as generateSecret, i as normalizeDatabaseUrl, D as parseEnv, v as pendingChoices, x as productionEnv, y as resolvePreset, T as runDoctor, o as scaffold, O as setEnvValues, c as toProjectName, S as validateDomain, l as validateProjectName } from "./scaffold-DbgGYEsc.js";
|
|
@@ -348,186 +348,6 @@ function productionEnv(devEnv, options) {
|
|
|
348
348
|
todo
|
|
349
349
|
};
|
|
350
350
|
}
|
|
351
|
-
// src/version.ts
|
|
352
|
-
/** The version of this package, the same for every @django-stack package. */
|
|
353
|
-
const CLI_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
354
|
-
// src/scaffold.ts
|
|
355
|
-
const FRONTENDS = ["astro"];
|
|
356
|
-
/** The local S3 service of the `s3` profile. It accepts any credentials. */
|
|
357
|
-
const LOCAL_S3 = {
|
|
358
|
-
S3_BUCKET: "media",
|
|
359
|
-
S3_ENDPOINT_URL: "http://s3:8333",
|
|
360
|
-
S3_REGION: "us-east-1",
|
|
361
|
-
S3_ACCESS_KEY_ID: "local",
|
|
362
|
-
S3_SECRET_ACCESS_KEY: "local",
|
|
363
|
-
S3_PUBLIC_URL: "http://localhost:8333/media",
|
|
364
|
-
S3_CREATE_BUCKET: "1"
|
|
365
|
-
};
|
|
366
|
-
const TEXT_EXTENSIONS = /\.(py|ts|mjs|js|json|astro|css|md|yaml|yml|toml|txt|html|sh|svg)$|Dockerfile$|gitignore$|\.dockerignore$|env\.example$/;
|
|
367
|
-
function defaultTemplatesDir() {
|
|
368
|
-
return fileURLToPath(new URL("../templates", import.meta.url));
|
|
369
|
-
}
|
|
370
|
-
/** A valid project name from a directory name: `My Site` becomes `my-site`. */
|
|
371
|
-
function toProjectName(value) {
|
|
372
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
373
|
-
}
|
|
374
|
-
function validateProjectName(value) {
|
|
375
|
-
if (!/^[a-z][a-z0-9-]{0,49}$/.test(value)) return "Use lowercase letters, digits and dashes, starting with a letter.";
|
|
376
|
-
}
|
|
377
|
-
/** Postgres URLs for hosts other than this machine must use TLS. */
|
|
378
|
-
function normalizeDatabaseUrl(url) {
|
|
379
|
-
let parsed;
|
|
380
|
-
try {
|
|
381
|
-
parsed = new URL(url);
|
|
382
|
-
} catch {
|
|
383
|
-
throw new Error(`The database URL is not a URL: ${url}`);
|
|
384
|
-
}
|
|
385
|
-
if (!["postgres:", "postgresql:"].includes(parsed.protocol)) throw new Error("The database URL must start with postgres:// or postgresql://.");
|
|
386
|
-
if (![
|
|
387
|
-
"localhost",
|
|
388
|
-
"127.0.0.1",
|
|
389
|
-
"host.docker.internal",
|
|
390
|
-
"db"
|
|
391
|
-
].includes(parsed.hostname) && !parsed.searchParams.has("sslmode")) parsed.searchParams.set("sslmode", "require");
|
|
392
|
-
return parsed.toString();
|
|
393
|
-
}
|
|
394
|
-
function walk(directory) {
|
|
395
|
-
return readdirSync(directory).flatMap((entry) => {
|
|
396
|
-
const path = join(directory, entry);
|
|
397
|
-
return statSync(path).isDirectory() ? walk(path) : [path];
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
function render(text, values) {
|
|
401
|
-
return text.replace(/\{\{([a-z_]+)\}\}/g, (match, key) => values[key] ?? match);
|
|
402
|
-
}
|
|
403
|
-
/** The IANA time zones Node knows, with UTC first. */
|
|
404
|
-
function timeZones() {
|
|
405
|
-
return ["UTC", ...Intl.supportedValuesOf("timeZone").filter((zone) => zone !== "UTC")];
|
|
406
|
-
}
|
|
407
|
-
function validateTimeZone(value) {
|
|
408
|
-
if (timeZones().includes(value)) return void 0;
|
|
409
|
-
return `${JSON.stringify(value)} is not a time zone. Use a name such as Europe/Amsterdam or UTC.`;
|
|
410
|
-
}
|
|
411
|
-
function scaffold(options) {
|
|
412
|
-
const templates = options.templatesDir ?? defaultTemplatesDir();
|
|
413
|
-
const target = options.directory;
|
|
414
|
-
if (existsSync(target) && readdirSync(target).length > 0) throw new Error(`${target} exists and is not empty. Choose another directory.`);
|
|
415
|
-
const nameError = validateProjectName(options.name);
|
|
416
|
-
if (nameError) throw new Error(`The project name ${JSON.stringify(options.name)} is not valid. ${nameError}`);
|
|
417
|
-
if (options.timeZone) {
|
|
418
|
-
const zoneError = validateTimeZone(options.timeZone);
|
|
419
|
-
if (zoneError) throw new Error(zoneError);
|
|
420
|
-
}
|
|
421
|
-
mkdirSync(target, { recursive: true });
|
|
422
|
-
cpSync(join(templates, "base"), target, { recursive: true });
|
|
423
|
-
cpSync(join(templates, "backend"), join(target, "backend"), { recursive: true });
|
|
424
|
-
cpSync(join(templates, `frontend-${options.frontend}`), join(target, "frontend"), { recursive: true });
|
|
425
|
-
renameSync(join(target, "gitignore"), join(target, ".gitignore"));
|
|
426
|
-
const values = {
|
|
427
|
-
project_name: options.name,
|
|
428
|
-
frontend: options.frontend,
|
|
429
|
-
cli_version: CLI_VERSION,
|
|
430
|
-
preset: options.preset ?? "a"
|
|
431
|
-
};
|
|
432
|
-
const files = walk(target);
|
|
433
|
-
for (const file of files) {
|
|
434
|
-
if (!TEXT_EXTENSIONS.test(file)) continue;
|
|
435
|
-
const text = readFileSync(file, "utf8");
|
|
436
|
-
const rendered = render(text, values);
|
|
437
|
-
if (rendered !== text) writeFileSync(file, rendered);
|
|
438
|
-
}
|
|
439
|
-
if (options.timeZone && options.timeZone !== "UTC") {
|
|
440
|
-
const settings = join(target, "backend", "config", "settings.py");
|
|
441
|
-
const text = readFileSync(settings, "utf8");
|
|
442
|
-
writeFileSync(settings, text.replace("TIME_ZONE = \"UTC\"", `TIME_ZONE = ${JSON.stringify(options.timeZone)}`));
|
|
443
|
-
}
|
|
444
|
-
const template = readFileSync(join(target, "env.example"), "utf8");
|
|
445
|
-
const secrets = generateProjectSecrets();
|
|
446
|
-
let env = template.replace(/\{\{secret:([a-z_]+)\}\}/g, (match, key) => secrets[key] ?? match);
|
|
447
|
-
const envExample = template.replace(/\{\{secret:[a-z_]+\}\}/g, "");
|
|
448
|
-
const profiles = ["dev"];
|
|
449
|
-
if (options.databaseUrl) env = setEnvValues(env, { DATABASE_URL: normalizeDatabaseUrl(options.databaseUrl) });
|
|
450
|
-
else profiles.push("db");
|
|
451
|
-
if (options.localS3) {
|
|
452
|
-
profiles.push("s3");
|
|
453
|
-
env = setEnvValues(env, LOCAL_S3);
|
|
454
|
-
}
|
|
455
|
-
env = setEnvValues(env, { COMPOSE_PROFILES: profiles.join(",") });
|
|
456
|
-
if (!options.demoContent) env = setEnvValues(env, { SEED_DEMO_CONTENT: "0" });
|
|
457
|
-
writeFileSync(join(target, ".env"), env, { mode: 384 });
|
|
458
|
-
writeFileSync(join(target, ".env.example"), envExample);
|
|
459
|
-
rmSync(join(target, "env.example"));
|
|
460
|
-
if (options.link) linkPackages(target, options.link);
|
|
461
|
-
return {
|
|
462
|
-
directory: target,
|
|
463
|
-
adminUsername: "admin",
|
|
464
|
-
adminPassword: secrets.admin ?? "",
|
|
465
|
-
files: walk(target).length
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Development only: build django-stack and @django-stack/utils from a
|
|
470
|
-
* monorepo checkout into the project's vendor/ directories and point the
|
|
471
|
-
* dependencies at them, so a project can run before a release.
|
|
472
|
-
*/
|
|
473
|
-
function linkPackages(target, monorepo) {
|
|
474
|
-
const backendVendor = join(target, "backend", "vendor");
|
|
475
|
-
const frontendVendor = join(target, "frontend", "vendor");
|
|
476
|
-
mkdirSync(backendVendor, { recursive: true });
|
|
477
|
-
mkdirSync(frontendVendor, { recursive: true });
|
|
478
|
-
execFileSync("uv", [
|
|
479
|
-
"build",
|
|
480
|
-
"--wheel",
|
|
481
|
-
"--out-dir",
|
|
482
|
-
backendVendor
|
|
483
|
-
], {
|
|
484
|
-
cwd: join(monorepo, "python", "django-stack"),
|
|
485
|
-
stdio: "ignore"
|
|
486
|
-
});
|
|
487
|
-
rmSync(join(backendVendor, ".gitignore"), { force: true });
|
|
488
|
-
const wheel = readdirSync(backendVendor).find((file) => file.endsWith(".whl"));
|
|
489
|
-
if (!wheel) throw new Error("Building the django-stack wheel failed.");
|
|
490
|
-
const pyproject = join(target, "backend", "pyproject.toml");
|
|
491
|
-
writeFileSync(pyproject, `${readFileSync(pyproject, "utf8").trimEnd()}\n\n[tool.uv.sources]\ndjango-stack = { path = "vendor/${wheel}" }\n`);
|
|
492
|
-
execFileSync("pnpm", [
|
|
493
|
-
"--filter",
|
|
494
|
-
"@django-stack/utils",
|
|
495
|
-
"build"
|
|
496
|
-
], {
|
|
497
|
-
cwd: monorepo,
|
|
498
|
-
stdio: "ignore"
|
|
499
|
-
});
|
|
500
|
-
execFileSync("pnpm", [
|
|
501
|
-
"pack",
|
|
502
|
-
"--pack-destination",
|
|
503
|
-
frontendVendor
|
|
504
|
-
], {
|
|
505
|
-
cwd: join(monorepo, "packages", "utils"),
|
|
506
|
-
stdio: "ignore"
|
|
507
|
-
});
|
|
508
|
-
const tarball = readdirSync(frontendVendor).find((file) => file.endsWith(".tgz"));
|
|
509
|
-
if (!tarball) throw new Error("Packing @django-stack/utils failed.");
|
|
510
|
-
const packageJson = join(target, "frontend", "package.json");
|
|
511
|
-
const manifest = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
512
|
-
manifest.dependencies["@django-stack/utils"] = `file:vendor/${tarball}`;
|
|
513
|
-
writeFileSync(packageJson, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
514
|
-
}
|
|
515
|
-
/** Locks the backend's dependencies with uv, when uv is installed. */
|
|
516
|
-
function lockBackend(target) {
|
|
517
|
-
try {
|
|
518
|
-
execFileSync("uv", ["lock", "--quiet"], {
|
|
519
|
-
cwd: join(target, "backend"),
|
|
520
|
-
stdio: "ignore"
|
|
521
|
-
});
|
|
522
|
-
return true;
|
|
523
|
-
} catch {
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function relativeDirectory(directory) {
|
|
528
|
-
const path = relative(process.cwd(), directory) || ".";
|
|
529
|
-
return path.startsWith("..") ? directory : path;
|
|
530
|
-
}
|
|
531
351
|
// src/stack.ts
|
|
532
352
|
const STACK_OPTIONS = [
|
|
533
353
|
{
|
|
@@ -546,7 +366,7 @@ const STACK_OPTIONS = [
|
|
|
546
366
|
value: "next",
|
|
547
367
|
label: "Next.js",
|
|
548
368
|
hint: "React, server components",
|
|
549
|
-
generated:
|
|
369
|
+
generated: true
|
|
550
370
|
},
|
|
551
371
|
{
|
|
552
372
|
value: "headless",
|
|
@@ -559,6 +379,18 @@ const STACK_OPTIONS = [
|
|
|
559
379
|
label: "Django templates",
|
|
560
380
|
hint: "no Node, Django renders the site",
|
|
561
381
|
generated: false
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
value: "nuxt",
|
|
385
|
+
label: "Nuxt",
|
|
386
|
+
hint: "Vue, server-rendered",
|
|
387
|
+
generated: true
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
value: "sveltekit",
|
|
391
|
+
label: "SvelteKit",
|
|
392
|
+
hint: "Svelte, server-rendered",
|
|
393
|
+
generated: true
|
|
562
394
|
}
|
|
563
395
|
]
|
|
564
396
|
},
|
|
@@ -715,9 +547,16 @@ function stackOption(key) {
|
|
|
715
547
|
return option;
|
|
716
548
|
}
|
|
717
549
|
const DEFAULT_STACK = Object.fromEntries(STACK_OPTIONS.map((option) => [option.key, option.values[0].value]));
|
|
550
|
+
/** The frontends that run on Node, with a `frontend/` directory and a package manager. */
|
|
551
|
+
const NODE_FRONTENDS = [
|
|
552
|
+
"astro",
|
|
553
|
+
"next",
|
|
554
|
+
"nuxt",
|
|
555
|
+
"sveltekit"
|
|
556
|
+
];
|
|
718
557
|
/** Whether an option applies to a stack: the package manager only with a Node frontend. */
|
|
719
558
|
function applies(key, stack) {
|
|
720
|
-
if (key === "packageManager") return stack.frontend === void 0 ||
|
|
559
|
+
if (key === "packageManager") return stack.frontend === void 0 || NODE_FRONTENDS.includes(stack.frontend);
|
|
721
560
|
return true;
|
|
722
561
|
}
|
|
723
562
|
/** The preset format version, the first character of a code. */
|
|
@@ -797,4 +636,198 @@ function pendingChoices(stack) {
|
|
|
797
636
|
}];
|
|
798
637
|
});
|
|
799
638
|
}
|
|
800
|
-
|
|
639
|
+
// src/version.ts
|
|
640
|
+
/** The version of this package, the same for every @django-stack package. */
|
|
641
|
+
const CLI_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
642
|
+
// src/scaffold.ts
|
|
643
|
+
/** The frontends this version of the CLI generates. */
|
|
644
|
+
const FRONTENDS = stackOption("frontend").values.filter((value) => value.generated).map((value) => value.value);
|
|
645
|
+
/** The local S3 service of the `s3` profile. It accepts any credentials. */
|
|
646
|
+
const LOCAL_S3 = {
|
|
647
|
+
S3_BUCKET: "media",
|
|
648
|
+
S3_ENDPOINT_URL: "http://s3:8333",
|
|
649
|
+
S3_REGION: "us-east-1",
|
|
650
|
+
S3_ACCESS_KEY_ID: "local",
|
|
651
|
+
S3_SECRET_ACCESS_KEY: "local",
|
|
652
|
+
S3_PUBLIC_URL: "http://localhost:8333/media",
|
|
653
|
+
S3_CREATE_BUCKET: "1"
|
|
654
|
+
};
|
|
655
|
+
const TEXT_EXTENSIONS = /\.(py|ts|tsx|mjs|js|jsx|json|astro|vue|svelte|css|md|yaml|yml|toml|txt|html|sh|svg)$|Dockerfile$|gitignore$|\.dockerignore$|env\.example$/;
|
|
656
|
+
function defaultTemplatesDir() {
|
|
657
|
+
return fileURLToPath(new URL("../templates", import.meta.url));
|
|
658
|
+
}
|
|
659
|
+
/** A valid project name from a directory name: `My Site` becomes `my-site`. */
|
|
660
|
+
function toProjectName(value) {
|
|
661
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
662
|
+
}
|
|
663
|
+
function validateProjectName(value) {
|
|
664
|
+
if (!/^[a-z][a-z0-9-]{0,49}$/.test(value)) return "Use lowercase letters, digits and dashes, starting with a letter.";
|
|
665
|
+
}
|
|
666
|
+
/** Postgres URLs for hosts other than this machine must use TLS. */
|
|
667
|
+
function normalizeDatabaseUrl(url) {
|
|
668
|
+
let parsed;
|
|
669
|
+
try {
|
|
670
|
+
parsed = new URL(url);
|
|
671
|
+
} catch {
|
|
672
|
+
throw new Error(`The database URL is not a URL: ${url}`);
|
|
673
|
+
}
|
|
674
|
+
if (!["postgres:", "postgresql:"].includes(parsed.protocol)) throw new Error("The database URL must start with postgres:// or postgresql://.");
|
|
675
|
+
if (![
|
|
676
|
+
"localhost",
|
|
677
|
+
"127.0.0.1",
|
|
678
|
+
"host.docker.internal",
|
|
679
|
+
"db"
|
|
680
|
+
].includes(parsed.hostname) && !parsed.searchParams.has("sslmode")) parsed.searchParams.set("sslmode", "require");
|
|
681
|
+
return parsed.toString();
|
|
682
|
+
}
|
|
683
|
+
function walk(directory) {
|
|
684
|
+
return readdirSync(directory).flatMap((entry) => {
|
|
685
|
+
const path = join(directory, entry);
|
|
686
|
+
return statSync(path).isDirectory() ? walk(path) : [path];
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function render(text, values) {
|
|
690
|
+
return text.replace(/\{\{([a-z_]+)\}\}/g, (match, key) => values[key] ?? match);
|
|
691
|
+
}
|
|
692
|
+
/** The IANA time zones Node knows, with UTC first. */
|
|
693
|
+
function timeZones() {
|
|
694
|
+
return ["UTC", ...Intl.supportedValuesOf("timeZone").filter((zone) => zone !== "UTC")];
|
|
695
|
+
}
|
|
696
|
+
function validateTimeZone(value) {
|
|
697
|
+
if (timeZones().includes(value)) return void 0;
|
|
698
|
+
return `${JSON.stringify(value)} is not a time zone. Use a name such as Europe/Amsterdam or UTC.`;
|
|
699
|
+
}
|
|
700
|
+
function scaffold(options) {
|
|
701
|
+
const templates = options.templatesDir ?? defaultTemplatesDir();
|
|
702
|
+
const target = options.directory;
|
|
703
|
+
if (existsSync(target) && readdirSync(target).length > 0) throw new Error(`${target} exists and is not empty. Choose another directory.`);
|
|
704
|
+
const nameError = validateProjectName(options.name);
|
|
705
|
+
if (nameError) throw new Error(`The project name ${JSON.stringify(options.name)} is not valid. ${nameError}`);
|
|
706
|
+
if (options.timeZone) {
|
|
707
|
+
const zoneError = validateTimeZone(options.timeZone);
|
|
708
|
+
if (zoneError) throw new Error(zoneError);
|
|
709
|
+
}
|
|
710
|
+
const frontendTemplate = join(templates, `frontend-${options.frontend}`);
|
|
711
|
+
if (!existsSync(frontendTemplate)) throw new Error(`This version of the CLI has no ${options.frontend} frontend.`);
|
|
712
|
+
mkdirSync(target, { recursive: true });
|
|
713
|
+
cpSync(join(templates, "base"), target, { recursive: true });
|
|
714
|
+
cpSync(join(templates, "backend"), join(target, "backend"), { recursive: true });
|
|
715
|
+
cpSync(frontendTemplate, join(target, "frontend"), { recursive: true });
|
|
716
|
+
renameSync(join(target, "gitignore"), join(target, ".gitignore"));
|
|
717
|
+
const about = join(target, "frontend", "template.json");
|
|
718
|
+
const frontend = JSON.parse(readFileSync(about, "utf8"));
|
|
719
|
+
rmSync(about);
|
|
720
|
+
const values = {
|
|
721
|
+
project_name: options.name,
|
|
722
|
+
frontend: options.frontend,
|
|
723
|
+
frontend_label: frontend.label,
|
|
724
|
+
frontend_url: frontend.url,
|
|
725
|
+
frontend_security: frontend.security,
|
|
726
|
+
cli_version: CLI_VERSION,
|
|
727
|
+
preset: options.preset ?? "a"
|
|
728
|
+
};
|
|
729
|
+
const files = walk(target);
|
|
730
|
+
for (const file of files) {
|
|
731
|
+
if (!TEXT_EXTENSIONS.test(file)) continue;
|
|
732
|
+
const text = readFileSync(file, "utf8");
|
|
733
|
+
const rendered = render(text, values);
|
|
734
|
+
if (rendered !== text) writeFileSync(file, rendered);
|
|
735
|
+
}
|
|
736
|
+
if (options.timeZone && options.timeZone !== "UTC") {
|
|
737
|
+
const settings = join(target, "backend", "config", "settings.py");
|
|
738
|
+
const text = readFileSync(settings, "utf8");
|
|
739
|
+
writeFileSync(settings, text.replace("TIME_ZONE = \"UTC\"", `TIME_ZONE = ${JSON.stringify(options.timeZone)}`));
|
|
740
|
+
}
|
|
741
|
+
const template = readFileSync(join(target, "env.example"), "utf8");
|
|
742
|
+
const secrets = generateProjectSecrets();
|
|
743
|
+
let env = template.replace(/\{\{secret:([a-z_]+)\}\}/g, (match, key) => secrets[key] ?? match);
|
|
744
|
+
const envExample = template.replace(/\{\{secret:[a-z_]+\}\}/g, "");
|
|
745
|
+
const profiles = ["dev"];
|
|
746
|
+
if (options.databaseUrl) env = setEnvValues(env, { DATABASE_URL: normalizeDatabaseUrl(options.databaseUrl) });
|
|
747
|
+
else profiles.push("db");
|
|
748
|
+
if (options.localS3) {
|
|
749
|
+
profiles.push("s3");
|
|
750
|
+
env = setEnvValues(env, LOCAL_S3);
|
|
751
|
+
}
|
|
752
|
+
env = setEnvValues(env, { COMPOSE_PROFILES: profiles.join(",") });
|
|
753
|
+
if (!options.demoContent) env = setEnvValues(env, { SEED_DEMO_CONTENT: "0" });
|
|
754
|
+
writeFileSync(join(target, ".env"), env, { mode: 384 });
|
|
755
|
+
writeFileSync(join(target, ".env.example"), envExample);
|
|
756
|
+
rmSync(join(target, "env.example"));
|
|
757
|
+
if (options.link) linkPackages(target, options.link);
|
|
758
|
+
return {
|
|
759
|
+
directory: target,
|
|
760
|
+
adminUsername: "admin",
|
|
761
|
+
adminPassword: secrets.admin ?? "",
|
|
762
|
+
files: walk(target).length
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Development only: build django-stack and the @django-stack packages from a
|
|
767
|
+
* monorepo checkout into the project's vendor/ directories and point the
|
|
768
|
+
* dependencies at them, so a project can run before a release.
|
|
769
|
+
*/
|
|
770
|
+
function linkPackages(target, monorepo) {
|
|
771
|
+
const backendVendor = join(target, "backend", "vendor");
|
|
772
|
+
const frontendVendor = join(target, "frontend", "vendor");
|
|
773
|
+
mkdirSync(backendVendor, { recursive: true });
|
|
774
|
+
mkdirSync(frontendVendor, { recursive: true });
|
|
775
|
+
execFileSync("uv", [
|
|
776
|
+
"build",
|
|
777
|
+
"--wheel",
|
|
778
|
+
"--out-dir",
|
|
779
|
+
backendVendor
|
|
780
|
+
], {
|
|
781
|
+
cwd: join(monorepo, "python", "django-stack"),
|
|
782
|
+
stdio: "ignore"
|
|
783
|
+
});
|
|
784
|
+
rmSync(join(backendVendor, ".gitignore"), { force: true });
|
|
785
|
+
const wheel = readdirSync(backendVendor).find((file) => file.endsWith(".whl"));
|
|
786
|
+
if (!wheel) throw new Error("Building the django-stack wheel failed.");
|
|
787
|
+
const pyproject = join(target, "backend", "pyproject.toml");
|
|
788
|
+
writeFileSync(pyproject, `${readFileSync(pyproject, "utf8").trimEnd()}\n\n[tool.uv.sources]\ndjango-stack = { path = "vendor/${wheel}" }\n`);
|
|
789
|
+
const packageJson = join(target, "frontend", "package.json");
|
|
790
|
+
const manifest = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
791
|
+
const packages = Object.keys(manifest.dependencies ?? {}).filter((name) => name.startsWith("@django-stack/"));
|
|
792
|
+
for (const name of packages) {
|
|
793
|
+
const directory = join(monorepo, "packages", name.slice(14));
|
|
794
|
+
execFileSync("pnpm", [
|
|
795
|
+
"--filter",
|
|
796
|
+
name,
|
|
797
|
+
"build"
|
|
798
|
+
], {
|
|
799
|
+
cwd: monorepo,
|
|
800
|
+
stdio: "ignore"
|
|
801
|
+
});
|
|
802
|
+
const before = new Set(readdirSync(frontendVendor));
|
|
803
|
+
execFileSync("pnpm", [
|
|
804
|
+
"pack",
|
|
805
|
+
"--pack-destination",
|
|
806
|
+
frontendVendor
|
|
807
|
+
], {
|
|
808
|
+
cwd: directory,
|
|
809
|
+
stdio: "ignore"
|
|
810
|
+
});
|
|
811
|
+
const tarball = readdirSync(frontendVendor).find((file) => file.endsWith(".tgz") && !before.has(file));
|
|
812
|
+
if (!tarball) throw new Error(`Packing ${name} failed.`);
|
|
813
|
+
manifest.dependencies[name] = `file:vendor/${tarball}`;
|
|
814
|
+
}
|
|
815
|
+
writeFileSync(packageJson, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
816
|
+
}
|
|
817
|
+
/** Locks the backend's dependencies with uv, when uv is installed. */
|
|
818
|
+
function lockBackend(target) {
|
|
819
|
+
try {
|
|
820
|
+
execFileSync("uv", ["lock", "--quiet"], {
|
|
821
|
+
cwd: join(target, "backend"),
|
|
822
|
+
stdio: "ignore"
|
|
823
|
+
});
|
|
824
|
+
return true;
|
|
825
|
+
} catch {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function relativeDirectory(directory) {
|
|
830
|
+
const path = relative(process.cwd(), directory) || ".";
|
|
831
|
+
return path.startsWith("..") ? directory : path;
|
|
832
|
+
}
|
|
833
|
+
export { generateProjectSecrets as C, parseEnv as D, checkTools as E, setEnvValues as O, validateDomain as S, runDoctor as T, encodePreset as _, relativeDirectory as a, stackOption as b, toProjectName as c, CLI_VERSION as d, DEFAULT_STACK as f, decodePreset as g, applies as h, normalizeDatabaseUrl as i, validateProjectName as l, STACK_OPTIONS as m, LOCAL_S3 as n, scaffold as o, NAMED_PRESETS as p, lockBackend as r, timeZones as s, FRONTENDS as t, validateTimeZone as u, pendingChoices as v, generateSecret as w, productionEnv as x, resolvePreset as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@django-stack/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -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.5.0",
|
|
9
9
|
"django-headless[schema]>=1.1.1",
|
|
10
10
|
"django-content-studio[images,huey]>=1.8.0",
|
|
11
11
|
]
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# {{project_name}}: guide for AI agents
|
|
2
2
|
|
|
3
3
|
A website built with DjangoStack: a Django backend (Django Headless serves
|
|
4
|
-
the content API, Django Content Studio is the admin) and
|
|
5
|
-
that renders the content, run with Docker Compose.
|
|
4
|
+
the content API, Django Content Studio is the admin) and a frontend built
|
|
5
|
+
with {{frontend_label}} that renders the content, run with Docker Compose.
|
|
6
|
+
`frontend/AGENTS.md` describes the frontend.
|
|
6
7
|
|
|
7
8
|
## Layout
|
|
8
9
|
|
|
@@ -15,12 +16,8 @@ backend/ Django project
|
|
|
15
16
|
content/admin.py how each model looks in the admin (studios)
|
|
16
17
|
content/seed.py demo content
|
|
17
18
|
content/tests/ pytest tests of the API
|
|
18
|
-
frontend/
|
|
19
|
-
src/pages/ routes: /, /[...slug], /blog, /blog/[slug], /category/[slug], /tag/[slug], sitemap.xml, rss.xml, robots.txt, /preview, /revalidate
|
|
20
|
-
src/components/ Blocks.astro maps block types to components in components/blocks/
|
|
21
|
-
src/lib/api.ts getContent(): content helpers with the read-only key, drafts in preview
|
|
19
|
+
frontend/ the {{frontend_label}} site; frontend/AGENTS.md lists its files
|
|
22
20
|
src/lib/headless.ts generated API types; do not edit, run `npm run generate:types`
|
|
23
|
-
src/middleware.ts page cache, editor-managed redirects, security headers
|
|
24
21
|
compose.yaml every service; COMPOSE_PROFILES in .env picks dev or prod
|
|
25
22
|
Caddyfile the prod profile's reverse proxy and HTTPS
|
|
26
23
|
.github/workflows/deploy.yml optional deploy to a server over SSH
|
|
@@ -56,16 +53,15 @@ in a running container.
|
|
|
56
53
|
`ModelStudio`.
|
|
57
54
|
3. Run `makemigrations`; the backend migrates on its next start, or run
|
|
58
55
|
`migrate`.
|
|
59
|
-
4. Run `npm run generate:types`, then read it in the frontend
|
|
60
|
-
`
|
|
56
|
+
4. Run `npm run generate:types`, then read it in the frontend as
|
|
57
|
+
`frontend/AGENTS.md` shows.
|
|
61
58
|
5. Add an API smoke test to `backend/content/tests/test_api.py`.
|
|
62
59
|
|
|
63
60
|
## How to add a block type
|
|
64
61
|
|
|
65
62
|
1. Add a `Block` subclass in `backend/content/blocks.py` and add it to
|
|
66
63
|
`BODY_BLOCKS`. Blocks need no migration.
|
|
67
|
-
2.
|
|
68
|
-
type in `frontend/src/components/Blocks.astro`.
|
|
64
|
+
2. Render it in the frontend as `frontend/AGENTS.md` shows.
|
|
69
65
|
|
|
70
66
|
## Rules
|
|
71
67
|
|
package/templates/base/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# {{project_name}}
|
|
2
2
|
|
|
3
|
-
A website with a Django backend and
|
|
3
|
+
A website with a Django backend and a frontend built with {{frontend_label}}, created with
|
|
4
4
|
[DjangoStack](https://github.com/dwarsbit/django-stack).
|
|
5
5
|
|
|
6
6
|
- [Django Headless](https://djangoheadless.org) serves the content API.
|
|
7
7
|
- [Django Content Studio](https://djangocontentstudio.org) is the admin.
|
|
8
|
-
- [
|
|
8
|
+
- [{{frontend_label}}]({{frontend_url}}) renders the site.
|
|
9
9
|
- Postgres stores the content; Valkey is the cache and task queue.
|
|
10
10
|
|
|
11
11
|
## Start
|
|
@@ -26,7 +26,7 @@ content. Edit a page in the admin, publish, and reload the site.
|
|
|
26
26
|
## Develop
|
|
27
27
|
|
|
28
28
|
Both sides reload on save: the backend runs Django's development server and
|
|
29
|
-
the frontend
|
|
29
|
+
the frontend {{frontend_label}}'s, with the source mounted from `backend/` and
|
|
30
30
|
`frontend/`. `AGENTS.md` lists the layout and the commands.
|
|
31
31
|
|
|
32
32
|
| Task | Command |
|