@prisma-next/vite-plugin-contract-emit 0.4.0-dev.8 → 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 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
- - **Watch mode**: Re-emits on changes to the config file and its transitive imports
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 your config module through Vite's SSR loader
64
- 2. **Module graph crawling**: It crawls the module graph to find all transitive imports
65
- 3. **Watch setup**: All discovered files are added to Vite's watcher
66
- 4. **Initial emit**: The contract is emitted immediately on server start
67
- 5. **Hot updates**: When any watched file changes, a debounced re-emit is triggered
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 SSR]
76
- D --> E[Crawl module graph]
77
- E --> F[Add files to watcher]
78
- F --> G[Initial emit]
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
- H[File change] --> I[handleHotUpdate hook]
81
- I --> J[Schedule debounced emit]
82
- J --> K[executeContractEmit]
83
- K --> L[Write artifacts]
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
- M[Error] --> N[Error overlay + console]
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 watches the config file and its transitive dependencies, re-emitting
28
- * contract artifacts on changes with debounce and "last change wins" semantics.
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/plugin.ts"],"sourcesContent":[],"mappings":";;;;;;AAGiB,UAAA,uBAAA,CAAuB;;;;ACiCxC;;;;;;;;;;;;;;ADjCA;;;;ACiCA;;;;;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,gCAEJ,0BACT"}
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 { resolve } from "node:path";
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 watches the config file and its transitive dependencies, re-emitting
12
- * contract artifacts on changes with debounce and "last change wins" semantics.
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
- async function emitContract() {
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
- async function collectWatchedFiles(viteServer) {
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 = [absoluteConfigPath];
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 collectWatchedFiles(viteServer);
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
- for (const file of await collectWatchedFiles(viteServer)) watchedFiles.add(file);
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
- if (watchedFiles.has(ctx.file)) {
182
- log(`Detected change: ${ctx.file}`, "debug");
183
- scheduleEmit();
184
- }
267
+ handleTrackedFileChange(ctx.file);
185
268
  }
186
269
  };
187
270
  }
@@ -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":";;;;AAMA,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAC5B,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4B5B,SAAgB,iBACd,aAAqB,qBACrB,SACQ;CACR,MAAM,aAAa,SAAS,cAAc;CAC1C,MAAM,WAAW,SAAS,YAAY;CAEtC,IAAIA;CACJ,MAAM,+BAAe,IAAI,KAAa;CACtC,IAAIC,gBAAsD;CAC1D,IAAIC,yBAAiD;CACrD,IAAIC,SAA+B;CACnC,IAAI,gBAAgB;CAEpB,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,eAAe,eAAmD;EAChE,MAAM,YAAY,EAAE;AAGpB,MAAI,uBACF,wBAAuB,OAAO;AAEhC,2BAAyB,IAAI,iBAAiB;EAC9C,MAAM,SAAS,uBAAuB;AAEtC,MAAI;GACF,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;AAGvC,OAAI,QAAQ;AACV,UAAM,mBAAmB,OAAO;AAChC,WAAO,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,eAAe,oBAAoB,YAAiD;EAClF,MAAM,wBAAQ,IAAI,KAAa;AAE/B,MAAI;AAEF,SAAM,WAAW,cAAc,mBAAmB;GAGlD,MAAM,0BAAU,IAAI,KAAa;GACjC,MAAM,QAAQ,CAAC,mBAAmB;AAElC,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;AAEnD,SAAM,IAAI,mBAAmB;;AAG/B,SAAO;;CAGT,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;GAGT,MAAM,gBAAgB;AACpB,QAAI,eAAe;AACjB,kBAAa,cAAc;AAC3B,qBAAgB;;AAElB,QAAI,wBAAwB;AAC1B,4BAAuB,OAAO;AAC9B,8BAAyB;;AAE3B,aAAS;AACT,iBAAa,OAAO;AACpB,QAAI,uCAAuC,QAAQ;;AAIrD,cAAW,YAAY,GAAG,SAAS,QAAQ;AAC3C,cAAW,SAAS,KAAK,SAAS,QAAQ;AAG1C,QAAK,MAAM,QAAQ,MAAM,oBAAoB,WAAW,CACtD,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,cAAc;;EAGtB,gBAAgB,KAAK;AAEnB,OAAI,aAAa,IAAI,IAAI,KAAK,EAAE;AAC9B,QAAI,oBAAoB,IAAI,QAAQ,QAAQ;AAC5C,kBAAc;;;EAGnB"}
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.0-dev.8",
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
- "@prisma-next/cli": "0.4.0-dev.8"
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/tsdown": "0.0.0",
20
- "@prisma-next/tsconfig": "0.0.0"
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 { resolve } from 'node:path';
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 watches the config file and its transitive dependencies, re-emitting
15
- * contract artifacts on changes with debounce and "last change wins" semantics.
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
- async function emitContract(): Promise<ContractEmitResult | null> {
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
- async function collectWatchedFiles(viteServer: ViteDevServer): Promise<Set<string>> {
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 = [absoluteConfigPath];
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 collectWatchedFiles(viteServer);
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
- // Collect files to watch from the module graph
251
- for (const file of await collectWatchedFiles(viteServer)) {
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
- // Check if the changed file is one we're watching
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
  }