@pob/rollup 8.2.0 → 8.4.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 CHANGED
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs";
2
+ import { createRequire } from "node:module";
2
3
  import path from "node:path";
4
+ import { setTimeout } from "node:timers/promises";
3
5
 
4
6
  export const nodeFormatToExt = (format, pkgType) => {
5
7
  if (format === "cjs" && pkgType === "module") return ".cjs";
@@ -31,3 +33,140 @@ export const resolveEntry = (cwd, entryName) => {
31
33
 
32
34
  return entryPath;
33
35
  };
36
+
37
+ const sourceExtensionRegExp = /\.[cm]?tsx?$/;
38
+
39
+ const readClosestPackageJson = (filePath) => {
40
+ let directory = path.dirname(filePath);
41
+ while (directory !== path.dirname(directory)) {
42
+ const pkgPath = path.join(directory, "package.json");
43
+ if (fs.existsSync(pkgPath)) {
44
+ return JSON.parse(fs.readFileSync(pkgPath));
45
+ }
46
+ directory = path.dirname(directory);
47
+ }
48
+ return {};
49
+ };
50
+
51
+ /**
52
+ * Workspace dependencies whose entry is a typescript source, like the
53
+ * "untranspiled-library" apps of pob. They must be bundled: node does not strip
54
+ * types under node_modules, so the build could not run outside the monorepo.
55
+ */
56
+ export const createSourceWorkspaceDependencies = (cwd, pkg) => {
57
+ const require = createRequire(path.join(cwd, "package.json"));
58
+ const resolve = (source) => {
59
+ try {
60
+ return fs.realpathSync(require.resolve(source));
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ };
65
+
66
+ const dependencies = Object.entries(pkg.dependencies || {})
67
+ .filter(([, version]) => version.startsWith("workspace:"))
68
+ .flatMap(([name]) => {
69
+ const entryPath = resolve(name);
70
+ if (!entryPath || !sourceExtensionRegExp.test(entryPath)) return [];
71
+ return [{ name, pkg: readClosestPackageJson(entryPath) }];
72
+ });
73
+
74
+ const isSourceWorkspaceDependency = (id) =>
75
+ dependencies.some(({ name }) => id === name || id.startsWith(`${name}/`));
76
+
77
+ return {
78
+ isSourceWorkspaceDependency,
79
+ // their own dependencies stay external
80
+ pkgs: dependencies.map((dependency) => dependency.pkg),
81
+ plugin: {
82
+ name: "@pob/rollup/source-workspace-dependencies",
83
+ resolveId(source) {
84
+ if (!isSourceWorkspaceDependency(source)) return null;
85
+ return resolve(source) ?? null;
86
+ },
87
+ },
88
+ };
89
+ };
90
+
91
+ const nodeImportConditions = new Set(["node", "import", "default"]);
92
+
93
+ const resolveExportTarget = (target) => {
94
+ if (typeof target === "string") return target;
95
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
96
+ return undefined;
97
+ }
98
+ for (const [condition, value] of Object.entries(target)) {
99
+ if (nodeImportConditions.has(condition)) {
100
+ const resolved = resolveExportTarget(value);
101
+ if (resolved) return resolved;
102
+ }
103
+ }
104
+ return undefined;
105
+ };
106
+
107
+ /** the file node imports for the package's main entry */
108
+ const getMainEntry = (pkg) => {
109
+ const { exports } = pkg;
110
+ if (exports === undefined) return pkg.module ?? pkg.main;
111
+ if (typeof exports === "string") return exports;
112
+ const isSubpathMap = Object.keys(exports).some((key) => key.startsWith("."));
113
+ return resolveExportTarget(isSubpathMap ? exports["."] : exports);
114
+ };
115
+
116
+ const dependencyPollIntervalMs = 100;
117
+ const dependencySlowAfterMs = 5000;
118
+
119
+ const waitForFile = async (file, onSlow) => {
120
+ let waitedMs = 0;
121
+ while (!fs.existsSync(file)) {
122
+ if (waitedMs === dependencySlowAfterMs) onSlow();
123
+ await setTimeout(dependencyPollIntervalMs);
124
+ waitedMs += dependencyPollIntervalMs;
125
+ }
126
+ };
127
+
128
+ const findDependencyPackageJson = (cwd, name) => {
129
+ let directory = cwd;
130
+ while (true) {
131
+ const pkgPath = path.join(directory, "node_modules", name, "package.json");
132
+ if (fs.existsSync(pkgPath)) return fs.realpathSync(pkgPath);
133
+ if (directory === path.dirname(directory)) return undefined;
134
+ directory = path.dirname(directory);
135
+ }
136
+ };
137
+
138
+ /**
139
+ * Workspace dependencies imported from their build stay external, and node
140
+ * loads them once: the process started by the run plugin would keep running a
141
+ * stale build of them. Watching their entry rebuilds, and so restarts it, each
142
+ * time one of them is rebuilt. A watched file that gets deleted, by the
143
+ * `clean:build` of its package, is no longer watched once recreated: the
144
+ * rebuild its deletion triggers waits for it to come back, and watches the new
145
+ * one.
146
+ */
147
+ export const createWatchWorkspaceDependencies = (cwd, pkg) => {
148
+ const builtEntries = Object.entries(pkg.dependencies || {})
149
+ .filter(([, version]) => version.startsWith("workspace:"))
150
+ .flatMap(([name]) => {
151
+ const dependencyPkgPath = findDependencyPackageJson(cwd, name);
152
+ if (!dependencyPkgPath) return [];
153
+ const entry = getMainEntry(
154
+ JSON.parse(fs.readFileSync(dependencyPkgPath)),
155
+ );
156
+ if (!entry || sourceExtensionRegExp.test(entry)) return [];
157
+ return [path.resolve(path.dirname(dependencyPkgPath), entry)];
158
+ });
159
+
160
+ return {
161
+ name: "@pob/rollup/watch-workspace-dependencies",
162
+ async buildStart() {
163
+ if (!this.meta.watchMode) return;
164
+ for (const entry of builtEntries) {
165
+ await waitForFile(entry, () => {
166
+ this.warn(`Waiting for ${entry} to be built`);
167
+ });
168
+ this.addWatchFile(entry);
169
+ }
170
+ },
171
+ };
172
+ };
@@ -0,0 +1,111 @@
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 { createWatchWorkspaceDependencies } from "./index.js";
6
+
7
+ const writeJson = (filePath, value) => {
8
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
9
+ fs.writeFileSync(filePath, JSON.stringify(value));
10
+ };
11
+
12
+ describe("createWatchWorkspaceDependencies", () => {
13
+ let root;
14
+ let appDirectory;
15
+
16
+ /** a workspace package, linked in the root node_modules as pnpm hoists it */
17
+ const addWorkspacePackage = (name, pkg) => {
18
+ const directory = path.join(root, "packages", name);
19
+ writeJson(path.join(directory, "package.json"), { name, ...pkg });
20
+ fs.mkdirSync(path.join(root, "node_modules"), { recursive: true });
21
+ fs.symlinkSync(directory, path.join(root, "node_modules", name), "dir");
22
+ return fs.realpathSync(directory);
23
+ };
24
+
25
+ const watchedFiles = async (pkg, { watchMode = true } = {}) => {
26
+ const addWatchFile = vi.fn();
27
+ await createWatchWorkspaceDependencies(appDirectory, pkg).buildStart.call({
28
+ meta: { watchMode },
29
+ addWatchFile,
30
+ warn: vi.fn(),
31
+ });
32
+ return addWatchFile.mock.calls.map(([file]) => file);
33
+ };
34
+
35
+ beforeEach(() => {
36
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "pob-rollup-"));
37
+ appDirectory = path.join(root, "packages", "app");
38
+ fs.mkdirSync(appDirectory, { recursive: true });
39
+ });
40
+
41
+ afterEach(() => {
42
+ fs.rmSync(root, { recursive: true, force: true });
43
+ });
44
+
45
+ it("watches the build node imports of each workspace dependency", async () => {
46
+ const core = addWorkspacePackage("core", {
47
+ exports: {
48
+ "./package.json": "./package.json",
49
+ ".": {
50
+ types: "./src/index.ts",
51
+ node: { import: "./build/index-node.mjs" },
52
+ },
53
+ },
54
+ });
55
+ const legacy = addWorkspacePackage("legacy", { main: "./dist/index.js" });
56
+ const builtFiles = [
57
+ path.join(core, "build/index-node.mjs"),
58
+ path.join(legacy, "dist/index.js"),
59
+ ];
60
+ builtFiles.forEach((builtFile) => {
61
+ fs.mkdirSync(path.dirname(builtFile));
62
+ fs.writeFileSync(builtFile, "export {};");
63
+ });
64
+
65
+ expect(
66
+ await watchedFiles({
67
+ dependencies: { core: "workspace:*", legacy: "workspace:^" },
68
+ }),
69
+ ).toEqual(builtFiles);
70
+ });
71
+
72
+ it("leaves out sources, registry dependencies and unknown packages", async () => {
73
+ addWorkspacePackage("untranspiled", { exports: "./src/index.ts" });
74
+ addWorkspacePackage("published", { main: "./index.js" });
75
+
76
+ expect(
77
+ await watchedFiles({
78
+ dependencies: {
79
+ untranspiled: "workspace:*",
80
+ published: "1.0.0",
81
+ missing: "workspace:*",
82
+ },
83
+ }),
84
+ ).toEqual([]);
85
+ });
86
+
87
+ it("watches nothing outside watch mode", async () => {
88
+ addWorkspacePackage("core", { main: "./build/index.js" });
89
+
90
+ expect(
91
+ await watchedFiles(
92
+ { dependencies: { core: "workspace:*" } },
93
+ { watchMode: false },
94
+ ),
95
+ ).toEqual([]);
96
+ });
97
+
98
+ it("waits for a dependency being rebuilt before watching it", async () => {
99
+ const core = addWorkspacePackage("core", { main: "./build/index.js" });
100
+ const builtFile = path.join(core, "build/index.js");
101
+ setTimeout(() => {
102
+ fs.mkdirSync(path.dirname(builtFile));
103
+ fs.writeFileSync(builtFile, "export {};");
104
+ }, 150);
105
+
106
+ expect(
107
+ await watchedFiles({ dependencies: { core: "workspace:*" } }),
108
+ ).toEqual([builtFile]);
109
+ expect(fs.existsSync(builtFile)).toBe(true);
110
+ });
111
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pob/rollup",
3
- "version": "8.2.0",
3
+ "version": "8.4.0",
4
4
  "description": "rollup configuration for pob",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/christophehurpeau/pob",