1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3import { relative } from "node:path";
4import * as Effect from "effect/Effect";
5import {
6 HEADERS_VERSION,
7 PLACEHOLDER_REGEX,
8 REDIRECTS_VERSION,
9 SPLAT_REGEX,
10} from "../../shared/configuration/constants.ts";
11import type {
12 AssetConfig,
13 MetadataHeaders,
14 MetadataRedirects,
15 MetadataStaticRedirects,
16} from "../../shared/types.ts";
17import type {
18 ParsedHeaders,
19 ParsedRedirects,
20} from "../../shared/configuration/types.ts";
21
22export const constructRedirects = Effect.fn("constructRedirects")(function* ({
23 redirects,
24 redirectsFile,
25}: {
26 redirects?: ParsedRedirects;
27 redirectsFile?: string;
28}) {
29 if (!redirects) {
30 return {};
31 }
32
33 const num_valid = redirects.rules.length;
34 const num_invalid = redirects.invalid.length;
35
36 // exhaustive check, since we could not have parsed `redirects` out of
37 // a non-existing redirects file
38 const redirectsRelativePath = redirectsFile
39 ? relative(process.cwd(), redirectsFile)
40 : "";
41
42 yield* Effect.log(
43 `✨ Parsed ${num_valid} valid redirect rule${num_valid === 1 ? "" : "s"}.`,
44 );
45
46 if (num_invalid > 0) {
47 let invalidRedirectRulesList = ``;
48
49 for (const { line, lineNumber, message } of redirects.invalid) {
50 invalidRedirectRulesList += `▶︎ ${message}\n`;
51
52 if (line) {
53 invalidRedirectRulesList += ` at ${redirectsRelativePath}${lineNumber ? `:${lineNumber}` : ""} | ${line}\n\n`;
54 }
55 }
56
57 yield* Effect.logWarning(
58 `Found ${num_invalid} invalid redirect rule${num_invalid === 1 ? "" : "s"}:\n` +
59 `${invalidRedirectRulesList}`,
60 );
61 }
62
63 /* Better to return no Redirects object at all than one with empty rules */
64 if (num_valid === 0) {
65 return {};
66 }
67
68 const staticRedirects: MetadataStaticRedirects = {};
69 const dynamicRedirects: MetadataRedirects = {};
70 let canCreateStaticRule = true;
71 for (const rule of redirects.rules) {
72 if (!rule.from.match(SPLAT_REGEX) && !rule.from.match(PLACEHOLDER_REGEX)) {
73 if (canCreateStaticRule) {
74 staticRedirects[rule.from] = {
75 status: rule.status,
76 to: rule.to,
77 lineNumber: rule.lineNumber,
78 };
79 continue;
80 } else {
81 yield* Effect.logInfo(
82 `The redirect rule ${rule.from} → ${rule.status} ${rule.to} could be made more performant by bringing it above any lines with splats or placeholders.`,
83 );
84 }
85 }
86
87 dynamicRedirects[rule.from] = { status: rule.status, to: rule.to };
88 canCreateStaticRule = false;
89 }
90
91 return {
92 redirects: {
93 version: REDIRECTS_VERSION,
94 staticRules: staticRedirects,
95 rules: dynamicRedirects,
96 },
97 } satisfies Pick<AssetConfig, "redirects">;
98});
99
100export const constructHeaders = Effect.fn("constructHeaders")(function* ({
101 headers,
102 headersFile,
103}: {
104 headers?: ParsedHeaders;
105 headersFile?: string;
106}) {
107 if (!headers) {
108 return {};
109 }
110
111 const num_valid = headers.rules.length;
112 const num_invalid = headers.invalid.length;
113
114 // exhaustive check, since we could not have parsed `headers` out of
115 // a non-existing headers file
116 const headersRelativePath = headersFile
117 ? relative(process.cwd(), headersFile)
118 : "";
119
120 yield* Effect.log(
121 `✨ Parsed ${num_valid} valid header rule${num_valid === 1 ? "" : "s"}.`,
122 );
123
124 if (num_invalid > 0) {
125 let invalidHeaderRulesList = ``;
126
127 for (const { line, lineNumber, message } of headers.invalid) {
128 invalidHeaderRulesList += `▶︎ ${message}\n`;
129
130 if (line) {
131 invalidHeaderRulesList += ` at ${headersRelativePath}${lineNumber ? `:${lineNumber}` : ""} | ${line}\n\n`;
132 }
133 }
134
135 yield* Effect.logWarning(
136 `Found ${num_invalid} invalid header rule${num_invalid === 1 ? "" : "s"}:\n` +
137 `${invalidHeaderRulesList}`,
138 );
139 }
140
141 /* Better to return no Headers object at all than one with empty rules */
142 if (num_valid === 0) {
143 return {};
144 }
145
146 const rules: MetadataHeaders = {};
147 for (const rule of headers.rules) {
148 const configuredRule: MetadataHeaders[string] = {};
149
150 if (Object.keys(rule.headers).length) {
151 configuredRule.set = rule.headers;
152 }
153 if (rule.unsetHeaders.length) {
154 configuredRule.unset = rule.unsetHeaders;
155 }
156
157 rules[rule.path] = configuredRule;
158 }
159
160 return {
161 headers: {
162 version: HEADERS_VERSION,
163 rules,
164 },
165 } satisfies Pick<AssetConfig, "headers">;
166});