mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
87a3f37a49
* fix(build): exclude filtered require.context modules require.context regexps previously filtered only the runtime map after a broad eager glob had imported every file. This evaluated and bundled excluded modules, including from client components. Resolve and filter context entries during the transform so only accepted files become static dependencies. Keep context directories watched so create and delete events can update the generated module set. * test: update require-context unit tests for static-import transform * fix(build): harden require.context enumeration and bindings Replace fs.glob (withFileTypes needs Node 22.2, engines allow >=22) with a readdir walk that follows directory symlinks like webpack and guards cycles via realpath, and grow the generated import binding prefix past any identifier already present in the source. * fix(build): scope symlink cycle guard to the recursion path A global realpath set deduplicated distinct symlink aliases of the same directory; track realpaths only along the current recursion path so aliases keep their own context keys while cycles still terminate. * fix(build): stat directory entries with unknown dirent types Filesystems without dirent type info (NFS, SMB, FUSE) report entries that are neither file nor directory; fall back to stat for any unknown type instead of only symlinks, and skip unresolvable ENOENT/ELOOP entries. * fix(build): make require.context deterministic and dev-invalidation complete Assign import binding indices after sorting so readdir order cannot change bundle bytes; invalidate recursive contexts on any membership event since a directory create/delete can change matching descendants without matching the file regexp; and drop watched-context entries for updated modules so importers that lose their last require.context call stop invalidating. --------- Co-authored-by: James <james@eli.cx>
87 lines
3.8 KiB
TypeScript
87 lines
3.8 KiB
TypeScript
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { parseAst } from "vite";
|
|
import { describe, expect, it } from "vite-plus/test";
|
|
import { createRequireContextPlugin } from "../packages/vinext/src/plugins/require-context.js";
|
|
|
|
const importerId = path.resolve(
|
|
import.meta.dirname,
|
|
"./fixtures/app-basic/app/nextjs-compat/require-context/page.tsx",
|
|
);
|
|
|
|
function createTransform(): (code: string, id: string) => Promise<{ code: string } | null> {
|
|
const plugin = createRequireContextPlugin();
|
|
const hook = plugin.transform;
|
|
const handler = typeof hook === "function" ? hook : hook?.handler;
|
|
// The handler keys per-environment state and registers directory watchers;
|
|
// give it the minimal plugin context those two calls need.
|
|
const context = { environment: {}, addWatchFile: () => {} };
|
|
return handler!.bind(context as never) as never;
|
|
}
|
|
|
|
describe("vinext:require-context", () => {
|
|
it("emits static imports only for modules accepted by the regexp", async () => {
|
|
const transform = createTransform();
|
|
const result = await transform(
|
|
`const ctx = require.context("./filtered", false, /\\.safe\\.js$/);`,
|
|
importerId,
|
|
);
|
|
|
|
expect(result?.code).toContain('from "./filtered/included.safe.js"');
|
|
expect(result?.code).not.toContain("excluded.js");
|
|
expect(result?.code).toContain('["./included.safe.js"]');
|
|
});
|
|
|
|
it("inserts generated imports after the directive prologue", async () => {
|
|
const transform = createTransform();
|
|
const source = `"use client";\nconst ctx = require.context("./filtered", false, /\\.safe\\.js$/);`;
|
|
const result = await transform(source, importerId);
|
|
|
|
const code = result!.code;
|
|
expect(code.indexOf('"use client"')).toBeLessThan(code.indexOf("import * as "));
|
|
});
|
|
|
|
it("avoids colliding with existing identifiers when generating import bindings", async () => {
|
|
const transform = createTransform();
|
|
const source = [
|
|
`const __vinext_require_context_0_0 = 1;`,
|
|
`const ctx = require.context("./filtered", false, /\\.safe\\.js$/);`,
|
|
`export { ctx, __vinext_require_context_0_0 };`,
|
|
].join("\n");
|
|
const result = await transform(source, importerId);
|
|
|
|
const code = result!.code;
|
|
expect(code).not.toMatch(/import \* as __vinext_require_context_0_0 /);
|
|
// Redeclaring the user's binding would make the module fail to parse.
|
|
expect(() => parseAst(code)).not.toThrow();
|
|
});
|
|
|
|
it("traverses symlinked directories in recursive contexts", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "vinext-require-context-"));
|
|
try {
|
|
await mkdir(path.join(root, "target/sub"), { recursive: true });
|
|
await writeFile(path.join(root, "target/sub/deep.js"), "export default 1;\n");
|
|
await mkdir(path.join(root, "context"));
|
|
await symlink(path.join(root, "target"), path.join(root, "context/link"));
|
|
await symlink(path.join(root, "target"), path.join(root, "context/alias"));
|
|
// A cycle back into the context itself must terminate, not recurse forever.
|
|
await symlink(path.join(root, "context"), path.join(root, "target/loop"));
|
|
// A self-referential symlink (stat -> ELOOP) must be skipped, not throw.
|
|
await symlink(path.join(root, "context/self"), path.join(root, "context/self"));
|
|
|
|
const transform = createTransform();
|
|
const result = await transform(
|
|
`const ctx = require.context("./context", true, /\\.js$/);`,
|
|
path.join(root, "page.tsx"),
|
|
);
|
|
|
|
// Distinct symlink aliases of one target each keep their own keys.
|
|
expect(result?.code).toContain('"./link/sub/deep.js"');
|
|
expect(result?.code).toContain('"./alias/sub/deep.js"');
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|