@prisma-next/vite-plugin-contract-emit 0.4.0-dev.9 → 0.4.1
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 +21 -15
- package/dist/index.d.mts +3 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +102 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
- package/src/plugin.ts +157 -23
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@ This plugin integrates with Vite's dev server to automatically emit contract art
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Emit on startup**: Emits contract artifacts when the Vite dev server starts
|
|
12
|
-
- **
|
|
12
|
+
- **Config graph + resolved inputs**: Re-emits from the config module graph plus loader-finalized `contract.source.inputs`
|
|
13
13
|
- **Debounce**: Configurable debounce prevents rapid re-emission during rapid edits
|
|
14
14
|
- **Last-change-wins**: Overlapping emit requests are cancelled to avoid stale results
|
|
15
|
+
- **Config-only fallback warning**: Falls back to watching the config path and warns when loader-resolved inputs cannot be determined
|
|
15
16
|
- **Error overlay**: Emission failures are surfaced via Vite's error overlay
|
|
16
17
|
- **Console logging**: Compact success/error messages with optional debug output
|
|
17
18
|
|
|
@@ -60,11 +61,14 @@ interface PrismaVitePluginOptions {
|
|
|
60
61
|
|
|
61
62
|
## How It Works
|
|
62
63
|
|
|
63
|
-
1. **On server start**: The plugin loads
|
|
64
|
-
2. **
|
|
65
|
-
3. **
|
|
66
|
-
4. **
|
|
67
|
-
5. **
|
|
64
|
+
1. **On server start**: The plugin loads `prisma-next.config.ts` via the CLI config loader
|
|
65
|
+
2. **Resolve paths in the loader**: The loader returns absolute `contract.source.inputs` and `contract.output`
|
|
66
|
+
3. **Resolve watched files**: The plugin crawls the Vite module graph from the config entrypoint
|
|
67
|
+
4. **Merge declared inputs**: It adds any explicit `contract.source.inputs`, and treats JS/TS inputs as additional module-graph roots
|
|
68
|
+
5. **Filter emitted artifacts**: Output files are removed from the watch set to avoid self-trigger loops
|
|
69
|
+
6. **Fallback on load failure**: If resolved inputs cannot be loaded, it watches only the config path and warns that coverage is partial
|
|
70
|
+
7. **Initial emit**: The contract is emitted immediately on server start
|
|
71
|
+
8. **Hot updates**: When any watched file changes, a debounced re-emit is triggered
|
|
68
72
|
|
|
69
73
|
## Architecture
|
|
70
74
|
|
|
@@ -72,17 +76,19 @@ interface PrismaVitePluginOptions {
|
|
|
72
76
|
graph TD
|
|
73
77
|
A[Vite Dev Server] --> B[prismaVitePlugin]
|
|
74
78
|
B --> C[configureServer hook]
|
|
75
|
-
C --> D[Load config via
|
|
76
|
-
D --> E[
|
|
77
|
-
E --> F[
|
|
78
|
-
F --> G[
|
|
79
|
+
C --> D[Load config via CLI loader]
|
|
80
|
+
D --> E[Collect config module graph]
|
|
81
|
+
E --> F[Merge source.inputs]
|
|
82
|
+
F --> G[Filter emitted artifacts]
|
|
83
|
+
G --> H[Add files to watcher]
|
|
84
|
+
H --> I[Initial emit]
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
J[File change] --> K[handleHotUpdate hook]
|
|
87
|
+
K --> L[Schedule debounced emit]
|
|
88
|
+
L --> M[executeContractEmit]
|
|
89
|
+
M --> N[Write artifacts]
|
|
84
90
|
|
|
85
|
-
|
|
91
|
+
P[Error] --> Q[Overlay or console logging]
|
|
86
92
|
```
|
|
87
93
|
|
|
88
94
|
## Dependencies
|
package/dist/index.d.mts
CHANGED
|
@@ -24,8 +24,9 @@ interface PrismaVitePluginOptions {
|
|
|
24
24
|
/**
|
|
25
25
|
* Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
|
|
26
26
|
*
|
|
27
|
-
* The plugin
|
|
28
|
-
* contract artifacts on changes with debounce and "last change wins"
|
|
27
|
+
* The plugin resolves watched files from contract source provider metadata,
|
|
28
|
+
* re-emitting contract artifacts on changes with debounce and "last change wins"
|
|
29
|
+
* semantics.
|
|
29
30
|
*
|
|
30
31
|
* @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
|
|
31
32
|
* @param options - Optional plugin configuration
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/plugin.ts"],"sourcesContent":[],"mappings":";;;;;;AAGiB,UAAA,uBAAA,CAAuB;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/plugin.ts"],"sourcesContent":[],"mappings":";;;;;;AAGiB,UAAA,uBAAA,CAAuB;;;;AC8CxC;;;;;;;;;;;;;;AD9CA;;;;AC8CA;;;;;;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,gCAEJ,0BACT"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadConfig } from "@prisma-next/cli/config-loader";
|
|
2
2
|
import { executeContractEmit } from "@prisma-next/cli/control-api";
|
|
3
|
+
import { getEmittedArtifactPaths } from "@prisma-next/emitter";
|
|
4
|
+
import { extname, resolve } from "pathe";
|
|
3
5
|
|
|
4
6
|
//#region src/plugin.ts
|
|
5
7
|
const PLUGIN_NAME = "prisma-vite-plugin-contract-emit";
|
|
6
8
|
const DEFAULT_DEBOUNCE_MS = 150;
|
|
7
9
|
const DEFAULT_CONFIG_PATH = "prisma-next.config.ts";
|
|
10
|
+
const MODULE_GRAPH_EXTENSIONS = new Set([
|
|
11
|
+
".js",
|
|
12
|
+
".jsx",
|
|
13
|
+
".mjs",
|
|
14
|
+
".cjs",
|
|
15
|
+
".ts",
|
|
16
|
+
".tsx",
|
|
17
|
+
".mts",
|
|
18
|
+
".cts"
|
|
19
|
+
]);
|
|
8
20
|
/**
|
|
9
21
|
* Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
|
|
10
22
|
*
|
|
11
|
-
* The plugin
|
|
12
|
-
* contract artifacts on changes with debounce and "last change wins"
|
|
23
|
+
* The plugin resolves watched files from contract source provider metadata,
|
|
24
|
+
* re-emitting contract artifacts on changes with debounce and "last change wins"
|
|
25
|
+
* semantics.
|
|
13
26
|
*
|
|
14
27
|
* @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
|
|
15
28
|
* @param options - Optional plugin configuration
|
|
@@ -36,10 +49,12 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
36
49
|
const logLevel = options?.logLevel ?? "info";
|
|
37
50
|
let absoluteConfigPath;
|
|
38
51
|
const watchedFiles = /* @__PURE__ */ new Set();
|
|
52
|
+
const ignoredOutputFiles = /* @__PURE__ */ new Set();
|
|
39
53
|
let debounceTimer = null;
|
|
40
54
|
let currentAbortController = null;
|
|
41
55
|
let server = null;
|
|
42
56
|
let emitRequestId = 0;
|
|
57
|
+
let didWarnConfigWatchFallback = false;
|
|
43
58
|
function log(message, level = "info") {
|
|
44
59
|
if (logLevel === "silent") return;
|
|
45
60
|
if (level === "debug" && logLevel !== "debug") return;
|
|
@@ -51,12 +66,28 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
51
66
|
console.error(`[${PLUGIN_NAME}] ${message}${errorMessage ? ` ${errorMessage}` : ""}`);
|
|
52
67
|
if (error instanceof Error && error.stack && logLevel === "debug") console.error(error.stack);
|
|
53
68
|
}
|
|
54
|
-
|
|
69
|
+
function logWarning(message) {
|
|
70
|
+
if (logLevel === "silent") return;
|
|
71
|
+
console.warn(`[${PLUGIN_NAME}] ${message}`);
|
|
72
|
+
}
|
|
73
|
+
function handleTrackedFileChange(file) {
|
|
74
|
+
const normalized = resolve(file);
|
|
75
|
+
if (ignoredOutputFiles.has(normalized)) {
|
|
76
|
+
log(`Ignoring emitted artifact update: ${normalized}`, "debug");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (watchedFiles.has(normalized)) {
|
|
80
|
+
log(`Detected change: ${normalized}`, "debug");
|
|
81
|
+
scheduleEmit();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function emitContract({ refreshWatchedFiles = true } = {}) {
|
|
55
85
|
const requestId = ++emitRequestId;
|
|
56
86
|
if (currentAbortController) currentAbortController.abort();
|
|
57
87
|
currentAbortController = new AbortController();
|
|
58
88
|
const signal = currentAbortController.signal;
|
|
59
89
|
try {
|
|
90
|
+
if (server && refreshWatchedFiles) await updateWatchedFiles(server);
|
|
60
91
|
const result = await executeContractEmit({
|
|
61
92
|
configPath: absoluteConfigPath,
|
|
62
93
|
signal
|
|
@@ -68,10 +99,7 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
68
99
|
log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);
|
|
69
100
|
log(` → ${result.files.json}`, "debug");
|
|
70
101
|
log(` → ${result.files.dts}`, "debug");
|
|
71
|
-
if (server) {
|
|
72
|
-
await updateWatchedFiles(server);
|
|
73
|
-
server.ws.send({ type: "full-reload" });
|
|
74
|
-
}
|
|
102
|
+
if (server) server.ws.send({ type: "full-reload" });
|
|
75
103
|
return result;
|
|
76
104
|
} catch (error) {
|
|
77
105
|
if (signal.aborted || error instanceof Error && error.name === "AbortError") {
|
|
@@ -102,12 +130,26 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
102
130
|
emitContract();
|
|
103
131
|
}, debounceMs);
|
|
104
132
|
}
|
|
105
|
-
|
|
133
|
+
function resolveContractOutputFiles(contractOutput) {
|
|
134
|
+
if (contractOutput === void 0) return /* @__PURE__ */ new Set();
|
|
135
|
+
const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
|
|
136
|
+
return new Set([jsonPath, dtsPath]);
|
|
137
|
+
}
|
|
138
|
+
function isModuleGraphRoot(filePath) {
|
|
139
|
+
return MODULE_GRAPH_EXTENSIONS.has(extname(filePath));
|
|
140
|
+
}
|
|
141
|
+
async function collectModuleGraphFiles(viteServer, roots) {
|
|
106
142
|
const files = /* @__PURE__ */ new Set();
|
|
143
|
+
const uniqueRoots = [...new Set(roots)];
|
|
144
|
+
for (const root of uniqueRoots) try {
|
|
145
|
+
await viteServer.ssrLoadModule(root);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (root === absoluteConfigPath) logError("Failed to load config module graph root:", error);
|
|
148
|
+
else log(`Skipped module-graph root after load failure: ${root}`, "debug");
|
|
149
|
+
}
|
|
107
150
|
try {
|
|
108
|
-
await viteServer.ssrLoadModule(absoluteConfigPath);
|
|
109
151
|
const visited = /* @__PURE__ */ new Set();
|
|
110
|
-
const queue = [
|
|
152
|
+
const queue = [...uniqueRoots];
|
|
111
153
|
while (queue.length > 0) {
|
|
112
154
|
const current = queue.shift();
|
|
113
155
|
if (current === void 0 || visited.has(current)) continue;
|
|
@@ -119,12 +161,44 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
119
161
|
}
|
|
120
162
|
} catch (error) {
|
|
121
163
|
logError("Failed to collect watched files:", error);
|
|
122
|
-
files.add(absoluteConfigPath);
|
|
123
164
|
}
|
|
124
165
|
return files;
|
|
125
166
|
}
|
|
167
|
+
async function resolveWatchedFiles(viteServer) {
|
|
168
|
+
const previousWatchedFiles = new Set(watchedFiles);
|
|
169
|
+
const previousIgnoredOutputFiles = new Set(ignoredOutputFiles);
|
|
170
|
+
ignoredOutputFiles.clear();
|
|
171
|
+
try {
|
|
172
|
+
const config = await loadConfig(absoluteConfigPath);
|
|
173
|
+
didWarnConfigWatchFallback = false;
|
|
174
|
+
const contract = config.contract;
|
|
175
|
+
if (!contract) return new Set([absoluteConfigPath]);
|
|
176
|
+
const files = new Set([absoluteConfigPath]);
|
|
177
|
+
const inputs = contract.source.inputs ?? [];
|
|
178
|
+
for (const outputFile of resolveContractOutputFiles(contract.output)) ignoredOutputFiles.add(outputFile);
|
|
179
|
+
const moduleGraphRoots = [absoluteConfigPath];
|
|
180
|
+
for (const input of inputs) {
|
|
181
|
+
if (!ignoredOutputFiles.has(input)) files.add(input);
|
|
182
|
+
if (isModuleGraphRoot(input)) moduleGraphRoots.push(input);
|
|
183
|
+
}
|
|
184
|
+
for (const file of await collectModuleGraphFiles(viteServer, moduleGraphRoots)) if (!ignoredOutputFiles.has(file)) files.add(file);
|
|
185
|
+
return files;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (previousIgnoredOutputFiles.size > 0) for (const outputFile of previousIgnoredOutputFiles) ignoredOutputFiles.add(outputFile);
|
|
188
|
+
if (!didWarnConfigWatchFallback) {
|
|
189
|
+
didWarnConfigWatchFallback = true;
|
|
190
|
+
const reason = error instanceof Error ? ` ${error.message}` : "";
|
|
191
|
+
logWarning(`${previousWatchedFiles.size > 0 ? `Watching the previous dependency set plus ${absoluteConfigPath}` : `Watching only ${absoluteConfigPath}`} because Prisma Next config inputs could not be resolved.${reason} Contract watch coverage is partial.`);
|
|
192
|
+
}
|
|
193
|
+
if (previousWatchedFiles.size > 0) {
|
|
194
|
+
previousWatchedFiles.add(absoluteConfigPath);
|
|
195
|
+
return previousWatchedFiles;
|
|
196
|
+
}
|
|
197
|
+
return new Set([absoluteConfigPath]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
126
200
|
async function updateWatchedFiles(viteServer) {
|
|
127
|
-
const newWatchedFiles = await
|
|
201
|
+
const newWatchedFiles = await resolveWatchedFiles(viteServer);
|
|
128
202
|
const toAdd = [];
|
|
129
203
|
const toRemove = [];
|
|
130
204
|
for (const file of newWatchedFiles) if (!watchedFiles.has(file)) toAdd.push(file);
|
|
@@ -143,6 +217,9 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
143
217
|
},
|
|
144
218
|
async configureServer(viteServer) {
|
|
145
219
|
server = viteServer;
|
|
220
|
+
const onTrackedWatcherEvent = (file) => {
|
|
221
|
+
handleTrackedFileChange(file);
|
|
222
|
+
};
|
|
146
223
|
const cleanup = () => {
|
|
147
224
|
if (debounceTimer) {
|
|
148
225
|
clearTimeout(debounceTimer);
|
|
@@ -152,13 +229,22 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
152
229
|
currentAbortController.abort();
|
|
153
230
|
currentAbortController = null;
|
|
154
231
|
}
|
|
232
|
+
viteServer.watcher.off?.("change", onTrackedWatcherEvent);
|
|
233
|
+
viteServer.watcher.off?.("add", onTrackedWatcherEvent);
|
|
234
|
+
viteServer.watcher.off?.("unlink", onTrackedWatcherEvent);
|
|
235
|
+
ignoredOutputFiles.clear();
|
|
236
|
+
didWarnConfigWatchFallback = false;
|
|
155
237
|
server = null;
|
|
156
238
|
watchedFiles.clear();
|
|
157
239
|
log("Server closed, cleaned up resources", "debug");
|
|
158
240
|
};
|
|
159
241
|
viteServer.httpServer?.on("close", cleanup);
|
|
160
242
|
viteServer.watcher?.on?.("close", cleanup);
|
|
161
|
-
|
|
243
|
+
viteServer.watcher.on("change", onTrackedWatcherEvent);
|
|
244
|
+
viteServer.watcher.on("add", onTrackedWatcherEvent);
|
|
245
|
+
viteServer.watcher.on("unlink", onTrackedWatcherEvent);
|
|
246
|
+
const initialWatchedFiles = await resolveWatchedFiles(viteServer);
|
|
247
|
+
for (const file of initialWatchedFiles) watchedFiles.add(file);
|
|
162
248
|
for (const file of watchedFiles) viteServer.watcher.add(file);
|
|
163
249
|
if (watchedFiles.size === 0) {
|
|
164
250
|
const errorMessage = `No files are being watched. The config file "${absoluteConfigPath}" could not be loaded or has no dependencies. HMR for contract changes will not work.`;
|
|
@@ -175,13 +261,10 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
175
261
|
log(`Watching ${watchedFiles.size} files`, "debug");
|
|
176
262
|
if (logLevel === "debug") for (const file of watchedFiles) log(` ${file}`, "debug");
|
|
177
263
|
}
|
|
178
|
-
await emitContract();
|
|
264
|
+
await emitContract({ refreshWatchedFiles: false });
|
|
179
265
|
},
|
|
180
266
|
handleHotUpdate(ctx) {
|
|
181
|
-
|
|
182
|
-
log(`Detected change: ${ctx.file}`, "debug");
|
|
183
|
-
scheduleEmit();
|
|
184
|
-
}
|
|
267
|
+
handleTrackedFileChange(ctx.file);
|
|
185
268
|
}
|
|
186
269
|
};
|
|
187
270
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["absoluteConfigPath: string","debounceTimer: ReturnType<typeof setTimeout> | null","currentAbortController: AbortController | null","server: ViteDevServer | null","toAdd: string[]","toRemove: string[]"],"sources":["../src/plugin.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type { ContractEmitResult } from '@prisma-next/cli/control-api';\nimport { executeContractEmit } from '@prisma-next/cli/control-api';\nimport type { Plugin, ViteDevServer } from 'vite';\nimport type { PrismaVitePluginOptions } from './types';\n\nconst PLUGIN_NAME = 'prisma-vite-plugin-contract-emit';\nconst DEFAULT_DEBOUNCE_MS = 150;\nconst DEFAULT_CONFIG_PATH = 'prisma-next.config.ts';\n\n/**\n * Creates a Vite plugin that automatically emits Prisma Next contract artifacts.\n *\n * The plugin watches the config file and its transitive dependencies, re-emitting\n * contract artifacts on changes with debounce and \"last change wins\" semantics.\n *\n * @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'\n * @param options - Optional plugin configuration\n * @returns Vite plugin\n *\n * @example\n * ```ts\n * import { defineConfig } from 'vite';\n * import { prismaVitePlugin } from '@prisma-next/vite-plugin-contract-emit';\n *\n * // Use default config path\n * export default defineConfig({\n * plugins: [prismaVitePlugin()],\n * });\n *\n * // Or specify a custom path\n * export default defineConfig({\n * plugins: [prismaVitePlugin('custom/prisma-next.config.ts')],\n * });\n * ```\n */\nexport function prismaVitePlugin(\n configPath: string = DEFAULT_CONFIG_PATH,\n options?: PrismaVitePluginOptions,\n): Plugin {\n const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n const logLevel = options?.logLevel ?? 'info';\n\n let absoluteConfigPath: string;\n const watchedFiles = new Set<string>();\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let currentAbortController: AbortController | null = null;\n let server: ViteDevServer | null = null;\n let emitRequestId = 0;\n\n function log(message: string, level: 'info' | 'debug' = 'info') {\n if (logLevel === 'silent') return;\n if (level === 'debug' && logLevel !== 'debug') return;\n console.log(`[${PLUGIN_NAME}] ${message}`);\n }\n\n function logError(message: string, error?: unknown) {\n if (logLevel === 'silent') return;\n const errorMessage = error instanceof Error ? error.message : error ? String(error) : '';\n console.error(`[${PLUGIN_NAME}] ${message}${errorMessage ? ` ${errorMessage}` : ''}`);\n if (error instanceof Error && error.stack && logLevel === 'debug') {\n console.error(error.stack);\n }\n }\n\n async function emitContract(): Promise<ContractEmitResult | null> {\n const requestId = ++emitRequestId;\n\n // Cancel any in-flight emit\n if (currentAbortController) {\n currentAbortController.abort();\n }\n currentAbortController = new AbortController();\n const signal = currentAbortController.signal;\n\n try {\n const result = await executeContractEmit({\n configPath: absoluteConfigPath,\n signal,\n });\n\n // Check if this emit is still the latest request\n if (requestId !== emitRequestId) {\n log('Emit superseded by newer request', 'debug');\n return null;\n }\n\n log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);\n log(` → ${result.files.json}`, 'debug');\n log(` → ${result.files.dts}`, 'debug');\n\n // Update watched files to include any new transitive dependencies\n if (server) {\n await updateWatchedFiles(server);\n server.ws.send({ type: 'full-reload' });\n }\n\n return result;\n } catch (error) {\n // Ignore cancellation - check signal first, then error name\n if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) {\n log('Emit cancelled', 'debug');\n return null;\n }\n\n // Check if this emit is still the latest request\n if (requestId !== emitRequestId) {\n return null;\n }\n\n logError('Contract emit failed:', error);\n\n // Send error to Vite overlay\n if (server) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorStack = error instanceof Error ? error.stack : undefined;\n server.ws.send({\n type: 'error',\n err: {\n message: `[prisma-next] ${errorMessage}`,\n stack: errorStack ?? '',\n plugin: PLUGIN_NAME,\n },\n });\n }\n\n return null;\n }\n }\n\n function scheduleEmit() {\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void emitContract();\n }, debounceMs);\n }\n\n async function collectWatchedFiles(viteServer: ViteDevServer): Promise<Set<string>> {\n const files = new Set<string>();\n\n try {\n // Load the config module through Vite's SSR loader to populate the module graph\n await viteServer.ssrLoadModule(absoluteConfigPath);\n\n // Crawl the module graph starting from the config file\n const visited = new Set<string>();\n const queue = [absoluteConfigPath];\n\n while (queue.length > 0) {\n const current = queue.shift();\n if (current === undefined || visited.has(current)) continue;\n visited.add(current);\n\n const mod = viteServer.moduleGraph.getModuleById(current);\n if (!mod) continue;\n\n // Add file to watched set if it's a file path\n if (mod.file) {\n files.add(mod.file);\n }\n\n // Add imported modules to queue\n for (const imported of mod.importedModules) {\n if (imported.id && !visited.has(imported.id)) {\n queue.push(imported.id);\n }\n }\n }\n } catch (error) {\n logError('Failed to collect watched files:', error);\n // At minimum, watch the config file itself\n files.add(absoluteConfigPath);\n }\n\n return files;\n }\n\n async function updateWatchedFiles(viteServer: ViteDevServer): Promise<void> {\n const newWatchedFiles = await collectWatchedFiles(viteServer);\n\n // Find files to add and remove\n const toAdd: string[] = [];\n const toRemove: string[] = [];\n\n for (const file of newWatchedFiles) {\n if (!watchedFiles.has(file)) {\n toAdd.push(file);\n }\n }\n\n for (const file of watchedFiles) {\n if (!newWatchedFiles.has(file)) {\n toRemove.push(file);\n }\n }\n\n // Update the watcher\n for (const file of toAdd) {\n viteServer.watcher.add(file);\n }\n for (const file of toRemove) {\n viteServer.watcher.unwatch(file);\n }\n\n // Replace the watched files set\n watchedFiles.clear();\n for (const file of newWatchedFiles) {\n watchedFiles.add(file);\n }\n\n if (toAdd.length > 0 || toRemove.length > 0) {\n log(`Updated watched files: +${toAdd.length} -${toRemove.length}`, 'debug');\n }\n }\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(config) {\n // Resolve config path to absolute path based on Vite root\n absoluteConfigPath = resolve(config.root, configPath);\n log(`Config path: ${absoluteConfigPath}`, 'debug');\n },\n\n async configureServer(viteServer) {\n server = viteServer;\n\n // Register close hook to clean up timers and abort in-flight work\n const cleanup = () => {\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n if (currentAbortController) {\n currentAbortController.abort();\n currentAbortController = null;\n }\n server = null;\n watchedFiles.clear();\n log('Server closed, cleaned up resources', 'debug');\n };\n\n // Register cleanup on server close via httpServer or watcher\n viteServer.httpServer?.on('close', cleanup);\n viteServer.watcher?.on?.('close', cleanup);\n\n // Collect files to watch from the module graph\n for (const file of await collectWatchedFiles(viteServer)) {\n watchedFiles.add(file);\n }\n\n // Add all dependency files to Vite's watcher\n for (const file of watchedFiles) {\n viteServer.watcher.add(file);\n }\n\n // Error if no files are being watched - this indicates a configuration problem\n if (watchedFiles.size === 0) {\n const errorMessage =\n `No files are being watched. The config file \"${absoluteConfigPath}\" could not be loaded ` +\n 'or has no dependencies. HMR for contract changes will not work.';\n logError(errorMessage);\n viteServer.ws.send({\n type: 'error',\n err: {\n message: `[prisma-next] ${errorMessage}`,\n stack: '',\n plugin: PLUGIN_NAME,\n },\n });\n } else {\n log(`Watching ${watchedFiles.size} files`, 'debug');\n if (logLevel === 'debug') {\n for (const file of watchedFiles) {\n log(` ${file}`, 'debug');\n }\n }\n }\n\n // Initial emit on server start\n await emitContract();\n },\n\n handleHotUpdate(ctx) {\n // Check if the changed file is one we're watching\n if (watchedFiles.has(ctx.file)) {\n log(`Detected change: ${ctx.file}`, 'debug');\n scheduleEmit();\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["absoluteConfigPath: string","debounceTimer: ReturnType<typeof setTimeout> | null","currentAbortController: AbortController | null","server: ViteDevServer | null","toAdd: string[]","toRemove: string[]"],"sources":["../src/plugin.ts"],"sourcesContent":["import { loadConfig } from '@prisma-next/cli/config-loader';\nimport type { ContractEmitResult } from '@prisma-next/cli/control-api';\nimport { executeContractEmit } from '@prisma-next/cli/control-api';\nimport { getEmittedArtifactPaths } from '@prisma-next/emitter';\nimport { extname, resolve } from 'pathe';\nimport type { Plugin, ViteDevServer } from 'vite';\nimport type { PrismaVitePluginOptions } from './types';\n\nconst PLUGIN_NAME = 'prisma-vite-plugin-contract-emit';\nconst DEFAULT_DEBOUNCE_MS = 150;\nconst DEFAULT_CONFIG_PATH = 'prisma-next.config.ts';\nconst MODULE_GRAPH_EXTENSIONS = new Set([\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n]);\n\n/**\n * Creates a Vite plugin that automatically emits Prisma Next contract artifacts.\n *\n * The plugin resolves watched files from contract source provider metadata,\n * re-emitting contract artifacts on changes with debounce and \"last change wins\"\n * semantics.\n *\n * @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'\n * @param options - Optional plugin configuration\n * @returns Vite plugin\n *\n * @example\n * ```ts\n * import { defineConfig } from 'vite';\n * import { prismaVitePlugin } from '@prisma-next/vite-plugin-contract-emit';\n *\n * // Use default config path\n * export default defineConfig({\n * plugins: [prismaVitePlugin()],\n * });\n *\n * // Or specify a custom path\n * export default defineConfig({\n * plugins: [prismaVitePlugin('custom/prisma-next.config.ts')],\n * });\n * ```\n */\nexport function prismaVitePlugin(\n configPath: string = DEFAULT_CONFIG_PATH,\n options?: PrismaVitePluginOptions,\n): Plugin {\n const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n const logLevel = options?.logLevel ?? 'info';\n\n let absoluteConfigPath: string;\n const watchedFiles = new Set<string>();\n // Vite watches the project root, so writes to emitted artifacts can still surface as change\n // events even when those files are excluded from watchedFiles.\n const ignoredOutputFiles = new Set<string>();\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let currentAbortController: AbortController | null = null;\n let server: ViteDevServer | null = null;\n let emitRequestId = 0;\n let didWarnConfigWatchFallback = false;\n\n function log(message: string, level: 'info' | 'debug' = 'info') {\n if (logLevel === 'silent') return;\n if (level === 'debug' && logLevel !== 'debug') return;\n console.log(`[${PLUGIN_NAME}] ${message}`);\n }\n\n function logError(message: string, error?: unknown) {\n if (logLevel === 'silent') return;\n const errorMessage = error instanceof Error ? error.message : error ? String(error) : '';\n console.error(`[${PLUGIN_NAME}] ${message}${errorMessage ? ` ${errorMessage}` : ''}`);\n if (error instanceof Error && error.stack && logLevel === 'debug') {\n console.error(error.stack);\n }\n }\n\n function logWarning(message: string) {\n if (logLevel === 'silent') return;\n console.warn(`[${PLUGIN_NAME}] ${message}`);\n }\n\n function handleTrackedFileChange(file: string) {\n const normalized = resolve(file);\n if (ignoredOutputFiles.has(normalized)) {\n log(`Ignoring emitted artifact update: ${normalized}`, 'debug');\n return;\n }\n\n if (watchedFiles.has(normalized)) {\n log(`Detected change: ${normalized}`, 'debug');\n scheduleEmit();\n }\n }\n\n async function emitContract({\n refreshWatchedFiles = true,\n }: {\n refreshWatchedFiles?: boolean;\n } = {}): Promise<ContractEmitResult | null> {\n const requestId = ++emitRequestId;\n\n // Cancel any in-flight emit\n if (currentAbortController) {\n currentAbortController.abort();\n }\n currentAbortController = new AbortController();\n const signal = currentAbortController.signal;\n\n try {\n if (server && refreshWatchedFiles) {\n await updateWatchedFiles(server);\n }\n\n const result = await executeContractEmit({\n configPath: absoluteConfigPath,\n signal,\n });\n\n // Check if this emit is still the latest request\n if (requestId !== emitRequestId) {\n log('Emit superseded by newer request', 'debug');\n return null;\n }\n\n log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);\n log(` → ${result.files.json}`, 'debug');\n log(` → ${result.files.dts}`, 'debug');\n\n if (server) {\n server.ws.send({ type: 'full-reload' });\n }\n\n return result;\n } catch (error) {\n // Ignore cancellation - check signal first, then error name\n if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) {\n log('Emit cancelled', 'debug');\n return null;\n }\n\n // Check if this emit is still the latest request\n if (requestId !== emitRequestId) {\n return null;\n }\n\n logError('Contract emit failed:', error);\n\n // Send error to Vite overlay\n if (server) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorStack = error instanceof Error ? error.stack : undefined;\n server.ws.send({\n type: 'error',\n err: {\n message: `[prisma-next] ${errorMessage}`,\n stack: errorStack ?? '',\n plugin: PLUGIN_NAME,\n },\n });\n }\n\n return null;\n }\n }\n\n function scheduleEmit() {\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void emitContract();\n }, debounceMs);\n }\n\n function resolveContractOutputFiles(contractOutput: string | undefined): Set<string> {\n if (contractOutput === undefined) {\n return new Set();\n }\n const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);\n return new Set<string>([jsonPath, dtsPath]);\n }\n\n function isModuleGraphRoot(filePath: string): boolean {\n return MODULE_GRAPH_EXTENSIONS.has(extname(filePath));\n }\n\n async function collectModuleGraphFiles(\n viteServer: ViteDevServer,\n roots: readonly string[],\n ): Promise<Set<string>> {\n const files = new Set<string>();\n const uniqueRoots = [...new Set(roots)];\n\n for (const root of uniqueRoots) {\n try {\n await viteServer.ssrLoadModule(root);\n } catch (error) {\n if (root === absoluteConfigPath) {\n logError('Failed to load config module graph root:', error);\n } else {\n log(`Skipped module-graph root after load failure: ${root}`, 'debug');\n }\n }\n }\n\n try {\n const visited = new Set<string>();\n const queue = [...uniqueRoots];\n\n while (queue.length > 0) {\n const current = queue.shift();\n if (current === undefined || visited.has(current)) continue;\n visited.add(current);\n\n const mod = viteServer.moduleGraph.getModuleById(current);\n if (!mod) continue;\n\n // Add file to watched set if it's a file path\n if (mod.file) {\n files.add(mod.file);\n }\n\n // Add imported modules to queue\n for (const imported of mod.importedModules) {\n if (imported.id && !visited.has(imported.id)) {\n queue.push(imported.id);\n }\n }\n }\n } catch (error) {\n logError('Failed to collect watched files:', error);\n }\n\n return files;\n }\n\n async function resolveWatchedFiles(viteServer: ViteDevServer): Promise<Set<string>> {\n const previousWatchedFiles = new Set(watchedFiles);\n const previousIgnoredOutputFiles = new Set(ignoredOutputFiles);\n ignoredOutputFiles.clear();\n\n try {\n const config = await loadConfig(absoluteConfigPath);\n didWarnConfigWatchFallback = false;\n const contract = config.contract;\n\n if (!contract) {\n return new Set([absoluteConfigPath]);\n }\n\n const files = new Set<string>([absoluteConfigPath]);\n const inputs = contract.source.inputs ?? [];\n for (const outputFile of resolveContractOutputFiles(contract.output)) {\n ignoredOutputFiles.add(outputFile);\n }\n\n const moduleGraphRoots = [absoluteConfigPath];\n for (const input of inputs) {\n if (!ignoredOutputFiles.has(input)) {\n files.add(input);\n }\n if (isModuleGraphRoot(input)) {\n moduleGraphRoots.push(input);\n }\n }\n\n for (const file of await collectModuleGraphFiles(viteServer, moduleGraphRoots)) {\n if (!ignoredOutputFiles.has(file)) {\n files.add(file);\n }\n }\n\n return files;\n } catch (error) {\n if (previousIgnoredOutputFiles.size > 0) {\n for (const outputFile of previousIgnoredOutputFiles) {\n ignoredOutputFiles.add(outputFile);\n }\n }\n if (!didWarnConfigWatchFallback) {\n didWarnConfigWatchFallback = true;\n const reason = error instanceof Error ? ` ${error.message}` : '';\n const watchScope =\n previousWatchedFiles.size > 0\n ? `Watching the previous dependency set plus ${absoluteConfigPath}`\n : `Watching only ${absoluteConfigPath}`;\n logWarning(\n `${watchScope} because Prisma Next config inputs could not be resolved.${reason} Contract watch coverage is partial.`,\n );\n }\n if (previousWatchedFiles.size > 0) {\n previousWatchedFiles.add(absoluteConfigPath);\n return previousWatchedFiles;\n }\n return new Set([absoluteConfigPath]);\n }\n }\n\n async function updateWatchedFiles(viteServer: ViteDevServer): Promise<void> {\n const newWatchedFiles = await resolveWatchedFiles(viteServer);\n\n // Find files to add and remove\n const toAdd: string[] = [];\n const toRemove: string[] = [];\n\n for (const file of newWatchedFiles) {\n if (!watchedFiles.has(file)) {\n toAdd.push(file);\n }\n }\n\n for (const file of watchedFiles) {\n if (!newWatchedFiles.has(file)) {\n toRemove.push(file);\n }\n }\n\n // Update the watcher\n for (const file of toAdd) {\n viteServer.watcher.add(file);\n }\n for (const file of toRemove) {\n viteServer.watcher.unwatch(file);\n }\n\n // Replace the watched files set\n watchedFiles.clear();\n for (const file of newWatchedFiles) {\n watchedFiles.add(file);\n }\n\n if (toAdd.length > 0 || toRemove.length > 0) {\n log(`Updated watched files: +${toAdd.length} -${toRemove.length}`, 'debug');\n }\n }\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(config) {\n // Resolve config path to absolute path based on Vite root\n absoluteConfigPath = resolve(config.root, configPath);\n log(`Config path: ${absoluteConfigPath}`, 'debug');\n },\n\n async configureServer(viteServer) {\n server = viteServer;\n const onTrackedWatcherEvent = (file: string) => {\n handleTrackedFileChange(file);\n };\n\n // Register close hook to clean up timers and abort in-flight work\n const cleanup = () => {\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n if (currentAbortController) {\n currentAbortController.abort();\n currentAbortController = null;\n }\n viteServer.watcher.off?.('change', onTrackedWatcherEvent);\n viteServer.watcher.off?.('add', onTrackedWatcherEvent);\n viteServer.watcher.off?.('unlink', onTrackedWatcherEvent);\n ignoredOutputFiles.clear();\n didWarnConfigWatchFallback = false;\n server = null;\n watchedFiles.clear();\n log('Server closed, cleaned up resources', 'debug');\n };\n\n // Register cleanup on server close via httpServer or watcher\n viteServer.httpServer?.on('close', cleanup);\n viteServer.watcher?.on?.('close', cleanup);\n viteServer.watcher.on('change', onTrackedWatcherEvent);\n viteServer.watcher.on('add', onTrackedWatcherEvent);\n viteServer.watcher.on('unlink', onTrackedWatcherEvent);\n\n const initialWatchedFiles = await resolveWatchedFiles(viteServer);\n\n // Collect files to watch from provider metadata\n for (const file of initialWatchedFiles) {\n watchedFiles.add(file);\n }\n\n // Add all dependency files to Vite's watcher\n for (const file of watchedFiles) {\n viteServer.watcher.add(file);\n }\n\n // Error if no files are being watched - this indicates a configuration problem\n if (watchedFiles.size === 0) {\n const errorMessage =\n `No files are being watched. The config file \"${absoluteConfigPath}\" could not be loaded ` +\n 'or has no dependencies. HMR for contract changes will not work.';\n logError(errorMessage);\n viteServer.ws.send({\n type: 'error',\n err: {\n message: `[prisma-next] ${errorMessage}`,\n stack: '',\n plugin: PLUGIN_NAME,\n },\n });\n } else {\n log(`Watching ${watchedFiles.size} files`, 'debug');\n if (logLevel === 'debug') {\n for (const file of watchedFiles) {\n log(` ${file}`, 'debug');\n }\n }\n }\n\n // Initial emit on server start\n await emitContract({ refreshWatchedFiles: false });\n },\n\n handleHotUpdate(ctx) {\n handleTrackedFileChange(ctx.file);\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/vite-plugin-contract-emit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"src"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"
|
|
11
|
+
"pathe": "^2.0.3",
|
|
12
|
+
"@prisma-next/emitter": "0.4.1",
|
|
13
|
+
"@prisma-next/cli": "0.4.1"
|
|
12
14
|
},
|
|
13
15
|
"devDependencies": {
|
|
14
16
|
"@types/node": "24.10.4",
|
|
@@ -16,8 +18,8 @@
|
|
|
16
18
|
"typescript": "5.9.3",
|
|
17
19
|
"vite": "7.3.1",
|
|
18
20
|
"vitest": "4.0.17",
|
|
19
|
-
"@prisma-next/
|
|
20
|
-
"@prisma-next/
|
|
21
|
+
"@prisma-next/tsconfig": "0.0.0",
|
|
22
|
+
"@prisma-next/tsdown": "0.0.0"
|
|
21
23
|
},
|
|
22
24
|
"peerDependencies": {
|
|
23
25
|
"vite": ">=5.0.0"
|
package/src/plugin.ts
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadConfig } from '@prisma-next/cli/config-loader';
|
|
2
2
|
import type { ContractEmitResult } from '@prisma-next/cli/control-api';
|
|
3
3
|
import { executeContractEmit } from '@prisma-next/cli/control-api';
|
|
4
|
+
import { getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
5
|
+
import { extname, resolve } from 'pathe';
|
|
4
6
|
import type { Plugin, ViteDevServer } from 'vite';
|
|
5
7
|
import type { PrismaVitePluginOptions } from './types';
|
|
6
8
|
|
|
7
9
|
const PLUGIN_NAME = 'prisma-vite-plugin-contract-emit';
|
|
8
10
|
const DEFAULT_DEBOUNCE_MS = 150;
|
|
9
11
|
const DEFAULT_CONFIG_PATH = 'prisma-next.config.ts';
|
|
12
|
+
const MODULE_GRAPH_EXTENSIONS = new Set([
|
|
13
|
+
'.js',
|
|
14
|
+
'.jsx',
|
|
15
|
+
'.mjs',
|
|
16
|
+
'.cjs',
|
|
17
|
+
'.ts',
|
|
18
|
+
'.tsx',
|
|
19
|
+
'.mts',
|
|
20
|
+
'.cts',
|
|
21
|
+
]);
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
|
|
13
25
|
*
|
|
14
|
-
* The plugin
|
|
15
|
-
* contract artifacts on changes with debounce and "last change wins"
|
|
26
|
+
* The plugin resolves watched files from contract source provider metadata,
|
|
27
|
+
* re-emitting contract artifacts on changes with debounce and "last change wins"
|
|
28
|
+
* semantics.
|
|
16
29
|
*
|
|
17
30
|
* @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
|
|
18
31
|
* @param options - Optional plugin configuration
|
|
@@ -43,10 +56,14 @@ export function prismaVitePlugin(
|
|
|
43
56
|
|
|
44
57
|
let absoluteConfigPath: string;
|
|
45
58
|
const watchedFiles = new Set<string>();
|
|
59
|
+
// Vite watches the project root, so writes to emitted artifacts can still surface as change
|
|
60
|
+
// events even when those files are excluded from watchedFiles.
|
|
61
|
+
const ignoredOutputFiles = new Set<string>();
|
|
46
62
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
63
|
let currentAbortController: AbortController | null = null;
|
|
48
64
|
let server: ViteDevServer | null = null;
|
|
49
65
|
let emitRequestId = 0;
|
|
66
|
+
let didWarnConfigWatchFallback = false;
|
|
50
67
|
|
|
51
68
|
function log(message: string, level: 'info' | 'debug' = 'info') {
|
|
52
69
|
if (logLevel === 'silent') return;
|
|
@@ -63,7 +80,29 @@ export function prismaVitePlugin(
|
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
|
|
83
|
+
function logWarning(message: string) {
|
|
84
|
+
if (logLevel === 'silent') return;
|
|
85
|
+
console.warn(`[${PLUGIN_NAME}] ${message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function handleTrackedFileChange(file: string) {
|
|
89
|
+
const normalized = resolve(file);
|
|
90
|
+
if (ignoredOutputFiles.has(normalized)) {
|
|
91
|
+
log(`Ignoring emitted artifact update: ${normalized}`, 'debug');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (watchedFiles.has(normalized)) {
|
|
96
|
+
log(`Detected change: ${normalized}`, 'debug');
|
|
97
|
+
scheduleEmit();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function emitContract({
|
|
102
|
+
refreshWatchedFiles = true,
|
|
103
|
+
}: {
|
|
104
|
+
refreshWatchedFiles?: boolean;
|
|
105
|
+
} = {}): Promise<ContractEmitResult | null> {
|
|
67
106
|
const requestId = ++emitRequestId;
|
|
68
107
|
|
|
69
108
|
// Cancel any in-flight emit
|
|
@@ -74,6 +113,10 @@ export function prismaVitePlugin(
|
|
|
74
113
|
const signal = currentAbortController.signal;
|
|
75
114
|
|
|
76
115
|
try {
|
|
116
|
+
if (server && refreshWatchedFiles) {
|
|
117
|
+
await updateWatchedFiles(server);
|
|
118
|
+
}
|
|
119
|
+
|
|
77
120
|
const result = await executeContractEmit({
|
|
78
121
|
configPath: absoluteConfigPath,
|
|
79
122
|
signal,
|
|
@@ -89,9 +132,7 @@ export function prismaVitePlugin(
|
|
|
89
132
|
log(` → ${result.files.json}`, 'debug');
|
|
90
133
|
log(` → ${result.files.dts}`, 'debug');
|
|
91
134
|
|
|
92
|
-
// Update watched files to include any new transitive dependencies
|
|
93
135
|
if (server) {
|
|
94
|
-
await updateWatchedFiles(server);
|
|
95
136
|
server.ws.send({ type: 'full-reload' });
|
|
96
137
|
}
|
|
97
138
|
|
|
@@ -138,16 +179,40 @@ export function prismaVitePlugin(
|
|
|
138
179
|
}, debounceMs);
|
|
139
180
|
}
|
|
140
181
|
|
|
141
|
-
|
|
182
|
+
function resolveContractOutputFiles(contractOutput: string | undefined): Set<string> {
|
|
183
|
+
if (contractOutput === undefined) {
|
|
184
|
+
return new Set();
|
|
185
|
+
}
|
|
186
|
+
const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
|
|
187
|
+
return new Set<string>([jsonPath, dtsPath]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isModuleGraphRoot(filePath: string): boolean {
|
|
191
|
+
return MODULE_GRAPH_EXTENSIONS.has(extname(filePath));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function collectModuleGraphFiles(
|
|
195
|
+
viteServer: ViteDevServer,
|
|
196
|
+
roots: readonly string[],
|
|
197
|
+
): Promise<Set<string>> {
|
|
142
198
|
const files = new Set<string>();
|
|
199
|
+
const uniqueRoots = [...new Set(roots)];
|
|
200
|
+
|
|
201
|
+
for (const root of uniqueRoots) {
|
|
202
|
+
try {
|
|
203
|
+
await viteServer.ssrLoadModule(root);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (root === absoluteConfigPath) {
|
|
206
|
+
logError('Failed to load config module graph root:', error);
|
|
207
|
+
} else {
|
|
208
|
+
log(`Skipped module-graph root after load failure: ${root}`, 'debug');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
143
212
|
|
|
144
213
|
try {
|
|
145
|
-
// Load the config module through Vite's SSR loader to populate the module graph
|
|
146
|
-
await viteServer.ssrLoadModule(absoluteConfigPath);
|
|
147
|
-
|
|
148
|
-
// Crawl the module graph starting from the config file
|
|
149
214
|
const visited = new Set<string>();
|
|
150
|
-
const queue = [
|
|
215
|
+
const queue = [...uniqueRoots];
|
|
151
216
|
|
|
152
217
|
while (queue.length > 0) {
|
|
153
218
|
const current = queue.shift();
|
|
@@ -171,15 +236,75 @@ export function prismaVitePlugin(
|
|
|
171
236
|
}
|
|
172
237
|
} catch (error) {
|
|
173
238
|
logError('Failed to collect watched files:', error);
|
|
174
|
-
// At minimum, watch the config file itself
|
|
175
|
-
files.add(absoluteConfigPath);
|
|
176
239
|
}
|
|
177
240
|
|
|
178
241
|
return files;
|
|
179
242
|
}
|
|
180
243
|
|
|
244
|
+
async function resolveWatchedFiles(viteServer: ViteDevServer): Promise<Set<string>> {
|
|
245
|
+
const previousWatchedFiles = new Set(watchedFiles);
|
|
246
|
+
const previousIgnoredOutputFiles = new Set(ignoredOutputFiles);
|
|
247
|
+
ignoredOutputFiles.clear();
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const config = await loadConfig(absoluteConfigPath);
|
|
251
|
+
didWarnConfigWatchFallback = false;
|
|
252
|
+
const contract = config.contract;
|
|
253
|
+
|
|
254
|
+
if (!contract) {
|
|
255
|
+
return new Set([absoluteConfigPath]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const files = new Set<string>([absoluteConfigPath]);
|
|
259
|
+
const inputs = contract.source.inputs ?? [];
|
|
260
|
+
for (const outputFile of resolveContractOutputFiles(contract.output)) {
|
|
261
|
+
ignoredOutputFiles.add(outputFile);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const moduleGraphRoots = [absoluteConfigPath];
|
|
265
|
+
for (const input of inputs) {
|
|
266
|
+
if (!ignoredOutputFiles.has(input)) {
|
|
267
|
+
files.add(input);
|
|
268
|
+
}
|
|
269
|
+
if (isModuleGraphRoot(input)) {
|
|
270
|
+
moduleGraphRoots.push(input);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const file of await collectModuleGraphFiles(viteServer, moduleGraphRoots)) {
|
|
275
|
+
if (!ignoredOutputFiles.has(file)) {
|
|
276
|
+
files.add(file);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return files;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (previousIgnoredOutputFiles.size > 0) {
|
|
283
|
+
for (const outputFile of previousIgnoredOutputFiles) {
|
|
284
|
+
ignoredOutputFiles.add(outputFile);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (!didWarnConfigWatchFallback) {
|
|
288
|
+
didWarnConfigWatchFallback = true;
|
|
289
|
+
const reason = error instanceof Error ? ` ${error.message}` : '';
|
|
290
|
+
const watchScope =
|
|
291
|
+
previousWatchedFiles.size > 0
|
|
292
|
+
? `Watching the previous dependency set plus ${absoluteConfigPath}`
|
|
293
|
+
: `Watching only ${absoluteConfigPath}`;
|
|
294
|
+
logWarning(
|
|
295
|
+
`${watchScope} because Prisma Next config inputs could not be resolved.${reason} Contract watch coverage is partial.`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (previousWatchedFiles.size > 0) {
|
|
299
|
+
previousWatchedFiles.add(absoluteConfigPath);
|
|
300
|
+
return previousWatchedFiles;
|
|
301
|
+
}
|
|
302
|
+
return new Set([absoluteConfigPath]);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
181
306
|
async function updateWatchedFiles(viteServer: ViteDevServer): Promise<void> {
|
|
182
|
-
const newWatchedFiles = await
|
|
307
|
+
const newWatchedFiles = await resolveWatchedFiles(viteServer);
|
|
183
308
|
|
|
184
309
|
// Find files to add and remove
|
|
185
310
|
const toAdd: string[] = [];
|
|
@@ -227,6 +352,9 @@ export function prismaVitePlugin(
|
|
|
227
352
|
|
|
228
353
|
async configureServer(viteServer) {
|
|
229
354
|
server = viteServer;
|
|
355
|
+
const onTrackedWatcherEvent = (file: string) => {
|
|
356
|
+
handleTrackedFileChange(file);
|
|
357
|
+
};
|
|
230
358
|
|
|
231
359
|
// Register close hook to clean up timers and abort in-flight work
|
|
232
360
|
const cleanup = () => {
|
|
@@ -238,6 +366,11 @@ export function prismaVitePlugin(
|
|
|
238
366
|
currentAbortController.abort();
|
|
239
367
|
currentAbortController = null;
|
|
240
368
|
}
|
|
369
|
+
viteServer.watcher.off?.('change', onTrackedWatcherEvent);
|
|
370
|
+
viteServer.watcher.off?.('add', onTrackedWatcherEvent);
|
|
371
|
+
viteServer.watcher.off?.('unlink', onTrackedWatcherEvent);
|
|
372
|
+
ignoredOutputFiles.clear();
|
|
373
|
+
didWarnConfigWatchFallback = false;
|
|
241
374
|
server = null;
|
|
242
375
|
watchedFiles.clear();
|
|
243
376
|
log('Server closed, cleaned up resources', 'debug');
|
|
@@ -246,9 +379,14 @@ export function prismaVitePlugin(
|
|
|
246
379
|
// Register cleanup on server close via httpServer or watcher
|
|
247
380
|
viteServer.httpServer?.on('close', cleanup);
|
|
248
381
|
viteServer.watcher?.on?.('close', cleanup);
|
|
382
|
+
viteServer.watcher.on('change', onTrackedWatcherEvent);
|
|
383
|
+
viteServer.watcher.on('add', onTrackedWatcherEvent);
|
|
384
|
+
viteServer.watcher.on('unlink', onTrackedWatcherEvent);
|
|
249
385
|
|
|
250
|
-
|
|
251
|
-
|
|
386
|
+
const initialWatchedFiles = await resolveWatchedFiles(viteServer);
|
|
387
|
+
|
|
388
|
+
// Collect files to watch from provider metadata
|
|
389
|
+
for (const file of initialWatchedFiles) {
|
|
252
390
|
watchedFiles.add(file);
|
|
253
391
|
}
|
|
254
392
|
|
|
@@ -281,15 +419,11 @@ export function prismaVitePlugin(
|
|
|
281
419
|
}
|
|
282
420
|
|
|
283
421
|
// Initial emit on server start
|
|
284
|
-
await emitContract();
|
|
422
|
+
await emitContract({ refreshWatchedFiles: false });
|
|
285
423
|
},
|
|
286
424
|
|
|
287
425
|
handleHotUpdate(ctx) {
|
|
288
|
-
|
|
289
|
-
if (watchedFiles.has(ctx.file)) {
|
|
290
|
-
log(`Detected change: ${ctx.file}`, 'debug');
|
|
291
|
-
scheduleEmit();
|
|
292
|
-
}
|
|
426
|
+
handleTrackedFileChange(ctx.file);
|
|
293
427
|
},
|
|
294
428
|
};
|
|
295
429
|
}
|