create-ampless 0.2.0-alpha.7 → 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 +625 -88
- package/dist/templates/_shared/amplify/backend.custom.ts +41 -0
- package/dist/templates/_shared/amplify/backend.ts +10 -2
- package/dist/templates/_shared/amplify/data/resource.custom.ts +33 -0
- package/dist/templates/_shared/amplify/data/resource.ts +6 -10
- package/dist/templates/_shared/amplify/functions/user-admin/handler.ts +4 -0
- package/dist/templates/_shared/amplify/functions/user-admin/resource.ts +6 -0
- package/dist/templates/_shared/app/(admin)/admin/users/page.tsx +5 -0
- package/dist/templates/_shared/package.json +12 -9
- package/package.json +1 -1
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
|
|
5
|
-
import { existsSync as
|
|
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
|
|
231
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
135
232
|
await Promise.all(
|
|
136
233
|
entries.map(async (entry) => {
|
|
137
|
-
const fullPath =
|
|
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
|
|
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
|
|
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 (!
|
|
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) =>
|
|
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 (
|
|
1030
|
-
await writeFile2(target,
|
|
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);
|
|
@@ -1053,11 +1191,18 @@ function shortName2(opts) {
|
|
|
1053
1191
|
return basename2(opts.projectDir);
|
|
1054
1192
|
}
|
|
1055
1193
|
async function ghRepoExists(name, token) {
|
|
1056
|
-
const r = await execa2("gh", ["repo", "view", name], {
|
|
1194
|
+
const r = await execa2("gh", ["repo", "view", name, "--json", "nameWithOwner"], {
|
|
1057
1195
|
reject: false,
|
|
1058
1196
|
env: { ...process.env, GH_TOKEN: token }
|
|
1059
1197
|
});
|
|
1060
|
-
|
|
1198
|
+
if (r.exitCode !== 0) return false;
|
|
1199
|
+
try {
|
|
1200
|
+
const parsed = JSON.parse(r.stdout?.toString() ?? "{}");
|
|
1201
|
+
const resolved = parsed.nameWithOwner?.toLowerCase();
|
|
1202
|
+
return resolved === name.toLowerCase();
|
|
1203
|
+
} catch {
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1061
1206
|
}
|
|
1062
1207
|
async function getOriginUrl(dir) {
|
|
1063
1208
|
const r = await execa2("git", ["remote", "get-url", "origin"], {
|
|
@@ -1354,6 +1499,17 @@ async function runDeploy(opts) {
|
|
|
1354
1499
|
}
|
|
1355
1500
|
}
|
|
1356
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
|
+
}
|
|
1357
1513
|
const created = {};
|
|
1358
1514
|
const fail = (step, cause) => {
|
|
1359
1515
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -1490,7 +1646,7 @@ async function gatherDeployOptions(args, projectDir, projectName) {
|
|
|
1490
1646
|
githubOwner = answer.trim();
|
|
1491
1647
|
}
|
|
1492
1648
|
let githubPrivate = args.githubPrivate;
|
|
1493
|
-
if (!args.githubPrivate) {
|
|
1649
|
+
if (!args.githubPrivate && !args.skipConfirm) {
|
|
1494
1650
|
const choice = await p2.select({
|
|
1495
1651
|
message: "Repository visibility",
|
|
1496
1652
|
options: [
|
|
@@ -1598,15 +1754,388 @@ async function gatherDeployOptions(args, projectDir, projectName) {
|
|
|
1598
1754
|
};
|
|
1599
1755
|
}
|
|
1600
1756
|
|
|
1601
|
-
// src/
|
|
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";
|
|
1602
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;
|
|
1603
2132
|
function buildNonInteractiveOpts(args) {
|
|
1604
2133
|
const projectName = args.projectName ?? (() => {
|
|
1605
2134
|
const b = basename3(process.cwd());
|
|
1606
2135
|
return b && b !== "/" ? b : "my-ampless-site";
|
|
1607
2136
|
})();
|
|
1608
2137
|
const siteName = args.siteName ?? "My Blog";
|
|
1609
|
-
const themes = args.themes ?? [
|
|
2138
|
+
const themes = args.themes ?? [...DEFAULT_THEMES];
|
|
1610
2139
|
const plugins = args.plugins ?? ["seo"];
|
|
1611
2140
|
return {
|
|
1612
2141
|
projectName,
|
|
@@ -1623,7 +2152,7 @@ function warnIgnoredScaffoldFlags(args) {
|
|
|
1623
2152
|
if (args.plugins) ignored.push("--plugins");
|
|
1624
2153
|
if (args.projectName) ignored.push("<project-name> positional");
|
|
1625
2154
|
if (ignored.length > 0) {
|
|
1626
|
-
|
|
2155
|
+
log5.warn(`--mount mode ignores: ${ignored.join(", ")}`);
|
|
1627
2156
|
}
|
|
1628
2157
|
}
|
|
1629
2158
|
async function runMount(args) {
|
|
@@ -1632,8 +2161,8 @@ async function runMount(args) {
|
|
|
1632
2161
|
warnIgnoredScaffoldFlags(args);
|
|
1633
2162
|
const problem = validateMountableProject(destDir);
|
|
1634
2163
|
if (problem) {
|
|
1635
|
-
|
|
1636
|
-
|
|
2164
|
+
log5.error(problem);
|
|
2165
|
+
log5.info(
|
|
1637
2166
|
"Run `npx create-ampless@alpha <name>` first to scaffold, or `cd` into a scaffolded project before passing --mount."
|
|
1638
2167
|
);
|
|
1639
2168
|
process.exit(1);
|
|
@@ -1647,23 +2176,23 @@ async function runMount(args) {
|
|
|
1647
2176
|
if (err instanceof PreflightError) {
|
|
1648
2177
|
process.exit(1);
|
|
1649
2178
|
}
|
|
1650
|
-
|
|
2179
|
+
log5.error(err instanceof Error ? err.message : String(err));
|
|
1651
2180
|
process.exit(1);
|
|
1652
2181
|
}
|
|
1653
2182
|
}
|
|
1654
2183
|
function printDeployResult(result) {
|
|
1655
2184
|
const lines = [
|
|
1656
|
-
`${
|
|
2185
|
+
`${pc5.green("\u2714")} Project deployed`,
|
|
1657
2186
|
``,
|
|
1658
|
-
` GitHub: ${
|
|
1659
|
-
` Amplify app: ${
|
|
1660
|
-
` Amplify URL: ${
|
|
2187
|
+
` GitHub: ${pc5.cyan(result.githubRepoUrl)}`,
|
|
2188
|
+
` Amplify app: ${pc5.cyan(result.amplifyAppId)}`,
|
|
2189
|
+
` Amplify URL: ${pc5.cyan(result.amplifyAppUrl)}`
|
|
1661
2190
|
];
|
|
1662
2191
|
if (result.domainUrl) {
|
|
1663
|
-
lines.push(` Custom domain: ${
|
|
2192
|
+
lines.push(` Custom domain: ${pc5.cyan(result.domainUrl)}`);
|
|
1664
2193
|
}
|
|
1665
2194
|
if (result.domainVerification && result.domainVerification.length > 0) {
|
|
1666
|
-
lines.push("", ` ${
|
|
2195
|
+
lines.push("", ` ${pc5.bold("Add these DNS records to verify the domain:")}`);
|
|
1667
2196
|
for (const v of result.domainVerification) {
|
|
1668
2197
|
lines.push(` ${v.cname} CNAME ${v.target}`);
|
|
1669
2198
|
}
|
|
@@ -1671,9 +2200,9 @@ function printDeployResult(result) {
|
|
|
1671
2200
|
lines.push(
|
|
1672
2201
|
"",
|
|
1673
2202
|
` First build is now running in Amplify Hosting.`,
|
|
1674
|
-
` Watch it at ${
|
|
2203
|
+
` Watch it at ${pc5.cyan(`https://console.aws.amazon.com/amplify/home#/${result.amplifyAppId}`)}`
|
|
1675
2204
|
);
|
|
1676
|
-
|
|
2205
|
+
outro3(lines.join("\n"));
|
|
1677
2206
|
}
|
|
1678
2207
|
async function main() {
|
|
1679
2208
|
const args = parseDeployArgs(process.argv.slice(2));
|
|
@@ -1682,7 +2211,15 @@ async function main() {
|
|
|
1682
2211
|
return;
|
|
1683
2212
|
}
|
|
1684
2213
|
for (const flag of args.unknown) {
|
|
1685
|
-
|
|
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;
|
|
1686
2223
|
}
|
|
1687
2224
|
if (args.mount) {
|
|
1688
2225
|
await runMount(args);
|
|
@@ -1696,8 +2233,8 @@ async function main() {
|
|
|
1696
2233
|
}
|
|
1697
2234
|
if (!opts) return;
|
|
1698
2235
|
const destDir = resolve5(process.cwd(), opts.projectName);
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
2236
|
+
if (existsSync7(destDir)) {
|
|
2237
|
+
log5.error(`Directory already exists: ${destDir}`);
|
|
1701
2238
|
process.exit(1);
|
|
1702
2239
|
}
|
|
1703
2240
|
const sharedDir = sharedTemplateDir();
|
|
@@ -1708,7 +2245,7 @@ async function main() {
|
|
|
1708
2245
|
s.stop("Done!");
|
|
1709
2246
|
} catch (err) {
|
|
1710
2247
|
s.stop("Failed.");
|
|
1711
|
-
|
|
2248
|
+
log5.error(String(err));
|
|
1712
2249
|
process.exit(1);
|
|
1713
2250
|
}
|
|
1714
2251
|
if (args.deploy) {
|
|
@@ -1721,19 +2258,19 @@ async function main() {
|
|
|
1721
2258
|
if (err instanceof PreflightError) {
|
|
1722
2259
|
process.exit(1);
|
|
1723
2260
|
}
|
|
1724
|
-
|
|
2261
|
+
log5.error(err instanceof Error ? err.message : String(err));
|
|
1725
2262
|
process.exit(1);
|
|
1726
2263
|
}
|
|
1727
2264
|
return;
|
|
1728
2265
|
}
|
|
1729
|
-
|
|
1730
|
-
`${
|
|
2266
|
+
outro3(
|
|
2267
|
+
`${pc5.green("\u2714")} Project created at ${pc5.bold(opts.projectName)}
|
|
1731
2268
|
|
|
1732
2269
|
Next steps:
|
|
1733
|
-
${
|
|
1734
|
-
${
|
|
1735
|
-
${
|
|
1736
|
-
${
|
|
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")}`
|
|
1737
2274
|
);
|
|
1738
2275
|
}
|
|
1739
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
|
-
//
|
|
17
|
-
// returns the same object Amplify Gen 2's
|
|
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
|
|
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>
|
|
@@ -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.
|
|
24
|
-
"@ampless/plugin-rss": "^0.2.0-alpha.
|
|
25
|
-
"@ampless/plugin-seo": "^0.2.0-alpha.
|
|
26
|
-
"@ampless/plugin-webhook": "^0.2.0-alpha.
|
|
27
|
-
"@ampless/admin": "^0.2.0-alpha.
|
|
28
|
-
"@ampless/backend": "^0.2.0-alpha.
|
|
29
|
-
"@ampless/runtime": "^0.2.0-alpha.
|
|
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.
|
|
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",
|