@jk2908/cxx 0.1.10 → 0.2.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.
Files changed (46) hide show
  1. package/dist/config.d.ts +5 -0
  2. package/dist/config.d.ts.map +1 -0
  3. package/dist/index.d.ts +40 -8
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +196 -6
  6. package/dist/index.js.map +13 -0
  7. package/dist/logger.d.ts +54 -0
  8. package/dist/logger.d.ts.map +1 -0
  9. package/dist/lru.d.ts +21 -0
  10. package/dist/lru.d.ts.map +1 -0
  11. package/dist/next/cxx-loader.cjs +412 -5
  12. package/dist/next/cxx-loader.cjs.map +16 -0
  13. package/dist/next/index.d.ts +104 -262
  14. package/dist/next/index.d.ts.map +1 -0
  15. package/dist/next/index.js +305 -51
  16. package/dist/next/index.js.map +14 -0
  17. package/dist/types.d.ts +21 -0
  18. package/dist/types.d.ts.map +1 -0
  19. package/dist/utils.d.ts +4 -0
  20. package/dist/utils.d.ts.map +1 -0
  21. package/dist/vite/index.d.ts +13 -0
  22. package/dist/vite/index.d.ts.map +1 -0
  23. package/dist/vite/index.js +380 -0
  24. package/dist/vite/index.js.map +15 -0
  25. package/package.json +11 -26
  26. package/src/config.ts +4 -0
  27. package/src/index.ts +218 -9
  28. package/src/logger.ts +151 -0
  29. package/src/lru.ts +51 -0
  30. package/src/next/cxx-loader.js +16 -4
  31. package/src/next/index.ts +169 -46
  32. package/src/types.ts +28 -0
  33. package/src/utils.ts +24 -0
  34. package/src/vite/index.ts +156 -0
  35. package/LICENSE +0 -21
  36. package/README.md +0 -71
  37. package/dist/inject/index.cjs +0 -60
  38. package/dist/inject/index.d.ts +0 -8
  39. package/dist/inject/index.js +0 -58
  40. package/dist/next/cxx-loader.d.ts +0 -7
  41. package/dist/next/cxx-loader.js +0 -7
  42. package/dist/vite-plugin-cxx/index.d.ts +0 -6
  43. package/dist/vite-plugin-cxx/index.js +0 -16
  44. package/src/cyrb53.ts +0 -20
  45. package/src/inject.ts +0 -54
  46. package/src/vite-plugin-cxx/index.ts +0 -16
package/src/utils.ts ADDED
@@ -0,0 +1,24 @@
1
+ export function capitalise(str: string) {
2
+ return str.charAt(0).toUpperCase() + str.slice(1)
3
+ }
4
+
5
+ export function pascalise(str: string) {
6
+ return str
7
+ .split(/[ _-]+/)
8
+ .map(capitalise)
9
+ .join('')
10
+ }
11
+
12
+ export function debounce<T extends unknown[]>(fn: (...args: T) => void, wait: number) {
13
+ let timeoutId: ReturnType<typeof setTimeout> | null = null
14
+
15
+ return (...args: T) => {
16
+ if (timeoutId) {
17
+ clearTimeout(timeoutId)
18
+ }
19
+
20
+ timeoutId = setTimeout(() => {
21
+ fn.apply(null, args)
22
+ }, wait)
23
+ }
24
+ }
@@ -0,0 +1,156 @@
1
+ import { realpathSync } from 'node:fs'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ import { type Plugin, type ViteDevServer } from 'vite'
6
+
7
+ import type { BuildContext, PluginConfig } from '../types.js'
8
+ import { GENERATED_DIR } from '../config.js'
9
+ import { collect, flattenTags, isENOENT, writeTypes } from '../index.js'
10
+ import { Logger } from '../logger.js'
11
+ import { debounce } from '../utils.js'
12
+
13
+ const ACCEPTED_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx'])
14
+
15
+ function normaliseWatchPath(p: string) {
16
+ return p.replace(/\\/g, '/')
17
+ }
18
+
19
+ export default function cxx(pluginConfig: PluginConfig = {}) {
20
+ const buildContext = {
21
+ tagsByFile: new Map(),
22
+ } satisfies BuildContext
23
+
24
+ const logger = new Logger(
25
+ (pluginConfig?.logger?.level ?? process.env.NODE_ENV === 'production')
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
41
+ function resolveWatchFile(filePath: string) {
42
+ const absolutePath = path.resolve(watchRoot, filePath)
43
+ const parentPath = path.dirname(absolutePath)
44
+
45
+ try {
46
+ const resolvedParentPath = normaliseWatchPath(realpathSync.native(parentPath))
47
+
48
+ return normaliseWatchPath(
49
+ path.join(resolvedParentPath, path.basename(absolutePath)),
50
+ )
51
+ } catch {
52
+ return normaliseWatchPath(absolutePath)
53
+ }
54
+ }
55
+
56
+ function watchedFile(filePath: string) {
57
+ const resolvedPath = resolveWatchFile(filePath)
58
+
59
+ return (
60
+ watchedRoots.some(root => resolvedPath.startsWith(root)) &&
61
+ !resolvedPath.includes(`/${GENERATED_DIR}/`) &&
62
+ ACCEPTED_EXTENSIONS.has(path.extname(resolvedPath))
63
+ )
64
+ }
65
+
66
+ const rebuild = debounce((event: 'add' | 'change' | 'unlink', filePath: string) => {
67
+ function queue() {
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
+ }
75
+
76
+ rebuildRunning = true
77
+
78
+ do {
79
+ rebuildQueued = false
80
+
81
+ try {
82
+ const hadTags = buildContext.tagsByFile.has(file)
83
+ let hasTags = false
84
+
85
+ if (event === 'unlink') {
86
+ buildContext.tagsByFile.delete(file)
87
+ } else {
88
+ const source = await fs.readFile(file, 'utf-8')
89
+ const { tags } = collect(source, file, pluginConfig)
90
+
91
+ hasTags = tags.size > 0
92
+
93
+ if (hasTags) {
94
+ buildContext.tagsByFile.set(file, tags)
95
+ } else {
96
+ buildContext.tagsByFile.delete(file)
97
+ }
98
+ }
99
+
100
+ if (!hadTags && !hasTags) continue
101
+
102
+ await writeTypes(flattenTags(buildContext.tagsByFile))
103
+ logger.info(`updated types from ${file}`)
104
+ } catch (err) {
105
+ if (isENOENT(err)) {
106
+ if (!buildContext.tagsByFile.has(file)) continue
107
+
108
+ buildContext.tagsByFile.delete(file)
109
+ await writeTypes(flattenTags(buildContext.tagsByFile))
110
+
111
+ logger.info(`updated types from ${file}`)
112
+
113
+ continue
114
+ }
115
+
116
+ logger.error(`failed to update types from ${file}`, err)
117
+ }
118
+ } while (rebuildQueued)
119
+
120
+ rebuildRunning = false
121
+ })()
122
+ }
123
+
124
+ // ignore anything outside the watched content dirs
125
+ if (!watchedFile(filePath)) return
126
+
127
+ const file = resolveWatchFile(filePath)
128
+ queue()
129
+ }, 75)
130
+
131
+ return {
132
+ name: 'cxx',
133
+ enforce: 'pre',
134
+ transform(code, id) {
135
+ const { template, tags } = collect(code, id, pluginConfig)
136
+
137
+ if (tags.size) buildContext.tagsByFile.set(id, tags)
138
+
139
+ return {
140
+ code: template,
141
+ map: null,
142
+ }
143
+ },
144
+ configureServer(server: ViteDevServer) {
145
+ logger.info('Watching for changes...')
146
+
147
+ server.watcher
148
+ .on('add', (p: string) => rebuild('add', p))
149
+ .on('change', (p: string) => rebuild('change', p))
150
+ .on('unlink', (p: string) => rebuild('unlink', p))
151
+ },
152
+ async writeBundle() {
153
+ await writeTypes(flattenTags(buildContext.tagsByFile))
154
+ },
155
+ } satisfies Plugin
156
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Jerome Kenway
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/README.md DELETED
@@ -1,71 +0,0 @@
1
- # @jk2908/cxx
2
-
3
- Build-time CSS template tag utilities for React 19.
4
-
5
- ## Install
6
-
7
- ```sh
8
- npm install @jk2908/cxx lightningcss react react-dom
9
- ```
10
-
11
- ## Usage
12
-
13
- ```tsx
14
- import { cxx } from '@jk2908/cxx'
15
-
16
- const [css, styles, href] = cxx`
17
- .header {
18
- display: flex;
19
- gap: 0.75rem;
20
- }
21
- `
22
-
23
- export function Header() {
24
- return (
25
- <header className={styles.header}>
26
- <style href={href} precedence="medium">
27
- {css}
28
- </style>
29
- </header>
30
- )
31
- }
32
- ```
33
-
34
- The `cxx` tag is a build-time marker. Use the Vite plugin or Next helper so the placeholders are replaced before runtime. String interpolation inside the template is not supported.
35
-
36
- ## Vite
37
-
38
- ```ts
39
- import { defineConfig } from 'vite'
40
- import react from '@vitejs/plugin-react'
41
- import cxx from '@jk2908/cxx/vite-plugin-cxx'
42
-
43
- export default defineConfig({
44
- plugins: [react(), cxx()],
45
- })
46
- ```
47
-
48
- ## Next
49
-
50
- ```ts
51
- import { withCxx } from '@jk2908/cxx/next'
52
-
53
- const nextConfig = {}
54
-
55
- export default withCxx(nextConfig)
56
- ```
57
-
58
- Both integrations accept an optional config object with `lightningcss` options. The default transform enables minification and CSS Modules output.
59
-
60
- ```ts
61
- cxx({
62
- lightningcss: {
63
- minify: false,
64
- cssModules: false,
65
- },
66
- })
67
- ```
68
-
69
- ## License
70
-
71
- MIT
@@ -1,60 +0,0 @@
1
- Object.defineProperty(exports, '__esModule', { value: true });
2
-
3
- var lightningcss = require('lightningcss');
4
-
5
- // cyrb53 (c) 2018 bryc (github.com/bryc)
6
- // https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
7
- function cyrb53(str, seed = 0) {
8
- let h1 = 0xdeadbeef ^ seed;
9
- let h2 = 0x41c6ce57 ^ seed;
10
- for(let i = 0, ch; i < str.length; i++){
11
- ch = str.charCodeAt(i);
12
- h1 = Math.imul(h1 ^ ch, 2654435761);
13
- h2 = Math.imul(h2 ^ ch, 1597334677);
14
- }
15
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
16
- h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
17
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
18
- h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
19
- return 4294967296 * (2097151 & h2) + (h1 >>> 0);
20
- }
21
-
22
- const varMatch = /(?:const|var|let)\s*\[(\w+|)\s*,?\s*(\w+|)\s*,?\s*(\w+|)\s*\]\s*=\s*cxx\s*`([\s\S]*?)`/gm;
23
- const DEFAULT_CONFIG = {
24
- lightningcss: {
25
- minify: true,
26
- cssModules: true
27
- }
28
- };
29
- function inject(source, id, config = {}) {
30
- try {
31
- const tmpl = source.replace(varMatch, (_, ...args)=>{
32
- const [varOne, varTwo, varThree, css] = args;
33
- const { code, exports } = lightningcss.transform({
34
- ...DEFAULT_CONFIG.lightningcss,
35
- ...config.lightningcss,
36
- filename: id,
37
- code: new TextEncoder().encode(css)
38
- });
39
- const href = cyrb53([
40
- ...Object.keys(exports ?? {}),
41
- id
42
- ].join(''));
43
- const classes = exports ? Object.fromEntries(Object.entries(exports).map(([key, value])=>[
44
- key,
45
- value.name
46
- ])) : {};
47
- return [
48
- varOne && `const ${varOne} = \`${code}\``,
49
- varTwo && `const ${varTwo} = ${JSON.stringify(classes)}`,
50
- varThree && `const ${varThree} = \`${href}\``
51
- ].filter(Boolean).join('\n');
52
- });
53
- return tmpl;
54
- } catch (err) {
55
- console.error('cxx error :(', err);
56
- return source;
57
- }
58
- }
59
-
60
- exports.inject = inject;
@@ -1,8 +0,0 @@
1
- import { CustomAtRules, TransformOptions } from 'lightningcss';
2
-
3
- type Config<C extends CustomAtRules = CustomAtRules> = {
4
- lightningcss?: Partial<TransformOptions<C>>;
5
- };
6
- declare function inject<C extends CustomAtRules = CustomAtRules>(source: string, id: string, config?: Config<C>): string;
7
-
8
- export { type Config, inject };
@@ -1,58 +0,0 @@
1
- import { transform } from 'lightningcss';
2
-
3
- // cyrb53 (c) 2018 bryc (github.com/bryc)
4
- // https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
5
- function cyrb53(str, seed = 0) {
6
- let h1 = 0xdeadbeef ^ seed;
7
- let h2 = 0x41c6ce57 ^ seed;
8
- for(let i = 0, ch; i < str.length; i++){
9
- ch = str.charCodeAt(i);
10
- h1 = Math.imul(h1 ^ ch, 2654435761);
11
- h2 = Math.imul(h2 ^ ch, 1597334677);
12
- }
13
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
14
- h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
15
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
16
- h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
17
- return 4294967296 * (2097151 & h2) + (h1 >>> 0);
18
- }
19
-
20
- const varMatch = /(?:const|var|let)\s*\[(\w+|)\s*,?\s*(\w+|)\s*,?\s*(\w+|)\s*\]\s*=\s*cxx\s*`([\s\S]*?)`/gm;
21
- const DEFAULT_CONFIG = {
22
- lightningcss: {
23
- minify: true,
24
- cssModules: true
25
- }
26
- };
27
- function inject(source, id, config = {}) {
28
- try {
29
- const tmpl = source.replace(varMatch, (_, ...args)=>{
30
- const [varOne, varTwo, varThree, css] = args;
31
- const { code, exports } = transform({
32
- ...DEFAULT_CONFIG.lightningcss,
33
- ...config.lightningcss,
34
- filename: id,
35
- code: new TextEncoder().encode(css)
36
- });
37
- const href = cyrb53([
38
- ...Object.keys(exports ?? {}),
39
- id
40
- ].join(''));
41
- const classes = exports ? Object.fromEntries(Object.entries(exports).map(([key, value])=>[
42
- key,
43
- value.name
44
- ])) : {};
45
- return [
46
- varOne && `const ${varOne} = \`${code}\``,
47
- varTwo && `const ${varTwo} = ${JSON.stringify(classes)}`,
48
- varThree && `const ${varThree} = \`${href}\``
49
- ].filter(Boolean).join('\n');
50
- });
51
- return tmpl;
52
- } catch (err) {
53
- console.error('cxx error :(', err);
54
- return source;
55
- }
56
- }
57
-
58
- export { inject };
@@ -1,7 +0,0 @@
1
- import { inject } from '../inject/index.js';
2
-
3
- function loader(source) {
4
- return inject(source, this.resourcePath, this.getOptions())
5
- }
6
-
7
- export { loader as default };
@@ -1,7 +0,0 @@
1
- import { inject } from '../inject/index.js';
2
-
3
- function loader(source) {
4
- return inject(source, this.resourcePath, this.getOptions());
5
- }
6
-
7
- export { loader as default };
@@ -1,6 +0,0 @@
1
- import { Plugin } from 'vite';
2
- import { Config } from '../inject/index.js';
3
-
4
- declare function vitePluginCxx(config?: Config): Plugin;
5
-
6
- export { vitePluginCxx as default };
@@ -1,16 +0,0 @@
1
- import { inject } from '../inject/index.js';
2
-
3
- function vitePluginCxx(config) {
4
- return {
5
- name: 'cxx',
6
- enforce: 'pre',
7
- transform (code, id) {
8
- return {
9
- code: inject(code, id, config),
10
- map: null
11
- };
12
- }
13
- };
14
- }
15
-
16
- export { vitePluginCxx as default };
package/src/cyrb53.ts DELETED
@@ -1,20 +0,0 @@
1
- // cyrb53 (c) 2018 bryc (github.com/bryc)
2
- // https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
3
-
4
- export function cyrb53(str: string, seed = 0) {
5
- let h1 = 0xdeadbeef ^ seed
6
- let h2 = 0x41c6ce57 ^ seed
7
-
8
- for (let i = 0, ch: number; i < str.length; i++) {
9
- ch = str.charCodeAt(i)
10
- h1 = Math.imul(h1 ^ ch, 2654435761)
11
- h2 = Math.imul(h2 ^ ch, 1597334677)
12
- }
13
-
14
- h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
15
- h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
16
- h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
17
- h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
18
-
19
- return 4294967296 * (2097151 & h2) + (h1 >>> 0)
20
- }
package/src/inject.ts DELETED
@@ -1,54 +0,0 @@
1
- import { cyrb53 } from './cyrb53'
2
-
3
- import { type TransformOptions, type CustomAtRules, transform } from 'lightningcss'
4
-
5
- const varMatch =
6
- /(?:const|var|let)\s*\[(\w+|)\s*,?\s*(\w+|)\s*,?\s*(\w+|)\s*\]\s*=\s*cxx\s*`([\s\S]*?)`/gm
7
-
8
- export type Config<C extends CustomAtRules = CustomAtRules> = {
9
- lightningcss?: Partial<TransformOptions<C>>
10
- }
11
-
12
- const DEFAULT_CONFIG: Config = {
13
- lightningcss: {
14
- minify: true,
15
- cssModules: true,
16
- },
17
- } as const
18
-
19
- export function inject<C extends CustomAtRules = CustomAtRules>(
20
- source: string,
21
- id: string,
22
- config: Config<C> = {},
23
- ) {
24
- try {
25
- const tmpl = source.replace(varMatch, (_: string, ...args: string[]) => {
26
- const [varOne, varTwo, varThree, css] = args
27
-
28
- const { code, exports } = transform({
29
- ...DEFAULT_CONFIG.lightningcss,
30
- ...config.lightningcss,
31
- filename: id,
32
- code: new TextEncoder().encode(css),
33
- })
34
-
35
- const href = cyrb53([...Object.keys(exports ?? {}), id].join(''))
36
- const classes = exports
37
- ? Object.fromEntries(Object.entries(exports).map(([key, value]) => [key, value.name]))
38
- : {}
39
-
40
- return [
41
- varOne && `const ${varOne} = \`${code}\``,
42
- varTwo && `const ${varTwo} = ${JSON.stringify(classes)}`,
43
- varThree && `const ${varThree} = \`${href}\``,
44
- ]
45
- .filter(Boolean)
46
- .join('\n')
47
- })
48
-
49
- return tmpl
50
- } catch (err) {
51
- console.error('cxx error :(', err)
52
- return source
53
- }
54
- }
@@ -1,16 +0,0 @@
1
- import type { Plugin } from 'vite'
2
-
3
- import { inject, type Config } from '../inject'
4
-
5
- export default function vitePluginCxx(config?: Config): Plugin {
6
- return {
7
- name: 'cxx',
8
- enforce: 'pre',
9
- transform(code, id) {
10
- return {
11
- code: inject(code, id, config),
12
- map: null
13
- }
14
- }
15
- }
16
- }