@django-stack/cli 0.3.0 → 0.4.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.
Files changed (139) hide show
  1. package/dist/bin.js +3 -3
  2. package/dist/index.d.ts +52 -40
  3. package/dist/index.js +1 -1
  4. package/dist/{stack-CTyqYWCV.js → scaffold-DbgGYEsc.js} +216 -183
  5. package/package.json +1 -1
  6. package/templates/backend/pyproject.toml +1 -1
  7. package/templates/base/AGENTS.md +7 -11
  8. package/templates/base/README.md +3 -3
  9. package/templates/base/SECURITY.md +1 -1
  10. package/templates/base/gitignore +6 -0
  11. package/templates/base/package.json +1 -1
  12. package/templates/frontend-astro/AGENTS.md +43 -0
  13. package/templates/frontend-astro/package.json +1 -1
  14. package/templates/frontend-astro/template.json +5 -0
  15. package/templates/frontend-next/.dockerignore +3 -0
  16. package/templates/frontend-next/AGENTS.md +59 -0
  17. package/templates/frontend-next/Dockerfile +32 -0
  18. package/templates/frontend-next/next.config.ts +19 -0
  19. package/templates/frontend-next/package.json +34 -0
  20. package/templates/frontend-next/public/favicon.svg +1 -0
  21. package/templates/frontend-next/src/app/[...path]/page.tsx +28 -0
  22. package/templates/frontend-next/src/app/blog/[slug]/page.tsx +60 -0
  23. package/templates/frontend-next/src/app/blog/page.tsx +20 -0
  24. package/templates/frontend-next/src/app/category/[slug]/page.tsx +34 -0
  25. package/templates/frontend-next/src/app/error.tsx +13 -0
  26. package/templates/frontend-next/src/app/globals.css +250 -0
  27. package/templates/frontend-next/src/app/layout.tsx +70 -0
  28. package/templates/frontend-next/src/app/not-found.tsx +15 -0
  29. package/templates/frontend-next/src/app/page.tsx +35 -0
  30. package/templates/frontend-next/src/app/preview/exit/route.ts +3 -0
  31. package/templates/frontend-next/src/app/preview/route.ts +4 -0
  32. package/templates/frontend-next/src/app/revalidate/route.ts +8 -0
  33. package/templates/frontend-next/src/app/robots.txt/route.ts +13 -0
  34. package/templates/frontend-next/src/app/rss.xml/route.ts +19 -0
  35. package/templates/frontend-next/src/app/sitemap.xml/route.ts +28 -0
  36. package/templates/frontend-next/src/app/tag/[slug]/page.tsx +29 -0
  37. package/templates/frontend-next/src/components/ContactForm.tsx +55 -0
  38. package/templates/frontend-next/src/components/NavLink.tsx +15 -0
  39. package/templates/frontend-next/src/components/PostCard.tsx +25 -0
  40. package/templates/frontend-next/src/components/PostList.tsx +28 -0
  41. package/templates/frontend-next/src/lib/api.ts +71 -0
  42. package/templates/frontend-next/src/lib/headless.ts +344 -0
  43. package/templates/frontend-next/src/lib/rss.ts +44 -0
  44. package/templates/frontend-next/src/proxy.ts +34 -0
  45. package/templates/frontend-next/template.json +5 -0
  46. package/templates/frontend-next/test/rss.test.ts +17 -0
  47. package/templates/frontend-next/tsconfig.json +42 -0
  48. package/templates/frontend-next/vendor/.gitkeep +0 -0
  49. package/templates/frontend-next/vitest.config.ts +7 -0
  50. package/templates/frontend-nuxt/.dockerignore +4 -0
  51. package/templates/frontend-nuxt/AGENTS.md +64 -0
  52. package/templates/frontend-nuxt/Dockerfile +32 -0
  53. package/templates/frontend-nuxt/nuxt.config.ts +69 -0
  54. package/templates/frontend-nuxt/package.json +34 -0
  55. package/templates/frontend-nuxt/public/favicon.svg +1 -0
  56. package/templates/frontend-nuxt/server/plugins/media-origins.ts +27 -0
  57. package/templates/frontend-nuxt/server/routes/_content/category/[slug].get.ts +13 -0
  58. package/templates/frontend-nuxt/server/routes/_content/home.get.ts +10 -0
  59. package/templates/frontend-nuxt/server/routes/_content/layout.get.ts +13 -0
  60. package/templates/frontend-nuxt/server/routes/_content/page.get.ts +7 -0
  61. package/templates/frontend-nuxt/server/routes/_content/post/[slug].get.ts +7 -0
  62. package/templates/frontend-nuxt/server/routes/_content/posts.get.ts +4 -0
  63. package/templates/frontend-nuxt/server/routes/_content/redirect.get.ts +7 -0
  64. package/templates/frontend-nuxt/server/routes/_content/tag/[slug].get.ts +13 -0
  65. package/templates/frontend-nuxt/server/routes/preview/exit.get.ts +4 -0
  66. package/templates/frontend-nuxt/server/routes/preview/index.get.ts +26 -0
  67. package/templates/frontend-nuxt/server/routes/revalidate.post.ts +15 -0
  68. package/templates/frontend-nuxt/server/routes/robots.txt.get.ts +4 -0
  69. package/templates/frontend-nuxt/server/routes/rss.xml.get.ts +19 -0
  70. package/templates/frontend-nuxt/server/routes/sitemap.xml.get.ts +29 -0
  71. package/templates/frontend-nuxt/server/utils/cache.ts +48 -0
  72. package/templates/frontend-nuxt/server/utils/content.ts +41 -0
  73. package/templates/frontend-nuxt/server/utils/rss.ts +44 -0
  74. package/templates/frontend-nuxt/src/app.vue +5 -0
  75. package/templates/frontend-nuxt/src/assets/main.css +126 -0
  76. package/templates/frontend-nuxt/src/components/ContactForm.vue +66 -0
  77. package/templates/frontend-nuxt/src/components/PostCard.vue +43 -0
  78. package/templates/frontend-nuxt/src/components/PostList.vue +33 -0
  79. package/templates/frontend-nuxt/src/composables/useContent.ts +27 -0
  80. package/templates/frontend-nuxt/src/composables/useLayoutContent.ts +4 -0
  81. package/templates/frontend-nuxt/src/composables/usePageSeo.ts +33 -0
  82. package/templates/frontend-nuxt/src/error.vue +23 -0
  83. package/templates/frontend-nuxt/src/layouts/default.vue +99 -0
  84. package/templates/frontend-nuxt/src/lib/headless.ts +344 -0
  85. package/templates/frontend-nuxt/src/pages/[...path].vue +16 -0
  86. package/templates/frontend-nuxt/src/pages/blog/[slug].vue +43 -0
  87. package/templates/frontend-nuxt/src/pages/blog/index.vue +12 -0
  88. package/templates/frontend-nuxt/src/pages/category/[slug].vue +14 -0
  89. package/templates/frontend-nuxt/src/pages/index.vue +18 -0
  90. package/templates/frontend-nuxt/src/pages/tag/[slug].vue +13 -0
  91. package/templates/frontend-nuxt/template.json +5 -0
  92. package/templates/frontend-nuxt/test/cache.test.ts +20 -0
  93. package/templates/frontend-nuxt/test/rss.test.ts +17 -0
  94. package/templates/frontend-nuxt/tsconfig.json +9 -0
  95. package/templates/frontend-nuxt/vendor/.gitkeep +0 -0
  96. package/templates/frontend-nuxt/vitest.config.ts +7 -0
  97. package/templates/frontend-sveltekit/.dockerignore +4 -0
  98. package/templates/frontend-sveltekit/AGENTS.md +53 -0
  99. package/templates/frontend-sveltekit/Dockerfile +32 -0
  100. package/templates/frontend-sveltekit/package.json +39 -0
  101. package/templates/frontend-sveltekit/src/app.css +126 -0
  102. package/templates/frontend-sveltekit/src/app.d.ts +12 -0
  103. package/templates/frontend-sveltekit/src/app.html +12 -0
  104. package/templates/frontend-sveltekit/src/hooks.server.ts +70 -0
  105. package/templates/frontend-sveltekit/src/lib/components/ContactForm.svelte +65 -0
  106. package/templates/frontend-sveltekit/src/lib/components/PostCard.svelte +41 -0
  107. package/templates/frontend-sveltekit/src/lib/components/PostList.svelte +39 -0
  108. package/templates/frontend-sveltekit/src/lib/headless.ts +344 -0
  109. package/templates/frontend-sveltekit/src/lib/server/api.ts +28 -0
  110. package/templates/frontend-sveltekit/src/lib/server/cache.ts +58 -0
  111. package/templates/frontend-sveltekit/src/lib/server/rss.ts +44 -0
  112. package/templates/frontend-sveltekit/src/routes/+error.svelte +14 -0
  113. package/templates/frontend-sveltekit/src/routes/+layout.server.ts +18 -0
  114. package/templates/frontend-sveltekit/src/routes/+layout.svelte +112 -0
  115. package/templates/frontend-sveltekit/src/routes/+page.server.ts +10 -0
  116. package/templates/frontend-sveltekit/src/routes/+page.svelte +19 -0
  117. package/templates/frontend-sveltekit/src/routes/[...path]/+page.server.ts +9 -0
  118. package/templates/frontend-sveltekit/src/routes/[...path]/+page.svelte +12 -0
  119. package/templates/frontend-sveltekit/src/routes/blog/+page.server.ts +7 -0
  120. package/templates/frontend-sveltekit/src/routes/blog/+page.svelte +10 -0
  121. package/templates/frontend-sveltekit/src/routes/blog/[slug]/+page.server.ts +9 -0
  122. package/templates/frontend-sveltekit/src/routes/blog/[slug]/+page.svelte +41 -0
  123. package/templates/frontend-sveltekit/src/routes/category/[slug]/+page.server.ts +11 -0
  124. package/templates/frontend-sveltekit/src/routes/category/[slug]/+page.svelte +12 -0
  125. package/templates/frontend-sveltekit/src/routes/preview/+server.ts +27 -0
  126. package/templates/frontend-sveltekit/src/routes/preview/exit/+server.ts +8 -0
  127. package/templates/frontend-sveltekit/src/routes/revalidate/+server.ts +16 -0
  128. package/templates/frontend-sveltekit/src/routes/robots.txt/+server.ts +7 -0
  129. package/templates/frontend-sveltekit/src/routes/rss.xml/+server.ts +20 -0
  130. package/templates/frontend-sveltekit/src/routes/sitemap.xml/+server.ts +29 -0
  131. package/templates/frontend-sveltekit/src/routes/tag/[slug]/+page.server.ts +11 -0
  132. package/templates/frontend-sveltekit/src/routes/tag/[slug]/+page.svelte +11 -0
  133. package/templates/frontend-sveltekit/static/favicon.svg +1 -0
  134. package/templates/frontend-sveltekit/template.json +5 -0
  135. package/templates/frontend-sveltekit/test/cache.test.ts +22 -0
  136. package/templates/frontend-sveltekit/test/rss.test.ts +17 -0
  137. package/templates/frontend-sveltekit/tsconfig.json +5 -0
  138. package/templates/frontend-sveltekit/vendor/.gitkeep +0 -0
  139. 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 toProjectName, a as decodePreset, b as CLI_VERSION, c as resolvePreset, d as LOCAL_S3, f as lockBackend, g as timeZones, h as scaffold, i as applies, l as stackOption, m as relativeDirectory, n as NAMED_PRESETS, o as encodePreset, r as STACK_OPTIONS, s as pendingChoices, t as DEFAULT_STACK, v as validateProjectName, w as generateSecret, x as productionEnv, y as validateTimeZone } from "./stack-CTyqYWCV.js";
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: "astro",
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: "Astro check",
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: false;
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 { t as DEFAULT_STACK, u as FRONTENDS, n as NAMED_PRESETS, r as STACK_OPTIONS, b as VERSION, a as decodePreset, o as encodePreset, C as generateProjectSecrets, w as generateSecret, p as normalizeDatabaseUrl, D as parseEnv, s as pendingChoices, x as productionEnv, c as resolvePreset, T as runDoctor, h as scaffold, O as setEnvValues, _ as toProjectName, S as validateDomain, v as validateProjectName } from "./stack-CTyqYWCV.js";
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: false
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 || ["astro", "next"].includes(stack.frontend);
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
- export { generateProjectSecrets as C, parseEnv as D, checkTools as E, setEnvValues as O, validateDomain as S, runDoctor as T, toProjectName as _, decodePreset as a, CLI_VERSION as b, resolvePreset as c, LOCAL_S3 as d, lockBackend as f, timeZones as g, scaffold as h, applies as i, stackOption as l, relativeDirectory as m, NAMED_PRESETS as n, encodePreset as o, normalizeDatabaseUrl as p, STACK_OPTIONS as r, pendingChoices as s, DEFAULT_STACK as t, FRONTENDS as u, validateProjectName as v, generateSecret as w, productionEnv as x, validateTimeZone as y };
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.0",
3
+ "version": "0.4.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.3.0",
8
+ "django-stack[server,s3]>=0.4.0",
9
9
  "django-headless[schema]>=1.1.1",
10
10
  "django-content-studio[images,huey]>=1.8.0",
11
11
  ]
@@ -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 an Astro frontend
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/ Astro site (server output, Node adapter)
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 through
60
- `getClient()` from `src/lib/api.ts`: `getClient(Astro.cookies).model("content.mymodel").list()`.
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. Add a component in `frontend/src/components/blocks/` and map the block's
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
 
@@ -1,11 +1,11 @@
1
1
  # {{project_name}}
2
2
 
3
- A website with a Django backend and an Astro frontend, created with
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
- - [Astro](https://astro.build) renders the site.
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 Astro's, with the source mounted from `backend/` and
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 |