Skip to content

Commit ac7321c

Browse files
committed
patch aws to get mathjax working
1 parent 7ba626f commit ac7321c

7 files changed

Lines changed: 357 additions & 38 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Regression reproduction for https://github.com/opennextjs/opennextjs-cloudflare/issues/1229
2+
import { mathjax } from "@mathjax/src/js/mathjax.js";
3+
import { TeX } from "@mathjax/src/js/input/tex.js";
4+
import "@mathjax/src/js/input/tex/mhchem/MhchemConfiguration.js";
5+
import { SVG } from "@mathjax/src/js/output/svg.js";
6+
import { liteAdaptor } from "@mathjax/src/js/adaptors/liteAdaptor.js";
7+
import { RegisterHTMLHandler } from "@mathjax/src/js/handlers/html.js";
8+
9+
export async function GET() {
10+
const adaptor = liteAdaptor();
11+
RegisterHTMLHandler(adaptor as never);
12+
13+
const tex = new TeX({ packages: ["base", "mhchem"] });
14+
const svg = new SVG({ fontCache: "none" });
15+
const html = mathjax.document("", { InputJax: tex, OutputJax: svg });
16+
17+
const node = html.convert("\\ce{H2O}", { display: true });
18+
const rendered = adaptor.innerHTML(node as never);
19+
20+
return new Response(JSON.stringify({ rendered }), {
21+
headers: { "content-type": "application/json" },
22+
});
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
// Regression test for https://github.com/opennextjs/opennextjs-cloudflare/issues/1229
4+
// `@mathjax/src` uses Node.js package.json#imports to resolve internal subpath
5+
// specifiers (e.g. `#mhchem/*` → `mhchemparser/esm/*`). NFT does not follow the
6+
// `imports` field, so remap targets were missing from the traced output and esbuild
7+
// failed to resolve them. This verifies both that the build succeeds and that the
8+
// remapped modules are actually loadable at runtime.
9+
test("mathjax tex→svg via API route (exercises package.json#imports remap)", async ({ request }) => {
10+
const response = await request.get("/api/mathjax");
11+
expect(response.status()).toEqual(200);
12+
13+
const json = await response.json();
14+
expect(json).toHaveProperty("rendered");
15+
// The rendered output should contain an SVG (proves MathJax actually executed,
16+
// which in turn proves the `#mhchem/*` → mhchemparser remap resolved at runtime).
17+
expect(typeof json.rendered).toBe("string");
18+
expect(json.rendered).toMatch(/<svg/);
19+
});

examples/playground16/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ initOpenNextCloudflareForDev();
55

66
const nextConfig: NextConfig = {
77
typescript: { ignoreBuildErrors: true },
8-
serverExternalPackages: ["shiki"],
8+
serverExternalPackages: ["shiki", "@mathjax/src"],
99
};
1010

1111
export default nextConfig;

examples/playground16/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
"cf-typegen": "wrangler types --env-interface CloudflareEnv"
1818
},
1919
"dependencies": {
20+
"@mathjax/src": "4.1.1",
2021
"next": "16.2.3",
2122
"react-dom": "19.2.4",
2223
"react": "19.2.4",
23-
"shiki": "^3.22.0"
24+
"shiki": "3.22.0"
2425
},
2526
"devDependencies": {
2627
"@opennextjs/cloudflare": "workspace:*",
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
diff --git a/dist/build/copyTracedFiles.js b/dist/build/copyTracedFiles.js
2+
index 9701b0401e987ee7cd696873af9d15b751580c52..61c62b3b16205b4d043f92e9e3c5bb5f2c2415d1 100644
3+
--- a/dist/build/copyTracedFiles.js
4+
+++ b/dist/build/copyTracedFiles.js
5+
@@ -1,3 +1,4 @@
6+
+import { createRequire } from "node:module";
7+
import url from "node:url";
8+
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
9+
import path from "node:path";
10+
@@ -46,6 +47,111 @@ const nonLinuxPlatformRegex = getCrossPlatformPathRegex(`/node_modules/(?:@[^/]+
11+
export function isNonLinuxPlatformPackage(srcPath) {
12+
return nonLinuxPlatformRegex.test(srcPath);
13+
}
14+
+function collectImportTargets(value, out) {
15+
+ if (typeof value === "string") {
16+
+ out.push(value);
17+
+ }
18+
+ else if (Array.isArray(value)) {
19+
+ value.forEach((v) => collectImportTargets(v, out));
20+
+ }
21+
+ else if (value && typeof value === "object") {
22+
+ Object.values(value).forEach((v) => collectImportTargets(v, out));
23+
+ }
24+
+}
25+
+function augmentWithImportsRemaps(filesToCopy, buildOutputPath) {
26+
+ const visitedConsumers = new Set();
27+
+ const projectRequire = createRequire(path.join(buildOutputPath, "package.json"));
28+
+ const pending = [];
29+
+ for (const [src, dst] of filesToCopy) {
30+
+ if (src.endsWith("/package.json"))
31+
+ pending.push([src, dst]);
32+
+ }
33+
+ while (pending.length) {
34+
+ const [pkgSrc, pkgDst] = pending.shift();
35+
+ if (visitedConsumers.has(pkgSrc))
36+
+ continue;
37+
+ visitedConsumers.add(pkgSrc);
38+
+ let pkg;
39+
+ try {
40+
+ pkg = JSON.parse(readFileSync(pkgSrc, "utf-8"));
41+
+ }
42+
+ catch {
43+
+ continue;
44+
+ }
45+
+ if (!pkg.imports || typeof pkg.imports !== "object")
46+
+ continue;
47+
+ const consumerName = typeof pkg.name === "string" ? pkg.name : "";
48+
+ const upLevels = consumerName.startsWith("@") ? 2 : 1;
49+
+ const consumerSrcDir = path.dirname(pkgSrc);
50+
+ const consumerDstDir = path.dirname(pkgDst);
51+
+ const dstNodeModules = path.resolve(consumerDstDir, "../".repeat(upLevels));
52+
+ // Consumer resolves deps via its own source location in the real project.
53+
+ const realConsumerDir = consumerName
54+
+ ? (() => {
55+
+ try {
56+
+ return path.dirname(projectRequire.resolve(`${consumerName}/package.json`));
57+
+ }
58+
+ catch {
59+
+ return consumerSrcDir;
60+
+ }
61+
+ })()
62+
+ : consumerSrcDir;
63+
+ const consumerRequire = createRequire(path.join(realConsumerDir, "package.json"));
64+
+ const targets = [];
65+
+ for (const v of Object.values(pkg.imports))
66+
+ collectImportTargets(v, targets);
67+
+ for (const target of targets) {
68+
+ if (!target ||
69+
+ target.startsWith("./") ||
70+
+ target.startsWith("../") ||
71+
+ target.startsWith("/") ||
72+
+ target.startsWith("#"))
73+
+ continue;
74+
+ const parts = target.split("/");
75+
+ const targetPkg = parts[0].startsWith("@")
76+
+ ? `${parts[0]}/${parts[1]}`
77+
+ : parts[0];
78+
+ if (!targetPkg)
79+
+ continue;
80+
+ let targetPkgJson;
81+
+ try {
82+
+ targetPkgJson = consumerRequire.resolve(`${targetPkg}/package.json`);
83+
+ }
84+
+ catch {
85+
+ logger.debug(`imports-remap: could not resolve ${targetPkg} from ${realConsumerDir}`);
86+
+ continue;
87+
+ }
88+
+ const targetSrcDir = path.dirname(targetPkgJson);
89+
+ const targetDstDir = path.join(dstNodeModules, targetPkg);
90+
+ const walk = (src, dst) => {
91+
+ let entries;
92+
+ try {
93+
+ entries = readdirSync(src, { withFileTypes: true });
94+
+ }
95+
+ catch {
96+
+ return;
97+
+ }
98+
+ for (const entry of entries) {
99+
+ const srcEntry = path.join(src, entry.name);
100+
+ const dstEntry = path.join(dst, entry.name);
101+
+ if (entry.isDirectory()) {
102+
+ if (entry.name === "node_modules")
103+
+ continue;
104+
+ walk(srcEntry, dstEntry);
105+
+ }
106+
+ else if (entry.isFile()) {
107+
+ if (filesToCopy.has(srcEntry))
108+
+ continue;
109+
+ filesToCopy.set(srcEntry, dstEntry);
110+
+ if (srcEntry.endsWith("/package.json"))
111+
+ pending.push([srcEntry, dstEntry]);
112+
+ }
113+
+ }
114+
+ };
115+
+ walk(targetSrcDir, targetDstDir);
116+
+ }
117+
+ }
118+
+}
119+
function copyPatchFile(outputDir) {
120+
const patchFile = path.join(__dirname, "patch", "patchedAsyncStorage.js");
121+
const outputPatchFile = path.join(outputDir, "patchedAsyncStorage.cjs");
122+
@@ -192,6 +298,12 @@ File ${serverPath} does not exist
123+
routes.forEach((route) => {
124+
computeCopyFilesForPage(route);
125+
});
126+
+ // Augment traced files with targets of `package.json#imports` subpath remaps.
127+
+ // Next's NFT tracer does not follow the `imports` field, so packages that are
128+
+ // only reachable via remaps (e.g. @mathjax/src's "#mhchem/*" → "mhchemparser/esm/*")
129+
+ // are missing from the trace. Scan every traced package.json for an `imports`
130+
+ // field and pull in any bare-specifier remap targets from source.
131+
+ augmentWithImportsRemaps(filesToCopy, buildOutputPath);
132+
// Only files that are actually copied
133+
const tracedFiles = [];
134+
const erroredFiles = [];

0 commit comments

Comments
 (0)