@prisma-next/vite-plugin-contract-emit 0.5.0-dev.9 → 0.6.0-dev.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 CHANGED
@@ -6,18 +6,28 @@ Vite plugin for automatic Prisma Next contract artifact emission during developm
6
6
 
7
7
  This plugin integrates with Vite's dev server to automatically emit contract artifacts (`contract.json` and `contract.d.ts`) when you start the server and whenever your contract authoring files change.
8
8
 
9
+ ## Support Matrix
10
+
11
+ - Supported Vite majors: 7 and 8
12
+ - Peer dependency range: `^7.0.0 || ^8.0.0`
13
+ - Validation: the repo runs `test/integration/test/vite-plugin.hmr.e2e.test.ts` against both majors
14
+ - Compatibility note: the current implementation uses the same `configureServer` and `handleHotUpdate` flow on Vite 7 and Vite 8; there is no Vite-8-specific code path today, so the support matrix exists to catch future hook or overlay regressions early
15
+
9
16
  ## Features
10
17
 
11
18
  - **Emit on startup**: Emits contract artifacts when the Vite dev server starts
12
19
  - **Config graph + resolved inputs**: Re-emits from the config module graph plus loader-finalized `contract.source.inputs`
13
20
  - **Debounce**: Configurable debounce prevents rapid re-emission during rapid edits
14
- - **Last-change-wins**: Overlapping emit requests are cancelled to avoid stale results
21
+ - **Serialized re-emits**: Overlapping change bursts are coalesced into one follow-up emit instead of cancelling the emit already in flight
22
+ - **Ordered pair publication**: Emits stage temp artifacts, rename `contract.d.ts` before `contract.json`, and attempts to restore the last good pair if publication fails
15
23
  - **Config-only fallback warning**: Falls back to watching the config path and warns when loader-resolved inputs cannot be determined
16
24
  - **Error overlay**: Emission failures are surfaced via Vite's error overlay
17
25
  - **Console logging**: Compact success/error messages with optional debug output
18
26
 
19
27
  ## Installation
20
28
 
29
+ Install the plugin alongside Vite 7 or Vite 8.
30
+
21
31
  ```bash
22
32
  pnpm add -D @prisma-next/vite-plugin-contract-emit vite
23
33
  ```
@@ -67,8 +77,9 @@ interface PrismaVitePluginOptions {
67
77
  4. **Merge declared inputs**: It adds any explicit `contract.source.inputs`, and treats JS/TS inputs as additional module-graph roots
68
78
  5. **Filter emitted artifacts**: Output files are removed from the watch set to avoid self-trigger loops
69
79
  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
80
+ 7. **Publish staged artifacts**: Emits write temp files beside the output paths, rename `contract.d.ts` first, then rename `contract.json`, and attempts to roll back to the previous pair if publication fails
81
+ 8. **Initial emit**: The contract is emitted immediately on server start
82
+ 9. **Queued hot updates**: When any watched file changes, a debounced re-emit is requested; if another emit is already running, the plugin runs one follow-up emit after the current one settles
72
83
 
73
84
  ## Architecture
74
85
 
@@ -86,24 +97,39 @@ graph TD
86
97
  J[File change] --> K[handleHotUpdate hook]
87
98
  K --> L[Schedule debounced emit]
88
99
  L --> M[executeContractEmit]
89
- M --> N[Write artifacts]
90
-
100
+ M --> N[Write temp artifacts]
101
+ N --> O[Rename d.ts then json]
102
+
91
103
  P[Error] --> Q[Overlay or console logging]
92
104
  ```
93
105
 
94
- ## Dependencies
106
+ ## Canonical publish path
107
+
108
+ > **For agents/contributors**: this plugin must publish through
109
+ > `executeContractEmit` from `@prisma-next/cli/control-api`. Do NOT call
110
+ > `publishContractArtifactPair` directly, do NOT re-implement the load → emit
111
+ > → publish dance, and do NOT add a parallel "fast path" for any reason. The
112
+ > atomic-rename invariant and the per-output FIFO queue live in one place;
113
+ > bypassing them races with the CLI command and other callers.
114
+ >
115
+ > Lifecycle: track every `outputJsonPath` you publish to and call
116
+ > `disposeEmitQueue(outputJsonPath)` from your server-close cleanup hook. The
117
+ > per-output queue is module-global; not disposing leaks one entry per unique
118
+ > output path for the lifetime of the process.
119
+ >
120
+ > `ContractEmitResult.validationWarning` is the dependency-validation message
121
+ > from the operation; render it through your plugin's logger when present.
95
122
 
96
- - **@prisma-next/cli**: Uses the control-api `executeContractEmit` operation
97
- - **vite**: Peer dependency (>=5.0.0)
123
+ ## Dependencies
98
124
 
99
- ## Example
125
+ - **@prisma-next/cli**: Uses the control-api `executeContractEmit` and
126
+ `disposeEmitQueue` exports — the canonical publish path
127
+ - **vite**: Peer dependency (`^7.0.0 || ^8.0.0`)
100
128
 
101
- See `examples/prisma-next-demo` for a working example with:
102
- - `vite.config.ts` configured with the plugin
103
- - `pnpm dev` script to start Vite
104
- - `prisma/contract.ts` as the contract authoring source
129
+ ## Examples
105
130
 
106
- Run `pnpm dev` in the demo, edit `prisma/contract.ts`, and watch the artifacts regenerate.
131
+ - `examples/prisma-next-demo` plain Vite + React SPA, covers TS-first and PSL-first contracts. Run `pnpm dev`, edit `prisma/contract.ts`, watch the artifacts regenerate.
132
+ - `examples/react-router-demo` — React Router v7 Framework Mode (SSR). The plugin runs alongside `@react-router/dev/vite`; a `loader` and an `action` exercise the Prisma Next runtime on the server, and a smoke test proves a PSL edit re-emits the contract without a manual command. See `examples/react-router-demo/test/react-router.smoke.e2e.test.ts` for the validation flow.
107
133
 
108
134
  ## Related
109
135
 
package/dist/index.d.mts CHANGED
@@ -25,8 +25,8 @@ interface PrismaVitePluginOptions {
25
25
  * Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
26
26
  *
27
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.
28
+ * re-emitting contract artifacts on changes with debounce while serializing
29
+ * overlapping emits into a single follow-up run.
30
30
  *
31
31
  * @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
32
32
  * @param options - Optional plugin configuration
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/plugin.ts"],"mappings":";;;;;;UAGiB,uBAAA;EAAuB;;;;EAAA,SAK7B,UAAA;;;ACyCX;;;;;WDjCW,QAAA;AAAA;;;;AAbX;;;;;;;;AC8CA;;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,CACd,UAAA,WACA,OAAA,GAAU,uBAAA,GACT,MAAA"}
package/dist/index.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  import { loadConfig } from "@prisma-next/cli/config-loader";
2
- import { executeContractEmit } from "@prisma-next/cli/control-api";
2
+ import { disposeEmitQueue, executeContractEmit } from "@prisma-next/cli/control-api";
3
3
  import { getEmittedArtifactPaths } from "@prisma-next/emitter";
4
4
  import { extname, resolve } from "pathe";
5
-
6
5
  //#region src/plugin.ts
7
6
  const PLUGIN_NAME = "prisma-vite-plugin-contract-emit";
8
7
  const DEFAULT_DEBOUNCE_MS = 150;
@@ -21,8 +20,8 @@ const MODULE_GRAPH_EXTENSIONS = new Set([
21
20
  * Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
22
21
  *
23
22
  * 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.
23
+ * re-emitting contract artifacts on changes with debounce while serializing
24
+ * overlapping emits into a single follow-up run.
26
25
  *
27
26
  * @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
28
27
  * @param options - Optional plugin configuration
@@ -50,10 +49,13 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
50
49
  let absoluteConfigPath;
51
50
  const watchedFiles = /* @__PURE__ */ new Set();
52
51
  const ignoredOutputFiles = /* @__PURE__ */ new Set();
52
+ const ownedOutputJsonPaths = /* @__PURE__ */ new Set();
53
53
  let debounceTimer = null;
54
- let currentAbortController = null;
54
+ let lifecycleAbortController = new AbortController();
55
55
  let server = null;
56
- let emitRequestId = 0;
56
+ let isEmitInFlight = false;
57
+ let hasQueuedEmit = false;
58
+ let queuedEmitNeedsWatchedFileRefresh = false;
57
59
  let didWarnConfigWatchFallback = false;
58
60
  function log(message, level = "info") {
59
61
  if (logLevel === "silent") return;
@@ -82,31 +84,28 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
82
84
  }
83
85
  }
84
86
  async function emitContract({ refreshWatchedFiles = true } = {}) {
85
- const requestId = ++emitRequestId;
86
- if (currentAbortController) currentAbortController.abort();
87
- currentAbortController = new AbortController();
88
- const signal = currentAbortController.signal;
87
+ const signal = lifecycleAbortController.signal;
89
88
  try {
90
89
  if (server && refreshWatchedFiles) await updateWatchedFiles(server);
91
90
  const result = await executeContractEmit({
92
91
  configPath: absoluteConfigPath,
93
92
  signal
94
93
  });
95
- if (requestId !== emitRequestId) {
96
- log("Emit superseded by newer request", "debug");
97
- return null;
98
- }
99
94
  log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);
100
95
  log(` → ${result.files.json}`, "debug");
101
96
  log(` → ${result.files.dts}`, "debug");
102
- if (server) server.ws.send({ type: "full-reload" });
97
+ if (server) {
98
+ server.moduleGraph.onFileChange(result.files.json);
99
+ server.moduleGraph.onFileChange(result.files.dts);
100
+ }
101
+ if (server && !hasQueuedEmit) server.ws.send({ type: "full-reload" });
102
+ else if (hasQueuedEmit) log("Skipped full reload because a newer emit is queued", "debug");
103
103
  return result;
104
104
  } catch (error) {
105
105
  if (signal.aborted || error instanceof Error && error.name === "AbortError") {
106
106
  log("Emit cancelled", "debug");
107
107
  return null;
108
108
  }
109
- if (requestId !== emitRequestId) return null;
110
109
  logError("Contract emit failed:", error);
111
110
  if (server) {
112
111
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -123,16 +122,41 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
123
122
  return null;
124
123
  }
125
124
  }
125
+ async function drainQueuedEmits() {
126
+ if (isEmitInFlight || lifecycleAbortController.signal.aborted) return;
127
+ isEmitInFlight = true;
128
+ try {
129
+ while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {
130
+ const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;
131
+ hasQueuedEmit = false;
132
+ queuedEmitNeedsWatchedFileRefresh = false;
133
+ await emitContract({ refreshWatchedFiles });
134
+ }
135
+ } finally {
136
+ isEmitInFlight = false;
137
+ }
138
+ }
139
+ function requestEmit({ refreshWatchedFiles = true } = {}) {
140
+ if (lifecycleAbortController.signal.aborted) return Promise.resolve();
141
+ hasQueuedEmit = true;
142
+ queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;
143
+ if (isEmitInFlight) {
144
+ log("Queued follow-up emit while another emit is running", "debug");
145
+ return Promise.resolve();
146
+ }
147
+ return drainQueuedEmits();
148
+ }
126
149
  function scheduleEmit() {
127
150
  if (debounceTimer) clearTimeout(debounceTimer);
128
151
  debounceTimer = setTimeout(() => {
129
152
  debounceTimer = null;
130
- emitContract();
153
+ requestEmit();
131
154
  }, debounceMs);
132
155
  }
133
156
  function resolveContractOutputFiles(contractOutput) {
134
157
  if (contractOutput === void 0) return /* @__PURE__ */ new Set();
135
158
  const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
159
+ ownedOutputJsonPaths.add(jsonPath);
136
160
  return new Set([jsonPath, dtsPath]);
137
161
  }
138
162
  function isModuleGraphRoot(filePath) {
@@ -217,6 +241,10 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
217
241
  },
218
242
  async configureServer(viteServer) {
219
243
  server = viteServer;
244
+ lifecycleAbortController = new AbortController();
245
+ isEmitInFlight = false;
246
+ hasQueuedEmit = false;
247
+ queuedEmitNeedsWatchedFileRefresh = false;
220
248
  const onTrackedWatcherEvent = (file) => {
221
249
  handleTrackedFileChange(file);
222
250
  };
@@ -225,14 +253,15 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
225
253
  clearTimeout(debounceTimer);
226
254
  debounceTimer = null;
227
255
  }
228
- if (currentAbortController) {
229
- currentAbortController.abort();
230
- currentAbortController = null;
231
- }
256
+ hasQueuedEmit = false;
257
+ queuedEmitNeedsWatchedFileRefresh = false;
258
+ lifecycleAbortController.abort();
232
259
  viteServer.watcher.off?.("change", onTrackedWatcherEvent);
233
260
  viteServer.watcher.off?.("add", onTrackedWatcherEvent);
234
261
  viteServer.watcher.off?.("unlink", onTrackedWatcherEvent);
235
262
  ignoredOutputFiles.clear();
263
+ for (const outputJsonPath of ownedOutputJsonPaths) disposeEmitQueue(outputJsonPath);
264
+ ownedOutputJsonPaths.clear();
236
265
  didWarnConfigWatchFallback = false;
237
266
  server = null;
238
267
  watchedFiles.clear();
@@ -261,14 +290,14 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
261
290
  log(`Watching ${watchedFiles.size} files`, "debug");
262
291
  if (logLevel === "debug") for (const file of watchedFiles) log(` ${file}`, "debug");
263
292
  }
264
- await emitContract({ refreshWatchedFiles: false });
293
+ await requestEmit({ refreshWatchedFiles: false });
265
294
  },
266
295
  handleHotUpdate(ctx) {
267
296
  handleTrackedFileChange(ctx.file);
268
297
  }
269
298
  };
270
299
  }
271
-
272
300
  //#endregion
273
301
  export { prismaVitePlugin };
302
+
274
303
  //# sourceMappingURL=index.mjs.map
@@ -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 { 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":";;;;;;AAQA,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAC5B,MAAM,sBAAsB;AAC5B,MAAM,0BAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BF,SAAgB,iBACd,aAAqB,qBACrB,SACQ;CACR,MAAM,aAAa,SAAS,cAAc;CAC1C,MAAM,WAAW,SAAS,YAAY;CAEtC,IAAIA;CACJ,MAAM,+BAAe,IAAI,KAAa;CAGtC,MAAM,qCAAqB,IAAI,KAAa;CAC5C,IAAIC,gBAAsD;CAC1D,IAAIC,yBAAiD;CACrD,IAAIC,SAA+B;CACnC,IAAI,gBAAgB;CACpB,IAAI,6BAA6B;CAEjC,SAAS,IAAI,SAAiB,QAA0B,QAAQ;AAC9D,MAAI,aAAa,SAAU;AAC3B,MAAI,UAAU,WAAW,aAAa,QAAS;AAC/C,UAAQ,IAAI,IAAI,YAAY,IAAI,UAAU;;CAG5C,SAAS,SAAS,SAAiB,OAAiB;AAClD,MAAI,aAAa,SAAU;EAC3B,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,QAAQ,OAAO,MAAM,GAAG;AACtF,UAAQ,MAAM,IAAI,YAAY,IAAI,UAAU,eAAe,IAAI,iBAAiB,KAAK;AACrF,MAAI,iBAAiB,SAAS,MAAM,SAAS,aAAa,QACxD,SAAQ,MAAM,MAAM,MAAM;;CAI9B,SAAS,WAAW,SAAiB;AACnC,MAAI,aAAa,SAAU;AAC3B,UAAQ,KAAK,IAAI,YAAY,IAAI,UAAU;;CAG7C,SAAS,wBAAwB,MAAc;EAC7C,MAAM,aAAa,QAAQ,KAAK;AAChC,MAAI,mBAAmB,IAAI,WAAW,EAAE;AACtC,OAAI,qCAAqC,cAAc,QAAQ;AAC/D;;AAGF,MAAI,aAAa,IAAI,WAAW,EAAE;AAChC,OAAI,oBAAoB,cAAc,QAAQ;AAC9C,iBAAc;;;CAIlB,eAAe,aAAa,EAC1B,sBAAsB,SAGpB,EAAE,EAAsC;EAC1C,MAAM,YAAY,EAAE;AAGpB,MAAI,uBACF,wBAAuB,OAAO;AAEhC,2BAAyB,IAAI,iBAAiB;EAC9C,MAAM,SAAS,uBAAuB;AAEtC,MAAI;AACF,OAAI,UAAU,oBACZ,OAAM,mBAAmB,OAAO;GAGlC,MAAM,SAAS,MAAM,oBAAoB;IACvC,YAAY;IACZ;IACD,CAAC;AAGF,OAAI,cAAc,eAAe;AAC/B,QAAI,oCAAoC,QAAQ;AAChD,WAAO;;AAGT,OAAI,kCAAkC,OAAO,YAAY,MAAM,GAAG,EAAE,CAAC,MAAM;AAC3E,OAAI,OAAO,OAAO,MAAM,QAAQ,QAAQ;AACxC,OAAI,OAAO,OAAO,MAAM,OAAO,QAAQ;AAEvC,OAAI,OACF,QAAO,GAAG,KAAK,EAAE,MAAM,eAAe,CAAC;AAGzC,UAAO;WACA,OAAO;AAEd,OAAI,OAAO,WAAY,iBAAiB,SAAS,MAAM,SAAS,cAAe;AAC7E,QAAI,kBAAkB,QAAQ;AAC9B,WAAO;;AAIT,OAAI,cAAc,cAChB,QAAO;AAGT,YAAS,yBAAyB,MAAM;AAGxC,OAAI,QAAQ;IACV,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;IAC3E,MAAM,aAAa,iBAAiB,QAAQ,MAAM,QAAQ;AAC1D,WAAO,GAAG,KAAK;KACb,MAAM;KACN,KAAK;MACH,SAAS,iBAAiB;MAC1B,OAAO,cAAc;MACrB,QAAQ;MACT;KACF,CAAC;;AAGJ,UAAO;;;CAIX,SAAS,eAAe;AACtB,MAAI,cACF,cAAa,cAAc;AAE7B,kBAAgB,iBAAiB;AAC/B,mBAAgB;AAChB,GAAK,cAAc;KAClB,WAAW;;CAGhB,SAAS,2BAA2B,gBAAiD;AACnF,MAAI,mBAAmB,OACrB,wBAAO,IAAI,KAAK;EAElB,MAAM,EAAE,UAAU,YAAY,wBAAwB,eAAe;AACrE,SAAO,IAAI,IAAY,CAAC,UAAU,QAAQ,CAAC;;CAG7C,SAAS,kBAAkB,UAA2B;AACpD,SAAO,wBAAwB,IAAI,QAAQ,SAAS,CAAC;;CAGvD,eAAe,wBACb,YACA,OACsB;EACtB,MAAM,wBAAQ,IAAI,KAAa;EAC/B,MAAM,cAAc,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAEvC,OAAK,MAAM,QAAQ,YACjB,KAAI;AACF,SAAM,WAAW,cAAc,KAAK;WAC7B,OAAO;AACd,OAAI,SAAS,mBACX,UAAS,4CAA4C,MAAM;OAE3D,KAAI,iDAAiD,QAAQ,QAAQ;;AAK3E,MAAI;GACF,MAAM,0BAAU,IAAI,KAAa;GACjC,MAAM,QAAQ,CAAC,GAAG,YAAY;AAE9B,UAAO,MAAM,SAAS,GAAG;IACvB,MAAM,UAAU,MAAM,OAAO;AAC7B,QAAI,YAAY,UAAa,QAAQ,IAAI,QAAQ,CAAE;AACnD,YAAQ,IAAI,QAAQ;IAEpB,MAAM,MAAM,WAAW,YAAY,cAAc,QAAQ;AACzD,QAAI,CAAC,IAAK;AAGV,QAAI,IAAI,KACN,OAAM,IAAI,IAAI,KAAK;AAIrB,SAAK,MAAM,YAAY,IAAI,gBACzB,KAAI,SAAS,MAAM,CAAC,QAAQ,IAAI,SAAS,GAAG,CAC1C,OAAM,KAAK,SAAS,GAAG;;WAItB,OAAO;AACd,YAAS,oCAAoC,MAAM;;AAGrD,SAAO;;CAGT,eAAe,oBAAoB,YAAiD;EAClF,MAAM,uBAAuB,IAAI,IAAI,aAAa;EAClD,MAAM,6BAA6B,IAAI,IAAI,mBAAmB;AAC9D,qBAAmB,OAAO;AAE1B,MAAI;GACF,MAAM,SAAS,MAAM,WAAW,mBAAmB;AACnD,gCAA6B;GAC7B,MAAM,WAAW,OAAO;AAExB,OAAI,CAAC,SACH,QAAO,IAAI,IAAI,CAAC,mBAAmB,CAAC;GAGtC,MAAM,QAAQ,IAAI,IAAY,CAAC,mBAAmB,CAAC;GACnD,MAAM,SAAS,SAAS,OAAO,UAAU,EAAE;AAC3C,QAAK,MAAM,cAAc,2BAA2B,SAAS,OAAO,CAClE,oBAAmB,IAAI,WAAW;GAGpC,MAAM,mBAAmB,CAAC,mBAAmB;AAC7C,QAAK,MAAM,SAAS,QAAQ;AAC1B,QAAI,CAAC,mBAAmB,IAAI,MAAM,CAChC,OAAM,IAAI,MAAM;AAElB,QAAI,kBAAkB,MAAM,CAC1B,kBAAiB,KAAK,MAAM;;AAIhC,QAAK,MAAM,QAAQ,MAAM,wBAAwB,YAAY,iBAAiB,CAC5E,KAAI,CAAC,mBAAmB,IAAI,KAAK,CAC/B,OAAM,IAAI,KAAK;AAInB,UAAO;WACA,OAAO;AACd,OAAI,2BAA2B,OAAO,EACpC,MAAK,MAAM,cAAc,2BACvB,oBAAmB,IAAI,WAAW;AAGtC,OAAI,CAAC,4BAA4B;AAC/B,iCAA6B;IAC7B,MAAM,SAAS,iBAAiB,QAAQ,IAAI,MAAM,YAAY;AAK9D,eACE,GAJA,qBAAqB,OAAO,IACxB,6CAA6C,uBAC7C,iBAAiB,qBAEP,2DAA2D,OAAO,sCACjF;;AAEH,OAAI,qBAAqB,OAAO,GAAG;AACjC,yBAAqB,IAAI,mBAAmB;AAC5C,WAAO;;AAET,UAAO,IAAI,IAAI,CAAC,mBAAmB,CAAC;;;CAIxC,eAAe,mBAAmB,YAA0C;EAC1E,MAAM,kBAAkB,MAAM,oBAAoB,WAAW;EAG7D,MAAMC,QAAkB,EAAE;EAC1B,MAAMC,WAAqB,EAAE;AAE7B,OAAK,MAAM,QAAQ,gBACjB,KAAI,CAAC,aAAa,IAAI,KAAK,CACzB,OAAM,KAAK,KAAK;AAIpB,OAAK,MAAM,QAAQ,aACjB,KAAI,CAAC,gBAAgB,IAAI,KAAK,CAC5B,UAAS,KAAK,KAAK;AAKvB,OAAK,MAAM,QAAQ,MACjB,YAAW,QAAQ,IAAI,KAAK;AAE9B,OAAK,MAAM,QAAQ,SACjB,YAAW,QAAQ,QAAQ,KAAK;AAIlC,eAAa,OAAO;AACpB,OAAK,MAAM,QAAQ,gBACjB,cAAa,IAAI,KAAK;AAGxB,MAAI,MAAM,SAAS,KAAK,SAAS,SAAS,EACxC,KAAI,2BAA2B,MAAM,OAAO,IAAI,SAAS,UAAU,QAAQ;;AAI/E,QAAO;EACL,MAAM;EAEN,eAAe,QAAQ;AAErB,wBAAqB,QAAQ,OAAO,MAAM,WAAW;AACrD,OAAI,gBAAgB,sBAAsB,QAAQ;;EAGpD,MAAM,gBAAgB,YAAY;AAChC,YAAS;GACT,MAAM,yBAAyB,SAAiB;AAC9C,4BAAwB,KAAK;;GAI/B,MAAM,gBAAgB;AACpB,QAAI,eAAe;AACjB,kBAAa,cAAc;AAC3B,qBAAgB;;AAElB,QAAI,wBAAwB;AAC1B,4BAAuB,OAAO;AAC9B,8BAAyB;;AAE3B,eAAW,QAAQ,MAAM,UAAU,sBAAsB;AACzD,eAAW,QAAQ,MAAM,OAAO,sBAAsB;AACtD,eAAW,QAAQ,MAAM,UAAU,sBAAsB;AACzD,uBAAmB,OAAO;AAC1B,iCAA6B;AAC7B,aAAS;AACT,iBAAa,OAAO;AACpB,QAAI,uCAAuC,QAAQ;;AAIrD,cAAW,YAAY,GAAG,SAAS,QAAQ;AAC3C,cAAW,SAAS,KAAK,SAAS,QAAQ;AAC1C,cAAW,QAAQ,GAAG,UAAU,sBAAsB;AACtD,cAAW,QAAQ,GAAG,OAAO,sBAAsB;AACnD,cAAW,QAAQ,GAAG,UAAU,sBAAsB;GAEtD,MAAM,sBAAsB,MAAM,oBAAoB,WAAW;AAGjE,QAAK,MAAM,QAAQ,oBACjB,cAAa,IAAI,KAAK;AAIxB,QAAK,MAAM,QAAQ,aACjB,YAAW,QAAQ,IAAI,KAAK;AAI9B,OAAI,aAAa,SAAS,GAAG;IAC3B,MAAM,eACJ,gDAAgD,mBAAmB;AAErE,aAAS,aAAa;AACtB,eAAW,GAAG,KAAK;KACjB,MAAM;KACN,KAAK;MACH,SAAS,iBAAiB;MAC1B,OAAO;MACP,QAAQ;MACT;KACF,CAAC;UACG;AACL,QAAI,YAAY,aAAa,KAAK,SAAS,QAAQ;AACnD,QAAI,aAAa,QACf,MAAK,MAAM,QAAQ,aACjB,KAAI,KAAK,QAAQ,QAAQ;;AAM/B,SAAM,aAAa,EAAE,qBAAqB,OAAO,CAAC;;EAGpD,gBAAgB,KAAK;AACnB,2BAAwB,IAAI,KAAK;;EAEpC"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import { loadConfig } from '@prisma-next/cli/config-loader';\nimport type { ContractEmitResult } from '@prisma-next/cli/control-api';\nimport { disposeEmitQueue, 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 while serializing\n * overlapping emits into a single follow-up run.\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 // Output JSON paths whose serialization queue this plugin instance owns. Disposed on cleanup\n // so long-lived dev sessions don't accumulate per-process queue state across config edits.\n const ownedOutputJsonPaths = new Set<string>();\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let lifecycleAbortController = new AbortController();\n let server: ViteDevServer | null = null;\n let isEmitInFlight = false;\n let hasQueuedEmit = false;\n let queuedEmitNeedsWatchedFileRefresh = false;\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 signal = lifecycleAbortController.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 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.moduleGraph.onFileChange(result.files.json);\n server.moduleGraph.onFileChange(result.files.dts);\n }\n\n if (server && !hasQueuedEmit) {\n server.ws.send({ type: 'full-reload' });\n } else if (hasQueuedEmit) {\n log('Skipped full reload because a newer emit is queued', 'debug');\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 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 async function drainQueuedEmits(): Promise<void> {\n if (isEmitInFlight || lifecycleAbortController.signal.aborted) {\n return;\n }\n\n isEmitInFlight = true;\n\n try {\n while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {\n const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;\n hasQueuedEmit = false;\n queuedEmitNeedsWatchedFileRefresh = false;\n\n await emitContract({ refreshWatchedFiles });\n }\n } finally {\n isEmitInFlight = false;\n }\n }\n\n function requestEmit({\n refreshWatchedFiles = true,\n }: {\n refreshWatchedFiles?: boolean;\n } = {}): Promise<void> {\n if (lifecycleAbortController.signal.aborted) {\n return Promise.resolve();\n }\n\n hasQueuedEmit = true;\n queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;\n\n if (isEmitInFlight) {\n log('Queued follow-up emit while another emit is running', 'debug');\n return Promise.resolve();\n }\n\n return drainQueuedEmits();\n }\n\n function scheduleEmit() {\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void requestEmit();\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 ownedOutputJsonPaths.add(jsonPath);\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 lifecycleAbortController = new AbortController();\n isEmitInFlight = false;\n hasQueuedEmit = false;\n queuedEmitNeedsWatchedFileRefresh = false;\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 hasQueuedEmit = false;\n queuedEmitNeedsWatchedFileRefresh = false;\n lifecycleAbortController.abort();\n viteServer.watcher.off?.('change', onTrackedWatcherEvent);\n viteServer.watcher.off?.('add', onTrackedWatcherEvent);\n viteServer.watcher.off?.('unlink', onTrackedWatcherEvent);\n ignoredOutputFiles.clear();\n for (const outputJsonPath of ownedOutputJsonPaths) {\n disposeEmitQueue(outputJsonPath);\n }\n ownedOutputJsonPaths.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 requestEmit({ 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,7 @@
1
1
  {
2
2
  "name": "@prisma-next/vite-plugin-contract-emit",
3
- "version": "0.5.0-dev.9",
3
+ "version": "0.6.0-dev.1",
4
+ "license": "Apache-2.0",
4
5
  "type": "module",
5
6
  "sideEffects": false,
6
7
  "files": [
@@ -9,20 +10,20 @@
9
10
  ],
10
11
  "dependencies": {
11
12
  "pathe": "^2.0.3",
12
- "@prisma-next/emitter": "0.5.0-dev.9",
13
- "@prisma-next/cli": "0.5.0-dev.9"
13
+ "@prisma-next/cli": "0.6.0-dev.1",
14
+ "@prisma-next/emitter": "0.6.0-dev.1"
14
15
  },
15
16
  "devDependencies": {
16
17
  "@types/node": "24.10.4",
17
- "tsdown": "0.18.4",
18
+ "tsdown": "0.22.0",
18
19
  "typescript": "5.9.3",
19
- "vite": "7.3.1",
20
- "vitest": "4.0.17",
20
+ "vite": "8.0.11",
21
+ "vitest": "4.1.5",
21
22
  "@prisma-next/tsconfig": "0.0.0",
22
23
  "@prisma-next/tsdown": "0.0.0"
23
24
  },
24
25
  "peerDependencies": {
25
- "vite": ">=5.0.0"
26
+ "vite": "^7.0.0 || ^8.0.0"
26
27
  },
27
28
  "engines": {
28
29
  "node": ">=20"
@@ -31,8 +32,6 @@
31
32
  ".": "./dist/index.mjs",
32
33
  "./package.json": "./package.json"
33
34
  },
34
- "main": "./dist/index.mjs",
35
- "module": "./dist/index.mjs",
36
35
  "types": "./dist/index.d.mts",
37
36
  "repository": {
38
37
  "type": "git",
@@ -43,7 +42,7 @@
43
42
  "build": "tsdown",
44
43
  "test": "vitest run",
45
44
  "test:coverage": "vitest run --coverage",
46
- "typecheck": "tsc --project tsconfig.json --noEmit",
45
+ "typecheck": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json --noEmit",
47
46
  "lint": "biome check . --error-on-warnings",
48
47
  "lint:fix": "biome check --write .",
49
48
  "lint:fix:unsafe": "biome check --write --unsafe .",
package/src/plugin.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from '@prisma-next/cli/config-loader';
2
2
  import type { ContractEmitResult } from '@prisma-next/cli/control-api';
3
- import { executeContractEmit } from '@prisma-next/cli/control-api';
3
+ import { disposeEmitQueue, executeContractEmit } from '@prisma-next/cli/control-api';
4
4
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
5
5
  import { extname, resolve } from 'pathe';
6
6
  import type { Plugin, ViteDevServer } from 'vite';
@@ -24,8 +24,8 @@ const MODULE_GRAPH_EXTENSIONS = new Set([
24
24
  * Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
25
25
  *
26
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.
27
+ * re-emitting contract artifacts on changes with debounce while serializing
28
+ * overlapping emits into a single follow-up run.
29
29
  *
30
30
  * @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
31
31
  * @param options - Optional plugin configuration
@@ -59,10 +59,15 @@ export function prismaVitePlugin(
59
59
  // Vite watches the project root, so writes to emitted artifacts can still surface as change
60
60
  // events even when those files are excluded from watchedFiles.
61
61
  const ignoredOutputFiles = new Set<string>();
62
+ // Output JSON paths whose serialization queue this plugin instance owns. Disposed on cleanup
63
+ // so long-lived dev sessions don't accumulate per-process queue state across config edits.
64
+ const ownedOutputJsonPaths = new Set<string>();
62
65
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
63
- let currentAbortController: AbortController | null = null;
66
+ let lifecycleAbortController = new AbortController();
64
67
  let server: ViteDevServer | null = null;
65
- let emitRequestId = 0;
68
+ let isEmitInFlight = false;
69
+ let hasQueuedEmit = false;
70
+ let queuedEmitNeedsWatchedFileRefresh = false;
66
71
  let didWarnConfigWatchFallback = false;
67
72
 
68
73
  function log(message: string, level: 'info' | 'debug' = 'info') {
@@ -103,14 +108,7 @@ export function prismaVitePlugin(
103
108
  }: {
104
109
  refreshWatchedFiles?: boolean;
105
110
  } = {}): Promise<ContractEmitResult | null> {
106
- const requestId = ++emitRequestId;
107
-
108
- // Cancel any in-flight emit
109
- if (currentAbortController) {
110
- currentAbortController.abort();
111
- }
112
- currentAbortController = new AbortController();
113
- const signal = currentAbortController.signal;
111
+ const signal = lifecycleAbortController.signal;
114
112
 
115
113
  try {
116
114
  if (server && refreshWatchedFiles) {
@@ -122,18 +120,19 @@ export function prismaVitePlugin(
122
120
  signal,
123
121
  });
124
122
 
125
- // Check if this emit is still the latest request
126
- if (requestId !== emitRequestId) {
127
- log('Emit superseded by newer request', 'debug');
128
- return null;
129
- }
130
-
131
123
  log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);
132
124
  log(` → ${result.files.json}`, 'debug');
133
125
  log(` → ${result.files.dts}`, 'debug');
134
126
 
135
127
  if (server) {
128
+ server.moduleGraph.onFileChange(result.files.json);
129
+ server.moduleGraph.onFileChange(result.files.dts);
130
+ }
131
+
132
+ if (server && !hasQueuedEmit) {
136
133
  server.ws.send({ type: 'full-reload' });
134
+ } else if (hasQueuedEmit) {
135
+ log('Skipped full reload because a newer emit is queued', 'debug');
137
136
  }
138
137
 
139
138
  return result;
@@ -144,11 +143,6 @@ export function prismaVitePlugin(
144
143
  return null;
145
144
  }
146
145
 
147
- // Check if this emit is still the latest request
148
- if (requestId !== emitRequestId) {
149
- return null;
150
- }
151
-
152
146
  logError('Contract emit failed:', error);
153
147
 
154
148
  // Send error to Vite overlay
@@ -169,13 +163,53 @@ export function prismaVitePlugin(
169
163
  }
170
164
  }
171
165
 
166
+ async function drainQueuedEmits(): Promise<void> {
167
+ if (isEmitInFlight || lifecycleAbortController.signal.aborted) {
168
+ return;
169
+ }
170
+
171
+ isEmitInFlight = true;
172
+
173
+ try {
174
+ while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {
175
+ const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;
176
+ hasQueuedEmit = false;
177
+ queuedEmitNeedsWatchedFileRefresh = false;
178
+
179
+ await emitContract({ refreshWatchedFiles });
180
+ }
181
+ } finally {
182
+ isEmitInFlight = false;
183
+ }
184
+ }
185
+
186
+ function requestEmit({
187
+ refreshWatchedFiles = true,
188
+ }: {
189
+ refreshWatchedFiles?: boolean;
190
+ } = {}): Promise<void> {
191
+ if (lifecycleAbortController.signal.aborted) {
192
+ return Promise.resolve();
193
+ }
194
+
195
+ hasQueuedEmit = true;
196
+ queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;
197
+
198
+ if (isEmitInFlight) {
199
+ log('Queued follow-up emit while another emit is running', 'debug');
200
+ return Promise.resolve();
201
+ }
202
+
203
+ return drainQueuedEmits();
204
+ }
205
+
172
206
  function scheduleEmit() {
173
207
  if (debounceTimer) {
174
208
  clearTimeout(debounceTimer);
175
209
  }
176
210
  debounceTimer = setTimeout(() => {
177
211
  debounceTimer = null;
178
- void emitContract();
212
+ void requestEmit();
179
213
  }, debounceMs);
180
214
  }
181
215
 
@@ -184,6 +218,7 @@ export function prismaVitePlugin(
184
218
  return new Set();
185
219
  }
186
220
  const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
221
+ ownedOutputJsonPaths.add(jsonPath);
187
222
  return new Set<string>([jsonPath, dtsPath]);
188
223
  }
189
224
 
@@ -352,24 +387,31 @@ export function prismaVitePlugin(
352
387
 
353
388
  async configureServer(viteServer) {
354
389
  server = viteServer;
390
+ lifecycleAbortController = new AbortController();
391
+ isEmitInFlight = false;
392
+ hasQueuedEmit = false;
393
+ queuedEmitNeedsWatchedFileRefresh = false;
355
394
  const onTrackedWatcherEvent = (file: string) => {
356
395
  handleTrackedFileChange(file);
357
396
  };
358
397
 
359
- // Register close hook to clean up timers and abort in-flight work
398
+ // Register close hook to clean up timers and abort in-flight work.
360
399
  const cleanup = () => {
361
400
  if (debounceTimer) {
362
401
  clearTimeout(debounceTimer);
363
402
  debounceTimer = null;
364
403
  }
365
- if (currentAbortController) {
366
- currentAbortController.abort();
367
- currentAbortController = null;
368
- }
404
+ hasQueuedEmit = false;
405
+ queuedEmitNeedsWatchedFileRefresh = false;
406
+ lifecycleAbortController.abort();
369
407
  viteServer.watcher.off?.('change', onTrackedWatcherEvent);
370
408
  viteServer.watcher.off?.('add', onTrackedWatcherEvent);
371
409
  viteServer.watcher.off?.('unlink', onTrackedWatcherEvent);
372
410
  ignoredOutputFiles.clear();
411
+ for (const outputJsonPath of ownedOutputJsonPaths) {
412
+ disposeEmitQueue(outputJsonPath);
413
+ }
414
+ ownedOutputJsonPaths.clear();
373
415
  didWarnConfigWatchFallback = false;
374
416
  server = null;
375
417
  watchedFiles.clear();
@@ -419,7 +461,7 @@ export function prismaVitePlugin(
419
461
  }
420
462
 
421
463
  // Initial emit on server start
422
- await emitContract({ refreshWatchedFiles: false });
464
+ await requestEmit({ refreshWatchedFiles: false });
423
465
  },
424
466
 
425
467
  handleHotUpdate(ctx) {