create-payload-stack 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.
- package/README.md +2 -2
- package/dist/index.js +251 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@ Scaffold a SaaS on Payload CMS in one command.
|
|
|
6
6
|
npx create-payload-stack@latest
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Nine prompts: project name, database (PostgreSQL, MongoDB, SQLite, Vercel Postgres), connection string, sign-in methods, organizations, billing, media storage (skip for local disk, or Vercel Blob, AWS S3, Cloudflare R2, Azure Blob Storage, Google Cloud Storage, Uploadthing), transactional emails (edit copy in the Payload admin, or keep React Email components in code) and cookie consent (banner with audited legal pages, or plain legal pages). The CLI downloads [Payload Stack](https://www.payloadstack.com), writes your answers into `src/stack.config.ts`, swaps the Payload database adapter, wires the storage adapter into `payload.config.ts`, generates `.env` with fresh secrets, installs dependencies and initializes git.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
# non-interactive
|
|
13
|
-
npx create-payload-stack@latest ridgeline -d postgres --auth email-password,magic-link,passkey --social github --billing organization --storage vercel-blob -y
|
|
13
|
+
npx create-payload-stack@latest ridgeline -d postgres --auth email-password,magic-link,passkey --social github --billing organization --storage vercel-blob --emails --consent -y
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Run `npx create-payload-stack@latest --help` for every flag. Documentation: https://payload.solutions/docs/payload-stack
|
package/dist/index.js
CHANGED
|
@@ -249,6 +249,10 @@ function helpText(version2) {
|
|
|
249
249
|
--no-organizations Disable organizations
|
|
250
250
|
--billing <mode> organization | user | none
|
|
251
251
|
--storage <adapter> ${STORAGE_KEYS.join(" | ")} (none = local disk)
|
|
252
|
+
--emails Edit transactional email copy in the Payload admin
|
|
253
|
+
--no-emails Keep emails as React Email components in code
|
|
254
|
+
--consent Cookie banner and audited legal pages (Payload Consent)
|
|
255
|
+
--no-consent Plain legal pages, no banner
|
|
252
256
|
--use-pnpm | --use-npm | --use-yarn | --use-bun
|
|
253
257
|
--no-install Skip dependency installation
|
|
254
258
|
--no-git Skip git init
|
|
@@ -281,6 +285,8 @@ function parse(argv) {
|
|
|
281
285
|
organizations: { type: "boolean" },
|
|
282
286
|
billing: { type: "string" },
|
|
283
287
|
storage: { type: "string" },
|
|
288
|
+
emails: { type: "boolean" },
|
|
289
|
+
consent: { type: "boolean" },
|
|
284
290
|
"use-pnpm": { type: "boolean" },
|
|
285
291
|
"use-npm": { type: "boolean" },
|
|
286
292
|
"use-yarn": { type: "boolean" },
|
|
@@ -306,6 +312,8 @@ function parse(argv) {
|
|
|
306
312
|
organizations: values.organizations,
|
|
307
313
|
billing: values.billing,
|
|
308
314
|
storage: values.storage,
|
|
315
|
+
emails: values.emails,
|
|
316
|
+
consent: values.consent,
|
|
309
317
|
packageManager,
|
|
310
318
|
install: values.install ?? true,
|
|
311
319
|
git: values.git ?? true,
|
|
@@ -323,18 +331,174 @@ function parse(argv) {
|
|
|
323
331
|
// src/cli.ts
|
|
324
332
|
import * as p from "@clack/prompts";
|
|
325
333
|
import { execa as execa2 } from "execa";
|
|
326
|
-
import { existsSync as
|
|
327
|
-
import
|
|
334
|
+
import { existsSync as existsSync5 } from "fs";
|
|
335
|
+
import path6 from "path";
|
|
328
336
|
import pc2 from "picocolors";
|
|
329
337
|
|
|
330
338
|
// src/configure.ts
|
|
331
339
|
import { readFile, writeFile } from "fs/promises";
|
|
340
|
+
import path4 from "path";
|
|
341
|
+
|
|
342
|
+
// src/consent.ts
|
|
343
|
+
import { cp, rm as rm2, rmdir } from "fs/promises";
|
|
344
|
+
import { existsSync } from "fs";
|
|
345
|
+
import path2 from "path";
|
|
346
|
+
|
|
347
|
+
// src/variants.ts
|
|
348
|
+
import { rm } from "fs/promises";
|
|
332
349
|
import path from "path";
|
|
350
|
+
var VARIANT_DIR = "variants";
|
|
351
|
+
function variantPath(step) {
|
|
352
|
+
return path.join(VARIANT_DIR, step);
|
|
353
|
+
}
|
|
354
|
+
async function removeVariants(directory) {
|
|
355
|
+
await rm(path.join(directory, VARIANT_DIR), { force: true, recursive: true });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/consent.ts
|
|
359
|
+
var CONSENT_PACKAGE = "@payload-solutions/plugin-consent";
|
|
360
|
+
var CONSENT_REACT_PACKAGE = "@payload-solutions/consent-react";
|
|
361
|
+
var CONSENT_VERSION = "0.1.0";
|
|
362
|
+
var CONSENT_PACKAGES = {
|
|
363
|
+
[CONSENT_PACKAGE]: CONSENT_VERSION,
|
|
364
|
+
[CONSENT_REACT_PACKAGE]: CONSENT_VERSION
|
|
365
|
+
};
|
|
366
|
+
var VARIANT_CONSENT = variantPath("consent-plugin");
|
|
367
|
+
var SEAM_FILES = [
|
|
368
|
+
"plugin.ts",
|
|
369
|
+
"collections.ts",
|
|
370
|
+
"seed.ts",
|
|
371
|
+
"consent-root.tsx",
|
|
372
|
+
"consent-settings-link.tsx",
|
|
373
|
+
"legal-page.tsx",
|
|
374
|
+
"legal-setup-notice.tsx"
|
|
375
|
+
];
|
|
376
|
+
var BANNER_FILES = ["consent-banner.tsx", "consent-preferences-dialog.tsx"];
|
|
377
|
+
var TEMPLATE_LEGAL_FILES = ["src/collections/LegalPages.ts", "src/seed/legal.ts", "tests/unit/seed-legal.spec.ts"];
|
|
378
|
+
function swapConsentPackages(packageJson, enabled) {
|
|
379
|
+
const deps = { ...packageJson.dependencies };
|
|
380
|
+
const devDeps = { ...packageJson.devDependencies };
|
|
381
|
+
for (const pkg of Object.keys(CONSENT_PACKAGES)) {
|
|
382
|
+
delete devDeps[pkg];
|
|
383
|
+
if (enabled) deps[pkg] = CONSENT_PACKAGES[pkg];
|
|
384
|
+
else delete deps[pkg];
|
|
385
|
+
}
|
|
386
|
+
return { ...packageJson, dependencies: sortKeys(deps), devDependencies: sortKeys(devDeps) };
|
|
387
|
+
}
|
|
388
|
+
function sortKeys(obj) {
|
|
389
|
+
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
390
|
+
}
|
|
391
|
+
async function applyConsentChoice(directory, enabled) {
|
|
392
|
+
if (!enabled) return;
|
|
393
|
+
const variant = path2.join(directory, VARIANT_CONSENT);
|
|
394
|
+
if (!existsSync(variant)) {
|
|
395
|
+
throw new Error(`The template is missing ${VARIANT_CONSENT}; it cannot scaffold with consent.`);
|
|
396
|
+
}
|
|
397
|
+
for (const name of [...SEAM_FILES, ...BANNER_FILES]) {
|
|
398
|
+
await cp(path2.join(variant, name), path2.join(directory, "src/consent", name), { recursive: true });
|
|
399
|
+
}
|
|
400
|
+
await cp(path2.join(variant, "tests/consent.spec.ts"), path2.join(directory, "tests/unit/consent.spec.ts"));
|
|
401
|
+
for (const name of TEMPLATE_LEGAL_FILES) {
|
|
402
|
+
await rm2(path2.join(directory, name), { force: true, recursive: true });
|
|
403
|
+
}
|
|
404
|
+
await rmdir(path2.join(directory, "src/seed")).catch(() => {
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/emails.ts
|
|
409
|
+
import { cp as cp2, rm as rm3 } from "fs/promises";
|
|
410
|
+
import { existsSync as existsSync2 } from "fs";
|
|
411
|
+
import path3 from "path";
|
|
412
|
+
var EMAILS_PACKAGE = "@payload-solutions/plugin-emails";
|
|
413
|
+
var EMAILS_VERSION = "0.1.0";
|
|
414
|
+
var REACT_EMAIL_COMPONENTS = ["@react-email/components", "^1.0.12"];
|
|
415
|
+
var REACT_EMAIL_ONLY_PACKAGES = {
|
|
416
|
+
"@react-email/render": "^2.1.0",
|
|
417
|
+
"react-email": "^6.9.3"
|
|
418
|
+
};
|
|
419
|
+
var EMAIL_DEV_SCRIPT = ["email:dev", "email dev --dir src/components/auth/email --port 3001"];
|
|
420
|
+
var VARIANT_EMAILS = variantPath("emails-plugin");
|
|
421
|
+
var IMPORT_MARKER = "// emails-plugin-import";
|
|
422
|
+
var CONFIG_START = "// emails-plugin-config-start";
|
|
423
|
+
var CONFIG_END = "// emails-plugin-config-end";
|
|
424
|
+
var IMPORTS = `${IMPORT_MARKER}
|
|
425
|
+
import { emailsPlugin } from '${EMAILS_PACKAGE}'
|
|
426
|
+
import { emailDefinitions } from '@/emails/definitions'
|
|
427
|
+
import { StackEmailTemplate } from '@/emails/template'
|
|
428
|
+
import { globalVariableManifest, globalVariables } from '@/emails/variables'`;
|
|
429
|
+
var REGISTRATION = `plugins.push(
|
|
430
|
+
emailsPlugin({
|
|
431
|
+
// The catalogue, filtered by the features in stack.config.ts (src/emails/definitions).
|
|
432
|
+
emails: emailDefinitions,
|
|
433
|
+
// Branding lives in code; editors own the words (src/emails/template.tsx).
|
|
434
|
+
templates: { default: StackEmailTemplate },
|
|
435
|
+
// {{site.name}}, {{support.email}}, {{url.dashboard}} \u2026 in every email (src/emails/variables.ts).
|
|
436
|
+
globalVariables,
|
|
437
|
+
globalVariableManifest,
|
|
438
|
+
// Every send recorded, for 90 days. Set enabled: false to keep no record at all.
|
|
439
|
+
log: { enabled: true, retentionDays: 90 },
|
|
440
|
+
settings: { mediaCollection: 'media' },
|
|
441
|
+
}),
|
|
442
|
+
)`;
|
|
443
|
+
function addEmailsPlugin(payloadConfigSource) {
|
|
444
|
+
if (!payloadConfigSource.includes(IMPORT_MARKER)) {
|
|
445
|
+
throw new Error(`payload.config.ts is missing the "${IMPORT_MARKER}" marker`);
|
|
446
|
+
}
|
|
447
|
+
const start = payloadConfigSource.indexOf(CONFIG_START);
|
|
448
|
+
const end = payloadConfigSource.indexOf(CONFIG_END);
|
|
449
|
+
if (start === -1 || end === -1 || end < start) {
|
|
450
|
+
throw new Error(`payload.config.ts is missing the emails plugin markers`);
|
|
451
|
+
}
|
|
452
|
+
const withImports = payloadConfigSource.replace(IMPORT_MARKER, IMPORTS);
|
|
453
|
+
const at = withImports.indexOf(CONFIG_START);
|
|
454
|
+
const to = withImports.indexOf(CONFIG_END);
|
|
455
|
+
return `${withImports.slice(0, at)}${CONFIG_START}
|
|
456
|
+
${REGISTRATION}
|
|
457
|
+
${withImports.slice(to)}`;
|
|
458
|
+
}
|
|
459
|
+
function swapEmailsPackages(packageJson, enabled) {
|
|
460
|
+
const deps = { ...packageJson.dependencies };
|
|
461
|
+
const devDeps = { ...packageJson.devDependencies };
|
|
462
|
+
const scripts = { ...packageJson.scripts };
|
|
463
|
+
delete devDeps[EMAILS_PACKAGE];
|
|
464
|
+
delete devDeps[REACT_EMAIL_COMPONENTS[0]];
|
|
465
|
+
if (enabled) {
|
|
466
|
+
deps[EMAILS_PACKAGE] = EMAILS_VERSION;
|
|
467
|
+
deps[REACT_EMAIL_COMPONENTS[0]] = REACT_EMAIL_COMPONENTS[1];
|
|
468
|
+
for (const pkg of Object.keys(REACT_EMAIL_ONLY_PACKAGES)) delete deps[pkg];
|
|
469
|
+
delete scripts[EMAIL_DEV_SCRIPT[0]];
|
|
470
|
+
} else {
|
|
471
|
+
delete deps[EMAILS_PACKAGE];
|
|
472
|
+
delete deps[REACT_EMAIL_COMPONENTS[0]];
|
|
473
|
+
Object.assign(deps, REACT_EMAIL_ONLY_PACKAGES);
|
|
474
|
+
scripts[EMAIL_DEV_SCRIPT[0]] = EMAIL_DEV_SCRIPT[1];
|
|
475
|
+
}
|
|
476
|
+
return { ...packageJson, dependencies: sortKeys2(deps), devDependencies: sortKeys2(devDeps), scripts: sortKeys2(scripts) };
|
|
477
|
+
}
|
|
478
|
+
function sortKeys2(obj) {
|
|
479
|
+
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
480
|
+
}
|
|
481
|
+
var REACT_EMAIL_FILES = ["src/emails/send.ts", "src/components/auth/email"];
|
|
482
|
+
async function applyEmailsChoice(directory, enabled) {
|
|
483
|
+
const variant = path3.join(directory, VARIANT_EMAILS);
|
|
484
|
+
if (enabled) {
|
|
485
|
+
if (!existsSync2(variant)) {
|
|
486
|
+
throw new Error(`The template is missing ${VARIANT_EMAILS}; it cannot scaffold with emails.`);
|
|
487
|
+
}
|
|
488
|
+
for (const name of ["index.ts", "hooks.ts", "template.tsx", "variables.ts", "definitions"]) {
|
|
489
|
+
await cp2(path3.join(variant, name), path3.join(directory, "src/emails", name), { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
await cp2(path3.join(variant, "tests/emails.spec.ts"), path3.join(directory, "tests/unit/emails.spec.ts"));
|
|
492
|
+
for (const name of REACT_EMAIL_FILES) {
|
|
493
|
+
await rm3(path3.join(directory, name), { force: true, recursive: true });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
333
497
|
|
|
334
498
|
// src/utils.ts
|
|
335
499
|
import slugifyBase from "@sindresorhus/slugify";
|
|
336
500
|
import { randomBytes } from "crypto";
|
|
337
|
-
import { existsSync, readdirSync } from "fs";
|
|
501
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
338
502
|
import { execa } from "execa";
|
|
339
503
|
function slugify(input) {
|
|
340
504
|
return slugifyBase(input.trim()).replace(/^-+|-+$/g, "") || "my-saas";
|
|
@@ -343,7 +507,7 @@ function generateSecret(bytes = 32) {
|
|
|
343
507
|
return randomBytes(bytes).toString("base64url");
|
|
344
508
|
}
|
|
345
509
|
function isDirectoryEmpty(dir) {
|
|
346
|
-
if (!
|
|
510
|
+
if (!existsSync3(dir)) return true;
|
|
347
511
|
const entries = readdirSync(dir).filter((e) => ![".git", ".DS_Store"].includes(e));
|
|
348
512
|
return entries.length === 0;
|
|
349
513
|
}
|
|
@@ -408,6 +572,18 @@ function renderStackConfig(o) {
|
|
|
408
572
|
},
|
|
409
573
|
],
|
|
410
574
|
},`;
|
|
575
|
+
const legalBlock = o.consent ? ` legal: {
|
|
576
|
+
company: '${o.name.replace(/'/g, "\\'")} Ltd',
|
|
577
|
+
jurisdiction: 'Ireland',
|
|
578
|
+
// The seeded legal pages are written from here. Fill these in before you publish them:
|
|
579
|
+
// \`npx payload-consent scan\` lists every value still left as a placeholder.
|
|
580
|
+
// https://payload.solutions/docs/plugins/payload-consent/audit
|
|
581
|
+
legalName: '${o.name.replace(/'/g, "\\'")} Ltd',
|
|
582
|
+
address: '[REGISTERED ADDRESS]',
|
|
583
|
+
},` : ` legal: {
|
|
584
|
+
company: '${o.name.replace(/'/g, "\\'")} Ltd',
|
|
585
|
+
jurisdiction: 'Ireland',
|
|
586
|
+
},`;
|
|
411
587
|
return `import { defineStack } from '@/lib/stack'
|
|
412
588
|
|
|
413
589
|
/**
|
|
@@ -450,10 +626,7 @@ ${billingBlock}
|
|
|
450
626
|
sendPII: false,
|
|
451
627
|
},
|
|
452
628
|
|
|
453
|
-
|
|
454
|
-
company: '${o.name.replace(/'/g, "\\'")} Ltd',
|
|
455
|
-
jurisdiction: 'Ireland',
|
|
456
|
-
},
|
|
629
|
+
${legalBlock}
|
|
457
630
|
|
|
458
631
|
nav: [${o.billing !== "none" ? "{ label: 'Pricing', href: '/pricing' }, " : ""}{ label: 'Docs', href: 'https://payload.solutions/docs/payload-stack' }],
|
|
459
632
|
|
|
@@ -461,24 +634,24 @@ ${billingBlock}
|
|
|
461
634
|
})
|
|
462
635
|
`;
|
|
463
636
|
}
|
|
464
|
-
var
|
|
465
|
-
var
|
|
466
|
-
var
|
|
637
|
+
var IMPORT_MARKER2 = "// database-adapter-import";
|
|
638
|
+
var CONFIG_START2 = "// database-adapter-config-start";
|
|
639
|
+
var CONFIG_END2 = "// database-adapter-config-end";
|
|
467
640
|
function swapDatabaseAdapter(payloadConfigSource, db) {
|
|
468
|
-
const importLine = `${
|
|
641
|
+
const importLine = `${IMPORT_MARKER2}
|
|
469
642
|
import { ${db.importName} } from '${db.packageName}'`;
|
|
470
643
|
const withImport = payloadConfigSource.replace(
|
|
471
|
-
new RegExp(`${escapeRegExp(
|
|
644
|
+
new RegExp(`${escapeRegExp(IMPORT_MARKER2)}\\nimport [^\\n]+\\n`),
|
|
472
645
|
`${importLine}
|
|
473
646
|
`
|
|
474
647
|
);
|
|
475
648
|
if (withImport === payloadConfigSource && !payloadConfigSource.includes(importLine)) {
|
|
476
|
-
throw new Error(`payload.config.ts is missing the "${
|
|
649
|
+
throw new Error(`payload.config.ts is missing the "${IMPORT_MARKER2}" marker`);
|
|
477
650
|
}
|
|
478
|
-
const start = withImport.indexOf(
|
|
479
|
-
const end = withImport.indexOf(
|
|
651
|
+
const start = withImport.indexOf(CONFIG_START2);
|
|
652
|
+
const end = withImport.indexOf(CONFIG_END2);
|
|
480
653
|
if (start === -1 || end === -1) throw new Error(`payload.config.ts is missing the database adapter markers`);
|
|
481
|
-
return `${withImport.slice(0, start)}${
|
|
654
|
+
return `${withImport.slice(0, start)}${CONFIG_START2}
|
|
482
655
|
${db.config}
|
|
483
656
|
${withImport.slice(end)}`;
|
|
484
657
|
}
|
|
@@ -523,7 +696,7 @@ function swapStoragePackage(packageJson, storage) {
|
|
|
523
696
|
if (pkg !== storage?.packageName) delete deps[pkg];
|
|
524
697
|
}
|
|
525
698
|
if (storage) deps[storage.packageName] = payloadVersion;
|
|
526
|
-
return { ...packageJson, dependencies:
|
|
699
|
+
return { ...packageJson, dependencies: sortKeys3(deps) };
|
|
527
700
|
}
|
|
528
701
|
function storageChoice(key) {
|
|
529
702
|
return key === "none" ? null : STORAGE_CHOICES[key];
|
|
@@ -535,7 +708,16 @@ function swapDatabasePackage(packageJson, db) {
|
|
|
535
708
|
if (other.packageName !== db.packageName) delete deps[other.packageName];
|
|
536
709
|
}
|
|
537
710
|
deps[db.packageName] = payloadVersion;
|
|
538
|
-
return { ...packageJson, dependencies:
|
|
711
|
+
return { ...packageJson, dependencies: sortKeys3(deps) };
|
|
712
|
+
}
|
|
713
|
+
function stripWorkspaceDependencies(packageJson) {
|
|
714
|
+
const next = { ...packageJson };
|
|
715
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
716
|
+
const deps = next[field];
|
|
717
|
+
if (!deps) continue;
|
|
718
|
+
next[field] = Object.fromEntries(Object.entries(deps).filter(([, version2]) => !version2.startsWith("workspace:")));
|
|
719
|
+
}
|
|
720
|
+
return next;
|
|
539
721
|
}
|
|
540
722
|
function renameProject(packageJson, slug) {
|
|
541
723
|
const next = { ...packageJson, name: slug, version: "0.1.0", private: true };
|
|
@@ -560,7 +742,7 @@ function renderEnv(example, o) {
|
|
|
560
742
|
});
|
|
561
743
|
return lines.join("\n");
|
|
562
744
|
}
|
|
563
|
-
function
|
|
745
|
+
function sortKeys3(obj) {
|
|
564
746
|
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
565
747
|
}
|
|
566
748
|
function escapeRegExp(s) {
|
|
@@ -568,16 +750,24 @@ function escapeRegExp(s) {
|
|
|
568
750
|
}
|
|
569
751
|
async function configureProject(o) {
|
|
570
752
|
const db = DB_CHOICES[o.db];
|
|
571
|
-
const read = (rel) => readFile(
|
|
572
|
-
const write = (rel, content) => writeFile(
|
|
753
|
+
const read = (rel) => readFile(path4.join(o.directory, rel), "utf8");
|
|
754
|
+
const write = (rel, content) => writeFile(path4.join(o.directory, rel), content);
|
|
573
755
|
await write("src/stack.config.ts", renderStackConfig(o));
|
|
574
756
|
const storage = storageChoice(o.storage);
|
|
575
757
|
const payloadConfig = await read("src/payload.config.ts");
|
|
576
|
-
|
|
758
|
+
const withAdapters = swapStorageAdapter(swapDatabaseAdapter(payloadConfig, db), storage);
|
|
759
|
+
await write("src/payload.config.ts", o.emails ? addEmailsPlugin(withAdapters) : withAdapters);
|
|
760
|
+
await applyEmailsChoice(o.directory, o.emails);
|
|
761
|
+
await applyConsentChoice(o.directory, o.consent);
|
|
762
|
+
await removeVariants(o.directory);
|
|
577
763
|
const pkg = JSON.parse(await read("package.json"));
|
|
764
|
+
const configured = swapConsentPackages(
|
|
765
|
+
swapEmailsPackages(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.emails),
|
|
766
|
+
o.consent
|
|
767
|
+
);
|
|
578
768
|
await write(
|
|
579
769
|
"package.json",
|
|
580
|
-
JSON.stringify(renameProject(
|
|
770
|
+
JSON.stringify(renameProject(stripWorkspaceDependencies(configured), o.slug), null, 2) + "\n"
|
|
581
771
|
);
|
|
582
772
|
const example = await read(".env.example");
|
|
583
773
|
await write(".env", renderEnv(example, { connectionString: o.connectionString }));
|
|
@@ -588,9 +778,9 @@ var AUTH_METHODS = ["email-password", "magic-link", "passkey"];
|
|
|
588
778
|
var SOCIAL_PROVIDERS = ["google", "github"];
|
|
589
779
|
|
|
590
780
|
// src/template.ts
|
|
591
|
-
import { cp, mkdir, rm } from "fs/promises";
|
|
592
|
-
import { existsSync as
|
|
593
|
-
import
|
|
781
|
+
import { cp as cp3, mkdir, rm as rm4 } from "fs/promises";
|
|
782
|
+
import { existsSync as existsSync4 } from "fs";
|
|
783
|
+
import path5 from "path";
|
|
594
784
|
import { Readable } from "stream";
|
|
595
785
|
import { pipeline } from "stream/promises";
|
|
596
786
|
import * as tar from "tar";
|
|
@@ -618,29 +808,30 @@ Check your network, or pass --local-template <path> to scaffold from a local che
|
|
|
618
808
|
}
|
|
619
809
|
})
|
|
620
810
|
);
|
|
621
|
-
if (!
|
|
811
|
+
if (!existsSync4(path5.join(dest, "package.json"))) {
|
|
622
812
|
throw new Error(`The downloaded archive did not contain ${TEMPLATE_PATH}/package.json (branch "${branch}").`);
|
|
623
813
|
}
|
|
624
814
|
}
|
|
625
815
|
async function copyLocalTemplate({ from, dest }) {
|
|
626
|
-
const source =
|
|
627
|
-
if (!
|
|
816
|
+
const source = path5.resolve(from);
|
|
817
|
+
if (!existsSync4(path5.join(source, "package.json"))) {
|
|
628
818
|
throw new Error(`--local-template must point at a directory containing package.json (got ${source})`);
|
|
629
819
|
}
|
|
630
820
|
await mkdir(dest, { recursive: true });
|
|
631
|
-
await
|
|
821
|
+
await cp3(source, dest, {
|
|
632
822
|
recursive: true,
|
|
633
823
|
filter: (src) => {
|
|
634
|
-
const rel =
|
|
635
|
-
const top = rel.split(
|
|
824
|
+
const rel = path5.relative(source, src);
|
|
825
|
+
const top = rel.split(path5.sep)[0];
|
|
636
826
|
return !["node_modules", ".next", ".turbo", ".env", "test-results", "playwright-report", "next-env.d.ts", "tsconfig.tsbuildinfo", "CLAUDE.md"].includes(top ?? "");
|
|
637
827
|
}
|
|
638
828
|
});
|
|
639
|
-
await
|
|
829
|
+
await rm4(path5.join(dest, "CLAUDE.md"), { force: true });
|
|
640
830
|
}
|
|
641
831
|
|
|
642
832
|
// src/cli.ts
|
|
643
833
|
var DOCS = "https://payload.solutions/docs/payload-stack";
|
|
834
|
+
var CONSENT_DOCS = "https://payload.solutions/docs/plugins/payload-consent/audit";
|
|
644
835
|
function bail(message = "Cancelled.") {
|
|
645
836
|
p.cancel(message);
|
|
646
837
|
process.exit(1);
|
|
@@ -674,7 +865,7 @@ async function run(flags, positionalName) {
|
|
|
674
865
|
})
|
|
675
866
|
);
|
|
676
867
|
const slug = slugify(name);
|
|
677
|
-
const directory =
|
|
868
|
+
const directory = path6.resolve(process.cwd(), slug);
|
|
678
869
|
if (!isDirectoryEmpty(directory)) {
|
|
679
870
|
bail(`Directory ./${slug} already exists and is not empty.`);
|
|
680
871
|
}
|
|
@@ -759,6 +950,22 @@ async function run(flags, positionalName) {
|
|
|
759
950
|
]
|
|
760
951
|
})
|
|
761
952
|
));
|
|
953
|
+
const emails = flags.emails ?? (flags.defaults ? true : guard(
|
|
954
|
+
await p.confirm({
|
|
955
|
+
message: "Transactional emails",
|
|
956
|
+
initialValue: true,
|
|
957
|
+
active: "Editable in the admin (Payload Emails)",
|
|
958
|
+
inactive: "React Email components in code"
|
|
959
|
+
})
|
|
960
|
+
));
|
|
961
|
+
const consent = flags.consent ?? (flags.defaults ? true : guard(
|
|
962
|
+
await p.confirm({
|
|
963
|
+
message: "Cookie consent and legal pages",
|
|
964
|
+
initialValue: true,
|
|
965
|
+
active: "Banner, consent records and audited legal pages (Payload Consent)",
|
|
966
|
+
inactive: "Plain legal pages, no banner"
|
|
967
|
+
})
|
|
968
|
+
));
|
|
762
969
|
const packageManager = flags.packageManager ?? await detectPackageManager();
|
|
763
970
|
const options = {
|
|
764
971
|
name,
|
|
@@ -771,6 +978,8 @@ async function run(flags, positionalName) {
|
|
|
771
978
|
organizations,
|
|
772
979
|
billing,
|
|
773
980
|
storage,
|
|
981
|
+
emails,
|
|
982
|
+
consent,
|
|
774
983
|
packageManager,
|
|
775
984
|
install: flags.install,
|
|
776
985
|
git: flags.git
|
|
@@ -806,7 +1015,7 @@ async function run(flags, positionalName) {
|
|
|
806
1015
|
${error instanceof Error ? error.message.split("\n")[0] : ""}`);
|
|
807
1016
|
}
|
|
808
1017
|
}
|
|
809
|
-
if (options.git && !
|
|
1018
|
+
if (options.git && !existsSync5(path6.join(directory, ".git"))) {
|
|
810
1019
|
try {
|
|
811
1020
|
await execa2("git", ["init", "-b", "main"], { cwd: directory });
|
|
812
1021
|
await execa2("git", ["add", "-A"], { cwd: directory });
|
|
@@ -823,7 +1032,12 @@ ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
|
|
|
823
1032
|
...billing !== "none" ? ["# add STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and NEXT_PUBLIC_STRIPE_PRICE_* to .env when you are ready for billing"] : [],
|
|
824
1033
|
...storage !== "none" ? [`# add ${listWords(STORAGE_CHOICES[storage].envVars)} to .env to store uploads in ${STORAGE_CHOICES[storage].label} (local disk until then)`] : [],
|
|
825
1034
|
runCommand(packageManager, "dev"),
|
|
826
|
-
"open http://localhost:3000/admin"
|
|
1035
|
+
"open http://localhost:3000/admin",
|
|
1036
|
+
...emails ? ["# email copy is under Emails in the admin; the design is src/emails/template.tsx"] : [],
|
|
1037
|
+
...consent ? [
|
|
1038
|
+
"# the seeded legal pages are templates, not policy: the homepage shows the six steps in development",
|
|
1039
|
+
`# ${CONSENT_DOCS}`
|
|
1040
|
+
] : []
|
|
827
1041
|
];
|
|
828
1042
|
p.note(steps.join("\n"), "Done. Next steps:");
|
|
829
1043
|
p.outro(`Docs: ${pc2.underline(DOCS)}`);
|
package/package.json
CHANGED