@prisma-next/vite-plugin-contract-emit 0.5.0-dev.5 → 0.5.0-dev.50
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 +40 -14
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +47 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/plugin.ts +69 -32
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
|
-
- **
|
|
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. **
|
|
71
|
-
8. **
|
|
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
|
-
##
|
|
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
|
-
|
|
97
|
-
- **vite**: Peer dependency (>=5.0.0)
|
|
123
|
+
## Dependencies
|
|
98
124
|
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
29
|
-
*
|
|
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
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
|
|
@@ -21,8 +21,8 @@ const MODULE_GRAPH_EXTENSIONS = new Set([
|
|
|
21
21
|
* Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
|
|
22
22
|
*
|
|
23
23
|
* The plugin resolves watched files from contract source provider metadata,
|
|
24
|
-
* re-emitting contract artifacts on changes with debounce
|
|
25
|
-
*
|
|
24
|
+
* re-emitting contract artifacts on changes with debounce while serializing
|
|
25
|
+
* overlapping emits into a single follow-up run.
|
|
26
26
|
*
|
|
27
27
|
* @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
|
|
28
28
|
* @param options - Optional plugin configuration
|
|
@@ -50,10 +50,13 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
50
50
|
let absoluteConfigPath;
|
|
51
51
|
const watchedFiles = /* @__PURE__ */ new Set();
|
|
52
52
|
const ignoredOutputFiles = /* @__PURE__ */ new Set();
|
|
53
|
+
const ownedOutputJsonPaths = /* @__PURE__ */ new Set();
|
|
53
54
|
let debounceTimer = null;
|
|
54
|
-
let
|
|
55
|
+
let lifecycleAbortController = new AbortController();
|
|
55
56
|
let server = null;
|
|
56
|
-
let
|
|
57
|
+
let isEmitInFlight = false;
|
|
58
|
+
let hasQueuedEmit = false;
|
|
59
|
+
let queuedEmitNeedsWatchedFileRefresh = false;
|
|
57
60
|
let didWarnConfigWatchFallback = false;
|
|
58
61
|
function log(message, level = "info") {
|
|
59
62
|
if (logLevel === "silent") return;
|
|
@@ -82,31 +85,24 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
async function emitContract({ refreshWatchedFiles = true } = {}) {
|
|
85
|
-
const
|
|
86
|
-
if (currentAbortController) currentAbortController.abort();
|
|
87
|
-
currentAbortController = new AbortController();
|
|
88
|
-
const signal = currentAbortController.signal;
|
|
88
|
+
const signal = lifecycleAbortController.signal;
|
|
89
89
|
try {
|
|
90
90
|
if (server && refreshWatchedFiles) await updateWatchedFiles(server);
|
|
91
91
|
const result = await executeContractEmit({
|
|
92
92
|
configPath: absoluteConfigPath,
|
|
93
93
|
signal
|
|
94
94
|
});
|
|
95
|
-
if (requestId !== emitRequestId) {
|
|
96
|
-
log("Emit superseded by newer request", "debug");
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
95
|
log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);
|
|
100
96
|
log(` → ${result.files.json}`, "debug");
|
|
101
97
|
log(` → ${result.files.dts}`, "debug");
|
|
102
|
-
if (server) server.ws.send({ type: "full-reload" });
|
|
98
|
+
if (server && !hasQueuedEmit) server.ws.send({ type: "full-reload" });
|
|
99
|
+
else if (hasQueuedEmit) log("Skipped full reload because a newer emit is queued", "debug");
|
|
103
100
|
return result;
|
|
104
101
|
} catch (error) {
|
|
105
102
|
if (signal.aborted || error instanceof Error && error.name === "AbortError") {
|
|
106
103
|
log("Emit cancelled", "debug");
|
|
107
104
|
return null;
|
|
108
105
|
}
|
|
109
|
-
if (requestId !== emitRequestId) return null;
|
|
110
106
|
logError("Contract emit failed:", error);
|
|
111
107
|
if (server) {
|
|
112
108
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -123,16 +119,41 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
123
119
|
return null;
|
|
124
120
|
}
|
|
125
121
|
}
|
|
122
|
+
async function drainQueuedEmits() {
|
|
123
|
+
if (isEmitInFlight || lifecycleAbortController.signal.aborted) return;
|
|
124
|
+
isEmitInFlight = true;
|
|
125
|
+
try {
|
|
126
|
+
while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {
|
|
127
|
+
const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;
|
|
128
|
+
hasQueuedEmit = false;
|
|
129
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
130
|
+
await emitContract({ refreshWatchedFiles });
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
isEmitInFlight = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function requestEmit({ refreshWatchedFiles = true } = {}) {
|
|
137
|
+
if (lifecycleAbortController.signal.aborted) return Promise.resolve();
|
|
138
|
+
hasQueuedEmit = true;
|
|
139
|
+
queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;
|
|
140
|
+
if (isEmitInFlight) {
|
|
141
|
+
log("Queued follow-up emit while another emit is running", "debug");
|
|
142
|
+
return Promise.resolve();
|
|
143
|
+
}
|
|
144
|
+
return drainQueuedEmits();
|
|
145
|
+
}
|
|
126
146
|
function scheduleEmit() {
|
|
127
147
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
128
148
|
debounceTimer = setTimeout(() => {
|
|
129
149
|
debounceTimer = null;
|
|
130
|
-
|
|
150
|
+
requestEmit();
|
|
131
151
|
}, debounceMs);
|
|
132
152
|
}
|
|
133
153
|
function resolveContractOutputFiles(contractOutput) {
|
|
134
154
|
if (contractOutput === void 0) return /* @__PURE__ */ new Set();
|
|
135
155
|
const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
|
|
156
|
+
ownedOutputJsonPaths.add(jsonPath);
|
|
136
157
|
return new Set([jsonPath, dtsPath]);
|
|
137
158
|
}
|
|
138
159
|
function isModuleGraphRoot(filePath) {
|
|
@@ -217,6 +238,10 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
217
238
|
},
|
|
218
239
|
async configureServer(viteServer) {
|
|
219
240
|
server = viteServer;
|
|
241
|
+
lifecycleAbortController = new AbortController();
|
|
242
|
+
isEmitInFlight = false;
|
|
243
|
+
hasQueuedEmit = false;
|
|
244
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
220
245
|
const onTrackedWatcherEvent = (file) => {
|
|
221
246
|
handleTrackedFileChange(file);
|
|
222
247
|
};
|
|
@@ -225,14 +250,15 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
225
250
|
clearTimeout(debounceTimer);
|
|
226
251
|
debounceTimer = null;
|
|
227
252
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
253
|
+
hasQueuedEmit = false;
|
|
254
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
255
|
+
lifecycleAbortController.abort();
|
|
232
256
|
viteServer.watcher.off?.("change", onTrackedWatcherEvent);
|
|
233
257
|
viteServer.watcher.off?.("add", onTrackedWatcherEvent);
|
|
234
258
|
viteServer.watcher.off?.("unlink", onTrackedWatcherEvent);
|
|
235
259
|
ignoredOutputFiles.clear();
|
|
260
|
+
for (const outputJsonPath of ownedOutputJsonPaths) disposeEmitQueue(outputJsonPath);
|
|
261
|
+
ownedOutputJsonPaths.clear();
|
|
236
262
|
didWarnConfigWatchFallback = false;
|
|
237
263
|
server = null;
|
|
238
264
|
watchedFiles.clear();
|
|
@@ -261,7 +287,7 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
261
287
|
log(`Watching ${watchedFiles.size} files`, "debug");
|
|
262
288
|
if (logLevel === "debug") for (const file of watchedFiles) log(` ${file}`, "debug");
|
|
263
289
|
}
|
|
264
|
-
await
|
|
290
|
+
await requestEmit({ refreshWatchedFiles: false });
|
|
265
291
|
},
|
|
266
292
|
handleHotUpdate(ctx) {
|
|
267
293
|
handleTrackedFileChange(ctx.file);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["absoluteConfigPath: string","debounceTimer: ReturnType<typeof setTimeout> | null","currentAbortController: AbortController | null","server: ViteDevServer | null","toAdd: string[]","toRemove: string[]"],"sources":["../src/plugin.ts"],"sourcesContent":["import { 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":["absoluteConfigPath: string","debounceTimer: ReturnType<typeof setTimeout> | 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 { 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 && !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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/vite-plugin-contract-emit",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
],
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"pathe": "^2.0.3",
|
|
12
|
-
"@prisma-next/cli": "0.5.0-dev.
|
|
13
|
-
"@prisma-next/emitter": "0.5.0-dev.
|
|
12
|
+
"@prisma-next/cli": "0.5.0-dev.50",
|
|
13
|
+
"@prisma-next/emitter": "0.5.0-dev.50"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "24.10.4",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@prisma-next/tsdown": "0.0.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"vite": "
|
|
25
|
+
"vite": "^7.0.0 || ^8.0.0"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=20"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"build": "tsdown",
|
|
44
44
|
"test": "vitest run",
|
|
45
45
|
"test:coverage": "vitest run --coverage",
|
|
46
|
-
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
46
|
+
"typecheck": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json --noEmit",
|
|
47
47
|
"lint": "biome check . --error-on-warnings",
|
|
48
48
|
"lint:fix": "biome check --write .",
|
|
49
49
|
"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
|
|
28
|
-
*
|
|
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
|
|
66
|
+
let lifecycleAbortController = new AbortController();
|
|
64
67
|
let server: ViteDevServer | null = null;
|
|
65
|
-
let
|
|
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
|
|
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,14 @@ 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
|
-
if (server) {
|
|
127
|
+
if (server && !hasQueuedEmit) {
|
|
136
128
|
server.ws.send({ type: 'full-reload' });
|
|
129
|
+
} else if (hasQueuedEmit) {
|
|
130
|
+
log('Skipped full reload because a newer emit is queued', 'debug');
|
|
137
131
|
}
|
|
138
132
|
|
|
139
133
|
return result;
|
|
@@ -144,11 +138,6 @@ export function prismaVitePlugin(
|
|
|
144
138
|
return null;
|
|
145
139
|
}
|
|
146
140
|
|
|
147
|
-
// Check if this emit is still the latest request
|
|
148
|
-
if (requestId !== emitRequestId) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
141
|
logError('Contract emit failed:', error);
|
|
153
142
|
|
|
154
143
|
// Send error to Vite overlay
|
|
@@ -169,13 +158,53 @@ export function prismaVitePlugin(
|
|
|
169
158
|
}
|
|
170
159
|
}
|
|
171
160
|
|
|
161
|
+
async function drainQueuedEmits(): Promise<void> {
|
|
162
|
+
if (isEmitInFlight || lifecycleAbortController.signal.aborted) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
isEmitInFlight = true;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {
|
|
170
|
+
const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;
|
|
171
|
+
hasQueuedEmit = false;
|
|
172
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
173
|
+
|
|
174
|
+
await emitContract({ refreshWatchedFiles });
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
isEmitInFlight = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function requestEmit({
|
|
182
|
+
refreshWatchedFiles = true,
|
|
183
|
+
}: {
|
|
184
|
+
refreshWatchedFiles?: boolean;
|
|
185
|
+
} = {}): Promise<void> {
|
|
186
|
+
if (lifecycleAbortController.signal.aborted) {
|
|
187
|
+
return Promise.resolve();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
hasQueuedEmit = true;
|
|
191
|
+
queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;
|
|
192
|
+
|
|
193
|
+
if (isEmitInFlight) {
|
|
194
|
+
log('Queued follow-up emit while another emit is running', 'debug');
|
|
195
|
+
return Promise.resolve();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return drainQueuedEmits();
|
|
199
|
+
}
|
|
200
|
+
|
|
172
201
|
function scheduleEmit() {
|
|
173
202
|
if (debounceTimer) {
|
|
174
203
|
clearTimeout(debounceTimer);
|
|
175
204
|
}
|
|
176
205
|
debounceTimer = setTimeout(() => {
|
|
177
206
|
debounceTimer = null;
|
|
178
|
-
void
|
|
207
|
+
void requestEmit();
|
|
179
208
|
}, debounceMs);
|
|
180
209
|
}
|
|
181
210
|
|
|
@@ -184,6 +213,7 @@ export function prismaVitePlugin(
|
|
|
184
213
|
return new Set();
|
|
185
214
|
}
|
|
186
215
|
const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
|
|
216
|
+
ownedOutputJsonPaths.add(jsonPath);
|
|
187
217
|
return new Set<string>([jsonPath, dtsPath]);
|
|
188
218
|
}
|
|
189
219
|
|
|
@@ -352,24 +382,31 @@ export function prismaVitePlugin(
|
|
|
352
382
|
|
|
353
383
|
async configureServer(viteServer) {
|
|
354
384
|
server = viteServer;
|
|
385
|
+
lifecycleAbortController = new AbortController();
|
|
386
|
+
isEmitInFlight = false;
|
|
387
|
+
hasQueuedEmit = false;
|
|
388
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
355
389
|
const onTrackedWatcherEvent = (file: string) => {
|
|
356
390
|
handleTrackedFileChange(file);
|
|
357
391
|
};
|
|
358
392
|
|
|
359
|
-
// Register close hook to clean up timers and abort in-flight work
|
|
393
|
+
// Register close hook to clean up timers and abort in-flight work.
|
|
360
394
|
const cleanup = () => {
|
|
361
395
|
if (debounceTimer) {
|
|
362
396
|
clearTimeout(debounceTimer);
|
|
363
397
|
debounceTimer = null;
|
|
364
398
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
399
|
+
hasQueuedEmit = false;
|
|
400
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
401
|
+
lifecycleAbortController.abort();
|
|
369
402
|
viteServer.watcher.off?.('change', onTrackedWatcherEvent);
|
|
370
403
|
viteServer.watcher.off?.('add', onTrackedWatcherEvent);
|
|
371
404
|
viteServer.watcher.off?.('unlink', onTrackedWatcherEvent);
|
|
372
405
|
ignoredOutputFiles.clear();
|
|
406
|
+
for (const outputJsonPath of ownedOutputJsonPaths) {
|
|
407
|
+
disposeEmitQueue(outputJsonPath);
|
|
408
|
+
}
|
|
409
|
+
ownedOutputJsonPaths.clear();
|
|
373
410
|
didWarnConfigWatchFallback = false;
|
|
374
411
|
server = null;
|
|
375
412
|
watchedFiles.clear();
|
|
@@ -419,7 +456,7 @@ export function prismaVitePlugin(
|
|
|
419
456
|
}
|
|
420
457
|
|
|
421
458
|
// Initial emit on server start
|
|
422
|
-
await
|
|
459
|
+
await requestEmit({ refreshWatchedFiles: false });
|
|
423
460
|
},
|
|
424
461
|
|
|
425
462
|
handleHotUpdate(ctx) {
|