@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/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, Tags } from '../types.js'
12
- import { collect, flattenTags, isENOENT, writeTypes } from '../build.js'
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 latest extracted tags per file so one edit can rebuild the full type surface
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
- * Rebuild the generated type surface for one watched file after a Parcel event
26
- */
27
- async function rebuildFile(filePath: string, event: Event['type']) {
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
- const { pluginConfig, logger } = watchedFile
32
- const hadTags = tagsByFile.has(filePath)
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
- if (event === 'delete') {
35
- tagsByFile.delete(filePath)
36
- if (!hadTags) return
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
- await writeTypes(flattenTags(tagsByFile))
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
- try {
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
- await writeTypes(flattenTags(tagsByFile))
58
- logger.info(`updated types from ${filePath}`)
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
- * Transform one module and, in development, register it with the shared watcher state
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
- export function processFile(
74
- source: string,
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 (process.env.NODE_ENV !== 'production') {
84
- // Parcel watches directories, so remember the exact file inputs we care about
85
- watchedFiles.set(resolvedFilePath, { pluginConfig, logger })
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 eventFilePath = path.resolve(event.path)
98
- if (!watchedFiles.has(eventFilePath)) continue
99
-
100
- try {
101
- await rebuildFile(eventFilePath, event.type)
102
- } catch (watchErr) {
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
- subscriptionsByDir.set(directoryPath, subscription)
109
- }
110
- }
109
+ /**
110
+ * Close all active Parcel subscriptions.
111
+ */
112
+ async function cleanup() {
113
+ const active = subscription
111
114
 
112
- const result = collect(source, filePath, pluginConfig)
115
+ subscription = null
116
+ subscriptionPromise = null
117
+ watchedFiles.clear()
113
118
 
114
- if (result.tags.size > 0) {
115
- tagsByFile.set(filePath, result.tags)
116
- } else {
117
- tagsByFile.delete(filePath)
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
- void writeTypes(flattenTags(tagsByFile))
134
+ function registerCleanup() {
135
+ if (cleanupRegistered) return
121
136
 
122
- return result
137
+ cleanupRegistered = true
138
+ process.once('SIGINT', () => void shutdown('SIGINT'))
139
+ process.once('SIGTERM', () => void shutdown('SIGTERM'))
123
140
  }
124
141
 
125
142
  /**
126
- * Close all active Parcel subscriptions before the process exits
143
+ * Transform one module and, in development, register it with the shared watcher state.
127
144
  */
128
- async function cleanup() {
129
- await Promise.all(
130
- [...subscriptionsByDir].map(async ([directoryPath, subPromise]) => {
131
- subscriptionsByDir.delete(directoryPath)
132
- await (await subPromise).unsubscribe()
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.once('SIGINT', () => {
140
- cleanup().finally(() => process.exit(130))
141
- })
153
+ if (process.env['NODE_ENV'] !== 'production') {
154
+ watchedFiles.set(resolvedFilePath, { pluginConfig, logger })
155
+ ensureWatcher(pluginConfig, logger)
156
+ }
142
157
 
143
- process.once('SIGTERM', () => {
144
- cleanup().finally(() => process.exit(143))
145
- })
158
+ const result = collect(source, resolvedFilePath, pluginConfig)
146
159
 
147
- export async function withCxx(
148
- nextConfig: NextConfig = {},
149
- pluginConfig: PluginConfig = {},
150
- ) {
151
- const TURBOPACK_GLOB = '*.{tsx,jsx,ts,js}'
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
- logger?: {
14
- level?: LogLevel
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 function debounce<T extends unknown[]>(fn: (...args: T) => void, wait: number) {
13
- let timeoutId: ReturnType<typeof setTimeout> | null = null
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
- return (...args: T) => {
16
- if (timeoutId) {
17
- clearTimeout(timeoutId)
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
- timeoutId = setTimeout(() => {
21
- fn.apply(null, args)
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 { debounce } from '../utils.js'
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
- let rebuildRunning = false
36
- let rebuildQueued = false
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
- function watchedFile(filePath: string) {
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
- return (
60
- watchedRoots.some(root => resolvedPath.startsWith(root)) &&
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
- const rebuild = debounce((event: 'add' | 'change' | 'unlink', filePath: string) => {
67
- function queue() {
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
- rebuildRunning = true
61
+ async function onEvent(event: WatchEvent, file: string) {
62
+ const hadTags = buildContext.tagsByFile.has(file)
77
63
 
78
- do {
79
- rebuildQueued = false
64
+ if (event === 'unlink') {
65
+ if (!hadTags) return
80
66
 
81
- try {
82
- const hadTags = buildContext.tagsByFile.has(file)
83
- let hasTags = false
67
+ buildContext.tagsByFile.delete(file)
68
+ await writeTypes(flattenTags(buildContext.tagsByFile))
69
+ logger.info(`updated types from ${file}`)
84
70
 
85
- if (event === 'unlink') {
86
- buildContext.tagsByFile.delete(file)
87
- } else {
88
- const source = await fs.readFile(file, 'utf-8')
89
- const { tags } = collect(source, file, pluginConfig)
71
+ return
72
+ }
90
73
 
91
- hasTags = tags.size > 0
74
+ let source: string
92
75
 
93
- if (hasTags) {
94
- buildContext.tagsByFile.set(file, tags)
95
- } else {
96
- buildContext.tagsByFile.delete(file)
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
- if (!hadTags && !hasTags) continue
82
+ buildContext.tagsByFile.delete(file)
83
+ await writeTypes(flattenTags(buildContext.tagsByFile))
84
+ logger.info(`updated types from ${file}`)
101
85
 
102
- await writeTypes(flattenTags(buildContext.tagsByFile))
103
- logger.info(`updated types from ${file}`)
104
- } catch (err) {
105
- if (isENOENT(err)) {
106
- if (!buildContext.tagsByFile.has(file)) continue
86
+ return
87
+ }
107
88
 
108
- buildContext.tagsByFile.delete(file)
109
- await writeTypes(flattenTags(buildContext.tagsByFile))
89
+ const { tags } = collect(source, file, pluginConfig)
90
+ const hasTags = tags.size > 0
110
91
 
111
- logger.info(`updated types from ${file}`)
92
+ if (hasTags) {
93
+ buildContext.tagsByFile.set(file, tags)
94
+ } else {
95
+ buildContext.tagsByFile.delete(file)
96
+ }
112
97
 
113
- continue
114
- }
98
+ if (!hadTags && !hasTags) return
115
99
 
116
- logger.error(`failed to update types from ${file}`, err)
117
- }
118
- } while (rebuildQueued)
100
+ await writeTypes(flattenTags(buildContext.tagsByFile))
101
+ logger.info(`updated types from ${file}`)
102
+ }
119
103
 
120
- rebuildRunning = false
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
- const { template, tags } = collect(code, id, pluginConfig)
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
- if (tags.size) buildContext.tagsByFile.set(id, tags)
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: null,
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) => rebuild('add', p))
149
- .on('change', (p: string) => rebuild('change', p))
150
- .on('unlink', (p: string) => rebuild('unlink', p))
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))