@prisma-next/vite-plugin-contract-emit 0.5.0-dev.8 → 0.5.0-dev.80
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.d.mts.map +1 -1
- package/dist/index.mjs +52 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -10
- package/src/plugin.ts +73 -31
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.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/plugin.ts"],"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/plugin.ts"],"mappings":";;;;;;UAGiB,uBAAA;EAAuB;;;;EAAA,SAK7B,UAAA;;;ACyCX;;;;;WDjCW,QAAA;AAAA;;;;AAbX;;;;;;;;AC8CA;;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,CACd,UAAA,WACA,OAAA,GAAU,uBAAA,GACT,MAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { loadConfig } from "@prisma-next/cli/config-loader";
|
|
2
|
-
import { executeContractEmit } from "@prisma-next/cli/control-api";
|
|
2
|
+
import { disposeEmitQueue, executeContractEmit } from "@prisma-next/cli/control-api";
|
|
3
3
|
import { getEmittedArtifactPaths } from "@prisma-next/emitter";
|
|
4
4
|
import { extname, resolve } from "pathe";
|
|
5
|
-
|
|
6
5
|
//#region src/plugin.ts
|
|
7
6
|
const PLUGIN_NAME = "prisma-vite-plugin-contract-emit";
|
|
8
7
|
const DEFAULT_DEBOUNCE_MS = 150;
|
|
@@ -21,8 +20,8 @@ const MODULE_GRAPH_EXTENSIONS = new Set([
|
|
|
21
20
|
* Creates a Vite plugin that automatically emits Prisma Next contract artifacts.
|
|
22
21
|
*
|
|
23
22
|
* The plugin resolves watched files from contract source provider metadata,
|
|
24
|
-
* re-emitting contract artifacts on changes with debounce
|
|
25
|
-
*
|
|
23
|
+
* re-emitting contract artifacts on changes with debounce while serializing
|
|
24
|
+
* overlapping emits into a single follow-up run.
|
|
26
25
|
*
|
|
27
26
|
* @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'
|
|
28
27
|
* @param options - Optional plugin configuration
|
|
@@ -50,10 +49,13 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
50
49
|
let absoluteConfigPath;
|
|
51
50
|
const watchedFiles = /* @__PURE__ */ new Set();
|
|
52
51
|
const ignoredOutputFiles = /* @__PURE__ */ new Set();
|
|
52
|
+
const ownedOutputJsonPaths = /* @__PURE__ */ new Set();
|
|
53
53
|
let debounceTimer = null;
|
|
54
|
-
let
|
|
54
|
+
let lifecycleAbortController = new AbortController();
|
|
55
55
|
let server = null;
|
|
56
|
-
let
|
|
56
|
+
let isEmitInFlight = false;
|
|
57
|
+
let hasQueuedEmit = false;
|
|
58
|
+
let queuedEmitNeedsWatchedFileRefresh = false;
|
|
57
59
|
let didWarnConfigWatchFallback = false;
|
|
58
60
|
function log(message, level = "info") {
|
|
59
61
|
if (logLevel === "silent") return;
|
|
@@ -82,31 +84,28 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
async function emitContract({ refreshWatchedFiles = true } = {}) {
|
|
85
|
-
const
|
|
86
|
-
if (currentAbortController) currentAbortController.abort();
|
|
87
|
-
currentAbortController = new AbortController();
|
|
88
|
-
const signal = currentAbortController.signal;
|
|
87
|
+
const signal = lifecycleAbortController.signal;
|
|
89
88
|
try {
|
|
90
89
|
if (server && refreshWatchedFiles) await updateWatchedFiles(server);
|
|
91
90
|
const result = await executeContractEmit({
|
|
92
91
|
configPath: absoluteConfigPath,
|
|
93
92
|
signal
|
|
94
93
|
});
|
|
95
|
-
if (requestId !== emitRequestId) {
|
|
96
|
-
log("Emit superseded by newer request", "debug");
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
94
|
log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);
|
|
100
95
|
log(` → ${result.files.json}`, "debug");
|
|
101
96
|
log(` → ${result.files.dts}`, "debug");
|
|
102
|
-
if (server)
|
|
97
|
+
if (server) {
|
|
98
|
+
server.moduleGraph.onFileChange(result.files.json);
|
|
99
|
+
server.moduleGraph.onFileChange(result.files.dts);
|
|
100
|
+
}
|
|
101
|
+
if (server && !hasQueuedEmit) server.ws.send({ type: "full-reload" });
|
|
102
|
+
else if (hasQueuedEmit) log("Skipped full reload because a newer emit is queued", "debug");
|
|
103
103
|
return result;
|
|
104
104
|
} catch (error) {
|
|
105
105
|
if (signal.aborted || error instanceof Error && error.name === "AbortError") {
|
|
106
106
|
log("Emit cancelled", "debug");
|
|
107
107
|
return null;
|
|
108
108
|
}
|
|
109
|
-
if (requestId !== emitRequestId) return null;
|
|
110
109
|
logError("Contract emit failed:", error);
|
|
111
110
|
if (server) {
|
|
112
111
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -123,16 +122,41 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
123
122
|
return null;
|
|
124
123
|
}
|
|
125
124
|
}
|
|
125
|
+
async function drainQueuedEmits() {
|
|
126
|
+
if (isEmitInFlight || lifecycleAbortController.signal.aborted) return;
|
|
127
|
+
isEmitInFlight = true;
|
|
128
|
+
try {
|
|
129
|
+
while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {
|
|
130
|
+
const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;
|
|
131
|
+
hasQueuedEmit = false;
|
|
132
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
133
|
+
await emitContract({ refreshWatchedFiles });
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
isEmitInFlight = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function requestEmit({ refreshWatchedFiles = true } = {}) {
|
|
140
|
+
if (lifecycleAbortController.signal.aborted) return Promise.resolve();
|
|
141
|
+
hasQueuedEmit = true;
|
|
142
|
+
queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;
|
|
143
|
+
if (isEmitInFlight) {
|
|
144
|
+
log("Queued follow-up emit while another emit is running", "debug");
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
}
|
|
147
|
+
return drainQueuedEmits();
|
|
148
|
+
}
|
|
126
149
|
function scheduleEmit() {
|
|
127
150
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
128
151
|
debounceTimer = setTimeout(() => {
|
|
129
152
|
debounceTimer = null;
|
|
130
|
-
|
|
153
|
+
requestEmit();
|
|
131
154
|
}, debounceMs);
|
|
132
155
|
}
|
|
133
156
|
function resolveContractOutputFiles(contractOutput) {
|
|
134
157
|
if (contractOutput === void 0) return /* @__PURE__ */ new Set();
|
|
135
158
|
const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
|
|
159
|
+
ownedOutputJsonPaths.add(jsonPath);
|
|
136
160
|
return new Set([jsonPath, dtsPath]);
|
|
137
161
|
}
|
|
138
162
|
function isModuleGraphRoot(filePath) {
|
|
@@ -217,6 +241,10 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
217
241
|
},
|
|
218
242
|
async configureServer(viteServer) {
|
|
219
243
|
server = viteServer;
|
|
244
|
+
lifecycleAbortController = new AbortController();
|
|
245
|
+
isEmitInFlight = false;
|
|
246
|
+
hasQueuedEmit = false;
|
|
247
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
220
248
|
const onTrackedWatcherEvent = (file) => {
|
|
221
249
|
handleTrackedFileChange(file);
|
|
222
250
|
};
|
|
@@ -225,14 +253,15 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
225
253
|
clearTimeout(debounceTimer);
|
|
226
254
|
debounceTimer = null;
|
|
227
255
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
256
|
+
hasQueuedEmit = false;
|
|
257
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
258
|
+
lifecycleAbortController.abort();
|
|
232
259
|
viteServer.watcher.off?.("change", onTrackedWatcherEvent);
|
|
233
260
|
viteServer.watcher.off?.("add", onTrackedWatcherEvent);
|
|
234
261
|
viteServer.watcher.off?.("unlink", onTrackedWatcherEvent);
|
|
235
262
|
ignoredOutputFiles.clear();
|
|
263
|
+
for (const outputJsonPath of ownedOutputJsonPaths) disposeEmitQueue(outputJsonPath);
|
|
264
|
+
ownedOutputJsonPaths.clear();
|
|
236
265
|
didWarnConfigWatchFallback = false;
|
|
237
266
|
server = null;
|
|
238
267
|
watchedFiles.clear();
|
|
@@ -261,14 +290,14 @@ function prismaVitePlugin(configPath = DEFAULT_CONFIG_PATH, options) {
|
|
|
261
290
|
log(`Watching ${watchedFiles.size} files`, "debug");
|
|
262
291
|
if (logLevel === "debug") for (const file of watchedFiles) log(` ${file}`, "debug");
|
|
263
292
|
}
|
|
264
|
-
await
|
|
293
|
+
await requestEmit({ refreshWatchedFiles: false });
|
|
265
294
|
},
|
|
266
295
|
handleHotUpdate(ctx) {
|
|
267
296
|
handleTrackedFileChange(ctx.file);
|
|
268
297
|
}
|
|
269
298
|
};
|
|
270
299
|
}
|
|
271
|
-
|
|
272
300
|
//#endregion
|
|
273
301
|
export { prismaVitePlugin };
|
|
302
|
+
|
|
274
303
|
//# sourceMappingURL=index.mjs.map
|
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":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import { loadConfig } from '@prisma-next/cli/config-loader';\nimport type { ContractEmitResult } from '@prisma-next/cli/control-api';\nimport { disposeEmitQueue, executeContractEmit } from '@prisma-next/cli/control-api';\nimport { getEmittedArtifactPaths } from '@prisma-next/emitter';\nimport { extname, resolve } from 'pathe';\nimport type { Plugin, ViteDevServer } from 'vite';\nimport type { PrismaVitePluginOptions } from './types';\n\nconst PLUGIN_NAME = 'prisma-vite-plugin-contract-emit';\nconst DEFAULT_DEBOUNCE_MS = 150;\nconst DEFAULT_CONFIG_PATH = 'prisma-next.config.ts';\nconst MODULE_GRAPH_EXTENSIONS = new Set([\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n]);\n\n/**\n * Creates a Vite plugin that automatically emits Prisma Next contract artifacts.\n *\n * The plugin resolves watched files from contract source provider metadata,\n * re-emitting contract artifacts on changes with debounce while serializing\n * overlapping emits into a single follow-up run.\n *\n * @param configPath - Path to prisma-next.config.ts (relative or absolute). Defaults to 'prisma-next.config.ts'\n * @param options - Optional plugin configuration\n * @returns Vite plugin\n *\n * @example\n * ```ts\n * import { defineConfig } from 'vite';\n * import { prismaVitePlugin } from '@prisma-next/vite-plugin-contract-emit';\n *\n * // Use default config path\n * export default defineConfig({\n * plugins: [prismaVitePlugin()],\n * });\n *\n * // Or specify a custom path\n * export default defineConfig({\n * plugins: [prismaVitePlugin('custom/prisma-next.config.ts')],\n * });\n * ```\n */\nexport function prismaVitePlugin(\n configPath: string = DEFAULT_CONFIG_PATH,\n options?: PrismaVitePluginOptions,\n): Plugin {\n const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n const logLevel = options?.logLevel ?? 'info';\n\n let absoluteConfigPath: string;\n const watchedFiles = new Set<string>();\n // Vite watches the project root, so writes to emitted artifacts can still surface as change\n // events even when those files are excluded from watchedFiles.\n const ignoredOutputFiles = new Set<string>();\n // Output JSON paths whose serialization queue this plugin instance owns. Disposed on cleanup\n // so long-lived dev sessions don't accumulate per-process queue state across config edits.\n const ownedOutputJsonPaths = new Set<string>();\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let lifecycleAbortController = new AbortController();\n let server: ViteDevServer | null = null;\n let isEmitInFlight = false;\n let hasQueuedEmit = false;\n let queuedEmitNeedsWatchedFileRefresh = false;\n let didWarnConfigWatchFallback = false;\n\n function log(message: string, level: 'info' | 'debug' = 'info') {\n if (logLevel === 'silent') return;\n if (level === 'debug' && logLevel !== 'debug') return;\n console.log(`[${PLUGIN_NAME}] ${message}`);\n }\n\n function logError(message: string, error?: unknown) {\n if (logLevel === 'silent') return;\n const errorMessage = error instanceof Error ? error.message : error ? String(error) : '';\n console.error(`[${PLUGIN_NAME}] ${message}${errorMessage ? ` ${errorMessage}` : ''}`);\n if (error instanceof Error && error.stack && logLevel === 'debug') {\n console.error(error.stack);\n }\n }\n\n function logWarning(message: string) {\n if (logLevel === 'silent') return;\n console.warn(`[${PLUGIN_NAME}] ${message}`);\n }\n\n function handleTrackedFileChange(file: string) {\n const normalized = resolve(file);\n if (ignoredOutputFiles.has(normalized)) {\n log(`Ignoring emitted artifact update: ${normalized}`, 'debug');\n return;\n }\n\n if (watchedFiles.has(normalized)) {\n log(`Detected change: ${normalized}`, 'debug');\n scheduleEmit();\n }\n }\n\n async function emitContract({\n refreshWatchedFiles = true,\n }: {\n refreshWatchedFiles?: boolean;\n } = {}): Promise<ContractEmitResult | null> {\n const signal = lifecycleAbortController.signal;\n\n try {\n if (server && refreshWatchedFiles) {\n await updateWatchedFiles(server);\n }\n\n const result = await executeContractEmit({\n configPath: absoluteConfigPath,\n signal,\n });\n\n log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);\n log(` → ${result.files.json}`, 'debug');\n log(` → ${result.files.dts}`, 'debug');\n\n if (server) {\n server.moduleGraph.onFileChange(result.files.json);\n server.moduleGraph.onFileChange(result.files.dts);\n }\n\n if (server && !hasQueuedEmit) {\n server.ws.send({ type: 'full-reload' });\n } else if (hasQueuedEmit) {\n log('Skipped full reload because a newer emit is queued', 'debug');\n }\n\n return result;\n } catch (error) {\n // Ignore cancellation - check signal first, then error name\n if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) {\n log('Emit cancelled', 'debug');\n return null;\n }\n\n logError('Contract emit failed:', error);\n\n // Send error to Vite overlay\n if (server) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorStack = error instanceof Error ? error.stack : undefined;\n server.ws.send({\n type: 'error',\n err: {\n message: `[prisma-next] ${errorMessage}`,\n stack: errorStack ?? '',\n plugin: PLUGIN_NAME,\n },\n });\n }\n\n return null;\n }\n }\n\n async function drainQueuedEmits(): Promise<void> {\n if (isEmitInFlight || lifecycleAbortController.signal.aborted) {\n return;\n }\n\n isEmitInFlight = true;\n\n try {\n while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {\n const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;\n hasQueuedEmit = false;\n queuedEmitNeedsWatchedFileRefresh = false;\n\n await emitContract({ refreshWatchedFiles });\n }\n } finally {\n isEmitInFlight = false;\n }\n }\n\n function requestEmit({\n refreshWatchedFiles = true,\n }: {\n refreshWatchedFiles?: boolean;\n } = {}): Promise<void> {\n if (lifecycleAbortController.signal.aborted) {\n return Promise.resolve();\n }\n\n hasQueuedEmit = true;\n queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;\n\n if (isEmitInFlight) {\n log('Queued follow-up emit while another emit is running', 'debug');\n return Promise.resolve();\n }\n\n return drainQueuedEmits();\n }\n\n function scheduleEmit() {\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void requestEmit();\n }, debounceMs);\n }\n\n function resolveContractOutputFiles(contractOutput: string | undefined): Set<string> {\n if (contractOutput === undefined) {\n return new Set();\n }\n const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);\n ownedOutputJsonPaths.add(jsonPath);\n return new Set<string>([jsonPath, dtsPath]);\n }\n\n function isModuleGraphRoot(filePath: string): boolean {\n return MODULE_GRAPH_EXTENSIONS.has(extname(filePath));\n }\n\n async function collectModuleGraphFiles(\n viteServer: ViteDevServer,\n roots: readonly string[],\n ): Promise<Set<string>> {\n const files = new Set<string>();\n const uniqueRoots = [...new Set(roots)];\n\n for (const root of uniqueRoots) {\n try {\n await viteServer.ssrLoadModule(root);\n } catch (error) {\n if (root === absoluteConfigPath) {\n logError('Failed to load config module graph root:', error);\n } else {\n log(`Skipped module-graph root after load failure: ${root}`, 'debug');\n }\n }\n }\n\n try {\n const visited = new Set<string>();\n const queue = [...uniqueRoots];\n\n while (queue.length > 0) {\n const current = queue.shift();\n if (current === undefined || visited.has(current)) continue;\n visited.add(current);\n\n const mod = viteServer.moduleGraph.getModuleById(current);\n if (!mod) continue;\n\n // Add file to watched set if it's a file path\n if (mod.file) {\n files.add(mod.file);\n }\n\n // Add imported modules to queue\n for (const imported of mod.importedModules) {\n if (imported.id && !visited.has(imported.id)) {\n queue.push(imported.id);\n }\n }\n }\n } catch (error) {\n logError('Failed to collect watched files:', error);\n }\n\n return files;\n }\n\n async function resolveWatchedFiles(viteServer: ViteDevServer): Promise<Set<string>> {\n const previousWatchedFiles = new Set(watchedFiles);\n const previousIgnoredOutputFiles = new Set(ignoredOutputFiles);\n ignoredOutputFiles.clear();\n\n try {\n const config = await loadConfig(absoluteConfigPath);\n didWarnConfigWatchFallback = false;\n const contract = config.contract;\n\n if (!contract) {\n return new Set([absoluteConfigPath]);\n }\n\n const files = new Set<string>([absoluteConfigPath]);\n const inputs = contract.source.inputs ?? [];\n for (const outputFile of resolveContractOutputFiles(contract.output)) {\n ignoredOutputFiles.add(outputFile);\n }\n\n const moduleGraphRoots = [absoluteConfigPath];\n for (const input of inputs) {\n if (!ignoredOutputFiles.has(input)) {\n files.add(input);\n }\n if (isModuleGraphRoot(input)) {\n moduleGraphRoots.push(input);\n }\n }\n\n for (const file of await collectModuleGraphFiles(viteServer, moduleGraphRoots)) {\n if (!ignoredOutputFiles.has(file)) {\n files.add(file);\n }\n }\n\n return files;\n } catch (error) {\n if (previousIgnoredOutputFiles.size > 0) {\n for (const outputFile of previousIgnoredOutputFiles) {\n ignoredOutputFiles.add(outputFile);\n }\n }\n if (!didWarnConfigWatchFallback) {\n didWarnConfigWatchFallback = true;\n const reason = error instanceof Error ? ` ${error.message}` : '';\n const watchScope =\n previousWatchedFiles.size > 0\n ? `Watching the previous dependency set plus ${absoluteConfigPath}`\n : `Watching only ${absoluteConfigPath}`;\n logWarning(\n `${watchScope} because Prisma Next config inputs could not be resolved.${reason} Contract watch coverage is partial.`,\n );\n }\n if (previousWatchedFiles.size > 0) {\n previousWatchedFiles.add(absoluteConfigPath);\n return previousWatchedFiles;\n }\n return new Set([absoluteConfigPath]);\n }\n }\n\n async function updateWatchedFiles(viteServer: ViteDevServer): Promise<void> {\n const newWatchedFiles = await resolveWatchedFiles(viteServer);\n\n // Find files to add and remove\n const toAdd: string[] = [];\n const toRemove: string[] = [];\n\n for (const file of newWatchedFiles) {\n if (!watchedFiles.has(file)) {\n toAdd.push(file);\n }\n }\n\n for (const file of watchedFiles) {\n if (!newWatchedFiles.has(file)) {\n toRemove.push(file);\n }\n }\n\n // Update the watcher\n for (const file of toAdd) {\n viteServer.watcher.add(file);\n }\n for (const file of toRemove) {\n viteServer.watcher.unwatch(file);\n }\n\n // Replace the watched files set\n watchedFiles.clear();\n for (const file of newWatchedFiles) {\n watchedFiles.add(file);\n }\n\n if (toAdd.length > 0 || toRemove.length > 0) {\n log(`Updated watched files: +${toAdd.length} -${toRemove.length}`, 'debug');\n }\n }\n\n return {\n name: PLUGIN_NAME,\n\n configResolved(config) {\n // Resolve config path to absolute path based on Vite root\n absoluteConfigPath = resolve(config.root, configPath);\n log(`Config path: ${absoluteConfigPath}`, 'debug');\n },\n\n async configureServer(viteServer) {\n server = viteServer;\n lifecycleAbortController = new AbortController();\n isEmitInFlight = false;\n hasQueuedEmit = false;\n queuedEmitNeedsWatchedFileRefresh = false;\n const onTrackedWatcherEvent = (file: string) => {\n handleTrackedFileChange(file);\n };\n\n // Register close hook to clean up timers and abort in-flight work.\n const cleanup = () => {\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n hasQueuedEmit = false;\n queuedEmitNeedsWatchedFileRefresh = false;\n lifecycleAbortController.abort();\n viteServer.watcher.off?.('change', onTrackedWatcherEvent);\n viteServer.watcher.off?.('add', onTrackedWatcherEvent);\n viteServer.watcher.off?.('unlink', onTrackedWatcherEvent);\n ignoredOutputFiles.clear();\n for (const outputJsonPath of ownedOutputJsonPaths) {\n disposeEmitQueue(outputJsonPath);\n }\n ownedOutputJsonPaths.clear();\n didWarnConfigWatchFallback = false;\n server = null;\n watchedFiles.clear();\n log('Server closed, cleaned up resources', 'debug');\n };\n\n // Register cleanup on server close via httpServer or watcher\n viteServer.httpServer?.on('close', cleanup);\n viteServer.watcher?.on?.('close', cleanup);\n viteServer.watcher.on('change', onTrackedWatcherEvent);\n viteServer.watcher.on('add', onTrackedWatcherEvent);\n viteServer.watcher.on('unlink', onTrackedWatcherEvent);\n\n const initialWatchedFiles = await resolveWatchedFiles(viteServer);\n\n // Collect files to watch from provider metadata\n for (const file of initialWatchedFiles) {\n watchedFiles.add(file);\n }\n\n // Add all dependency files to Vite's watcher\n for (const file of watchedFiles) {\n viteServer.watcher.add(file);\n }\n\n // Error if no files are being watched - this indicates a configuration problem\n if (watchedFiles.size === 0) {\n const errorMessage =\n `No files are being watched. The config file \"${absoluteConfigPath}\" could not be loaded ` +\n 'or has no dependencies. HMR for contract changes will not work.';\n logError(errorMessage);\n viteServer.ws.send({\n type: 'error',\n err: {\n message: `[prisma-next] ${errorMessage}`,\n stack: '',\n plugin: PLUGIN_NAME,\n },\n });\n } else {\n log(`Watching ${watchedFiles.size} files`, 'debug');\n if (logLevel === 'debug') {\n for (const file of watchedFiles) {\n log(` ${file}`, 'debug');\n }\n }\n }\n\n // Initial emit on server start\n await requestEmit({ refreshWatchedFiles: false });\n },\n\n handleHotUpdate(ctx) {\n handleTrackedFileChange(ctx.file);\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/vite-plugin-contract-emit",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.80",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"sideEffects": false,
|
|
6
7
|
"files": [
|
|
@@ -9,20 +10,20 @@
|
|
|
9
10
|
],
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"pathe": "^2.0.3",
|
|
12
|
-
"@prisma-next/cli": "0.5.0-dev.
|
|
13
|
-
"@prisma-next/emitter": "0.5.0-dev.
|
|
13
|
+
"@prisma-next/cli": "0.5.0-dev.80",
|
|
14
|
+
"@prisma-next/emitter": "0.5.0-dev.80"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"@types/node": "24.10.4",
|
|
17
|
-
"tsdown": "0.
|
|
18
|
+
"tsdown": "0.22.0",
|
|
18
19
|
"typescript": "5.9.3",
|
|
19
|
-
"vite": "
|
|
20
|
-
"vitest": "4.
|
|
21
|
-
"@prisma-next/
|
|
22
|
-
"@prisma-next/
|
|
20
|
+
"vite": "8.0.11",
|
|
21
|
+
"vitest": "4.1.5",
|
|
22
|
+
"@prisma-next/tsconfig": "0.0.0",
|
|
23
|
+
"@prisma-next/tsdown": "0.0.0"
|
|
23
24
|
},
|
|
24
25
|
"peerDependencies": {
|
|
25
|
-
"vite": "
|
|
26
|
+
"vite": "^7.0.0 || ^8.0.0"
|
|
26
27
|
},
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=20"
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"build": "tsdown",
|
|
44
45
|
"test": "vitest run",
|
|
45
46
|
"test:coverage": "vitest run --coverage",
|
|
46
|
-
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
47
|
+
"typecheck": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json --noEmit",
|
|
47
48
|
"lint": "biome check . --error-on-warnings",
|
|
48
49
|
"lint:fix": "biome check --write .",
|
|
49
50
|
"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,19 @@ export function prismaVitePlugin(
|
|
|
122
120
|
signal,
|
|
123
121
|
});
|
|
124
122
|
|
|
125
|
-
// Check if this emit is still the latest request
|
|
126
|
-
if (requestId !== emitRequestId) {
|
|
127
|
-
log('Emit superseded by newer request', 'debug');
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
123
|
log(`Emitted contract (storageHash: ${result.storageHash.slice(0, 8)}...)`);
|
|
132
124
|
log(` → ${result.files.json}`, 'debug');
|
|
133
125
|
log(` → ${result.files.dts}`, 'debug');
|
|
134
126
|
|
|
135
127
|
if (server) {
|
|
128
|
+
server.moduleGraph.onFileChange(result.files.json);
|
|
129
|
+
server.moduleGraph.onFileChange(result.files.dts);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (server && !hasQueuedEmit) {
|
|
136
133
|
server.ws.send({ type: 'full-reload' });
|
|
134
|
+
} else if (hasQueuedEmit) {
|
|
135
|
+
log('Skipped full reload because a newer emit is queued', 'debug');
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
return result;
|
|
@@ -144,11 +143,6 @@ export function prismaVitePlugin(
|
|
|
144
143
|
return null;
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
// Check if this emit is still the latest request
|
|
148
|
-
if (requestId !== emitRequestId) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
146
|
logError('Contract emit failed:', error);
|
|
153
147
|
|
|
154
148
|
// Send error to Vite overlay
|
|
@@ -169,13 +163,53 @@ export function prismaVitePlugin(
|
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
|
|
166
|
+
async function drainQueuedEmits(): Promise<void> {
|
|
167
|
+
if (isEmitInFlight || lifecycleAbortController.signal.aborted) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
isEmitInFlight = true;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
while (hasQueuedEmit && !lifecycleAbortController.signal.aborted) {
|
|
175
|
+
const refreshWatchedFiles = queuedEmitNeedsWatchedFileRefresh;
|
|
176
|
+
hasQueuedEmit = false;
|
|
177
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
178
|
+
|
|
179
|
+
await emitContract({ refreshWatchedFiles });
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
isEmitInFlight = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requestEmit({
|
|
187
|
+
refreshWatchedFiles = true,
|
|
188
|
+
}: {
|
|
189
|
+
refreshWatchedFiles?: boolean;
|
|
190
|
+
} = {}): Promise<void> {
|
|
191
|
+
if (lifecycleAbortController.signal.aborted) {
|
|
192
|
+
return Promise.resolve();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
hasQueuedEmit = true;
|
|
196
|
+
queuedEmitNeedsWatchedFileRefresh ||= refreshWatchedFiles;
|
|
197
|
+
|
|
198
|
+
if (isEmitInFlight) {
|
|
199
|
+
log('Queued follow-up emit while another emit is running', 'debug');
|
|
200
|
+
return Promise.resolve();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return drainQueuedEmits();
|
|
204
|
+
}
|
|
205
|
+
|
|
172
206
|
function scheduleEmit() {
|
|
173
207
|
if (debounceTimer) {
|
|
174
208
|
clearTimeout(debounceTimer);
|
|
175
209
|
}
|
|
176
210
|
debounceTimer = setTimeout(() => {
|
|
177
211
|
debounceTimer = null;
|
|
178
|
-
void
|
|
212
|
+
void requestEmit();
|
|
179
213
|
}, debounceMs);
|
|
180
214
|
}
|
|
181
215
|
|
|
@@ -184,6 +218,7 @@ export function prismaVitePlugin(
|
|
|
184
218
|
return new Set();
|
|
185
219
|
}
|
|
186
220
|
const { jsonPath, dtsPath } = getEmittedArtifactPaths(contractOutput);
|
|
221
|
+
ownedOutputJsonPaths.add(jsonPath);
|
|
187
222
|
return new Set<string>([jsonPath, dtsPath]);
|
|
188
223
|
}
|
|
189
224
|
|
|
@@ -352,24 +387,31 @@ export function prismaVitePlugin(
|
|
|
352
387
|
|
|
353
388
|
async configureServer(viteServer) {
|
|
354
389
|
server = viteServer;
|
|
390
|
+
lifecycleAbortController = new AbortController();
|
|
391
|
+
isEmitInFlight = false;
|
|
392
|
+
hasQueuedEmit = false;
|
|
393
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
355
394
|
const onTrackedWatcherEvent = (file: string) => {
|
|
356
395
|
handleTrackedFileChange(file);
|
|
357
396
|
};
|
|
358
397
|
|
|
359
|
-
// Register close hook to clean up timers and abort in-flight work
|
|
398
|
+
// Register close hook to clean up timers and abort in-flight work.
|
|
360
399
|
const cleanup = () => {
|
|
361
400
|
if (debounceTimer) {
|
|
362
401
|
clearTimeout(debounceTimer);
|
|
363
402
|
debounceTimer = null;
|
|
364
403
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
404
|
+
hasQueuedEmit = false;
|
|
405
|
+
queuedEmitNeedsWatchedFileRefresh = false;
|
|
406
|
+
lifecycleAbortController.abort();
|
|
369
407
|
viteServer.watcher.off?.('change', onTrackedWatcherEvent);
|
|
370
408
|
viteServer.watcher.off?.('add', onTrackedWatcherEvent);
|
|
371
409
|
viteServer.watcher.off?.('unlink', onTrackedWatcherEvent);
|
|
372
410
|
ignoredOutputFiles.clear();
|
|
411
|
+
for (const outputJsonPath of ownedOutputJsonPaths) {
|
|
412
|
+
disposeEmitQueue(outputJsonPath);
|
|
413
|
+
}
|
|
414
|
+
ownedOutputJsonPaths.clear();
|
|
373
415
|
didWarnConfigWatchFallback = false;
|
|
374
416
|
server = null;
|
|
375
417
|
watchedFiles.clear();
|
|
@@ -419,7 +461,7 @@ export function prismaVitePlugin(
|
|
|
419
461
|
}
|
|
420
462
|
|
|
421
463
|
// Initial emit on server start
|
|
422
|
-
await
|
|
464
|
+
await requestEmit({ refreshWatchedFiles: false });
|
|
423
465
|
},
|
|
424
466
|
|
|
425
467
|
handleHotUpdate(ctx) {
|