@jk2908/cxx 0.3.6 → 0.4.0
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 +51 -18
- package/dist/build.d.ts +17 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +72 -18
- package/dist/build.js.map +3 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -4
- package/dist/index.js.map +5 -4
- package/dist/next/cxx-loader.cjs +55 -19
- package/dist/next/cxx-loader.cjs.map +4 -4
- package/dist/next/index.d.ts +6 -5
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +73 -19
- package/dist/next/index.js.map +4 -4
- package/dist/style.d.ts +6 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/types.d.ts +35 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +77 -22
- package/dist/vite/index.js.map +4 -4
- package/package.json +1 -1
- package/src/build.ts +91 -24
- package/src/index.ts +41 -14
- package/src/next/index.ts +10 -2
- package/src/style.tsx +14 -0
- package/src/types.ts +52 -9
- package/src/vite/index.ts +10 -4
package/src/build.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Dirent } from 'node:fs'
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
4
5
|
import fs from 'node:fs/promises'
|
|
5
6
|
import path from 'node:path'
|
|
6
7
|
|
|
7
8
|
import { transform } from 'lightningcss'
|
|
8
9
|
import MagicString, { type SourceMap } from 'magic-string'
|
|
9
10
|
|
|
10
|
-
import type { PluginConfig, Tags } from './types.js'
|
|
11
|
+
import type { EmbedStyleDescriptor, PluginConfig, Tags } from './types.js'
|
|
11
12
|
import { AUTOGEN_MSG, GENERATED_DIR, NAME, PKG_NAME } from './config.js'
|
|
12
13
|
import { Logger } from './logger.js'
|
|
13
14
|
import { LruCache } from './lru.js'
|
|
@@ -22,11 +23,17 @@ const encoder = new TextEncoder()
|
|
|
22
23
|
* LRU cache of `collect` results by file id + last seen source + config so watcher and transform
|
|
23
24
|
* can reuse the same parsed template/tags.
|
|
24
25
|
*/
|
|
26
|
+
export type StyleSheet = {
|
|
27
|
+
hash: string
|
|
28
|
+
css: string
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
const collectCache = new LruCache<{
|
|
26
32
|
source: string
|
|
27
33
|
config: string
|
|
28
34
|
template: string
|
|
29
35
|
tags: Tags
|
|
36
|
+
sheets: StyleSheet[]
|
|
30
37
|
map: SourceMap | null
|
|
31
38
|
}>()
|
|
32
39
|
|
|
@@ -94,19 +101,20 @@ export async function maybeWrite(filePath: string, content: string) {
|
|
|
94
101
|
return true
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
// matches
|
|
98
|
-
// capture groups: 1
|
|
99
|
-
// css body, 6 quote kind, 7 tag name, 8 tagged css body
|
|
104
|
+
// matches cxx.embed and cxx.extract, with an optional `.tag('name')`.
|
|
105
|
+
// capture groups: 1 mode, 2 quote kind, 3 tag name, 4 css body
|
|
100
106
|
const regex =
|
|
101
|
-
|
|
107
|
+
/\bcxx\.(embed|extract)(?:\.tag(?:\s*<[^>]+>)?\s*\(\s*(['"])(.*?)\2\s*\))?\s*`([\s\S]*?)`/g
|
|
102
108
|
|
|
103
109
|
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
104
110
|
|
|
105
111
|
/**
|
|
106
|
-
* Hash transformed CSS into a stable href
|
|
112
|
+
* Hash transformed CSS into a stable href and asset key.
|
|
107
113
|
*/
|
|
108
114
|
function hashCss(code: Uint8Array) {
|
|
109
|
-
|
|
115
|
+
const hash = createHash('sha256').update(code).digest('base64url').slice(0, 12)
|
|
116
|
+
|
|
117
|
+
return { href: `cxx-${hash}`, key: hash }
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
export class DuplicateTagError extends Error {
|
|
@@ -139,6 +147,7 @@ export function collect(source: string, id: string, config: PluginConfig) {
|
|
|
139
147
|
if (cached?.source === source && cached.config === configKey) return cached
|
|
140
148
|
|
|
141
149
|
const tags: Tags = new Map()
|
|
150
|
+
const sheets: StyleSheet[] = []
|
|
142
151
|
const magic = new MagicString(source)
|
|
143
152
|
|
|
144
153
|
let matched = false
|
|
@@ -152,9 +161,8 @@ export function collect(source: string, id: string, config: PluginConfig) {
|
|
|
152
161
|
while ((match = regex.exec(source))) {
|
|
153
162
|
matched = true
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
const css = plainCss ?? taggedCss
|
|
164
|
+
const [, rawMode, , tag, css] = match
|
|
165
|
+
const mode = rawMode === 'extract' ? 'extract' : 'embed'
|
|
158
166
|
const matchIndex = match.index
|
|
159
167
|
|
|
160
168
|
// compile the CSS to minified CSS Modules, reporting the source line on failure
|
|
@@ -172,7 +180,9 @@ export function collect(source: string, id: string, config: PluginConfig) {
|
|
|
172
180
|
}
|
|
173
181
|
})()
|
|
174
182
|
|
|
175
|
-
const href = hashCss(code)
|
|
183
|
+
const { href, key } = hashCss(code)
|
|
184
|
+
const cssText = decoder.decode(code)
|
|
185
|
+
|
|
176
186
|
// map each class name to its generated (scoped) name
|
|
177
187
|
const styles = Object.fromEntries(
|
|
178
188
|
Object.entries(classExports).map(([k, v]) => [k, v.name]),
|
|
@@ -194,22 +204,23 @@ export function collect(source: string, id: string, config: PluginConfig) {
|
|
|
194
204
|
tags.set(tagWithSuffix, Object.keys(styles))
|
|
195
205
|
}
|
|
196
206
|
|
|
197
|
-
|
|
207
|
+
let replacement: string
|
|
198
208
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
if (mode === 'extract') {
|
|
210
|
+
sheets.push({ hash: key, css: cssText })
|
|
211
|
+
replacement = `{\n\tmode: 'extract',\n\tstyles: ${JSON.stringify(styles)}\n}`
|
|
212
|
+
} else {
|
|
213
|
+
const styleProps: EmbedStyleDescriptor = {
|
|
214
|
+
mode: 'embed',
|
|
215
|
+
href,
|
|
216
|
+
css: cssText,
|
|
217
|
+
precedence: 'default',
|
|
218
|
+
}
|
|
206
219
|
|
|
207
|
-
|
|
208
|
-
statements.push(`${exp}const ${varThree} = ${JSON.stringify(href)}`)
|
|
220
|
+
replacement = `{\n\tmode: 'embed',\n\tstyles: ${JSON.stringify(styles)},\n\tstyleProps: ${JSON.stringify(styleProps)}\n}`
|
|
209
221
|
}
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
magic.overwrite(matchIndex, matchIndex + match[0].length, statements.join('\n'))
|
|
223
|
+
magic.overwrite(matchIndex, matchIndex + match[0].length, replacement)
|
|
213
224
|
}
|
|
214
225
|
|
|
215
226
|
const template = magic.toString()
|
|
@@ -217,13 +228,44 @@ export function collect(source: string, id: string, config: PluginConfig) {
|
|
|
217
228
|
const map = matched
|
|
218
229
|
? magic.generateMap({ source: id, includeContent: true, hires: true })
|
|
219
230
|
: null
|
|
220
|
-
const result = { source, config: configKey, template, tags, map }
|
|
231
|
+
const result = { source, config: configKey, template, tags, sheets, map }
|
|
221
232
|
|
|
222
233
|
collectCache.set(id, result)
|
|
223
234
|
|
|
224
235
|
return result
|
|
225
236
|
}
|
|
226
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Write generated styles next to their source file and return relative import specifiers.
|
|
240
|
+
*/
|
|
241
|
+
export function writeStyleSheets(filePath: string, sheets: StyleSheet[]) {
|
|
242
|
+
if (sheets.length === 0) return []
|
|
243
|
+
|
|
244
|
+
const dir = path.join(path.dirname(filePath), '.cxx')
|
|
245
|
+
mkdirSync(dir, { recursive: true })
|
|
246
|
+
|
|
247
|
+
return sheets.map(sheet => {
|
|
248
|
+
writeFileSync(path.join(dir, `${sheet.hash}.css`), sheet.css)
|
|
249
|
+
return `./.cxx/${sheet.hash}.css`
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Insert style imports after any leading module directive.
|
|
255
|
+
*/
|
|
256
|
+
export function injectStyleImports(template: string, imports: string[]) {
|
|
257
|
+
if (imports.length === 0) return template
|
|
258
|
+
|
|
259
|
+
const directive =
|
|
260
|
+
/^(?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*(['"])use (?:client|server)\1;?/.exec(template)
|
|
261
|
+
const at = directive ? directive[0].length : 0
|
|
262
|
+
const lines = imports
|
|
263
|
+
.map(specifier => `import ${JSON.stringify(specifier)};`)
|
|
264
|
+
.join('\n')
|
|
265
|
+
|
|
266
|
+
return `${template.slice(0, at)}\n${lines}\n${template.slice(at)}`
|
|
267
|
+
}
|
|
268
|
+
|
|
227
269
|
/**
|
|
228
270
|
* Render the generated declaration file contents for a set of collected tags.
|
|
229
271
|
*/
|
|
@@ -447,11 +489,36 @@ export async function refreshFile(file: string, config: PluginConfig) {
|
|
|
447
489
|
await writeTypesForFile(file, tags)
|
|
448
490
|
}
|
|
449
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Remove orphaned `.tmp` files left by a write that was killed before its rename. Only the seed
|
|
494
|
+
* does this, because it runs once at startup before any write is in flight.
|
|
495
|
+
*/
|
|
496
|
+
async function sweepTmpFiles() {
|
|
497
|
+
for (const dir of [GENERATED_DIR, PARTIAL_DIR]) {
|
|
498
|
+
let entries: string[]
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
entries = await fs.readdir(dir)
|
|
502
|
+
} catch (err) {
|
|
503
|
+
if (isENOENT(err)) continue
|
|
504
|
+
throw err
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
for (const entry of entries) {
|
|
508
|
+
if (entry.endsWith('.tmp')) {
|
|
509
|
+
await fs.rm(path.join(dir, entry), { force: true })
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
450
515
|
/**
|
|
451
516
|
* Walk every source file under `root` and seed the type surface, so a fresh checkout (and a build
|
|
452
517
|
* with no edits) has types without waiting for a transform.
|
|
453
518
|
*/
|
|
454
519
|
export async function seedTypes(root: string, config: PluginConfig) {
|
|
520
|
+
await sweepTmpFiles()
|
|
521
|
+
|
|
455
522
|
async function scan(dir: string) {
|
|
456
523
|
let entries: Dirent[]
|
|
457
524
|
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Cxx,
|
|
3
|
+
EmbedResult,
|
|
4
|
+
EmbedStyleTag,
|
|
5
|
+
ExtractResult,
|
|
6
|
+
ExtractStyleTag,
|
|
7
|
+
} from './types.js'
|
|
2
8
|
|
|
3
|
-
export
|
|
9
|
+
export { Style } from './style.js'
|
|
10
|
+
export type {
|
|
11
|
+
Cxx,
|
|
12
|
+
EmbedResult,
|
|
13
|
+
EmbedStyleDescriptor,
|
|
14
|
+
EmbedStyleTag,
|
|
15
|
+
ExtractResult,
|
|
16
|
+
ExtractStyleTag,
|
|
17
|
+
Result,
|
|
18
|
+
StyleMode,
|
|
19
|
+
StyleProps,
|
|
20
|
+
} from './types.js'
|
|
4
21
|
|
|
5
22
|
/**
|
|
6
23
|
* Hit when a `cxx` template reaches runtime, which means the build transform did
|
|
@@ -9,8 +26,8 @@ export type { Cxx, Result, Tag } from './types.js'
|
|
|
9
26
|
* @example
|
|
10
27
|
* ```ts
|
|
11
28
|
* // no cxx plugin configured
|
|
12
|
-
* const
|
|
13
|
-
* //
|
|
29
|
+
* const button = cxx.embed`.a { color: red; }`
|
|
30
|
+
* // error: cxx template was not transformed ...
|
|
14
31
|
* ```
|
|
15
32
|
*/
|
|
16
33
|
function untransformed(): never {
|
|
@@ -19,15 +36,25 @@ function untransformed(): never {
|
|
|
19
36
|
)
|
|
20
37
|
}
|
|
21
38
|
|
|
39
|
+
function tag<_ClassName extends string = string>(_: string) {
|
|
40
|
+
return (_strings: TemplateStringsArray): never => untransformed()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function embedTag(): EmbedStyleTag {
|
|
44
|
+
return Object.assign((_: TemplateStringsArray): EmbedResult => untransformed(), { tag })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractTag(): ExtractStyleTag {
|
|
48
|
+
return Object.assign((_: TemplateStringsArray): ExtractResult => untransformed(), {
|
|
49
|
+
tag,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const embed = embedTag()
|
|
54
|
+
const extract = extractTag()
|
|
55
|
+
|
|
22
56
|
/**
|
|
23
|
-
* Build-time template tag
|
|
57
|
+
* Build-time template tag namespace used by the transform to emit class maps
|
|
58
|
+
* and style descriptors.
|
|
24
59
|
*/
|
|
25
|
-
export const cxx: Cxx =
|
|
26
|
-
(_: TemplateStringsArray): Result => untransformed(),
|
|
27
|
-
{
|
|
28
|
-
tag:
|
|
29
|
-
<ClassName extends string = string>(_: string) =>
|
|
30
|
-
(_strings: TemplateStringsArray): Result<ClassName> =>
|
|
31
|
-
untransformed(),
|
|
32
|
-
},
|
|
33
|
-
)
|
|
60
|
+
export const cxx: Cxx = { embed, extract }
|
package/src/next/index.ts
CHANGED
|
@@ -4,7 +4,13 @@ import path from 'node:path'
|
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
|
|
6
6
|
import type { PluginConfig } from '../types.js'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
collect,
|
|
9
|
+
injectStyleImports,
|
|
10
|
+
seedTypes,
|
|
11
|
+
writeStyleSheets,
|
|
12
|
+
writeTypesForFile,
|
|
13
|
+
} from '../build.js'
|
|
8
14
|
import { Logger } from '../logger.js'
|
|
9
15
|
|
|
10
16
|
const typeLogger = new Logger('cxx')
|
|
@@ -29,10 +35,12 @@ export function processFile(
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const result = collect(source, resolvedFilePath, pluginConfig)
|
|
38
|
+
const imports = writeStyleSheets(resolvedFilePath, result.sheets)
|
|
39
|
+
const template = injectStyleImports(result.template, imports)
|
|
32
40
|
|
|
33
41
|
void writeTypesForFile(resolvedFilePath, result.tags)
|
|
34
42
|
|
|
35
|
-
return result
|
|
43
|
+
return { ...result, template }
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const TURBOPACK_GLOB = '*.{tsx,jsx,ts,js}'
|
package/src/style.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StyleProps } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render an embedded style descriptor as a hoisted style element.
|
|
5
|
+
*/
|
|
6
|
+
export function Style(props: StyleProps) {
|
|
7
|
+
const { mode: _mode, href, css, precedence, ...rest } = props
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<style {...rest} href={href} precedence={precedence}>
|
|
11
|
+
{css}
|
|
12
|
+
</style>
|
|
13
|
+
)
|
|
14
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { ComponentPropsWithRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export type StyleMode = 'embed' | 'extract'
|
|
4
|
+
|
|
5
|
+
export type EmbedStyleDescriptor = {
|
|
6
|
+
mode: 'embed'
|
|
7
|
+
href: string
|
|
8
|
+
css: string
|
|
9
|
+
precedence: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type EmbedResult<ClassName extends string = string> = {
|
|
13
|
+
mode: 'embed'
|
|
14
|
+
styles: Readonly<Record<ClassName, string>>
|
|
15
|
+
styleProps: EmbedStyleDescriptor
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ExtractResult<ClassName extends string = string> = {
|
|
19
|
+
mode: 'extract'
|
|
20
|
+
styles: Readonly<Record<ClassName, string>>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Result<ClassName extends string = string> =
|
|
24
|
+
| EmbedResult<ClassName>
|
|
25
|
+
| ExtractResult<ClassName>
|
|
26
|
+
|
|
27
|
+
type StyleElementProps = Omit<
|
|
28
|
+
ComponentPropsWithRef<'style'>,
|
|
29
|
+
'href' | 'precedence' | 'children'
|
|
30
|
+
> & {
|
|
31
|
+
href: string
|
|
32
|
+
precedence: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type StyleProps = EmbedStyleDescriptor & StyleElementProps
|
|
6
36
|
|
|
7
37
|
export type Tags = Map<string, string[]>
|
|
8
38
|
|
|
@@ -14,10 +44,23 @@ export type PluginConfig = {
|
|
|
14
44
|
}
|
|
15
45
|
}
|
|
16
46
|
|
|
17
|
-
export type
|
|
47
|
+
export type EmbedTag = <ClassName extends string = string>(
|
|
48
|
+
name: string,
|
|
49
|
+
) => (_: TemplateStringsArray) => EmbedResult<ClassName>
|
|
50
|
+
|
|
51
|
+
export type ExtractTag = <ClassName extends string = string>(
|
|
18
52
|
name: string,
|
|
19
|
-
) => (_: TemplateStringsArray) =>
|
|
53
|
+
) => (_: TemplateStringsArray) => ExtractResult<ClassName>
|
|
54
|
+
|
|
55
|
+
export type EmbedStyleTag = ((_: TemplateStringsArray) => EmbedResult) & {
|
|
56
|
+
tag: EmbedTag
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ExtractStyleTag = ((_: TemplateStringsArray) => ExtractResult) & {
|
|
60
|
+
tag: ExtractTag
|
|
61
|
+
}
|
|
20
62
|
|
|
21
|
-
export type Cxx =
|
|
22
|
-
|
|
63
|
+
export type Cxx = {
|
|
64
|
+
embed: EmbedStyleTag
|
|
65
|
+
extract: ExtractStyleTag
|
|
23
66
|
}
|
package/src/vite/index.ts
CHANGED
|
@@ -7,9 +7,11 @@ import type { PluginConfig } from '../types.js'
|
|
|
7
7
|
import {
|
|
8
8
|
collect,
|
|
9
9
|
flushTypes,
|
|
10
|
+
injectStyleImports,
|
|
10
11
|
refreshFile,
|
|
11
12
|
seedTypes,
|
|
12
13
|
SOURCE_EXTENSIONS,
|
|
14
|
+
writeStyleSheets,
|
|
13
15
|
writeTypesForFile,
|
|
14
16
|
} from '../build.js'
|
|
15
17
|
import { GENERATED_DIR } from '../config.js'
|
|
@@ -121,15 +123,19 @@ export default function cxx(pluginConfig: PluginConfig = {}) {
|
|
|
121
123
|
|
|
122
124
|
if (!SOURCE_EXTENSIONS.has(path.extname(cleanId))) return null
|
|
123
125
|
|
|
124
|
-
const
|
|
126
|
+
const result = collect(code, cleanId, pluginConfig)
|
|
125
127
|
|
|
126
|
-
if (template === code) return null
|
|
128
|
+
if (result.template === code) return null
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
const file = resolveWatchFile(cleanId)
|
|
131
|
+
const imports = writeStyleSheets(file, result.sheets)
|
|
132
|
+
const template = injectStyleImports(result.template, imports)
|
|
133
|
+
|
|
134
|
+
void writeTypesForFile(file, result.tags)
|
|
129
135
|
|
|
130
136
|
return {
|
|
131
137
|
code: template,
|
|
132
|
-
map,
|
|
138
|
+
map: result.map,
|
|
133
139
|
}
|
|
134
140
|
},
|
|
135
141
|
configureServer(server: ViteDevServer) {
|