@anthusai/papyrus 1.0.0-next.48 → 1.0.0-next.49
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/amplify/functions/content-actions/handler.py +5 -1
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/reproject-on-deploy.js +31 -0
- package/amplify/publication/reproject-on-deploy.ts +36 -0
- package/amplify/site-backend-config.js +3 -0
- package/amplify/site-backend-config.ts +15 -2
- package/amplify/site-backend.js +13 -6
- package/amplify/site-backend.ts +12 -5
- package/infra/build-specs.js +6 -3
- package/infra/build-specs.ts +8 -4
- package/infra/site-config.js +20 -3
- package/infra/site-config.ts +31 -3
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
from papyrus_content.edge_invalidation import distribution_ids_from_environment, invalidate_revalidated_paths
|
|
8
8
|
from papyrus_content.graphql_authoring import create_authoring_client
|
|
9
9
|
from papyrus_content.markus_renderer.derive import derive_body
|
|
10
|
-
from papyrus_content.projection.engine import project, project_item_pages
|
|
10
|
+
from papyrus_content.projection.engine import project, project_all, project_item_pages
|
|
11
11
|
from papyrus_content.projection.runtime import (
|
|
12
12
|
DEAD_LETTER_QUEUE_ENV,
|
|
13
13
|
context_from_environment,
|
|
@@ -155,6 +155,10 @@ def run_projection_task(task: dict) -> dict:
|
|
|
155
155
|
report = project(projection_context(client), task["itemId"], unpublished=bool(task.get("unpublished")))
|
|
156
156
|
if report.follow_up_item_pages:
|
|
157
157
|
schedule_projection_task({"task": "item-pages", "exceptItemId": task["itemId"]}, "follow-up")
|
|
158
|
+
elif task.get("task") == "all":
|
|
159
|
+
report = project_all(projection_context(client))
|
|
160
|
+
elif task.get("task") == "all":
|
|
161
|
+
report = project_all(projection_context(client))
|
|
158
162
|
elif task.get("task") == "invalidate":
|
|
159
163
|
invalidate = projection_context(client).invalidate
|
|
160
164
|
if invalidate is None:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const DEFAULT_MARKUS_PUBLICATION = { kind: "s3-projection", reprojectOnDeploy: true };
|
|
2
|
+
/**
|
|
3
|
+
* The publication a site backend is built with. A Markus site that names a `reader` Amplify app
|
|
4
|
+
* keeps publishing through it (`amplify-static`); every other Markus site gets the s3-projection
|
|
5
|
+
* default unless it writes `publication: false`. Sites with another renderer get nothing unless
|
|
6
|
+
* they write a `publication`.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveSitePublication(backend, rendererKind) {
|
|
9
|
+
if (backend.publication === false)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (backend.publication !== undefined)
|
|
12
|
+
return backend.publication;
|
|
13
|
+
if (rendererKind !== "markus")
|
|
14
|
+
return undefined;
|
|
15
|
+
if (backend.reader !== undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
return DEFAULT_MARKUS_PUBLICATION;
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PapyrusPublicationConfig, PapyrusSiteBackendConfig } from "../site-backend-config";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MARKUS_PUBLICATION: PapyrusPublicationConfig = { kind: "s3-projection", reprojectOnDeploy: true };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The publication a site backend is built with. A Markus site that names a `reader` Amplify app
|
|
7
|
+
* keeps publishing through it (`amplify-static`); every other Markus site gets the s3-projection
|
|
8
|
+
* default unless it writes `publication: false`. Sites with another renderer get nothing unless
|
|
9
|
+
* they write a `publication`.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveSitePublication(
|
|
12
|
+
backend: Pick<PapyrusSiteBackendConfig, "publication" | "reader">,
|
|
13
|
+
rendererKind: string | undefined,
|
|
14
|
+
): PapyrusPublicationConfig | undefined {
|
|
15
|
+
if (backend.publication === false) return undefined;
|
|
16
|
+
if (backend.publication !== undefined) return backend.publication;
|
|
17
|
+
if (rendererKind !== "markus") return undefined;
|
|
18
|
+
if (backend.reader !== undefined) return undefined;
|
|
19
|
+
return DEFAULT_MARKUS_PUBLICATION;
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
2
|
+
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
|
|
3
|
+
export const REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS = 300;
|
|
4
|
+
export function reprojectDeployToken(environment, now = Date.now) {
|
|
5
|
+
const buildJobId = (environment.AWS_JOB_ID ?? "").trim();
|
|
6
|
+
return buildJobId !== "" ? `job-${buildJobId}` : `time-${now()}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Starts the content-actions `all` projection task (asynchronously) every time the stack deploys.
|
|
10
|
+
* The token is in the request, so CloudFormation sees a changed property on each deploy.
|
|
11
|
+
*/
|
|
12
|
+
export function addReprojectOnDeploy(stack, contentActions, deployToken) {
|
|
13
|
+
const invocation = {
|
|
14
|
+
service: "Lambda",
|
|
15
|
+
action: "invoke",
|
|
16
|
+
parameters: {
|
|
17
|
+
FunctionName: contentActions.functionName,
|
|
18
|
+
InvocationType: "Event",
|
|
19
|
+
Payload: JSON.stringify({ projectionTask: { task: "all", deployToken } }),
|
|
20
|
+
},
|
|
21
|
+
physicalResourceId: PhysicalResourceId.of(`reproject-${deployToken}`),
|
|
22
|
+
};
|
|
23
|
+
return new AwsCustomResource(stack, "ReprojectOnDeploy", {
|
|
24
|
+
onCreate: invocation,
|
|
25
|
+
onUpdate: invocation,
|
|
26
|
+
installLatestAwsSdk: false,
|
|
27
|
+
policy: AwsCustomResourcePolicy.fromStatements([
|
|
28
|
+
new PolicyStatement({ actions: ["lambda:InvokeFunction"], resources: [contentActions.functionArn] }),
|
|
29
|
+
]),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import type { IFunction } from "aws-cdk-lib/aws-lambda";
|
|
4
|
+
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
|
|
5
|
+
|
|
6
|
+
export const REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS = 300;
|
|
7
|
+
|
|
8
|
+
export function reprojectDeployToken(environment: Record<string, string | undefined>, now: () => number = Date.now): string {
|
|
9
|
+
const buildJobId = (environment.AWS_JOB_ID ?? "").trim();
|
|
10
|
+
return buildJobId !== "" ? `job-${buildJobId}` : `time-${now()}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Starts the content-actions `all` projection task (asynchronously) every time the stack deploys.
|
|
15
|
+
* The token is in the request, so CloudFormation sees a changed property on each deploy.
|
|
16
|
+
*/
|
|
17
|
+
export function addReprojectOnDeploy(stack: Stack, contentActions: IFunction, deployToken: string): AwsCustomResource {
|
|
18
|
+
const invocation = {
|
|
19
|
+
service: "Lambda",
|
|
20
|
+
action: "invoke",
|
|
21
|
+
parameters: {
|
|
22
|
+
FunctionName: contentActions.functionName,
|
|
23
|
+
InvocationType: "Event",
|
|
24
|
+
Payload: JSON.stringify({ projectionTask: { task: "all", deployToken } }),
|
|
25
|
+
},
|
|
26
|
+
physicalResourceId: PhysicalResourceId.of(`reproject-${deployToken}`),
|
|
27
|
+
};
|
|
28
|
+
return new AwsCustomResource(stack, "ReprojectOnDeploy", {
|
|
29
|
+
onCreate: invocation,
|
|
30
|
+
onUpdate: invocation,
|
|
31
|
+
installLatestAwsSdk: false,
|
|
32
|
+
policy: AwsCustomResourcePolicy.fromStatements([
|
|
33
|
+
new PolicyStatement({ actions: ["lambda:InvokeFunction"], resources: [contentActions.functionArn] }),
|
|
34
|
+
]),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -27,6 +27,9 @@ export function validatePublicationConfig(publication) {
|
|
|
27
27
|
if (config.edgeDistributionArn !== undefined && !CLOUDFRONT_DISTRIBUTION_ARN.test(config.edgeDistributionArn)) {
|
|
28
28
|
problems.push("publication.edgeDistributionArn must be a CloudFront distribution ARN (arn:aws:cloudfront::<account>:distribution/<id>)");
|
|
29
29
|
}
|
|
30
|
+
if (config.reprojectOnDeploy !== undefined && typeof config.reprojectOnDeploy !== "boolean") {
|
|
31
|
+
problems.push("publication.reprojectOnDeploy must be a boolean");
|
|
32
|
+
}
|
|
30
33
|
const cache = config.cache;
|
|
31
34
|
if (cache !== undefined) {
|
|
32
35
|
const lifetime = cache.htmlSMaxAgeSeconds;
|
|
@@ -52,6 +52,12 @@ export type PapyrusPublicationConfig = {
|
|
|
52
52
|
distribution?: S3ProjectionDistributionConfig;
|
|
53
53
|
/** Default true: the site bucket is retained when the stack is deleted. `false` destroys it and its objects. */
|
|
54
54
|
retainBucketOnDelete?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Re-project every published page after each backend deploy, so a theme or code change reaches the
|
|
57
|
+
* site without a manual `papyrus ops content project --all`. Default: true for the publication a
|
|
58
|
+
* Markus site gets when it sets none, false when `publication` is written out in the site config.
|
|
59
|
+
*/
|
|
60
|
+
reprojectOnDeploy?: boolean;
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
const CLOUDFRONT_DISTRIBUTION_ARN = /^arn:aws[a-z-]*:cloudfront::\d{12}:distribution\/[A-Z0-9]+$/;
|
|
@@ -83,6 +89,9 @@ export function validatePublicationConfig(publication: unknown): string[] {
|
|
|
83
89
|
if (config.edgeDistributionArn !== undefined && !CLOUDFRONT_DISTRIBUTION_ARN.test(config.edgeDistributionArn)) {
|
|
84
90
|
problems.push("publication.edgeDistributionArn must be a CloudFront distribution ARN (arn:aws:cloudfront::<account>:distribution/<id>)");
|
|
85
91
|
}
|
|
92
|
+
if (config.reprojectOnDeploy !== undefined && typeof config.reprojectOnDeploy !== "boolean") {
|
|
93
|
+
problems.push("publication.reprojectOnDeploy must be a boolean");
|
|
94
|
+
}
|
|
86
95
|
const cache = config.cache;
|
|
87
96
|
if (cache !== undefined) {
|
|
88
97
|
const lifetime = cache.htmlSMaxAgeSeconds;
|
|
@@ -156,8 +165,12 @@ export type PapyrusSiteBackendConfig = {
|
|
|
156
165
|
/** Let this stack verify the SES domain identity and write its TXT record. Default false: verify once by hand. */
|
|
157
166
|
manageDomainIdentity?: boolean;
|
|
158
167
|
};
|
|
159
|
-
/**
|
|
160
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Static projection of published pages into an S3 site bucket. Unset: a Markus site without a
|
|
170
|
+
* `reader` gets `{ kind: "s3-projection" }`; any other site gets nothing. `false` opts a Markus
|
|
171
|
+
* site out. See `resolveSitePublication`.
|
|
172
|
+
*/
|
|
173
|
+
publication?: PapyrusPublicationConfig | false;
|
|
161
174
|
features?: {
|
|
162
175
|
consoleResponder?: boolean;
|
|
163
176
|
inboundEmail?: boolean;
|
package/amplify/site-backend.js
CHANGED
|
@@ -7,6 +7,8 @@ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aw
|
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves.js";
|
|
10
|
+
import { resolveSitePublication } from "./publication/default-publication.js";
|
|
11
|
+
import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy.js";
|
|
10
12
|
import { addProjectionRetries, addS3ProjectionPublication } from "./publication/s3-projection.js";
|
|
11
13
|
import { planRenditions } from "./publication/renditions-settings.js";
|
|
12
14
|
import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings.js";
|
|
@@ -68,10 +70,11 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
68
70
|
...baseAuthConfig,
|
|
69
71
|
newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
|
|
70
72
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
73
|
+
const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
|
|
74
|
+
if (publication !== undefined)
|
|
75
|
+
assertValidPublicationConfig(publication);
|
|
76
|
+
const publicationPlan = planS3ProjectionBackend(publication);
|
|
77
|
+
const renditionsPlan = planRenditions(publication, process.cwd());
|
|
75
78
|
setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
|
|
76
79
|
const backend = defineBackend({
|
|
77
80
|
assignmentAction,
|
|
@@ -314,12 +317,16 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
314
317
|
contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
|
|
315
318
|
}
|
|
316
319
|
let s3ProjectionPublication;
|
|
317
|
-
if (
|
|
318
|
-
s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"),
|
|
320
|
+
if (publication !== undefined) {
|
|
321
|
+
s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), publication, {
|
|
319
322
|
mediaBucket: storageBucket,
|
|
320
323
|
renditions: renditionsPlan,
|
|
321
324
|
});
|
|
322
325
|
contentActionsLambda.node.defaultChild.memorySize = publicationPlan.contentActionsMemorySize;
|
|
326
|
+
if (publication.reprojectOnDeploy === true) {
|
|
327
|
+
contentActionsLambda.node.defaultChild.timeout = REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS;
|
|
328
|
+
addReprojectOnDeploy(backend.createStack("reproject-on-deploy"), contentActionsLambda, reprojectDeployToken(process.env));
|
|
329
|
+
}
|
|
323
330
|
}
|
|
324
331
|
if (s3ProjectionPublication !== undefined) {
|
|
325
332
|
addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
|
package/amplify/site-backend.ts
CHANGED
|
@@ -10,6 +10,8 @@ import type { Construct } from "constructs";
|
|
|
10
10
|
import { dirname, resolve } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves";
|
|
13
|
+
import { resolveSitePublication } from "./publication/default-publication";
|
|
14
|
+
import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy";
|
|
13
15
|
import { addProjectionRetries, addS3ProjectionPublication, type S3ProjectionPublication } from "./publication/s3-projection";
|
|
14
16
|
import { planRenditions } from "./publication/renditions-settings";
|
|
15
17
|
import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings";
|
|
@@ -90,9 +92,10 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
90
92
|
...baseAuthConfig,
|
|
91
93
|
newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
|
|
92
94
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
95
|
+
const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
|
|
96
|
+
if (publication !== undefined) assertValidPublicationConfig(publication);
|
|
97
|
+
const publicationPlan = planS3ProjectionBackend(publication);
|
|
98
|
+
const renditionsPlan = planRenditions(publication, process.cwd());
|
|
96
99
|
setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
|
|
97
100
|
const backend = defineBackend({
|
|
98
101
|
assignmentAction,
|
|
@@ -403,12 +406,16 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
403
406
|
);
|
|
404
407
|
}
|
|
405
408
|
let s3ProjectionPublication: S3ProjectionPublication | undefined;
|
|
406
|
-
if (
|
|
407
|
-
s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"),
|
|
409
|
+
if (publication !== undefined) {
|
|
410
|
+
s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), publication, {
|
|
408
411
|
mediaBucket: storageBucket,
|
|
409
412
|
renditions: renditionsPlan,
|
|
410
413
|
});
|
|
411
414
|
(contentActionsLambda.node.defaultChild as CfnFunction).memorySize = publicationPlan.contentActionsMemorySize;
|
|
415
|
+
if (publication.reprojectOnDeploy === true) {
|
|
416
|
+
(contentActionsLambda.node.defaultChild as CfnFunction).timeout = REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS;
|
|
417
|
+
addReprojectOnDeploy(backend.createStack("reproject-on-deploy"), contentActionsLambda, reprojectDeployToken(process.env));
|
|
418
|
+
}
|
|
412
419
|
}
|
|
413
420
|
if (s3ProjectionPublication !== undefined) {
|
|
414
421
|
addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
|
package/infra/build-specs.js
CHANGED
|
@@ -32,6 +32,9 @@ function reader(config) {
|
|
|
32
32
|
throw new Error(`site ${config.siteId} has no reader app (frontend ${config.frontend})`);
|
|
33
33
|
return config.reader;
|
|
34
34
|
}
|
|
35
|
+
function stagingPreviewFor(config) {
|
|
36
|
+
return config.cms?.stagingPreview ?? config.reader;
|
|
37
|
+
}
|
|
35
38
|
function frontendSection(preBuildCommands, buildCommands, artifactsDirectory, cachePaths) {
|
|
36
39
|
const lines = ["frontend:", " phases:"];
|
|
37
40
|
if (preBuildCommands.length > 0) {
|
|
@@ -62,9 +65,9 @@ export function cmsProductionBuildSpec(config) {
|
|
|
62
65
|
export function cmsStagingBuildSpec(config) {
|
|
63
66
|
const preBuild = [NPM_CLEAN_INSTALL, "npx ampx generate outputs --branch main --app-id $AWS_APP_ID"];
|
|
64
67
|
const cachePaths = [".next/cache/**/*", ".npm/**/*"];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
preBuild.push(...pythonProvisioningCommands(config), `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content export-published --drafts --out content-export --clean`,
|
|
68
|
+
const preview = config.frontend === "markus-static" ? stagingPreviewFor(config) : undefined;
|
|
69
|
+
if (preview !== undefined) {
|
|
70
|
+
preBuild.push(...pythonProvisioningCommands(config), `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content export-published --drafts --out content-export --clean`, preview.buildCommand, `${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content upload-preview --dir ${preview.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`);
|
|
68
71
|
cachePaths.push(".uv-cache/**/*");
|
|
69
72
|
}
|
|
70
73
|
return [
|
package/infra/build-specs.ts
CHANGED
|
@@ -43,6 +43,10 @@ function reader(config: AmplifyAppShellSiteConfig): NonNullable<AmplifyAppShellS
|
|
|
43
43
|
return config.reader;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function stagingPreviewFor(config: AmplifyAppShellSiteConfig): { buildCommand: string; baseDirectory: string } | undefined {
|
|
47
|
+
return config.cms?.stagingPreview ?? config.reader;
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
function frontendSection(preBuildCommands: string[], buildCommands: string[], artifactsDirectory: string, cachePaths: string[]): string[] {
|
|
47
51
|
const lines = ["frontend:", " phases:"];
|
|
48
52
|
if (preBuildCommands.length > 0) {
|
|
@@ -94,13 +98,13 @@ export function cmsProductionBuildSpec(config: AmplifyAppShellSiteConfig): strin
|
|
|
94
98
|
export function cmsStagingBuildSpec(config: AmplifyAppShellSiteConfig): string {
|
|
95
99
|
const preBuild = [NPM_CLEAN_INSTALL, "npx ampx generate outputs --branch main --app-id $AWS_APP_ID"];
|
|
96
100
|
const cachePaths = [".next/cache/**/*", ".npm/**/*"];
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
const preview = config.frontend === "markus-static" ? stagingPreviewFor(config) : undefined;
|
|
102
|
+
if (preview !== undefined) {
|
|
99
103
|
preBuild.push(
|
|
100
104
|
...pythonProvisioningCommands(config),
|
|
101
105
|
`${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content export-published --drafts --out content-export --clean`,
|
|
102
|
-
|
|
103
|
-
`${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content upload-preview --dir ${
|
|
106
|
+
preview.buildCommand,
|
|
107
|
+
`${CHECKOUT_AS_PAPYRUS_ROOT} papyrus ops content upload-preview --dir ${preview.baseDirectory} --prefix ${resolveStoragePreviewPrefix(config)}`,
|
|
104
108
|
);
|
|
105
109
|
cachePaths.push(".uv-cache/**/*");
|
|
106
110
|
}
|
package/infra/site-config.js
CHANGED
|
@@ -169,9 +169,19 @@ export function resolveStackName(config) {
|
|
|
169
169
|
export function resolveStoragePreviewPrefix(config) {
|
|
170
170
|
return config.storagePreviewPrefix ?? "preview/";
|
|
171
171
|
}
|
|
172
|
+
function parseStagingPreview(value) {
|
|
173
|
+
if (value === undefined)
|
|
174
|
+
return undefined;
|
|
175
|
+
const record = requireObject(value, "cms.stagingPreview");
|
|
176
|
+
rejectUnknownKeys(record, ["buildCommand", "baseDirectory"], "cms.stagingPreview");
|
|
177
|
+
return {
|
|
178
|
+
buildCommand: requireString(record.buildCommand, "cms.stagingPreview.buildCommand"),
|
|
179
|
+
baseDirectory: requireString(record.baseDirectory, "cms.stagingPreview.baseDirectory"),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
172
182
|
function parseCms(value) {
|
|
173
183
|
const cmsRecord = requireObject(value, "cms");
|
|
174
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
|
|
184
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview"], "cms");
|
|
175
185
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
176
186
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
177
187
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -194,6 +204,7 @@ function parseCms(value) {
|
|
|
194
204
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
195
205
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
196
206
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
207
|
+
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
197
208
|
};
|
|
198
209
|
const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
|
|
199
210
|
if (reservedWord) {
|
|
@@ -306,8 +317,14 @@ export function parseSiteConfig(raw) {
|
|
|
306
317
|
if (reader.domainPrefixes && !reader.domainName)
|
|
307
318
|
fail("reader.domainPrefixes", "requires reader.domainName");
|
|
308
319
|
}
|
|
309
|
-
else if (frontend === "markus-static") {
|
|
310
|
-
fail("reader", `is required when frontend is "markus-static"`);
|
|
320
|
+
else if (frontend === "markus-static" && cms === undefined) {
|
|
321
|
+
fail("reader", `is required when frontend is "markus-static" and there is no cms section`);
|
|
322
|
+
}
|
|
323
|
+
if (cms?.stagingPreview !== undefined && frontend !== "markus-static") {
|
|
324
|
+
fail("cms.stagingPreview", `is only allowed when frontend is "markus-static"`);
|
|
325
|
+
}
|
|
326
|
+
if (cms?.stagingPreview !== undefined && cms.staging === false) {
|
|
327
|
+
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
311
328
|
}
|
|
312
329
|
let github;
|
|
313
330
|
if (record.github !== undefined) {
|
package/infra/site-config.ts
CHANGED
|
@@ -18,11 +18,22 @@ export type ReaderCustomHeaderRule = {
|
|
|
18
18
|
|
|
19
19
|
export const READER_REDIRECT_STATUSES: ReaderRedirectStatus[] = ["301", "302", "200", "404", "404-200"];
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What the CMS `staging` branch builds to preview drafts of a markus-static site: the site build
|
|
23
|
+
* command and the directory it writes. Without it, and without a `reader` to borrow them from,
|
|
24
|
+
* the staging branch builds the newsroom only.
|
|
25
|
+
*/
|
|
26
|
+
export type StagingPreviewConfig = {
|
|
27
|
+
buildCommand: string;
|
|
28
|
+
baseDirectory: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
export type CmsConfig = {
|
|
22
32
|
appName?: string;
|
|
23
33
|
domainName?: string;
|
|
24
34
|
domainPrefix?: string;
|
|
25
35
|
staging?: boolean;
|
|
36
|
+
stagingPreview?: StagingPreviewConfig;
|
|
26
37
|
cognitoDomainPrefix: string;
|
|
27
38
|
applyCognitoDomainPrefix?: boolean;
|
|
28
39
|
buildComputeType?: "STANDARD" | "STANDARD_8GB";
|
|
@@ -236,9 +247,19 @@ export function resolveStoragePreviewPrefix(config: Pick<AmplifyAppShellSiteConf
|
|
|
236
247
|
return config.storagePreviewPrefix ?? "preview/";
|
|
237
248
|
}
|
|
238
249
|
|
|
250
|
+
function parseStagingPreview(value: unknown): StagingPreviewConfig | undefined {
|
|
251
|
+
if (value === undefined) return undefined;
|
|
252
|
+
const record = requireObject(value, "cms.stagingPreview");
|
|
253
|
+
rejectUnknownKeys(record, ["buildCommand", "baseDirectory"], "cms.stagingPreview");
|
|
254
|
+
return {
|
|
255
|
+
buildCommand: requireString(record.buildCommand, "cms.stagingPreview.buildCommand"),
|
|
256
|
+
baseDirectory: requireString(record.baseDirectory, "cms.stagingPreview.baseDirectory"),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
239
260
|
function parseCms(value: unknown): CmsConfig {
|
|
240
261
|
const cmsRecord = requireObject(value, "cms");
|
|
241
|
-
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects"], "cms");
|
|
262
|
+
rejectUnknownKeys(cmsRecord, ["appName", "domainName", "staging", "cognitoDomainPrefix", "applyCognitoDomainPrefix", "buildComputeType", "environment", "stagingDomainName", "domainPrefix", "stagingDomainPrefix", "redirects", "stagingPreview"], "cms");
|
|
242
263
|
const buildComputeType = cmsRecord.buildComputeType;
|
|
243
264
|
if (buildComputeType !== undefined && buildComputeType !== "STANDARD" && buildComputeType !== "STANDARD_8GB") {
|
|
244
265
|
fail("cms.buildComputeType", `must be "STANDARD" or "STANDARD_8GB", got ${JSON.stringify(buildComputeType)}`);
|
|
@@ -266,6 +287,7 @@ function parseCms(value: unknown): CmsConfig {
|
|
|
266
287
|
stagingDomainName: optionalString(cmsRecord.stagingDomainName, "cms.stagingDomainName", HOST_NAME_PATTERN, "host name"),
|
|
267
288
|
stagingDomainPrefix: optionalString(cmsRecord.stagingDomainPrefix, "cms.stagingDomainPrefix", DOMAIN_PREFIX_PATTERN, "single DNS label"),
|
|
268
289
|
redirects: parseRedirects(cmsRecord.redirects),
|
|
290
|
+
stagingPreview: parseStagingPreview(cmsRecord.stagingPreview),
|
|
269
291
|
};
|
|
270
292
|
|
|
271
293
|
const reservedWord = COGNITO_RESERVED_WORDS.find((word) => cms.cognitoDomainPrefix.includes(word));
|
|
@@ -384,8 +406,14 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
384
406
|
skipContentExport: readerRecord.skipContentExport as boolean | undefined,
|
|
385
407
|
};
|
|
386
408
|
if (reader.domainPrefixes && !reader.domainName) fail("reader.domainPrefixes", "requires reader.domainName");
|
|
387
|
-
} else if (frontend === "markus-static") {
|
|
388
|
-
fail("reader", `is required when frontend is "markus-static"`);
|
|
409
|
+
} else if (frontend === "markus-static" && cms === undefined) {
|
|
410
|
+
fail("reader", `is required when frontend is "markus-static" and there is no cms section`);
|
|
411
|
+
}
|
|
412
|
+
if (cms?.stagingPreview !== undefined && frontend !== "markus-static") {
|
|
413
|
+
fail("cms.stagingPreview", `is only allowed when frontend is "markus-static"`);
|
|
414
|
+
}
|
|
415
|
+
if (cms?.stagingPreview !== undefined && cms.staging === false) {
|
|
416
|
+
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
389
417
|
}
|
|
390
418
|
|
|
391
419
|
let github: AmplifyAppShellSiteConfig["github"];
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED