@jk2908/cxx 0.1.10 → 0.2.1

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/README.md +37 -20
  2. package/dist/config.d.ts +5 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/index.d.ts +40 -8
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +196 -6
  7. package/dist/index.js.map +13 -0
  8. package/dist/logger.d.ts +54 -0
  9. package/dist/logger.d.ts.map +1 -0
  10. package/dist/lru.d.ts +21 -0
  11. package/dist/lru.d.ts.map +1 -0
  12. package/dist/next/cxx-loader.cjs +412 -5
  13. package/dist/next/cxx-loader.cjs.map +16 -0
  14. package/dist/next/index.d.ts +104 -262
  15. package/dist/next/index.d.ts.map +1 -0
  16. package/dist/next/index.js +305 -51
  17. package/dist/next/index.js.map +14 -0
  18. package/dist/types.d.ts +21 -0
  19. package/dist/types.d.ts.map +1 -0
  20. package/dist/utils.d.ts +4 -0
  21. package/dist/utils.d.ts.map +1 -0
  22. package/dist/vite/index.d.ts +13 -0
  23. package/dist/vite/index.d.ts.map +1 -0
  24. package/dist/vite/index.js +380 -0
  25. package/dist/vite/index.js.map +15 -0
  26. package/package.json +11 -26
  27. package/src/config.ts +4 -0
  28. package/src/index.ts +218 -9
  29. package/src/logger.ts +151 -0
  30. package/src/lru.ts +51 -0
  31. package/src/next/cxx-loader.js +16 -4
  32. package/src/next/index.ts +169 -46
  33. package/src/types.ts +28 -0
  34. package/src/utils.ts +24 -0
  35. package/src/vite/index.ts +156 -0
  36. package/LICENSE +0 -21
  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/index.ts CHANGED
@@ -1,12 +1,221 @@
1
- declare module 'react' {
2
- interface StyleHTMLAttributes<T> extends React.HTMLAttributes<T> {
3
- href?: string
4
- precedence?: 'low' | 'medium' | 'high'
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ import { transform } from 'lightningcss'
6
+
7
+ import type { BuildContext, Cxx, PluginConfig, Result, Tags } from './types.js'
8
+ import { AUTOGEN_MSG, GENERATED_DIR, PKG_NAME } from './config.js'
9
+ import { LruCache } from './lru.js'
10
+ import { pascalise } from './utils.js'
11
+
12
+ /**
13
+ * LRU cache of `collect` results by file id and last seen source so watcher and transform can
14
+ * reuse the same parsed template/tags
15
+ */
16
+ const collectCache = new LruCache<{ source: string; template: string; tags: Tags }>()
17
+
18
+ /**
19
+ * LRU cache for file content. Stores the last written content per file path so `maybeWrite`
20
+ * can skip disk i/o when nothing has changed
21
+ */
22
+ const fileCache = new LruCache<string>()
23
+
24
+ /**
25
+ * Returns whether the error was caused by a missing file or directory
26
+ */
27
+ export function isENOENT(err: unknown) {
28
+ return err instanceof Error && 'code' in err && err.code === 'ENOENT'
29
+ }
30
+
31
+ /**
32
+ * Write a file only if the content has changed since the last build
33
+ */
34
+ export async function maybeWrite(filePath: string, content: string) {
35
+ const cached = fileCache.get(filePath)
36
+
37
+ if (cached !== content) {
38
+ // cache says content changed, write without reading
39
+ await fs.writeFile(filePath, content)
40
+ fileCache.set(filePath, content)
41
+
42
+ return true
43
+ }
44
+
45
+ // cache miss or cache hit with same content — verify on disk
46
+ try {
47
+ if ((await fs.readFile(filePath, 'utf-8')) === content) {
48
+ fileCache.set(filePath, content)
49
+ return false
50
+ }
51
+ } catch (err) {
52
+ if (!isENOENT(err)) throw err
5
53
  }
54
+
55
+ await fs.writeFile(filePath, content)
56
+ fileCache.set(filePath, content)
57
+
58
+ return true
6
59
  }
7
60
 
8
- export const cxx = (_: TemplateStringsArray): readonly ['', Record<string, string>, ''] => [
9
- '',
10
- {},
11
- '',
12
- ]
61
+ // matches `[css, classes, href] = cxx\`...\`` and `cxx.tag<T>('name')\`...\``
62
+ // capture groups: 1-3 assigned variable names, 4 plain css body, 5 quote
63
+ // kind, 6 tag name, 7 tagged css body
64
+ const regex =
65
+ /(?:const|var|let)\s*\[(\w+|)\s*,?\s*(\w+|)\s*,?\s*(\w+|)\s*\]\s*=\s*cxx(?:\s*`([\s\S]*?)`|\s*\.\s*tag(?:\s*<[^>]+>)?\s*\(\s*(['"])(.*?)\5\s*\)\s*`([\s\S]*?)`)/gm
66
+
67
+ /**
68
+ * Hash transformed CSS into a stable href for style deduplication
69
+ */
70
+ function hashCss(code: Uint8Array) {
71
+ return `cxx-${createHash('sha256').update(code).digest('base64url').slice(0, 12)}`
72
+ }
73
+
74
+ export class TagDuplicateError extends Error {
75
+ constructor(tag: string) {
76
+ super(`Tag ${tag} has already been seen. Tags must be unique across your application`)
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Transform `cxx` templates in one source file and collect any exported tag types
82
+ */
83
+ export function collect(source: string, id: string, config: PluginConfig) {
84
+ const tags: Tags = new Map()
85
+
86
+ const cached = collectCache.get(id)
87
+ if (cached?.source === source) return cached
88
+
89
+ const template = source.replace(
90
+ regex,
91
+ (_match, varOne, varTwo, varThree, plainCss, _quote, tag, taggedCss) => {
92
+ const css = plainCss ?? taggedCss
93
+
94
+ const { code, exports = {} } = transform({
95
+ minify: true,
96
+ cssModules: true,
97
+ filename: id,
98
+ code: new TextEncoder().encode(css),
99
+ })
100
+
101
+ const href = hashCss(code)
102
+ const styles = Object.fromEntries(
103
+ Object.entries(exports).map(([k, v]) => [k, v.name]),
104
+ )
105
+
106
+ if (tag) {
107
+ const suffix =
108
+ typeof config.typeSuffix === 'string'
109
+ ? config.typeSuffix
110
+ : config.typeSuffix === false
111
+ ? ''
112
+ : 'Classes'
113
+ const tagWithSuffix = pascalise(`${tag}${suffix}`)
114
+
115
+ if (tags.has(tagWithSuffix)) throw new TagDuplicateError(tagWithSuffix)
116
+
117
+ // side effect but neccessary
118
+ tags.set(tagWithSuffix, Object.keys(styles))
119
+ }
120
+
121
+ return [
122
+ varOne && `const ${varOne} = \`${code}\``,
123
+ varTwo && `const ${varTwo} = ${JSON.stringify(styles)}`,
124
+ varThree && `const ${varThree} = \`${href}\``,
125
+ ]
126
+ .filter(Boolean)
127
+ .join('\n')
128
+ },
129
+ )
130
+
131
+ // cache
132
+ collectCache.set(id, { source, template, tags })
133
+
134
+ return {
135
+ template,
136
+ tags,
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Write the generated declaration file for the current set of collected tags
142
+ */
143
+ export async function writeTypes(tags: Tags) {
144
+ const filePath = path.join(GENERATED_DIR, 'index.d.ts')
145
+
146
+ if (!tags.size) {
147
+ try {
148
+ await fs.rm(filePath, { force: true })
149
+ fileCache.delete(filePath)
150
+ } catch (err) {
151
+ if (!isENOENT(err)) throw err
152
+ }
153
+
154
+ return
155
+ }
156
+
157
+ await fs.mkdir(GENERATED_DIR, { recursive: true })
158
+
159
+ await maybeWrite(
160
+ filePath,
161
+ `${AUTOGEN_MSG}
162
+
163
+ import '${PKG_NAME}'
164
+
165
+ declare module '${PKG_NAME}' {
166
+ ${tagsToType(tags)}
167
+ }
168
+ `,
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Render collected tags as a module augmentation for the package entrypoint
174
+ */
175
+ export function tagsToType(tags: Tags) {
176
+ return [...tags]
177
+ .map(
178
+ ([tag, classes]) => `
179
+ export type ${tag} = ${
180
+ classes.length > 0
181
+ ? classes
182
+ .toSorted((a, b) => a.length - b.length)
183
+ .map(c => `'${c}'`)
184
+ .join(' | ')
185
+ : 'never'
186
+ }
187
+ `,
188
+ )
189
+ .join('\n')
190
+ }
191
+
192
+ /**
193
+ * Merge per-file tag maps into one application-wide tag map
194
+ */
195
+ export function flattenTags(tagsByFile: BuildContext['tagsByFile']) {
196
+ const tags = new Map<string, string[]>()
197
+
198
+ for (const fileTags of tagsByFile.values()) {
199
+ for (const [name, classes] of fileTags) {
200
+ if (tags.has(name)) throw new TagDuplicateError(name)
201
+ tags.set(name, classes)
202
+ }
203
+ }
204
+
205
+ return tags
206
+ }
207
+
208
+ /**
209
+ * Build-time template tag placeholder used by the transform to emit CSS, classes and href values
210
+ */
211
+ export const cxx: Cxx = Object.assign((_: TemplateStringsArray): Result => ['', {}, ''], {
212
+ tag:
213
+ <ClassName extends string = string>(_: string) =>
214
+ (_strings: TemplateStringsArray): Result<ClassName> => [
215
+ '',
216
+ {} as Readonly<Record<ClassName, string>>,
217
+ '',
218
+ ],
219
+ })
220
+
221
+ export const css = cxx
package/src/logger.ts ADDED
@@ -0,0 +1,151 @@
1
+ const LEVELS = {
2
+ debug: 0,
3
+ info: 1,
4
+ warn: 2,
5
+ error: 3,
6
+ fatal: 4,
7
+ } as const
8
+
9
+ export type LogLevel = keyof typeof LEVELS
10
+
11
+ type LogEntry = {
12
+ ts: number
13
+ level: LogLevel
14
+ message: string
15
+ error?:
16
+ | Error
17
+ | {
18
+ message: string
19
+ stack?: string
20
+ cause?: unknown
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Log messages with different severity levels
26
+ */
27
+ export class Logger {
28
+ static #defaultLevel: LogLevel = 'info'
29
+
30
+ #level?: LogLevel
31
+
32
+ constructor(
33
+ level?: LogLevel,
34
+ public name?: string,
35
+ ) {
36
+ this.#level = level
37
+ }
38
+
39
+ static set defaultLevel(level: LogLevel) {
40
+ Logger.#defaultLevel = level
41
+ }
42
+
43
+ static get defaultLevel() {
44
+ return Logger.#defaultLevel
45
+ }
46
+
47
+ /**
48
+ * Convert a value to an Error instance
49
+ */
50
+ static toError(err: unknown) {
51
+ return err instanceof Error ? err : new Error(String(err), { cause: err })
52
+ }
53
+
54
+ /**
55
+ * Stringify the error for logging
56
+ */
57
+ static print(err: unknown) {
58
+ if (err instanceof Error) {
59
+ return err.message + (err.stack ? `\n${err.stack}` : '')
60
+ }
61
+
62
+ // for plain objects, attempt to stringify with indentation
63
+ // for readability
64
+ if (typeof err === 'object' && err !== null) {
65
+ try {
66
+ return JSON.stringify(err, null, 2)
67
+ } catch {
68
+ // if stringify fails (e.g. circular reference), fall back
69
+ // to basic string conversion
70
+ return String(err)
71
+ }
72
+ }
73
+
74
+ return String(err)
75
+ }
76
+
77
+ set level(level: LogLevel) {
78
+ this.#level = level
79
+ }
80
+
81
+ get level() {
82
+ return this.#level ?? Logger.#defaultLevel
83
+ }
84
+
85
+ /**
86
+ * Log a message with a specific level
87
+ */
88
+ log(level: LogLevel, message: string, error?: Error) {
89
+ if (LEVELS[level] < LEVELS[this.level]) return
90
+
91
+ const entry: LogEntry = {
92
+ ts: Date.now(),
93
+ level,
94
+ message,
95
+ }
96
+
97
+ if (level === 'error' || level === 'fatal') {
98
+ entry.error = error ? Logger.toError(error) : new Error(message)
99
+ }
100
+
101
+ const line = `${this.name ? `[${this.name}]` : ''} [${entry.ts}] [${level.toUpperCase()}] ${message}`
102
+ const extra = entry.error ? `\n${Logger.print(entry.error)}` : ''
103
+
104
+ if (level === 'warn') {
105
+ console.warn(line, extra)
106
+ return
107
+ }
108
+
109
+ if (level === 'error' || level === 'fatal') {
110
+ console.error(line, extra)
111
+ return
112
+ }
113
+
114
+ console.log(line, extra)
115
+ }
116
+
117
+ /**
118
+ * Log a debug message
119
+ */
120
+ debug(...messages: string[]) {
121
+ this.log('debug', messages.join(' '))
122
+ }
123
+
124
+ /**
125
+ * Log an info message
126
+ */
127
+ info(...messages: string[]) {
128
+ this.log('info', messages.join(' '))
129
+ }
130
+
131
+ /**
132
+ * Log a warning message
133
+ */
134
+ warn(...messages: string[]) {
135
+ this.log('warn', messages.join(' '))
136
+ }
137
+
138
+ /**
139
+ * Log an error message
140
+ */
141
+ error(message: string, error?: unknown) {
142
+ this.log('error', message, error === undefined ? undefined : Logger.toError(error))
143
+ }
144
+
145
+ /**
146
+ * Log a fatal error message
147
+ */
148
+ fatal(message: string, error?: unknown) {
149
+ this.log('fatal', message, error === undefined ? undefined : Logger.toError(error))
150
+ }
151
+ }
package/src/lru.ts ADDED
@@ -0,0 +1,51 @@
1
+ export const CACHE_MAX_SIZE = 100
2
+
3
+ export class LruCache<T> {
4
+ #cache: Map<string, T> = new Map()
5
+ #maxSize: number
6
+
7
+ constructor(maxSize = CACHE_MAX_SIZE) {
8
+ this.#maxSize = maxSize
9
+ }
10
+
11
+ get size() {
12
+ return this.#cache.size
13
+ }
14
+
15
+ /**
16
+ * Retrieve a cached value and promote it to most-recently-used so it won't
17
+ * be evicted while still actively referenced
18
+ */
19
+ get(key: string) {
20
+ const cached = this.#cache.get(key)
21
+ if (this.#cache.has(key)) this.set(key, cached as T)
22
+
23
+ return cached
24
+ }
25
+
26
+ /**
27
+ * Promote an existing cache entry to most-recently-used by deleting and
28
+ * re-inserting it, which moves it to the end of the Map's iteration
29
+ * order. If the cache is at capacity, evict the least recently used
30
+ * entry (front) before inserting
31
+ */
32
+ set(key: string, value: T) {
33
+ // drop any existing entry
34
+ this.#cache.delete(key)
35
+
36
+ if (this.#cache.size >= this.#maxSize) {
37
+ const lru = this.#cache.keys().next().value
38
+ if (lru !== undefined) this.#cache.delete(lru)
39
+ }
40
+
41
+ this.#cache.set(key, value)
42
+ }
43
+
44
+ delete(key: string) {
45
+ return this.#cache.delete(key)
46
+ }
47
+
48
+ clear() {
49
+ return this.#cache.clear()
50
+ }
51
+ }
@@ -1,5 +1,17 @@
1
- import { inject } from '../inject'
1
+ import { Logger } from '../logger.js'
2
+ import { processFile } from './index.js'
2
3
 
3
- export default function loader(source) {
4
- return inject(source, this.resourcePath, this.getOptions())
5
- }
4
+ const loader = function (source) {
5
+ const { pluginConfig } = this.getOptions()
6
+ const logger = new Logger(
7
+ (pluginConfig?.logger?.level ?? process.env.NODE_ENV === 'production')
8
+ ? 'error'
9
+ : 'debug',
10
+ )
11
+
12
+ const { template } = processFile(source, this.resourcePath, pluginConfig, logger)
13
+
14
+ return template
15
+ }
16
+
17
+ export default loader
package/src/next/index.ts CHANGED
@@ -1,56 +1,179 @@
1
+ import type { AsyncSubscription, Event } from '@parcel/watcher'
2
+ import type { NextConfig } from 'next'
3
+
4
+ import fs from 'node:fs/promises'
1
5
  import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
2
7
 
3
- import type { Configuration } from 'webpack'
4
- import type { NextConfig } from 'next'
5
- import type { WebpackConfigContext } from 'next/dist/server/config-shared'
6
-
7
- import type { Config } from '../inject'
8
-
9
- export function withCxx(nextConfig: NextConfig = {}, config: Config = {}) {
10
- const pkg = require('next/package.json')
11
-
12
- const IS_CANARY = pkg.version?.includes('canary') ?? false
13
- const IS_TURBO = process.env.TURBOPACK === '1'
14
- const TURBO_GLOB = '*.{tsx,jsx,ts,js}'
15
-
16
- const loader = path.resolve(__dirname, `cxx-loader.${IS_TURBO ? 'cjs' : 'js'}`)
17
-
18
- const turboConfig = {
19
- ...nextConfig.turbo,
20
- rules: {
21
- ...nextConfig.turbo?.rules,
22
- [TURBO_GLOB]: {
23
- loaders: [
24
- ...(nextConfig.turbo?.rules?.[TURBO_GLOB]?.loaders ?? []),
25
- {
26
- loader,
27
- options: config,
28
- },
29
- ],
30
- },
8
+ import watcher from '@parcel/watcher'
9
+
10
+ import type { Logger } from '../logger.js'
11
+ import type { PluginConfig, Tags } from '../types.js'
12
+ import { collect, flattenTags, isENOENT, writeTypes } from '../index.js'
13
+
14
+ const nextDir = path.dirname(fileURLToPath(import.meta.url))
15
+ const loader = path.join(nextDir, 'cxx-loader.cjs')
16
+
17
+ // keep the latest extracted tags per file so one edit can rebuild the full type surface
18
+ const tagsByFile: Map<string, Tags> = new Map()
19
+
20
+ // directory watches are shared, but rebuilds still need the file's config and logger
21
+ const watchedFiles = new Map<string, { pluginConfig: PluginConfig; logger: Logger }>()
22
+ const subscriptionsByDir = new Map<string, Promise<AsyncSubscription>>()
23
+
24
+ /**
25
+ * Rebuild the generated type surface for one watched file after a Parcel event
26
+ */
27
+ async function rebuildFile(filePath: string, event: Event['type']) {
28
+ const watchedFile = watchedFiles.get(filePath)
29
+ if (!watchedFile) return
30
+
31
+ const { pluginConfig, logger } = watchedFile
32
+ const hadTags = tagsByFile.has(filePath)
33
+
34
+ if (event === 'delete') {
35
+ tagsByFile.delete(filePath)
36
+ if (!hadTags) return
37
+
38
+ await writeTypes(flattenTags(tagsByFile))
39
+ logger.info(`updated types from ${filePath}`)
40
+
41
+ return
42
+ }
43
+
44
+ try {
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
+ }
54
+
55
+ if (!hadTags && !hasTags) return
56
+
57
+ await writeTypes(flattenTags(tagsByFile))
58
+ logger.info(`updated types from ${filePath}`)
59
+ } catch (err) {
60
+ if (!isENOENT(err)) throw err
61
+ if (!hadTags) return
62
+
63
+ tagsByFile.delete(filePath)
64
+ await writeTypes(flattenTags(tagsByFile))
65
+
66
+ logger.info(`updated types from ${filePath}`)
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Transform one module and, in development, register it with the shared watcher state
72
+ */
73
+ export function processFile(
74
+ source: string,
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)
82
+
83
+ if (process.env.NODE_ENV !== 'production') {
84
+ // Parcel watches directories, so remember the exact file inputs we care about
85
+ watchedFiles.set(resolvedFilePath, { pluginConfig, logger })
86
+
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
+ return
93
+ }
94
+
95
+ // Parcel can report unrelated files from the same directory, so ignore anything unregistered
96
+ for (const event of events) {
97
+ const eventFilePath = path.resolve(event.path)
98
+ if (!watchedFiles.has(eventFilePath)) continue
99
+
100
+ try {
101
+ await rebuildFile(eventFilePath, event.type)
102
+ } catch (watchErr) {
103
+ logger.error(`failed to update types from ${eventFilePath}`, watchErr)
104
+ }
105
+ }
106
+ })
107
+
108
+ subscriptionsByDir.set(directoryPath, subscription)
109
+ }
110
+ }
111
+
112
+ const result = collect(source, filePath, pluginConfig)
113
+
114
+ if (result.tags.size > 0) {
115
+ tagsByFile.set(filePath, result.tags)
116
+ } else {
117
+ tagsByFile.delete(filePath)
118
+ }
119
+
120
+ void writeTypes(flattenTags(tagsByFile))
121
+
122
+ return result
123
+ }
124
+
125
+ /**
126
+ * Close all active Parcel subscriptions before the process exits.
127
+ */
128
+ async function cleanup() {
129
+ await Promise.all(
130
+ [...subscriptionsByDir].map(async ([directoryPath, subPromise]) => {
131
+ subscriptionsByDir.delete(directoryPath)
132
+ await (await subPromise).unsubscribe()
133
+ }),
134
+ )
135
+
136
+ subscriptionsByDir.clear()
137
+ }
138
+
139
+ process.once('SIGINT', () => {
140
+ cleanup().finally(() => process.exit(130))
141
+ })
142
+
143
+ process.once('SIGTERM', () => {
144
+ cleanup().finally(() => process.exit(143))
145
+ })
146
+
147
+ export async function withCxx(
148
+ nextConfig: NextConfig = {},
149
+ pluginConfig: PluginConfig = {},
150
+ ) {
151
+ const TURBOPACK_GLOB = '*.{tsx,jsx,ts,js}'
152
+
153
+ const loaderItem = {
154
+ loader,
155
+ options: {
156
+ pluginConfig,
31
157
  },
32
158
  }
33
159
 
160
+ const currentRule = nextConfig.turbopack?.rules?.[TURBOPACK_GLOB]
161
+
34
162
  return {
35
163
  ...nextConfig,
36
- webpack(conf: Configuration, options: WebpackConfigContext) {
37
- const resolvedWebpackConfig = (
38
- typeof nextConfig?.webpack === 'function' ? nextConfig?.webpack(conf, options) : conf
39
- ) satisfies Configuration
40
-
41
- resolvedWebpackConfig?.module?.rules?.push({
42
- test: /\.(js|jsx|ts|tsx)$/,
43
- exclude: /node_modules/,
44
- use: {
45
- loader,
46
- options: config,
47
- },
48
- })
49
-
50
- return resolvedWebpackConfig
164
+ turbopack: {
165
+ ...nextConfig.turbopack,
166
+ rules: {
167
+ ...nextConfig.turbopack?.rules,
168
+ [TURBOPACK_GLOB]: Array.isArray(currentRule)
169
+ ? [...currentRule, loaderItem]
170
+ : currentRule
171
+ ? {
172
+ ...currentRule,
173
+ loaders: [...(currentRule.loaders ?? []), loaderItem],
174
+ }
175
+ : { loaders: [loaderItem] },
176
+ },
51
177
  },
52
- ...(IS_CANARY
53
- ? { experimental: { ...nextConfig?.experimental, turbo: turboConfig } }
54
- : { turbo: turboConfig }),
55
178
  }
56
179
  }
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { LogLevel } from './logger.js'
2
+
3
+ export type Result<ClassName extends string = string> = readonly [
4
+ css: string,
5
+ classes: Readonly<Record<ClassName, string>>,
6
+ href: string,
7
+ ]
8
+
9
+ export type Tags = Map<string, string[]>
10
+
11
+ export type PluginConfig = {
12
+ typeSuffix?: string | false
13
+ logger?: {
14
+ level?: LogLevel
15
+ }
16
+ }
17
+
18
+ export type BuildContext = {
19
+ tagsByFile: Map<string, Tags>
20
+ }
21
+
22
+ export type Tag = <ClassName extends string = string>(
23
+ name: string,
24
+ ) => (_: TemplateStringsArray) => Result<ClassName>
25
+
26
+ export type Cxx = ((_: TemplateStringsArray) => Result) & {
27
+ tag: Tag
28
+ }