create-ampless 0.2.0-alpha.8 → 0.2.0-alpha.9

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/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { spinner as spinner2, outro, log as log3 } from "@clack/prompts";
5
- import { existsSync as existsSync4 } from "fs";
4
+ import { spinner as spinner2, outro as outro3, log as log5 } from "@clack/prompts";
5
+ import { existsSync as existsSync7 } from "fs";
6
6
  import { basename as basename3, resolve as resolve5 } from "path";
7
7
 
8
8
  // src/prompts.ts
@@ -28,7 +28,10 @@ async function runPrompts(argProjectName) {
28
28
  // Multiple themes can ship side-by-side. The first selected is the
29
29
  // default active theme; admins can switch per-site at runtime. Add
30
30
  // / remove themes later by editing themes-registry.ts and
31
- // themes/<name>/.
31
+ // themes/<name>/. Initial values pick every shipped theme so the
32
+ // shared `themes-registry.ts` placeholder (which imports all of
33
+ // them) compiles out of the box; deselect any the project won't
34
+ // need before confirming.
32
35
  themes: () => p.multiselect({
33
36
  message: "Themes to install (space to toggle)",
34
37
  options: [
@@ -39,7 +42,7 @@ async function runPrompts(argProjectName) {
39
42
  { value: "docs", label: "Docs \u2014 sidebar-led docs (tag-driven sections)" },
40
43
  { value: "dads", label: "DADS \u2014 Digital Agency Design System (Japanese government style)" }
41
44
  ],
42
- initialValues: ["blog"],
45
+ initialValues: ["blog", "minimal", "landing", "corporate", "docs", "dads"],
43
46
  required: true
44
47
  }),
45
48
  plugins: () => p.multiselect({
@@ -83,8 +86,8 @@ async function runPrompts(argProjectName) {
83
86
  }
84
87
 
85
88
  // src/scaffold.ts
86
- import { cp, readFile, writeFile, readdir, mkdir, rm } from "fs/promises";
87
- import { join, extname, resolve as resolve2 } from "path";
89
+ import { cp, readFile, writeFile, readdir as readdir2, mkdir, rm } from "fs/promises";
90
+ import { join as join2, extname, resolve as resolve2 } from "path";
88
91
 
89
92
  // src/templates.ts
90
93
  import { existsSync } from "fs";
@@ -104,6 +107,100 @@ function templatePath(theme) {
104
107
  return resolve(templatesDir, theme);
105
108
  }
106
109
 
110
+ // src/gitignore.ts
111
+ var DEFAULT_GITIGNORE = `# Dependencies
112
+ node_modules/
113
+
114
+ # Next.js
115
+ .next/
116
+ next-env.d.ts
117
+
118
+ # Amplify
119
+ .amplify/
120
+ amplify_outputs.json
121
+
122
+ # TypeScript build artifacts
123
+ *.tsbuildinfo
124
+
125
+ # Env / OS noise
126
+ .env
127
+ .env.local
128
+ .env.*.local
129
+ .DS_Store
130
+
131
+ # Logs
132
+ npm-debug.log*
133
+ yarn-debug.log*
134
+ yarn-error.log*
135
+
136
+ # Editor
137
+ .vscode/
138
+ .idea/
139
+ `;
140
+
141
+ // src/themes-registry.ts
142
+ import { readdir } from "fs/promises";
143
+ import { existsSync as existsSync2 } from "fs";
144
+ import { join } from "path";
145
+ var CUSTOM_THEME_PREFIX = "my-";
146
+ function toIdentifier(themeName) {
147
+ if (!themeName.includes("-")) return themeName;
148
+ return themeName.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
149
+ }
150
+ function isCustomTheme(themeName) {
151
+ return themeName.startsWith(CUSTOM_THEME_PREFIX);
152
+ }
153
+ async function discoverInstalledThemes(destDir) {
154
+ const themesDir = join(destDir, "themes");
155
+ if (!existsSync2(themesDir)) return [];
156
+ const entries = await readdir(themesDir, { withFileTypes: true });
157
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
158
+ }
159
+ function buildRegistry(themes) {
160
+ const sorted = [...themes].sort((a, b) => {
161
+ const ca = isCustomTheme(a);
162
+ const cb = isCustomTheme(b);
163
+ if (ca !== cb) return ca ? 1 : -1;
164
+ return a.localeCompare(b);
165
+ });
166
+ const imports = sorted.map((t) => `import ${toIdentifier(t)} from '@/themes/${t}'`).join("\n");
167
+ const map = sorted.map((t) => {
168
+ const id = toIdentifier(t);
169
+ return id === t ? ` ${t},` : ` '${t}': ${id},`;
170
+ }).join("\n");
171
+ const fallback = sorted[0] ?? "blog";
172
+ return `// Generated by create-ampless. Lists every theme installed under
173
+ // \`themes/<name>/\`. Auto-managed: \`create-ampless upgrade\` and
174
+ // \`copy-theme\` regenerate this file from the directories present
175
+ // under \`themes/\`. Edits made by hand will survive only until the
176
+ // next upgrade.
177
+ //
178
+ // \`theme.active\` (per-site, in KvStore) picks which theme renders for
179
+ // each request; everything listed here gets bundled so switching is
180
+ // instant. Custom themes use the \`${CUSTOM_THEME_PREFIX}\` prefix and
181
+ // are never touched by upgrade \u2014 clone an ampless default theme into
182
+ // a \`${CUSTOM_THEME_PREFIX}<name>\` directory via \`copy-theme\` before
183
+ // modifying it.
184
+
185
+ ${imports}
186
+
187
+ export const themes = {
188
+ ${map}
189
+ } as const
190
+
191
+ export type ThemeName = keyof typeof themes
192
+
193
+ export const themeList = Object.values(themes)
194
+
195
+ /**
196
+ * Default theme used when a site has no \`theme.active\` override.
197
+ * Falls back to the first registered theme so an empty registry would
198
+ * still surface a clear runtime error rather than silent breakage.
199
+ */
200
+ export const DEFAULT_THEME: ThemeName = (themeList[0]?.name as ThemeName) ?? '${fallback}'
201
+ `;
202
+ }
203
+
107
204
  // src/scaffold.ts
108
205
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
109
206
  ".json",
@@ -131,10 +228,10 @@ async function substituteFile(filePath, vars) {
131
228
  if (replaced !== content) await writeFile(filePath, replaced, "utf-8");
132
229
  }
133
230
  async function substituteDir(dirPath, vars) {
134
- const entries = await readdir(dirPath, { withFileTypes: true });
231
+ const entries = await readdir2(dirPath, { withFileTypes: true });
135
232
  await Promise.all(
136
233
  entries.map(async (entry) => {
137
- const fullPath = join(dirPath, entry.name);
234
+ const fullPath = join2(dirPath, entry.name);
138
235
  if (entry.isDirectory()) {
139
236
  await substituteDir(fullPath, vars);
140
237
  } else {
@@ -143,30 +240,6 @@ async function substituteDir(dirPath, vars) {
143
240
  })
144
241
  );
145
242
  }
146
- function buildRegistry(themes) {
147
- const imports = themes.map((t) => `import ${t} from '@/themes/${t}'`).join("\n");
148
- const map = themes.map((t) => ` ${t},`).join("\n");
149
- return `// Generated by create-ampless. Lists every theme installed under
150
- // \`themes/<name>/\`. Adding a theme = drop a directory under themes/,
151
- // add an import + map entry below, and redeploy.
152
- //
153
- // \`theme.active\` (per-site, in KvStore) picks which theme renders for
154
- // each request; everything listed here gets bundled so switching is
155
- // instant.
156
-
157
- ${imports}
158
-
159
- export const themes = {
160
- ${map}
161
- } as const
162
-
163
- export type ThemeName = keyof typeof themes
164
-
165
- export const themeList = Object.values(themes)
166
-
167
- export const DEFAULT_THEME: ThemeName = (themeList[0]?.name as ThemeName) ?? '${themes[0]}'
168
- `;
169
- }
170
243
  async function scaffold(sharedDir, themesRoot, destDir, opts) {
171
244
  await cp(sharedDir, destDir, { recursive: true });
172
245
  const destThemesDir = resolve2(destDir, "themes");
@@ -180,6 +253,8 @@ async function scaffold(sharedDir, themesRoot, destDir, opts) {
180
253
  void themesRoot;
181
254
  const registryPath = resolve2(destDir, "themes-registry.ts");
182
255
  await writeFile(registryPath, buildRegistry(opts.themes), "utf-8");
256
+ const gitignorePath = resolve2(destDir, ".gitignore");
257
+ await writeFile(gitignorePath, DEFAULT_GITIGNORE, "utf-8");
183
258
  const vars = {
184
259
  projectName: opts.projectName,
185
260
  siteName: opts.siteName,
@@ -209,6 +284,9 @@ var STRING_FLAGS = /* @__PURE__ */ new Set([
209
284
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
210
285
  "--deploy",
211
286
  "--mount",
287
+ "--upgrade",
288
+ "--dry-run",
289
+ "--no-install",
212
290
  "--github-private",
213
291
  "--create-iam-role",
214
292
  "--skip-confirm",
@@ -219,6 +297,10 @@ function parseDeployArgs(argv) {
219
297
  const out = {
220
298
  deploy: false,
221
299
  mount: false,
300
+ upgrade: false,
301
+ copyTheme: false,
302
+ dryRun: false,
303
+ noInstall: false,
222
304
  githubPrivate: false,
223
305
  createIamRole: false,
224
306
  skipConfirm: false,
@@ -242,6 +324,15 @@ function parseDeployArgs(argv) {
242
324
  case "--mount":
243
325
  out.mount = true;
244
326
  break;
327
+ case "--upgrade":
328
+ out.upgrade = true;
329
+ break;
330
+ case "--dry-run":
331
+ out.dryRun = true;
332
+ break;
333
+ case "--no-install":
334
+ out.noInstall = true;
335
+ break;
245
336
  case "--github-private":
246
337
  out.githubPrivate = true;
247
338
  break;
@@ -317,6 +408,22 @@ function parseDeployArgs(argv) {
317
408
  out.unknown.push(raw);
318
409
  continue;
319
410
  }
411
+ if (raw === "upgrade" && out.projectName === void 0) {
412
+ out.upgrade = true;
413
+ continue;
414
+ }
415
+ if (raw === "copy-theme" && out.projectName === void 0 && !out.copyTheme) {
416
+ out.copyTheme = true;
417
+ continue;
418
+ }
419
+ if (out.copyTheme && out.copyThemeSource === void 0) {
420
+ out.copyThemeSource = raw;
421
+ continue;
422
+ }
423
+ if (out.copyTheme && out.copyThemeTarget === void 0) {
424
+ out.copyThemeTarget = raw;
425
+ continue;
426
+ }
320
427
  if (out.projectName === void 0) {
321
428
  out.projectName = raw;
322
429
  } else {
@@ -330,6 +437,7 @@ var HELP_TEXT = `create-ampless \u2014 scaffold an ampless project
330
437
  Usage:
331
438
  npx create-ampless@alpha <project-name> [options]
332
439
  npx create-ampless@alpha --mount [options] # in an existing project dir
440
+ npx create-ampless@alpha upgrade [options] # in an existing project dir
333
441
 
334
442
  Options:
335
443
  --site-name <name> Site display name (default: "My Blog")
@@ -365,6 +473,21 @@ Options:
365
473
  --skip-confirm Skip all interactive prompts and use defaults /
366
474
  flag values (for CI / automation)
367
475
  -h, --help Show this message
476
+
477
+ upgrade Sync ampless package files / deps to latest alpha.
478
+ Run inside an existing ampless project.
479
+
480
+ --dry-run Show what would change without writing any files
481
+ --no-install Skip running pnpm/npm install after updating
482
+ package.json
483
+
484
+ copy-theme <source> <target>
485
+ Copy a theme so the original stays managed by ampless and
486
+ customisations live in the copy. Target must start with "my-"
487
+ (the convention that flags it as user-owned, so upgrade leaves
488
+ it alone). Run inside an existing ampless project.
489
+
490
+ Example: npx create-ampless@alpha copy-theme blog my-blog
368
491
  `;
369
492
 
370
493
  // src/deploy-prompts.ts
@@ -373,8 +496,8 @@ import { execa as execa3 } from "execa";
373
496
 
374
497
  // src/deploy.ts
375
498
  import { execa as execa2 } from "execa";
376
- import { existsSync as existsSync3 } from "fs";
377
- import { writeFile as writeFile2 } from "fs/promises";
499
+ import { existsSync as existsSync4 } from "fs";
500
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
378
501
  import { basename as basename2, resolve as resolve4 } from "path";
379
502
  import { spinner, log } from "@clack/prompts";
380
503
  import pc2 from "picocolors";
@@ -852,42 +975,21 @@ function printPreflightReport(problems) {
852
975
  }
853
976
 
854
977
  // src/mount.ts
855
- import { existsSync as existsSync2 } from "fs";
978
+ import { existsSync as existsSync3 } from "fs";
856
979
  import { resolve as resolve3 } from "path";
857
980
  function validateMountableProject(destDir) {
858
981
  const required = ["package.json", "cms.config.ts"];
859
982
  for (const f of required) {
860
- if (!existsSync2(resolve3(destDir, f))) {
983
+ if (!existsSync3(resolve3(destDir, f))) {
861
984
  return `Not an ampless project (missing ${f} in ${destDir})`;
862
985
  }
863
986
  }
864
987
  const amplifyFiles = ["amplify/backend.ts", "amplify/data/resource.ts"];
865
- if (!amplifyFiles.some((f) => existsSync2(resolve3(destDir, f)))) {
988
+ if (!amplifyFiles.some((f) => existsSync3(resolve3(destDir, f)))) {
866
989
  return `Not an ampless project (missing amplify/ in ${destDir})`;
867
990
  }
868
991
  return null;
869
992
  }
870
- var MOUNT_DEFAULT_GITIGNORE = `# Dependencies
871
- node_modules/
872
-
873
- # Next.js
874
- .next/
875
- .amplify/
876
-
877
- # Amplify outputs (regenerated on every deploy / sandbox)
878
- amplify_outputs.json
879
-
880
- # Env / OS noise
881
- .env
882
- .env.local
883
- .env.*.local
884
- .DS_Store
885
-
886
- # Logs
887
- npm-debug.log*
888
- yarn-debug.log*
889
- yarn-error.log*
890
- `;
891
993
  function originPointsAt(origin, owner, name) {
892
994
  const candidates = [
893
995
  `https://github.com/${owner}/${name}`,
@@ -986,6 +1088,42 @@ function splitDomain(domain, subdomain) {
986
1088
  const prefix = domain.slice(0, domain.length - registrable.length - 1);
987
1089
  return { registrable, subdomain: prefix };
988
1090
  }
1091
+ async function rewriteCmsConfigForDomain(projectDir, fullDomain) {
1092
+ const path = resolve4(projectDir, "cms.config.ts");
1093
+ if (!existsSync4(path)) {
1094
+ return { urlRewritten: false, sitesInjected: false };
1095
+ }
1096
+ let content = await readFile2(path, "utf-8");
1097
+ let urlRewritten = false;
1098
+ let sitesInjected = false;
1099
+ const urlRe = /url:\s*['"]http:\/\/localhost:3000['"]/;
1100
+ if (urlRe.test(content)) {
1101
+ content = content.replace(urlRe, `url: 'https://${fullDomain}'`);
1102
+ urlRewritten = true;
1103
+ }
1104
+ const sitesActiveRe = /^\s*sites:\s*\{/m;
1105
+ if (!sitesActiveRe.test(content)) {
1106
+ const siteCloseRe = /(\n(\s*)site:\s*\{[\s\S]*?\n\2\},)/;
1107
+ const m = siteCloseRe.exec(content);
1108
+ if (m) {
1109
+ const indent = m[2] ?? " ";
1110
+ const inner = indent + " ";
1111
+ const innermost = inner + " ";
1112
+ const inject = `
1113
+ ${indent}sites: {
1114
+ ${inner}default: {
1115
+ ${innermost}domains: ['${fullDomain}'],
1116
+ ${inner}},
1117
+ ${indent}},`;
1118
+ content = content.replace(siteCloseRe, `$1${inject}`);
1119
+ sitesInjected = true;
1120
+ }
1121
+ }
1122
+ if (urlRewritten || sitesInjected) {
1123
+ await writeFile2(path, content, "utf-8");
1124
+ }
1125
+ return { urlRewritten, sitesInjected };
1126
+ }
989
1127
  async function run(cmd, args, opts = {}) {
990
1128
  const { step, ...execaOpts } = opts;
991
1129
  try {
@@ -1026,8 +1164,8 @@ async function currentBranch(dir) {
1026
1164
  }
1027
1165
  async function ensureGitignore(dir) {
1028
1166
  const target = resolve4(dir, ".gitignore");
1029
- if (existsSync3(target)) return;
1030
- await writeFile2(target, MOUNT_DEFAULT_GITIGNORE, "utf-8");
1167
+ if (existsSync4(target)) return;
1168
+ await writeFile2(target, DEFAULT_GITIGNORE, "utf-8");
1031
1169
  }
1032
1170
  async function gitInitOrReuse(dir, mount) {
1033
1171
  const repo = await isGitRepo(dir);
@@ -1361,6 +1499,17 @@ async function runDeploy(opts) {
1361
1499
  }
1362
1500
  }
1363
1501
  await writeFile2(resolve4(opts.projectDir, "amplify.yml"), AMPLIFY_BUILD_SPEC, "utf-8");
1502
+ if (opts.domain) {
1503
+ const { registrable, subdomain } = splitDomain(opts.domain, opts.subdomain);
1504
+ const fullDomain = subdomain ? `${subdomain}.${registrable}` : registrable;
1505
+ const mutations = await rewriteCmsConfigForDomain(opts.projectDir, fullDomain);
1506
+ if (mutations.urlRewritten || mutations.sitesInjected) {
1507
+ const parts = [];
1508
+ if (mutations.urlRewritten) parts.push(`site.url \u2192 https://${fullDomain}`);
1509
+ if (mutations.sitesInjected) parts.push(`sites.default.domains += ${fullDomain}`);
1510
+ log.info(`cms.config.ts updated: ${parts.join(", ")}`);
1511
+ }
1512
+ }
1364
1513
  const created = {};
1365
1514
  const fail = (step, cause) => {
1366
1515
  const msg = cause instanceof Error ? cause.message : String(cause);
@@ -1605,15 +1754,388 @@ async function gatherDeployOptions(args, projectDir, projectName) {
1605
1754
  };
1606
1755
  }
1607
1756
 
1608
- // src/index.ts
1757
+ // src/upgrade.ts
1758
+ import { cp as cp2, readFile as readFile3, writeFile as writeFile3, readdir as readdir3, mkdir as mkdir2, rm as rm2 } from "fs/promises";
1759
+ import { existsSync as existsSync5 } from "fs";
1760
+ import { join as join3, relative, extname as extname2, dirname } from "path";
1761
+ import { log as log3, outro } from "@clack/prompts";
1609
1762
  import pc3 from "picocolors";
1763
+ import { execa as execa4 } from "execa";
1764
+ var AMPLESS_PACKAGES = /* @__PURE__ */ new Set([
1765
+ "ampless",
1766
+ "@ampless/admin",
1767
+ "@ampless/backend",
1768
+ "@ampless/runtime",
1769
+ "@ampless/plugin-seo",
1770
+ "@ampless/plugin-rss",
1771
+ "@ampless/plugin-webhook",
1772
+ "@ampless/plugin-og-image"
1773
+ ]);
1774
+ var AMPLESS_MANAGED_SCRIPTS = /* @__PURE__ */ new Set([
1775
+ "sandbox",
1776
+ "sandbox:dev",
1777
+ "update-ampless",
1778
+ "copy-theme"
1779
+ ]);
1780
+ var PROTECTED_PATTERNS = [
1781
+ /^cms\.config\.ts$/,
1782
+ // `themes/` and `themes-registry.ts` are handled separately — see
1783
+ // syncThemes() below. The themes directory is owned per-entry by
1784
+ // either ampless (default themes, resynced on upgrade) or the user
1785
+ // (`my-*` themes, preserved). The registry is regenerated to match
1786
+ // whatever is on disk afterward.
1787
+ /^\.env/,
1788
+ /^node_modules(\/|$)/,
1789
+ /^\.next(\/|$)/,
1790
+ /^\.turbo(\/|$)/,
1791
+ /^\.amplify(\/|$)/,
1792
+ /^amplify_outputs\.json$/,
1793
+ /^next-env\.d\.ts$/,
1794
+ /^tsconfig\.tsbuildinfo$/,
1795
+ /^pnpm-lock\.yaml$/,
1796
+ /^package-lock\.json$/,
1797
+ // Anything under themes/ is handled by syncThemes; the file-walk
1798
+ // classifier must not double-process it.
1799
+ /^themes(\/|$)/,
1800
+ /^themes-registry\.ts$/
1801
+ ];
1802
+ var SEED_IF_MISSING_PATTERN = /\.custom\.ts$/;
1803
+ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
1804
+ ".json",
1805
+ ".md",
1806
+ ".ts",
1807
+ ".tsx",
1808
+ ".js",
1809
+ ".jsx",
1810
+ ".mjs",
1811
+ ".cjs",
1812
+ ".html",
1813
+ ".css",
1814
+ ".env",
1815
+ ".txt",
1816
+ ".yaml",
1817
+ ".yml",
1818
+ ".toml",
1819
+ ".gitignore"
1820
+ ]);
1821
+ function isProtected(relPath) {
1822
+ return PROTECTED_PATTERNS.some((re) => re.test(relPath));
1823
+ }
1824
+ async function listShippedThemes(templatesRoot) {
1825
+ if (!existsSync5(templatesRoot)) return [];
1826
+ const entries = await readdir3(templatesRoot, { withFileTypes: true });
1827
+ return entries.filter((e) => e.isDirectory() && e.name !== "_shared").map((e) => e.name).sort();
1828
+ }
1829
+ var USER_OWNED_THEME_FILES = /* @__PURE__ */ new Set(["README.md", ".gitignore"]);
1830
+ function isUserOwnedThemeFile(path) {
1831
+ const name = path.split(/[/\\]/).pop() ?? "";
1832
+ return USER_OWNED_THEME_FILES.has(name);
1833
+ }
1834
+ async function captureUserOwnedFiles(dir) {
1835
+ const out = /* @__PURE__ */ new Map();
1836
+ if (!existsSync5(dir)) return out;
1837
+ for (const name of USER_OWNED_THEME_FILES) {
1838
+ const p3 = join3(dir, name);
1839
+ if (existsSync5(p3)) {
1840
+ out.set(name, await readFile3(p3));
1841
+ }
1842
+ }
1843
+ return out;
1844
+ }
1845
+ async function restorePreservedFiles(dir, files) {
1846
+ for (const [name, content] of files) {
1847
+ await mkdir2(dir, { recursive: true });
1848
+ await writeFile3(join3(dir, name), content);
1849
+ }
1850
+ }
1851
+ async function syncThemes(destDir, templatesRoot) {
1852
+ const shipped = await listShippedThemes(templatesRoot);
1853
+ const themesDir = join3(destDir, "themes");
1854
+ await mkdir2(themesDir, { recursive: true });
1855
+ for (const name of shipped) {
1856
+ const src = join3(templatesRoot, name);
1857
+ const dst = join3(themesDir, name);
1858
+ const preservedFiles = await captureUserOwnedFiles(dst);
1859
+ await rm2(dst, { recursive: true, force: true });
1860
+ await cp2(src, dst, {
1861
+ recursive: true,
1862
+ filter: (sourcePath) => !isUserOwnedThemeFile(sourcePath)
1863
+ });
1864
+ await restorePreservedFiles(dst, preservedFiles);
1865
+ }
1866
+ const installed = await discoverInstalledThemes(destDir);
1867
+ const shippedSet = new Set(shipped);
1868
+ const preserved = installed.filter((t) => !shippedSet.has(t));
1869
+ const all = [...shipped, ...preserved];
1870
+ await writeFile3(join3(destDir, "themes-registry.ts"), buildRegistry(all), "utf-8");
1871
+ return { synced: shipped, preserved };
1872
+ }
1873
+ async function walkDir(dir, base, out) {
1874
+ const entries = await readdir3(dir, { withFileTypes: true });
1875
+ for (const entry of entries) {
1876
+ const full = join3(dir, entry.name);
1877
+ const rel = relative(base, full);
1878
+ if (entry.isDirectory()) {
1879
+ await walkDir(full, base, out);
1880
+ } else {
1881
+ out.push(rel);
1882
+ }
1883
+ }
1884
+ }
1885
+ function substituteVars(content, vars) {
1886
+ return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
1887
+ }
1888
+ async function copyWithSubstitution(src, dst, vars) {
1889
+ const ext = extname2(src) || (src.endsWith(".gitignore") ? ".gitignore" : "");
1890
+ await mkdir2(dirname(dst), { recursive: true });
1891
+ if (TEXT_EXTENSIONS2.has(ext)) {
1892
+ const content = await readFile3(src, "utf-8");
1893
+ await writeFile3(dst, substituteVars(content, vars), "utf-8");
1894
+ } else {
1895
+ const buf = await readFile3(src);
1896
+ await writeFile3(dst, buf);
1897
+ }
1898
+ }
1899
+ function detectIndent(jsonStr) {
1900
+ const m = jsonStr.match(/^{\n(\s+)/);
1901
+ if (!m) return 2;
1902
+ return m[1].length;
1903
+ }
1904
+ async function runUpgradeIn(destDir, sharedDir, opts = {}) {
1905
+ const derivedRoot = opts.templatesRoot ?? join3(sharedDir, "..");
1906
+ const themeSyncEnabled = existsSync5(join3(derivedRoot, "_shared"));
1907
+ const templatesRoot = themeSyncEnabled ? derivedRoot : "";
1908
+ const problem = validateMountableProject(destDir);
1909
+ if (problem) {
1910
+ throw new Error(problem);
1911
+ }
1912
+ const projectPkgPath = join3(destDir, "package.json");
1913
+ const projectPkgRaw = await readFile3(projectPkgPath, "utf-8");
1914
+ const projectPkg = JSON.parse(projectPkgRaw);
1915
+ const projectName = typeof projectPkg.name === "string" && projectPkg.name ? projectPkg.name : "my-ampless-site";
1916
+ const vars = { projectName };
1917
+ const allRelPaths = [];
1918
+ await walkDir(sharedDir, sharedDir, allRelPaths);
1919
+ const classification = { replace: [], merge: [], seed: [], protected: [] };
1920
+ for (const rel of allRelPaths) {
1921
+ if (SEED_IF_MISSING_PATTERN.test(rel)) {
1922
+ classification.seed.push(rel);
1923
+ } else if (isProtected(rel)) {
1924
+ classification.protected.push(rel);
1925
+ } else if (rel === "package.json") {
1926
+ classification.merge.push(rel);
1927
+ } else {
1928
+ classification.replace.push(rel);
1929
+ }
1930
+ }
1931
+ const replaceNew = classification.replace.filter((r) => !existsSync5(join3(destDir, r)));
1932
+ const replaceUpdate = classification.replace.filter((r) => existsSync5(join3(destDir, r)));
1933
+ const seedNew = classification.seed.filter((r) => !existsSync5(join3(destDir, r)));
1934
+ const seedSkipped = classification.seed.filter((r) => existsSync5(join3(destDir, r)));
1935
+ const shippedThemes = themeSyncEnabled ? await listShippedThemes(templatesRoot) : [];
1936
+ const existingThemes = themeSyncEnabled ? await discoverInstalledThemes(destDir) : [];
1937
+ const preservedThemes = existingThemes.filter((t) => !shippedThemes.includes(t));
1938
+ log3.info(
1939
+ `replace: ${pc3.green(`\u8FFD\u52A0 ${replaceNew.length}`)} / ${pc3.yellow(`\u66F4\u65B0 ${replaceUpdate.length}`)}`
1940
+ );
1941
+ log3.info(`merge: ${pc3.cyan("package.json: ampless deps / managed scripts \u3092\u30C6\u30F3\u30D7\u30EC\u5074\u306B\u5408\u308F\u305B\u308B")}`);
1942
+ if (classification.seed.length > 0) {
1943
+ log3.info(
1944
+ `seed: ${pc3.green(`\u8FFD\u52A0 ${seedNew.length}`)} / ${pc3.dim(`\u65E2\u5B58\u30D5\u30A1\u30A4\u30EB\u306F\u4E0A\u66F8\u304D\u3057\u306A\u3044: ${seedSkipped.length}`)} (*.custom.ts)`
1945
+ );
1946
+ }
1947
+ if (themeSyncEnabled) {
1948
+ log3.info(
1949
+ `themes: ${pc3.cyan(`\u30C7\u30D5\u30A9\u30EB\u30C8\u30C6\u30FC\u30DE\u3092\u540C\u671F: ${shippedThemes.length}`)} / ${pc3.dim(`\u30AB\u30B9\u30BF\u30E0\u30C6\u30FC\u30DE (my-*) \u3092\u4FDD\u6301: ${preservedThemes.length}`)}`
1950
+ );
1951
+ }
1952
+ log3.info(`protected: ${pc3.dim(`\u30C6\u30F3\u30D7\u30EC\u306B\u5B58\u5728\u3059\u308B\u304C\u89E6\u3089\u306A\u3044: ${classification.protected.length} \u500B`)}`);
1953
+ if (opts.dryRun) {
1954
+ return {
1955
+ added: replaceNew,
1956
+ updated: replaceUpdate,
1957
+ seeded: seedNew,
1958
+ protected: classification.protected,
1959
+ themesSynced: shippedThemes,
1960
+ themesPreserved: preservedThemes,
1961
+ packageJsonMerged: false
1962
+ };
1963
+ }
1964
+ for (const rel of classification.replace) {
1965
+ const src = join3(sharedDir, rel);
1966
+ const dst = join3(destDir, rel);
1967
+ await copyWithSubstitution(src, dst, vars);
1968
+ }
1969
+ for (const rel of seedNew) {
1970
+ const src = join3(sharedDir, rel);
1971
+ const dst = join3(destDir, rel);
1972
+ await copyWithSubstitution(src, dst, vars);
1973
+ }
1974
+ const themeResult = themeSyncEnabled ? await syncThemes(destDir, templatesRoot) : { synced: [], preserved: [] };
1975
+ const templatePkgRaw = await readFile3(join3(sharedDir, "package.json"), "utf-8");
1976
+ const templatePkg = JSON.parse(templatePkgRaw);
1977
+ const indent = detectIndent(projectPkgRaw);
1978
+ for (const section of ["dependencies", "devDependencies"]) {
1979
+ const templateDeps = templatePkg[section] ?? {};
1980
+ const projectDeps = projectPkg[section] ?? {};
1981
+ for (const [name, version] of Object.entries(templateDeps)) {
1982
+ if (!AMPLESS_PACKAGES.has(name)) continue;
1983
+ projectDeps[name] = version;
1984
+ }
1985
+ if (Object.keys(projectDeps).length > 0) {
1986
+ projectPkg[section] = projectDeps;
1987
+ }
1988
+ }
1989
+ const templateScripts = templatePkg["scripts"] ?? {};
1990
+ const projectScripts = projectPkg["scripts"] ?? {};
1991
+ for (const [name, body] of Object.entries(templateScripts)) {
1992
+ if (!AMPLESS_MANAGED_SCRIPTS.has(name)) continue;
1993
+ projectScripts[name] = body;
1994
+ }
1995
+ if (Object.keys(projectScripts).length > 0) {
1996
+ projectPkg["scripts"] = projectScripts;
1997
+ }
1998
+ await writeFile3(projectPkgPath, JSON.stringify(projectPkg, null, indent) + "\n", "utf-8");
1999
+ if (!opts.noInstall) {
2000
+ const usePnpm = existsSync5(join3(destDir, "pnpm-lock.yaml"));
2001
+ const pm = usePnpm ? "pnpm" : "npm";
2002
+ await execa4(pm, ["install"], { cwd: destDir, stdio: "inherit" });
2003
+ }
2004
+ return {
2005
+ added: replaceNew,
2006
+ updated: replaceUpdate,
2007
+ seeded: seedNew,
2008
+ protected: classification.protected,
2009
+ themesSynced: themeResult.synced,
2010
+ themesPreserved: themeResult.preserved,
2011
+ packageJsonMerged: true
2012
+ };
2013
+ }
2014
+ async function runUpgrade(args) {
2015
+ const destDir = process.cwd();
2016
+ try {
2017
+ await runUpgradeIn(destDir, sharedTemplateDir(), {
2018
+ dryRun: args.dryRun,
2019
+ noInstall: args.noInstall
2020
+ });
2021
+ } catch (err) {
2022
+ log3.error(err instanceof Error ? err.message : String(err));
2023
+ process.exit(1);
2024
+ }
2025
+ if (args.dryRun) {
2026
+ outro(`${pc3.dim("(dry-run) No files were changed.")}`);
2027
+ return;
2028
+ }
2029
+ outro(
2030
+ `${pc3.green("\u2714")} Upgrade complete
2031
+
2032
+ Next steps:
2033
+ ${pc3.cyan("git diff")} ${pc3.dim("# review changes")}
2034
+ ${pc3.cyan("git commit && git push")} ${pc3.dim("# deploy via Amplify Hosting")}`
2035
+ );
2036
+ }
2037
+
2038
+ // src/copy-theme.ts
2039
+ import { cp as cp3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2040
+ import { existsSync as existsSync6 } from "fs";
2041
+ import { join as join4 } from "path";
2042
+ import { log as log4, outro as outro2 } from "@clack/prompts";
2043
+ import pc4 from "picocolors";
2044
+ async function runCopyThemeIn(destDir, source, target) {
2045
+ const problem = validateMountableProject(destDir);
2046
+ if (problem) {
2047
+ throw new Error(problem);
2048
+ }
2049
+ if (!isCustomTheme(target)) {
2050
+ throw new Error(
2051
+ `Target theme name must start with "${CUSTOM_THEME_PREFIX}" (got "${target}"). This prefix marks the theme as user-owned so create-ampless upgrade leaves it alone.`
2052
+ );
2053
+ }
2054
+ if (source === target) {
2055
+ throw new Error(`Source and target are identical (${source}).`);
2056
+ }
2057
+ const themesDir = join4(destDir, "themes");
2058
+ const sourceDir = join4(themesDir, source);
2059
+ const targetDir = join4(themesDir, target);
2060
+ if (!existsSync6(sourceDir)) {
2061
+ throw new Error(`Source theme not found: themes/${source}/`);
2062
+ }
2063
+ if (existsSync6(targetDir)) {
2064
+ throw new Error(`Target theme already exists: themes/${target}/`);
2065
+ }
2066
+ await cp3(sourceDir, targetDir, { recursive: true });
2067
+ const filesRewritten = await rewriteThemeName(targetDir, source, target);
2068
+ const installed = await discoverInstalledThemes(destDir);
2069
+ await writeFile4(join4(destDir, "themes-registry.ts"), buildRegistry(installed), "utf-8");
2070
+ return { source, target, filesRewritten };
2071
+ }
2072
+ async function rewriteThemeName(targetDir, source, target) {
2073
+ const touched = [];
2074
+ await rewriteFile(
2075
+ join4(targetDir, "index.ts"),
2076
+ (s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
2077
+ touched
2078
+ );
2079
+ await rewriteFile(
2080
+ join4(targetDir, "manifest.ts"),
2081
+ (s) => s.replace(new RegExp(`name:\\s*'${escapeRegex(source)}'`), `name: '${target}'`).replace(new RegExp(`name:\\s*"${escapeRegex(source)}"`), `name: "${target}"`),
2082
+ touched
2083
+ );
2084
+ await rewriteFile(
2085
+ join4(targetDir, "tokens.css"),
2086
+ (s) => s.replace(new RegExp(`\\[data-theme='${escapeRegex(source)}'\\]`, "g"), `[data-theme='${target}']`).replace(new RegExp(`\\[data-theme="${escapeRegex(source)}"\\]`, "g"), `[data-theme="${target}"]`),
2087
+ touched
2088
+ );
2089
+ return touched;
2090
+ }
2091
+ async function rewriteFile(path, transform, touched) {
2092
+ if (!existsSync6(path)) return;
2093
+ const before = await readFile4(path, "utf-8");
2094
+ const after = transform(before);
2095
+ if (after === before) return;
2096
+ await writeFile4(path, after, "utf-8");
2097
+ touched.push(path);
2098
+ }
2099
+ function escapeRegex(s) {
2100
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2101
+ }
2102
+ async function runCopyTheme(args) {
2103
+ const destDir = process.cwd();
2104
+ const source = args.copyThemeSource;
2105
+ const target = args.copyThemeTarget;
2106
+ if (!source || !target) {
2107
+ log4.error("Usage: npx create-ampless@alpha copy-theme <source> <target>");
2108
+ log4.info("Example: npx create-ampless@alpha copy-theme blog my-blog");
2109
+ process.exit(1);
2110
+ }
2111
+ try {
2112
+ const result = await runCopyThemeIn(destDir, source, target);
2113
+ outro2(
2114
+ `${pc4.green("\u2714")} Copied themes/${result.source}/ \u2192 themes/${result.target}/
2115
+
2116
+ Files rewritten:
2117
+ ` + result.filesRewritten.map((f) => ` ${pc4.dim(f)}`).join("\n") + `
2118
+
2119
+ themes-registry.ts updated. Next:
2120
+ ${pc4.cyan(`Open themes/${result.target}/ and start customising`)}
2121
+ ${pc4.cyan(`Activate via /admin/sites/<siteId>/theme`)}`
2122
+ );
2123
+ } catch (err) {
2124
+ log4.error(err instanceof Error ? err.message : String(err));
2125
+ process.exit(1);
2126
+ }
2127
+ }
2128
+
2129
+ // src/index.ts
2130
+ import pc5 from "picocolors";
2131
+ var DEFAULT_THEMES = VALID_THEMES;
1610
2132
  function buildNonInteractiveOpts(args) {
1611
2133
  const projectName = args.projectName ?? (() => {
1612
2134
  const b = basename3(process.cwd());
1613
2135
  return b && b !== "/" ? b : "my-ampless-site";
1614
2136
  })();
1615
2137
  const siteName = args.siteName ?? "My Blog";
1616
- const themes = args.themes ?? ["blog"];
2138
+ const themes = args.themes ?? [...DEFAULT_THEMES];
1617
2139
  const plugins = args.plugins ?? ["seo"];
1618
2140
  return {
1619
2141
  projectName,
@@ -1630,7 +2152,7 @@ function warnIgnoredScaffoldFlags(args) {
1630
2152
  if (args.plugins) ignored.push("--plugins");
1631
2153
  if (args.projectName) ignored.push("<project-name> positional");
1632
2154
  if (ignored.length > 0) {
1633
- log3.warn(`--mount mode ignores: ${ignored.join(", ")}`);
2155
+ log5.warn(`--mount mode ignores: ${ignored.join(", ")}`);
1634
2156
  }
1635
2157
  }
1636
2158
  async function runMount(args) {
@@ -1639,8 +2161,8 @@ async function runMount(args) {
1639
2161
  warnIgnoredScaffoldFlags(args);
1640
2162
  const problem = validateMountableProject(destDir);
1641
2163
  if (problem) {
1642
- log3.error(problem);
1643
- log3.info(
2164
+ log5.error(problem);
2165
+ log5.info(
1644
2166
  "Run `npx create-ampless@alpha <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
1645
2167
  );
1646
2168
  process.exit(1);
@@ -1654,23 +2176,23 @@ async function runMount(args) {
1654
2176
  if (err instanceof PreflightError) {
1655
2177
  process.exit(1);
1656
2178
  }
1657
- log3.error(err instanceof Error ? err.message : String(err));
2179
+ log5.error(err instanceof Error ? err.message : String(err));
1658
2180
  process.exit(1);
1659
2181
  }
1660
2182
  }
1661
2183
  function printDeployResult(result) {
1662
2184
  const lines = [
1663
- `${pc3.green("\u2714")} Project deployed`,
2185
+ `${pc5.green("\u2714")} Project deployed`,
1664
2186
  ``,
1665
- ` GitHub: ${pc3.cyan(result.githubRepoUrl)}`,
1666
- ` Amplify app: ${pc3.cyan(result.amplifyAppId)}`,
1667
- ` Amplify URL: ${pc3.cyan(result.amplifyAppUrl)}`
2187
+ ` GitHub: ${pc5.cyan(result.githubRepoUrl)}`,
2188
+ ` Amplify app: ${pc5.cyan(result.amplifyAppId)}`,
2189
+ ` Amplify URL: ${pc5.cyan(result.amplifyAppUrl)}`
1668
2190
  ];
1669
2191
  if (result.domainUrl) {
1670
- lines.push(` Custom domain: ${pc3.cyan(result.domainUrl)}`);
2192
+ lines.push(` Custom domain: ${pc5.cyan(result.domainUrl)}`);
1671
2193
  }
1672
2194
  if (result.domainVerification && result.domainVerification.length > 0) {
1673
- lines.push("", ` ${pc3.bold("Add these DNS records to verify the domain:")}`);
2195
+ lines.push("", ` ${pc5.bold("Add these DNS records to verify the domain:")}`);
1674
2196
  for (const v of result.domainVerification) {
1675
2197
  lines.push(` ${v.cname} CNAME ${v.target}`);
1676
2198
  }
@@ -1678,9 +2200,9 @@ function printDeployResult(result) {
1678
2200
  lines.push(
1679
2201
  "",
1680
2202
  ` First build is now running in Amplify Hosting.`,
1681
- ` Watch it at ${pc3.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
2203
+ ` Watch it at ${pc5.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
1682
2204
  );
1683
- outro(lines.join("\n"));
2205
+ outro3(lines.join("\n"));
1684
2206
  }
1685
2207
  async function main() {
1686
2208
  const args = parseDeployArgs(process.argv.slice(2));
@@ -1689,7 +2211,15 @@ async function main() {
1689
2211
  return;
1690
2212
  }
1691
2213
  for (const flag of args.unknown) {
1692
- log3.warn(`Unknown argument ignored: ${flag}`);
2214
+ log5.warn(`Unknown argument ignored: ${flag}`);
2215
+ }
2216
+ if (args.upgrade) {
2217
+ await runUpgrade(args);
2218
+ return;
2219
+ }
2220
+ if (args.copyTheme) {
2221
+ await runCopyTheme(args);
2222
+ return;
1693
2223
  }
1694
2224
  if (args.mount) {
1695
2225
  await runMount(args);
@@ -1703,8 +2233,8 @@ async function main() {
1703
2233
  }
1704
2234
  if (!opts) return;
1705
2235
  const destDir = resolve5(process.cwd(), opts.projectName);
1706
- if (existsSync4(destDir)) {
1707
- log3.error(`Directory already exists: ${destDir}`);
2236
+ if (existsSync7(destDir)) {
2237
+ log5.error(`Directory already exists: ${destDir}`);
1708
2238
  process.exit(1);
1709
2239
  }
1710
2240
  const sharedDir = sharedTemplateDir();
@@ -1715,7 +2245,7 @@ async function main() {
1715
2245
  s.stop("Done!");
1716
2246
  } catch (err) {
1717
2247
  s.stop("Failed.");
1718
- log3.error(String(err));
2248
+ log5.error(String(err));
1719
2249
  process.exit(1);
1720
2250
  }
1721
2251
  if (args.deploy) {
@@ -1728,19 +2258,19 @@ async function main() {
1728
2258
  if (err instanceof PreflightError) {
1729
2259
  process.exit(1);
1730
2260
  }
1731
- log3.error(err instanceof Error ? err.message : String(err));
2261
+ log5.error(err instanceof Error ? err.message : String(err));
1732
2262
  process.exit(1);
1733
2263
  }
1734
2264
  return;
1735
2265
  }
1736
- outro(
1737
- `${pc3.green("\u2714")} Project created at ${pc3.bold(opts.projectName)}
2266
+ outro3(
2267
+ `${pc5.green("\u2714")} Project created at ${pc5.bold(opts.projectName)}
1738
2268
 
1739
2269
  Next steps:
1740
- ${pc3.cyan("cd")} ${opts.projectName}
1741
- ${pc3.cyan("npm install")}
1742
- ${pc3.cyan("npx ampx sandbox")} ${pc3.dim("# start Amplify backend")}
1743
- ${pc3.cyan("npm run dev")} ${pc3.dim("# start Next.js")}`
2270
+ ${pc5.cyan("cd")} ${opts.projectName}
2271
+ ${pc5.cyan("npm install")}
2272
+ ${pc5.cyan("npx ampx sandbox")} ${pc5.dim("# start Amplify backend")}
2273
+ ${pc5.cyan("npm run dev")} ${pc5.dim("# start Next.js")}`
1744
2274
  );
1745
2275
  }
1746
2276
  main().catch((err) => {
@@ -0,0 +1,41 @@
1
+ import type { AmplessBackend } from '@ampless/backend'
2
+
3
+ // Custom backend extensions for this project.
4
+ //
5
+ // `amplify/backend.ts` calls `customizeBackend(backend)` right after
6
+ // ampless's baseline wiring. Mutate the passed `backend` instance to
7
+ // add Lambda permissions, attach event sources, register custom CDK
8
+ // constructs, etc.
9
+ //
10
+ // This file is NEVER overwritten by `create-ampless upgrade` —
11
+ // `amplify/backend.ts` is, so keep your customizations here.
12
+ //
13
+ // (Function-export rather than side-effect import on purpose: a
14
+ // top-level `import { backend } from './backend.js'` would create an
15
+ // ESM circular dependency, leaving `backend` in its TDZ at evaluation
16
+ // time. The factory pattern sidesteps that.)
17
+ //
18
+ // Example: granting an existing ampless Lambda extra IAM permissions:
19
+ //
20
+ // import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'
21
+ //
22
+ // export function customizeBackend(backend: AmplessBackend): void {
23
+ // backend.processorTrusted.resources.lambda.addToRolePolicy(
24
+ // new PolicyStatement({
25
+ // effect: Effect.ALLOW,
26
+ // actions: ['ses:SendEmail'],
27
+ // resources: ['*'],
28
+ // })
29
+ // )
30
+ // }
31
+ //
32
+ // Example: adding your own Lambda function — define it in
33
+ // `amplify/functions/<name>/resource.ts`, import it here, and attach
34
+ // it inside `customizeBackend` via `backend.createStack(...)` or by
35
+ // granting it permissions on existing resources.
36
+ //
37
+ // If you don't need any extensions, leave the body empty.
38
+
39
+ export function customizeBackend(_backend: AmplessBackend): void {
40
+ // No-op by default.
41
+ }
@@ -8,13 +8,16 @@ import { eventDispatcher } from './events/dispatcher/resource.js'
8
8
  import { processorTrusted } from './events/processor-trusted/resource.js'
9
9
  import { processorUntrusted } from './events/processor-untrusted/resource.js'
10
10
  import { apiKeyRenewer } from './functions/api-key-renewer/resource.js'
11
+ import { userAdmin } from './functions/user-admin/resource.js'
12
+ import { customizeBackend } from './backend.custom.js'
11
13
 
12
14
  // `defineAmplessBackend` provisions auth, data, storage, the event
13
15
  // system (DynamoDB Streams → SQS-trusted / SQS-untrusted → trust_level
14
16
  // Lambdas), the AppSync API key renewer, and every IAM / CORS /
15
17
  // password policy override. Add custom CDK constructs / IAM policies
16
- // below by mutating the returned `backend` — `defineAmplessBackend`
17
- // returns the same object Amplify Gen 2's `defineBackend` does.
18
+ // in `amplify/backend.custom.ts` by mutating the `backend` instance
19
+ // `defineAmplessBackend` returns the same object Amplify Gen 2's
20
+ // `defineBackend` does.
18
21
  const backend = defineAmplessBackend({
19
22
  auth,
20
23
  data,
@@ -24,6 +27,11 @@ const backend = defineAmplessBackend({
24
27
  processorTrusted,
25
28
  processorUntrusted,
26
29
  apiKeyRenewer,
30
+ userAdmin,
27
31
  })
28
32
 
33
+ // Run user-defined customizations after baseline wiring. `backend.custom.ts`
34
+ // is never overwritten by `create-ampless upgrade`.
35
+ customizeBackend(backend)
36
+
29
37
  export default backend
@@ -0,0 +1,33 @@
1
+ // Custom data schema models for this project.
2
+ //
3
+ // `amplify/data/resource.ts` calls `customSchemaModels(a)` and spreads
4
+ // the result alongside ampless's built-in models in the same
5
+ // `a.schema({...})` call, which is the only way Amplify Gen 2's
6
+ // `ClientSchema<typeof schema>` inference picks up extra models.
7
+ //
8
+ // This file is NEVER overwritten by `create-ampless upgrade` —
9
+ // `amplify/data/resource.ts` is, so keep your custom models here.
10
+ //
11
+ // Example:
12
+ //
13
+ // export function customSchemaModels(a: any) {
14
+ // return {
15
+ // Bookmark: a
16
+ // .model({
17
+ // siteId: a.string().required(),
18
+ // bookmarkId: a.id().required(),
19
+ // url: a.string().required(),
20
+ // title: a.string(),
21
+ // })
22
+ // .identifier(['siteId', 'bookmarkId'])
23
+ // .authorization((allow: any) => [allow.groups(['ampless-admin', 'ampless-editor'])]),
24
+ // }
25
+ // }
26
+ //
27
+ // `a` is the Amplify Gen 2 schema builder; see Amplify Data docs for
28
+ // the full DSL. Return an empty object if no customizations are needed.
29
+
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ export function customSchemaModels(_a: any): Record<string, unknown> {
32
+ return {}
33
+ }
@@ -2,6 +2,8 @@ import { fileURLToPath } from 'node:url'
2
2
  import { dirname, resolve } from 'node:path'
3
3
  import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
4
4
  import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
5
+ import { userAdmin } from '../functions/user-admin/resource.js'
6
+ import { customSchemaModels } from './resource.custom.js'
5
7
 
6
8
  // AppSync's `a.handler.custom({ entry })` paths are resolved by CDK
7
9
  // relative to the file that called `a.handler.custom`. When the call
@@ -16,15 +18,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
16
18
  // KvStore) plus the three public-read custom queries
17
19
  // (listPublishedPosts / getPublishedPost / listPostsByTag).
18
20
  //
19
- // Add project-specific models alongside the spread:
20
- //
21
- // const schema = a.schema({
22
- // ...amplessSchemaModels(a, { resolverPaths }),
23
- // MyCustomModel: a
24
- // .model({ siteId: a.string().required(), foo: a.string() })
25
- // .identifier(['siteId', 'foo'])
26
- // .authorization((allow) => [allow.groups(['ampless-admin'])]),
27
- // })
21
+ // Add project-specific models in `amplify/data/resource.custom.ts`
22
+ // that file is never overwritten by `create-ampless upgrade`.
28
23
  const resolverPaths = {
29
24
  listPublishedPosts: resolve(__dirname, 'list-published-posts.js'),
30
25
  getPublishedPost: resolve(__dirname, 'get-published-post.js'),
@@ -32,7 +27,8 @@ const resolverPaths = {
32
27
  }
33
28
 
34
29
  const schema = a.schema({
35
- ...amplessSchemaModels(a, { resolverPaths }),
30
+ ...amplessSchemaModels(a, { resolverPaths, userAdminFunction: userAdmin }),
31
+ ...customSchemaModels(a),
36
32
  })
37
33
 
38
34
  export type Schema = ClientSchema<typeof schema>
@@ -0,0 +1,4 @@
1
+ // Re-exported from @ampless/backend so the package can ship Lambda
2
+ // handler updates via `npm update`. Amplify's esbuild follows this
3
+ // import and bundles the real handler into the Lambda artifact.
4
+ export { handler } from '@ampless/backend/auth/user-admin'
@@ -0,0 +1,6 @@
1
+ import { defineFunction } from '@aws-amplify/backend'
2
+
3
+ export const userAdmin = defineFunction({
4
+ name: 'user-admin',
5
+ entry: './handler.ts',
6
+ })
@@ -0,0 +1,5 @@
1
+ import { admin } from '@/lib/admin'
2
+ import { createUsersListPage } from '@ampless/admin/pages'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+ export default createUsersListPage(admin)
@@ -8,7 +8,10 @@
8
8
  "build": "next build",
9
9
  "start": "next start",
10
10
  "lint": "next lint",
11
- "sandbox": "ampx sandbox"
11
+ "sandbox": "ampx sandbox",
12
+ "sandbox:dev": "ampx sandbox --once && next dev",
13
+ "update-ampless": "npx create-ampless@alpha upgrade",
14
+ "copy-theme": "npx create-ampless@alpha copy-theme"
12
15
  },
13
16
  "dependencies": {
14
17
  "@aws-amplify/adapter-nextjs": "^1.6.0",
@@ -20,15 +23,15 @@
20
23
  "@tiptap/pm": "^3.23.4",
21
24
  "@tiptap/react": "^3.23.4",
22
25
  "@tiptap/starter-kit": "^3.23.4",
23
- "@ampless/plugin-og-image": "^0.2.0-alpha.1",
24
- "@ampless/plugin-rss": "^0.2.0-alpha.1",
25
- "@ampless/plugin-seo": "^0.2.0-alpha.1",
26
- "@ampless/plugin-webhook": "^0.2.0-alpha.1",
27
- "@ampless/admin": "^0.2.0-alpha.6",
28
- "@ampless/backend": "^0.2.0-alpha.2",
29
- "@ampless/runtime": "^0.2.0-alpha.3",
26
+ "@ampless/plugin-og-image": "^0.2.0-alpha.2",
27
+ "@ampless/plugin-rss": "^0.2.0-alpha.2",
28
+ "@ampless/plugin-seo": "^0.2.0-alpha.2",
29
+ "@ampless/plugin-webhook": "^0.2.0-alpha.2",
30
+ "@ampless/admin": "^0.2.0-alpha.7",
31
+ "@ampless/backend": "^0.2.0-alpha.3",
32
+ "@ampless/runtime": "^0.2.0-alpha.4",
30
33
  "@digital-go-jp/tailwind-theme-plugin": "^0.3.4",
31
- "ampless": "^0.2.0-alpha.1",
34
+ "ampless": "^0.2.0-alpha.2",
32
35
  "aws-amplify": "^6.10.0",
33
36
  "class-variance-authority": "^0.7.1",
34
37
  "clsx": "^2.1.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ampless",
3
- "version": "0.2.0-alpha.8",
3
+ "version": "0.2.0-alpha.9",
4
4
  "description": "Create a new ampless project",
5
5
  "license": "MIT",
6
6
  "type": "module",