@jk2908/cxx 0.2.6 → 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/README.md +18 -2
- package/dist/build.d.ts +22 -11
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +242 -73
- 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 +287 -146
- package/dist/next/cxx-loader.cjs.map +7 -7
- package/dist/next/index.d.ts +34 -9
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +338 -142
- package/dist/next/index.js.map +5 -5
- package/dist/types.d.ts +5 -6
- 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 +4 -3
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +336 -139
- package/dist/vite/index.js.map +6 -6
- package/package.json +85 -81
- package/src/build.ts +312 -108
- package/src/index.ts +26 -9
- package/src/logger.ts +5 -13
- package/src/next/cxx-loader.js +9 -6
- package/src/next/index.ts +152 -100
- package/src/types.ts +5 -8
- package/src/utils.ts +81 -8
- package/src/vite/index.ts +141 -89
package/src/vite/index.ts
CHANGED
|
@@ -2,42 +2,46 @@ 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'
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
5
|
+
import { type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite'
|
|
6
|
+
|
|
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
|
-
import {
|
|
17
|
+
import { createBatcher } from '../utils.js'
|
|
12
18
|
|
|
13
19
|
const ACCEPTED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx'])
|
|
14
20
|
|
|
21
|
+
type WatchEvent = 'add' | 'change' | 'unlink'
|
|
22
|
+
|
|
15
23
|
function normaliseWatchPath(p: string) {
|
|
16
24
|
return p.replace(/\\/g, '/')
|
|
17
25
|
}
|
|
18
26
|
|
|
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
|
+
|
|
19
38
|
export default function cxx(pluginConfig: PluginConfig = {}) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
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}/`]
|
|
34
|
-
|
|
35
|
-
let rebuildRunning = false
|
|
36
|
-
let rebuildQueued = false
|
|
37
|
-
|
|
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
|
|
39
|
+
const logger = new Logger()
|
|
40
|
+
|
|
41
|
+
// resolved from Vite's root in `configResolved`, not from `process.cwd()`
|
|
42
|
+
let watchRoot = normaliseWatchPath(process.cwd())
|
|
43
|
+
const ignored = (pluginConfig.watch?.ignore ?? []).map(globToRegExp)
|
|
44
|
+
|
|
41
45
|
function resolveWatchFile(filePath: string) {
|
|
42
46
|
const absolutePath = path.resolve(watchRoot, filePath)
|
|
43
47
|
const parentPath = path.dirname(absolutePath)
|
|
@@ -53,104 +57,152 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
/** Returns the canonical, watchable path, or null when the file is irrelevant. */
|
|
61
|
+
function watchedFile(filePath: string): string | null {
|
|
57
62
|
const resolvedPath = resolveWatchFile(filePath)
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
if (
|
|
65
|
+
resolvedPath.startsWith(`${watchRoot}/`) &&
|
|
61
66
|
!resolvedPath.includes(`/${GENERATED_DIR}/`) &&
|
|
62
|
-
ACCEPTED_EXTENSIONS.has(path.extname(resolvedPath))
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
ACCEPTED_EXTENSIONS.has(path.extname(resolvedPath)) &&
|
|
68
|
+
!ignored.some(regexp => regexp.test(resolvedPath))
|
|
69
|
+
) {
|
|
70
|
+
return resolvedPath
|
|
71
|
+
}
|
|
65
72
|
|
|
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
|
-
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
async function recordFile(file: string) {
|
|
77
|
+
let source: string
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
try {
|
|
80
|
+
source = await fs.readFile(file, 'utf-8')
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (!isENOENT(err)) throw err
|
|
83
|
+
await writeTypesForFile(file, new Map())
|
|
84
|
+
return
|
|
85
|
+
}
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const tags = source.includes('cxx')
|
|
88
|
+
? collect(source, file, pluginConfig).tags
|
|
89
|
+
: new Map()
|
|
90
|
+
await writeTypesForFile(file, tags)
|
|
91
|
+
}
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
try {
|
|
101
|
+
entries = await fs.readdir(dir, { withFileTypes: true })
|
|
102
|
+
} catch {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
buildContext.tagsByFile.delete(file)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
106
|
+
await Promise.all(
|
|
107
|
+
entries.map(async entry => {
|
|
108
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') return
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
const full = path.join(dir, entry.name)
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!buildContext.tagsByFile.has(file)) continue
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
await scan(full)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
await writeTypes(flattenTags(buildContext.tagsByFile))
|
|
117
|
+
if (!ACCEPTED_EXTENSIONS.has(path.extname(entry.name))) return
|
|
110
118
|
|
|
111
|
-
|
|
119
|
+
const resolved = resolveWatchFile(full)
|
|
120
|
+
if (ignored.some(regexp => regexp.test(resolved))) return
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
await recordFile(resolved)
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
115
126
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
138
|
|
|
120
|
-
|
|
121
|
-
}
|
|
139
|
+
logger.info(`updated types from ${file}`)
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.error(`failed to update types from ${file}`, err)
|
|
142
|
+
}
|
|
122
143
|
}
|
|
123
|
-
|
|
124
|
-
// ignore anything outside the watched content dirs
|
|
125
|
-
if (!watchedFile(filePath)) return
|
|
126
|
-
|
|
127
|
-
const file = resolveWatchFile(filePath)
|
|
128
|
-
queue()
|
|
129
|
-
}, 75)
|
|
144
|
+
})
|
|
130
145
|
|
|
131
146
|
return {
|
|
132
147
|
name: 'cxx',
|
|
133
148
|
enforce: 'pre',
|
|
149
|
+
configResolved(config: ResolvedConfig) {
|
|
150
|
+
const root = pluginConfig.watch?.root ?? config.root
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
watchRoot = normaliseWatchPath(realpathSync.native(root))
|
|
154
|
+
} catch {
|
|
155
|
+
watchRoot = normaliseWatchPath(root)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
134
158
|
transform(code, id) {
|
|
135
|
-
|
|
159
|
+
if (id.startsWith('\0') || id.includes('/node_modules/')) return null
|
|
160
|
+
|
|
161
|
+
// cheap bail-out before running the regex/lightningcss on every module
|
|
162
|
+
if (!code.includes('cxx')) return null
|
|
163
|
+
|
|
164
|
+
const cleanId = id.split('?')[0]
|
|
165
|
+
|
|
166
|
+
if (!ACCEPTED_EXTENSIONS.has(path.extname(cleanId))) return null
|
|
167
|
+
|
|
168
|
+
const { template, tags, map } = collect(code, cleanId, pluginConfig)
|
|
169
|
+
|
|
170
|
+
if (template === code) return null
|
|
136
171
|
|
|
137
|
-
|
|
172
|
+
void writeTypesForFile(resolveWatchFile(cleanId), tags)
|
|
138
173
|
|
|
139
174
|
return {
|
|
140
175
|
code: template,
|
|
141
|
-
map
|
|
176
|
+
map,
|
|
142
177
|
}
|
|
143
178
|
},
|
|
144
179
|
configureServer(server: ViteDevServer) {
|
|
145
180
|
logger.info('Watching for changes...')
|
|
146
181
|
|
|
147
182
|
server.watcher
|
|
148
|
-
.on('add', (p: string) =>
|
|
149
|
-
|
|
150
|
-
|
|
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))
|
|
151
203
|
},
|
|
152
204
|
async writeBundle() {
|
|
153
|
-
await
|
|
205
|
+
await flushTypes()
|
|
154
206
|
},
|
|
155
207
|
} satisfies Plugin
|
|
156
208
|
}
|