@hanzogui/vite-plugin 4.4.0 → 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,550 +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 config(_, env) {
219
- const options = await ensureLoaded()
220
-
221
- if (!options) {
222
- throw new Error(`No hanzo-gui options loaded`)
223
- }
224
-
225
- // start watching config if enabled
226
- if (!options.disableWatchGuiConfig) {
227
- watcher = Static.watchGuiConfig({
228
- components: ['@hanzo/gui'],
229
- config: './src/gui.config.ts',
230
- ...options,
231
- }).catch((err) => {
232
- console.error(` [GUI] Error watching config: ${err}`)
233
- })
234
- }
235
-
236
- return {
237
- envPrefix: ['GUI_'],
238
-
239
- environments: {
240
- client: {
241
- define: {
242
- 'process.env.GUI_IS_CLIENT': JSON.stringify(true),
243
- 'process.env.GUI_ENVIRONMENT': '"client"',
244
- },
245
- },
246
- },
247
-
248
- define: {
249
- // reanimated support
250
- _frameTimestamp: undefined,
251
- _WORKLET: false,
252
- __DEV__: `${env.mode === 'development'}`,
253
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || env.mode),
254
- 'process.env.ENABLE_RSC': JSON.stringify(process.env.ENABLE_RSC || ''),
255
- 'process.env.ENABLE_STEPS': JSON.stringify(process.env.ENABLE_STEPS || ''),
256
- 'process.env.IS_STATIC': JSON.stringify(false),
257
- ...(env.mode === 'production' && {
258
- 'process.env.GUI_OPTIMIZE_THEMES': JSON.stringify(true),
259
- }),
260
- },
261
- resolve:
262
- disableResolveConfig || enableNativeEnv
263
- ? {}
264
- : {
265
- extensions,
266
- alias: {
267
- ...(options.platform !== 'native' && {
268
- 'react-native/Libraries/Renderer/shims/ReactFabric':
269
- resolve('@hanzogui/proxy-worm'),
270
- 'react-native/Libraries/Utilities/codegenNativeComponent':
271
- resolve('@hanzogui/proxy-worm'),
272
- 'react-native-svg': resolve('@hanzogui/react-native-svg'),
273
- ...(!options?.useReactNativeWebLite && {
274
- 'react-native': resolve('react-native-web'),
275
- }),
276
- }),
277
- },
278
- },
279
- }
280
- },
281
- }
282
-
283
- const rnwLitePlugin: Plugin = {
284
- name: 'gui-rnw-lite',
285
-
286
- config() {
287
- if (enableNativeEnv) {
288
- return {}
289
- }
290
-
291
- const options = getGuiOptions()
292
- if (!options?.useReactNativeWebLite) {
293
- return {}
294
- }
295
-
296
- return {
297
- resolve: {
298
- alias: guiAliases({ rnwLite: options.useReactNativeWebLite }),
299
- },
300
- optimizeDeps: {
301
- // upstream react-native-web must not be pre-bundled when aliased to lite
302
- exclude: ['react-native-web'],
303
- },
304
- }
305
- },
306
- }
307
-
308
- // extract plugin for optimize mode
309
- // always included, but checks shouldExtract dynamically after config loads
310
- const extractPlugin: Plugin = {
311
- name: 'gui-extract',
312
- enforce: 'pre',
313
-
314
- async config(userConf) {
315
- // wait for config to load to know if we should extract
316
- const options = await ensureLoaded()
317
-
318
- userConf.optimizeDeps ||= {}
319
- userConf.optimizeDeps.include ||= []
320
-
321
- // inline-style-prefixer is CJS with __esModule and breaks without pre-bundling
322
- // (ReferenceError: exports is not defined). always include it.
323
- userConf.optimizeDeps.include.push('inline-style-prefixer')
324
-
325
- if (!shouldExtract) return
326
-
327
- userConf.optimizeDeps.include.push('@hanzogui/core/inject-styles')
328
- },
329
-
330
- async configResolved(resolvedConfig) {
331
- config = resolvedConfig
332
- },
333
-
334
- async resolveId(source) {
335
- if (!shouldExtract) return
336
-
337
- if (isNative(this.environment)) {
338
- return
339
- }
340
-
341
- if (isNotClient(this.environment)) {
342
- return
343
- }
344
-
345
- const [validId, query] = source.split('?')
346
-
347
- if (!validId.endsWith(virtualExt)) {
348
- return
349
- }
350
-
351
- const absoluteId = source.startsWith(config.root)
352
- ? source
353
- : getAbsoluteVirtualFileId(validId)
354
-
355
- if (cssMap.has(absoluteId)) {
356
- return absoluteId + (query ? `?${query}` : '')
357
- }
358
- },
359
-
360
- async load(id) {
361
- if (!shouldExtract) return
362
-
363
- const options = getGuiOptions()
364
- if (options?.disable) {
365
- return
366
- }
367
-
368
- if (isNative(this.environment)) {
369
- return
370
- }
371
-
372
- if (isNotClient(this.environment)) {
373
- return
374
- }
375
-
376
- const [validId] = id.split('?')
377
- return cssMap.get(validId)
378
- },
379
-
380
- transform: {
381
- order: 'pre',
382
- async handler(code, id) {
383
- // ensure hanzo-gui is loaded before transform
384
- const options = await ensureLoaded()
385
-
386
- // ensure full config (heavy bundling) is loaded before extraction
387
- await ensureFullConfigLoaded()
388
-
389
- // fully disabled = no extraction AND no debug attrs
390
- if (options?.disable) {
391
- return
392
- }
393
-
394
- if (isNative(this.environment)) {
395
- return
396
- }
397
-
398
- const [validId] = id.split('?')
399
- if (!validId.endsWith('.tsx')) {
400
- return
401
- }
402
-
403
- const { shouldDisable, shouldPrintDebug } = await getPragmaOptions({
404
- source: code,
405
- path: validId,
406
- })
407
-
408
- if (shouldPrintDebug) {
409
- console.trace(
410
- `Current file: ${id} in environment: ${this.environment?.name}, shouldDisable: ${shouldDisable}`
411
- )
412
- console.info(`\n\nOriginal source:\n${code}\n\n`)
413
- }
414
-
415
- if (shouldDisable) {
416
- return
417
- }
418
-
419
- const isSSR = isNotClient(this.environment)
420
-
421
- // cache key without environment - share compiled JS between SSR/client
422
- const cacheKey = getHash(`${code}${id}`)
423
- const pending = getPendingExtractions()
424
-
425
- // helper to format result based on environment
426
- const formatResult = (entry: CacheEntry) => {
427
- const finalCode =
428
- !isSSR && entry.cssImport ? `${entry.js}\n${entry.cssImport}` : entry.js
429
- return { code: finalCode, map: entry.map }
430
- }
431
-
432
- // check cache first
433
- const cached = memoryCache[cacheKey]
434
- if (cached) {
435
- if (process.env.DEBUG_GUI_CACHE) {
436
- console.info(
437
- `[gui-cache] HIT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
438
- )
439
- }
440
- return formatResult(cached)
441
- }
442
-
443
- // check if another request is already extracting this file
444
- const pendingExtraction = pending.get(cacheKey)
445
- if (pendingExtraction) {
446
- if (process.env.DEBUG_GUI_CACHE) {
447
- console.info(
448
- `[gui-cache] WAIT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
449
- )
450
- }
451
- const result = await pendingExtraction
452
- if (result) {
453
- return formatResult(result)
454
- }
455
- return
456
- }
457
-
458
- if (process.env.DEBUG_GUI_CACHE) {
459
- console.info(
460
- `[gui-cache] EXTRACT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
461
- )
462
- }
463
-
464
- // create extraction promise and store it for deduplication
465
- const extractionPromise = (async (): Promise<CacheEntry | null> => {
466
- let extracted: ExtractedResponse | null
467
- try {
468
- extracted = await Static!.extractToClassNames({
469
- source: code,
470
- sourcePath: validId,
471
- options: options!,
472
- shouldPrintDebug,
473
- })
474
- } catch (err) {
475
- if (process.env.DEBUG_GUI_CACHE) {
476
- console.info(`[gui-cache] ERROR extracting ${id.split('/').pop()}:`, err)
477
- }
478
- console.error(err instanceof Error ? err.message : String(err))
479
- return null
480
- }
481
-
482
- if (!extracted) {
483
- if (process.env.DEBUG_GUI_CACHE) {
484
- console.info(`[gui-cache] no extraction result for ${id.split('/').pop()}`)
485
- }
486
- return null
487
- }
488
-
489
- const rootRelativeId = `${validId}${virtualExt}`
490
- const absoluteId = getAbsoluteVirtualFileId(rootRelativeId)
491
-
492
- let cssImport: string | null = null
493
-
494
- // store CSS and prepare import (but don't include in cached JS)
495
- if (extracted.styles) {
496
- this.addWatchFile(rootRelativeId)
497
-
498
- if (server && cssMap.has(absoluteId)) {
499
- invalidateModule(rootRelativeId)
500
- }
501
-
502
- cssImport = `import "${rootRelativeId}";`
503
- cssMap.set(absoluteId, extracted.styles)
504
- }
505
-
506
- // cache the JS separately from CSS import
507
- const jsCode = extracted.js.toString()
508
- const cacheEntry: CacheEntry = {
509
- js: jsCode,
510
- map: extracted.map,
511
- cssImport,
512
- }
513
-
514
- // track cache size and clear if too large (64MB)
515
- const newSize = getSharedCacheSize() + jsCode.length
516
- if (newSize > 67108864) {
517
- clearSharedCache()
518
- } else {
519
- setSharedCacheSize(newSize)
520
- }
521
- memoryCache[cacheKey] = cacheEntry
522
-
523
- if (process.env.DEBUG_GUI_CACHE) {
524
- console.info(
525
- `[gui-cache] WRITE key=${cacheKey.slice(0, 8)} cacheSize=${Object.keys(memoryCache).length}`
526
- )
527
- }
528
-
529
- return cacheEntry
530
- })()
531
-
532
- // store pending promise for deduplication
533
- pending.set(cacheKey, extractionPromise)
534
-
535
- try {
536
- const result = await extractionPromise
537
- if (result) {
538
- return formatResult(result)
539
- }
540
- return
541
- } finally {
542
- // clean up pending map
543
- pending.delete(cacheKey)
544
- }
545
- },
546
- },
547
- }
548
-
549
- return [basePlugin, rnwLitePlugin, extractPlugin]
550
- }
@@ -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,CA0apB"}