@doweljs/vite 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +47 -2
- package/dist/analysis/analyze.d.ts +62 -0
- package/dist/analysis/analyze.d.ts.map +1 -0
- package/dist/analysis/analyze.js +396 -0
- package/dist/analysis/analyze.js.map +1 -0
- package/dist/analysis/registry.d.ts +74 -0
- package/dist/analysis/registry.d.ts.map +1 -0
- package/dist/analysis/registry.js +195 -0
- package/dist/analysis/registry.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/inject.d.ts +21 -0
- package/dist/inject.d.ts.map +1 -0
- package/dist/inject.js +33 -0
- package/dist/inject.js.map +1 -0
- package/dist/overlay.d.ts +15 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +19 -0
- package/dist/overlay.js.map +1 -0
- package/package.json +39 -5
- package/src/analysis/analyze.ts +475 -0
- package/src/analysis/registry.ts +239 -0
- package/src/index.ts +155 -0
- package/src/inject.ts +48 -0
- package/src/overlay.ts +27 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project-wide graph assembled from per-module analyses.
|
|
3
|
+
*
|
|
4
|
+
* Nodes are identified by `kind:name`. Names are unique the way the runtime
|
|
5
|
+
* requires: scopes share one name space, and slots, services and executions
|
|
6
|
+
* share another. Dependency references are resolved through each module's
|
|
7
|
+
* imports and re-exports, so edges cross module boundaries and barrels.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Declaration, ModuleAnalysis, Ref } from './analyze.ts'
|
|
11
|
+
|
|
12
|
+
export interface GraphEdge {
|
|
13
|
+
readonly key: string
|
|
14
|
+
/** Target node id, or null when the reference could not be resolved statically. */
|
|
15
|
+
readonly target: string | null
|
|
16
|
+
readonly optional: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GraphNode {
|
|
20
|
+
readonly id: string
|
|
21
|
+
readonly kind: 'slot' | 'service' | 'scope' | 'execution'
|
|
22
|
+
readonly name: string
|
|
23
|
+
/** The scope's name when known statically, or null when it is inferred at runtime. */
|
|
24
|
+
readonly scope: string | null
|
|
25
|
+
/** For scopes: the parent scope's node id. */
|
|
26
|
+
readonly parent: string | null
|
|
27
|
+
/** Whether a slot is a multi-binding; null when its options cannot be read statically. */
|
|
28
|
+
readonly many: boolean | null
|
|
29
|
+
readonly module: string
|
|
30
|
+
readonly local: string
|
|
31
|
+
/** Offsets of the declaration in the code the plugin received, after earlier transforms. */
|
|
32
|
+
readonly start: number
|
|
33
|
+
readonly end: number
|
|
34
|
+
readonly deps: readonly GraphEdge[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Duplicate {
|
|
38
|
+
/** The name, prefixed with `scope ` for scopes. */
|
|
39
|
+
readonly id: string
|
|
40
|
+
/** Every module declaring it, once per declaration. */
|
|
41
|
+
readonly modules: readonly string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Graph {
|
|
45
|
+
readonly nodes: readonly GraphNode[]
|
|
46
|
+
readonly duplicates: readonly Duplicate[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Entry {
|
|
50
|
+
readonly analysis: ModuleAnalysis
|
|
51
|
+
/** Import and re-export sources to the module ids they resolve to. */
|
|
52
|
+
readonly resolved: ReadonlyMap<string, string>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nodeId = (declaration: Pick<Declaration, 'kind' | 'name'>): string =>
|
|
56
|
+
`${declaration.kind}:${declaration.name}`
|
|
57
|
+
|
|
58
|
+
/** The runtime's name space for a declaration (see `keyOf` in `@doweljs/core/hot`). */
|
|
59
|
+
const nameKey = (declaration: Pick<Declaration, 'kind' | 'name'>): string =>
|
|
60
|
+
declaration.kind === 'scope' ? `scope ${declaration.name}` : declaration.name
|
|
61
|
+
|
|
62
|
+
/** How deep re-export and import chains are followed. */
|
|
63
|
+
const MAX_HOPS = 8
|
|
64
|
+
|
|
65
|
+
export class Registry {
|
|
66
|
+
private readonly modules = new Map<string, Entry>()
|
|
67
|
+
/** Declarations by node id, then by module. */
|
|
68
|
+
private readonly byId = new Map<string, Map<string, Declaration>>()
|
|
69
|
+
/** Declaration counts by name key, then by module. */
|
|
70
|
+
private readonly byName = new Map<string, Map<string, number>>()
|
|
71
|
+
|
|
72
|
+
set(id: string, analysis: ModuleAnalysis, resolved: ReadonlyMap<string, string>): void {
|
|
73
|
+
this.delete(id)
|
|
74
|
+
this.modules.set(id, { analysis, resolved })
|
|
75
|
+
for (const declaration of analysis.declarations) {
|
|
76
|
+
index(this.byId, nodeId(declaration)).set(id, declaration)
|
|
77
|
+
const counts = index(this.byName, nameKey(declaration))
|
|
78
|
+
counts.set(id, (counts.get(id) ?? 0) + 1)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
delete(id: string): void {
|
|
83
|
+
const entry = this.modules.get(id)
|
|
84
|
+
if (entry === undefined) return
|
|
85
|
+
this.modules.delete(id)
|
|
86
|
+
for (const declaration of entry.analysis.declarations) {
|
|
87
|
+
unindex(this.byId, nodeId(declaration), id)
|
|
88
|
+
unindex(this.byName, nameKey(declaration), id)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Forgets every module of a deleted file, queries included. */
|
|
93
|
+
deleteFile(file: string): void {
|
|
94
|
+
for (const id of [...this.modules.keys()]) {
|
|
95
|
+
if (id === file || id.startsWith(`${file}?`)) this.delete(id)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Names a module declares that are also declared elsewhere, or more than once in it. */
|
|
100
|
+
duplicatesOf(id: string): Duplicate[] {
|
|
101
|
+
const entry = this.modules.get(id)
|
|
102
|
+
if (entry === undefined) return []
|
|
103
|
+
const keys = new Set(entry.analysis.declarations.map(nameKey))
|
|
104
|
+
return [...keys].flatMap((key) => duplicate(key, this.byName.get(key)))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
graph(): Graph {
|
|
108
|
+
const nodes: GraphNode[] = []
|
|
109
|
+
for (const [module, entry] of this.modules) {
|
|
110
|
+
for (const declaration of entry.analysis.declarations) {
|
|
111
|
+
nodes.push({
|
|
112
|
+
id: nodeId(declaration),
|
|
113
|
+
kind: declaration.kind,
|
|
114
|
+
name: declaration.name,
|
|
115
|
+
scope: this.resolveScope(module, declaration),
|
|
116
|
+
parent:
|
|
117
|
+
declaration.kind === 'scope' && declaration.scopeRef !== null
|
|
118
|
+
? this.resolveRef(module, declaration.scopeRef)
|
|
119
|
+
: null,
|
|
120
|
+
many: declaration.many,
|
|
121
|
+
module,
|
|
122
|
+
local: declaration.local,
|
|
123
|
+
start: declaration.start,
|
|
124
|
+
end: declaration.end,
|
|
125
|
+
deps: declaration.deps.map((dep) => {
|
|
126
|
+
const target = dep.ref === null ? null : this.resolveRef(module, dep.ref)
|
|
127
|
+
return {
|
|
128
|
+
key: dep.key,
|
|
129
|
+
target,
|
|
130
|
+
optional: dep.optional,
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const duplicates = [...this.byName].flatMap(([key, counts]) => duplicate(key, counts))
|
|
137
|
+
return { nodes, duplicates }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The scope name of a declaration: literal, or the name of the referenced
|
|
142
|
+
* scope declaration. An execution lives in the parent of the scope its
|
|
143
|
+
* runs open, as it does at runtime.
|
|
144
|
+
*/
|
|
145
|
+
private resolveScope(module: string, declaration: Declaration): string | null {
|
|
146
|
+
if (declaration.scope !== null) return declaration.scope
|
|
147
|
+
if (declaration.scopeRef === null) return null
|
|
148
|
+
const id = this.resolveRef(module, declaration.scopeRef)
|
|
149
|
+
const found = id === null ? null : this.find(id)
|
|
150
|
+
if (found === null) return null
|
|
151
|
+
return declaration.kind === 'execution'
|
|
152
|
+
? this.resolveScope(found.module, found.declaration)
|
|
153
|
+
: found.declaration.name
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private find(id: string): { module: string; declaration: Declaration } | null {
|
|
157
|
+
const declarations = this.byId.get(id)
|
|
158
|
+
if (declarations === undefined) return null
|
|
159
|
+
for (const [module, declaration] of declarations) return { module, declaration }
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Resolves a reference in a module to a node id, following imports and re-exports. */
|
|
164
|
+
private resolveRef(module: string, ref: Ref, hops = 0): string | null {
|
|
165
|
+
const entry = this.modules.get(module)
|
|
166
|
+
if (entry === undefined || hops > MAX_HOPS) return null
|
|
167
|
+
const local = entry.analysis.declarations.find((d) => d.local === ref.local)
|
|
168
|
+
if (local !== undefined) return ref.member === null ? nodeId(local) : null
|
|
169
|
+
const imported = entry.analysis.imports.get(ref.local)
|
|
170
|
+
const source = imported === undefined ? undefined : entry.resolved.get(imported.source)
|
|
171
|
+
if (imported === undefined || source === undefined) return null
|
|
172
|
+
if (imported.imported === '*') {
|
|
173
|
+
return ref.member === null ? null : this.resolveExport(source, ref.member, hops + 1)
|
|
174
|
+
}
|
|
175
|
+
return ref.member === null ? this.resolveExport(source, imported.imported, hops + 1) : null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Resolves a name a module exports to a node id. */
|
|
179
|
+
private resolveExport(module: string, name: string, hops: number): string | null {
|
|
180
|
+
const entry = this.modules.get(module)
|
|
181
|
+
if (entry === undefined || hops > MAX_HOPS) return null
|
|
182
|
+
const local = entry.analysis.exports.get(name)
|
|
183
|
+
if (local !== undefined) return this.resolveRef(module, { local, member: null }, hops + 1)
|
|
184
|
+
const reexport = entry.analysis.reexports.get(name)
|
|
185
|
+
if (reexport !== undefined) {
|
|
186
|
+
const source = entry.resolved.get(reexport.source)
|
|
187
|
+
if (source === undefined || reexport.imported === '*') return null
|
|
188
|
+
return this.resolveExport(source, reexport.imported, hops + 1)
|
|
189
|
+
}
|
|
190
|
+
for (const star of entry.analysis.stars) {
|
|
191
|
+
const source = entry.resolved.get(star)
|
|
192
|
+
const found = source === undefined ? null : this.resolveExport(source, name, hops + 1)
|
|
193
|
+
if (found !== null) return found
|
|
194
|
+
}
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The import and re-export sources a module's graph edges may lead through:
|
|
201
|
+
* those `Registry` follows when it resolves the module's references and
|
|
202
|
+
* exports. The plugin resolves exactly these to module ids.
|
|
203
|
+
*/
|
|
204
|
+
export function sourcesToResolve(analysis: ModuleAnalysis): Set<string> {
|
|
205
|
+
const sources = new Set<string>(analysis.stars)
|
|
206
|
+
for (const reexport of analysis.reexports.values()) sources.add(reexport.source)
|
|
207
|
+
const locals = new Set<string>(analysis.exports.values())
|
|
208
|
+
for (const declaration of analysis.declarations) {
|
|
209
|
+
for (const ref of [declaration.scopeRef, ...declaration.deps.map((dep) => dep.ref)]) {
|
|
210
|
+
if (ref !== null) locals.add(ref.local)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const local of locals) {
|
|
214
|
+
const imported = analysis.imports.get(local)
|
|
215
|
+
if (imported !== undefined) sources.add(imported.source)
|
|
216
|
+
}
|
|
217
|
+
return sources
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function index<V>(map: Map<string, Map<string, V>>, key: string): Map<string, V> {
|
|
221
|
+
let inner = map.get(key)
|
|
222
|
+
if (inner === undefined) {
|
|
223
|
+
inner = new Map()
|
|
224
|
+
map.set(key, inner)
|
|
225
|
+
}
|
|
226
|
+
return inner
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function unindex(map: Map<string, Map<string, unknown>>, key: string, module: string): void {
|
|
230
|
+
const inner = map.get(key)
|
|
231
|
+
inner?.delete(module)
|
|
232
|
+
if (inner?.size === 0) map.delete(key)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function duplicate(key: string, counts: ReadonlyMap<string, number> | undefined): Duplicate[] {
|
|
236
|
+
if (counts === undefined) return []
|
|
237
|
+
const modules = [...counts].flatMap(([module, count]) => Array<string>(count).fill(module))
|
|
238
|
+
return modules.length > 1 ? [{ id: key, modules }] : []
|
|
239
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@doweljs/vite`: Vite integration for dowel.
|
|
3
|
+
*
|
|
4
|
+
* - Hot replacement: editing a service swaps it (and its dependents) inside
|
|
5
|
+
* live containers without reloading, keeping unrelated instances such as
|
|
6
|
+
* database connections alive.
|
|
7
|
+
* - Diagnostics: duplicate slot or service names across modules are reported
|
|
8
|
+
* at transform time.
|
|
9
|
+
* - A static graph of every declaration, which `@doweljs/devtools` draws.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import dowel from '@doweljs/vite'
|
|
13
|
+
* export default defineConfig({ plugins: [dowel()] })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Plugin } from 'vite'
|
|
18
|
+
import { analyze, mayDeclare } from './analysis/analyze.ts'
|
|
19
|
+
import { type Graph, Registry, sourcesToResolve } from './analysis/registry.ts'
|
|
20
|
+
import { inject } from './inject.ts'
|
|
21
|
+
import { showHotErrors } from './overlay.ts'
|
|
22
|
+
|
|
23
|
+
export type { Duplicate, Graph, GraphEdge, GraphNode } from './analysis/registry.ts'
|
|
24
|
+
|
|
25
|
+
export interface DowelPluginOptions {
|
|
26
|
+
/**
|
|
27
|
+
* How modules that declare slots or services take part in hot replacement.
|
|
28
|
+
* - `auto` (default): modules register their declarations on every
|
|
29
|
+
* evaluation, and accept their own updates when every export is a dowel
|
|
30
|
+
* declaration. Mixed modules fall back to Vite's normal propagation.
|
|
31
|
+
* - `always`: every declaring module accepts its own updates.
|
|
32
|
+
* - `false`: no hot replacement code is injected.
|
|
33
|
+
*/
|
|
34
|
+
readonly hot?: 'auto' | 'always' | false
|
|
35
|
+
/** Module ids to analyze. Default: every `.js`/`.ts` module. */
|
|
36
|
+
readonly include?: RegExp | readonly RegExp[]
|
|
37
|
+
/** Module ids to skip, in addition to `node_modules`, which is always skipped. */
|
|
38
|
+
readonly exclude?: RegExp | readonly RegExp[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What other plugins can ask of the dowel plugin, found by its name in the resolved config. */
|
|
42
|
+
export interface DowelPluginApi {
|
|
43
|
+
/** The graph as currently known from static analysis. */
|
|
44
|
+
graph(): Graph
|
|
45
|
+
/**
|
|
46
|
+
* Has every module given hot code import `id` too, so it runs in each
|
|
47
|
+
* environment that declares nodes. The devtools load their client this way.
|
|
48
|
+
*/
|
|
49
|
+
addClientModule(id: string): void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DowelPlugin extends Plugin {
|
|
53
|
+
readonly api: DowelPluginApi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_INCLUDE = /\.[cm]?[jt]sx?$/
|
|
57
|
+
const DEFAULT_EXCLUDE = /node_modules/
|
|
58
|
+
|
|
59
|
+
export default function dowel(options: DowelPluginOptions = {}): DowelPlugin {
|
|
60
|
+
const hot = options.hot ?? 'auto'
|
|
61
|
+
const registry = new Registry()
|
|
62
|
+
const clients = new Set<string>()
|
|
63
|
+
let serve = false
|
|
64
|
+
/** Modules given hot code, by environment: each environment's runtime keeps its own registrations. */
|
|
65
|
+
const hotModules = new Set<string>()
|
|
66
|
+
const hotKey = (environment: string | undefined, id: string) => `${environment ?? ''}\0${id}`
|
|
67
|
+
/**
|
|
68
|
+
* A module that registered declarations and now has none still registers
|
|
69
|
+
* the empty set, so @doweljs/core/hot releases its names for other modules.
|
|
70
|
+
*/
|
|
71
|
+
const release = (environment: string | undefined, code: string, id: string) => {
|
|
72
|
+
if (!serve || hot === false || !hotModules.has(hotKey(environment, id))) return
|
|
73
|
+
return inject(code, { id, locals: [], accept: false, clients: [] })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
name: '@doweljs/vite',
|
|
78
|
+
enforce: 'post',
|
|
79
|
+
|
|
80
|
+
api: {
|
|
81
|
+
graph: () => registry.graph(),
|
|
82
|
+
addClientModule: (id) => void clients.add(id),
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
config(config, { command }) {
|
|
86
|
+
// The injected `import "@doweljs/core/hot"` is invisible to the dependency scanner. Optimizing it
|
|
87
|
+
// together with dowel up front avoids a re-optimization and reload on first load, during
|
|
88
|
+
// which the two could be separate copies.
|
|
89
|
+
if (command !== 'serve' || hot === false) return
|
|
90
|
+
if (config.optimizeDeps?.exclude?.includes('@doweljs/core')) return
|
|
91
|
+
return { optimizeDeps: { include: ['@doweljs/core', '@doweljs/core/hot'] } }
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
configResolved(config) {
|
|
95
|
+
// No hot code where nothing replaces modules: test runners such as Vitest, or HMR turned off.
|
|
96
|
+
serve =
|
|
97
|
+
config.command === 'serve' &&
|
|
98
|
+
config.server.hmr !== false &&
|
|
99
|
+
process.env['VITEST'] === undefined
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
transform: {
|
|
103
|
+
filter: {
|
|
104
|
+
id: {
|
|
105
|
+
include: patterns(options.include ?? DEFAULT_INCLUDE),
|
|
106
|
+
exclude: [DEFAULT_EXCLUDE, ...patterns(options.exclude ?? [])],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
async handler(code, id) {
|
|
110
|
+
const analysis = mayDeclare(code) ? analyze(id, code) : null
|
|
111
|
+
if (analysis === null) {
|
|
112
|
+
registry.delete(id)
|
|
113
|
+
return release(this.environment?.name, code, id)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Only the sources the graph can follow, resolved in parallel.
|
|
117
|
+
const sources = sourcesToResolve(analysis)
|
|
118
|
+
const resolved = new Map<string, string>()
|
|
119
|
+
await Promise.all(
|
|
120
|
+
[...sources].map(async (source) => {
|
|
121
|
+
const target = await this.resolve(source, id)
|
|
122
|
+
if (target !== null) resolved.set(source, target.id)
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
registry.set(id, analysis, resolved)
|
|
126
|
+
|
|
127
|
+
for (const duplicate of registry.duplicatesOf(id)) {
|
|
128
|
+
this.warn(
|
|
129
|
+
`'${duplicate.id}' is declared more than once. Names must be unique; ` +
|
|
130
|
+
`hot replacement treats these as the same node.\n ${duplicate.modules.join('\n ')}`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (analysis.declarations.length === 0) return release(this.environment?.name, code, id)
|
|
135
|
+
if (!serve || hot === false) return
|
|
136
|
+
hotModules.add(hotKey(this.environment?.name, id))
|
|
137
|
+
return inject(code, {
|
|
138
|
+
id,
|
|
139
|
+
locals: analysis.declarations.map((declaration) => declaration.local),
|
|
140
|
+
accept: hot === 'always' || analysis.selfContained,
|
|
141
|
+
clients: [...clients],
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
configureServer(server) {
|
|
147
|
+
server.watcher.on('unlink', (file) => registry.deleteFile(file))
|
|
148
|
+
if (hot !== false) showHotErrors(server)
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function patterns(value: RegExp | readonly RegExp[]): RegExp[] {
|
|
154
|
+
return value instanceof RegExp ? [value] : [...value]
|
|
155
|
+
}
|
package/src/inject.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appends the development-only hot replacement snippet to a module that
|
|
3
|
+
* declares slots or services. Appending keeps every original line in place,
|
|
4
|
+
* so the generated source map is a plain extension of the original.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { MagicString } from 'magic-string'
|
|
8
|
+
import { HOT_ERROR } from './overlay.ts'
|
|
9
|
+
|
|
10
|
+
export interface InjectOptions {
|
|
11
|
+
/** Module id, used as the registration key for `@doweljs/core/hot`. */
|
|
12
|
+
readonly id: string
|
|
13
|
+
/** Local names of the module's slot and service declarations. */
|
|
14
|
+
readonly locals: readonly string[]
|
|
15
|
+
/** Whether the module should accept its own updates. */
|
|
16
|
+
readonly accept: boolean
|
|
17
|
+
/** Modules other plugins asked every module with hot code to import. */
|
|
18
|
+
readonly clients: readonly string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function inject(
|
|
22
|
+
code: string,
|
|
23
|
+
options: InjectOptions,
|
|
24
|
+
): { code: string; map: ReturnType<MagicString['generateMap']> } {
|
|
25
|
+
const s = new MagicString(code)
|
|
26
|
+
const declarations = options.locals.join(', ')
|
|
27
|
+
s.append(`
|
|
28
|
+
|
|
29
|
+
// @doweljs/vite: hot replacement
|
|
30
|
+
import * as __dowel_hot__ from "@doweljs/core/hot";
|
|
31
|
+
${options.clients.map((client) => `import ${JSON.stringify(client)};\n`).join('')}if (import.meta.hot) {
|
|
32
|
+
__dowel_hot__.update(${JSON.stringify(options.id)}, { ${declarations} }).then(
|
|
33
|
+
(result) => {
|
|
34
|
+
if (result.changed.length > 0) console.info("[dowel] replaced " + result.changed.join(", "));
|
|
35
|
+
},
|
|
36
|
+
(error) => {
|
|
37
|
+
const message = __dowel_hot__.describeFailure(error);
|
|
38
|
+
console.error(message);
|
|
39
|
+
import.meta.hot.send(${JSON.stringify(HOT_ERROR)}, { message, stack: String((error && error.stack) || "") });
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
// Release the module's names once nothing imports it any more, so another module may declare them.
|
|
43
|
+
import.meta.hot.prune(() => __dowel_hot__.forget(${JSON.stringify(options.id)}));
|
|
44
|
+
${options.accept ? '\timport.meta.hot.accept();\n' : ''}}
|
|
45
|
+
`)
|
|
46
|
+
// Only appended code changes, so a map at token boundaries is exact enough.
|
|
47
|
+
return { code: s.toString(), map: s.generateMap({ hires: 'boundary' }) }
|
|
48
|
+
}
|
package/src/overlay.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot replacement failures in Vite's error overlay. The injected code reports
|
|
3
|
+
* a failed update over its environment's hot channel; the plugin shows it in
|
|
4
|
+
* the browser, whichever environment it came from.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ViteDevServer } from 'vite'
|
|
8
|
+
|
|
9
|
+
/** The event injected code sends when an update fails. */
|
|
10
|
+
export const HOT_ERROR = 'dowel:error'
|
|
11
|
+
|
|
12
|
+
export interface HotErrorMessage {
|
|
13
|
+
readonly message: string
|
|
14
|
+
readonly stack: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Shows the failures every environment of `server` reports in the browser's error overlay. */
|
|
18
|
+
export function showHotErrors(server: ViteDevServer): void {
|
|
19
|
+
for (const environment of Object.values(server.environments)) {
|
|
20
|
+
environment.hot.on(HOT_ERROR, (data: HotErrorMessage) => {
|
|
21
|
+
server.environments.client.hot.send({
|
|
22
|
+
type: 'error',
|
|
23
|
+
err: { message: data.message, stack: data.stack, plugin: '@doweljs/vite' },
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|