@hanzogui/vite-plugin 4.3.1 → 7.0.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/src/plugin.ts DELETED
@@ -1,559 +0,0 @@
1
- import type { GuiOptions, ExtractedResponse } from '@hanzogui/static-worker'
2
- import * as Static from '@hanzogui/static-worker'
3
- import { getPragmaOptions } from '@hanzogui/static-worker'
4
- import { createHash } from 'node:crypto'
5
- import { existsSync } from 'node:fs'
6
- import path from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import type { Plugin, PluginOption, ResolvedConfig, ViteDevServer } from 'vite'
9
- import { normalizePath, transformWithEsbuild, type Environment } from 'vite'
10
- import {
11
- loadGuiBuildConfig,
12
- getLoadPromise,
13
- getGuiOptions,
14
- ensureFullConfigLoaded,
15
- } from './loadGui'
16
-
17
- const resolve = (name: string) => fileURLToPath(import.meta.resolve(name))
18
-
19
- // shared cache across all plugin instances/environments via globalThis
20
- type CacheEntry = {
21
- js: string
22
- map: any
23
- cssImport: string | null
24
- }
25
-
26
- const CACHE_KEY = '__gui_vite_cache__'
27
- const CACHE_SIZE_KEY = '__gui_vite_cache_size__'
28
- const PENDING_KEY = '__gui_vite_pending__'
29
-
30
- function getSharedCache(): Record<string, CacheEntry> {
31
- if (!(globalThis as any)[CACHE_KEY]) {
32
- ;(globalThis as any)[CACHE_KEY] = {}
33
- }
34
- return (globalThis as any)[CACHE_KEY]
35
- }
36
-
37
- function getSharedCacheSize(): number {
38
- return (globalThis as any)[CACHE_SIZE_KEY] || 0
39
- }
40
-
41
- function setSharedCacheSize(size: number) {
42
- ;(globalThis as any)[CACHE_SIZE_KEY] = size
43
- }
44
-
45
- function clearSharedCache() {
46
- ;(globalThis as any)[CACHE_KEY] = {}
47
- ;(globalThis as any)[CACHE_SIZE_KEY] = 0
48
- }
49
-
50
- // pending extractions map - dedupes concurrent requests for same file
51
- function getPendingExtractions(): Map<string, Promise<CacheEntry | null>> {
52
- if (!(globalThis as any)[PENDING_KEY]) {
53
- ;(globalThis as any)[PENDING_KEY] = new Map()
54
- }
55
- return (globalThis as any)[PENDING_KEY]
56
- }
57
-
58
- type AliasOptions = {
59
- /** use @hanzogui/react-native-web-lite, 'without-animated' for smaller bundle */
60
- rnwLite?: boolean | 'without-animated'
61
- /** alias react-native-svg to @hanzogui/react-native-svg */
62
- svg?: boolean
63
- }
64
-
65
- type AliasEntry = { find: string | RegExp; replacement: string }
66
-
67
- /**
68
- * returns vite-compatible aliases for gui
69
- * use this when you need control over alias ordering in your config
70
- */
71
- export function guiAliases(options: AliasOptions = {}): AliasEntry[] {
72
- const aliases: AliasEntry[] = []
73
-
74
- if (options.svg) {
75
- aliases.push({
76
- find: 'react-native-svg',
77
- replacement: resolve('@hanzogui/react-native-svg'),
78
- })
79
- }
80
-
81
- if (options.rnwLite) {
82
- // entry point for main import (may be without-animated variant)
83
- const rnwl = resolve(
84
- options.rnwLite === 'without-animated'
85
- ? '@hanzogui/react-native-web-lite/without-animated'
86
- : '@hanzogui/react-native-web-lite'
87
- )
88
- // base package path for subpath imports (package directory, not entry file)
89
- const rnwlBase = path.dirname(resolve('@hanzogui/react-native-web-lite/package.json'))
90
- aliases.push(
91
- {
92
- // map deep RNW paths like dist/exports/StyleSheet/preprocess to rnw-lite's flat structure
93
- // extracts the final path segment (e.g. "preprocess" or "createReactDOMStyle")
94
- find: /^react-native(?:-web)?\/dist\/(?:exports|modules)\/.*\/([^/]+)$/,
95
- replacement: `${rnwlBase}/dist/esm/$1.mjs`,
96
- },
97
- {
98
- find: /^react-native$/,
99
- replacement: rnwl,
100
- },
101
- {
102
- find: /^react-native\/(Libraries\/Utilities\/codegenNativeComponent|Libraries\/Utilities\/codegenNativeCommand)$/,
103
- replacement: `${rnwlBase}/$1`,
104
- },
105
- {
106
- find: 'react-native/package.json',
107
- replacement: resolve('@hanzogui/react-native-web-lite/package.json'),
108
- },
109
- {
110
- find: /^react-native-web$/,
111
- replacement: rnwl,
112
- }
113
- )
114
- }
115
-
116
- return aliases
117
- }
118
-
119
- export function guiPlugin({
120
- disableResolveConfig,
121
- ...guiOptionsIn
122
- }: GuiOptions & {
123
- disableResolveConfig?: boolean
124
- } = {}): PluginOption {
125
- // extraction ON by default, set disableExtraction: true to opt out
126
- let shouldExtract = !guiOptionsIn.disableExtraction
127
- let watcher: Promise<{ dispose: () => void } | void | undefined> | undefined
128
-
129
- // TODO temporary fix
130
- const enableNativeEnv = !!globalThis.__vxrnEnableNativeEnv
131
-
132
- const extensions = [
133
- `.web.mjs`,
134
- `.web.js`,
135
- `.web.jsx`,
136
- `.web.ts`,
137
- `.web.tsx`,
138
- '.mjs',
139
- '.js',
140
- '.mts',
141
- '.ts',
142
- '.jsx',
143
- '.tsx',
144
- '.json',
145
- ]
146
-
147
- // start loading immediately but don't block
148
- loadGuiBuildConfig(guiOptionsIn)
149
-
150
- // helper to await load when needed
151
- const ensureLoaded = async () => {
152
- const promise = getLoadPromise()
153
- if (promise) await promise
154
- const options = getGuiOptions()
155
- // update shouldExtract from loaded config (gui.build.ts)
156
- if (options) {
157
- shouldExtract = !options.disableExtraction
158
- }
159
- return options
160
- }
161
-
162
- // extract plugin state
163
- const getHash = (input: string) => createHash('sha1').update(input).digest('base64')
164
-
165
- // use shared cache across environments
166
- const memoryCache = getSharedCache()
167
-
168
- const cssMap = new Map<string, string>()
169
- let config: ResolvedConfig
170
- let server: ViteDevServer
171
- const virtualExt = `.gui.css`
172
-
173
- const getAbsoluteVirtualFileId = (filePath: string) => {
174
- if (filePath.startsWith(config.root)) {
175
- return filePath
176
- }
177
- return normalizePath(path.join(config.root, filePath))
178
- }
179
-
180
- function isNotClient(environment?: Environment) {
181
- return environment?.name && environment.name !== 'client'
182
- }
183
-
184
- function isNative(environment?: Environment) {
185
- return (
186
- environment?.name && (environment.name === 'ios' || environment.name === 'android')
187
- )
188
- }
189
-
190
- function invalidateModule(absoluteId: string) {
191
- if (!server) return
192
-
193
- const { moduleGraph } = server
194
- const modules = moduleGraph.getModulesByFile(absoluteId)
195
-
196
- if (modules) {
197
- for (const module of modules) {
198
- moduleGraph.invalidateModule(module)
199
- module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now()
200
- }
201
- }
202
- }
203
-
204
- const basePlugin: Plugin = {
205
- name: 'hanzo-gui',
206
- enforce: 'pre',
207
-
208
- configureServer(_server) {
209
- server = _server
210
- },
211
-
212
- async buildEnd() {
213
- await watcher?.then((res) => {
214
- res?.dispose()
215
- })
216
- },
217
-
218
- async transform(code, id) {
219
- if (id.includes('expo-linear-gradient')) {
220
- return transformWithEsbuild(code, id, {
221
- loader: 'jsx',
222
- jsx: 'automatic',
223
- })
224
- }
225
- },
226
-
227
- async config(_, env) {
228
- const options = await ensureLoaded()
229
-
230
- if (!options) {
231
- throw new Error(`No hanzo-gui options loaded`)
232
- }
233
-
234
- // start watching config if enabled
235
- if (!options.disableWatchGuiConfig) {
236
- watcher = Static.watchGuiConfig({
237
- components: ['@hanzo/gui'],
238
- config: './src/gui.config.ts',
239
- ...options,
240
- }).catch((err) => {
241
- console.error(` [GUI] Error watching config: ${err}`)
242
- })
243
- }
244
-
245
- return {
246
- envPrefix: ['GUI_'],
247
-
248
- environments: {
249
- client: {
250
- define: {
251
- 'process.env.GUI_IS_CLIENT': JSON.stringify(true),
252
- 'process.env.GUI_ENVIRONMENT': '"client"',
253
- },
254
- },
255
- },
256
-
257
- define: {
258
- // reanimated support
259
- _frameTimestamp: undefined,
260
- _WORKLET: false,
261
- __DEV__: `${env.mode === 'development'}`,
262
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || env.mode),
263
- 'process.env.ENABLE_RSC': JSON.stringify(process.env.ENABLE_RSC || ''),
264
- 'process.env.ENABLE_STEPS': JSON.stringify(process.env.ENABLE_STEPS || ''),
265
- 'process.env.IS_STATIC': JSON.stringify(false),
266
- ...(env.mode === 'production' && {
267
- 'process.env.GUI_OPTIMIZE_THEMES': JSON.stringify(true),
268
- }),
269
- },
270
- resolve:
271
- disableResolveConfig || enableNativeEnv
272
- ? {}
273
- : {
274
- extensions,
275
- alias: {
276
- ...(options.platform !== 'native' && {
277
- 'react-native/Libraries/Renderer/shims/ReactFabric':
278
- resolve('@hanzogui/proxy-worm'),
279
- 'react-native/Libraries/Utilities/codegenNativeComponent':
280
- resolve('@hanzogui/proxy-worm'),
281
- 'react-native-svg': resolve('@hanzogui/react-native-svg'),
282
- ...(!options?.useReactNativeWebLite && {
283
- 'react-native': resolve('react-native-web'),
284
- }),
285
- }),
286
- },
287
- },
288
- }
289
- },
290
- }
291
-
292
- const rnwLitePlugin: Plugin = {
293
- name: 'gui-rnw-lite',
294
-
295
- config() {
296
- if (enableNativeEnv) {
297
- return {}
298
- }
299
-
300
- const options = getGuiOptions()
301
- if (!options?.useReactNativeWebLite) {
302
- return {}
303
- }
304
-
305
- return {
306
- resolve: {
307
- alias: guiAliases({ rnwLite: options.useReactNativeWebLite }),
308
- },
309
- optimizeDeps: {
310
- // upstream react-native-web must not be pre-bundled when aliased to lite
311
- exclude: ['react-native-web'],
312
- },
313
- }
314
- },
315
- }
316
-
317
- // extract plugin for optimize mode
318
- // always included, but checks shouldExtract dynamically after config loads
319
- const extractPlugin: Plugin = {
320
- name: 'gui-extract',
321
- enforce: 'pre',
322
-
323
- async config(userConf) {
324
- // wait for config to load to know if we should extract
325
- const options = await ensureLoaded()
326
-
327
- userConf.optimizeDeps ||= {}
328
- userConf.optimizeDeps.include ||= []
329
-
330
- // inline-style-prefixer is CJS with __esModule and breaks without pre-bundling
331
- // (ReferenceError: exports is not defined). always include it.
332
- userConf.optimizeDeps.include.push('inline-style-prefixer')
333
-
334
- if (!shouldExtract) return
335
-
336
- userConf.optimizeDeps.include.push('@hanzogui/core/inject-styles')
337
- },
338
-
339
- async configResolved(resolvedConfig) {
340
- config = resolvedConfig
341
- },
342
-
343
- async resolveId(source) {
344
- if (!shouldExtract) return
345
-
346
- if (isNative(this.environment)) {
347
- return
348
- }
349
-
350
- if (isNotClient(this.environment)) {
351
- return
352
- }
353
-
354
- const [validId, query] = source.split('?')
355
-
356
- if (!validId.endsWith(virtualExt)) {
357
- return
358
- }
359
-
360
- const absoluteId = source.startsWith(config.root)
361
- ? source
362
- : getAbsoluteVirtualFileId(validId)
363
-
364
- if (cssMap.has(absoluteId)) {
365
- return absoluteId + (query ? `?${query}` : '')
366
- }
367
- },
368
-
369
- async load(id) {
370
- if (!shouldExtract) return
371
-
372
- const options = getGuiOptions()
373
- if (options?.disable) {
374
- return
375
- }
376
-
377
- if (isNative(this.environment)) {
378
- return
379
- }
380
-
381
- if (isNotClient(this.environment)) {
382
- return
383
- }
384
-
385
- const [validId] = id.split('?')
386
- return cssMap.get(validId)
387
- },
388
-
389
- transform: {
390
- order: 'pre',
391
- async handler(code, id) {
392
- // ensure hanzo-gui is loaded before transform
393
- const options = await ensureLoaded()
394
-
395
- // ensure full config (heavy bundling) is loaded before extraction
396
- await ensureFullConfigLoaded()
397
-
398
- // fully disabled = no extraction AND no debug attrs
399
- if (options?.disable) {
400
- return
401
- }
402
-
403
- if (isNative(this.environment)) {
404
- return
405
- }
406
-
407
- const [validId] = id.split('?')
408
- if (!validId.endsWith('.tsx')) {
409
- return
410
- }
411
-
412
- const { shouldDisable, shouldPrintDebug } = await getPragmaOptions({
413
- source: code,
414
- path: validId,
415
- })
416
-
417
- if (shouldPrintDebug) {
418
- console.trace(
419
- `Current file: ${id} in environment: ${this.environment?.name}, shouldDisable: ${shouldDisable}`
420
- )
421
- console.info(`\n\nOriginal source:\n${code}\n\n`)
422
- }
423
-
424
- if (shouldDisable) {
425
- return
426
- }
427
-
428
- const isSSR = isNotClient(this.environment)
429
-
430
- // cache key without environment - share compiled JS between SSR/client
431
- const cacheKey = getHash(`${code}${id}`)
432
- const pending = getPendingExtractions()
433
-
434
- // helper to format result based on environment
435
- const formatResult = (entry: CacheEntry) => {
436
- const finalCode =
437
- !isSSR && entry.cssImport ? `${entry.js}\n${entry.cssImport}` : entry.js
438
- return { code: finalCode, map: entry.map }
439
- }
440
-
441
- // check cache first
442
- const cached = memoryCache[cacheKey]
443
- if (cached) {
444
- if (process.env.DEBUG_GUI_CACHE) {
445
- console.info(
446
- `[gui-cache] HIT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
447
- )
448
- }
449
- return formatResult(cached)
450
- }
451
-
452
- // check if another request is already extracting this file
453
- const pendingExtraction = pending.get(cacheKey)
454
- if (pendingExtraction) {
455
- if (process.env.DEBUG_GUI_CACHE) {
456
- console.info(
457
- `[gui-cache] WAIT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
458
- )
459
- }
460
- const result = await pendingExtraction
461
- if (result) {
462
- return formatResult(result)
463
- }
464
- return
465
- }
466
-
467
- if (process.env.DEBUG_GUI_CACHE) {
468
- console.info(
469
- `[gui-cache] EXTRACT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
470
- )
471
- }
472
-
473
- // create extraction promise and store it for deduplication
474
- const extractionPromise = (async (): Promise<CacheEntry | null> => {
475
- let extracted: ExtractedResponse | null
476
- try {
477
- extracted = await Static!.extractToClassNames({
478
- source: code,
479
- sourcePath: validId,
480
- options: options!,
481
- shouldPrintDebug,
482
- })
483
- } catch (err) {
484
- if (process.env.DEBUG_GUI_CACHE) {
485
- console.info(`[gui-cache] ERROR extracting ${id.split('/').pop()}:`, err)
486
- }
487
- console.error(err instanceof Error ? err.message : String(err))
488
- return null
489
- }
490
-
491
- if (!extracted) {
492
- if (process.env.DEBUG_GUI_CACHE) {
493
- console.info(`[gui-cache] no extraction result for ${id.split('/').pop()}`)
494
- }
495
- return null
496
- }
497
-
498
- const rootRelativeId = `${validId}${virtualExt}`
499
- const absoluteId = getAbsoluteVirtualFileId(rootRelativeId)
500
-
501
- let cssImport: string | null = null
502
-
503
- // store CSS and prepare import (but don't include in cached JS)
504
- if (extracted.styles) {
505
- this.addWatchFile(rootRelativeId)
506
-
507
- if (server && cssMap.has(absoluteId)) {
508
- invalidateModule(rootRelativeId)
509
- }
510
-
511
- cssImport = `import "${rootRelativeId}";`
512
- cssMap.set(absoluteId, extracted.styles)
513
- }
514
-
515
- // cache the JS separately from CSS import
516
- const jsCode = extracted.js.toString()
517
- const cacheEntry: CacheEntry = {
518
- js: jsCode,
519
- map: extracted.map,
520
- cssImport,
521
- }
522
-
523
- // track cache size and clear if too large (64MB)
524
- const newSize = getSharedCacheSize() + jsCode.length
525
- if (newSize > 67108864) {
526
- clearSharedCache()
527
- } else {
528
- setSharedCacheSize(newSize)
529
- }
530
- memoryCache[cacheKey] = cacheEntry
531
-
532
- if (process.env.DEBUG_GUI_CACHE) {
533
- console.info(
534
- `[gui-cache] WRITE key=${cacheKey.slice(0, 8)} cacheSize=${Object.keys(memoryCache).length}`
535
- )
536
- }
537
-
538
- return cacheEntry
539
- })()
540
-
541
- // store pending promise for deduplication
542
- pending.set(cacheKey, extractionPromise)
543
-
544
- try {
545
- const result = await extractionPromise
546
- if (result) {
547
- return formatResult(result)
548
- }
549
- return
550
- } finally {
551
- // clean up pending map
552
- pending.delete(cacheKey)
553
- }
554
- },
555
- },
556
- }
557
-
558
- return [basePlugin, rnwLitePlugin, extractPlugin]
559
- }
@@ -1,2 +0,0 @@
1
- export declare const extensions: string[];
2
- //# sourceMappingURL=extensions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,UAWtB,CAAA"}
package/types/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './plugin';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA"}
@@ -1,15 +0,0 @@
1
- import type { GuiOptions } from '@hanzogui/types';
2
- export declare function getGuiOptions(): GuiOptions | null;
3
- export declare function getLoadPromise(): Promise<GuiOptions> | null;
4
- /**
5
- * Load just the gui.build.ts config (lightweight)
6
- * This doesn't bundle the full gui config - call ensureFullConfigLoaded() for that
7
- */
8
- export declare function loadGuiBuildConfig(optionsIn?: Partial<GuiOptions>): Promise<GuiOptions>;
9
- /**
10
- * Ensure the full gui config is loaded (heavy - bundles config + components)
11
- * Call this lazily when transform/extraction is actually needed
12
- */
13
- export declare function ensureFullConfigLoaded(): Promise<void>;
14
- export declare function cleanup(): Promise<void>;
15
- //# sourceMappingURL=loadGui.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"loadGui.d.ts","sourceRoot":"","sources":["../src/loadGui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAwBjD,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AAED,wBAAgB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAE3D;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC9B,OAAO,CAAC,UAAU,CAAC,CAgBrB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB5D;AAED,wBAAsB,OAAO,kBAO5B"}
package/types/plugin.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import type { GuiOptions } from '@hanzogui/static-worker';
2
- import type { PluginOption } from 'vite';
3
- type AliasOptions = {
4
- /** use @hanzogui/react-native-web-lite, 'without-animated' for smaller bundle */
5
- rnwLite?: boolean | 'without-animated';
6
- /** alias react-native-svg to @hanzogui/react-native-svg */
7
- svg?: boolean;
8
- };
9
- type AliasEntry = {
10
- find: string | RegExp;
11
- replacement: string;
12
- };
13
- /**
14
- * returns vite-compatible aliases for gui
15
- * use this when you need control over alias ordering in your config
16
- */
17
- export declare function guiAliases(options?: AliasOptions): AliasEntry[];
18
- export declare function guiPlugin({ disableResolveConfig, ...guiOptionsIn }?: GuiOptions & {
19
- disableResolveConfig?: boolean;
20
- }): PluginOption;
21
- export {};
22
- //# sourceMappingURL=plugin.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,yBAAyB,CAAA;AAO5E,OAAO,KAAK,EAAU,YAAY,EAAiC,MAAM,MAAM,CAAA;AAkD/E,KAAK,YAAY,GAAG;IAClB,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAA;IACtC,2DAA2D;IAC3D,GAAG,CAAC,EAAE,OAAO,CAAA;CACd,CAAA;AAED,KAAK,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,YAAiB,GAAG,UAAU,EAAE,CA8CnE;AAED,wBAAgB,SAAS,CAAC,EACxB,oBAAoB,EACpB,GAAG,YAAY,EAChB,GAAE,UAAU,GAAG;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC1B,GAAG,YAAY,CAmbpB"}