@anthusai/papyrus 1.0.0-next.40 → 1.0.0-next.41
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/infra/amplify-app-shell.js +18 -2
- package/infra/amplify-app-shell.ts +19 -3
- package/infra/build-specs.js +2 -1
- package/infra/build-specs.ts +2 -1
- package/infra/site-config.js +69 -1
- package/infra/site-config.ts +80 -1
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -7,6 +7,18 @@ import { isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, r
|
|
|
7
7
|
function environmentVariables(variables) {
|
|
8
8
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
9
9
|
}
|
|
10
|
+
function readerCustomHeaders(rules) {
|
|
11
|
+
if (!rules)
|
|
12
|
+
return undefined;
|
|
13
|
+
const lines = ["customHeaders:"];
|
|
14
|
+
for (const rule of rules) {
|
|
15
|
+
lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
|
|
16
|
+
for (const [key, value] of Object.entries(rule.headers)) {
|
|
17
|
+
lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return lines.join("\n") + "\n";
|
|
21
|
+
}
|
|
10
22
|
/**
|
|
11
23
|
* Provisions the Amplify *app shell* of a Papyrus publication from its
|
|
12
24
|
* site.json:
|
|
@@ -282,7 +294,11 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
282
294
|
platform: "WEB",
|
|
283
295
|
iamServiceRole: readerServiceRole.roleArn,
|
|
284
296
|
buildSpec: readerBuildSpec(config),
|
|
285
|
-
customRules: [
|
|
297
|
+
customRules: [
|
|
298
|
+
...(reader.redirects ?? []),
|
|
299
|
+
{ source: "/<*>", target: "/404.html", status: "404-200" },
|
|
300
|
+
],
|
|
301
|
+
customHeaders: readerCustomHeaders(reader.customHeaders),
|
|
286
302
|
});
|
|
287
303
|
const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
|
|
288
304
|
appId: readerApp.attrAppId,
|
|
@@ -299,7 +315,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
299
315
|
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
300
316
|
appId: readerApp.attrAppId,
|
|
301
317
|
domainName: reader.domainName,
|
|
302
|
-
subDomainSettings: [{ branchName: readerBranchName, prefix
|
|
318
|
+
subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
|
|
303
319
|
});
|
|
304
320
|
readerDomain.addResourceDependency(readerBranch);
|
|
305
321
|
}
|
|
@@ -4,12 +4,24 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|
|
4
4
|
import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
5
5
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
6
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
7
|
-
import { AmplifyAppShellSiteConfig, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
7
|
+
import { AmplifyAppShellSiteConfig, ReaderCustomHeaderRule, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
8
8
|
|
|
9
9
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
10
10
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function readerCustomHeaders(rules: ReaderCustomHeaderRule[] | undefined): string | undefined {
|
|
14
|
+
if (!rules) return undefined;
|
|
15
|
+
const lines = ["customHeaders:"];
|
|
16
|
+
for (const rule of rules) {
|
|
17
|
+
lines.push(` - pattern: ${JSON.stringify(rule.pattern)}`, " headers:");
|
|
18
|
+
for (const [key, value] of Object.entries(rule.headers)) {
|
|
19
|
+
lines.push(` - key: ${JSON.stringify(key)}`, ` value: ${JSON.stringify(value)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return lines.join("\n") + "\n";
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* Provisions the Amplify *app shell* of a Papyrus publication from its
|
|
15
27
|
* site.json:
|
|
@@ -333,7 +345,11 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
333
345
|
platform: "WEB",
|
|
334
346
|
iamServiceRole: readerServiceRole.roleArn,
|
|
335
347
|
buildSpec: readerBuildSpec(config),
|
|
336
|
-
customRules: [
|
|
348
|
+
customRules: [
|
|
349
|
+
...(reader.redirects ?? []),
|
|
350
|
+
{ source: "/<*>", target: "/404.html", status: "404-200" },
|
|
351
|
+
],
|
|
352
|
+
customHeaders: readerCustomHeaders(reader.customHeaders),
|
|
337
353
|
});
|
|
338
354
|
|
|
339
355
|
const readerBranch = new amplify.CfnBranch(this, "ReaderBranch", {
|
|
@@ -352,7 +368,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
352
368
|
const readerDomain = new amplify.CfnDomain(this, "ReaderDomain", {
|
|
353
369
|
appId: readerApp.attrAppId,
|
|
354
370
|
domainName: reader.domainName,
|
|
355
|
-
subDomainSettings: [{ branchName: readerBranchName, prefix
|
|
371
|
+
subDomainSettings: (reader.domainPrefixes ?? [""]).map((prefix) => ({ branchName: readerBranchName, prefix })),
|
|
356
372
|
});
|
|
357
373
|
readerDomain.addResourceDependency(readerBranch);
|
|
358
374
|
}
|
package/infra/build-specs.js
CHANGED
|
@@ -82,7 +82,8 @@ export function readerBuildSpec(config) {
|
|
|
82
82
|
"version: 1",
|
|
83
83
|
...frontendSection([
|
|
84
84
|
...pythonProvisioningCommands(config),
|
|
85
|
-
|
|
85
|
+
...(readerConfig.preBuildCommands ?? []),
|
|
86
|
+
...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
|
|
86
87
|
], [readerConfig.buildCommand], readerConfig.baseDirectory, [".uv-cache/**/*"]),
|
|
87
88
|
].join("\n") + "\n";
|
|
88
89
|
}
|
package/infra/build-specs.ts
CHANGED
|
@@ -126,7 +126,8 @@ export function readerBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
|
126
126
|
...frontendSection(
|
|
127
127
|
[
|
|
128
128
|
...pythonProvisioningCommands(config),
|
|
129
|
-
|
|
129
|
+
...(readerConfig.preBuildCommands ?? []),
|
|
130
|
+
...(readerConfig.skipContentExport ? [] : ["papyrus ops content export-published --auth guest --out content-export --clean"]),
|
|
130
131
|
],
|
|
131
132
|
[readerConfig.buildCommand],
|
|
132
133
|
readerConfig.baseDirectory,
|
package/infra/site-config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const READER_REDIRECT_STATUSES = ["301", "302", "200", "404", "404-200"];
|
|
1
2
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
2
3
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
3
4
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -78,6 +79,63 @@ function parseRedirects(value) {
|
|
|
78
79
|
return { source, status };
|
|
79
80
|
});
|
|
80
81
|
}
|
|
82
|
+
function parseReaderDomainPrefixes(value) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
86
|
+
fail("reader.domainPrefixes", "must be a non-empty array");
|
|
87
|
+
const prefixes = value.map((entry, index) => {
|
|
88
|
+
const field = `reader.domainPrefixes[${index}]`;
|
|
89
|
+
if (entry === "")
|
|
90
|
+
return "";
|
|
91
|
+
return requireString(entry, field, DOMAIN_PREFIX_PATTERN, 'single DNS label, or "" for the apex');
|
|
92
|
+
});
|
|
93
|
+
if (new Set(prefixes).size !== prefixes.length)
|
|
94
|
+
fail("reader.domainPrefixes", "must not repeat a prefix");
|
|
95
|
+
return prefixes;
|
|
96
|
+
}
|
|
97
|
+
function parseReaderRedirects(value) {
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
101
|
+
fail("reader.redirects", "must be a non-empty array");
|
|
102
|
+
return value.map((entry, index) => {
|
|
103
|
+
const field = `reader.redirects[${index}]`;
|
|
104
|
+
const record = requireObject(entry, field);
|
|
105
|
+
rejectUnknownKeys(record, ["source", "target", "status"], field);
|
|
106
|
+
const status = record.status ?? "301";
|
|
107
|
+
if (typeof status !== "string" || !READER_REDIRECT_STATUSES.includes(status)) {
|
|
108
|
+
fail(`${field}.status`, `must be one of ${READER_REDIRECT_STATUSES.join(", ")}, got ${JSON.stringify(status)}`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
source: requireString(record.source, `${field}.source`),
|
|
112
|
+
target: requireString(record.target, `${field}.target`),
|
|
113
|
+
status: status,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function parseReaderCustomHeaders(value) {
|
|
118
|
+
if (value === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
121
|
+
fail("reader.customHeaders", "must be a non-empty array");
|
|
122
|
+
return value.map((entry, index) => {
|
|
123
|
+
const field = `reader.customHeaders[${index}]`;
|
|
124
|
+
const record = requireObject(entry, field);
|
|
125
|
+
rejectUnknownKeys(record, ["pattern", "headers"], field);
|
|
126
|
+
const headers = requireStringRecord(record.headers, `${field}.headers`);
|
|
127
|
+
if (Object.keys(headers).length === 0)
|
|
128
|
+
fail(`${field}.headers`, "must not be empty");
|
|
129
|
+
return { pattern: requireString(record.pattern, `${field}.pattern`), headers };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function parseReaderPreBuildCommands(value) {
|
|
133
|
+
if (value === undefined)
|
|
134
|
+
return undefined;
|
|
135
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
136
|
+
fail("reader.preBuildCommands", "must be a non-empty array of commands");
|
|
137
|
+
return value.map((command, index) => requireString(command, `reader.preBuildCommands[${index}]`));
|
|
138
|
+
}
|
|
81
139
|
export function resolveRedirectSubDomainPrefix(rootDomain, source) {
|
|
82
140
|
return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
|
|
83
141
|
}
|
|
@@ -203,7 +261,10 @@ export function parseSiteConfig(raw) {
|
|
|
203
261
|
if (frontend !== "markus-static")
|
|
204
262
|
fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
205
263
|
const readerRecord = requireObject(record.reader, "reader");
|
|
206
|
-
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
264
|
+
rejectUnknownKeys(readerRecord, ["appName", "domainName", "domainPrefixes", "branchName", "buildCommand", "baseDirectory", "environment", "redirects", "customHeaders", "preBuildCommands", "skipContentExport"], "reader");
|
|
265
|
+
if (readerRecord.skipContentExport !== undefined && typeof readerRecord.skipContentExport !== "boolean") {
|
|
266
|
+
fail("reader.skipContentExport", `must be a boolean, got ${JSON.stringify(readerRecord.skipContentExport)}`);
|
|
267
|
+
}
|
|
207
268
|
reader = {
|
|
208
269
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
209
270
|
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
@@ -211,7 +272,14 @@ export function parseSiteConfig(raw) {
|
|
|
211
272
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
212
273
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
213
274
|
environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
|
|
275
|
+
domainPrefixes: parseReaderDomainPrefixes(readerRecord.domainPrefixes),
|
|
276
|
+
redirects: parseReaderRedirects(readerRecord.redirects),
|
|
277
|
+
customHeaders: parseReaderCustomHeaders(readerRecord.customHeaders),
|
|
278
|
+
preBuildCommands: parseReaderPreBuildCommands(readerRecord.preBuildCommands),
|
|
279
|
+
skipContentExport: readerRecord.skipContentExport,
|
|
214
280
|
};
|
|
281
|
+
if (reader.domainPrefixes && !reader.domainName)
|
|
282
|
+
fail("reader.domainPrefixes", "requires reader.domainName");
|
|
215
283
|
}
|
|
216
284
|
else if (frontend === "markus-static") {
|
|
217
285
|
fail("reader", `is required when frontend is "markus-static"`);
|
package/infra/site-config.ts
CHANGED
|
@@ -3,6 +3,21 @@ export type CmsRedirect = {
|
|
|
3
3
|
status: 301 | 302;
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
+
export type ReaderRedirectStatus = "301" | "302" | "200" | "404" | "404-200";
|
|
7
|
+
|
|
8
|
+
export type ReaderRedirect = {
|
|
9
|
+
source: string;
|
|
10
|
+
target: string;
|
|
11
|
+
status: ReaderRedirectStatus;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ReaderCustomHeaderRule = {
|
|
15
|
+
pattern: string;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const READER_REDIRECT_STATUSES: ReaderRedirectStatus[] = ["301", "302", "200", "404", "404-200"];
|
|
20
|
+
|
|
6
21
|
export type AmplifyAppShellSiteConfig = {
|
|
7
22
|
siteId: string;
|
|
8
23
|
repository: string;
|
|
@@ -26,10 +41,15 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
26
41
|
reader?: {
|
|
27
42
|
appName?: string;
|
|
28
43
|
domainName?: string;
|
|
44
|
+
domainPrefixes?: string[];
|
|
29
45
|
branchName?: string;
|
|
30
46
|
buildCommand: string;
|
|
31
47
|
baseDirectory: string;
|
|
32
48
|
environment?: Record<string, string>;
|
|
49
|
+
redirects?: ReaderRedirect[];
|
|
50
|
+
customHeaders?: ReaderCustomHeaderRule[];
|
|
51
|
+
preBuildCommands?: string[];
|
|
52
|
+
skipContentExport?: boolean;
|
|
33
53
|
};
|
|
34
54
|
papyrusVersion: string;
|
|
35
55
|
storagePreviewPrefix?: string;
|
|
@@ -126,6 +146,56 @@ function parseRedirects(value: unknown): CmsRedirect[] | undefined {
|
|
|
126
146
|
});
|
|
127
147
|
}
|
|
128
148
|
|
|
149
|
+
function parseReaderDomainPrefixes(value: unknown): string[] | undefined {
|
|
150
|
+
if (value === undefined) return undefined;
|
|
151
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.domainPrefixes", "must be a non-empty array");
|
|
152
|
+
const prefixes = (value as unknown[]).map((entry, index) => {
|
|
153
|
+
const field = `reader.domainPrefixes[${index}]`;
|
|
154
|
+
if (entry === "") return "";
|
|
155
|
+
return requireString(entry, field, DOMAIN_PREFIX_PATTERN, 'single DNS label, or "" for the apex');
|
|
156
|
+
});
|
|
157
|
+
if (new Set(prefixes).size !== prefixes.length) fail("reader.domainPrefixes", "must not repeat a prefix");
|
|
158
|
+
return prefixes;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseReaderRedirects(value: unknown): ReaderRedirect[] | undefined {
|
|
162
|
+
if (value === undefined) return undefined;
|
|
163
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.redirects", "must be a non-empty array");
|
|
164
|
+
return (value as unknown[]).map((entry, index) => {
|
|
165
|
+
const field = `reader.redirects[${index}]`;
|
|
166
|
+
const record = requireObject(entry, field);
|
|
167
|
+
rejectUnknownKeys(record, ["source", "target", "status"], field);
|
|
168
|
+
const status = record.status ?? "301";
|
|
169
|
+
if (typeof status !== "string" || !READER_REDIRECT_STATUSES.includes(status as ReaderRedirectStatus)) {
|
|
170
|
+
fail(`${field}.status`, `must be one of ${READER_REDIRECT_STATUSES.join(", ")}, got ${JSON.stringify(status)}`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
source: requireString(record.source, `${field}.source`),
|
|
174
|
+
target: requireString(record.target, `${field}.target`),
|
|
175
|
+
status: status as ReaderRedirectStatus,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseReaderCustomHeaders(value: unknown): ReaderCustomHeaderRule[] | undefined {
|
|
181
|
+
if (value === undefined) return undefined;
|
|
182
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.customHeaders", "must be a non-empty array");
|
|
183
|
+
return (value as unknown[]).map((entry, index) => {
|
|
184
|
+
const field = `reader.customHeaders[${index}]`;
|
|
185
|
+
const record = requireObject(entry, field);
|
|
186
|
+
rejectUnknownKeys(record, ["pattern", "headers"], field);
|
|
187
|
+
const headers = requireStringRecord(record.headers, `${field}.headers`);
|
|
188
|
+
if (Object.keys(headers).length === 0) fail(`${field}.headers`, "must not be empty");
|
|
189
|
+
return { pattern: requireString(record.pattern, `${field}.pattern`), headers };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseReaderPreBuildCommands(value: unknown): string[] | undefined {
|
|
194
|
+
if (value === undefined) return undefined;
|
|
195
|
+
if (!Array.isArray(value) || value.length === 0) fail("reader.preBuildCommands", "must be a non-empty array of commands");
|
|
196
|
+
return (value as unknown[]).map((command, index) => requireString(command, `reader.preBuildCommands[${index}]`));
|
|
197
|
+
}
|
|
198
|
+
|
|
129
199
|
export function resolveRedirectSubDomainPrefix(rootDomain: string, source: string): string {
|
|
130
200
|
return source === rootDomain ? "" : source.slice(0, -rootDomain.length - 1);
|
|
131
201
|
}
|
|
@@ -264,7 +334,10 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
264
334
|
if (record.reader !== undefined) {
|
|
265
335
|
if (frontend !== "markus-static") fail("reader", `is only allowed when frontend is "markus-static"`);
|
|
266
336
|
const readerRecord = requireObject(record.reader, "reader");
|
|
267
|
-
rejectUnknownKeys(readerRecord, ["appName", "domainName", "branchName", "buildCommand", "baseDirectory", "environment"], "reader");
|
|
337
|
+
rejectUnknownKeys(readerRecord, ["appName", "domainName", "domainPrefixes", "branchName", "buildCommand", "baseDirectory", "environment", "redirects", "customHeaders", "preBuildCommands", "skipContentExport"], "reader");
|
|
338
|
+
if (readerRecord.skipContentExport !== undefined && typeof readerRecord.skipContentExport !== "boolean") {
|
|
339
|
+
fail("reader.skipContentExport", `must be a boolean, got ${JSON.stringify(readerRecord.skipContentExport)}`);
|
|
340
|
+
}
|
|
268
341
|
reader = {
|
|
269
342
|
appName: optionalString(readerRecord.appName, "reader.appName", APP_NAME_PATTERN),
|
|
270
343
|
domainName: optionalString(readerRecord.domainName, "reader.domainName", HOST_NAME_PATTERN, "host name"),
|
|
@@ -272,7 +345,13 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
272
345
|
buildCommand: requireString(readerRecord.buildCommand, "reader.buildCommand"),
|
|
273
346
|
baseDirectory: requireString(readerRecord.baseDirectory, "reader.baseDirectory"),
|
|
274
347
|
environment: readerRecord.environment === undefined ? undefined : requireStringRecord(readerRecord.environment, "reader.environment"),
|
|
348
|
+
domainPrefixes: parseReaderDomainPrefixes(readerRecord.domainPrefixes),
|
|
349
|
+
redirects: parseReaderRedirects(readerRecord.redirects),
|
|
350
|
+
customHeaders: parseReaderCustomHeaders(readerRecord.customHeaders),
|
|
351
|
+
preBuildCommands: parseReaderPreBuildCommands(readerRecord.preBuildCommands),
|
|
352
|
+
skipContentExport: readerRecord.skipContentExport as boolean | undefined,
|
|
275
353
|
};
|
|
354
|
+
if (reader.domainPrefixes && !reader.domainName) fail("reader.domainPrefixes", "requires reader.domainName");
|
|
276
355
|
} else if (frontend === "markus-static") {
|
|
277
356
|
fail("reader", `is required when frontend is "markus-static"`);
|
|
278
357
|
}
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED