mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +00:00
fix(plugins): treat Node type declarations as host types in the type walk
Dependency declarations that `import "node"` (e.g. utif2) pulled @types/node into the install-time type walk, which failed on node: specifiers newer than the host Node (node:stream/iter on Node 24) or, with @types/node absent, on the unresolved "node" import. Map bare "node" in declarations to @types/node and treat any node: specifier as a builtin.
This commit is contained in:
@@ -386,6 +386,46 @@ export type Value = string;`,
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "import 'node' with Node types installed", source: 'import "node";', nodeTypes: true },
|
||||
{ name: "import 'node' without Node types", source: 'import "node";', nodeTypes: false },
|
||||
{
|
||||
name: "a node: builtin unknown to the host",
|
||||
source: 'import type { Future } from "node:paseo-future-builtin";',
|
||||
nodeTypes: false,
|
||||
},
|
||||
])("treats Node declarations as host types in server dependencies: $name", async (fixture) => {
|
||||
const entries = await createSplitPlugin();
|
||||
const dependency = path.join(entries.directory, "node_modules/image-codec");
|
||||
await mkdir(dependency, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(dependency, "package.json"),
|
||||
JSON.stringify({ name: "image-codec", main: "index.js", types: "index.d.ts" }),
|
||||
);
|
||||
await writeFile(path.join(dependency, "index.js"), 'module.exports = "decoded";');
|
||||
await writeFile(
|
||||
path.join(dependency, "index.d.ts"),
|
||||
`${fixture.source}\ndeclare const value: string;\nexport = value;`,
|
||||
);
|
||||
if (fixture.nodeTypes) {
|
||||
const nodeTypes = path.join(entries.directory, "node_modules/@types/node");
|
||||
await mkdir(nodeTypes, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(nodeTypes, "package.json"),
|
||||
JSON.stringify({ name: "@types/node", types: "index.d.ts" }),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(nodeTypes, "index.d.ts"),
|
||||
'declare module "node:paseo-future-builtin" { export * from "node:stream"; }\nimport type { Future } from "node:paseo-future-builtin";',
|
||||
);
|
||||
}
|
||||
await writeFile(entries.server, 'import value from "image-codec"; export default value;');
|
||||
|
||||
await expect(compilePlugin({ client: null, server: entries.server })).resolves.toMatchObject({
|
||||
serverBundle: expect.stringContaining("decoded"),
|
||||
});
|
||||
});
|
||||
|
||||
it("checks the physical owner of type-only symlinks", async () => {
|
||||
const entries = await createSplitPlugin();
|
||||
await writeFile(
|
||||
|
||||
@@ -92,6 +92,18 @@ function containsPath(directory: string, filePath: string): boolean {
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
// @types/node can declare builtins newer than the host Node (node:stream/iter on Node 24).
|
||||
function isNodeSpecifier(specifier: string): boolean {
|
||||
return isBuiltin(specifier) || specifier.startsWith("node:") || specifier === "@types/node";
|
||||
}
|
||||
|
||||
function declaredPackage(specifier: string, kind: PluginImportKind, typeOnly: boolean): string {
|
||||
if (kind === "type-reference") return `@types/${specifier.replace(/^@/, "").replace("/", "__")}`;
|
||||
// Declarations load Node's ambient types with `import "node"`.
|
||||
if (typeOnly && specifier === "node") return "@types/node";
|
||||
return specifier;
|
||||
}
|
||||
|
||||
function dependencyName(specifier: string): string {
|
||||
const segments = specifier.split("/");
|
||||
return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
|
||||
@@ -236,10 +248,7 @@ function createRuntimeBoundaryPlugin(target: PluginBuildTarget, pluginDirectory:
|
||||
if (checked.has(key) || !/\.[cm]?[jt]sx?$/.test(file)) return null;
|
||||
checked.add(key);
|
||||
for (const { specifier, kind, typeOnly } of imports.read(file)) {
|
||||
const packageSpecifier =
|
||||
kind === "type-reference"
|
||||
? `@types/${specifier.replace(/^@/, "").replace("/", "__")}`
|
||||
: specifier;
|
||||
const packageSpecifier = declaredPackage(specifier, kind, typeOnly);
|
||||
const error =
|
||||
runtimeSpecifierError(specifier, owner, file) ??
|
||||
runtimeSpecifierError(packageSpecifier, owner, file) ??
|
||||
@@ -253,7 +262,7 @@ function createRuntimeBoundaryPlugin(target: PluginBuildTarget, pluginDirectory:
|
||||
if (
|
||||
(PLUGIN_SDK_SPECIFIERS as readonly string[]).includes(specifier) ||
|
||||
/^(zod|react|react-native|@tanstack\/react-query)(\/|$)/.test(specifier) ||
|
||||
isBuiltin(specifier) ||
|
||||
isNodeSpecifier(specifier) ||
|
||||
packageSpecifier === "@types/node"
|
||||
)
|
||||
continue;
|
||||
@@ -328,8 +337,7 @@ function runtimeSpecifierError(
|
||||
!(PLUGIN_SDK_SPECIFIERS as readonly string[]).includes(specifier)
|
||||
)
|
||||
kind = "Unknown SDK";
|
||||
else if (target !== "server" && (isBuiltin(specifier) || specifier === "@types/node"))
|
||||
kind = "Node";
|
||||
else if (target !== "server" && isNodeSpecifier(specifier)) kind = "Node";
|
||||
else if (target !== "server" && isPluginServerOnlySdkSpecifier(specifier)) kind = "server-only";
|
||||
else if (
|
||||
target !== "client" &&
|
||||
|
||||
Reference in New Issue
Block a user