@tamagui/vite-plugin 2.7.7 → 3.0.0-beta.1097.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.
- package/dist/cjs/compilerStats.cjs +126 -0
- package/dist/cjs/extensions.cjs +25 -18
- package/dist/cjs/index.cjs +9 -11
- package/dist/cjs/internal.cjs +24 -0
- package/dist/cjs/loadTamagui.cjs +204 -85
- package/dist/cjs/plugin.cjs +1146 -439
- package/dist/cjs/zeroRuntime.cjs +153 -0
- package/dist/esm/compilerStats.mjs +98 -0
- package/dist/esm/compilerStats.mjs.map +1 -0
- package/dist/esm/extensions.mjs +13 -1
- package/dist/esm/extensions.mjs.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/index.mjs +1 -2
- package/dist/esm/internal.mjs +3 -0
- package/dist/esm/loadTamagui.mjs +189 -59
- package/dist/esm/loadTamagui.mjs.map +1 -1
- package/dist/esm/plugin.mjs +1129 -417
- package/dist/esm/plugin.mjs.map +1 -1
- package/dist/esm/zeroRuntime.mjs +120 -0
- package/dist/esm/zeroRuntime.mjs.map +1 -0
- package/package.json +25 -10
- package/src/compilerStats.ts +168 -0
- package/src/internal.ts +9 -0
- package/src/loadTamagui.ts +268 -74
- package/src/plugin.ts +1530 -254
- package/src/zeroRuntime.ts +205 -0
- package/types/compilerStats.d.ts +44 -0
- package/types/compilerStats.d.ts.map +1 -0
- package/types/internal.d.ts +10 -0
- package/types/internal.d.ts.map +1 -0
- package/types/loadTamagui.d.ts +39 -13
- package/types/loadTamagui.d.ts.map +1 -1
- package/types/plugin.d.ts +33 -5
- package/types/plugin.d.ts.map +1 -1
- package/types/zeroRuntime.d.ts +60 -0
- package/types/zeroRuntime.d.ts.map +1 -0
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
package/src/plugin.ts
CHANGED
|
@@ -1,56 +1,429 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { getPragmaOptions } from '@tamagui/static-worker'
|
|
1
|
+
import Static from '@tamagui/static'
|
|
2
|
+
import type { TamaguiOptions, ZeroGraphReceipt } from '@tamagui/static'
|
|
4
3
|
import { createHash } from 'node:crypto'
|
|
5
|
-
import { readdirSync } from 'node:fs'
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { gzipSync } from 'node:zlib'
|
|
6
|
+
import { readFile } from 'node:fs/promises'
|
|
6
7
|
import { createRequire } from 'node:module'
|
|
7
8
|
import path from 'node:path'
|
|
8
9
|
import { fileURLToPath } from 'node:url'
|
|
9
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
createFilter,
|
|
12
|
+
createIdResolver,
|
|
13
|
+
createRunnableDevEnvironment,
|
|
14
|
+
defaultClientConditions,
|
|
15
|
+
defaultClientMainFields,
|
|
16
|
+
isRunnableDevEnvironment,
|
|
17
|
+
resolveConfig,
|
|
18
|
+
} from 'vite'
|
|
19
|
+
import type {
|
|
20
|
+
DevEnvironment,
|
|
21
|
+
EnvironmentOptions,
|
|
22
|
+
EnvironmentModuleNode,
|
|
23
|
+
Plugin,
|
|
24
|
+
PluginOption,
|
|
25
|
+
ResolvedConfig,
|
|
26
|
+
ViteDevServer,
|
|
27
|
+
} from 'vite'
|
|
10
28
|
import type { Environment } from 'vite'
|
|
29
|
+
import type { ViteTamaguiLoader } from './loadTamagui'
|
|
30
|
+
import { createViteTamaguiLoader, TAMAGUI_EVALUATION_ENVIRONMENT } from './loadTamagui'
|
|
31
|
+
import {
|
|
32
|
+
createCompilerStatsReport,
|
|
33
|
+
formatCompilerStatsReport,
|
|
34
|
+
type CompilerModuleReport,
|
|
35
|
+
} from './compilerStats'
|
|
11
36
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
37
|
+
assertZeroGraph,
|
|
38
|
+
buildIsland,
|
|
39
|
+
createZeroRuntimeController,
|
|
40
|
+
finalizeZeroCSS,
|
|
41
|
+
zeroModuleKey,
|
|
42
|
+
ZERO_CSS_FILENAME,
|
|
43
|
+
ZERO_ISLAND_DIRNAME,
|
|
44
|
+
type ZeroIslandBuildContext,
|
|
45
|
+
type ZeroRuntimeController,
|
|
46
|
+
} from './zeroRuntime'
|
|
47
|
+
|
|
48
|
+
const environmentSpecificTransformPluginNames = new Set([
|
|
49
|
+
'one:compiler',
|
|
50
|
+
'one:compiler-css-to-js',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
const oneTsconfigPathsPluginName = 'one:tsconfig-paths'
|
|
54
|
+
const bareTamaguiPackage = /^@tamagui\/[^/?#]+(?:[/?#]|$)/
|
|
55
|
+
const inlineEvaluationTamaguiPackage = /^@tamagui\/(?:config|core|slider|web)(?:[/?#]|$)/
|
|
56
|
+
const externalizablePackageExtensions = new Set(['', '.js', '.mjs', '.cjs'])
|
|
57
|
+
|
|
58
|
+
// Export condition the compiler's evaluation environment resolves with. A
|
|
59
|
+
// Tamagui package that cannot load outside an app publishes a runtime-free
|
|
60
|
+
// build under this key; nothing else in a user's graph ever sees it.
|
|
61
|
+
const TAMAGUI_COMPILER_CONDITION = 'tamagui-compiler'
|
|
62
|
+
|
|
63
|
+
// A condition only decides anything while Vite is the one resolving. Externalized
|
|
64
|
+
// packages are handed to node as a bare specifier, and node resolves them again
|
|
65
|
+
// under its own conditions, so a compiler build wins the first resolution and is
|
|
66
|
+
// thrown away at load time. Packages publishing one therefore have to be inlined
|
|
67
|
+
// into the evaluation graph. They are the cheap ones to inline by construction:
|
|
68
|
+
// the build exists precisely because it carries no app runtime.
|
|
69
|
+
function packageDeclaresCompilerCondition(packageDir: string) {
|
|
70
|
+
const manifest = path.join(packageDir, 'package.json')
|
|
71
|
+
if (!existsSync(manifest)) return false
|
|
72
|
+
try {
|
|
73
|
+
const exports = JSON.parse(readFileSync(manifest, 'utf8')).exports
|
|
74
|
+
return JSON.stringify(exports ?? null).includes(`"${TAMAGUI_COMPILER_CONDITION}"`)
|
|
75
|
+
} catch {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
type EvaluationNoExternal = NonNullable<EnvironmentOptions['resolve']>['noExternal']
|
|
80
|
+
|
|
81
|
+
function mergeEvaluationNoExternal(
|
|
82
|
+
required: (string | RegExp)[],
|
|
83
|
+
userNoExternal: EvaluationNoExternal
|
|
84
|
+
): EvaluationNoExternal {
|
|
85
|
+
if (userNoExternal === true) return true
|
|
86
|
+
if (!userNoExternal) return required
|
|
87
|
+
return [
|
|
88
|
+
...required,
|
|
89
|
+
...(Array.isArray(userNoExternal) ? userNoExternal : [userNoExternal]),
|
|
90
|
+
]
|
|
91
|
+
}
|
|
17
92
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
93
|
+
type EvaluationResolveIdHandler = (this: any, source: string, ...args: any[]) => any
|
|
94
|
+
type EvaluationBarePackageResolver = (
|
|
95
|
+
environment: Environment,
|
|
96
|
+
source: string,
|
|
97
|
+
importer?: string
|
|
98
|
+
) =>
|
|
99
|
+
| Promise<string | { id: string; external: true } | undefined>
|
|
100
|
+
| string
|
|
101
|
+
| { id: string; external: true }
|
|
102
|
+
| undefined
|
|
103
|
+
|
|
104
|
+
function createEvaluationResolveId(
|
|
105
|
+
plugin: Plugin,
|
|
106
|
+
resolveBarePackage?: EvaluationBarePackageResolver
|
|
107
|
+
): Plugin['resolveId'] {
|
|
108
|
+
const resolveId = plugin.resolveId
|
|
109
|
+
if (plugin.name !== oneTsconfigPathsPluginName || !resolveId) {
|
|
110
|
+
return resolveId
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handler = (
|
|
114
|
+
typeof resolveId === 'object' ? resolveId.handler : resolveId
|
|
115
|
+
) as EvaluationResolveIdHandler
|
|
116
|
+
const evaluationHandler = function (this: any, source: string, ...args: any[]) {
|
|
117
|
+
// One's TS-path resolver can map workspace package imports to Metro's CJS
|
|
118
|
+
// directory fallbacks before Vite can apply the package exports map. Keep
|
|
119
|
+
// user TS aliases in this resolver, but let Tamagui packages use Vite's
|
|
120
|
+
// normal package resolution and externalization policy.
|
|
121
|
+
if (bareTamaguiPackage.test(source)) {
|
|
122
|
+
const importer = typeof args[0] === 'string' ? args[0] : undefined
|
|
123
|
+
return resolveBarePackage?.(this.environment, source, importer)
|
|
124
|
+
}
|
|
125
|
+
return Reflect.apply(handler, this, [source, ...args])
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return typeof resolveId === 'object'
|
|
129
|
+
? { ...resolveId, handler: evaluationHandler }
|
|
130
|
+
: evaluationHandler
|
|
131
|
+
}
|
|
24
132
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
133
|
+
function createEvaluationPluginFacade(
|
|
134
|
+
plugin: Plugin,
|
|
135
|
+
resolveBarePackage?: EvaluationBarePackageResolver
|
|
136
|
+
): Plugin {
|
|
137
|
+
return {
|
|
138
|
+
name: plugin.name,
|
|
139
|
+
enforce: plugin.enforce,
|
|
140
|
+
resolveId: createEvaluationResolveId(plugin, resolveBarePackage),
|
|
141
|
+
load: plugin.load,
|
|
142
|
+
transform: environmentSpecificTransformPluginNames.has(plugin.name)
|
|
143
|
+
? undefined
|
|
144
|
+
: plugin.transform,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tamaguiEvaluationPluginNames = new Set([
|
|
149
|
+
'tamagui',
|
|
150
|
+
'tamagui-extract',
|
|
151
|
+
'tamagui-rnw-lite',
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
function isEvaluationUserPlugin(plugin: Plugin) {
|
|
155
|
+
return (
|
|
156
|
+
!!(plugin.resolveId || plugin.load || plugin.transform) &&
|
|
157
|
+
plugin.name !== 'alias' &&
|
|
158
|
+
!plugin.name.startsWith('native:') &&
|
|
159
|
+
!plugin.name.startsWith('vite:') &&
|
|
160
|
+
!plugin.name.startsWith('builtin:vite-') &&
|
|
161
|
+
!tamaguiEvaluationPluginNames.has(plugin.name)
|
|
162
|
+
)
|
|
30
163
|
}
|
|
31
164
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
165
|
+
function isEvaluationCorePlugin(plugin: Plugin) {
|
|
166
|
+
return (
|
|
167
|
+
plugin.name === 'alias' ||
|
|
168
|
+
plugin.name.startsWith('vite:') ||
|
|
169
|
+
plugin.name.startsWith('builtin:vite-')
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isConfiguredEvaluationPackage(source: string, packages: Set<string>) {
|
|
174
|
+
const cleanSource = source.split(/[?#]/, 1)[0]
|
|
175
|
+
return [...packages].some(
|
|
176
|
+
(packageName) =>
|
|
177
|
+
cleanSource === packageName || cleanSource.startsWith(`${packageName}/`)
|
|
178
|
+
)
|
|
179
|
+
}
|
|
35
180
|
|
|
36
|
-
function
|
|
37
|
-
if (!
|
|
38
|
-
|
|
181
|
+
function getEvaluationPackageName(source: string | undefined) {
|
|
182
|
+
if (!source) return
|
|
183
|
+
const cleanSource = source.split(/[?#]/, 1)[0]
|
|
184
|
+
if (
|
|
185
|
+
!cleanSource ||
|
|
186
|
+
cleanSource.startsWith('.') ||
|
|
187
|
+
cleanSource.startsWith('#') ||
|
|
188
|
+
cleanSource.startsWith('\0') ||
|
|
189
|
+
path.isAbsolute(cleanSource)
|
|
190
|
+
) {
|
|
191
|
+
return
|
|
39
192
|
}
|
|
40
|
-
|
|
193
|
+
if (cleanSource.startsWith('@')) {
|
|
194
|
+
const [scope, name] = cleanSource.split('/')
|
|
195
|
+
return scope && name ? `${scope}/${name}` : undefined
|
|
196
|
+
}
|
|
197
|
+
const [name] = cleanSource.split('/')
|
|
198
|
+
return name && !path.extname(name) ? name : undefined
|
|
41
199
|
}
|
|
42
200
|
|
|
43
|
-
function
|
|
44
|
-
|
|
201
|
+
function scanInstalledTamaguiPackages(
|
|
202
|
+
root: string,
|
|
203
|
+
configuredEvaluationPackages: Set<string>
|
|
204
|
+
) {
|
|
205
|
+
const packageRequire = createRequire(path.join(root, 'package.json'))
|
|
206
|
+
// externalizable: evaluated through node, the default for a Tamagui package.
|
|
207
|
+
// compilerCondition: publishes a runtime-free build the compiler must inline
|
|
208
|
+
// to keep, see packageDeclaresCompilerCondition.
|
|
209
|
+
const externalizable = new Set<string>()
|
|
210
|
+
const compilerCondition = new Set<string>()
|
|
211
|
+
|
|
212
|
+
for (const modulePath of packageRequire.resolve.paths('@tamagui/core') || []) {
|
|
213
|
+
const scopePath = path.join(modulePath, '@tamagui')
|
|
214
|
+
if (!existsSync(scopePath)) continue
|
|
215
|
+
for (const entry of readdirSync(scopePath, { withFileTypes: true })) {
|
|
216
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
217
|
+
const packageName = `@tamagui/${entry.name}`
|
|
218
|
+
if (
|
|
219
|
+
inlineEvaluationTamaguiPackage.test(packageName) ||
|
|
220
|
+
configuredEvaluationPackages.has(packageName)
|
|
221
|
+
) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (packageDeclaresCompilerCondition(path.join(scopePath, entry.name))) {
|
|
225
|
+
compilerCondition.add(packageName)
|
|
226
|
+
} else {
|
|
227
|
+
externalizable.add(packageName)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { externalizable, compilerCondition }
|
|
45
233
|
}
|
|
46
234
|
|
|
47
|
-
function
|
|
48
|
-
|
|
235
|
+
function getEvaluationResolve(
|
|
236
|
+
resolve: ResolvedConfig['environments'][string]['resolve'],
|
|
237
|
+
root: string,
|
|
238
|
+
disableTsconfigPaths: boolean,
|
|
239
|
+
configuredEvaluationPackages: Set<string>
|
|
240
|
+
) {
|
|
241
|
+
const noExternal = resolve.noExternal
|
|
242
|
+
const noExternalFilter =
|
|
243
|
+
noExternal && noExternal !== true
|
|
244
|
+
? createFilter(undefined, noExternal, { resolve: false })
|
|
245
|
+
: undefined
|
|
246
|
+
const isNoExternalPackage =
|
|
247
|
+
noExternal === true
|
|
248
|
+
? () => true
|
|
249
|
+
: noExternalFilter
|
|
250
|
+
? (packageName: string) => !noExternalFilter(packageName)
|
|
251
|
+
: () => false
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...resolve,
|
|
255
|
+
external:
|
|
256
|
+
resolve.external === true
|
|
257
|
+
? (true as const)
|
|
258
|
+
: [
|
|
259
|
+
...new Set([
|
|
260
|
+
...(resolve.external || []).filter(
|
|
261
|
+
(packageName) =>
|
|
262
|
+
!isConfiguredEvaluationPackage(
|
|
263
|
+
packageName,
|
|
264
|
+
configuredEvaluationPackages
|
|
265
|
+
)
|
|
266
|
+
),
|
|
267
|
+
...[
|
|
268
|
+
...scanInstalledTamaguiPackages(root, configuredEvaluationPackages)
|
|
269
|
+
.externalizable,
|
|
270
|
+
].filter((packageName) => !isNoExternalPackage(packageName)),
|
|
271
|
+
]),
|
|
272
|
+
],
|
|
273
|
+
...(disableTsconfigPaths && { tsconfigPaths: false }),
|
|
274
|
+
}
|
|
49
275
|
}
|
|
50
276
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
277
|
+
function isConfiguredExternalPackage(
|
|
278
|
+
source: string,
|
|
279
|
+
external: string[] | true | undefined
|
|
280
|
+
) {
|
|
281
|
+
if (external === true) return true
|
|
282
|
+
const cleanSource = source.split(/[?#]/, 1)[0]
|
|
283
|
+
return external?.some(
|
|
284
|
+
(packageName) =>
|
|
285
|
+
cleanSource === packageName || cleanSource.startsWith(`${packageName}/`)
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function createServeEvaluationConfig(
|
|
290
|
+
config: ResolvedConfig,
|
|
291
|
+
configuredEvaluationPackages: Set<string>
|
|
292
|
+
): ResolvedConfig {
|
|
293
|
+
const environment = config.environments[TAMAGUI_EVALUATION_ENVIRONMENT]
|
|
294
|
+
let packageResolver: ReturnType<typeof createIdResolver> | undefined
|
|
295
|
+
const resolveBarePackage: EvaluationBarePackageResolver = async (
|
|
296
|
+
evaluationEnvironment,
|
|
297
|
+
source,
|
|
298
|
+
importer
|
|
299
|
+
) => {
|
|
300
|
+
const resolved = await packageResolver?.(evaluationEnvironment, source, importer)
|
|
301
|
+
if (!resolved) return
|
|
302
|
+
const cleanResolved = resolved.split(/[?#]/, 1)[0]
|
|
303
|
+
if (
|
|
304
|
+
!inlineEvaluationTamaguiPackage.test(source) &&
|
|
305
|
+
!isConfiguredEvaluationPackage(source, configuredEvaluationPackages) &&
|
|
306
|
+
isConfiguredExternalPackage(source, evaluationEnvironment.config.resolve.external)
|
|
307
|
+
) {
|
|
308
|
+
return { id: source, external: true }
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
inlineEvaluationTamaguiPackage.test(source) ||
|
|
312
|
+
isConfiguredEvaluationPackage(source, configuredEvaluationPackages) ||
|
|
313
|
+
!normalizePath(cleanResolved).includes('/node_modules/') ||
|
|
314
|
+
!externalizablePackageExtensions.has(path.extname(cleanResolved))
|
|
315
|
+
) {
|
|
316
|
+
return resolved
|
|
317
|
+
}
|
|
318
|
+
return { id: source, external: true }
|
|
319
|
+
}
|
|
320
|
+
const plugins = environment.plugins.flatMap((plugin) => {
|
|
321
|
+
if (isEvaluationCorePlugin(plugin)) {
|
|
322
|
+
return [plugin]
|
|
323
|
+
}
|
|
324
|
+
if (isEvaluationUserPlugin(plugin)) {
|
|
325
|
+
return [createEvaluationPluginFacade(plugin, resolveBarePackage)]
|
|
326
|
+
}
|
|
327
|
+
return []
|
|
328
|
+
})
|
|
329
|
+
const resolve = getEvaluationResolve(
|
|
330
|
+
environment.resolve,
|
|
331
|
+
config.root,
|
|
332
|
+
plugins.some((plugin) => plugin.name === oneTsconfigPathsPluginName),
|
|
333
|
+
configuredEvaluationPackages
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
const evaluationConfig: ResolvedConfig = {
|
|
337
|
+
...config,
|
|
338
|
+
environments: {
|
|
339
|
+
...config.environments,
|
|
340
|
+
[TAMAGUI_EVALUATION_ENVIRONMENT]: {
|
|
341
|
+
...environment,
|
|
342
|
+
plugins,
|
|
343
|
+
resolve,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
packageResolver = createIdResolver(evaluationConfig)
|
|
348
|
+
return evaluationConfig
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function createOwnedEvaluationConfig(
|
|
352
|
+
config: ResolvedConfig,
|
|
353
|
+
configuredEvaluationPackages: Set<string>
|
|
354
|
+
) {
|
|
355
|
+
const environment = config.environments[TAMAGUI_EVALUATION_ENVIRONMENT]
|
|
356
|
+
const plugins = environment.plugins
|
|
357
|
+
.filter(isEvaluationUserPlugin)
|
|
358
|
+
.map((plugin) => createEvaluationPluginFacade(plugin))
|
|
359
|
+
const resolve = getEvaluationResolve(
|
|
360
|
+
environment.resolve,
|
|
361
|
+
config.root,
|
|
362
|
+
plugins.some((plugin) => plugin.name === oneTsconfigPathsPluginName),
|
|
363
|
+
configuredEvaluationPackages
|
|
364
|
+
)
|
|
365
|
+
const { createEnvironment: _createEnvironment, ...dev } = environment.dev
|
|
366
|
+
|
|
367
|
+
// ModuleRunner needs Vite's serve-time core pipeline (especially import
|
|
368
|
+
// analysis), but user plugin selection must remain the already-resolved
|
|
369
|
+
// pipeline for the outer command. The facades retain only evaluation hooks,
|
|
370
|
+
// so resolving this owned config cannot replay user configuration or outer
|
|
371
|
+
// lifecycles.
|
|
372
|
+
return resolveConfig(
|
|
373
|
+
{
|
|
374
|
+
configFile: false,
|
|
375
|
+
root: config.root,
|
|
376
|
+
mode: config.mode,
|
|
377
|
+
logLevel: config.logLevel,
|
|
378
|
+
plugins,
|
|
379
|
+
define: environment.define,
|
|
380
|
+
resolve,
|
|
381
|
+
environments: {
|
|
382
|
+
[TAMAGUI_EVALUATION_ENVIRONMENT]: {
|
|
383
|
+
consumer: environment.consumer,
|
|
384
|
+
keepProcessEnv: environment.keepProcessEnv,
|
|
385
|
+
define: environment.define,
|
|
386
|
+
resolve,
|
|
387
|
+
optimizeDeps: environment.optimizeDeps,
|
|
388
|
+
dev: {
|
|
389
|
+
...dev,
|
|
390
|
+
moduleRunnerTransform: true,
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
'serve',
|
|
396
|
+
config.mode
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// handle ESM/CJS duality for plugin dependencies - resolve from plugin's location, not user's project
|
|
401
|
+
const _pluginRequire = createRequire(
|
|
402
|
+
typeof __filename === 'string' ? __filename : fileURLToPath(import.meta.url)
|
|
403
|
+
)
|
|
404
|
+
const resolve = (name: string) => _pluginRequire.resolve(name)
|
|
405
|
+
const normalizePath = (value: string) => value.replace(/\\/g, '/')
|
|
406
|
+
|
|
407
|
+
const PLUGIN_INSTANCE_KEY = '__tamagui_vite_plugin_instance__'
|
|
408
|
+
|
|
409
|
+
function reportCompilerStats(root: string, reports: Map<string, CompilerModuleReport>) {
|
|
410
|
+
const report = createCompilerStatsReport(root, reports)
|
|
411
|
+
console.info(
|
|
412
|
+
formatCompilerStatsReport(report, process.env.TAMAGUI_COMPILER_STATS === 'verbose')
|
|
413
|
+
)
|
|
414
|
+
if (process.env.TAMAGUI_COMPILER_STATS_FILE) {
|
|
415
|
+
const outputPath = path.resolve(root, process.env.TAMAGUI_COMPILER_STATS_FILE)
|
|
416
|
+
writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`)
|
|
417
|
+
console.info(
|
|
418
|
+
`[tamagui] compiler stats JSON: ${path.relative(process.cwd(), outputPath)}`
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getNextPluginInstanceId() {
|
|
424
|
+
const next = ((globalThis as any)[PLUGIN_INSTANCE_KEY] || 0) + 1
|
|
425
|
+
;(globalThis as any)[PLUGIN_INSTANCE_KEY] = next
|
|
426
|
+
return next
|
|
54
427
|
}
|
|
55
428
|
|
|
56
429
|
// resolves package ids against the user's project root (not the plugin's
|
|
@@ -81,23 +454,26 @@ function addIfInstalled(
|
|
|
81
454
|
}
|
|
82
455
|
}
|
|
83
456
|
|
|
84
|
-
// pending extractions map - dedupes concurrent requests for same file
|
|
85
|
-
function getPendingExtractions(): Map<string, Promise<CacheEntry | null>> {
|
|
86
|
-
if (!(globalThis as any)[PENDING_KEY]) {
|
|
87
|
-
;(globalThis as any)[PENDING_KEY] = new Map()
|
|
88
|
-
}
|
|
89
|
-
return (globalThis as any)[PENDING_KEY]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
457
|
type AliasOptions = {
|
|
93
458
|
/** use @tamagui/react-native-web-lite, 'without-animated' for smaller bundle */
|
|
94
459
|
rnwLite?: boolean | 'without-animated'
|
|
95
|
-
/** alias react-native-svg to @tamagui/react-native-svg */
|
|
460
|
+
/** alias react-native-svg to @tamagui/react-native-svg's web entry */
|
|
96
461
|
svg?: boolean
|
|
97
462
|
}
|
|
98
463
|
|
|
99
464
|
type AliasEntry = { find: string | RegExp; replacement: string }
|
|
100
465
|
|
|
466
|
+
function svgWebEntry() {
|
|
467
|
+
// A linked workspace dependency is served directly during development.
|
|
468
|
+
// require.resolve selects CJS, whose named exports browsers cannot import.
|
|
469
|
+
return normalizePath(
|
|
470
|
+
path.join(
|
|
471
|
+
path.dirname(resolve('@tamagui/react-native-svg/package.json')),
|
|
472
|
+
'dist/esm/index.mjs'
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
101
477
|
/**
|
|
102
478
|
* returns vite-compatible aliases for tamagui
|
|
103
479
|
* use this when you need control over alias ordering in your config
|
|
@@ -106,10 +482,13 @@ export function tamaguiAliases(options: AliasOptions = {}): AliasEntry[] {
|
|
|
106
482
|
const aliases: AliasEntry[] = []
|
|
107
483
|
|
|
108
484
|
if (options.svg) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
485
|
+
// both spellings, because a bare-specifier alias leaves the cjs entry
|
|
486
|
+
// reachable and vite serves that file to the browser untransformed.
|
|
487
|
+
const svg = svgWebEntry()
|
|
488
|
+
aliases.push(
|
|
489
|
+
{ find: 'react-native-svg', replacement: svg },
|
|
490
|
+
{ find: '@tamagui/react-native-svg', replacement: svg }
|
|
491
|
+
)
|
|
113
492
|
}
|
|
114
493
|
|
|
115
494
|
if (options.rnwLite) {
|
|
@@ -124,8 +503,8 @@ export function tamaguiAliases(options: AliasOptions = {}): AliasEntry[] {
|
|
|
124
503
|
: 'dist/esm/index.mjs'
|
|
125
504
|
)
|
|
126
505
|
)
|
|
127
|
-
//
|
|
128
|
-
//
|
|
506
|
+
// only alias deep imports that rnw-lite actually implements. unimplemented
|
|
507
|
+
// react-native-web exports must fall through to the full package.
|
|
129
508
|
const rnwlFlatModules = readdirSync(path.join(rnwlBase, 'dist/esm'))
|
|
130
509
|
.filter((file) => file.endsWith('.mjs'))
|
|
131
510
|
.map((file) => file.slice(0, -'.mjs'.length))
|
|
@@ -134,15 +513,6 @@ export function tamaguiAliases(options: AliasOptions = {}): AliasEntry[] {
|
|
|
134
513
|
{
|
|
135
514
|
// map deep RNW paths like dist/exports/StyleSheet/preprocess to rnw-lite's flat structure
|
|
136
515
|
// extracts the final path segment (e.g. "preprocess" or "createReactDOMStyle")
|
|
137
|
-
//
|
|
138
|
-
// only match segments rnw-lite actually ships. it implements part of
|
|
139
|
-
// react-native-web's export surface, not all of it, and there is no
|
|
140
|
-
// flat StyleSheet.mjs. expo sdk 56 added
|
|
141
|
-
// expo/src/launch/AppRegistry.web.tsx, which does
|
|
142
|
-
// `require('react-native-web/dist/exports/StyleSheet')`; the unscoped
|
|
143
|
-
// pattern rewrote that onto a file that does not exist and failed the
|
|
144
|
-
// whole optimize. anything lite lacks now falls through to the real
|
|
145
|
-
// package.
|
|
146
516
|
find: new RegExp(
|
|
147
517
|
`^react-native(?:-web)?\\/dist\\/(?:exports|modules)\\/(?:.*\\/)?(${rnwlFlatModules.join('|')})$`
|
|
148
518
|
),
|
|
@@ -170,18 +540,283 @@ export function tamaguiAliases(options: AliasOptions = {}): AliasEntry[] {
|
|
|
170
540
|
return aliases
|
|
171
541
|
}
|
|
172
542
|
|
|
173
|
-
|
|
543
|
+
type VxrnNativePluginContext = {
|
|
544
|
+
root: string
|
|
545
|
+
platform: 'ios' | 'android'
|
|
546
|
+
dev: boolean
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function createTamaguiNativePlugin(
|
|
550
|
+
tamaguiOptionsIn: TamaguiOptions,
|
|
551
|
+
nativeContext?: VxrnNativePluginContext
|
|
552
|
+
): Plugin {
|
|
553
|
+
let compilerFrontend = new Static.CompilerFrontend()
|
|
554
|
+
const projectDependencies = new Set<string>()
|
|
555
|
+
let root = nativeContext?.root || process.cwd()
|
|
556
|
+
let projectPromise: Promise<Static.CompilerProject | null> | null = null
|
|
557
|
+
let nativeOptions: TamaguiOptions | null = null
|
|
558
|
+
let rebuildProject = false
|
|
559
|
+
let generation = 0
|
|
560
|
+
|
|
561
|
+
const loadProject = async (resolveModule: (specifier: string) => Promise<string>) => {
|
|
562
|
+
if (projectPromise) return projectPromise
|
|
563
|
+
const shouldRebuild = rebuildProject
|
|
564
|
+
rebuildProject = false
|
|
565
|
+
const pending = (async () => {
|
|
566
|
+
projectDependencies.clear()
|
|
567
|
+
const loadedOptions = await Static.loadTamaguiBuildConfigAsync({
|
|
568
|
+
...tamaguiOptionsIn,
|
|
569
|
+
root,
|
|
570
|
+
platform: 'native',
|
|
571
|
+
outputCSS: undefined,
|
|
572
|
+
})
|
|
573
|
+
const options = { ...loadedOptions, root, outputCSS: undefined }
|
|
574
|
+
nativeOptions = options
|
|
575
|
+
for (const dependency of Static.getTamaguiBuildConfigDependencies(loadedOptions)) {
|
|
576
|
+
projectDependencies.add(normalizePath(dependency))
|
|
577
|
+
}
|
|
578
|
+
if (options.disable || options.disableExtraction) return null
|
|
579
|
+
const project = await Static.loadCompilerProject({
|
|
580
|
+
root,
|
|
581
|
+
target: 'native',
|
|
582
|
+
options,
|
|
583
|
+
rebuild: shouldRebuild,
|
|
584
|
+
generation: `vite-native:${generation + 1}`,
|
|
585
|
+
missingProjectMessage:
|
|
586
|
+
'Unable to load the Tamagui project for Vite native compilation',
|
|
587
|
+
async resolveComponents(moduleNames) {
|
|
588
|
+
return Promise.all(
|
|
589
|
+
moduleNames.map(async (moduleName) => {
|
|
590
|
+
const id = await resolveModule(moduleName)
|
|
591
|
+
projectDependencies.add(normalizePath(id.split(/[?#]/, 1)[0]))
|
|
592
|
+
return { moduleName, id }
|
|
593
|
+
})
|
|
594
|
+
)
|
|
595
|
+
},
|
|
596
|
+
})
|
|
597
|
+
for (const dependency of project.projectInfo.dependencies ?? []) {
|
|
598
|
+
projectDependencies.add(normalizePath(dependency.split(/[?#]/, 1)[0]))
|
|
599
|
+
}
|
|
600
|
+
const configPath = options.config || 'tamagui.config.ts'
|
|
601
|
+
projectDependencies.add(
|
|
602
|
+
normalizePath(
|
|
603
|
+
path.isAbsolute(configPath) ? configPath : path.resolve(root, configPath)
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
const buildFile = options.buildFile || 'tamagui.build.ts'
|
|
607
|
+
projectDependencies.add(
|
|
608
|
+
normalizePath(
|
|
609
|
+
path.isAbsolute(buildFile) ? buildFile : path.resolve(root, buildFile)
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
if (options.themeBuilder?.input) {
|
|
613
|
+
projectDependencies.add(
|
|
614
|
+
normalizePath(
|
|
615
|
+
path.isAbsolute(options.themeBuilder.input)
|
|
616
|
+
? options.themeBuilder.input
|
|
617
|
+
: path.resolve(root, options.themeBuilder.input)
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
generation++
|
|
622
|
+
return project
|
|
623
|
+
})()
|
|
624
|
+
const guarded = pending.catch((error) => {
|
|
625
|
+
if (projectPromise === guarded) projectPromise = null
|
|
626
|
+
rebuildProject = true
|
|
627
|
+
throw error
|
|
628
|
+
})
|
|
629
|
+
projectPromise = guarded
|
|
630
|
+
return projectPromise
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
name: 'tamagui-native-compiler',
|
|
635
|
+
enforce: 'post',
|
|
636
|
+
configResolved(config) {
|
|
637
|
+
root = config.root
|
|
638
|
+
},
|
|
639
|
+
watchChange(id) {
|
|
640
|
+
if (projectDependencies.has(normalizePath(id.split(/[?#]/, 1)[0]))) {
|
|
641
|
+
rebuildProject = true
|
|
642
|
+
projectPromise = null
|
|
643
|
+
compilerFrontend = new Static.CompilerFrontend()
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
transform: {
|
|
647
|
+
order: 'pre',
|
|
648
|
+
async handler(code, id) {
|
|
649
|
+
const environmentName = nativeContext?.platform || this.environment?.name
|
|
650
|
+
if (environmentName !== 'ios' && environmentName !== 'android') return
|
|
651
|
+
const [validId] = id.split('?')
|
|
652
|
+
if (
|
|
653
|
+
!validId ||
|
|
654
|
+
!/\.[jt]sx$/.test(validId) ||
|
|
655
|
+
normalizePath(validId).split('/').includes('node_modules')
|
|
656
|
+
) {
|
|
657
|
+
return
|
|
658
|
+
}
|
|
659
|
+
const { shouldDisable } = await Static.getPragmaOptions({
|
|
660
|
+
source: code,
|
|
661
|
+
path: validId,
|
|
662
|
+
})
|
|
663
|
+
if (shouldDisable) return
|
|
664
|
+
|
|
665
|
+
const resolve = async (specifier: string, importer: string) => {
|
|
666
|
+
const resolution = await this.resolve(specifier, importer, { skipSelf: true })
|
|
667
|
+
return resolution
|
|
668
|
+
? { id: resolution.id, external: resolution.external === true }
|
|
669
|
+
: null
|
|
670
|
+
}
|
|
671
|
+
const project = await loadProject(async (specifier) => {
|
|
672
|
+
const resolution = await resolve(
|
|
673
|
+
specifier,
|
|
674
|
+
path.join(root, '__tamagui_native.tsx')
|
|
675
|
+
)
|
|
676
|
+
if (!resolution) {
|
|
677
|
+
throw new Error(`Unable to resolve native compiler component ${specifier}`)
|
|
678
|
+
}
|
|
679
|
+
return resolution.id
|
|
680
|
+
})
|
|
681
|
+
if (!project) return
|
|
682
|
+
for (const dependency of projectDependencies) this.addWatchFile(dependency)
|
|
683
|
+
|
|
684
|
+
const result = await compilerFrontend.compile({
|
|
685
|
+
id: validId,
|
|
686
|
+
source: code,
|
|
687
|
+
root,
|
|
688
|
+
target: 'native',
|
|
689
|
+
project,
|
|
690
|
+
resolve,
|
|
691
|
+
evaluate: async ({ id: moduleId }) =>
|
|
692
|
+
nativeOptions
|
|
693
|
+
? Static.evaluateComponentModule(nativeOptions, moduleId)
|
|
694
|
+
: null,
|
|
695
|
+
load: async (dependencyId) => {
|
|
696
|
+
const cleanDependencyId = dependencyId.split(/[?#]/, 1)[0]
|
|
697
|
+
if (!path.isAbsolute(cleanDependencyId)) return null
|
|
698
|
+
try {
|
|
699
|
+
return await readFile(cleanDependencyId, 'utf8')
|
|
700
|
+
} catch {
|
|
701
|
+
return null
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
})
|
|
705
|
+
for (const dependency of result.plan.dependencies) {
|
|
706
|
+
if (path.isAbsolute(dependency)) this.addWatchFile(dependency)
|
|
707
|
+
}
|
|
708
|
+
if (result.plan.css) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`Native Tamagui compilation produced unexpected CSS for ${validId}`
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
return result.output.changed
|
|
714
|
+
? { code: result.output.code, map: result.output.map as any }
|
|
715
|
+
: undefined
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function tamaguiNativePlugin(tamaguiOptionsIn: TamaguiOptions = {}): Plugin {
|
|
722
|
+
const plugin = createTamaguiNativePlugin(tamaguiOptionsIn)
|
|
723
|
+
const api =
|
|
724
|
+
plugin.api && typeof plugin.api === 'object'
|
|
725
|
+
? (plugin.api as Record<string, unknown>)
|
|
726
|
+
: {}
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
...plugin,
|
|
730
|
+
api: {
|
|
731
|
+
...api,
|
|
732
|
+
vxrnNative: (context: VxrnNativePluginContext) =>
|
|
733
|
+
createTamaguiNativePlugin(tamaguiOptionsIn, context),
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export type TamaguiVitePluginOptions = TamaguiOptions & {
|
|
739
|
+
disableResolveConfig?: boolean
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export type TamaguiInternalPluginOptions = TamaguiVitePluginOptions & {
|
|
743
|
+
/**
|
|
744
|
+
* Wraps compiler-extracted Tamagui CSS before it is served.
|
|
745
|
+
* `@tamagui/tailwind/vite` uses it to put those rules in `@layer tamagui`, which is
|
|
746
|
+
* what orders them against official Tailwind's `theme`/`utilities` layers.
|
|
747
|
+
*/
|
|
748
|
+
wrapExtractedCSS?: (css: string) => string
|
|
749
|
+
/**
|
|
750
|
+
* Set by the zero-runtime controller when this invocation is an island child
|
|
751
|
+
* build. The island keeps the full runtime and contributes its compiler atomic
|
|
752
|
+
* CSS to the parent's single artifact instead of injecting its own.
|
|
753
|
+
*/
|
|
754
|
+
zeroIslandBuild?: ZeroIslandBuildContext
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* The base Tamagui Vite plugins plus the one config loader they evaluate through.
|
|
759
|
+
*
|
|
760
|
+
* `@tamagui/tailwind/vite` wraps this: it reuses the returned loader for its own
|
|
761
|
+
* scanner plugin, so the Tamagui config is evaluated exactly once for both.
|
|
762
|
+
*/
|
|
763
|
+
export function createTamaguiPlugins({
|
|
174
764
|
disableResolveConfig,
|
|
765
|
+
wrapExtractedCSS = (css) => css,
|
|
766
|
+
zeroIslandBuild,
|
|
175
767
|
...tamaguiOptionsIn
|
|
176
|
-
}:
|
|
177
|
-
|
|
178
|
-
|
|
768
|
+
}: TamaguiInternalPluginOptions = {}): {
|
|
769
|
+
plugins: PluginOption[]
|
|
770
|
+
loader: ViteTamaguiLoader
|
|
771
|
+
} {
|
|
179
772
|
// extraction ON by default, set disableExtraction: true to opt out
|
|
180
773
|
let shouldExtract = !tamaguiOptionsIn.disableExtraction
|
|
181
|
-
let watcher: Promise<{ dispose: () => void } | void | undefined> | undefined
|
|
182
774
|
|
|
183
775
|
// temporary vxrn native env bridge
|
|
184
776
|
const enableNativeEnv = !!globalThis.__vxrnEnableNativeEnv
|
|
777
|
+
const tamaguiLoader = createViteTamaguiLoader(tamaguiOptionsIn)
|
|
778
|
+
const compilerFrontends = new WeakMap<Environment, Static.CompilerFrontend>()
|
|
779
|
+
const getCompilerFrontend = (environment: Environment) => {
|
|
780
|
+
let frontend = compilerFrontends.get(environment)
|
|
781
|
+
if (!frontend) {
|
|
782
|
+
frontend = new Static.CompilerFrontend()
|
|
783
|
+
compilerFrontends.set(environment, frontend)
|
|
784
|
+
}
|
|
785
|
+
return frontend
|
|
786
|
+
}
|
|
787
|
+
const pluginInstanceId = getNextPluginInstanceId()
|
|
788
|
+
const configuredEvaluationPackages = new Set<string>()
|
|
789
|
+
let buildEnvironmentPromise: Promise<void> | null = null
|
|
790
|
+
let buildCleanupPromise: Promise<void> | null = null
|
|
791
|
+
const activeBuildEnvironments = new Set<Environment>()
|
|
792
|
+
const compilerReports =
|
|
793
|
+
process.env.TAMAGUI_COMPILER_STATS || process.env.TAMAGUI_COMPILER_STATS_FILE
|
|
794
|
+
? new Map<string, CompilerModuleReport>()
|
|
795
|
+
: null
|
|
796
|
+
|
|
797
|
+
const releaseBuildEnvironment = async (environment: Environment) => {
|
|
798
|
+
if (!activeBuildEnvironments.delete(environment) || activeBuildEnvironments.size) {
|
|
799
|
+
return
|
|
800
|
+
}
|
|
801
|
+
if (compilerReports?.size) {
|
|
802
|
+
reportCompilerStats(config?.root ?? process.cwd(), compilerReports)
|
|
803
|
+
}
|
|
804
|
+
const currentCleanup = Promise.resolve().then(async () => {
|
|
805
|
+
try {
|
|
806
|
+
await tamaguiLoader.cleanup()
|
|
807
|
+
} finally {
|
|
808
|
+
buildEnvironmentPromise = null
|
|
809
|
+
}
|
|
810
|
+
})
|
|
811
|
+
buildCleanupPromise = currentCleanup
|
|
812
|
+
try {
|
|
813
|
+
await currentCleanup
|
|
814
|
+
} finally {
|
|
815
|
+
if (buildCleanupPromise === currentCleanup) {
|
|
816
|
+
buildCleanupPromise = null
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
185
820
|
|
|
186
821
|
const extensions = [
|
|
187
822
|
`.web.mjs`,
|
|
@@ -198,14 +833,71 @@ export function tamaguiPlugin({
|
|
|
198
833
|
'.json',
|
|
199
834
|
]
|
|
200
835
|
|
|
836
|
+
const getEvaluationEnvironmentOptions = (
|
|
837
|
+
resolvedRoot: string,
|
|
838
|
+
userNoExternal: EvaluationNoExternal
|
|
839
|
+
): EnvironmentOptions => ({
|
|
840
|
+
consumer: 'server',
|
|
841
|
+
keepProcessEnv: true,
|
|
842
|
+
define: {
|
|
843
|
+
'process.env.IS_STATIC': JSON.stringify('is_static'),
|
|
844
|
+
'process.env.TAMAGUI_IS_CLIENT': JSON.stringify(false),
|
|
845
|
+
'process.env.TAMAGUI_IS_SERVER': JSON.stringify(true),
|
|
846
|
+
'process.env.TAMAGUI_TARGET': JSON.stringify('web'),
|
|
847
|
+
'process.env.TAMAGUI_ENVIRONMENT': JSON.stringify(TAMAGUI_EVALUATION_ENVIRONMENT),
|
|
848
|
+
// Config evaluation must retain createTamagui and CSS generation even when
|
|
849
|
+
// the client graph is zero or the client claims the artifact. Inheriting
|
|
850
|
+
// either literal from the outer build empties the artifact it generates.
|
|
851
|
+
'process.env.TAMAGUI_RUNTIME': JSON.stringify('full'),
|
|
852
|
+
'process.env.TAMAGUI_DID_OUTPUT_CSS': JSON.stringify(''),
|
|
853
|
+
// Client configs may strip theme values. Compiler evaluation and outputCSS
|
|
854
|
+
// must use the full config regardless of which outer Vite environment runs last.
|
|
855
|
+
'process.env.VITE_ENVIRONMENT': JSON.stringify('ssr'),
|
|
856
|
+
'process.env.TAMAGUI_DISABLE_SLIDER_INTERVAL': JSON.stringify('1'),
|
|
857
|
+
},
|
|
858
|
+
resolve: {
|
|
859
|
+
// `tamagui-compiler` first, so a package we control can publish a build
|
|
860
|
+
// with no app runtime in it and have the compiler pick that instead. The
|
|
861
|
+
// reanimated driver uses it: the real one imports react-native-reanimated
|
|
862
|
+
// at module scope, which node cannot load (extensionless and directory
|
|
863
|
+
// relative imports), so evaluating any config registering that driver used
|
|
864
|
+
// to fail outright. A condition is the portable way to express this, since
|
|
865
|
+
// every bundler integration can add the same one and an app configures
|
|
866
|
+
// nothing.
|
|
867
|
+
conditions: [TAMAGUI_COMPILER_CONDITION, ...defaultClientConditions],
|
|
868
|
+
mainFields: [...defaultClientMainFields],
|
|
869
|
+
noExternal: mergeEvaluationNoExternal(
|
|
870
|
+
[
|
|
871
|
+
inlineEvaluationTamaguiPackage,
|
|
872
|
+
...configuredEvaluationPackages,
|
|
873
|
+
// a condition only holds while Vite resolves, so these must not reach node
|
|
874
|
+
...scanInstalledTamaguiPackages(resolvedRoot, configuredEvaluationPackages)
|
|
875
|
+
.compilerCondition,
|
|
876
|
+
],
|
|
877
|
+
userNoExternal
|
|
878
|
+
),
|
|
879
|
+
extensions,
|
|
880
|
+
},
|
|
881
|
+
dev: {
|
|
882
|
+
createEnvironment(name, resolved) {
|
|
883
|
+
const evaluationConfig = createServeEvaluationConfig(
|
|
884
|
+
resolved,
|
|
885
|
+
configuredEvaluationPackages
|
|
886
|
+
)
|
|
887
|
+
return createRunnableDevEnvironment(name, evaluationConfig)
|
|
888
|
+
},
|
|
889
|
+
moduleRunnerTransform: true,
|
|
890
|
+
},
|
|
891
|
+
})
|
|
892
|
+
|
|
201
893
|
// start loading immediately but don't block
|
|
202
|
-
loadTamaguiBuildConfig(
|
|
894
|
+
tamaguiLoader.loadTamaguiBuildConfig()
|
|
203
895
|
|
|
204
896
|
// helper to await load when needed
|
|
205
897
|
const ensureLoaded = async () => {
|
|
206
|
-
const promise = getLoadPromise()
|
|
898
|
+
const promise = tamaguiLoader.getLoadPromise()
|
|
207
899
|
if (promise) await promise
|
|
208
|
-
const options = getTamaguiOptions()
|
|
900
|
+
const options = tamaguiLoader.getTamaguiOptions()
|
|
209
901
|
// update shouldExtract from loaded config (tamagui.build.ts)
|
|
210
902
|
if (options) {
|
|
211
903
|
shouldExtract = !options.disableExtraction
|
|
@@ -216,12 +908,25 @@ export function tamaguiPlugin({
|
|
|
216
908
|
// extract plugin state
|
|
217
909
|
const getHash = (input: string) => createHash('sha1').update(input).digest('base64')
|
|
218
910
|
|
|
219
|
-
// use shared cache across environments
|
|
220
|
-
const memoryCache = getSharedCache()
|
|
221
|
-
|
|
222
911
|
const cssMap = new Map<string, string>()
|
|
912
|
+
const transformedModuleIds = new Set<string>()
|
|
913
|
+
const compilerHotUpdateSignatures = new Map<string, string>()
|
|
914
|
+
const compilerHotReloadSignatures = new Map<string, string>()
|
|
223
915
|
let config: ResolvedConfig
|
|
224
916
|
let server: ViteDevServer
|
|
917
|
+
let zero: ZeroRuntimeController | null = null
|
|
918
|
+
let zeroReceipt: ZeroGraphReceipt | null = null
|
|
919
|
+
// closeBundle runs even when the build already failed, so a check there would
|
|
920
|
+
// replace the real error with a derived one
|
|
921
|
+
let zeroBuildFailed = false
|
|
922
|
+
// The compiled-global-CSS tier: an ordinary compiled build that also owns an
|
|
923
|
+
// `outputCSS` artifact and therefore derives TAMAGUI_DID_OUTPUT_CSS from it.
|
|
924
|
+
let globalCSS: Static.GlobalCSSOwnership | null = null
|
|
925
|
+
let globalCSSExpected: string | null = null
|
|
926
|
+
// How many HTML entries received the zero artifact's stylesheet link. A zero
|
|
927
|
+
// build with no HTML entry strips the rules and loads nothing.
|
|
928
|
+
let zeroHtmlEntries = 0
|
|
929
|
+
let zeroDevIslands: Promise<unknown> = Promise.resolve()
|
|
225
930
|
const virtualExt = `.tamagui.css`
|
|
226
931
|
|
|
227
932
|
const getAbsoluteVirtualFileId = (filePath: string) => {
|
|
@@ -231,28 +936,48 @@ export function tamaguiPlugin({
|
|
|
231
936
|
return normalizePath(path.join(config.root, filePath))
|
|
232
937
|
}
|
|
233
938
|
|
|
939
|
+
const isAppJSXSource = (filePath: string) => {
|
|
940
|
+
if (!/\.[jt]sx$/.test(filePath)) return false
|
|
941
|
+
const relative = path.relative(config.root, filePath)
|
|
942
|
+
return (
|
|
943
|
+
relative !== '' &&
|
|
944
|
+
relative !== '..' &&
|
|
945
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
946
|
+
!relative.split(path.sep).includes('node_modules')
|
|
947
|
+
)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const isFrameworkAnalysisRequest = (id: string) =>
|
|
951
|
+
id.includes('__react-router-build-client-route')
|
|
952
|
+
|
|
234
953
|
function isNotClient(environment?: Environment) {
|
|
235
954
|
return environment?.name && environment.name !== 'client'
|
|
236
955
|
}
|
|
237
956
|
|
|
957
|
+
const isDevEnvironment = (environment: Environment): environment is DevEnvironment =>
|
|
958
|
+
environment.mode === 'dev'
|
|
959
|
+
|
|
238
960
|
function isNative(environment?: Environment) {
|
|
239
961
|
return (
|
|
240
962
|
environment?.name && (environment.name === 'ios' || environment.name === 'android')
|
|
241
963
|
)
|
|
242
964
|
}
|
|
243
965
|
|
|
244
|
-
function
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
966
|
+
function invalidateCompilerModules() {
|
|
967
|
+
if (server) {
|
|
968
|
+
const ids = new Set([...transformedModuleIds, ...cssMap.keys()])
|
|
969
|
+
for (const environment of Object.values(server.environments)) {
|
|
970
|
+
if (environment.name === TAMAGUI_EVALUATION_ENVIRONMENT) continue
|
|
971
|
+
for (const id of ids) {
|
|
972
|
+
const modules = environment.moduleGraph.getModulesByFile(id)
|
|
973
|
+
if (!modules) continue
|
|
974
|
+
for (const module of modules) {
|
|
975
|
+
environment.moduleGraph.invalidateModule(module)
|
|
976
|
+
}
|
|
977
|
+
}
|
|
254
978
|
}
|
|
255
979
|
}
|
|
980
|
+
cssMap.clear()
|
|
256
981
|
}
|
|
257
982
|
|
|
258
983
|
const basePlugin: Plugin = {
|
|
@@ -261,15 +986,20 @@ export function tamaguiPlugin({
|
|
|
261
986
|
|
|
262
987
|
configureServer(_server) {
|
|
263
988
|
server = _server
|
|
989
|
+
const evaluationEnvironment = server.environments[TAMAGUI_EVALUATION_ENVIRONMENT]
|
|
990
|
+
if (!isRunnableDevEnvironment(evaluationEnvironment)) {
|
|
991
|
+
throw new Error(
|
|
992
|
+
`The ${TAMAGUI_EVALUATION_ENVIRONMENT} Vite environment must support ModuleRunner evaluation`
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
tamaguiLoader.setEnvironment(evaluationEnvironment)
|
|
264
996
|
},
|
|
265
997
|
|
|
266
998
|
async buildEnd() {
|
|
267
|
-
await
|
|
268
|
-
res?.dispose()
|
|
269
|
-
})
|
|
999
|
+
await releaseBuildEnvironment(this.environment)
|
|
270
1000
|
},
|
|
271
1001
|
|
|
272
|
-
async config(
|
|
1002
|
+
async config(userConfig, env) {
|
|
273
1003
|
const options = await ensureLoaded()
|
|
274
1004
|
|
|
275
1005
|
if (!options) {
|
|
@@ -278,17 +1008,29 @@ export function tamaguiPlugin({
|
|
|
278
1008
|
const useReactNativeWebLite =
|
|
279
1009
|
tamaguiOptionsIn.useReactNativeWebLite ?? options.useReactNativeWebLite
|
|
280
1010
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
...options,
|
|
287
|
-
}).catch((err) => {
|
|
288
|
-
console.error(` [Tamagui] Error watching config: ${err}`)
|
|
289
|
-
})
|
|
1011
|
+
for (const source of [options.config, ...(options.components || [])]) {
|
|
1012
|
+
const packageName = getEvaluationPackageName(source)
|
|
1013
|
+
if (packageName) {
|
|
1014
|
+
configuredEvaluationPackages.add(packageName)
|
|
1015
|
+
}
|
|
290
1016
|
}
|
|
291
1017
|
|
|
1018
|
+
const resolvedRoot = userConfig.root ? path.resolve(userConfig.root) : process.cwd()
|
|
1019
|
+
|
|
1020
|
+
// An island child build is the full-runtime half of the same project, so it
|
|
1021
|
+
// never re-enters zero mode even though it reads the same tamagui.build.ts.
|
|
1022
|
+
zero = zeroIslandBuild
|
|
1023
|
+
? null
|
|
1024
|
+
: await createZeroRuntimeController(options, resolvedRoot, userConfig.base || '/')
|
|
1025
|
+
|
|
1026
|
+
// The island child build's artifact is the parent's, and a dev server has
|
|
1027
|
+
// no final graph to prove the relationship against, so both keep runtime
|
|
1028
|
+
// CSS generation. Production is where the claim is made and gated.
|
|
1029
|
+
globalCSS =
|
|
1030
|
+
zeroIslandBuild || env.command !== 'build'
|
|
1031
|
+
? null
|
|
1032
|
+
: Static.resolveGlobalCSSOwnership(options, resolvedRoot)
|
|
1033
|
+
|
|
292
1034
|
return {
|
|
293
1035
|
envPrefix: ['TAMAGUI_'],
|
|
294
1036
|
|
|
@@ -297,11 +1039,38 @@ export function tamaguiPlugin({
|
|
|
297
1039
|
define: {
|
|
298
1040
|
'process.env.TAMAGUI_IS_CLIENT': JSON.stringify(true),
|
|
299
1041
|
'process.env.TAMAGUI_ENVIRONMENT': '"client"',
|
|
1042
|
+
// An enforced zero client and its SSR peer both receive 'zero', so
|
|
1043
|
+
// SSR never imports a runtime hydration removed.
|
|
1044
|
+
...(zero?.isEnforcing && {
|
|
1045
|
+
'process.env.TAMAGUI_RUNTIME': JSON.stringify('zero'),
|
|
1046
|
+
}),
|
|
1047
|
+
// Derived, never author-set. generateBundle proves the artifact
|
|
1048
|
+
// exists, matches this build's config, and is in the client graph;
|
|
1049
|
+
// a build that cannot prove it fails instead of shipping.
|
|
1050
|
+
...(globalCSS && {
|
|
1051
|
+
'process.env.TAMAGUI_DID_OUTPUT_CSS': JSON.stringify('1'),
|
|
1052
|
+
}),
|
|
300
1053
|
},
|
|
301
1054
|
},
|
|
1055
|
+
ssr: {
|
|
1056
|
+
define: {
|
|
1057
|
+
// the server and client must make the same class-versus-inline
|
|
1058
|
+
// decision or the first client render cannot hydrate the server HTML.
|
|
1059
|
+
...(globalCSS && {
|
|
1060
|
+
'process.env.TAMAGUI_DID_OUTPUT_CSS': JSON.stringify('1'),
|
|
1061
|
+
}),
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
[TAMAGUI_EVALUATION_ENVIRONMENT]: getEvaluationEnvironmentOptions(
|
|
1065
|
+
resolvedRoot,
|
|
1066
|
+
userConfig.environments?.[TAMAGUI_EVALUATION_ENVIRONMENT]?.resolve?.noExternal
|
|
1067
|
+
),
|
|
302
1068
|
},
|
|
303
1069
|
|
|
304
1070
|
define: {
|
|
1071
|
+
// Config evaluation, report builds, native builds, and full-runtime
|
|
1072
|
+
// island child builds all keep ordinary Tamagui runtime behavior.
|
|
1073
|
+
'process.env.TAMAGUI_RUNTIME': JSON.stringify('full'),
|
|
305
1074
|
// reanimated support
|
|
306
1075
|
_frameTimestamp: undefined,
|
|
307
1076
|
_WORKLET: false,
|
|
@@ -325,7 +1094,8 @@ export function tamaguiPlugin({
|
|
|
325
1094
|
resolve('@tamagui/proxy-worm'),
|
|
326
1095
|
'react-native/Libraries/Utilities/codegenNativeComponent':
|
|
327
1096
|
resolve('@tamagui/proxy-worm'),
|
|
328
|
-
'react-native-svg':
|
|
1097
|
+
'react-native-svg': svgWebEntry(),
|
|
1098
|
+
'@tamagui/react-native-svg': svgWebEntry(),
|
|
329
1099
|
...(!useReactNativeWebLite && {
|
|
330
1100
|
'react-native': resolve('react-native-web'),
|
|
331
1101
|
}),
|
|
@@ -338,8 +1108,8 @@ export function tamaguiPlugin({
|
|
|
338
1108
|
|
|
339
1109
|
const rnwLitePlugin: Plugin = {
|
|
340
1110
|
name: 'tamagui-rnw-lite',
|
|
341
|
-
// framework plugins
|
|
342
|
-
// normal config hook
|
|
1111
|
+
// framework plugins add their default react-native-web aliases from a
|
|
1112
|
+
// normal config hook, so apply the explicit lite choice after them.
|
|
343
1113
|
enforce: 'post',
|
|
344
1114
|
|
|
345
1115
|
config() {
|
|
@@ -347,16 +1117,16 @@ export function tamaguiPlugin({
|
|
|
347
1117
|
return {}
|
|
348
1118
|
}
|
|
349
1119
|
|
|
350
|
-
const options = getTamaguiOptions()
|
|
1120
|
+
const options = tamaguiLoader.getTamaguiOptions()
|
|
351
1121
|
const useReactNativeWebLite =
|
|
352
1122
|
tamaguiOptionsIn.useReactNativeWebLite ?? options?.useReactNativeWebLite
|
|
353
1123
|
if (!useReactNativeWebLite) {
|
|
354
1124
|
return {}
|
|
355
1125
|
}
|
|
356
1126
|
|
|
357
|
-
// the dep scanner
|
|
358
|
-
// react-native
|
|
359
|
-
// would otherwise reach the browser raw or trigger a mid-load
|
|
1127
|
+
// the dep scanner does not follow transitive packages through the
|
|
1128
|
+
// react-native to rnw-lite alias. pre-include the CJS dependencies that
|
|
1129
|
+
// would otherwise reach the browser raw or trigger a mid-load optimize.
|
|
360
1130
|
const include: string[] = []
|
|
361
1131
|
for (const dependency of ['memoize-one', '@react-native/normalize-color']) {
|
|
362
1132
|
if (isInstalled(process.cwd(), dependency)) include.push(dependency)
|
|
@@ -366,6 +1136,13 @@ export function tamaguiPlugin({
|
|
|
366
1136
|
resolve: {
|
|
367
1137
|
alias: tamaguiAliases({ rnwLite: useReactNativeWebLite }),
|
|
368
1138
|
},
|
|
1139
|
+
ssr: {
|
|
1140
|
+
// Installed packages are externalized by default in SSR builds, which
|
|
1141
|
+
// bypasses the RNW-lite alias and executes React Native Web's CJS entry
|
|
1142
|
+
// directly in Node. Bundle the Tamagui/RN boundary just as Vite does
|
|
1143
|
+
// for linked workspace packages.
|
|
1144
|
+
noExternal: [/^@tamagui\//, 'tamagui', 'react-native', 'react-native-web'],
|
|
1145
|
+
},
|
|
369
1146
|
optimizeDeps: {
|
|
370
1147
|
// upstream react-native-web must not be pre-bundled when aliased to lite
|
|
371
1148
|
exclude: ['react-native-web'],
|
|
@@ -388,14 +1165,16 @@ export function tamaguiPlugin({
|
|
|
388
1165
|
userConf.optimizeDeps ||= {}
|
|
389
1166
|
userConf.optimizeDeps.include ||= []
|
|
390
1167
|
|
|
391
|
-
//
|
|
392
|
-
// (
|
|
1168
|
+
// These dependencies are CJS and break when served directly to the browser
|
|
1169
|
+
// (`exports`/`module` is not defined). Pre-bundle them before Tamagui's linked
|
|
1170
|
+
// package graph can expose them as late-discovered transitive dependencies.
|
|
393
1171
|
userConf.optimizeDeps.include.push('inline-style-prefixer')
|
|
1172
|
+
addIfInstalled(userConf, userConf.root, ['@react-native/normalize-color'])
|
|
394
1173
|
|
|
395
|
-
// pre-bundle tamagui packages that use internal
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
1174
|
+
// pre-bundle core and web alongside tamagui packages that use internal
|
|
1175
|
+
// contexts and hooks. if either remains linked while these entries are
|
|
1176
|
+
// optimized, Provider imports and optimized consumers can receive
|
|
1177
|
+
// separate theme/component/config contexts even with resolve.dedupe.
|
|
399
1178
|
//
|
|
400
1179
|
// @tamagui/sheet/controller is the lightweight controller subpath imported
|
|
401
1180
|
// by popover/dialog/select; the app imports @tamagui/sheet (full). if these
|
|
@@ -403,22 +1182,35 @@ export function tamaguiPlugin({
|
|
|
403
1182
|
// SheetControllerContext, so the SheetController provider (from /controller)
|
|
404
1183
|
// and the Sheet consumer (from the full entry) never match and adapted
|
|
405
1184
|
// sheets silently never open. include both so they share one context chunk.
|
|
1185
|
+
// ThemeUpdate follows the same rule: its internal implementation must
|
|
1186
|
+
// patch the Theme context provided by the root package instead of
|
|
1187
|
+
// creating a second copy.
|
|
406
1188
|
addIfInstalled(userConf, userConf.root, [
|
|
1189
|
+
'@tamagui/core',
|
|
1190
|
+
'@tamagui/core/theme-update',
|
|
1191
|
+
'@tamagui/web',
|
|
1192
|
+
'@tamagui/web/theme-update',
|
|
1193
|
+
'@tamagui/animations-css',
|
|
1194
|
+
'@tamagui/animations-css/extras',
|
|
407
1195
|
'@tamagui/toast',
|
|
408
|
-
'@tamagui/toast/v2',
|
|
409
1196
|
'@tamagui/sheet',
|
|
410
1197
|
'@tamagui/sheet/controller',
|
|
411
1198
|
])
|
|
412
1199
|
|
|
413
1200
|
// dedupe tamagui packages so nested resolutions collapse to a single
|
|
414
1201
|
// instance. pairs with the include above: include pre-bundles, dedupe
|
|
415
|
-
// prevents duplicate bundling when sub-deps re-resolve them.
|
|
1202
|
+
// prevents duplicate bundling when sub-deps re-resolve them. animations-css
|
|
1203
|
+
// needs the same treatment because apps can import both its root transition
|
|
1204
|
+
// driver and its /extras animated-number driver.
|
|
416
1205
|
userConf.resolve ||= {}
|
|
417
1206
|
userConf.resolve.dedupe ||= []
|
|
418
1207
|
for (const id of [
|
|
419
1208
|
'tamagui',
|
|
420
1209
|
'@tamagui/core',
|
|
1210
|
+
'@tamagui/core/theme-update',
|
|
421
1211
|
'@tamagui/web',
|
|
1212
|
+
'@tamagui/web/theme-update',
|
|
1213
|
+
'@tamagui/animations-css',
|
|
422
1214
|
'@tamagui/toast',
|
|
423
1215
|
'@tamagui/sheet',
|
|
424
1216
|
]) {
|
|
@@ -439,9 +1231,162 @@ export function tamaguiPlugin({
|
|
|
439
1231
|
config = resolvedConfig
|
|
440
1232
|
},
|
|
441
1233
|
|
|
442
|
-
async
|
|
443
|
-
|
|
1234
|
+
async buildStart() {
|
|
1235
|
+
const buildConfig = this.environment.getTopLevelConfig()
|
|
1236
|
+
if (buildConfig.command !== 'build') return
|
|
1237
|
+
|
|
1238
|
+
const pendingCleanup = buildCleanupPromise
|
|
1239
|
+
if (pendingCleanup) {
|
|
1240
|
+
await pendingCleanup
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const buildEnvironment = this.environment
|
|
1244
|
+
activeBuildEnvironments.add(buildEnvironment)
|
|
1245
|
+
try {
|
|
1246
|
+
if (!tamaguiLoader.getEnvironment()) {
|
|
1247
|
+
await tamaguiLoader.loadTamaguiBuildConfig()
|
|
1248
|
+
buildEnvironmentPromise ||= (async () => {
|
|
1249
|
+
const evaluationConfig = await createOwnedEvaluationConfig(
|
|
1250
|
+
buildConfig,
|
|
1251
|
+
configuredEvaluationPackages
|
|
1252
|
+
)
|
|
1253
|
+
const evaluationEnvironment = createRunnableDevEnvironment(
|
|
1254
|
+
TAMAGUI_EVALUATION_ENVIRONMENT,
|
|
1255
|
+
evaluationConfig,
|
|
1256
|
+
{ hot: false }
|
|
1257
|
+
)
|
|
1258
|
+
try {
|
|
1259
|
+
await evaluationEnvironment.init()
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
await evaluationEnvironment.close().catch(() => undefined)
|
|
1262
|
+
throw error
|
|
1263
|
+
}
|
|
1264
|
+
tamaguiLoader.setEnvironment(evaluationEnvironment, { owned: true })
|
|
1265
|
+
})()
|
|
1266
|
+
await buildEnvironmentPromise
|
|
1267
|
+
}
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
await releaseBuildEnvironment(buildEnvironment)
|
|
1270
|
+
throw error
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
|
|
1274
|
+
hotUpdate: {
|
|
1275
|
+
order: 'post',
|
|
1276
|
+
async handler(options) {
|
|
1277
|
+
if (!tamaguiLoader.isEvaluationDependency(options.file)) {
|
|
1278
|
+
if (this.environment.name !== 'client') return
|
|
1279
|
+
const compilerFrontend = getCompilerFrontend(this.environment)
|
|
1280
|
+
const source = options.type === 'delete' ? null : await options.read()
|
|
1281
|
+
const affectedModules = new Set<EnvironmentModuleNode>()
|
|
1282
|
+
const compilerHmrRoots = new Set<string>(
|
|
1283
|
+
compilerFrontend.dependentsOf(options.file)
|
|
1284
|
+
)
|
|
1285
|
+
if (compilerHmrRoots.size || compilerFrontend.has(options.file)) {
|
|
1286
|
+
compilerHmrRoots.add(options.file)
|
|
1287
|
+
}
|
|
1288
|
+
if (compilerFrontend.has(options.file) || compilerHmrRoots.size > 0) {
|
|
1289
|
+
const loadedOptions = await ensureLoaded()
|
|
1290
|
+
if (!loadedOptions?.disable) {
|
|
1291
|
+
const invalidatedIds =
|
|
1292
|
+
options.type === 'delete'
|
|
1293
|
+
? (await compilerFrontend.remove(options.file)).invalidatedIds
|
|
1294
|
+
: await compilerFrontend.update({
|
|
1295
|
+
id: options.file,
|
|
1296
|
+
source: source!,
|
|
1297
|
+
root: config.root,
|
|
1298
|
+
target: 'web',
|
|
1299
|
+
environment: this.environment.name,
|
|
1300
|
+
project: {
|
|
1301
|
+
...(await tamaguiLoader.getCompilerProject()),
|
|
1302
|
+
generation: `${pluginInstanceId}:${tamaguiLoader.getGeneration()}`,
|
|
1303
|
+
},
|
|
1304
|
+
resolve: async (specifier, importer) => {
|
|
1305
|
+
const resolution =
|
|
1306
|
+
await this.environment.pluginContainer.resolveId(
|
|
1307
|
+
specifier,
|
|
1308
|
+
importer
|
|
1309
|
+
)
|
|
1310
|
+
return resolution
|
|
1311
|
+
? { id: resolution.id, external: resolution.external === true }
|
|
1312
|
+
: null
|
|
1313
|
+
},
|
|
1314
|
+
load: async (dependencyId) => {
|
|
1315
|
+
const cleanDependencyId = dependencyId.split(/[?#]/, 1)[0]
|
|
1316
|
+
if (!path.isAbsolute(cleanDependencyId)) return null
|
|
1317
|
+
try {
|
|
1318
|
+
return await readFile(cleanDependencyId, 'utf8')
|
|
1319
|
+
} catch {
|
|
1320
|
+
return null
|
|
1321
|
+
}
|
|
1322
|
+
},
|
|
1323
|
+
})
|
|
1324
|
+
for (const invalidatedId of invalidatedIds) {
|
|
1325
|
+
for (const module of this.environment.moduleGraph.getModulesByFile(
|
|
1326
|
+
invalidatedId
|
|
1327
|
+
) ?? []) {
|
|
1328
|
+
this.environment.moduleGraph.invalidateModule(module)
|
|
1329
|
+
if (compilerHmrRoots.has(invalidatedId) || module.isSelfAccepting) {
|
|
1330
|
+
affectedModules.add(module)
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const cssId = getAbsoluteVirtualFileId(`${invalidatedId}${virtualExt}`)
|
|
1334
|
+
const cssModule = this.environment.moduleGraph.getModuleById(cssId)
|
|
1335
|
+
if (cssModule) {
|
|
1336
|
+
// returned alongside its importer so vite sends a css-update with a
|
|
1337
|
+
// fresh timestamp; an invalidation alone leaves the browser on the
|
|
1338
|
+
// cached stylesheet while the new class hashes have no rules
|
|
1339
|
+
this.environment.moduleGraph.invalidateModule(cssModule)
|
|
1340
|
+
affectedModules.add(cssModule)
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return affectedModules.size ? [...affectedModules] : undefined
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const signature = await (async () => {
|
|
1349
|
+
if (options.type === 'delete') {
|
|
1350
|
+
return getHash(`${options.type}:${options.file}`)
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
return getHash(`${options.type}:${options.file}:${await options.read()}`)
|
|
1354
|
+
} catch {
|
|
1355
|
+
return getHash(`${options.type}:${options.file}:${options.timestamp}`)
|
|
1356
|
+
}
|
|
1357
|
+
})()
|
|
444
1358
|
|
|
1359
|
+
if (compilerHotUpdateSignatures.get(options.file) !== signature) {
|
|
1360
|
+
compilerHotUpdateSignatures.set(options.file, signature)
|
|
1361
|
+
tamaguiLoader.invalidate(options.file)
|
|
1362
|
+
invalidateCompilerModules()
|
|
1363
|
+
}
|
|
1364
|
+
if (
|
|
1365
|
+
this.environment.name === 'client' &&
|
|
1366
|
+
compilerHotReloadSignatures.get(options.file) !== signature
|
|
1367
|
+
) {
|
|
1368
|
+
compilerHotReloadSignatures.set(options.file, signature)
|
|
1369
|
+
this.environment.hot.send({
|
|
1370
|
+
type: 'full-reload',
|
|
1371
|
+
path: '*',
|
|
1372
|
+
triggeredBy: options.file,
|
|
1373
|
+
})
|
|
1374
|
+
}
|
|
1375
|
+
return []
|
|
1376
|
+
},
|
|
1377
|
+
},
|
|
1378
|
+
|
|
1379
|
+
async watchChange(id) {
|
|
1380
|
+
if (config.command !== 'build') {
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
if (tamaguiLoader.isEvaluationDependency(id)) {
|
|
1384
|
+
tamaguiLoader.invalidate(id)
|
|
1385
|
+
invalidateCompilerModules()
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
|
|
1389
|
+
async resolveId(source) {
|
|
445
1390
|
if (isNative(this.environment)) {
|
|
446
1391
|
return
|
|
447
1392
|
}
|
|
@@ -450,14 +1395,16 @@ export function tamaguiPlugin({
|
|
|
450
1395
|
return
|
|
451
1396
|
}
|
|
452
1397
|
|
|
1398
|
+
if (!shouldExtract) return
|
|
1399
|
+
|
|
453
1400
|
const [validId, query] = source.split('?')
|
|
454
1401
|
|
|
455
1402
|
if (!validId.endsWith(virtualExt)) {
|
|
456
1403
|
return
|
|
457
1404
|
}
|
|
458
1405
|
|
|
459
|
-
const absoluteId =
|
|
460
|
-
?
|
|
1406
|
+
const absoluteId = validId.startsWith(config.root)
|
|
1407
|
+
? validId
|
|
461
1408
|
: getAbsoluteVirtualFileId(validId)
|
|
462
1409
|
|
|
463
1410
|
if (cssMap.has(absoluteId)) {
|
|
@@ -466,9 +1413,7 @@ export function tamaguiPlugin({
|
|
|
466
1413
|
},
|
|
467
1414
|
|
|
468
1415
|
async load(id) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const options = getTamaguiOptions()
|
|
1416
|
+
const options = tamaguiLoader.getTamaguiOptions()
|
|
472
1417
|
if (options?.disable) {
|
|
473
1418
|
return
|
|
474
1419
|
}
|
|
@@ -481,183 +1426,514 @@ export function tamaguiPlugin({
|
|
|
481
1426
|
return
|
|
482
1427
|
}
|
|
483
1428
|
|
|
1429
|
+
if (!shouldExtract) return
|
|
1430
|
+
|
|
484
1431
|
const [validId] = id.split('?')
|
|
1432
|
+
if (!validId.endsWith(virtualExt)) return
|
|
1433
|
+
// the importer's transform is what fills this module's css. after a hot
|
|
1434
|
+
// update the browser can fetch the css-update before it re-imports the
|
|
1435
|
+
// component, so run (or join) that transform here instead of serving the
|
|
1436
|
+
// rules of the previous edit
|
|
1437
|
+
if (isDevEnvironment(this.environment)) {
|
|
1438
|
+
const importer = this.environment.moduleGraph.getModuleById(
|
|
1439
|
+
validId.slice(0, -virtualExt.length)
|
|
1440
|
+
)
|
|
1441
|
+
if (importer && importer.transformResult == null) {
|
|
1442
|
+
await this.environment.transformRequest(importer.url)
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
485
1445
|
return cssMap.get(validId)
|
|
486
1446
|
},
|
|
1447
|
+
}
|
|
487
1448
|
|
|
1449
|
+
// Source and compiled JSX reach this filtered post-transform after user syntax
|
|
1450
|
+
// plugins and before Vite import analysis.
|
|
1451
|
+
const sharedCompilerPlugin: Plugin = {
|
|
1452
|
+
name: 'tamagui-compiler',
|
|
1453
|
+
enforce: 'post',
|
|
488
1454
|
transform: {
|
|
489
1455
|
order: 'pre',
|
|
490
1456
|
async handler(code, id) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
// ensure full config (heavy bundling) is loaded before extraction
|
|
495
|
-
await ensureFullConfigLoaded()
|
|
496
|
-
|
|
497
|
-
// fully disabled = no extraction AND no debug attrs
|
|
498
|
-
if (options?.disable) {
|
|
499
|
-
return
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (isNative(this.environment)) {
|
|
503
|
-
return
|
|
504
|
-
}
|
|
1457
|
+
if (this.environment?.name === TAMAGUI_EVALUATION_ENVIRONMENT) return
|
|
1458
|
+
if (!tamaguiLoader.getEnvironment()) return
|
|
1459
|
+
if (isNative(this.environment)) return
|
|
505
1460
|
|
|
506
1461
|
const [validId] = id.split('?')
|
|
507
|
-
if (
|
|
1462
|
+
if (
|
|
1463
|
+
isFrameworkAnalysisRequest(id) ||
|
|
1464
|
+
!isAppJSXSource(validId) ||
|
|
1465
|
+
!/\.[jt]sx$/.test(validId)
|
|
1466
|
+
)
|
|
508
1467
|
return
|
|
509
|
-
|
|
1468
|
+
const options = await ensureLoaded()
|
|
1469
|
+
if (options?.disable || !shouldExtract) return
|
|
510
1470
|
|
|
511
|
-
const { shouldDisable
|
|
1471
|
+
const { shouldDisable } = await Static.getPragmaOptions({
|
|
512
1472
|
source: code,
|
|
513
1473
|
path: validId,
|
|
514
1474
|
})
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
const result = await pendingExtraction
|
|
560
|
-
if (result) {
|
|
561
|
-
return formatResult(result)
|
|
562
|
-
}
|
|
563
|
-
return
|
|
1475
|
+
if (shouldDisable) return
|
|
1476
|
+
|
|
1477
|
+
const evaluationDependencies = await tamaguiLoader.ensureFullConfigLoaded()
|
|
1478
|
+
for (const dependency of evaluationDependencies) this.addWatchFile(dependency)
|
|
1479
|
+
const compilerProject = await tamaguiLoader.getCompilerProject()
|
|
1480
|
+
const compilerFrontend = getCompilerFrontend(this.environment)
|
|
1481
|
+
const result = await compilerFrontend.compile({
|
|
1482
|
+
id: validId,
|
|
1483
|
+
source: code,
|
|
1484
|
+
root: config.root,
|
|
1485
|
+
target: 'web',
|
|
1486
|
+
environment: this.environment.name,
|
|
1487
|
+
project: {
|
|
1488
|
+
...compilerProject,
|
|
1489
|
+
generation: `${pluginInstanceId}:${tamaguiLoader.getGeneration()}`,
|
|
1490
|
+
// `report` runs the same analysis as `enforce`, including the
|
|
1491
|
+
// mode-aware diagnostics, so both emit the identical violation list
|
|
1492
|
+
zeroRuntime: zero !== null,
|
|
1493
|
+
},
|
|
1494
|
+
resolve: async (specifier, importer) => {
|
|
1495
|
+
const resolution = await this.resolve(specifier, importer, { skipSelf: true })
|
|
1496
|
+
return resolution
|
|
1497
|
+
? { id: resolution.id, external: resolution.external === true }
|
|
1498
|
+
: null
|
|
1499
|
+
},
|
|
1500
|
+
evaluate: ({ id: moduleId }) => tamaguiLoader.evaluateModule(moduleId),
|
|
1501
|
+
load: async (dependencyId) => {
|
|
1502
|
+
const cleanDependencyId = dependencyId.split(/[?#]/, 1)[0]
|
|
1503
|
+
if (!path.isAbsolute(cleanDependencyId)) return null
|
|
1504
|
+
try {
|
|
1505
|
+
return await readFile(cleanDependencyId, 'utf8')
|
|
1506
|
+
} catch {
|
|
1507
|
+
return null
|
|
1508
|
+
}
|
|
1509
|
+
},
|
|
1510
|
+
})
|
|
1511
|
+
transformedModuleIds.add(validId)
|
|
1512
|
+
compilerReports?.set(validId, {
|
|
1513
|
+
stats: result.plan.stats,
|
|
1514
|
+
diagnostics: result.plan.diagnostics,
|
|
1515
|
+
})
|
|
1516
|
+
for (const dependency of result.plan.dependencies) {
|
|
1517
|
+
if (path.isAbsolute(dependency)) this.addWatchFile(dependency)
|
|
564
1518
|
}
|
|
565
1519
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1520
|
+
// Island child build: the parent owns the one CSS artifact, so route this
|
|
1521
|
+
// module's atomic rules there and inject nothing.
|
|
1522
|
+
if (zeroIslandBuild) {
|
|
1523
|
+
zeroIslandBuild.artifact.setIslandModuleCSS(
|
|
1524
|
+
zeroIslandBuild.islandId,
|
|
1525
|
+
validId,
|
|
1526
|
+
wrapExtractedCSS(result.plan.css)
|
|
569
1527
|
)
|
|
1528
|
+
return result.output.changed
|
|
1529
|
+
? { code: result.output.code, map: result.output.map as any }
|
|
1530
|
+
: undefined
|
|
570
1531
|
}
|
|
571
1532
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
console.info(
|
|
585
|
-
`[tamagui-cache] ERROR extracting ${id.split('/').pop()}:`,
|
|
586
|
-
err
|
|
1533
|
+
if (zero) {
|
|
1534
|
+
const zeroResult = Static.transformZeroModule({
|
|
1535
|
+
mode: zero.isEnforcing ? 'enforce' : 'report',
|
|
1536
|
+
id: validId,
|
|
1537
|
+
root: config.root,
|
|
1538
|
+
source: code,
|
|
1539
|
+
plan: result.plan,
|
|
1540
|
+
config: (await tamaguiLoader.getTamaguiConfig())!,
|
|
1541
|
+
isTamaguiSpecifier: Static.isTamaguiSpecifier,
|
|
1542
|
+
resolveIslandLoader: (specifier) => {
|
|
1543
|
+
const islandId = zero!.loaderIds.get(
|
|
1544
|
+
zeroModuleKey(path.resolve(path.dirname(validId), specifier))
|
|
587
1545
|
)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1546
|
+
return islandId ? { islandId } : null
|
|
1547
|
+
},
|
|
1548
|
+
resolveIslandModule: (specifier) =>
|
|
1549
|
+
zero!.islandModuleIds.get(
|
|
1550
|
+
zeroModuleKey(path.resolve(path.dirname(validId), specifier))
|
|
1551
|
+
) ?? null,
|
|
1552
|
+
})
|
|
1553
|
+
zero.transformed.add(validId)
|
|
1554
|
+
if (zeroResult.erased.exports.length) {
|
|
1555
|
+
zero.erasedExports.set(validId, zeroResult.erased.exports)
|
|
1556
|
+
}
|
|
1557
|
+
for (const violation of zeroResult.violations) {
|
|
1558
|
+
const { line, column } = Static.offsetToLineColumn(code, violation.span.start)
|
|
1559
|
+
zero.violations.push({
|
|
1560
|
+
file: path.relative(config.root, validId),
|
|
1561
|
+
line,
|
|
1562
|
+
column,
|
|
1563
|
+
rule: violation.rule,
|
|
1564
|
+
code: violation.code,
|
|
1565
|
+
component: violation.component,
|
|
1566
|
+
message: violation.message,
|
|
1567
|
+
})
|
|
591
1568
|
}
|
|
1569
|
+
// `report` runs the same analysis and then leaves everything else
|
|
1570
|
+
// alone: full runtime, ordinary CSS handling, unchanged source. So it
|
|
1571
|
+
// falls through to the ordinary path below.
|
|
1572
|
+
if (zero.isEnforcing) {
|
|
1573
|
+
Static.mergeIslandBridges(zero.bridges, zeroResult.bridges)
|
|
1574
|
+
const moduleCSS = [
|
|
1575
|
+
wrapExtractedCSS(result.plan.css),
|
|
1576
|
+
...zeroResult.bridgeCSS.values(),
|
|
1577
|
+
]
|
|
1578
|
+
.filter(Boolean)
|
|
1579
|
+
.join('\n')
|
|
1580
|
+
|
|
1581
|
+
// Production combines every module's rules into the one artifact the
|
|
1582
|
+
// entry loads. Development keeps them on Vite's per-module CSS
|
|
1583
|
+
// modules, where the importer owns the ordering and hot replacement
|
|
1584
|
+
// already works.
|
|
1585
|
+
if (config.command !== 'build') {
|
|
1586
|
+
let cssImport = ''
|
|
1587
|
+
if (moduleCSS) {
|
|
1588
|
+
const rootRelativeId = `${validId}${virtualExt}`
|
|
1589
|
+
cssMap.set(getAbsoluteVirtualFileId(rootRelativeId), moduleCSS)
|
|
1590
|
+
this.addWatchFile(rootRelativeId)
|
|
1591
|
+
cssImport = `\nimport "${rootRelativeId}";`
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
code: `${zeroResult.output.code}${cssImport}`,
|
|
1595
|
+
map: zeroResult.output.map as any,
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
592
1598
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
console.info(
|
|
596
|
-
`[tamagui-cache] no extraction result for ${id.split('/').pop()}`
|
|
597
|
-
)
|
|
1599
|
+
for (const [identifier, rules] of zeroResult.bridgeCSS) {
|
|
1600
|
+
zero.artifact.setBridgeRules(identifier, rules)
|
|
598
1601
|
}
|
|
599
|
-
|
|
1602
|
+
zero.artifact.setZeroModuleCSS(validId, wrapExtractedCSS(result.plan.css))
|
|
1603
|
+
return zeroResult.output.changed
|
|
1604
|
+
? { code: zeroResult.output.code, map: zeroResult.output.map as any }
|
|
1605
|
+
: undefined
|
|
600
1606
|
}
|
|
1607
|
+
}
|
|
601
1608
|
|
|
1609
|
+
const isSSR = isNotClient(this.environment)
|
|
1610
|
+
let cssImport: string | null = null
|
|
1611
|
+
if (result.plan.css) {
|
|
602
1612
|
const rootRelativeId = `${validId}${virtualExt}`
|
|
603
1613
|
const absoluteId = getAbsoluteVirtualFileId(rootRelativeId)
|
|
1614
|
+
cssMap.set(absoluteId, wrapExtractedCSS(result.plan.css))
|
|
1615
|
+
this.addWatchFile(rootRelativeId)
|
|
1616
|
+
if (!isSSR) cssImport = `import "${rootRelativeId}";`
|
|
1617
|
+
}
|
|
1618
|
+
const finalCode = cssImport
|
|
1619
|
+
? `${result.output.code}\n${cssImport}`
|
|
1620
|
+
: result.output.code
|
|
1621
|
+
return result.output.changed || cssImport
|
|
1622
|
+
? { code: finalCode, map: result.output.map as any }
|
|
1623
|
+
: undefined
|
|
1624
|
+
},
|
|
1625
|
+
},
|
|
1626
|
+
}
|
|
604
1627
|
|
|
605
|
-
|
|
1628
|
+
// Owns the single CSS artifact, the island child builds, and the module-graph
|
|
1629
|
+
// gate that is the only thing that actually proves the zero guarantee.
|
|
1630
|
+
//
|
|
1631
|
+
// Development runs the same lowering and reference erasure, so the runtime
|
|
1632
|
+
// that generates design-system, :root, font and theme CSS is gone there too.
|
|
1633
|
+
// The dev server therefore has to serve that CSS itself: it publishes the
|
|
1634
|
+
// config half at the same href production uses and builds the islands once at
|
|
1635
|
+
// startup. Per-module atomic rules keep Vite's own `.tamagui.css` modules in
|
|
1636
|
+
// dev, which is where hot replacement already works; production combines them
|
|
1637
|
+
// into the one artifact instead.
|
|
1638
|
+
const zeroRuntimePlugin: Plugin = {
|
|
1639
|
+
name: 'tamagui-zero-runtime',
|
|
1640
|
+
enforce: 'post',
|
|
606
1641
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1642
|
+
async buildStart() {
|
|
1643
|
+
if (!zero || this.environment.name !== 'client') return
|
|
1644
|
+
await tamaguiLoader.ensureFullConfigLoaded()
|
|
1645
|
+
const tamaguiConfig = await tamaguiLoader.getTamaguiConfig()
|
|
1646
|
+
if (!tamaguiConfig) {
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
`[tamagui zero-runtime] the Tamagui config did not evaluate, so no CSS artifact can be generated`
|
|
1649
|
+
)
|
|
1650
|
+
}
|
|
1651
|
+
zero.violations.length = 0
|
|
1652
|
+
zero.transformed.clear()
|
|
1653
|
+
zero.erasedExports.clear()
|
|
1654
|
+
if (!zero.isEnforcing) return
|
|
1655
|
+
Static.assertZeroConfigDrivers(tamaguiConfig)
|
|
1656
|
+
zero.artifact.clearGraphs()
|
|
1657
|
+
zero.bridges.clear()
|
|
1658
|
+
zeroHtmlEntries = 0
|
|
1659
|
+
zero.artifact.setConfigCSS(tamaguiConfig.getCSS())
|
|
1660
|
+
|
|
1661
|
+
// Production builds the islands at the end, once the zero graph is known.
|
|
1662
|
+
// Development has no such end, so they are built here, after the reset
|
|
1663
|
+
// that would otherwise discard their rules, and the dev server's artifact
|
|
1664
|
+
// route waits on this.
|
|
1665
|
+
if (config.command !== 'build') {
|
|
1666
|
+
const islands = zero
|
|
1667
|
+
zeroDevIslands = Promise.all(
|
|
1668
|
+
islands.resolved.islands.map((island) =>
|
|
1669
|
+
buildIsland({
|
|
1670
|
+
island,
|
|
1671
|
+
controller: islands,
|
|
1672
|
+
root: config.root,
|
|
1673
|
+
outDir: zeroDevIslandDir(islands),
|
|
1674
|
+
mode: 'development',
|
|
1675
|
+
})
|
|
1676
|
+
)
|
|
1677
|
+
)
|
|
1678
|
+
await zeroDevIslands
|
|
1679
|
+
}
|
|
1680
|
+
},
|
|
610
1681
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1682
|
+
async configureServer(devServer) {
|
|
1683
|
+
if (!zero?.isEnforcing) return
|
|
1684
|
+
const islandBase = `${zero.cssHref.replace(ZERO_CSS_FILENAME, '')}${ZERO_ISLAND_DIRNAME}/`
|
|
1685
|
+
devServer.middlewares.use(async (request, response, next) => {
|
|
1686
|
+
const url = (request.url || '').split('?')[0]
|
|
1687
|
+
if (url !== zero!.cssHref && !url.startsWith(islandBase)) return next()
|
|
1688
|
+
// buildStart owns the artifact's contents and the island builds, so a
|
|
1689
|
+
// request that arrives first waits for it rather than reading a
|
|
1690
|
+
// half-populated artifact
|
|
1691
|
+
await zeroDevIslands
|
|
1692
|
+
if (url === zero!.cssHref) {
|
|
1693
|
+
response.setHeader('content-type', 'text/css; charset=utf-8')
|
|
1694
|
+
response.setHeader('cache-control', 'no-cache')
|
|
1695
|
+
response.end(zero!.artifact.css())
|
|
1696
|
+
return
|
|
1697
|
+
}
|
|
1698
|
+
const islandId = url.slice(islandBase.length).replace(/\.js$/, '')
|
|
1699
|
+
const file = path.join(
|
|
1700
|
+
zeroDevIslandDir(zero!),
|
|
1701
|
+
ZERO_ISLAND_DIRNAME,
|
|
1702
|
+
`${islandId}.js`
|
|
1703
|
+
)
|
|
1704
|
+
if (!existsSync(file)) return next()
|
|
1705
|
+
response.setHeader('content-type', 'text/javascript; charset=utf-8')
|
|
1706
|
+
response.setHeader('cache-control', 'no-cache')
|
|
1707
|
+
response.end(readFileSync(file))
|
|
1708
|
+
})
|
|
1709
|
+
},
|
|
614
1710
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1711
|
+
// The last hook that still sees the resolved graph and runs before rolldown
|
|
1712
|
+
// renders chunks. An erased export that some module still imports has to be
|
|
1713
|
+
// reported here: by render time the bundler has already failed on it with a
|
|
1714
|
+
// message about a missing export, which says nothing about why it is missing.
|
|
1715
|
+
buildEnd(error) {
|
|
1716
|
+
if (!zero || this.environment.name !== 'client') return
|
|
1717
|
+
if (error) {
|
|
1718
|
+
zeroBuildFailed = true
|
|
1719
|
+
return
|
|
1720
|
+
}
|
|
1721
|
+
if (!zero.isEnforcing) return
|
|
1722
|
+
const importers = new Map<string, readonly string[]>()
|
|
1723
|
+
for (const moduleId of this.getModuleIds()) {
|
|
1724
|
+
importers.set(moduleId, this.getModuleInfo(moduleId)?.importers ?? [])
|
|
1725
|
+
}
|
|
1726
|
+
const escape = Static.erasedExportEscape({
|
|
1727
|
+
integration: 'vite',
|
|
1728
|
+
transformed: zero.transformed,
|
|
1729
|
+
erasedExports: zero.erasedExports,
|
|
1730
|
+
importersOf: importers,
|
|
1731
|
+
})
|
|
1732
|
+
if (escape) {
|
|
1733
|
+
zeroBuildFailed = true
|
|
1734
|
+
throw new Error(escape)
|
|
1735
|
+
}
|
|
1736
|
+
},
|
|
618
1737
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1738
|
+
transformIndexHtml: {
|
|
1739
|
+
order: 'post',
|
|
1740
|
+
handler(html) {
|
|
1741
|
+
if (!zero?.isEnforcing) return
|
|
1742
|
+
zeroHtmlEntries++
|
|
1743
|
+
return {
|
|
1744
|
+
html,
|
|
1745
|
+
tags: [
|
|
1746
|
+
{
|
|
1747
|
+
tag: 'link',
|
|
1748
|
+
attrs: { rel: 'stylesheet', href: zero.cssHref },
|
|
1749
|
+
injectTo: 'head',
|
|
1750
|
+
},
|
|
1751
|
+
],
|
|
1752
|
+
}
|
|
1753
|
+
},
|
|
1754
|
+
},
|
|
626
1755
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1756
|
+
generateBundle(_outputOptions, bundle) {
|
|
1757
|
+
if (!zero?.isEnforcing || this.environment.name !== 'client') return
|
|
1758
|
+
// rolldown reports the modules that contributed rendered code per chunk,
|
|
1759
|
+
// which is exactly what shipped. Importer edges come from the whole
|
|
1760
|
+
// resolved graph so a forbidden module can name its shortest chain.
|
|
1761
|
+
const importers = new Map<string, string[]>()
|
|
1762
|
+
for (const moduleId of this.getModuleIds()) {
|
|
1763
|
+
for (const imported of this.getModuleInfo(moduleId)?.importedIds ?? []) {
|
|
1764
|
+
const list = importers.get(imported)
|
|
1765
|
+
if (list) list.push(moduleId)
|
|
1766
|
+
else importers.set(imported, [moduleId])
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const entries: string[] = []
|
|
1770
|
+
const modules: { id: string; importers: readonly string[] }[] = []
|
|
1771
|
+
for (const chunk of Object.values(bundle)) {
|
|
1772
|
+
if (chunk.type !== 'chunk') continue
|
|
1773
|
+
for (const moduleId of Object.keys(chunk.modules)) {
|
|
1774
|
+
modules.push({ id: moduleId, importers: importers.get(moduleId) ?? [] })
|
|
1775
|
+
if (this.getModuleInfo(moduleId)?.isEntry) entries.push(moduleId)
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const checked = Static.checkZeroGraph({
|
|
1779
|
+
entries,
|
|
1780
|
+
modules,
|
|
1781
|
+
importerEdges: importers,
|
|
1782
|
+
root: zero.resolved.root,
|
|
1783
|
+
})
|
|
1784
|
+
zeroReceipt = {
|
|
1785
|
+
integration: 'vite',
|
|
1786
|
+
graph: 'zero',
|
|
1787
|
+
entries: entries.sort(),
|
|
1788
|
+
moduleCount: modules.length,
|
|
1789
|
+
tamaguiModules: checked.tamaguiModules,
|
|
1790
|
+
forbidden: checked.forbidden,
|
|
1791
|
+
cssArtifact: null,
|
|
1792
|
+
identity: '',
|
|
1793
|
+
gzip: Object.fromEntries(
|
|
1794
|
+
Object.values(bundle)
|
|
1795
|
+
.filter((chunk) => chunk.type === 'chunk')
|
|
1796
|
+
.map((chunk) => [
|
|
1797
|
+
chunk.fileName,
|
|
1798
|
+
gzipSync(Buffer.from((chunk as any).code), { level: 9 }).length,
|
|
1799
|
+
])
|
|
1800
|
+
),
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
635
1803
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1804
|
+
async closeBundle() {
|
|
1805
|
+
if (!zero || this.environment.name !== 'client') return
|
|
1806
|
+
const outDir = path.resolve(config.root, this.environment.config.build.outDir)
|
|
1807
|
+
// one receipt per output directory, so a zero build and its negative
|
|
1808
|
+
// control never overwrite each other's evidence
|
|
1809
|
+
const receiptName = `vite-${path.basename(outDir)}`
|
|
1810
|
+
// Written in both modes and before the failure, so `report` and `enforce`
|
|
1811
|
+
// emit the identical list and only their exit differs.
|
|
1812
|
+
Static.writeZeroViolationReport(zero.resolved.outDir, receiptName, {
|
|
1813
|
+
integration: 'vite',
|
|
1814
|
+
mode: zero.isEnforcing ? 'enforce' : 'report',
|
|
1815
|
+
violations: zero.violations,
|
|
1816
|
+
})
|
|
1817
|
+
if (!zero.isEnforcing || zeroBuildFailed) return
|
|
1818
|
+
if (zero.violations.length) {
|
|
1819
|
+
throw new Error(Static.formatZeroViolations(zero.violations))
|
|
1820
|
+
}
|
|
1821
|
+
const islandOutputHashes: Record<string, string> = {}
|
|
1822
|
+
for (const island of zero.resolved.islands) {
|
|
1823
|
+
const built = await buildIsland({
|
|
1824
|
+
island,
|
|
1825
|
+
controller: zero,
|
|
1826
|
+
root: config.root,
|
|
1827
|
+
outDir,
|
|
1828
|
+
mode: config.mode,
|
|
1829
|
+
})
|
|
1830
|
+
islandOutputHashes[island.id] = built.hash
|
|
1831
|
+
}
|
|
641
1832
|
|
|
642
|
-
|
|
643
|
-
|
|
1833
|
+
// The plugin, not the app, injects the zero artifact's stylesheet link, so
|
|
1834
|
+
// an entry graph with no HTML entry strips the rules and loads nothing.
|
|
1835
|
+
if (zeroHtmlEntries === 0) {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`[tamagui zero-runtime] the zero entry graph has no HTML entry, so the one generated CSS artifact ${zero.cssHref} is never loaded. Build a zero entry through its HTML document.`
|
|
1838
|
+
)
|
|
1839
|
+
}
|
|
644
1840
|
|
|
645
|
-
|
|
646
|
-
|
|
1841
|
+
const css = finalizeZeroCSS(zero, outDir)
|
|
1842
|
+
const bridgeManifest = Static.canonicalizeBridgeManifest(
|
|
1843
|
+
Object.fromEntries(
|
|
1844
|
+
[...zero.bridges.entries()].sort(([left], [right]) => (left < right ? -1 : 1))
|
|
1845
|
+
)
|
|
1846
|
+
)
|
|
1847
|
+
const identityInputs = {
|
|
1848
|
+
runtimeLiteral: 'zero' as const,
|
|
1849
|
+
target: 'web' as const,
|
|
1850
|
+
configGeneration: `${pluginInstanceId}:${tamaguiLoader.getGeneration()}`,
|
|
1851
|
+
cssHash: css.hash,
|
|
1852
|
+
compilerVersion: Static.ZERO_COMPILER_VERSION,
|
|
1853
|
+
islandEntries: zero.resolved.islands.map((island) => island.module),
|
|
1854
|
+
bridgeManifestHash: Static.hashBridgeManifest(bridgeManifest),
|
|
1855
|
+
islandOutputHashes,
|
|
1856
|
+
}
|
|
1857
|
+
const identity = Static.hashZeroIdentity(identityInputs)
|
|
647
1858
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
|
|
1859
|
+
if (!zeroReceipt) {
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
`[tamagui zero-runtime] no module graph was recorded for the zero entry`
|
|
1862
|
+
)
|
|
1863
|
+
}
|
|
1864
|
+
zeroReceipt.cssArtifact = { path: css.href, hash: css.hash }
|
|
1865
|
+
zeroReceipt.identity = identity
|
|
1866
|
+
Static.writeZeroGraphReceipt(zero.resolved.outDir, receiptName, zeroReceipt)
|
|
1867
|
+
writeFileSync(
|
|
1868
|
+
path.join(zero.resolved.outDir, `${receiptName}.bridges.json`),
|
|
1869
|
+
`${JSON.stringify(
|
|
1870
|
+
{ identity, identityInputs, cssGzip: css.gzip, bridges: bridgeManifest },
|
|
1871
|
+
null,
|
|
1872
|
+
2
|
|
1873
|
+
)}\n`
|
|
1874
|
+
)
|
|
1875
|
+
assertZeroGraph(zeroReceipt)
|
|
1876
|
+
},
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// The compiled-global-CSS tier. `TAMAGUI_DID_OUTPUT_CSS` was already inlined
|
|
1880
|
+
// in the client environment, so this proves the artifact that replaces those
|
|
1881
|
+
// stripped rules exists, matches this build's config, and is in the graph.
|
|
1882
|
+
const globalCSSPlugin: Plugin = {
|
|
1883
|
+
name: 'tamagui-global-css',
|
|
1884
|
+
enforce: 'post',
|
|
1885
|
+
apply: 'build',
|
|
1886
|
+
|
|
1887
|
+
async buildStart() {
|
|
1888
|
+
if (!globalCSS || this.environment.name !== 'client') return
|
|
1889
|
+
await tamaguiLoader.ensureFullConfigLoaded()
|
|
1890
|
+
const tamaguiConfig = await tamaguiLoader.getTamaguiConfig()
|
|
1891
|
+
if (!tamaguiConfig) {
|
|
1892
|
+
throw new Error(
|
|
1893
|
+
`[tamagui] outputCSS is set but the Tamagui config did not evaluate, so no CSS artifact can be generated`
|
|
1894
|
+
)
|
|
1895
|
+
}
|
|
1896
|
+
globalCSSExpected = tamaguiConfig.getCSS()
|
|
659
1897
|
},
|
|
1898
|
+
|
|
1899
|
+
generateBundle() {
|
|
1900
|
+
if (!globalCSS || this.environment.name !== 'client') return
|
|
1901
|
+
const failure = Static.checkGlobalCSSArtifact({
|
|
1902
|
+
cssPath: globalCSS.cssPath,
|
|
1903
|
+
expectedCSS: globalCSSExpected ?? '',
|
|
1904
|
+
loadedModuleIds: this.getModuleIds(),
|
|
1905
|
+
importHint: `Import it once from your client entry: import ${JSON.stringify(
|
|
1906
|
+
relativeImportSpecifier(config.root, globalCSS.cssPath)
|
|
1907
|
+
)}`,
|
|
1908
|
+
})
|
|
1909
|
+
if (failure) throw new Error(failure.message)
|
|
1910
|
+
},
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
return {
|
|
1914
|
+
plugins: [
|
|
1915
|
+
basePlugin,
|
|
1916
|
+
rnwLitePlugin,
|
|
1917
|
+
extractPlugin,
|
|
1918
|
+
sharedCompilerPlugin,
|
|
1919
|
+
zeroRuntimePlugin,
|
|
1920
|
+
globalCSSPlugin,
|
|
1921
|
+
tamaguiNativePlugin(tamaguiOptionsIn),
|
|
1922
|
+
],
|
|
1923
|
+
loader: tamaguiLoader,
|
|
660
1924
|
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/** Where the dev server's island bundles are built and served from. */
|
|
1928
|
+
function zeroDevIslandDir(zero: ZeroRuntimeController) {
|
|
1929
|
+
return path.join(zero.resolved.outDir, 'dev')
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function relativeImportSpecifier(from: string, to: string) {
|
|
1933
|
+
const relative = normalizePath(path.relative(from, to))
|
|
1934
|
+
return relative.startsWith('.') ? relative : `./${relative}`
|
|
1935
|
+
}
|
|
661
1936
|
|
|
662
|
-
|
|
1937
|
+
export function tamaguiPlugin(options: TamaguiVitePluginOptions = {}): PluginOption {
|
|
1938
|
+
return createTamaguiPlugins(options).plugins
|
|
663
1939
|
}
|