Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/a-created-require-is-a-module-load.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"nextly": patch
"create-nextly-app": patch
"@nextlyhq/admin": patch
"@nextlyhq/admin-css": patch
"@nextlyhq/blocks-engine": patch
"@nextlyhq/blocks-react": patch
"@nextlyhq/plugin-mcp": patch
"@nextlyhq/ui": patch
"@nextlyhq/adapter-drizzle": patch
"@nextlyhq/adapter-postgres": patch
"@nextlyhq/adapter-mysql": patch
"@nextlyhq/adapter-sqlite": patch
"@nextlyhq/storage-s3": patch
"@nextlyhq/storage-uploadthing": patch
"@nextlyhq/storage-vercel-blob": patch
"@nextlyhq/plugin-form-builder": patch
"@nextlyhq/plugin-page-builder": patch
"@nextlyhq/plugin-seo": patch
"@nextlyhq/plugin-sdk": patch
"@nextlyhq/eslint-config": patch
"@nextlyhq/eslint-plugin": patch
"@nextlyhq/prettier-config": patch
"@nextlyhq/telemetry": patch
"@nextlyhq/tsconfig": patch
"@nextlyhq/builder": patch
"@nextlyhq/module-specifiers": patch
---

`@nextlyhq/module-specifiers` reads a require function that `createRequire` returned, whether bound to a name or called where it is made, `require.resolve` and `import.meta.resolve`, and labels every reference that reaches runtime with the resolver that finds it and whether it runs the module or only finds it, so a caller can follow a relative specifier to the file Node would load and skip a file Node never runs.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"@eslint/js": "^9.34.0",
"@manypkg/get-packages": "^1.1.3",
"@nextlyhq/eslint-plugin": "workspace:*",
"@nextlyhq/module-specifiers": "workspace:*",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
Expand Down
23 changes: 23 additions & 0 deletions packages/module-specifiers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,26 @@ the green it always did. The corpus that catches that belongs beside the reader.
Note this is a different control from "the input set is non-empty". A guard can
read every file it was given and still be unable to fail on any input. Only a
known offender it must REJECT catches that.

## Which resolver finds a module, and whether it runs

A reference that survives to runtime also says which of Node's resolvers finds
it. `"esm"` covers an import declaration, a re-export, `import()` and
`import.meta.resolve()`, which take the path as written. `"cjs"` covers
`require()`, `module.require()`, `import x = require()`, a function
`createRequire` returned, whether bound to a name or called where it is made,
and `.resolve()` on either kind of require, which also try the extensions, the
directory's `package.json` `main` and the index files CommonJS adds. A caller
following a relative specifier to its file needs the difference: `./lib` is
`lib.js` to a require and nothing at all to an import.

It also says whether the module runs. `loads` is `false` for `require.resolve()`
and `import.meta.resolve()`, which return where a module is without running it.
A package that is not installed still makes them throw, but nothing the found
file imports is needed, so a caller walking what a program runs follows only the
references with `loads: true`.

A created require is recognised only when `createRequire` itself comes from
`module` or `node:module`. A helper of that name from anywhere else returns
whatever that helper returns, and reading its calls as module loads would report
a dependency nobody has.
187 changes: 179 additions & 8 deletions packages/module-specifiers/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,13 @@ describe("whether a reference survives to runtime", () => {

it("reports a plain import as reaching runtime", () => {
expect(refs(`import a from "pkg";`)).toEqual([
{ specifier: "pkg", typeOnly: false },
{ specifier: "pkg", typeOnly: false, resolution: "esm", loads: true },
]);
});

it("reports a bare side-effect import as reaching runtime", () => {
expect(refs(`import "pkg";`)).toEqual([
{ specifier: "pkg", typeOnly: false },
{ specifier: "pkg", typeOnly: false, resolution: "esm", loads: true },
]);
});

Expand All @@ -191,25 +191,25 @@ describe("whether a reference survives to runtime", () => {
// governing the whole clause erases a real runtime edge, which is the direction that answers
// "clean" for a bundle guard.
expect(refs(`import { a, type B } from "pkg";`)).toEqual([
{ specifier: "pkg", typeOnly: false },
{ specifier: "pkg", typeOnly: false, resolution: "esm", loads: true },
]);
});

it("reports a dynamic import as reaching runtime", () => {
expect(refs(`const f = () => import("pkg");`)).toEqual([
{ specifier: "pkg", typeOnly: false },
{ specifier: "pkg", typeOnly: false, resolution: "esm", loads: true },
]);
});

it("reports require as reaching runtime", () => {
expect(refs(`const a = require("pkg");`)).toEqual([
{ specifier: "pkg", typeOnly: false },
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: true },
]);
});

it("reports an import-equals as reaching runtime", () => {
expect(refs(`import a = require("pkg");`)).toEqual([
{ specifier: "pkg", typeOnly: false },
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: true },
]);
});

Expand Down Expand Up @@ -237,7 +237,12 @@ describe("whether a reference survives to runtime", () => {
it("keeps an unreadable target unreadable, and at runtime", () => {
// An unresolvable target has to stay a violation for both consumers.
expect(refs(`const a = require(name);`)).toEqual([
{ specifier: UNRESOLVABLE_SPECIFIER, typeOnly: false },
{
specifier: UNRESOLVABLE_SPECIFIER,
typeOnly: false,
resolution: "cjs",
loads: true,
},
]);
});

Expand Down Expand Up @@ -271,7 +276,9 @@ describe("module.require", () => {
).toEqual(["pkg"]);
expect(
moduleSpecifierRefs(`const a = module.require("pkg");`, "m.ts")
).toEqual([{ specifier: "pkg", typeOnly: false }]);
).toEqual([
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: true },
]);
});

it("reads the bracket spelling the same way", () => {
Expand Down Expand Up @@ -344,3 +351,167 @@ describe("module.require through wrappers and shadows", () => {
expect(importedSpecifiers(source, "m.ts")).toEqual(["pkg"]);
});
});

/**
* Which of Node's resolvers finds a reference that reaches runtime. A caller following a
* relative specifier needs it: `./lib` is `lib.js` to a require and nothing to an import.
*/
describe("the resolver that finds a runtime reference", () => {
it.each([
[`import a from "pkg";`, "esm"],
[`export { a } from "pkg";`, "esm"],
[`import "pkg";`, "esm"],
[`const f = () => import("pkg");`, "esm"],
[`const a = require("pkg");`, "cjs"],
[`const a = module.require("pkg");`, "cjs"],
[`import a = require("pkg");`, "cjs"],
[`const a = (require)("pkg");`, "cjs"],
])("finds %s with the %s resolver", (text, resolution) => {
expect(moduleSpecifierRefs(text, "module.ts")).toEqual([
{ specifier: "pkg", typeOnly: false, resolution, loads: true },
]);
});
});

/**
* `const load = createRequire(import.meta.url)` is how an ES module reaches CommonJS, and
* `load("pkg")` then loads a package exactly as `require` does. A reader that knows only the
* name `require` reports such a file as loading nothing.
*/
describe("a require function createRequire returned", () => {
it.each([
`import { createRequire } from "node:module";\nconst load = createRequire(import.meta.url);\nload("pkg");`,
`import { createRequire as make } from "module";\nconst load = make(import.meta.url);\nload("pkg");`,
`import * as nodeModule from "node:module";\nconst load = nodeModule.createRequire(import.meta.url);\nload("pkg");`,
`import nodeModule from "node:module";\nconst load = nodeModule["createRequire"](import.meta.url);\nload("pkg");`,
])("reads its call as a CommonJS load: %s", text => {
expect(
moduleSpecifierRefs(text, "module.mjs").filter(
ref => ref.specifier === "pkg"
)
).toEqual([
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: true },
]);
});

it("reads a createRequire destructured from require", () => {
const text = `const { createRequire } = require("node:module");\nconst load = createRequire(__filename);\nload("pkg");`;

expect(moduleSpecifierRefs(text, "module.cjs")).toEqual([
{
specifier: "node:module",
typeOnly: false,
resolution: "cjs",
loads: true,
},
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: true },
]);
});

it("keeps an unreadable target unreadable", () => {
const text = `import { createRequire } from "node:module";\nconst load = createRequire(import.meta.url);\nload(name);`;

expect(importedSpecifiers(text, "module.mjs")).toEqual([
"node:module",
UNRESOLVABLE_SPECIFIER,
]);
});

it("does not read a helper of the same name from anywhere else", () => {
// 🔴 The negative control. A `createRequire` from a local module returns whatever that helper
// returns, and reading its calls as module loads reports a dependency nobody has.
const text = `import { createRequire } from "./helpers.mjs";\nconst load = createRequire(import.meta.url);\nload("pkg");`;

expect(importedSpecifiers(text, "module.mjs")).toEqual(["./helpers.mjs"]);
});
});

/**
* `createRequire(import.meta.url)("pkg")` never binds the require function to a name, and this
* repository loads drizzle-kit exactly that way. A reader that knows only named require functions
* reports such a file as loading nothing.
*/
describe("a created require called where it is made", () => {
it.each([
`import { createRequire } from "node:module";\nexport const load = () => createRequire(import.meta.url)("pkg");`,
`import * as nodeModule from "node:module";\nnodeModule.createRequire(import.meta.url)("pkg");`,
`import { createRequire } from "node:module";\n(createRequire(import.meta.url))("pkg");`,
])("reads its call as a CommonJS load: %s", text => {
expect(
moduleSpecifierRefs(text, "module.mjs").filter(
ref => ref.specifier === "pkg"
)
).toEqual([
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: true },
]);
});

it("reads resolve on it as finding the module without running it", () => {
const text = `import { createRequire } from "node:module";\nconst where = createRequire(import.meta.url).resolve("pkg");`;

expect(
moduleSpecifierRefs(text, "module.mjs").filter(
ref => ref.specifier === "pkg"
)
).toEqual([
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: false },
]);
});

it("does not read a helper of the same name from anywhere else", () => {
// 🔴 The negative control, as for a created require bound to a name.
const text = `import { createRequire } from "./helpers.mjs";\ncreateRequire(import.meta.url)("pkg");`;

expect(importedSpecifiers(text, "module.mjs")).toEqual(["./helpers.mjs"]);
});
});

/**
* Resolving a package that is not installed fails exactly as loading it does, but the file a
* resolve finds never runs.
*/
describe("calls that find a module without loading it", () => {
it.each([
[`const p = require.resolve("pkg");`, "cjs"],
[`const p = require["resolve"]("pkg");`, "cjs"],
[`const p = import.meta.resolve("pkg");`, "esm"],
])("reads %s with the %s resolver", (text, resolution) => {
expect(moduleSpecifierRefs(text, "module.mjs")).toEqual([
{ specifier: "pkg", typeOnly: false, resolution, loads: false },
]);
});

it("reads resolve on a created require", () => {
const text = `import { createRequire } from "node:module";\nconst load = createRequire(import.meta.url);\nconst p = load.resolve("pkg");`;

expect(moduleSpecifierRefs(text, "module.mjs")).toEqual([
{
specifier: "node:module",
typeOnly: false,
resolution: "esm",
loads: true,
},
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: false },
]);
});

it("tells a resolve of a module from a load of the same module", () => {
const text = `const where = require.resolve("pkg");\nconst loaded = require("pkg");`;

expect(moduleSpecifierRefs(text, "module.cjs")).toEqual([
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: false },
{ specifier: "pkg", typeOnly: false, resolution: "cjs", loads: true },
]);
});

it("does not read resolve on any other object", () => {
// 🔴 The negative control. `Promise.resolve` and a router's `resolve` belong to somebody else's
// object, and reading every `.resolve` as a module resolve reports ordinary code.
expect(
importedSpecifiers(
`const a = Promise.resolve("pkg");\nconst b = router.resolve("pkg");`,
"module.mjs"
)
).toEqual([]);
});
});
Loading
Loading