@jk2908/cxx 0.3.3 → 0.3.4

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/utils.ts CHANGED
@@ -8,90 +8,3 @@ export function pascalise(str: string) {
8
8
  .map(capitalise)
9
9
  .join('')
10
10
  }
11
-
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
- // the run in progress, so `flush` can wait for it and we never start a second
37
- let current: Promise<void> | null = null
38
-
39
- function drain() {
40
- // a run is already going, so let it pick up the new work
41
- if (current) return current
42
-
43
- const run = (async () => {
44
- // keep looping while there is work, including work added during a flush
45
- while (pending.size > 0) {
46
- // snapshot the queued work
47
- const batch = new Map(pending)
48
- // empty the queue right away, so anything added during flush waits for the next loop
49
- pending.clear()
50
-
51
- await flush(batch)
52
- }
53
- })()
54
-
55
- current = run
56
- run.then(
57
- () => {
58
- if (current === run) current = null
59
- },
60
- () => {
61
- if (current === run) current = null
62
- },
63
- )
64
-
65
- return run
66
- }
67
-
68
- function schedule() {
69
- // cancel the previous wait, so the timer restarts from now
70
- if (timer) clearTimeout(timer)
71
-
72
- // run once the calls stop coming for `wait` ms
73
- timer = setTimeout(() => {
74
- // the timer has fired, so drop the stale handle
75
- timer = null
76
- // the flush callback handles its own errors, so just start the run
77
- void drain().catch(() => {})
78
- }, wait)
79
- }
80
-
81
- return {
82
- add(key, value) {
83
- // the newest value for this key wins
84
- pending.set(key, value)
85
- schedule()
86
- },
87
- flush() {
88
- // run now instead of waiting out the timer
89
- if (timer) {
90
- clearTimeout(timer)
91
- timer = null
92
- }
93
-
94
- return drain()
95
- },
96
- }
97
- }
package/src/vite/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { realpathSync } from 'node:fs'
2
- import fs from 'node:fs/promises'
3
2
  import path from 'node:path'
4
3
 
5
4
  import { type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite'
@@ -8,15 +7,13 @@ import type { PluginConfig } from '../types.js'
8
7
  import {
9
8
  collect,
10
9
  flushTypes,
11
- isENOENT,
12
- takeRebuildError,
10
+ refreshFile,
11
+ seedTypes,
12
+ SOURCE_EXTENSIONS,
13
13
  writeTypesForFile,
14
14
  } from '../build.js'
15
15
  import { GENERATED_DIR } from '../config.js'
16
16
  import { Logger } from '../logger.js'
17
- import { createBatcher } from '../utils.js'
18
-
19
- const ACCEPTED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx'])
20
17
 
21
18
  type WatchEvent = 'add' | 'change' | 'unlink'
22
19
 
@@ -24,48 +21,45 @@ function normaliseWatchPath(p: string) {
24
21
  return p.replace(/\\/g, '/')
25
22
  }
26
23
 
27
- /** Tiny glob matcher for the ignore patterns we support (`**`, `*`, `?`). */
28
- function globToRegExp(glob: string) {
29
- const pattern = glob
30
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
31
- .replace(/\*\*(\/|$)/g, '.*$1')
32
- .replace(/\*/g, '[^/]*')
33
- .replace(/\?/g, '[^/]')
34
-
35
- return new RegExp(`^${pattern}$`)
36
- }
37
-
38
24
  export default function cxx(pluginConfig: PluginConfig = {}) {
39
25
  const logger = new Logger()
40
26
 
41
27
  // resolved from Vite's root in `configResolved`, not from `process.cwd()`
42
28
  let watchRoot = normaliseWatchPath(process.cwd())
43
- const ignored = (pluginConfig.watch?.ignore ?? []).map(globToRegExp)
29
+ let seeded = false
30
+
31
+ // realpath is synchronous, so cache it per directory to keep it off the transform hot path
32
+ const realpaths = new Map<string, string>()
44
33
 
45
34
  function resolveWatchFile(filePath: string) {
46
35
  const absolutePath = path.resolve(watchRoot, filePath)
47
36
  const parentPath = path.dirname(absolutePath)
48
37
 
49
- try {
50
- const resolvedParentPath = normaliseWatchPath(realpathSync.native(parentPath))
38
+ let resolvedParentPath = realpaths.get(parentPath)
51
39
 
52
- return normaliseWatchPath(
53
- path.join(resolvedParentPath, path.basename(absolutePath)),
54
- )
55
- } catch {
56
- return normaliseWatchPath(absolutePath)
40
+ if (resolvedParentPath === undefined) {
41
+ try {
42
+ resolvedParentPath = normaliseWatchPath(realpathSync.native(parentPath))
43
+ } catch {
44
+ resolvedParentPath = normaliseWatchPath(parentPath)
45
+ }
46
+
47
+ realpaths.set(parentPath, resolvedParentPath)
57
48
  }
49
+
50
+ return normaliseWatchPath(path.join(resolvedParentPath, path.basename(absolutePath)))
58
51
  }
59
52
 
60
- /** Returns the canonical, watchable path, or null when the file is irrelevant. */
61
- function watchedFile(filePath: string): string | null {
53
+ /**
54
+ * Returns the canonical, watchable path, or null when the file is irrelevant.
55
+ */
56
+ function watchedFile(filePath: string) {
62
57
  const resolvedPath = resolveWatchFile(filePath)
63
58
 
64
59
  if (
65
60
  resolvedPath.startsWith(`${watchRoot}/`) &&
66
61
  !resolvedPath.includes(`/${GENERATED_DIR}/`) &&
67
- ACCEPTED_EXTENSIONS.has(path.extname(resolvedPath)) &&
68
- !ignored.some(regexp => regexp.test(resolvedPath))
62
+ SOURCE_EXTENSIONS.has(path.extname(resolvedPath))
69
63
  ) {
70
64
  return resolvedPath
71
65
  }
@@ -73,76 +67,34 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
73
67
  return null
74
68
  }
75
69
 
76
- async function recordFile(file: string) {
77
- let source: string
78
-
79
- try {
80
- source = await fs.readFile(file, 'utf-8')
81
- } catch (err) {
82
- if (!isENOENT(err)) throw err
70
+ async function handleEvent(event: WatchEvent, file: string) {
71
+ if (event === 'unlink') {
83
72
  await writeTypesForFile(file, new Map())
84
- return
73
+ } else {
74
+ await refreshFile(file, pluginConfig)
85
75
  }
86
76
 
87
- const tags = source.includes('cxx')
88
- ? collect(source, file, pluginConfig).tags
89
- : new Map()
90
- await writeTypesForFile(file, tags)
77
+ logger.info(`updated types from ${file}`)
91
78
  }
92
79
 
93
- /**
94
- * Seed the type surface from the source tree, so a fresh checkout has types before any file is
95
- * transformed or edited.
96
- */
97
- async function scan(dir: string) {
98
- let entries
80
+ function onWatch(event: WatchEvent) {
81
+ return (p: string) => {
82
+ const file = watchedFile(p)
83
+ if (!file) return
99
84
 
100
- try {
101
- entries = await fs.readdir(dir, { withFileTypes: true })
102
- } catch {
103
- return
85
+ void handleEvent(event, file).catch(err => {
86
+ logger.error(`failed to update types from ${file}`, err)
87
+ })
104
88
  }
89
+ }
105
90
 
106
- await Promise.all(
107
- entries.map(async entry => {
108
- if (entry.name.startsWith('.') || entry.name === 'node_modules') return
109
-
110
- const full = path.join(dir, entry.name)
111
-
112
- if (entry.isDirectory()) {
113
- await scan(full)
114
- return
115
- }
116
-
117
- if (!ACCEPTED_EXTENSIONS.has(path.extname(entry.name))) return
118
-
119
- const resolved = resolveWatchFile(full)
120
- if (ignored.some(regexp => regexp.test(resolved))) return
91
+ async function seed() {
92
+ if (seeded) return
93
+ seeded = true
121
94
 
122
- await recordFile(resolved)
123
- }),
124
- )
95
+ await seedTypes(watchRoot, pluginConfig)
125
96
  }
126
97
 
127
- // collect change events that arrive close together and handle each file once.
128
- // The file path is the key, so changes to different files are all kept, and a
129
- // change that arrives during a rebuild runs after that rebuild finishes.
130
- const queue = createBatcher<string, WatchEvent>(75, async batch => {
131
- for (const [file, event] of batch) {
132
- try {
133
- if (event === 'unlink') {
134
- await writeTypesForFile(file, new Map())
135
- } else {
136
- await recordFile(file)
137
- }
138
-
139
- logger.info(`updated types from ${file}`)
140
- } catch (err) {
141
- logger.error(`failed to update types from ${file}`, err)
142
- }
143
- }
144
- })
145
-
146
98
  return {
147
99
  name: 'cxx',
148
100
  enforce: 'pre',
@@ -154,16 +106,20 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
154
106
  } catch {
155
107
  watchRoot = normaliseWatchPath(root)
156
108
  }
109
+
110
+ realpaths.clear()
111
+ },
112
+ async buildStart() {
113
+ await seed()
157
114
  },
158
115
  transform(code, id) {
159
116
  if (id.startsWith('\0') || id.includes('/node_modules/')) return null
160
-
161
117
  // cheap bail-out before running the regex/lightningcss on every module
162
118
  if (!code.includes('cxx')) return null
163
119
 
164
120
  const cleanId = id.split('?')[0]
165
121
 
166
- if (!ACCEPTED_EXTENSIONS.has(path.extname(cleanId))) return null
122
+ if (!SOURCE_EXTENSIONS.has(path.extname(cleanId))) return null
167
123
 
168
124
  const { template, tags, map } = collect(code, cleanId, pluginConfig)
169
125
 
@@ -180,26 +136,9 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
180
136
  logger.info('Watching for changes...')
181
137
 
182
138
  server.watcher
183
- .on('add', (p: string) => {
184
- const file = watchedFile(p)
185
- if (file) queue.add(file, 'add')
186
- })
187
- .on('change', (p: string) => {
188
- const file = watchedFile(p)
189
- if (file) queue.add(file, 'change')
190
- })
191
- .on('unlink', (p: string) => {
192
- const file = watchedFile(p)
193
- if (file) queue.add(file, 'unlink')
194
- })
195
-
196
- void scan(watchRoot)
197
- .then(() => flushTypes())
198
- .then(() => {
199
- const err = takeRebuildError()
200
- if (err) logger.error('failed to generate types', err)
201
- })
202
- .catch(err => logger.error('failed to scan for cxx templates', err))
139
+ .on('add', onWatch('add'))
140
+ .on('change', onWatch('change'))
141
+ .on('unlink', onWatch('unlink'))
203
142
  },
204
143
  async writeBundle() {
205
144
  await flushTypes()