@anthusai/papyrus 5.18.0 → 5.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -91,6 +91,22 @@ function markdownTwin(uri) {
91
91
  return uri.substring(0, uri.length - 5) + ".md";
92
92
  }
93
93
 
94
+ // Directory-style sites keep a page's twin at /x/index.md, so a bare /x.md is rewritten there. The edge cannot test for
95
+ // existence, hence the rule: a last segment <name>.md with a name other than index. A path prefix (a base path) is just
96
+ // part of <path>. The flag is spliced in by redirect-rules.ts at deploy time from urls.style.
97
+ const directoryTwins = /*PAPYRUS_DIRECTORY_TWINS*/false/*END_PAPYRUS_DIRECTORY_TWINS*/;
98
+
99
+ function directoryTwin(uri) {
100
+ if (uri.length < 5 || uri.substring(uri.length - 3) !== ".md") {
101
+ return null;
102
+ }
103
+ const lastSegment = uri.substring(uri.lastIndexOf("/") + 1);
104
+ if (lastSegment === "index.md" || lastSegment === ".md") {
105
+ return null;
106
+ }
107
+ return uri.substring(0, uri.length - 3) + "/index.md";
108
+ }
109
+
94
110
  async function handler(event) {
95
111
  const request = event.request;
96
112
  const redirectTarget = await lookUpRedirectTarget(request.uri);
@@ -110,6 +126,12 @@ async function handler(event) {
110
126
  };
111
127
  }
112
128
  request.uri = rewriteToIndexDocument(request.uri);
129
+ if (directoryTwins) {
130
+ const bareTwin = directoryTwin(request.uri);
131
+ if (bareTwin !== null) {
132
+ request.uri = bareTwin;
133
+ }
134
+ }
113
135
  if (negotiateMarkdown && prefersMarkdown(request)) {
114
136
  const twin = markdownTwin(request.uri);
115
137
  if (twin !== null) {
@@ -27,20 +27,34 @@ export function edgeRulesFromSettings(settings) {
27
27
  export function negotiatesMarkdown(settings) {
28
28
  return settings.site !== undefined && settings.agents?.markdown !== false;
29
29
  }
30
+ /** A directory-style site keeps a page's Markdown twin at `/x/index.md`; the edge then serves a bare `/x.md` from there. */
31
+ export function usesDirectoryTwins(settings) {
32
+ return negotiatesMarkdown(settings) && settings.urls?.style === "directory";
33
+ }
30
34
  export function readEdgeConfig(staticDir, baseDirectory = process.cwd()) {
31
35
  if (staticDir === undefined)
32
- return { rules: [], negotiateMarkdown: false };
36
+ return { rules: [], negotiateMarkdown: false, directoryTwins: false };
33
37
  const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
34
38
  if (!existsSync(settingsPath))
35
- return { rules: [], negotiateMarkdown: false };
39
+ return { rules: [], negotiateMarkdown: false, directoryTwins: false };
36
40
  const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
37
- return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings) };
41
+ return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings), directoryTwins: usesDirectoryTwins(settings) };
38
42
  }
39
43
  export function readEdgeRules(staticDir, baseDirectory = process.cwd()) {
40
44
  return readEdgeConfig(staticDir, baseDirectory).rules;
41
45
  }
42
46
  const NEGOTIATE_START = "/*PAPYRUS_NEGOTIATE_MARKDOWN*/";
43
47
  const NEGOTIATE_END = "/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/";
48
+ const TWINS_START = "/*PAPYRUS_DIRECTORY_TWINS*/";
49
+ const TWINS_END = "/*END_PAPYRUS_DIRECTORY_TWINS*/";
50
+ function spliceFlag(source, start, end, label) {
51
+ const from = source.indexOf(start);
52
+ const to = source.indexOf(end);
53
+ if (from === -1 || to === -1 || to < from) {
54
+ throw new Error(`redirect function source has no ${label} markers`);
55
+ }
56
+ return source.slice(0, from + start.length) + "true" + source.slice(to);
57
+ }
44
58
  /** The CloudFront Function source with the site's pattern rules and Markdown negotiation flag spliced in. */
45
59
  export function compileRedirectFunctionSource(source, rules, options = {}) {
46
60
  const start = source.indexOf(START_MARKER);
@@ -50,12 +64,10 @@ export function compileRedirectFunctionSource(source, rules, options = {}) {
50
64
  }
51
65
  let compiled = rules.length === 0 ? source : source.slice(0, start + START_MARKER.length) + JSON.stringify(rules) + source.slice(end);
52
66
  if (options.negotiateMarkdown === true) {
53
- const from = compiled.indexOf(NEGOTIATE_START);
54
- const to = compiled.indexOf(NEGOTIATE_END);
55
- if (from === -1 || to === -1 || to < from) {
56
- throw new Error("redirect function source has no markdown negotiation markers");
57
- }
58
- compiled = compiled.slice(0, from + NEGOTIATE_START.length) + "true" + compiled.slice(to);
67
+ compiled = spliceFlag(compiled, NEGOTIATE_START, NEGOTIATE_END, "markdown negotiation");
68
+ }
69
+ if (options.directoryTwins === true) {
70
+ compiled = spliceFlag(compiled, TWINS_START, TWINS_END, "directory twin");
59
71
  }
60
72
  return compiled;
61
73
  }
@@ -38,14 +38,19 @@ export function negotiatesMarkdown(settings: ProjectionSettings): boolean {
38
38
  return settings.site !== undefined && settings.agents?.markdown !== false;
39
39
  }
40
40
 
41
- export type EdgeConfig = { rules: EdgeRedirectRule[]; negotiateMarkdown: boolean };
41
+ /** A directory-style site keeps a page's Markdown twin at `/x/index.md`; the edge then serves a bare `/x.md` from there. */
42
+ export function usesDirectoryTwins(settings: ProjectionSettings): boolean {
43
+ return negotiatesMarkdown(settings) && settings.urls?.style === "directory";
44
+ }
45
+
46
+ export type EdgeConfig = { rules: EdgeRedirectRule[]; negotiateMarkdown: boolean; directoryTwins: boolean };
42
47
 
43
48
  export function readEdgeConfig(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeConfig {
44
- if (staticDir === undefined) return { rules: [], negotiateMarkdown: false };
49
+ if (staticDir === undefined) return { rules: [], negotiateMarkdown: false, directoryTwins: false };
45
50
  const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
46
- if (!existsSync(settingsPath)) return { rules: [], negotiateMarkdown: false };
51
+ if (!existsSync(settingsPath)) return { rules: [], negotiateMarkdown: false, directoryTwins: false };
47
52
  const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as ProjectionSettings;
48
- return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings) };
53
+ return { rules: edgeRulesFromSettings(settings), negotiateMarkdown: negotiatesMarkdown(settings), directoryTwins: usesDirectoryTwins(settings) };
49
54
  }
50
55
 
51
56
  export function readEdgeRules(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeRedirectRule[] {
@@ -54,12 +59,23 @@ export function readEdgeRules(staticDir: string | undefined, baseDirectory: stri
54
59
 
55
60
  const NEGOTIATE_START = "/*PAPYRUS_NEGOTIATE_MARKDOWN*/";
56
61
  const NEGOTIATE_END = "/*END_PAPYRUS_NEGOTIATE_MARKDOWN*/";
62
+ const TWINS_START = "/*PAPYRUS_DIRECTORY_TWINS*/";
63
+ const TWINS_END = "/*END_PAPYRUS_DIRECTORY_TWINS*/";
64
+
65
+ function spliceFlag(source: string, start: string, end: string, label: string): string {
66
+ const from = source.indexOf(start);
67
+ const to = source.indexOf(end);
68
+ if (from === -1 || to === -1 || to < from) {
69
+ throw new Error(`redirect function source has no ${label} markers`);
70
+ }
71
+ return source.slice(0, from + start.length) + "true" + source.slice(to);
72
+ }
57
73
 
58
74
  /** The CloudFront Function source with the site's pattern rules and Markdown negotiation flag spliced in. */
59
75
  export function compileRedirectFunctionSource(
60
76
  source: string,
61
77
  rules: EdgeRedirectRule[],
62
- options: { negotiateMarkdown?: boolean } = {},
78
+ options: { negotiateMarkdown?: boolean; directoryTwins?: boolean } = {},
63
79
  ): string {
64
80
  const start = source.indexOf(START_MARKER);
65
81
  const end = source.indexOf(END_MARKER);
@@ -68,12 +84,10 @@ export function compileRedirectFunctionSource(
68
84
  }
69
85
  let compiled = rules.length === 0 ? source : source.slice(0, start + START_MARKER.length) + JSON.stringify(rules) + source.slice(end);
70
86
  if (options.negotiateMarkdown === true) {
71
- const from = compiled.indexOf(NEGOTIATE_START);
72
- const to = compiled.indexOf(NEGOTIATE_END);
73
- if (from === -1 || to === -1 || to < from) {
74
- throw new Error("redirect function source has no markdown negotiation markers");
75
- }
76
- compiled = compiled.slice(0, from + NEGOTIATE_START.length) + "true" + compiled.slice(to);
87
+ compiled = spliceFlag(compiled, NEGOTIATE_START, NEGOTIATE_END, "markdown negotiation");
88
+ }
89
+ if (options.directoryTwins === true) {
90
+ compiled = spliceFlag(compiled, TWINS_START, TWINS_END, "directory twin");
77
91
  }
78
92
  return compiled;
79
93
  }
@@ -70,10 +70,12 @@ export function addS3ProjectionPublication(scope, props, media) {
70
70
  assertValidPublicationConfig({ kind: "s3-projection", ...props });
71
71
  const bucket = createSiteBucket(scope, props.retainBucketOnDelete !== false);
72
72
  const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
73
+ const edgeConfig = readEdgeConfig(props.staticDir);
73
74
  const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
74
75
  runtime: cloudfront.FunctionRuntime.JS_2_0,
75
- code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeConfig(props.staticDir).rules, {
76
- negotiateMarkdown: readEdgeConfig(props.staticDir).negotiateMarkdown,
76
+ code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), edgeConfig.rules, {
77
+ negotiateMarkdown: edgeConfig.negotiateMarkdown,
78
+ directoryTwins: edgeConfig.directoryTwins,
77
79
  })),
78
80
  keyValueStore,
79
81
  });
@@ -112,11 +112,13 @@ export function addS3ProjectionPublication(
112
112
  const bucket = createSiteBucket(scope, props.retainBucketOnDelete !== false);
113
113
 
114
114
  const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
115
+ const edgeConfig = readEdgeConfig(props.staticDir);
115
116
  const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
116
117
  runtime: cloudfront.FunctionRuntime.JS_2_0,
117
118
  code: cloudfront.FunctionCode.fromInline(
118
- compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeConfig(props.staticDir).rules, {
119
- negotiateMarkdown: readEdgeConfig(props.staticDir).negotiateMarkdown,
119
+ compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), edgeConfig.rules, {
120
+ negotiateMarkdown: edgeConfig.negotiateMarkdown,
121
+ directoryTwins: edgeConfig.directoryTwins,
120
122
  }),
121
123
  ),
122
124
  keyValueStore,
@@ -53,7 +53,7 @@ if (command === "compile-edge-function") {
53
53
  const { compileRedirectFunctionSource, readEdgeConfig } = await import("../amplify/publication/redirect-rules.js");
54
54
  const functionSource = fs.readFileSync(new URL("../amplify/publication/redirect-and-index.js", import.meta.url), "utf8");
55
55
  const config = readEdgeConfig(staticDir);
56
- const compiled = compileRedirectFunctionSource(functionSource, config.rules, { negotiateMarkdown: config.negotiateMarkdown });
56
+ const compiled = compileRedirectFunctionSource(functionSource, config.rules, { negotiateMarkdown: config.negotiateMarkdown, directoryTwins: config.directoryTwins });
57
57
  const out = opt("out");
58
58
  if (out) fs.writeFileSync(path.resolve(out), compiled);
59
59
  else process.stdout.write(compiled);
package/package.json CHANGED
@@ -407,7 +407,7 @@
407
407
  "optional": true
408
408
  }
409
409
  },
410
- "version": "5.18.0",
410
+ "version": "5.19.0",
411
411
  "dependencies": {
412
412
  "@aws-amplify/adapter-nextjs": "^1.7.3",
413
413
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.18.0",
2
+ "version": "5.19.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",