@jk2908/cxx 0.3.1 → 0.3.3

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/vite/index.ts CHANGED
@@ -4,8 +4,14 @@ import path from 'node:path'
4
4
 
5
5
  import { type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite'
6
6
 
7
- import type { BuildContext, PluginConfig } from '../types.js'
8
- import { collect, flattenTags, isENOENT, writeTypes } from '../build.js'
7
+ import type { PluginConfig } from '../types.js'
8
+ import {
9
+ collect,
10
+ flushTypes,
11
+ isENOENT,
12
+ takeRebuildError,
13
+ writeTypesForFile,
14
+ } from '../build.js'
9
15
  import { GENERATED_DIR } from '../config.js'
10
16
  import { Logger } from '../logger.js'
11
17
  import { createBatcher } from '../utils.js'
@@ -18,15 +24,23 @@ function normaliseWatchPath(p: string) {
18
24
  return p.replace(/\\/g, '/')
19
25
  }
20
26
 
21
- export default function cxx(pluginConfig: PluginConfig = {}) {
22
- const buildContext = {
23
- tagsByFile: new Map(),
24
- } satisfies BuildContext
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
+ }
25
37
 
38
+ export default function cxx(pluginConfig: PluginConfig = {}) {
26
39
  const logger = new Logger()
27
40
 
28
41
  // resolved from Vite's root in `configResolved`, not from `process.cwd()`
29
42
  let watchRoot = normaliseWatchPath(process.cwd())
43
+ const ignored = (pluginConfig.watch?.ignore ?? []).map(globToRegExp)
30
44
 
31
45
  function resolveWatchFile(filePath: string) {
32
46
  const absolutePath = path.resolve(watchRoot, filePath)
@@ -43,14 +57,15 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
43
57
  }
44
58
  }
45
59
 
46
- // returns the canonical, watchable path or null when the file is irrelevant
60
+ /** Returns the canonical, watchable path, or null when the file is irrelevant. */
47
61
  function watchedFile(filePath: string): string | null {
48
62
  const resolvedPath = resolveWatchFile(filePath)
49
63
 
50
64
  if (
51
65
  resolvedPath.startsWith(`${watchRoot}/`) &&
52
66
  !resolvedPath.includes(`/${GENERATED_DIR}/`) &&
53
- ACCEPTED_EXTENSIONS.has(path.extname(resolvedPath))
67
+ ACCEPTED_EXTENSIONS.has(path.extname(resolvedPath)) &&
68
+ !ignored.some(regexp => regexp.test(resolvedPath))
54
69
  ) {
55
70
  return resolvedPath
56
71
  }
@@ -58,47 +73,55 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
58
73
  return null
59
74
  }
60
75
 
61
- async function onEvent(event: WatchEvent, file: string) {
62
- const hadTags = buildContext.tagsByFile.has(file)
63
-
64
- if (event === 'unlink') {
65
- if (!hadTags) return
66
-
67
- buildContext.tagsByFile.delete(file)
68
- await writeTypes(flattenTags(buildContext.tagsByFile))
69
- logger.info(`updated types from ${file}`)
70
-
71
- return
72
- }
73
-
76
+ async function recordFile(file: string) {
74
77
  let source: string
75
78
 
76
79
  try {
77
80
  source = await fs.readFile(file, 'utf-8')
78
81
  } catch (err) {
79
82
  if (!isENOENT(err)) throw err
80
- if (!hadTags) return
83
+ await writeTypesForFile(file, new Map())
84
+ return
85
+ }
81
86
 
82
- buildContext.tagsByFile.delete(file)
83
- await writeTypes(flattenTags(buildContext.tagsByFile))
84
- logger.info(`updated types from ${file}`)
87
+ const tags = source.includes('cxx')
88
+ ? collect(source, file, pluginConfig).tags
89
+ : new Map()
90
+ await writeTypesForFile(file, tags)
91
+ }
92
+
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
85
99
 
100
+ try {
101
+ entries = await fs.readdir(dir, { withFileTypes: true })
102
+ } catch {
86
103
  return
87
104
  }
88
105
 
89
- const { tags } = collect(source, file, pluginConfig)
90
- const hasTags = tags.size > 0
106
+ await Promise.all(
107
+ entries.map(async entry => {
108
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') return
91
109
 
92
- if (hasTags) {
93
- buildContext.tagsByFile.set(file, tags)
94
- } else {
95
- buildContext.tagsByFile.delete(file)
96
- }
110
+ const full = path.join(dir, entry.name)
111
+
112
+ if (entry.isDirectory()) {
113
+ await scan(full)
114
+ return
115
+ }
97
116
 
98
- if (!hadTags && !hasTags) return
117
+ if (!ACCEPTED_EXTENSIONS.has(path.extname(entry.name))) return
99
118
 
100
- await writeTypes(flattenTags(buildContext.tagsByFile))
101
- logger.info(`updated types from ${file}`)
119
+ const resolved = resolveWatchFile(full)
120
+ if (ignored.some(regexp => regexp.test(resolved))) return
121
+
122
+ await recordFile(resolved)
123
+ }),
124
+ )
102
125
  }
103
126
 
104
127
  // collect change events that arrive close together and handle each file once.
@@ -107,7 +130,13 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
107
130
  const queue = createBatcher<string, WatchEvent>(75, async batch => {
108
131
  for (const [file, event] of batch) {
109
132
  try {
110
- await onEvent(event, file)
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}`)
111
140
  } catch (err) {
112
141
  logger.error(`failed to update types from ${file}`, err)
113
142
  }
@@ -117,19 +146,6 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
117
146
  return {
118
147
  name: 'cxx',
119
148
  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
149
  configResolved(config: ResolvedConfig) {
134
150
  const root = pluginConfig.watch?.root ?? config.root
135
151
 
@@ -141,22 +157,19 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
141
157
  },
142
158
  transform(code, id) {
143
159
  if (id.startsWith('\0') || id.includes('/node_modules/')) return null
160
+
144
161
  // cheap bail-out before running the regex/lightningcss on every module
145
162
  if (!code.includes('cxx')) return null
146
163
 
147
164
  const cleanId = id.split('?')[0]
165
+
148
166
  if (!ACCEPTED_EXTENSIONS.has(path.extname(cleanId))) return null
149
167
 
150
168
  const { template, tags, map } = collect(code, cleanId, pluginConfig)
169
+
151
170
  if (template === code) return null
152
171
 
153
- const resolved = resolveWatchFile(cleanId)
154
-
155
- if (tags.size) {
156
- buildContext.tagsByFile.set(resolved, tags)
157
- } else {
158
- buildContext.tagsByFile.delete(resolved)
159
- }
172
+ void writeTypesForFile(resolveWatchFile(cleanId), tags)
160
173
 
161
174
  return {
162
175
  code: template,
@@ -179,9 +192,17 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
179
192
  const file = watchedFile(p)
180
193
  if (file) queue.add(file, 'unlink')
181
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))
182
203
  },
183
204
  async writeBundle() {
184
- await writeTypes(flattenTags(buildContext.tagsByFile))
205
+ await flushTypes()
185
206
  },
186
207
  } satisfies Plugin
187
208
  }