@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/next/index.ts
CHANGED
|
@@ -5,159 +5,192 @@ 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, takeRebuildError, 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
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
const { pluginConfig, logger } = watched
|
|
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
|
-
}
|
|
46
|
+
let source: string
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
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 = source.includes('cxx')
|
|
60
|
+
? collect(source, filePath, pluginConfig).tags
|
|
61
|
+
: new Map()
|
|
62
|
+
|
|
63
|
+
await writeTypesForFile(filePath, tags)
|
|
64
|
+
logger.info(`updated types from ${filePath}`)
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
/**
|
|
71
|
-
*
|
|
68
|
+
* Start the watcher lazily, on the first file we actually process, so merely importing this module
|
|
69
|
+
* has no side effects.
|
|
72
70
|
*/
|
|
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)
|
|
71
|
+
function ensureWatcher(pluginConfig: PluginConfig, logger: Logger) {
|
|
72
|
+
watcherLogger = logger
|
|
82
73
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
if (subscriptionPromise) return
|
|
75
|
+
|
|
76
|
+
const root = pluginConfig.watch?.root ?? process.cwd()
|
|
77
|
+
const ignore = [...DEFAULT_IGNORE, ...(pluginConfig.watch?.ignore ?? [])]
|
|
78
|
+
|
|
79
|
+
subscriptionPromise = (async () => {
|
|
80
|
+
// lazily imported so the native module is only loaded when watching is needed
|
|
81
|
+
const { subscribe } = await import('@parcel/watcher')
|
|
82
|
+
|
|
83
|
+
subscription = await subscribe(
|
|
84
|
+
root,
|
|
85
|
+
(watchErr, events) => {
|
|
86
|
+
if (watchErr) {
|
|
87
|
+
watcherLogger?.error(`failed to watch ${root}`, watchErr)
|
|
86
88
|
|
|
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
89
|
return
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
// Parcel can report unrelated files from the same directory, so ignore anything unregistered
|
|
96
92
|
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
|
-
}
|
|
93
|
+
const filePath = path.resolve(event.path)
|
|
94
|
+
if (!watchedFiles.has(filePath)) continue
|
|
95
|
+
|
|
96
|
+
void onEvent(event.type, filePath).catch(err => {
|
|
97
|
+
watcherLogger?.error(`failed to update types from ${filePath}`, err)
|
|
98
|
+
})
|
|
105
99
|
}
|
|
106
|
-
}
|
|
100
|
+
},
|
|
101
|
+
{ ignore },
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
registerCleanup()
|
|
105
|
+
})().catch(err => {
|
|
106
|
+
watcherLogger?.error('failed to start cxx watcher', err)
|
|
107
|
+
subscriptionPromise = null
|
|
108
|
+
})
|
|
109
|
+
}
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Close all active Parcel subscriptions.
|
|
113
|
+
*/
|
|
114
|
+
async function cleanup() {
|
|
115
|
+
const active = subscription
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
subscription = null
|
|
118
|
+
subscriptionPromise = null
|
|
119
|
+
watchedFiles.clear()
|
|
113
120
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
if (active) await active.unsubscribe()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Restore the host process's default signal behaviour instead of forcing an exit from a library.
|
|
126
|
+
*/
|
|
127
|
+
async function shutdown(signal: NodeJS.Signals) {
|
|
128
|
+
try {
|
|
129
|
+
await cleanup()
|
|
130
|
+
} finally {
|
|
131
|
+
// our `once` handlers have already been removed, so re-raising lets Node terminate as usual
|
|
132
|
+
process.kill(process.pid, signal)
|
|
118
133
|
}
|
|
134
|
+
}
|
|
119
135
|
|
|
120
|
-
|
|
136
|
+
function registerCleanup() {
|
|
137
|
+
if (cleanupRegistered) return
|
|
121
138
|
|
|
122
|
-
|
|
139
|
+
cleanupRegistered = true
|
|
140
|
+
process.once('SIGINT', () => void shutdown('SIGINT'))
|
|
141
|
+
process.once('SIGTERM', () => void shutdown('SIGTERM'))
|
|
123
142
|
}
|
|
124
143
|
|
|
125
144
|
/**
|
|
126
|
-
*
|
|
145
|
+
* Transform one module and, in development, register it with the shared watcher state.
|
|
127
146
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
subscriptionsByDir.clear()
|
|
137
|
-
}
|
|
147
|
+
export function processFile(
|
|
148
|
+
source: string,
|
|
149
|
+
filePath: string,
|
|
150
|
+
pluginConfig: PluginConfig,
|
|
151
|
+
logger: Logger,
|
|
152
|
+
) {
|
|
153
|
+
const resolvedFilePath = path.resolve(filePath)
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
155
|
+
// skip files that cannot contain a template, including node_modules
|
|
156
|
+
if (!source.includes('cxx') || /[\\/]node_modules[\\/]/.test(resolvedFilePath)) {
|
|
157
|
+
return { template: source, tags: new Map(), map: null }
|
|
158
|
+
}
|
|
142
159
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
// surface a duplicate-tag error from a previous background rebuild
|
|
161
|
+
const priorError = takeRebuildError()
|
|
162
|
+
if (priorError) throw priorError
|
|
146
163
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
if (process.env['NODE_ENV'] !== 'production') {
|
|
165
|
+
watchedFiles.set(resolvedFilePath, { pluginConfig, logger })
|
|
166
|
+
ensureWatcher(pluginConfig, logger)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = collect(source, resolvedFilePath, pluginConfig)
|
|
170
|
+
|
|
171
|
+
void writeTypesForFile(resolvedFilePath, result.tags)
|
|
172
|
+
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const TURBOPACK_GLOB = '*.{tsx,jsx,ts,js}'
|
|
177
|
+
|
|
178
|
+
export function withCxx(nextConfig: NextConfig = {}, pluginConfig: PluginConfig = {}) {
|
|
179
|
+
const watchRoot =
|
|
180
|
+
pluginConfig.watch?.root ?? nextConfig.turbopack?.root ?? process.cwd()
|
|
152
181
|
|
|
153
182
|
const loaderItem = {
|
|
154
183
|
loader,
|
|
155
184
|
options: {
|
|
156
|
-
pluginConfig
|
|
185
|
+
pluginConfig: {
|
|
186
|
+
...pluginConfig,
|
|
187
|
+
watch: { ...pluginConfig.watch, root: watchRoot },
|
|
188
|
+
},
|
|
157
189
|
},
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
const currentRule = nextConfig.turbopack?.rules?.[TURBOPACK_GLOB]
|
|
193
|
+
const userWebpack = nextConfig.webpack
|
|
161
194
|
|
|
162
195
|
return {
|
|
163
196
|
...nextConfig,
|
|
@@ -175,5 +208,24 @@ export async function withCxx(
|
|
|
175
208
|
: { loaders: [loaderItem] },
|
|
176
209
|
},
|
|
177
210
|
},
|
|
211
|
+
// webpack builds (Next without `--turbopack`) would otherwise silently skip the transform
|
|
212
|
+
webpack(config: unknown, context: unknown) {
|
|
213
|
+
const next = (
|
|
214
|
+
userWebpack ? userWebpack(config as never, context as never) : config
|
|
215
|
+
) as {
|
|
216
|
+
module?: { rules?: unknown[] }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
next.module ??= {}
|
|
220
|
+
next.module.rules ??= []
|
|
221
|
+
next.module.rules.push({
|
|
222
|
+
test: /\.[jt]sx?$/,
|
|
223
|
+
exclude: /node_modules/,
|
|
224
|
+
enforce: 'pre',
|
|
225
|
+
use: [loaderItem],
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return next
|
|
229
|
+
},
|
|
178
230
|
}
|
|
179
231
|
}
|
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,15 +8,14 @@ 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 or 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
|
|
|
18
|
-
export type BuildContext = {
|
|
19
|
-
tagsByFile: Map<string, Tags>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
19
|
export type Tag = <ClassName extends string = string>(
|
|
23
20
|
name: string,
|
|
24
21
|
) => (_: TemplateStringsArray) => Result<ClassName>
|
package/src/utils.ts
CHANGED
|
@@ -9,16 +9,89 @@ 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
|
+
// 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
|
|
14
42
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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()
|
|
19
50
|
|
|
20
|
-
|
|
21
|
-
|
|
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(() => {})
|
|
22
78
|
}, wait)
|
|
23
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
|
+
}
|
|
24
97
|
}
|