@pantoken/vite-workspace-orchestrator 0.1.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/README.md +51 -0
- package/dist/index.d.mts +105 -0
- package/dist/index.mjs +202 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @pantoken/vite-workspace-orchestrator
|
|
2
|
+
|
|
3
|
+
A Vite dev-server plugin that watches upstream workspace packages and rebuilds them (and their
|
|
4
|
+
dependents) when source changes.
|
|
5
|
+
|
|
6
|
+
When an app depends on local workspace packages, changes to those packages normally need a manual
|
|
7
|
+
rebuild before the dev server reflects them. This plugin automates that: it watches each upstream
|
|
8
|
+
package's source, rebuilds in topological order, and registers the built output with Vite's watcher
|
|
9
|
+
so the browser reloads. It applies during `serve` only — production builds are untouched.
|
|
10
|
+
|
|
11
|
+
Key behaviors:
|
|
12
|
+
|
|
13
|
+
- Watches with native `fs.watch`, not Vite's chokidar, so paths outside the project root aren't
|
|
14
|
+
filtered out.
|
|
15
|
+
- Debounces rapid changes per package before spawning a build.
|
|
16
|
+
- Rebuilds dependents in topological order after a successful build.
|
|
17
|
+
- Registers optional HMR paths with Vite's watcher, and mounts optional static file servers.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { workspaceOrchestrator } from "@pantoken/vite-workspace-orchestrator";
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
plugins: [
|
|
27
|
+
workspaceOrchestrator({
|
|
28
|
+
upstream: [
|
|
29
|
+
{
|
|
30
|
+
name: "@pantoken/components",
|
|
31
|
+
dir: resolve(root, "formats/components"),
|
|
32
|
+
watchPaths: [resolve(root, "formats/components/src")],
|
|
33
|
+
build: ["pnpm", "exec", "vp", "run", "@pantoken/components#build"],
|
|
34
|
+
dependents: [],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
hmrWatchPaths: [resolve(root, "formats/components/generated")],
|
|
38
|
+
debounceMs: 200,
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
pantoken's docs site wires it in `docs/.vitepress/config.ts` so edits to `@pantoken/css` and
|
|
45
|
+
`@pantoken/components` rebuild live during `docs:dev` instead of only at the next full build.
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
- **`workspaceOrchestrator(options)`** — returns a Vite plugin (`apply: "serve"`).
|
|
50
|
+
- **`matchesFilters(filename, node)`** — the include/ignore predicate, exported for testing.
|
|
51
|
+
- **`OrchestratorOptions`**, **`UpstreamNode`**, **`FileServerEntry`** — the option types.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ViteDevServer } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Types for {@link workspaceOrchestrator}.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/** One upstream workspace package to watch and rebuild. */
|
|
10
|
+
interface UpstreamNode {
|
|
11
|
+
/** Display name for log messages. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Package root directory (the cwd for the build command). */
|
|
14
|
+
dir: string;
|
|
15
|
+
/** Paths (files or directories) to watch — directories are watched recursively. */
|
|
16
|
+
watchPaths: string[];
|
|
17
|
+
/** Build command: first element is the executable, the rest are arguments. */
|
|
18
|
+
build: readonly [string, ...string[]];
|
|
19
|
+
/** Names of other upstream nodes to rebuild after this one succeeds. */
|
|
20
|
+
dependents: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* Glob patterns for files to include. When set, only changes to files matching at least one
|
|
23
|
+
* pattern trigger a rebuild. Omit to include everything.
|
|
24
|
+
*/
|
|
25
|
+
include?: readonly string[];
|
|
26
|
+
/**
|
|
27
|
+
* Glob patterns for files to ignore. Changes to matching files are silently skipped. Omit to
|
|
28
|
+
* ignore nothing.
|
|
29
|
+
*/
|
|
30
|
+
ignore?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/** One static file-serving middleware entry. */
|
|
33
|
+
interface FileServerEntry {
|
|
34
|
+
/** URL path prefix to mount the middleware at, e.g. `"/styles/apps"`. */
|
|
35
|
+
mountPath: string;
|
|
36
|
+
/** Local directory to serve files from. */
|
|
37
|
+
serveDir: string;
|
|
38
|
+
/** Only serve files whose URL path ends with this extension, e.g. `".css"`. */
|
|
39
|
+
extension: string;
|
|
40
|
+
/** Value for the `Content-Type` response header. */
|
|
41
|
+
contentType: string;
|
|
42
|
+
/**
|
|
43
|
+
* Optional transform applied to the URL-relative path before resolving against `serveDir`, e.g.
|
|
44
|
+
* `(p) => p.replace(/\/([^/]+)\.css$/, "/$1/app.css")`.
|
|
45
|
+
*/
|
|
46
|
+
pathTransform?: (urlRelativePath: string) => string;
|
|
47
|
+
}
|
|
48
|
+
/** Options for {@link workspaceOrchestrator}. */
|
|
49
|
+
interface OrchestratorOptions {
|
|
50
|
+
/** The upstream workspace dependency graph. */
|
|
51
|
+
upstream: readonly UpstreamNode[];
|
|
52
|
+
/**
|
|
53
|
+
* Paths to add to Vite's `server.watcher` so HMR fires when built output is updated (e.g. a
|
|
54
|
+
* package's `dist` or `generated` directory).
|
|
55
|
+
*/
|
|
56
|
+
hmrWatchPaths?: readonly string[];
|
|
57
|
+
/**
|
|
58
|
+
* Paths to watch with native `fs.watch` for triggering module invalidation. Unlike
|
|
59
|
+
* `hmrWatchPaths` (which relies on chokidar), these are monitored with Node's native `fs.watch` —
|
|
60
|
+
* which reliably detects changes for pnpm-symlinked workspace `dist` directories. On a change, a
|
|
61
|
+
* synthetic `"change"` event is emitted on `server.watcher` so the module graph is invalidated.
|
|
62
|
+
*/
|
|
63
|
+
reloadWatchPaths?: readonly string[];
|
|
64
|
+
/** Debounce delay in milliseconds before triggering a rebuild (default: 200). */
|
|
65
|
+
debounceMs?: number;
|
|
66
|
+
/** Optional static file-serving middleware entries. */
|
|
67
|
+
fileServers?: readonly FileServerEntry[];
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/index.d.ts
|
|
71
|
+
/**
|
|
72
|
+
* Returns `true` if the changed filename passes the node's include/ignore filters.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { matchesFilters } from "@pantoken/vite-workspace-orchestrator";
|
|
77
|
+
*
|
|
78
|
+
* const node = {
|
|
79
|
+
* name: "@pantoken/components",
|
|
80
|
+
* dir: "/repo/formats/components",
|
|
81
|
+
* watchPaths: ["/repo/formats/components/src"],
|
|
82
|
+
* build: ["pnpm", "run", "build"] as const,
|
|
83
|
+
* dependents: [],
|
|
84
|
+
* include: ["**\/*.ts"],
|
|
85
|
+
* ignore: ["**\/*.test.ts"],
|
|
86
|
+
* };
|
|
87
|
+
*
|
|
88
|
+
* matchesFilters("src/index.ts", node); // true
|
|
89
|
+
* matchesFilters("src/index.test.ts", node); // false (ignored)
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare function matchesFilters(filename: string, node: UpstreamNode): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Create the Vite dev-server plugin.
|
|
95
|
+
*
|
|
96
|
+
* @param options - {@link OrchestratorOptions}.
|
|
97
|
+
* @returns A Vite plugin object (`apply: "serve"`).
|
|
98
|
+
*/
|
|
99
|
+
declare const workspaceOrchestrator: (options: OrchestratorOptions) => {
|
|
100
|
+
apply: "serve";
|
|
101
|
+
name: string;
|
|
102
|
+
configureServer(server: ViteDevServer): void;
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { type FileServerEntry, type OrchestratorOptions, type UpstreamNode, matchesFilters, workspaceOrchestrator };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readFileSync, watch } from "node:fs";
|
|
2
|
+
import { matchesGlob, resolve } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
//#region src/file-server.ts
|
|
5
|
+
/**
|
|
6
|
+
* Static file-serving middleware for {@link workspaceOrchestrator}.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Register static file-serving middleware for each entry. Only files whose URL ends with the
|
|
12
|
+
* configured extension are served; everything else falls through to the next handler.
|
|
13
|
+
*
|
|
14
|
+
* @param fileServers - The entries to mount.
|
|
15
|
+
* @param middlewares - The connect-style middleware stack (e.g. `server.middlewares`).
|
|
16
|
+
*/
|
|
17
|
+
function mountFileServers(fileServers, middlewares) {
|
|
18
|
+
for (const entry of fileServers) middlewares.use(entry.mountPath, (req, res, next) => {
|
|
19
|
+
const filePath = req.url?.split("?")[0];
|
|
20
|
+
if (!filePath?.endsWith(entry.extension)) {
|
|
21
|
+
next();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const resolved = entry.pathTransform ? entry.pathTransform(filePath) : filePath;
|
|
25
|
+
const fullPath = resolve(entry.serveDir, resolved.slice(1));
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(fullPath, "utf8");
|
|
28
|
+
res.setHeader("Content-Type", entry.contentType);
|
|
29
|
+
res.end(content);
|
|
30
|
+
} catch {
|
|
31
|
+
next();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/scheduler.ts
|
|
37
|
+
/**
|
|
38
|
+
* A debounced, dependency-aware build scheduler for {@link workspaceOrchestrator}.
|
|
39
|
+
*
|
|
40
|
+
* @module
|
|
41
|
+
*/
|
|
42
|
+
const SUCCESS_EXIT_CODE = 0;
|
|
43
|
+
/**
|
|
44
|
+
* Create a debounced build scheduler for the given upstream node map. The returned function
|
|
45
|
+
* debounces file-change events before spawning a build, re-queues a pending build if one is already
|
|
46
|
+
* running, and rebuilds dependents in topological order on success.
|
|
47
|
+
*
|
|
48
|
+
* @param nodeMap - Upstream nodes keyed by name.
|
|
49
|
+
* @param logger - The Vite logger (or any `info`/`error` sink).
|
|
50
|
+
* @param debounceMs - Debounce delay before a build starts.
|
|
51
|
+
* @returns A `scheduleRebuild(name)` function.
|
|
52
|
+
*/
|
|
53
|
+
function createScheduler(nodeMap, logger, debounceMs) {
|
|
54
|
+
const stateMap = new Map([...nodeMap.keys()].map((name) => [name, {
|
|
55
|
+
building: false,
|
|
56
|
+
pending: false,
|
|
57
|
+
debounce: null
|
|
58
|
+
}]));
|
|
59
|
+
const scheduler = {
|
|
60
|
+
scheduleRebuild(name) {
|
|
61
|
+
const s = stateMap.get(name);
|
|
62
|
+
if (!s) return;
|
|
63
|
+
if (s.debounce) clearTimeout(s.debounce);
|
|
64
|
+
s.debounce = setTimeout(() => {
|
|
65
|
+
s.debounce = null;
|
|
66
|
+
scheduler.runBuild(name);
|
|
67
|
+
}, debounceMs);
|
|
68
|
+
},
|
|
69
|
+
runBuild(name) {
|
|
70
|
+
const node = nodeMap.get(name);
|
|
71
|
+
const s = stateMap.get(name);
|
|
72
|
+
if (!node || !s) return;
|
|
73
|
+
if (s.building) {
|
|
74
|
+
s.pending = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
s.building = true;
|
|
78
|
+
logger.info(`\n[orchestrator] ${name} — building…`, { timestamp: true });
|
|
79
|
+
const [cmd, ...args] = node.build;
|
|
80
|
+
spawn(cmd, args, {
|
|
81
|
+
cwd: node.dir,
|
|
82
|
+
stdio: "inherit"
|
|
83
|
+
}).on("close", (code) => {
|
|
84
|
+
s.building = false;
|
|
85
|
+
if (code === SUCCESS_EXIT_CODE) {
|
|
86
|
+
logger.info(`[orchestrator] ${name} — done.`, { timestamp: true });
|
|
87
|
+
for (const dep of node.dependents) scheduler.scheduleRebuild(dep);
|
|
88
|
+
} else logger.error(`[orchestrator] ${name} — build failed (exit ${String(code)}).`, { timestamp: true });
|
|
89
|
+
if (s.pending) {
|
|
90
|
+
s.pending = false;
|
|
91
|
+
scheduler.runBuild(name);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return (name) => scheduler.scheduleRebuild(name);
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/index.ts
|
|
100
|
+
/**
|
|
101
|
+
* `@pantoken/vite-workspace-orchestrator` — a Vite dev-server plugin that watches upstream workspace
|
|
102
|
+
* packages and rebuilds them (and their dependents) when source changes.
|
|
103
|
+
*
|
|
104
|
+
* During development, changes to a local workspace package normally need a manual rebuild before the
|
|
105
|
+
* dev server reflects them. This plugin automates that: it watches each upstream package's source
|
|
106
|
+
* with native `fs.watch` (not Vite's chokidar, which filters paths outside the project root),
|
|
107
|
+
* debounces rapid changes, rebuilds in topological order, and registers built output with Vite's
|
|
108
|
+
* watcher so the browser reloads. In pantoken's docs it keeps the generated CSS (`@pantoken/css`,
|
|
109
|
+
* `@pantoken/components`) fresh as you edit the libraries, instead of only at build time.
|
|
110
|
+
*
|
|
111
|
+
* It applies during `serve` only, so production builds are untouched.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { resolve } from "node:path";
|
|
116
|
+
* import { workspaceOrchestrator } from "@pantoken/vite-workspace-orchestrator";
|
|
117
|
+
*
|
|
118
|
+
* workspaceOrchestrator({
|
|
119
|
+
* upstream: [
|
|
120
|
+
* {
|
|
121
|
+
* name: "@pantoken/components",
|
|
122
|
+
* dir: resolve(root, "formats/components"),
|
|
123
|
+
* watchPaths: [resolve(root, "formats/components/src")],
|
|
124
|
+
* build: ["pnpm", "run", "build"],
|
|
125
|
+
* dependents: [],
|
|
126
|
+
* },
|
|
127
|
+
* ],
|
|
128
|
+
* hmrWatchPaths: [resolve(root, "formats/components/generated")],
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @module
|
|
133
|
+
* @experimental
|
|
134
|
+
*/
|
|
135
|
+
const DEFAULT_DEBOUNCE_MS = 200;
|
|
136
|
+
/**
|
|
137
|
+
* Returns `true` if the changed filename passes the node's include/ignore filters.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { matchesFilters } from "@pantoken/vite-workspace-orchestrator";
|
|
142
|
+
*
|
|
143
|
+
* const node = {
|
|
144
|
+
* name: "@pantoken/components",
|
|
145
|
+
* dir: "/repo/formats/components",
|
|
146
|
+
* watchPaths: ["/repo/formats/components/src"],
|
|
147
|
+
* build: ["pnpm", "run", "build"] as const,
|
|
148
|
+
* dependents: [],
|
|
149
|
+
* include: ["**\/*.ts"],
|
|
150
|
+
* ignore: ["**\/*.test.ts"],
|
|
151
|
+
* };
|
|
152
|
+
*
|
|
153
|
+
* matchesFilters("src/index.ts", node); // true
|
|
154
|
+
* matchesFilters("src/index.test.ts", node); // false (ignored)
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
function matchesFilters(filename, node) {
|
|
158
|
+
if (node.ignore?.some((pattern) => matchesGlob(filename, pattern))) return false;
|
|
159
|
+
if (node.include && !node.include.some((pattern) => matchesGlob(filename, pattern))) return false;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Create the Vite dev-server plugin.
|
|
164
|
+
*
|
|
165
|
+
* @param options - {@link OrchestratorOptions}.
|
|
166
|
+
* @returns A Vite plugin object (`apply: "serve"`).
|
|
167
|
+
*/
|
|
168
|
+
const workspaceOrchestrator = (options) => {
|
|
169
|
+
const { upstream, hmrWatchPaths = [], reloadWatchPaths = [], debounceMs = DEFAULT_DEBOUNCE_MS, fileServers = [] } = options;
|
|
170
|
+
return {
|
|
171
|
+
apply: "serve",
|
|
172
|
+
name: "workspace-orchestrator",
|
|
173
|
+
configureServer(server) {
|
|
174
|
+
const { logger } = server.config;
|
|
175
|
+
const scheduleRebuild = createScheduler(new Map(upstream.map((n) => [n.name, n])), logger, debounceMs);
|
|
176
|
+
const fsWatchers = [];
|
|
177
|
+
for (const node of upstream) for (const watchPath of node.watchPaths) try {
|
|
178
|
+
const watcher = watch(watchPath, { recursive: true }, (_event, filename) => {
|
|
179
|
+
if (filename !== null && !matchesFilters(filename, node)) return;
|
|
180
|
+
scheduleRebuild(node.name);
|
|
181
|
+
});
|
|
182
|
+
fsWatchers.push(watcher);
|
|
183
|
+
} catch {}
|
|
184
|
+
for (const hmrPath of hmrWatchPaths) server.watcher.add(hmrPath);
|
|
185
|
+
for (const reloadPath of reloadWatchPaths) try {
|
|
186
|
+
const watcher = watch(reloadPath, { recursive: true }, (_event, filename) => {
|
|
187
|
+
if (filename === null) return;
|
|
188
|
+
const fullPath = resolve(reloadPath, filename);
|
|
189
|
+
logger.info(`[orchestrator] invalidating ${fullPath}`, { timestamp: true });
|
|
190
|
+
server.watcher.emit("change", fullPath);
|
|
191
|
+
});
|
|
192
|
+
fsWatchers.push(watcher);
|
|
193
|
+
} catch {}
|
|
194
|
+
mountFileServers(fileServers, server.middlewares);
|
|
195
|
+
server.httpServer?.on("close", () => {
|
|
196
|
+
for (const w of fsWatchers) w.close();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
//#endregion
|
|
202
|
+
export { matchesFilters, workspaceOrchestrator };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pantoken/vite-workspace-orchestrator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vite dev-server plugin that watches upstream workspace packages and rebuilds them (and their dependents) when source changes.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^24.13.3",
|
|
19
|
+
"typescript": "^6.0.3",
|
|
20
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
|
|
21
|
+
"vite-plus": "0.2.4"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"vite": ">=6"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "vp pack",
|
|
28
|
+
"dev": "vp pack --watch",
|
|
29
|
+
"test": "vp test",
|
|
30
|
+
"check": "vp check"
|
|
31
|
+
}
|
|
32
|
+
}
|