@pob/rollup 8.3.0 → 8.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +113 -9
- package/lib/index.test.js +186 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
5
|
|
|
5
6
|
export const nodeFormatToExt = (format, pkgType) => {
|
|
6
7
|
if (format === "cjs" && pkgType === "module") return ".cjs";
|
|
@@ -62,27 +63,130 @@ export const createSourceWorkspaceDependencies = (cwd, pkg) => {
|
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
65
|
|
|
65
|
-
const
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
const findSourceDependencies = (dependencies = {}) =>
|
|
67
|
+
Object.entries(dependencies)
|
|
68
|
+
.filter(([, version]) => version.startsWith("workspace:"))
|
|
69
|
+
.flatMap(([name]) => {
|
|
70
|
+
const entryPath = resolve(name);
|
|
71
|
+
if (!entryPath || !sourceExtensionRegExp.test(entryPath)) return [];
|
|
72
|
+
return [{ name, pkg: readClosestPackageJson(entryPath) }];
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const dependencies = findSourceDependencies(pkg.dependencies);
|
|
76
|
+
// bundled as well, so the build still works, but reported: a package the
|
|
77
|
+
// build imports belongs in dependencies
|
|
78
|
+
const devDependencies = findSourceDependencies(pkg.devDependencies).filter(
|
|
79
|
+
({ name }) => !pkg.dependencies?.[name],
|
|
80
|
+
);
|
|
81
|
+
const allDependencies = [...dependencies, ...devDependencies];
|
|
82
|
+
const warnedDevDependencies = new Set();
|
|
83
|
+
|
|
84
|
+
const findDependency = (list, id) =>
|
|
85
|
+
list.find(({ name }) => id === name || id.startsWith(`${name}/`));
|
|
72
86
|
|
|
73
87
|
const isSourceWorkspaceDependency = (id) =>
|
|
74
|
-
|
|
88
|
+
findDependency(allDependencies, id) !== undefined;
|
|
75
89
|
|
|
76
90
|
return {
|
|
77
91
|
isSourceWorkspaceDependency,
|
|
78
92
|
// their own dependencies stay external
|
|
79
|
-
pkgs:
|
|
93
|
+
pkgs: allDependencies.map((dependency) => dependency.pkg),
|
|
80
94
|
plugin: {
|
|
81
95
|
name: "@pob/rollup/source-workspace-dependencies",
|
|
82
96
|
resolveId(source) {
|
|
83
97
|
if (!isSourceWorkspaceDependency(source)) return null;
|
|
98
|
+
const devDependency = findDependency(devDependencies, source);
|
|
99
|
+
if (devDependency && !warnedDevDependencies.has(devDependency.name)) {
|
|
100
|
+
warnedDevDependencies.add(devDependency.name);
|
|
101
|
+
this.warn(
|
|
102
|
+
`"${devDependency.name}" is imported but listed in devDependencies: move it to dependencies.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
84
105
|
return resolve(source) ?? null;
|
|
85
106
|
},
|
|
86
107
|
},
|
|
87
108
|
};
|
|
88
109
|
};
|
|
110
|
+
|
|
111
|
+
const nodeImportConditions = new Set(["node", "import", "default"]);
|
|
112
|
+
|
|
113
|
+
const resolveExportTarget = (target) => {
|
|
114
|
+
if (typeof target === "string") return target;
|
|
115
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
for (const [condition, value] of Object.entries(target)) {
|
|
119
|
+
if (nodeImportConditions.has(condition)) {
|
|
120
|
+
const resolved = resolveExportTarget(value);
|
|
121
|
+
if (resolved) return resolved;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** the file node imports for the package's main entry */
|
|
128
|
+
const getMainEntry = (pkg) => {
|
|
129
|
+
const { exports } = pkg;
|
|
130
|
+
if (exports === undefined) return pkg.module ?? pkg.main;
|
|
131
|
+
if (typeof exports === "string") return exports;
|
|
132
|
+
const isSubpathMap = Object.keys(exports).some((key) => key.startsWith("."));
|
|
133
|
+
return resolveExportTarget(isSubpathMap ? exports["."] : exports);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const dependencyPollIntervalMs = 100;
|
|
137
|
+
const dependencySlowAfterMs = 5000;
|
|
138
|
+
|
|
139
|
+
const waitForFile = async (file, onSlow) => {
|
|
140
|
+
let waitedMs = 0;
|
|
141
|
+
while (!fs.existsSync(file)) {
|
|
142
|
+
if (waitedMs === dependencySlowAfterMs) onSlow();
|
|
143
|
+
await setTimeout(dependencyPollIntervalMs);
|
|
144
|
+
waitedMs += dependencyPollIntervalMs;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const findDependencyPackageJson = (cwd, name) => {
|
|
149
|
+
let directory = cwd;
|
|
150
|
+
while (true) {
|
|
151
|
+
const pkgPath = path.join(directory, "node_modules", name, "package.json");
|
|
152
|
+
if (fs.existsSync(pkgPath)) return fs.realpathSync(pkgPath);
|
|
153
|
+
if (directory === path.dirname(directory)) return undefined;
|
|
154
|
+
directory = path.dirname(directory);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Workspace dependencies imported from their build stay external, and node
|
|
160
|
+
* loads them once: the process started by the run plugin would keep running a
|
|
161
|
+
* stale build of them. Watching their entry rebuilds, and so restarts it, each
|
|
162
|
+
* time one of them is rebuilt. A watched file that gets deleted, by the
|
|
163
|
+
* `clean:build` of its package, is no longer watched once recreated: the
|
|
164
|
+
* rebuild its deletion triggers waits for it to come back, and watches the new
|
|
165
|
+
* one.
|
|
166
|
+
*/
|
|
167
|
+
export const createWatchWorkspaceDependencies = (cwd, pkg) => {
|
|
168
|
+
const builtEntries = Object.entries(pkg.dependencies || {})
|
|
169
|
+
.filter(([, version]) => version.startsWith("workspace:"))
|
|
170
|
+
.flatMap(([name]) => {
|
|
171
|
+
const dependencyPkgPath = findDependencyPackageJson(cwd, name);
|
|
172
|
+
if (!dependencyPkgPath) return [];
|
|
173
|
+
const entry = getMainEntry(
|
|
174
|
+
JSON.parse(fs.readFileSync(dependencyPkgPath)),
|
|
175
|
+
);
|
|
176
|
+
if (!entry || sourceExtensionRegExp.test(entry)) return [];
|
|
177
|
+
return [path.resolve(path.dirname(dependencyPkgPath), entry)];
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
name: "@pob/rollup/watch-workspace-dependencies",
|
|
182
|
+
async buildStart() {
|
|
183
|
+
if (!this.meta.watchMode) return;
|
|
184
|
+
for (const entry of builtEntries) {
|
|
185
|
+
await waitForFile(entry, () => {
|
|
186
|
+
this.warn(`Waiting for ${entry} to be built`);
|
|
187
|
+
});
|
|
188
|
+
this.addWatchFile(entry);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
createSourceWorkspaceDependencies,
|
|
7
|
+
createWatchWorkspaceDependencies,
|
|
8
|
+
} from "./index.js";
|
|
9
|
+
|
|
10
|
+
const writeJson = (filePath, value) => {
|
|
11
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
12
|
+
fs.writeFileSync(filePath, JSON.stringify(value));
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("createSourceWorkspaceDependencies", () => {
|
|
16
|
+
let root;
|
|
17
|
+
let appDirectory;
|
|
18
|
+
|
|
19
|
+
const addWorkspacePackage = (name, pkg) => {
|
|
20
|
+
const directory = path.join(root, "packages", name);
|
|
21
|
+
writeJson(path.join(directory, "package.json"), { name, ...pkg });
|
|
22
|
+
fs.mkdirSync(path.join(directory, "src"), { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(directory, "src/index.ts"), "export {};");
|
|
24
|
+
fs.mkdirSync(path.join(root, "node_modules"), { recursive: true });
|
|
25
|
+
fs.symlinkSync(directory, path.join(root, "node_modules", name), "dir");
|
|
26
|
+
return fs.realpathSync(directory);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const resolveImports = (pkg, sources) => {
|
|
30
|
+
const warn = vi.fn();
|
|
31
|
+
const { plugin } = createSourceWorkspaceDependencies(appDirectory, pkg);
|
|
32
|
+
const resolved = sources.map((source) =>
|
|
33
|
+
plugin.resolveId.call({ warn }, source),
|
|
34
|
+
);
|
|
35
|
+
return { resolved, warnings: warn.mock.calls.map(([message]) => message) };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "pob-rollup-"));
|
|
40
|
+
appDirectory = path.join(root, "packages", "app");
|
|
41
|
+
fs.mkdirSync(appDirectory, { recursive: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("resolves a source dependency to its source, without warning", () => {
|
|
49
|
+
const shared = addWorkspacePackage("shared", { exports: "./src/index.ts" });
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
resolveImports({ dependencies: { shared: "workspace:*" } }, ["shared"]),
|
|
53
|
+
).toEqual({
|
|
54
|
+
resolved: [path.join(shared, "src/index.ts")],
|
|
55
|
+
warnings: [],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("resolves a source devDependency, warning once to move it", () => {
|
|
60
|
+
const shared = addWorkspacePackage("shared", { exports: "./src/index.ts" });
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
resolveImports({ devDependencies: { shared: "workspace:*" } }, [
|
|
64
|
+
"shared",
|
|
65
|
+
"shared",
|
|
66
|
+
]),
|
|
67
|
+
).toEqual({
|
|
68
|
+
resolved: [
|
|
69
|
+
path.join(shared, "src/index.ts"),
|
|
70
|
+
path.join(shared, "src/index.ts"),
|
|
71
|
+
],
|
|
72
|
+
warnings: [
|
|
73
|
+
'"shared" is imported but listed in devDependencies: move it to dependencies.',
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("leaves other imports to the other resolvers", () => {
|
|
79
|
+
addWorkspacePackage("shared", { exports: "./src/index.ts" });
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
resolveImports({ dependencies: { shared: "workspace:*" } }, ["other"]),
|
|
83
|
+
).toEqual({ resolved: [null], warnings: [] });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("createWatchWorkspaceDependencies", () => {
|
|
88
|
+
let root;
|
|
89
|
+
let appDirectory;
|
|
90
|
+
|
|
91
|
+
/** a workspace package, linked in the root node_modules as pnpm hoists it */
|
|
92
|
+
const addWorkspacePackage = (name, pkg) => {
|
|
93
|
+
const directory = path.join(root, "packages", name);
|
|
94
|
+
writeJson(path.join(directory, "package.json"), { name, ...pkg });
|
|
95
|
+
fs.mkdirSync(path.join(root, "node_modules"), { recursive: true });
|
|
96
|
+
fs.symlinkSync(directory, path.join(root, "node_modules", name), "dir");
|
|
97
|
+
return fs.realpathSync(directory);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const watchedFiles = async (pkg, { watchMode = true } = {}) => {
|
|
101
|
+
const addWatchFile = vi.fn();
|
|
102
|
+
await createWatchWorkspaceDependencies(appDirectory, pkg).buildStart.call({
|
|
103
|
+
meta: { watchMode },
|
|
104
|
+
addWatchFile,
|
|
105
|
+
warn: vi.fn(),
|
|
106
|
+
});
|
|
107
|
+
return addWatchFile.mock.calls.map(([file]) => file);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "pob-rollup-"));
|
|
112
|
+
appDirectory = path.join(root, "packages", "app");
|
|
113
|
+
fs.mkdirSync(appDirectory, { recursive: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("watches the build node imports of each workspace dependency", async () => {
|
|
121
|
+
const core = addWorkspacePackage("core", {
|
|
122
|
+
exports: {
|
|
123
|
+
"./package.json": "./package.json",
|
|
124
|
+
".": {
|
|
125
|
+
types: "./src/index.ts",
|
|
126
|
+
node: { import: "./build/index-node.mjs" },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const legacy = addWorkspacePackage("legacy", { main: "./dist/index.js" });
|
|
131
|
+
const builtFiles = [
|
|
132
|
+
path.join(core, "build/index-node.mjs"),
|
|
133
|
+
path.join(legacy, "dist/index.js"),
|
|
134
|
+
];
|
|
135
|
+
builtFiles.forEach((builtFile) => {
|
|
136
|
+
fs.mkdirSync(path.dirname(builtFile));
|
|
137
|
+
fs.writeFileSync(builtFile, "export {};");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(
|
|
141
|
+
await watchedFiles({
|
|
142
|
+
dependencies: { core: "workspace:*", legacy: "workspace:^" },
|
|
143
|
+
}),
|
|
144
|
+
).toEqual(builtFiles);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("leaves out sources, registry dependencies and unknown packages", async () => {
|
|
148
|
+
addWorkspacePackage("untranspiled", { exports: "./src/index.ts" });
|
|
149
|
+
addWorkspacePackage("published", { main: "./index.js" });
|
|
150
|
+
|
|
151
|
+
expect(
|
|
152
|
+
await watchedFiles({
|
|
153
|
+
dependencies: {
|
|
154
|
+
untranspiled: "workspace:*",
|
|
155
|
+
published: "1.0.0",
|
|
156
|
+
missing: "workspace:*",
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("watches nothing outside watch mode", async () => {
|
|
163
|
+
addWorkspacePackage("core", { main: "./build/index.js" });
|
|
164
|
+
|
|
165
|
+
expect(
|
|
166
|
+
await watchedFiles(
|
|
167
|
+
{ dependencies: { core: "workspace:*" } },
|
|
168
|
+
{ watchMode: false },
|
|
169
|
+
),
|
|
170
|
+
).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("waits for a dependency being rebuilt before watching it", async () => {
|
|
174
|
+
const core = addWorkspacePackage("core", { main: "./build/index.js" });
|
|
175
|
+
const builtFile = path.join(core, "build/index.js");
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
fs.mkdirSync(path.dirname(builtFile));
|
|
178
|
+
fs.writeFileSync(builtFile, "export {};");
|
|
179
|
+
}, 150);
|
|
180
|
+
|
|
181
|
+
expect(
|
|
182
|
+
await watchedFiles({ dependencies: { core: "workspace:*" } }),
|
|
183
|
+
).toEqual([builtFile]);
|
|
184
|
+
expect(fs.existsSync(builtFile)).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|