@jk2908/cxx 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/build.d.ts +13 -4
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +180 -54
- package/dist/build.js.map +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -7
- package/dist/index.js.map +3 -3
- package/dist/logger.d.ts +0 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/next/cxx-loader.cjs +214 -132
- package/dist/next/cxx-loader.cjs.map +7 -7
- package/dist/next/index.d.ts +32 -11
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +272 -123
- package/dist/next/index.js.map +5 -5
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +16 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/vite/index.d.ts +11 -3
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +273 -115
- package/dist/vite/index.js.map +6 -6
- package/package.json +83 -79
- package/src/build.ts +258 -93
- package/src/index.ts +26 -9
- package/src/logger.ts +5 -13
- package/src/next/cxx-loader.js +1 -5
- package/src/next/index.ts +143 -100
- package/src/types.ts +5 -4
- package/src/utils.ts +72 -7
- package/src/vite/index.ts +109 -78
package/src/next/index.ts
CHANGED
|
@@ -5,159 +5,183 @@ import fs from 'node:fs/promises'
|
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
|
|
8
|
-
import watcher from '@parcel/watcher'
|
|
9
|
-
|
|
10
8
|
import type { Logger } from '../logger.js'
|
|
11
|
-
import type { PluginConfig
|
|
12
|
-
import { collect,
|
|
9
|
+
import type { PluginConfig } from '../types.js'
|
|
10
|
+
import { collect, isENOENT, writeTypesForFile } from '../build.js'
|
|
11
|
+
import { GENERATED_DIR } from '../config.js'
|
|
13
12
|
|
|
14
13
|
const nextDir = path.dirname(fileURLToPath(import.meta.url))
|
|
15
14
|
const loader = path.join(nextDir, 'cxx-loader.cjs')
|
|
16
15
|
|
|
17
|
-
// keep the
|
|
18
|
-
const tagsByFile: Map<string, Tags> = new Map()
|
|
19
|
-
|
|
20
|
-
// directory watches are shared, but rebuilds still need the file's config and logger
|
|
16
|
+
// keep the exact file inputs and their config so a single recursive watcher can filter events
|
|
21
17
|
const watchedFiles = new Map<string, { pluginConfig: PluginConfig; logger: Logger }>()
|
|
22
|
-
const subscriptionsByDir = new Map<string, Promise<AsyncSubscription>>()
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const watchedFile = watchedFiles.get(filePath)
|
|
29
|
-
if (!watchedFile) return
|
|
19
|
+
let subscription: AsyncSubscription | null = null
|
|
20
|
+
let subscriptionPromise: Promise<void> | null = null
|
|
21
|
+
let cleanupRegistered = false
|
|
22
|
+
let watcherLogger: Logger | null = null
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
// a single recursive watch avoids overlapping per-directory watchers; ignore the parts of the
|
|
25
|
+
// tree that never contain source we transform
|
|
26
|
+
const DEFAULT_IGNORE = [
|
|
27
|
+
'**/node_modules/**',
|
|
28
|
+
'**/.next/**',
|
|
29
|
+
`**/${GENERATED_DIR}/**`,
|
|
30
|
+
'**/.git/**',
|
|
31
|
+
]
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
async function onEvent(event: Event['type'], filePath: string) {
|
|
34
|
+
const watched = watchedFiles.get(filePath)
|
|
35
|
+
if (!watched) return
|
|
36
|
+
|
|
37
|
+
const { pluginConfig, logger } = watched
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
if (event === 'delete') {
|
|
40
|
+
await writeTypesForFile(filePath, new Map())
|
|
39
41
|
logger.info(`updated types from ${filePath}`)
|
|
40
42
|
|
|
41
43
|
return
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
const source = await fs.readFile(filePath, 'utf-8')
|
|
46
|
-
const { tags } = collect(source, filePath, pluginConfig)
|
|
47
|
-
const hasTags = tags.size > 0
|
|
48
|
-
|
|
49
|
-
if (hasTags) {
|
|
50
|
-
tagsByFile.set(filePath, tags)
|
|
51
|
-
} else {
|
|
52
|
-
tagsByFile.delete(filePath)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!hadTags && !hasTags) return
|
|
46
|
+
let source: string
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
try {
|
|
49
|
+
source = await fs.readFile(filePath, 'utf-8')
|
|
59
50
|
} catch (err) {
|
|
60
51
|
if (!isENOENT(err)) throw err
|
|
61
|
-
if (!hadTags) return
|
|
62
|
-
|
|
63
|
-
tagsByFile.delete(filePath)
|
|
64
|
-
await writeTypes(flattenTags(tagsByFile))
|
|
65
52
|
|
|
53
|
+
await writeTypesForFile(filePath, new Map())
|
|
66
54
|
logger.info(`updated types from ${filePath}`)
|
|
55
|
+
|
|
56
|
+
return
|
|
67
57
|
}
|
|
58
|
+
|
|
59
|
+
const { tags } = collect(source, filePath, pluginConfig)
|
|
60
|
+
|
|
61
|
+
await writeTypesForFile(filePath, tags)
|
|
62
|
+
logger.info(`updated types from ${filePath}`)
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
/**
|
|
71
|
-
*
|
|
66
|
+
* Start the watcher lazily, on the first file we actually process, so merely importing this module
|
|
67
|
+
* has no side effects.
|
|
72
68
|
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
filePath: string,
|
|
76
|
-
pluginConfig: PluginConfig,
|
|
77
|
-
logger: Logger,
|
|
78
|
-
) {
|
|
79
|
-
const resolvedFilePath = path.resolve(filePath)
|
|
80
|
-
const directoryPath = path.dirname(resolvedFilePath)
|
|
81
|
-
let subscription = subscriptionsByDir.get(directoryPath)
|
|
69
|
+
function ensureWatcher(pluginConfig: PluginConfig, logger: Logger) {
|
|
70
|
+
watcherLogger = logger
|
|
82
71
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
if (subscriptionPromise) return
|
|
73
|
+
|
|
74
|
+
const root = pluginConfig.watch?.root ?? process.cwd()
|
|
75
|
+
const ignore = [...DEFAULT_IGNORE, ...(pluginConfig.watch?.ignore ?? [])]
|
|
76
|
+
|
|
77
|
+
subscriptionPromise = (async () => {
|
|
78
|
+
// lazily imported so the native module is only loaded when watching is needed
|
|
79
|
+
const { subscribe } = await import('@parcel/watcher')
|
|
80
|
+
|
|
81
|
+
subscription = await subscribe(
|
|
82
|
+
root,
|
|
83
|
+
(watchErr, events) => {
|
|
84
|
+
if (watchErr) {
|
|
85
|
+
watcherLogger?.error(`failed to watch ${root}`, watchErr)
|
|
86
86
|
|
|
87
|
-
if (!subscription) {
|
|
88
|
-
// one subscription per directory is enough; the callback filters back down to watched files
|
|
89
|
-
subscription = watcher.subscribe(directoryPath, async (err, events) => {
|
|
90
|
-
if (err) {
|
|
91
|
-
logger.error(`failed to update types from ${directoryPath}`, err)
|
|
92
87
|
return
|
|
93
88
|
}
|
|
94
89
|
|
|
95
|
-
// Parcel can report unrelated files from the same directory, so ignore anything unregistered
|
|
96
90
|
for (const event of events) {
|
|
97
|
-
const
|
|
98
|
-
if (!watchedFiles.has(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
logger.error(`failed to update types from ${eventFilePath}`, watchErr)
|
|
104
|
-
}
|
|
91
|
+
const filePath = path.resolve(event.path)
|
|
92
|
+
if (!watchedFiles.has(filePath)) continue
|
|
93
|
+
|
|
94
|
+
void onEvent(event.type, filePath).catch(err => {
|
|
95
|
+
watcherLogger?.error(`failed to update types from ${filePath}`, err)
|
|
96
|
+
})
|
|
105
97
|
}
|
|
106
|
-
}
|
|
98
|
+
},
|
|
99
|
+
{ ignore },
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
registerCleanup()
|
|
103
|
+
})().catch(err => {
|
|
104
|
+
watcherLogger?.error('failed to start cxx watcher', err)
|
|
105
|
+
subscriptionPromise = null
|
|
106
|
+
})
|
|
107
|
+
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Close all active Parcel subscriptions.
|
|
111
|
+
*/
|
|
112
|
+
async function cleanup() {
|
|
113
|
+
const active = subscription
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
subscription = null
|
|
116
|
+
subscriptionPromise = null
|
|
117
|
+
watchedFiles.clear()
|
|
113
118
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
if (active) await active.unsubscribe()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Restore the host process's default signal behaviour instead of forcing an exit from a library.
|
|
124
|
+
*/
|
|
125
|
+
async function shutdown(signal: NodeJS.Signals) {
|
|
126
|
+
try {
|
|
127
|
+
await cleanup()
|
|
128
|
+
} finally {
|
|
129
|
+
// our `once` handlers have already been removed, so re-raising lets Node terminate as usual
|
|
130
|
+
process.kill(process.pid, signal)
|
|
118
131
|
}
|
|
132
|
+
}
|
|
119
133
|
|
|
120
|
-
|
|
134
|
+
function registerCleanup() {
|
|
135
|
+
if (cleanupRegistered) return
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
cleanupRegistered = true
|
|
138
|
+
process.once('SIGINT', () => void shutdown('SIGINT'))
|
|
139
|
+
process.once('SIGTERM', () => void shutdown('SIGTERM'))
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
/**
|
|
126
|
-
*
|
|
143
|
+
* Transform one module and, in development, register it with the shared watcher state.
|
|
127
144
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
subscriptionsByDir.clear()
|
|
137
|
-
}
|
|
145
|
+
export function processFile(
|
|
146
|
+
source: string,
|
|
147
|
+
filePath: string,
|
|
148
|
+
pluginConfig: PluginConfig,
|
|
149
|
+
logger: Logger,
|
|
150
|
+
) {
|
|
151
|
+
const resolvedFilePath = path.resolve(filePath)
|
|
138
152
|
|
|
139
|
-
process.
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
if (process.env['NODE_ENV'] !== 'production') {
|
|
154
|
+
watchedFiles.set(resolvedFilePath, { pluginConfig, logger })
|
|
155
|
+
ensureWatcher(pluginConfig, logger)
|
|
156
|
+
}
|
|
142
157
|
|
|
143
|
-
|
|
144
|
-
cleanup().finally(() => process.exit(143))
|
|
145
|
-
})
|
|
158
|
+
const result = collect(source, resolvedFilePath, pluginConfig)
|
|
146
159
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
void writeTypesForFile(resolvedFilePath, result.tags).catch(err => {
|
|
161
|
+
logger.error(`failed to write types for ${resolvedFilePath}`, err)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const TURBOPACK_GLOB = '*.{tsx,jsx,ts,js}'
|
|
168
|
+
|
|
169
|
+
export function withCxx(nextConfig: NextConfig = {}, pluginConfig: PluginConfig = {}) {
|
|
170
|
+
const watchRoot =
|
|
171
|
+
pluginConfig.watch?.root ?? nextConfig.turbopack?.root ?? process.cwd()
|
|
152
172
|
|
|
153
173
|
const loaderItem = {
|
|
154
174
|
loader,
|
|
155
175
|
options: {
|
|
156
|
-
pluginConfig
|
|
176
|
+
pluginConfig: {
|
|
177
|
+
...pluginConfig,
|
|
178
|
+
watch: { ...pluginConfig.watch, root: watchRoot },
|
|
179
|
+
},
|
|
157
180
|
},
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
const currentRule = nextConfig.turbopack?.rules?.[TURBOPACK_GLOB]
|
|
184
|
+
const userWebpack = nextConfig.webpack
|
|
161
185
|
|
|
162
186
|
return {
|
|
163
187
|
...nextConfig,
|
|
@@ -175,5 +199,24 @@ export async function withCxx(
|
|
|
175
199
|
: { loaders: [loaderItem] },
|
|
176
200
|
},
|
|
177
201
|
},
|
|
202
|
+
// webpack builds (Next without `--turbopack`) would otherwise silently skip the transform
|
|
203
|
+
webpack(config: unknown, context: unknown) {
|
|
204
|
+
const next = (
|
|
205
|
+
userWebpack ? userWebpack(config as never, context as never) : config
|
|
206
|
+
) as {
|
|
207
|
+
module?: { rules?: unknown[] }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
next.module ??= {}
|
|
211
|
+
next.module.rules ??= []
|
|
212
|
+
next.module.rules.push({
|
|
213
|
+
test: /\.[jt]sx?$/,
|
|
214
|
+
exclude: /node_modules/,
|
|
215
|
+
enforce: 'pre',
|
|
216
|
+
use: [loaderItem],
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return next
|
|
220
|
+
},
|
|
178
221
|
}
|
|
179
222
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { LogLevel } from './logger.js'
|
|
2
|
-
|
|
3
1
|
export type Result<ClassName extends string = string> = readonly [
|
|
4
2
|
css: string,
|
|
5
3
|
classes: Readonly<Record<ClassName, string>>,
|
|
@@ -10,8 +8,11 @@ export type Tags = Map<string, string[]>
|
|
|
10
8
|
|
|
11
9
|
export type PluginConfig = {
|
|
12
10
|
typeSuffix?: string | false
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
watch?: {
|
|
12
|
+
// root used for type-regeneration watching; defaults to the Vite root / Next turbopack root
|
|
13
|
+
root?: string
|
|
14
|
+
// extra globs to ignore on top of the defaults
|
|
15
|
+
ignore?: string[]
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
package/src/utils.ts
CHANGED
|
@@ -9,16 +9,81 @@ export function pascalise(str: string) {
|
|
|
9
9
|
.join('')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export
|
|
13
|
-
|
|
12
|
+
export type Batcher<K, V> = {
|
|
13
|
+
/**
|
|
14
|
+
* Queue a value for a key. Queueing the same key again replaces the previous value.
|
|
15
|
+
*/
|
|
16
|
+
add: (key: K, value: V) => void
|
|
17
|
+
/**
|
|
18
|
+
* Run any waiting work now and resolve once it has finished.
|
|
19
|
+
*/
|
|
20
|
+
flush: () => Promise<void>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Group bursts of work and run them together. Every key queued within `wait` is
|
|
25
|
+
* included once, and a key queued while a batch is running is handled in the next
|
|
26
|
+
* batch instead of replacing the one in progress.
|
|
27
|
+
*/
|
|
28
|
+
export function createBatcher<K, V>(
|
|
29
|
+
wait: number,
|
|
30
|
+
flush: (batch: Map<K, V>) => void | Promise<void>,
|
|
31
|
+
): Batcher<K, V> {
|
|
32
|
+
// work waiting to run, keyed so each key is only queued once
|
|
33
|
+
const pending = new Map<K, V>()
|
|
34
|
+
// waits for a burst of changes to settle before running
|
|
35
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
36
|
+
// true while a batch is running, so we never start a second one
|
|
37
|
+
let running = false
|
|
38
|
+
|
|
39
|
+
async function drain() {
|
|
40
|
+
// a run is already going, so let it pick up the new work
|
|
41
|
+
if (running) return
|
|
42
|
+
running = true
|
|
14
43
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
44
|
+
try {
|
|
45
|
+
// keep looping while there is work, including work added during a flush
|
|
46
|
+
while (pending.size > 0) {
|
|
47
|
+
// snapshot the queued work
|
|
48
|
+
const batch = new Map(pending)
|
|
49
|
+
// empty the queue right away, so anything added during flush waits for the next loop
|
|
50
|
+
pending.clear()
|
|
51
|
+
|
|
52
|
+
await flush(batch)
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
// always allow the next run, even if flush threw
|
|
56
|
+
running = false
|
|
18
57
|
}
|
|
58
|
+
}
|
|
19
59
|
|
|
20
|
-
|
|
21
|
-
|
|
60
|
+
function schedule() {
|
|
61
|
+
// cancel the previous wait, so the timer restarts from now
|
|
62
|
+
if (timer) clearTimeout(timer)
|
|
63
|
+
|
|
64
|
+
// run once the calls stop coming for `wait` ms
|
|
65
|
+
timer = setTimeout(() => {
|
|
66
|
+
// the timer has fired, so drop the stale handle
|
|
67
|
+
timer = null
|
|
68
|
+
// start the run without waiting for it (this is a timer callback)
|
|
69
|
+
void drain()
|
|
22
70
|
}, wait)
|
|
23
71
|
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
add(key, value) {
|
|
75
|
+
// the newest value for this key wins
|
|
76
|
+
pending.set(key, value)
|
|
77
|
+
schedule()
|
|
78
|
+
},
|
|
79
|
+
flush() {
|
|
80
|
+
// run now instead of waiting out the timer
|
|
81
|
+
if (timer) {
|
|
82
|
+
clearTimeout(timer)
|
|
83
|
+
timer = null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return drain()
|
|
87
|
+
},
|
|
88
|
+
}
|
|
24
89
|
}
|
package/src/vite/index.ts
CHANGED
|
@@ -2,16 +2,18 @@ import { realpathSync } from 'node:fs'
|
|
|
2
2
|
import fs from 'node:fs/promises'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import { type Plugin, type ViteDevServer } from 'vite'
|
|
5
|
+
import { type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite'
|
|
6
6
|
|
|
7
7
|
import type { BuildContext, PluginConfig } from '../types.js'
|
|
8
8
|
import { collect, flattenTags, isENOENT, writeTypes } from '../build.js'
|
|
9
9
|
import { GENERATED_DIR } from '../config.js'
|
|
10
10
|
import { Logger } from '../logger.js'
|
|
11
|
-
import {
|
|
11
|
+
import { createBatcher } from '../utils.js'
|
|
12
12
|
|
|
13
13
|
const ACCEPTED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx'])
|
|
14
14
|
|
|
15
|
+
type WatchEvent = 'add' | 'change' | 'unlink'
|
|
16
|
+
|
|
15
17
|
function normaliseWatchPath(p: string) {
|
|
16
18
|
return p.replace(/\\/g, '/')
|
|
17
19
|
}
|
|
@@ -21,23 +23,11 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
|
|
|
21
23
|
tagsByFile: new Map(),
|
|
22
24
|
} satisfies BuildContext
|
|
23
25
|
|
|
24
|
-
const logger = new Logger(
|
|
25
|
-
(pluginConfig?.logger?.level ?? process.env.NODE_ENV === 'production')
|
|
26
|
-
? 'error'
|
|
27
|
-
: 'debug',
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
// write generated files into a hidden folder at the project root
|
|
31
|
-
// keep the generated surface out of src
|
|
32
|
-
const watchRoot = normaliseWatchPath(realpathSync.native(process.cwd()))
|
|
33
|
-
const watchedRoots = [`${watchRoot}/`]
|
|
26
|
+
const logger = new Logger()
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
let
|
|
28
|
+
// resolved from Vite's root in `configResolved`, not from `process.cwd()`
|
|
29
|
+
let watchRoot = normaliseWatchPath(process.cwd())
|
|
37
30
|
|
|
38
|
-
// watcher events can arrive through symlinked paths like /var while cwd has
|
|
39
|
-
// already resolved to /private/var, so canonicalise the parent dir once and
|
|
40
|
-
// reattach the file name for stable prefix checks
|
|
41
31
|
function resolveWatchFile(filePath: string) {
|
|
42
32
|
const absolutePath = path.resolve(watchRoot, filePath)
|
|
43
33
|
const parentPath = path.dirname(absolutePath)
|
|
@@ -53,101 +43,142 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
|
|
|
53
43
|
}
|
|
54
44
|
}
|
|
55
45
|
|
|
56
|
-
|
|
46
|
+
// returns the canonical, watchable path or null when the file is irrelevant
|
|
47
|
+
function watchedFile(filePath: string): string | null {
|
|
57
48
|
const resolvedPath = resolveWatchFile(filePath)
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
if (
|
|
51
|
+
resolvedPath.startsWith(`${watchRoot}/`) &&
|
|
61
52
|
!resolvedPath.includes(`/${GENERATED_DIR}/`) &&
|
|
62
53
|
ACCEPTED_EXTENSIONS.has(path.extname(resolvedPath))
|
|
63
|
-
)
|
|
64
|
-
|
|
54
|
+
) {
|
|
55
|
+
return resolvedPath
|
|
56
|
+
}
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
void (async () => {
|
|
69
|
-
// collapse bursts of file events into one active rebuild plus a single
|
|
70
|
-
// queued rerun when changes land mid-build
|
|
71
|
-
if (rebuildRunning) {
|
|
72
|
-
rebuildQueued = true
|
|
73
|
-
return
|
|
74
|
-
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
75
60
|
|
|
76
|
-
|
|
61
|
+
async function onEvent(event: WatchEvent, file: string) {
|
|
62
|
+
const hadTags = buildContext.tagsByFile.has(file)
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
if (event === 'unlink') {
|
|
65
|
+
if (!hadTags) return
|
|
80
66
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
67
|
+
buildContext.tagsByFile.delete(file)
|
|
68
|
+
await writeTypes(flattenTags(buildContext.tagsByFile))
|
|
69
|
+
logger.info(`updated types from ${file}`)
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} else {
|
|
88
|
-
const source = await fs.readFile(file, 'utf-8')
|
|
89
|
-
const { tags } = collect(source, file, pluginConfig)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
90
73
|
|
|
91
|
-
|
|
74
|
+
let source: string
|
|
92
75
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
76
|
+
try {
|
|
77
|
+
source = await fs.readFile(file, 'utf-8')
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (!isENOENT(err)) throw err
|
|
80
|
+
if (!hadTags) return
|
|
99
81
|
|
|
100
|
-
|
|
82
|
+
buildContext.tagsByFile.delete(file)
|
|
83
|
+
await writeTypes(flattenTags(buildContext.tagsByFile))
|
|
84
|
+
logger.info(`updated types from ${file}`)
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} catch (err) {
|
|
105
|
-
if (isENOENT(err)) {
|
|
106
|
-
if (!buildContext.tagsByFile.has(file)) continue
|
|
86
|
+
return
|
|
87
|
+
}
|
|
107
88
|
|
|
108
|
-
|
|
109
|
-
|
|
89
|
+
const { tags } = collect(source, file, pluginConfig)
|
|
90
|
+
const hasTags = tags.size > 0
|
|
110
91
|
|
|
111
|
-
|
|
92
|
+
if (hasTags) {
|
|
93
|
+
buildContext.tagsByFile.set(file, tags)
|
|
94
|
+
} else {
|
|
95
|
+
buildContext.tagsByFile.delete(file)
|
|
96
|
+
}
|
|
112
97
|
|
|
113
|
-
|
|
114
|
-
}
|
|
98
|
+
if (!hadTags && !hasTags) return
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
100
|
+
await writeTypes(flattenTags(buildContext.tagsByFile))
|
|
101
|
+
logger.info(`updated types from ${file}`)
|
|
102
|
+
}
|
|
119
103
|
|
|
120
|
-
|
|
121
|
-
|
|
104
|
+
// collect change events that arrive close together and handle each file once.
|
|
105
|
+
// The file path is the key, so changes to different files are all kept, and a
|
|
106
|
+
// change that arrives during a rebuild runs after that rebuild finishes.
|
|
107
|
+
const queue = createBatcher<string, WatchEvent>(75, async batch => {
|
|
108
|
+
for (const [file, event] of batch) {
|
|
109
|
+
try {
|
|
110
|
+
await onEvent(event, file)
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.error(`failed to update types from ${file}`, err)
|
|
113
|
+
}
|
|
122
114
|
}
|
|
123
|
-
|
|
124
|
-
// ignore anything outside the watched content dirs
|
|
125
|
-
if (!watchedFile(filePath)) return
|
|
126
|
-
|
|
127
|
-
const file = resolveWatchFile(filePath)
|
|
128
|
-
queue()
|
|
129
|
-
}, 75)
|
|
115
|
+
})
|
|
130
116
|
|
|
131
117
|
return {
|
|
132
118
|
name: 'cxx',
|
|
133
119
|
enforce: 'pre',
|
|
120
|
+
config() {
|
|
121
|
+
const ignore = pluginConfig.watch?.ignore
|
|
122
|
+
if (!ignore?.length) return null
|
|
123
|
+
|
|
124
|
+
// Vite concatenates `server.watch.ignored`, so only return the additions
|
|
125
|
+
return {
|
|
126
|
+
server: {
|
|
127
|
+
watch: {
|
|
128
|
+
ignored: ignore,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
configResolved(config: ResolvedConfig) {
|
|
134
|
+
const root = pluginConfig.watch?.root ?? config.root
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
watchRoot = normaliseWatchPath(realpathSync.native(root))
|
|
138
|
+
} catch {
|
|
139
|
+
watchRoot = normaliseWatchPath(root)
|
|
140
|
+
}
|
|
141
|
+
},
|
|
134
142
|
transform(code, id) {
|
|
135
|
-
|
|
143
|
+
if (id.startsWith('\0') || id.includes('/node_modules/')) return null
|
|
144
|
+
// cheap bail-out before running the regex/lightningcss on every module
|
|
145
|
+
if (!code.includes('cxx')) return null
|
|
146
|
+
|
|
147
|
+
const cleanId = id.split('?')[0]
|
|
148
|
+
if (!ACCEPTED_EXTENSIONS.has(path.extname(cleanId))) return null
|
|
136
149
|
|
|
137
|
-
|
|
150
|
+
const { template, tags, map } = collect(code, cleanId, pluginConfig)
|
|
151
|
+
if (template === code) return null
|
|
152
|
+
|
|
153
|
+
const resolved = resolveWatchFile(cleanId)
|
|
154
|
+
|
|
155
|
+
if (tags.size) {
|
|
156
|
+
buildContext.tagsByFile.set(resolved, tags)
|
|
157
|
+
} else {
|
|
158
|
+
buildContext.tagsByFile.delete(resolved)
|
|
159
|
+
}
|
|
138
160
|
|
|
139
161
|
return {
|
|
140
162
|
code: template,
|
|
141
|
-
map
|
|
163
|
+
map,
|
|
142
164
|
}
|
|
143
165
|
},
|
|
144
166
|
configureServer(server: ViteDevServer) {
|
|
145
167
|
logger.info('Watching for changes...')
|
|
146
168
|
|
|
147
169
|
server.watcher
|
|
148
|
-
.on('add', (p: string) =>
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
.on('add', (p: string) => {
|
|
171
|
+
const file = watchedFile(p)
|
|
172
|
+
if (file) queue.add(file, 'add')
|
|
173
|
+
})
|
|
174
|
+
.on('change', (p: string) => {
|
|
175
|
+
const file = watchedFile(p)
|
|
176
|
+
if (file) queue.add(file, 'change')
|
|
177
|
+
})
|
|
178
|
+
.on('unlink', (p: string) => {
|
|
179
|
+
const file = watchedFile(p)
|
|
180
|
+
if (file) queue.add(file, 'unlink')
|
|
181
|
+
})
|
|
151
182
|
},
|
|
152
183
|
async writeBundle() {
|
|
153
184
|
await writeTypes(flattenTags(buildContext.tagsByFile))
|