@hanzogui/static 2.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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/check-dep-versions.cjs +267 -0
  4. package/dist/checkDeps.cjs +200 -0
  5. package/dist/constants.cjs +49 -0
  6. package/dist/exports.cjs +44 -0
  7. package/dist/extractor/accessSafe.cjs +46 -0
  8. package/dist/extractor/babelParse.cjs +54 -0
  9. package/dist/extractor/buildClassName.cjs +62 -0
  10. package/dist/extractor/bundle.cjs +185 -0
  11. package/dist/extractor/bundleConfig.cjs +513 -0
  12. package/dist/extractor/concatClassName.cjs +80 -0
  13. package/dist/extractor/createEvaluator.cjs +75 -0
  14. package/dist/extractor/createExtractor.cjs +1150 -0
  15. package/dist/extractor/createLogger.cjs +45 -0
  16. package/dist/extractor/detectModuleFormat.cjs +49 -0
  17. package/dist/extractor/ensureImportingConcat.cjs +48 -0
  18. package/dist/extractor/errors.cjs +26 -0
  19. package/dist/extractor/esbuildAliasPlugin.cjs +44 -0
  20. package/dist/extractor/esbuildTsconfigPaths.cjs +86 -0
  21. package/dist/extractor/evaluateAstNode.cjs +83 -0
  22. package/dist/extractor/extractHelpers.cjs +119 -0
  23. package/dist/extractor/extractMediaStyle.cjs +128 -0
  24. package/dist/extractor/extractToClassNames.cjs +321 -0
  25. package/dist/extractor/extractToNative.cjs +266 -0
  26. package/dist/extractor/findTopmostFunction.cjs +34 -0
  27. package/dist/extractor/generatedUid.cjs +47 -0
  28. package/dist/extractor/getGuiConfigPathFromOptionsConfig.cjs +35 -0
  29. package/dist/extractor/getPrefixLogs.cjs +29 -0
  30. package/dist/extractor/getPropValueFromAttributes.cjs +67 -0
  31. package/dist/extractor/getSourceModule.cjs +64 -0
  32. package/dist/extractor/getStaticBindingsForScope.cjs +150 -0
  33. package/dist/extractor/hoistClassNames.cjs +62 -0
  34. package/dist/extractor/literalToAst.cjs +84 -0
  35. package/dist/extractor/loadFile.cjs +11 -0
  36. package/dist/extractor/loadGui.cjs +322 -0
  37. package/dist/extractor/logLines.cjs +35 -0
  38. package/dist/extractor/normalizeTernaries.cjs +73 -0
  39. package/dist/extractor/propsToFontFamilyCache.cjs +38 -0
  40. package/dist/extractor/regenerateConfig.cjs +150 -0
  41. package/dist/extractor/removeUnusedHooks.cjs +76 -0
  42. package/dist/extractor/timer.cjs +43 -0
  43. package/dist/extractor/validHTMLAttributes.cjs +77 -0
  44. package/dist/extractor/watchGuiConfig.cjs +58 -0
  45. package/dist/getPragmaOptions.cjs +58 -0
  46. package/dist/helpers/memoize.cjs +37 -0
  47. package/dist/helpers/requireGuiCore.cjs +33 -0
  48. package/dist/index.cjs +40 -0
  49. package/dist/registerRequire.cjs +115 -0
  50. package/dist/server.cjs +75 -0
  51. package/dist/setup.cjs +0 -0
  52. package/dist/types.cjs +16 -0
  53. package/dist/worker.cjs +89 -0
  54. package/package.json +101 -0
  55. package/src/check-dep-versions.ts +738 -0
  56. package/src/checkDeps.ts +371 -0
  57. package/src/constants.ts +14 -0
  58. package/src/exports.ts +15 -0
  59. package/src/extractor/accessSafe.ts +22 -0
  60. package/src/extractor/babelParse.ts +37 -0
  61. package/src/extractor/buildClassName.ts +76 -0
  62. package/src/extractor/bundle.ts +277 -0
  63. package/src/extractor/bundleConfig.ts +958 -0
  64. package/src/extractor/concatClassName.ts +105 -0
  65. package/src/extractor/createEvaluator.ts +82 -0
  66. package/src/extractor/createExtractor.ts +2665 -0
  67. package/src/extractor/createLogger.ts +41 -0
  68. package/src/extractor/detectModuleFormat.ts +42 -0
  69. package/src/extractor/ensureImportingConcat.ts +36 -0
  70. package/src/extractor/errors.ts +1 -0
  71. package/src/extractor/esbuildAliasPlugin.ts +40 -0
  72. package/src/extractor/esbuildTsconfigPaths.ts +103 -0
  73. package/src/extractor/evaluateAstNode.ts +136 -0
  74. package/src/extractor/extractHelpers.ts +211 -0
  75. package/src/extractor/extractMediaStyle.ts +194 -0
  76. package/src/extractor/extractToClassNames.ts +631 -0
  77. package/src/extractor/extractToNative.ts +463 -0
  78. package/src/extractor/findTopmostFunction.ts +24 -0
  79. package/src/extractor/generatedUid.ts +41 -0
  80. package/src/extractor/getGuiConfigPathFromOptionsConfig.ts +24 -0
  81. package/src/extractor/getPrefixLogs.ts +9 -0
  82. package/src/extractor/getPropValueFromAttributes.ts +95 -0
  83. package/src/extractor/getSourceModule.ts +96 -0
  84. package/src/extractor/getStaticBindingsForScope.ts +237 -0
  85. package/src/extractor/hoistClassNames.ts +52 -0
  86. package/src/extractor/literalToAst.ts +85 -0
  87. package/src/extractor/loadFile.ts +17 -0
  88. package/src/extractor/loadGui.ts +487 -0
  89. package/src/extractor/logLines.ts +16 -0
  90. package/src/extractor/normalizeTernaries.ts +75 -0
  91. package/src/extractor/propsToFontFamilyCache.ts +18 -0
  92. package/src/extractor/regenerateConfig.ts +186 -0
  93. package/src/extractor/removeUnusedHooks.ts +83 -0
  94. package/src/extractor/timer.ts +25 -0
  95. package/src/extractor/validHTMLAttributes.ts +52 -0
  96. package/src/extractor/watchGuiConfig.ts +57 -0
  97. package/src/getPragmaOptions.ts +57 -0
  98. package/src/helpers/memoize.ts +24 -0
  99. package/src/helpers/requireGuiCore.ts +28 -0
  100. package/src/index.ts +4 -0
  101. package/src/registerRequire.ts +236 -0
  102. package/src/server.ts +43 -0
  103. package/src/setup.ts +0 -0
  104. package/src/types.ts +109 -0
  105. package/src/worker.ts +142 -0
  106. package/types/check-dep-versions.d.ts +37 -0
  107. package/types/check-dep-versions.d.ts.map +1 -0
  108. package/types/checkDeps.d.ts +19 -0
  109. package/types/checkDeps.d.ts.map +1 -0
  110. package/types/constants.d.ts +6 -0
  111. package/types/constants.d.ts.map +1 -0
  112. package/types/exports.d.ts +16 -0
  113. package/types/exports.d.ts.map +1 -0
  114. package/types/extractor/accessSafe.d.ts +3 -0
  115. package/types/extractor/accessSafe.d.ts.map +1 -0
  116. package/types/extractor/babelParse.d.ts +5 -0
  117. package/types/extractor/babelParse.d.ts.map +1 -0
  118. package/types/extractor/buildClassName.d.ts +7 -0
  119. package/types/extractor/buildClassName.d.ts.map +1 -0
  120. package/types/extractor/bundle.d.ts +121 -0
  121. package/types/extractor/bundle.d.ts.map +1 -0
  122. package/types/extractor/bundleConfig.d.ts +49 -0
  123. package/types/extractor/bundleConfig.d.ts.map +1 -0
  124. package/types/extractor/concatClassName.d.ts +8 -0
  125. package/types/extractor/concatClassName.d.ts.map +1 -0
  126. package/types/extractor/createEvaluator.d.ts +12 -0
  127. package/types/extractor/createEvaluator.d.ts.map +1 -0
  128. package/types/extractor/createExtractor.d.ts +32 -0
  129. package/types/extractor/createExtractor.d.ts.map +1 -0
  130. package/types/extractor/createLogger.d.ts +3 -0
  131. package/types/extractor/createLogger.d.ts.map +1 -0
  132. package/types/extractor/detectModuleFormat.d.ts +5 -0
  133. package/types/extractor/detectModuleFormat.d.ts.map +1 -0
  134. package/types/extractor/ensureImportingConcat.d.ts +4 -0
  135. package/types/extractor/ensureImportingConcat.d.ts.map +1 -0
  136. package/types/extractor/errors.d.ts +3 -0
  137. package/types/extractor/errors.d.ts.map +1 -0
  138. package/types/extractor/esbuildAliasPlugin.d.ts +18 -0
  139. package/types/extractor/esbuildAliasPlugin.d.ts.map +1 -0
  140. package/types/extractor/esbuildTsconfigPaths.d.ts +11 -0
  141. package/types/extractor/esbuildTsconfigPaths.d.ts.map +1 -0
  142. package/types/extractor/evaluateAstNode.d.ts +3 -0
  143. package/types/extractor/evaluateAstNode.d.ts.map +1 -0
  144. package/types/extractor/extractHelpers.d.ts +28 -0
  145. package/types/extractor/extractHelpers.d.ts.map +1 -0
  146. package/types/extractor/extractMediaStyle.d.ts +11 -0
  147. package/types/extractor/extractMediaStyle.d.ts.map +1 -0
  148. package/types/extractor/extractToClassNames.d.ts +25 -0
  149. package/types/extractor/extractToClassNames.d.ts.map +1 -0
  150. package/types/extractor/extractToNative.d.ts +13 -0
  151. package/types/extractor/extractToNative.d.ts.map +1 -0
  152. package/types/extractor/findTopmostFunction.d.ts +4 -0
  153. package/types/extractor/findTopmostFunction.d.ts.map +1 -0
  154. package/types/extractor/generatedUid.d.ts +5 -0
  155. package/types/extractor/generatedUid.d.ts.map +1 -0
  156. package/types/extractor/getGuiConfigPathFromOptionsConfig.d.ts +3 -0
  157. package/types/extractor/getGuiConfigPathFromOptionsConfig.d.ts.map +1 -0
  158. package/types/extractor/getPrefixLogs.d.ts +3 -0
  159. package/types/extractor/getPrefixLogs.d.ts.map +1 -0
  160. package/types/extractor/getPropValueFromAttributes.d.ts +19 -0
  161. package/types/extractor/getPropValueFromAttributes.d.ts.map +1 -0
  162. package/types/extractor/getSourceModule.d.ts +16 -0
  163. package/types/extractor/getSourceModule.d.ts.map +1 -0
  164. package/types/extractor/getStaticBindingsForScope.d.ts +5 -0
  165. package/types/extractor/getStaticBindingsForScope.d.ts.map +1 -0
  166. package/types/extractor/hoistClassNames.d.ts +6 -0
  167. package/types/extractor/hoistClassNames.d.ts.map +1 -0
  168. package/types/extractor/literalToAst.d.ts +4 -0
  169. package/types/extractor/literalToAst.d.ts.map +1 -0
  170. package/types/extractor/loadFile.d.ts +1 -0
  171. package/types/extractor/loadFile.d.ts.map +1 -0
  172. package/types/extractor/loadGui.d.ts +22 -0
  173. package/types/extractor/loadGui.d.ts.map +1 -0
  174. package/types/extractor/logLines.d.ts +2 -0
  175. package/types/extractor/logLines.d.ts.map +1 -0
  176. package/types/extractor/normalizeTernaries.d.ts +3 -0
  177. package/types/extractor/normalizeTernaries.d.ts.map +1 -0
  178. package/types/extractor/propsToFontFamilyCache.d.ts +4 -0
  179. package/types/extractor/propsToFontFamilyCache.d.ts.map +1 -0
  180. package/types/extractor/regenerateConfig.d.ts +9 -0
  181. package/types/extractor/regenerateConfig.d.ts.map +1 -0
  182. package/types/extractor/removeUnusedHooks.d.ts +3 -0
  183. package/types/extractor/removeUnusedHooks.d.ts.map +1 -0
  184. package/types/extractor/timer.d.ts +5 -0
  185. package/types/extractor/timer.d.ts.map +1 -0
  186. package/types/extractor/validHTMLAttributes.d.ts +52 -0
  187. package/types/extractor/validHTMLAttributes.d.ts.map +1 -0
  188. package/types/extractor/watchGuiConfig.d.ts +5 -0
  189. package/types/extractor/watchGuiConfig.d.ts.map +1 -0
  190. package/types/getPragmaOptions.d.ts +8 -0
  191. package/types/getPragmaOptions.d.ts.map +1 -0
  192. package/types/helpers/memoize.d.ts +8 -0
  193. package/types/helpers/memoize.d.ts.map +1 -0
  194. package/types/helpers/requireGuiCore.d.ts +3 -0
  195. package/types/helpers/requireGuiCore.d.ts.map +1 -0
  196. package/types/index.d.ts +4 -0
  197. package/types/index.d.ts.map +1 -0
  198. package/types/registerRequire.d.ts +10 -0
  199. package/types/registerRequire.d.ts.map +1 -0
  200. package/types/server.d.ts +3 -0
  201. package/types/server.d.ts.map +1 -0
  202. package/types/setup.d.ts +1 -0
  203. package/types/setup.d.ts.map +1 -0
  204. package/types/types.d.ts +92 -0
  205. package/types/types.d.ts.map +1 -0
  206. package/types/worker.d.ts +46 -0
  207. package/types/worker.d.ts.map +1 -0
@@ -0,0 +1,958 @@
1
+ import generate from '@babel/generator'
2
+ import traverse from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import { createHash } from 'node:crypto'
5
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs'
6
+ import { basename, dirname, extname, join, relative, sep } from 'node:path'
7
+ import { pathToFileURL } from 'node:url'
8
+ // @ts-ignore why
9
+ import { Color, colorLog } from '@hanzogui/cli-color'
10
+ import { type StaticConfig, type GuiInternalConfig } from '@hanzogui/web'
11
+ import esbuild from 'esbuild'
12
+ import * as FS from 'fs-extra'
13
+ import { readFile } from 'node:fs/promises'
14
+ import { registerRequire, setRequireResult } from '../registerRequire'
15
+ import type { GuiOptions } from '../types'
16
+ import { babelParse } from './babelParse'
17
+ import { esbuildLoaderConfig, esbundleGuiConfig } from './bundle'
18
+ import { getGuiConfigPathFromOptionsConfig } from './getGuiConfigPathFromOptionsConfig'
19
+ import { requireGuiCore } from '../helpers/requireGuiCore'
20
+ import { detectModuleFormat } from './detectModuleFormat'
21
+
22
+ // track temp files for cleanup on exit
23
+ const activeTempFiles = new Set<string>()
24
+
25
+ function getDynamicEvalOutfile(name: string, format: 'esm' | 'cjs', contents: string) {
26
+ const ext = format === 'esm' ? 'mjs' : 'cjs'
27
+ const hash = createHash('sha1')
28
+ .update(name)
29
+ .update('\0')
30
+ .update(format)
31
+ .update('\0')
32
+ .update(contents)
33
+ .digest('hex')
34
+ .slice(0, 10)
35
+ return join(process.cwd(), '.gui', `dynamic-eval-${hash}-${basename(name)}.${ext}`)
36
+ }
37
+
38
+ function getEsbuildStdinLoader(filePath: string): esbuild.Loader {
39
+ if (filePath.endsWith('.tsx')) return 'tsx'
40
+ if (filePath.endsWith('.ts')) return 'ts'
41
+ if (filePath.endsWith('.jsx')) return 'jsx'
42
+ return 'js'
43
+ }
44
+
45
+ function resolvePackageEntry(packageName: string, format: 'esm' | 'cjs') {
46
+ if (format === 'cjs') {
47
+ return require.resolve(packageName)
48
+ }
49
+
50
+ const packageJsonPath = require.resolve(`${packageName}/package.json`)
51
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
52
+ const packageRoot = dirname(packageJsonPath)
53
+ const exportEntry = packageJson.exports?.['.']
54
+
55
+ const esmEntry =
56
+ exportEntry?.import ||
57
+ exportEntry?.module ||
58
+ exportEntry?.browser ||
59
+ packageJson.module
60
+
61
+ if (typeof esmEntry === 'string') {
62
+ return join(packageRoot, esmEntry)
63
+ }
64
+
65
+ return require.resolve(packageName)
66
+ }
67
+
68
+ function cleanupTempFiles() {
69
+ for (const f of activeTempFiles) {
70
+ try {
71
+ unlinkSync(f)
72
+ } catch {}
73
+ }
74
+ activeTempFiles.clear()
75
+ }
76
+
77
+ process.on('exit', cleanupTempFiles)
78
+ process.on('SIGINT', () => {
79
+ cleanupTempFiles()
80
+ process.exit()
81
+ })
82
+ process.on('SIGTERM', () => {
83
+ cleanupTempFiles()
84
+ process.exit()
85
+ })
86
+
87
+ type NameToPaths = {
88
+ [key: string]: Set<string>
89
+ }
90
+
91
+ export type LoadedComponents = {
92
+ moduleName: string
93
+ nameToInfo: Record<
94
+ string,
95
+ {
96
+ staticConfig: StaticConfig
97
+ }
98
+ >
99
+ }
100
+
101
+ export type GuiProjectInfo = {
102
+ components?: LoadedComponents[]
103
+ guiConfig?: GuiInternalConfig | null
104
+ nameToPaths?: NameToPaths
105
+ cached?: boolean
106
+ }
107
+
108
+ const external = [
109
+ '@hanzogui/core',
110
+ '@hanzogui/web',
111
+ 'react',
112
+ 'react-dom',
113
+ 'react-native-svg',
114
+ ]
115
+
116
+ const esbuildExtraOptions = {
117
+ define: {
118
+ __DEV__: `${process.env.NODE_ENV === 'development'}`,
119
+ },
120
+ }
121
+
122
+ // plugin to handle ESM-only features when bundling to CJS
123
+ const handleEsmFeaturesPlugin: esbuild.Plugin = {
124
+ name: 'handle-esm-features',
125
+ setup(build) {
126
+ // only apply transforms for CJS output - ESM supports these natively
127
+ const isCjs = build.initialOptions.format === 'cjs' || !build.initialOptions.format
128
+
129
+ build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs)$/ }, (args) => {
130
+ // skip if ESM output - import.meta and top-level await work natively
131
+ if (!isCjs) {
132
+ return null
133
+ }
134
+
135
+ // skip most node_modules
136
+ if (args.path.includes('node_modules') && !args.path.includes('@gui')) {
137
+ return null
138
+ }
139
+
140
+ let contents = readFileSync(args.path, 'utf8')
141
+ let modified = false
142
+
143
+ // transform import.meta.env -> process.env (Vite-style env vars)
144
+ if (contents.includes('import.meta.env')) {
145
+ contents = contents.replace(/import\.meta\.env/g, 'process.env')
146
+ modified = true
147
+ }
148
+
149
+ // transform import.meta.url -> "" (not needed for static extraction)
150
+ if (contents.includes('import.meta.url')) {
151
+ contents = contents.replace(/import\.meta\.url/g, '""')
152
+ modified = true
153
+ }
154
+
155
+ // transform import.meta.main -> false
156
+ if (contents.includes('import.meta.main')) {
157
+ contents = contents.replace(/import\.meta\.main/g, 'false')
158
+ modified = true
159
+ }
160
+
161
+ // stub files with top-level await - they're typically runtime-only
162
+ if (
163
+ /^\s*(?:const|let|var|export)\s+[^=]*=\s*await\b/m.test(contents) ||
164
+ /^await\s/m.test(contents)
165
+ ) {
166
+ if (process.env.DEBUG?.startsWith('@hanzo/gui')) {
167
+ console.info(`[hanzo-gui] stubbing file with top-level await: ${args.path}`)
168
+ }
169
+ return {
170
+ contents: `// stubbed - contains top-level await\nmodule.exports = {}`,
171
+ loader: 'js',
172
+ }
173
+ }
174
+
175
+ if (modified) {
176
+ return {
177
+ contents,
178
+ loader: args.path.endsWith('.tsx')
179
+ ? 'tsx'
180
+ : args.path.endsWith('.ts')
181
+ ? 'ts'
182
+ : args.path.endsWith('.jsx')
183
+ ? 'jsx'
184
+ : 'js',
185
+ }
186
+ }
187
+
188
+ return null
189
+ })
190
+ },
191
+ }
192
+
193
+ // base options for transformSync (no plugins)
194
+ const esbuildTransformOptions = {
195
+ target: 'es2022',
196
+ format: 'cjs',
197
+ jsx: 'automatic',
198
+ platform: 'node',
199
+ ...esbuildExtraOptions,
200
+ } satisfies esbuild.TransformOptions
201
+
202
+ // options for buildSync - NO plugins (buildSync doesn't support plugins)
203
+ export const esbuildOptions = {
204
+ ...esbuildTransformOptions,
205
+ } satisfies esbuild.BuildOptions
206
+
207
+ // options for async build (with plugins)
208
+ export const esbuildOptionsWithPlugins = {
209
+ ...esbuildTransformOptions,
210
+ plugins: [handleEsmFeaturesPlugin],
211
+ } satisfies esbuild.BuildOptions
212
+
213
+ export type BundledConfig = Exclude<Awaited<ReturnType<typeof bundleConfig>>, undefined>
214
+
215
+ // will use cached one if watching
216
+ let currentBundle: BundledConfig | null = null
217
+ let isBundling = false
218
+ let lastBundle: BundledConfig | null = null
219
+ const waitForBundle = new Set<Function>()
220
+
221
+ export function hasBundledConfigChanged() {
222
+ if (lastBundle === currentBundle) {
223
+ return false
224
+ }
225
+ lastBundle = currentBundle
226
+ return true
227
+ }
228
+
229
+ let loadedConfig: GuiInternalConfig | null = null
230
+
231
+ export const getLoadedConfig = () => loadedConfig
232
+
233
+ export async function getBundledConfig(props: GuiOptions, rebuild = false) {
234
+ if (isBundling) {
235
+ await new Promise((res) => {
236
+ waitForBundle.add(res)
237
+ })
238
+ } else if (!currentBundle || rebuild) {
239
+ return await bundleConfig(props)
240
+ }
241
+ return currentBundle
242
+ }
243
+
244
+ global.guiLastLoaded ||= 0
245
+
246
+ function updateLastLoaded(config: any) {
247
+ global.guiLastLoaded = Date.now()
248
+ global.guiLastBundledConfig = config
249
+ }
250
+
251
+ let hasBundledOnce = false
252
+
253
+ // use global to dedupe logging - this works within a single process
254
+ // but may log multiple times if worker threads are recreated
255
+ // that's acceptable - better than nothing
256
+ let hasLoggedBuild = false
257
+
258
+ export async function bundleConfig(props: GuiOptions) {
259
+ // webpack is calling this a ton for no reason
260
+ if (global.guiLastBundledConfig && Date.now() - global.guiLastLoaded < 3000) {
261
+ // just loaded recently
262
+ return global.guiLastBundledConfig
263
+ }
264
+
265
+ try {
266
+ isBundling = true
267
+
268
+ const configEntry = props.config
269
+ ? getGuiConfigPathFromOptionsConfig(props.config)
270
+ : ''
271
+ const tmpDir = join(process.cwd(), '.gui')
272
+ // detect module format from config entry point
273
+ const configFormat = configEntry ? detectModuleFormat(configEntry) : 'cjs'
274
+ const configExt = configFormat === 'esm' ? '.mjs' : '.cjs'
275
+ const configOutPath = join(tmpDir, `gui.config${configExt}`)
276
+ const baseComponents = (props.components || []).filter((x) => x !== '@hanzogui/core')
277
+ // detect format per component module
278
+ const componentFormats: Array<'esm' | 'cjs'> = baseComponents.map((mod) => {
279
+ try {
280
+ const pkgJson = require.resolve(mod + '/package.json')
281
+ const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'))
282
+ return pkg.type === 'module' ? 'esm' : 'cjs'
283
+ } catch {
284
+ return 'cjs'
285
+ }
286
+ })
287
+ const componentOutPaths = baseComponents.map((componentModule, i) => {
288
+ const ext = componentFormats[i] === 'esm' ? '.mjs' : '.cjs'
289
+ return join(
290
+ tmpDir,
291
+ `${componentModule
292
+ .split(sep)
293
+ .join('-')
294
+ .replace(/[^a-z0-9]+/gi, '')}-components.config${ext}`
295
+ )
296
+ })
297
+
298
+ if (
299
+ process.env.NODE_ENV === 'development' &&
300
+ process.env.DEBUG?.startsWith('@hanzo/gui')
301
+ ) {
302
+ console.info(`Building config entry`, configEntry)
303
+ }
304
+
305
+ // check if ALL output files (config + components) already exist and are recent
306
+ // (built by another worker) - this prevents duplicate builds across worker threads
307
+ // we must check ALL files, not just the config, to avoid a race where another
308
+ // worker has written the config but not yet finished writing component files
309
+ let shouldBuild = !props.disableInitialBuild
310
+ if (shouldBuild && props.config) {
311
+ const allOutFiles = [configOutPath, ...componentOutPaths]
312
+ try {
313
+ const stats = await Promise.all(
314
+ allOutFiles.map((f) => FS.stat(f).catch(() => null))
315
+ )
316
+ const allExistAndRecent = stats.every(
317
+ (s) => s !== null && Date.now() - s.mtimeMs < 3000
318
+ )
319
+ if (allExistAndRecent) {
320
+ shouldBuild = false
321
+ }
322
+ } catch {
323
+ // something went wrong checking files, just build
324
+ }
325
+ }
326
+
327
+ if (shouldBuild) {
328
+ // build them to node-compat versions
329
+ try {
330
+ await FS.ensureDir(tmpDir)
331
+ } catch {
332
+ //
333
+ }
334
+
335
+ const start = Date.now()
336
+
337
+ await Promise.all([
338
+ props.config
339
+ ? esbundleGuiConfig(
340
+ {
341
+ entryPoints: [configEntry],
342
+ external,
343
+ outfile: configOutPath,
344
+ target: 'node24',
345
+ format: configFormat,
346
+ ...esbuildExtraOptions,
347
+ },
348
+ props.platform || 'web'
349
+ )
350
+ : null,
351
+ ...baseComponents.map((componentModule, i) => {
352
+ return esbundleGuiConfig(
353
+ {
354
+ entryPoints: [componentModule],
355
+ resolvePlatformSpecificEntries: true,
356
+ external,
357
+ outfile: componentOutPaths[i],
358
+ target: 'node24',
359
+ format: componentFormats[i],
360
+ ...esbuildExtraOptions,
361
+ },
362
+ props.platform || 'web'
363
+ )
364
+ }),
365
+ ])
366
+
367
+ // only log once per process to avoid duplicate messages
368
+ // also skip if _skipBuildLog is set (used during worker recycle warmup)
369
+ if (!hasLoggedBuild && !props['_skipBuildLog']) {
370
+ hasLoggedBuild = true
371
+ colorLog(
372
+ Color.FgYellow,
373
+ `
374
+ ➡ [hanzo-gui] built config, components, prompt (${Date.now() - start}ms)`
375
+ )
376
+
377
+ if (process.env.DEBUG?.startsWith('@hanzo/gui')) {
378
+ colorLog(
379
+ Color.Dim,
380
+ `
381
+ Config .${sep}${relative(process.cwd(), configOutPath)}
382
+ Components ${componentOutPaths.map((p) => `.${sep}${relative(process.cwd(), p)}`).join('\n ')}
383
+ `
384
+ )
385
+ }
386
+ }
387
+ }
388
+
389
+ // clear specific output file caches so we pick up the fresh (or newly discovered) build
390
+ // only clear the built output files - not all require.cache entries, since that breaks
391
+ // external requires like @hanzogui/config/v3 that are externalized in the bundled CJS
392
+ if (hasBundledOnce) {
393
+ try {
394
+ delete require.cache[require.resolve(configOutPath)]
395
+ } catch {
396
+ // file may not exist yet
397
+ }
398
+ for (const p of componentOutPaths) {
399
+ try {
400
+ delete require.cache[require.resolve(p)]
401
+ } catch {
402
+ // file may not exist yet
403
+ }
404
+ }
405
+ } else {
406
+ hasBundledOnce = true
407
+ }
408
+
409
+ let out: any
410
+ if (configFormat === 'esm') {
411
+ // use file:// URL for proper ESM resolution
412
+ out = await import(pathToFileURL(configOutPath).href)
413
+ } else {
414
+ out = require(configOutPath)
415
+ }
416
+
417
+ // try and find .config, even if on .default
418
+ let config = out.default || out || out.config
419
+ if (config && config.config && !config.tokens) {
420
+ config = config.config
421
+ }
422
+
423
+ if (!config) {
424
+ throw new Error(`No config: ${config}`)
425
+ }
426
+
427
+ // check for ProxyWorm - indicates a module loading error
428
+ if (config._isProxyWorm) {
429
+ throw new Error(
430
+ `Got a proxied config - likely a module loading error. Set DEBUG=hanzo-gui for details.`
431
+ )
432
+ }
433
+
434
+ loadedConfig = config
435
+
436
+ if (!config.parsed) {
437
+ const { createGui } = requireGuiCore(props.platform || 'web')
438
+ // need to create it
439
+ config = createGui(config)
440
+ }
441
+
442
+ if (props.outputCSS) {
443
+ await writeGuiCSS(props.outputCSS, config)
444
+ }
445
+
446
+ let components = await loadComponents({
447
+ ...props,
448
+ components: componentOutPaths,
449
+ })
450
+
451
+ if (!components) {
452
+ throw new Error(`No components found: ${componentOutPaths.join(', ')}`)
453
+ }
454
+
455
+ // map from built back to original module names
456
+ for (const component of components) {
457
+ component.moduleName =
458
+ baseComponents[componentOutPaths.indexOf(component.moduleName)] ||
459
+ component.moduleName
460
+
461
+ if (!component.moduleName) {
462
+ if (process.env.DEBUG?.includes('@hanzo/gui') || process.env.IS_HANZO_GUI_DEV) {
463
+ console.warn(
464
+ `⚠️ no module name found: ${component.moduleName} ${JSON.stringify(
465
+ baseComponents
466
+ )} in ${JSON.stringify(componentOutPaths)}`
467
+ )
468
+ }
469
+ }
470
+ }
471
+
472
+ if (
473
+ process.env.NODE_ENV === 'development' &&
474
+ process.env.DEBUG?.startsWith('@hanzo/gui')
475
+ ) {
476
+ console.info('Loaded components', components)
477
+ }
478
+
479
+ const res = {
480
+ components,
481
+ nameToPaths: {},
482
+ guiConfig: config,
483
+ }
484
+
485
+ currentBundle = res
486
+ updateLastLoaded(res)
487
+
488
+ return res
489
+ } catch (err: any) {
490
+ console.error(
491
+ `Error bundling gui config: ${err?.message} (run with DEBUG=hanzo-gui to see stack)`
492
+ )
493
+ if (process.env.DEBUG?.includes('@hanzo/gui')) {
494
+ console.error(err.stack)
495
+ }
496
+ } finally {
497
+ isBundling = false
498
+ waitForBundle.forEach((cb) => cb())
499
+ waitForBundle.clear()
500
+ }
501
+ }
502
+
503
+ export async function writeGuiCSS(outputCSS: string, config: GuiInternalConfig) {
504
+ const flush = async () => {
505
+ colorLog(Color.FgYellow, ` ➡ [hanzo-gui] output css: ${outputCSS}`)
506
+ await FS.writeFile(outputCSS, css)
507
+ }
508
+
509
+ const css = config.getCSS()
510
+ if (typeof css !== 'string') {
511
+ throw new Error(`Invalid CSS: ${typeof css} ${css}`)
512
+ }
513
+ try {
514
+ if (existsSync(outputCSS) && (await readFile(outputCSS, 'utf8')) === css) {
515
+ // no change
516
+ } else {
517
+ await flush()
518
+ }
519
+ } catch (err) {
520
+ console.info('Error writing themes', err)
521
+ }
522
+ }
523
+
524
+ export async function loadComponents(props: GuiOptions, forceExports = false) {
525
+ const coreComponents = getCoreComponentsSync(props)
526
+ const otherComponents = await loadComponentsInner(props, forceExports)
527
+ return [...coreComponents, ...(otherComponents || [])]
528
+ }
529
+
530
+ export function loadComponentsSync(props: GuiOptions, forceExports = false) {
531
+ const coreComponents = getCoreComponentsSync(props)
532
+ const otherComponents = loadComponentsInnerSync(props, forceExports)
533
+ return [...coreComponents, ...(otherComponents || [])]
534
+ }
535
+
536
+ function getCoreComponentsSync(props: GuiOptions) {
537
+ const loaded = loadComponentsInnerSync({
538
+ ...props,
539
+ components: ['@hanzogui/core'],
540
+ })
541
+
542
+ if (!loaded) {
543
+ throw new Error(`Core should always load`)
544
+ }
545
+
546
+ // always load core so we can optimize if directly importing
547
+ return [
548
+ {
549
+ ...loaded[0],
550
+ moduleName: '@hanzogui/core',
551
+ },
552
+ ]
553
+ }
554
+
555
+ export async function loadComponentsInner(
556
+ props: GuiOptions,
557
+ forceExports = false
558
+ ): Promise<null | LoadedComponents[]> {
559
+ const componentsModules = props.components || []
560
+
561
+ const key = componentsModules.join('\0')
562
+
563
+ if (!forceExports && cacheComponents[key]) {
564
+ return cacheComponents[key]
565
+ }
566
+
567
+ const { unregister } = registerRequire(props.platform || 'web', {
568
+ proxyWormImports: forceExports,
569
+ })
570
+
571
+ try {
572
+ const results: LoadedComponents[] = []
573
+
574
+ for (const name of componentsModules) {
575
+ const extension = extname(name)
576
+ const isLocal = Boolean(extension)
577
+ const isDynamic = isLocal && forceExports
578
+ const format = isLocal ? detectModuleFormat(name) : ('cjs' as const)
579
+
580
+ const fileContents = isDynamic ? readFileSync(name, 'utf-8') : ''
581
+ let loadModule = name
582
+ let writtenContents = fileContents
583
+ let didBabel = false
584
+
585
+ const attemptLoad = async ({ forceExports = false } = {}) => {
586
+ if (isDynamic) {
587
+ writtenContents = forceExports
588
+ ? transformAddExports(babelParse(esbuildit(fileContents, 'modern'), name))
589
+ : fileContents
590
+ loadModule = getDynamicEvalOutfile(name, format, writtenContents)
591
+
592
+ FS.ensureDirSync(dirname(loadModule))
593
+ activeTempFiles.add(loadModule)
594
+
595
+ await esbuild.build({
596
+ ...esbuildOptionsWithPlugins,
597
+ format,
598
+ outfile: loadModule,
599
+ stdin: {
600
+ contents: writtenContents,
601
+ resolveDir: dirname(name),
602
+ sourcefile: name,
603
+ loader: getEsbuildStdinLoader(name),
604
+ },
605
+ alias: {
606
+ 'react-native': resolvePackageEntry(
607
+ '@hanzogui/react-native-web-lite',
608
+ format
609
+ ),
610
+ '@hanzogui/react-native-web-lite': resolvePackageEntry(
611
+ '@hanzogui/react-native-web-lite',
612
+ format
613
+ ),
614
+ '@hanzogui/react-native-web-internals': resolvePackageEntry(
615
+ '@hanzogui/react-native-web-internals',
616
+ format
617
+ ),
618
+ },
619
+ bundle: true,
620
+ packages: 'external',
621
+ allowOverwrite: true,
622
+ sourcemap: false,
623
+ loader: esbuildLoaderConfig,
624
+ })
625
+ }
626
+
627
+ if (process.env.DEBUG === '@hanzo/gui') {
628
+ console.info(`loadModule`, loadModule, format)
629
+ }
630
+
631
+ let moduleResult: any
632
+ if (format === 'esm') {
633
+ // use file:// URL for proper ESM resolution
634
+ moduleResult = await import(pathToFileURL(loadModule).href)
635
+ } else {
636
+ moduleResult = require(loadModule)
637
+ }
638
+
639
+ if (!forceExports) {
640
+ setRequireResult(name, moduleResult)
641
+ }
642
+
643
+ const nameToInfo = getComponentStaticConfigByName(
644
+ name,
645
+ interopDefaultExport(moduleResult)
646
+ )
647
+
648
+ return {
649
+ moduleName: name,
650
+ nameToInfo,
651
+ }
652
+ }
653
+
654
+ const dispose = () => {
655
+ if (isDynamic) {
656
+ FS.removeSync(loadModule)
657
+ activeTempFiles.delete(loadModule)
658
+ }
659
+ }
660
+
661
+ let loaded: LoadedComponents | LoadedComponents[] | undefined
662
+
663
+ try {
664
+ loaded = await attemptLoad({ forceExports: true })
665
+ didBabel = true
666
+ } catch (err) {
667
+ console.info('babel err', err, writtenContents)
668
+ writtenContents = fileContents
669
+ if (process.env.DEBUG?.startsWith('@hanzo/gui')) {
670
+ console.info(`Error parsing babel likely`, err)
671
+ }
672
+
673
+ try {
674
+ loaded = await attemptLoad({ forceExports: false })
675
+ } catch (err2) {
676
+ if (process.env.HANZO_GUI_ENABLE_WARN_DYNAMIC_LOAD) {
677
+ console.info(
678
+ `\nGui attempted but failed to dynamically optimize components in:\n ${name}\n`
679
+ )
680
+ console.info(err2)
681
+ console.info(
682
+ `At: ${loadModule}`,
683
+ `\ndidBabel: ${didBabel}`,
684
+ `\nIn:`,
685
+ writtenContents,
686
+ `\nisDynamic: `,
687
+ isDynamic
688
+ )
689
+ }
690
+ loaded = []
691
+ }
692
+ } finally {
693
+ dispose()
694
+ }
695
+
696
+ if (Array.isArray(loaded)) {
697
+ results.push(...loaded)
698
+ } else if (loaded) {
699
+ results.push(loaded)
700
+ }
701
+ }
702
+
703
+ cacheComponents[key] = results
704
+ return results
705
+ } catch (err: any) {
706
+ console.info(`Gui error bundling components`, err.message, err.stack)
707
+ return null
708
+ } finally {
709
+ unregister()
710
+ }
711
+ }
712
+
713
+ // sync version - uses cjs format for buildSync (no plugin support)
714
+ export function loadComponentsInnerSync(
715
+ props: GuiOptions,
716
+ forceExports = false
717
+ ): null | LoadedComponents[] {
718
+ const componentsModules = props.components || []
719
+
720
+ const key = componentsModules.join('\0')
721
+
722
+ if (!forceExports && cacheComponents[key]) {
723
+ return cacheComponents[key]
724
+ }
725
+
726
+ const { unregister } = registerRequire(props.platform || 'web', {
727
+ proxyWormImports: forceExports,
728
+ })
729
+
730
+ try {
731
+ const info: LoadedComponents[] = componentsModules.flatMap((name) => {
732
+ const extension = extname(name)
733
+ const isLocal = Boolean(extension)
734
+ const isDynamic = isLocal && forceExports
735
+
736
+ const fileContents = isDynamic ? readFileSync(name, 'utf-8') : ''
737
+ let loadModule = name
738
+ let writtenContents = fileContents
739
+ let didBabel = false
740
+
741
+ function attemptLoad({ forceExports = false } = {}) {
742
+ if (isDynamic) {
743
+ writtenContents = forceExports
744
+ ? transformAddExports(babelParse(esbuildit(fileContents, 'modern'), name))
745
+ : fileContents
746
+ loadModule = getDynamicEvalOutfile(name, 'cjs', writtenContents)
747
+
748
+ FS.ensureDirSync(dirname(loadModule))
749
+ activeTempFiles.add(loadModule)
750
+
751
+ esbuild.buildSync({
752
+ ...esbuildOptions,
753
+ outfile: loadModule,
754
+ stdin: {
755
+ contents: writtenContents,
756
+ resolveDir: dirname(name),
757
+ sourcefile: name,
758
+ loader: getEsbuildStdinLoader(name),
759
+ },
760
+ alias: {
761
+ 'react-native': resolvePackageEntry(
762
+ '@hanzogui/react-native-web-lite',
763
+ 'esm'
764
+ ),
765
+ '@hanzogui/react-native-web-lite': resolvePackageEntry(
766
+ '@hanzogui/react-native-web-lite',
767
+ 'esm'
768
+ ),
769
+ '@hanzogui/react-native-web-internals': resolvePackageEntry(
770
+ '@hanzogui/react-native-web-internals',
771
+ 'esm'
772
+ ),
773
+ },
774
+ bundle: true,
775
+ packages: 'external',
776
+ allowOverwrite: true,
777
+ sourcemap: false,
778
+ loader: esbuildLoaderConfig,
779
+ })
780
+ }
781
+
782
+ if (process.env.DEBUG === '@hanzo/gui') {
783
+ console.info(`loadModule`, loadModule, require.resolve(loadModule))
784
+ }
785
+
786
+ const moduleResult = require(loadModule)
787
+
788
+ if (!forceExports) {
789
+ setRequireResult(name, moduleResult)
790
+ }
791
+
792
+ const nameToInfo = getComponentStaticConfigByName(
793
+ name,
794
+ interopDefaultExport(moduleResult)
795
+ )
796
+
797
+ return {
798
+ moduleName: name,
799
+ nameToInfo,
800
+ }
801
+ }
802
+
803
+ const dispose = () => {
804
+ if (isDynamic) {
805
+ FS.removeSync(loadModule)
806
+ activeTempFiles.delete(loadModule)
807
+ }
808
+ }
809
+
810
+ try {
811
+ const res = attemptLoad({ forceExports: true })
812
+ didBabel = true
813
+ return res
814
+ } catch (err) {
815
+ console.info('babel err', err, writtenContents)
816
+ writtenContents = fileContents
817
+ if (process.env.DEBUG?.startsWith('@hanzo/gui')) {
818
+ console.info(`Error parsing babel likely`, err)
819
+ }
820
+ } finally {
821
+ dispose()
822
+ }
823
+
824
+ try {
825
+ return attemptLoad({ forceExports: false })
826
+ } catch (err) {
827
+ if (process.env.HANZO_GUI_ENABLE_WARN_DYNAMIC_LOAD) {
828
+ console.info(
829
+ `\nGui attempted but failed to dynamically optimize components in:\n ${name}\n`
830
+ )
831
+ console.info(err)
832
+ console.info(
833
+ `At: ${loadModule}`,
834
+ `\ndidBabel: ${didBabel}`,
835
+ `\nIn:`,
836
+ writtenContents,
837
+ `\nisDynamic: `,
838
+ isDynamic
839
+ )
840
+ }
841
+ return []
842
+ } finally {
843
+ dispose()
844
+ }
845
+ })
846
+ cacheComponents[key] = info
847
+ return info
848
+ } catch (err: any) {
849
+ console.info(`Gui error bundling components`, err.message, err.stack)
850
+ return null
851
+ } finally {
852
+ unregister()
853
+ }
854
+ }
855
+
856
+ const esbuildit = (src: string, target?: 'modern') => {
857
+ return esbuild.transformSync(src, {
858
+ ...esbuildTransformOptions,
859
+ ...(target === 'modern' && {
860
+ target: 'es2022',
861
+ jsx: 'automatic',
862
+ loader: 'tsx',
863
+ platform: 'neutral',
864
+ format: 'esm',
865
+ }),
866
+ }).code
867
+ }
868
+
869
+ function getComponentStaticConfigByName(name: string, exported: any) {
870
+ const components: Record<string, { staticConfig: StaticConfig }> = {}
871
+ try {
872
+ if (!exported || typeof exported !== 'object' || Array.isArray(exported)) {
873
+ throw new Error(`Invalid export from package ${name}: ${typeof exported}`)
874
+ }
875
+
876
+ for (const key in exported) {
877
+ const found = getGuiComponent(key, exported[key])
878
+ if (found) {
879
+ // remove non-stringifyable
880
+ const { Component, ...sc } = found.staticConfig
881
+ components[key] = { staticConfig: sc }
882
+ }
883
+ }
884
+ } catch (err) {
885
+ if (process.env.HANZO_GUI_ENABLE_WARN_DYNAMIC_LOAD) {
886
+ console.error(
887
+ `Gui failed getting components from ${name} (Disable error by setting environment variable HANZO_GUI_ENABLE_WARN_DYNAMIC_LOAD=1)`
888
+ )
889
+ console.error(err)
890
+ }
891
+ }
892
+ return components
893
+ }
894
+
895
+ function getGuiComponent(
896
+ name: string,
897
+ Component: any
898
+ ): undefined | { staticConfig: StaticConfig } {
899
+ if (name[0].toUpperCase() !== name[0]) {
900
+ return
901
+ }
902
+ const staticConfig = Component?.staticConfig as StaticConfig | undefined
903
+ if (staticConfig) {
904
+ return Component
905
+ }
906
+ }
907
+
908
+ function interopDefaultExport(mod: any) {
909
+ return mod?.default ?? mod
910
+ }
911
+
912
+ const cacheComponents: Record<string, LoadedComponents[]> = {}
913
+
914
+ function transformAddExports(ast: t.File) {
915
+ const usedNames = new Set<string>()
916
+
917
+ // avoid clobbering
918
+ // @ts-ignore
919
+ traverse(ast, {
920
+ ExportNamedDeclaration(nodePath) {
921
+ if (nodePath.node.specifiers) {
922
+ for (const spec of nodePath.node.specifiers) {
923
+ usedNames.add(
924
+ t.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value
925
+ )
926
+ }
927
+ }
928
+ },
929
+ })
930
+
931
+ // @ts-ignore
932
+ traverse(ast, {
933
+ VariableDeclaration(nodePath) {
934
+ // top level only
935
+ if (!t.isProgram(nodePath.parent)) return
936
+ const decs = nodePath.node.declarations
937
+ if (decs.length > 1) return
938
+ const [dec] = decs
939
+ if (!t.isIdentifier(dec.id)) return
940
+ if (!dec.init) return
941
+ if (usedNames.has(dec.id.name)) return
942
+ usedNames.add(dec.id.name)
943
+ nodePath.replaceWith(
944
+ t.exportNamedDeclaration(t.variableDeclaration('let', [dec]), [
945
+ t.exportSpecifier(t.identifier(dec.id.name), t.identifier(dec.id.name)),
946
+ ])
947
+ )
948
+ },
949
+ })
950
+
951
+ // @ts-ignore
952
+ return generate(ast as any, {
953
+ concise: false,
954
+ filename: 'test.tsx',
955
+ retainLines: false,
956
+ sourceMaps: false,
957
+ }).code
958
+ }