zyzz 0.0.6 → 0.0.8

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 (104) hide show
  1. package/README.md +26 -1
  2. package/dist/Style.d.ts +3 -3
  3. package/dist/Style.d.ts.map +1 -1
  4. package/dist/Style.js +12 -1
  5. package/dist/Style.js.map +1 -1
  6. package/dist/babel/index.d.ts +4 -1
  7. package/dist/babel/index.d.ts.map +1 -1
  8. package/dist/babel/index.js +6 -3
  9. package/dist/babel/index.js.map +1 -1
  10. package/dist/babel/internal/Compilation.d.ts +12 -0
  11. package/dist/babel/internal/Compilation.d.ts.map +1 -0
  12. package/dist/babel/internal/Compilation.js +3 -0
  13. package/dist/babel/internal/Compilation.js.map +1 -0
  14. package/dist/compiler/Graph.d.ts +6 -1
  15. package/dist/compiler/Graph.d.ts.map +1 -1
  16. package/dist/compiler/Graph.js +160 -36
  17. package/dist/compiler/Graph.js.map +1 -1
  18. package/dist/compiler/Transform.d.ts.map +1 -1
  19. package/dist/compiler/Transform.js +2 -1
  20. package/dist/compiler/Transform.js.map +1 -1
  21. package/dist/compiler/internal/Stylesheets.d.ts +8 -0
  22. package/dist/compiler/internal/Stylesheets.d.ts.map +1 -1
  23. package/dist/compiler/internal/Stylesheets.js +4 -0
  24. package/dist/compiler/internal/Stylesheets.js.map +1 -1
  25. package/dist/internal/Authoring.d.ts.map +1 -1
  26. package/dist/internal/Authoring.js +14 -0
  27. package/dist/internal/Authoring.js.map +1 -1
  28. package/dist/internal/Binding.d.ts +7 -0
  29. package/dist/internal/Binding.d.ts.map +1 -1
  30. package/dist/internal/Binding.js.map +1 -1
  31. package/dist/internal/Configuration.d.ts +1 -1
  32. package/dist/internal/Configuration.d.ts.map +1 -1
  33. package/dist/internal/Configuration.js +7 -6
  34. package/dist/internal/Configuration.js.map +1 -1
  35. package/dist/internal/Theme.d.ts +1 -1
  36. package/dist/internal/Theme.d.ts.map +1 -1
  37. package/dist/internal/Theme.js.map +1 -1
  38. package/dist/internal/Token.d.ts +3 -1
  39. package/dist/internal/Token.d.ts.map +1 -1
  40. package/dist/internal/Token.js +28 -3
  41. package/dist/internal/Token.js.map +1 -1
  42. package/dist/internal/Value.d.ts +22 -0
  43. package/dist/internal/Value.d.ts.map +1 -1
  44. package/dist/internal/Value.js +76 -9
  45. package/dist/internal/Value.js.map +1 -1
  46. package/dist/metro/Graph.d.ts +9 -1
  47. package/dist/metro/Graph.d.ts.map +1 -1
  48. package/dist/metro/Graph.js +5 -4
  49. package/dist/metro/Graph.js.map +1 -1
  50. package/dist/metro/transformer.d.ts +0 -1
  51. package/dist/metro/transformer.d.ts.map +1 -1
  52. package/dist/metro/transformer.js +19 -1
  53. package/dist/metro/transformer.js.map +1 -1
  54. package/dist/next/index.d.ts +3 -3
  55. package/dist/next/index.d.ts.map +1 -1
  56. package/dist/next/index.js +25 -30
  57. package/dist/next/index.js.map +1 -1
  58. package/dist/next/internal/Project.d.ts +28 -0
  59. package/dist/next/internal/Project.d.ts.map +1 -0
  60. package/dist/next/internal/Project.js +295 -0
  61. package/dist/next/internal/Project.js.map +1 -0
  62. package/dist/next/loader.d.ts +5 -6
  63. package/dist/next/loader.d.ts.map +1 -1
  64. package/dist/next/loader.js +161 -247
  65. package/dist/next/loader.js.map +1 -1
  66. package/dist/node/Host.d.ts.map +1 -1
  67. package/dist/node/Host.js +11 -3
  68. package/dist/node/Host.js.map +1 -1
  69. package/dist/node/internal/Snapshot.d.ts +12 -0
  70. package/dist/node/internal/Snapshot.d.ts.map +1 -0
  71. package/dist/node/internal/Snapshot.js +74 -0
  72. package/dist/node/internal/Snapshot.js.map +1 -0
  73. package/dist/unplugin/index.d.ts.map +1 -1
  74. package/dist/unplugin/index.js +10 -5
  75. package/dist/unplugin/index.js.map +1 -1
  76. package/dist/variants.d.ts +1 -1
  77. package/dist/variants.d.ts.map +1 -1
  78. package/dist/variants.js.map +1 -1
  79. package/dist/vite/index.d.ts.map +1 -1
  80. package/dist/vite/index.js +19 -4
  81. package/dist/vite/index.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/Style.ts +42 -8
  84. package/src/babel/index.ts +10 -4
  85. package/src/babel/internal/Compilation.ts +13 -0
  86. package/src/compiler/Graph.ts +213 -47
  87. package/src/compiler/Transform.ts +2 -1
  88. package/src/compiler/internal/Stylesheets.ts +9 -0
  89. package/src/internal/Authoring.ts +21 -0
  90. package/src/internal/Binding.ts +30 -0
  91. package/src/internal/Configuration.ts +19 -13
  92. package/src/internal/Theme.ts +5 -1
  93. package/src/internal/Token.ts +34 -3
  94. package/src/internal/Value.ts +180 -10
  95. package/src/metro/Graph.ts +5 -3
  96. package/src/metro/transformer.ts +26 -1
  97. package/src/next/index.ts +26 -31
  98. package/src/next/internal/Project.ts +378 -0
  99. package/src/next/loader.ts +195 -315
  100. package/src/node/Host.ts +15 -3
  101. package/src/node/internal/Snapshot.ts +79 -0
  102. package/src/unplugin/index.ts +10 -5
  103. package/src/variants.ts +1 -0
  104. package/src/vite/index.ts +24 -4
@@ -1,33 +1,26 @@
1
1
  /** Loads a physical source graph through Next.js resolution and emits cache-owned CSS. @module */
2
+ import * as AtRules from '../compiler/internal/AtRules.js'
2
3
  import * as Crypto from 'node:crypto'
3
4
  import * as Fs from 'node:fs/promises'
4
5
  import * as Path from 'node:path'
5
- import * as Parser from 'oxc-parser'
6
- import * as Graph from '../compiler/Graph.js'
7
- import * as Reset from '../node/Reset.js'
8
- import * as AtRules from '../compiler/internal/AtRules.js'
9
- import * as Contract from '../compiler/internal/Contract.js'
6
+ import * as Stylesheets from '../compiler/internal/Stylesheets.js'
7
+ import * as Project from './internal/Project.js'
10
8
 
11
- type Context = {
12
- addContextDependency(directory: string): void
13
- addDependency(file: string): void
9
+ type Context = Project.Context & {
14
10
  async(): (error: Error | null, code?: string, map?: object) => void
15
11
  getOptions(): {
16
12
  bundler: string
17
- mode: 'shared' | 'source' | 'style'
13
+ development?: boolean | undefined
14
+ mode?: 'style' | undefined
18
15
  reset?: boolean | undefined
19
16
  root: string
20
17
  }
21
- getResolve(
22
- options: object,
23
- ): (
24
- directory: string,
25
- specifier: string,
26
- callback: (error: Error | null, file?: string | false) => void,
27
- ) => void
28
- resourcePath: string
18
+ resourceQuery?: string | undefined
29
19
  }
30
20
 
21
+ // Next owns loader worker lifetimes. Each project retains only its latest graph in that worker.
22
+ const projects = new Map<string, ReturnType<typeof Project.create>>()
23
+
31
24
  /** Loader-runner boundary used by Webpack and Turbopack. */
32
25
  export default function loader(this: Context, source: string): void {
33
26
  const callback = this.async()
@@ -42,14 +35,15 @@ async function compile(context: Context, source: string) {
42
35
  const options = context.getOptions()
43
36
  const root = options.root
44
37
  const directory = Path.resolve(root, '.zyzz', 'next')
45
- const modules: Record<string, string> = Object.create(null)
46
- const imports: Record<string, Record<string, string | null>> = Object.create(
47
- null,
48
- )
49
- const contracts: Record<string, string> = Object.create(null)
50
- const resolve = context.getResolve({})
51
- const id = (file: string) =>
52
- `app/${Path.relative(root, file).split(Path.sep).join('/')}`
38
+ if (options.mode) {
39
+ const hash = new URLSearchParams(context.resourceQuery).get('zyzz')
40
+ if (!hash || !/^[a-f0-9]{64}$/.test(hash))
41
+ throw new Error('Invalid generated stylesheet request.')
42
+ const file = Path.join(directory, `${hash}.css`)
43
+ context.addDependency(file)
44
+ return { code: await Fs.readFile(file, 'utf8'), map: undefined }
45
+ }
46
+
53
47
  const eligible = (file: string) =>
54
48
  /\.[cm]?[jt]sx?$/.test(file) &&
55
49
  !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(file) &&
@@ -68,221 +62,25 @@ async function compile(context: Context, source: string) {
68
62
  )
69
63
  return { code: source, map: undefined }
70
64
 
71
- if (options.mode !== 'shared' && !eligible(context.resourcePath))
72
- return { code: source, map: undefined }
73
-
74
- async function visit(file: string, text?: string): Promise<void> {
75
- const name = id(file)
76
- if (Object.hasOwn(modules, name)) return
77
-
78
- context.addDependency(file)
79
- modules[name] = text ?? (await Fs.readFile(file, 'utf8'))
80
- const links: Record<string, string | null> = Object.create(null)
81
- imports[name] = links
82
- const parsed = Parser.parseSync(file, modules[name]!, {
83
- sourceType: 'module',
84
- })
85
-
86
- for (const node of parsed.program.body) {
87
- if (
88
- (node.type !== 'ImportDeclaration' &&
89
- node.type !== 'ExportNamedDeclaration' &&
90
- node.type !== 'ExportAllDeclaration') ||
91
- !node.source
92
- )
93
- continue
94
- if (
95
- node.type === 'ImportDeclaration'
96
- ? node.importKind === 'type'
97
- : node.exportKind === 'type'
98
- )
99
- continue
100
-
101
- const specifier = node.source.value
102
- links[specifier] = null
103
- // Next owns this virtual module, which has no physical stylesheet contract.
104
- if (specifier === 'next/root-params') continue
105
- if (
106
- specifier === 'zyzz' ||
107
- (specifier.startsWith('zyzz/') && specifier !== 'zyzz/default') ||
108
- specifier.startsWith('node:')
109
- )
110
- continue
111
-
112
- const resolved = await new Promise<string | false | undefined>(
113
- (accept, reject) => {
114
- resolve(Path.dirname(file), specifier, (error, value) =>
115
- error ? reject(error) : accept(value),
116
- )
117
- },
118
- )
119
- if (!resolved || !/\.[cm]?[jt]sx?$/.test(resolved)) continue
120
- if (eligible(resolved)) {
121
- links[specifier] = id(resolved)
122
- await visit(resolved)
123
- continue
124
- }
125
-
126
- const sidecar = `${resolved}.zyzz.json`
127
- try {
128
- contracts[resolved] = await Fs.readFile(sidecar, 'utf8')
129
- context.addDependency(sidecar)
130
- links[specifier] = resolved
131
- } catch (error) {
132
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
133
- }
134
- }
135
- }
136
-
137
- async function discover(directory: string): Promise<void> {
138
- const items = await Fs.readdir(directory, { withFileTypes: true })
139
-
140
- // Recursive tracking of a package root follows every installed dependency.
141
- // A dependency symlink back to an ancestor, as a package linked from its
142
- // own repository, is rejected as a loop, so such a root is tracked through
143
- // its files and subdirectories instead.
144
- if (
145
- !items.some((item) => item.name === 'node_modules') ||
146
- !(await loops(directory))
147
- )
148
- context.addContextDependency(directory)
65
+ if (!eligible(context.resourcePath)) return { code: source, map: undefined }
149
66
 
150
- for (const item of items) {
151
- if (
152
- item.name.startsWith('.') ||
153
- [
154
- 'node_modules',
155
- 'dist',
156
- 'build',
157
- 'coverage',
158
- 'test',
159
- 'tests',
160
- '__tests__',
161
- ].includes(item.name)
162
- )
163
- continue
164
- const file = Path.join(directory, item.name)
165
- if (item.isDirectory()) await discover(file)
166
- else if (item.isFile() && eligible(file)) await visit(file)
167
- }
168
- }
169
-
170
- if (options.mode !== 'shared') await visit(context.resourcePath, source)
171
- await discover(root)
172
- const loaded = new Set<string>()
173
- async function dependencies(file: string): Promise<void> {
174
- if (loaded.has(file)) return
175
- loaded.add(file)
176
-
177
- const metadata = Contract.read(contracts[file]!, new Map(), file)
178
- for (const section of metadata.stylesheets) {
179
- let owner = file
180
- for (const specifier of section.dependency ?? []) {
181
- const target = await new Promise<string | false | undefined>(
182
- (accept, reject) => {
183
- resolve(Path.dirname(owner), specifier, (error, value) =>
184
- error ? reject(error) : accept(value),
185
- )
186
- },
187
- )
188
- if (!target || !Path.isAbsolute(target))
189
- throw new Error('Unable to resolve packed stylesheet dependency.')
190
-
191
- const links = (imports[owner] ??= Object.create(null))
192
- links[specifier] = target
193
- if (!Object.hasOwn(contracts, target)) {
194
- const sidecar = `${target}.zyzz.json`
195
- contracts[target] = await Fs.readFile(sidecar, 'utf8')
196
- context.addDependency(sidecar)
197
- }
198
- await dependencies(target)
199
- owner = target
200
- }
201
- }
202
- }
203
- for (const file of Object.keys(contracts)) await dependencies(file)
204
-
205
- const graph = Graph.compile({
206
- contracts,
207
- imports,
208
- modules,
209
- reset: options.reset ? Reset.read() : undefined,
210
- })
211
- const output = graph.modules[id(context.resourcePath)]
212
-
213
- const assets = new Map<string, string>()
214
- for (const [placeholder, target] of Object.entries(
215
- graph.sharedAssets ?? {},
216
- )) {
217
- const raw = target.split(/[?#]/)[0]!
218
- const file = await Fs.realpath(
219
- target.startsWith('app/')
220
- ? Path.join(root, decodeURIComponent(raw.slice(4)))
221
- : decodeURIComponent(raw),
222
- )
223
- const identity = graph.sharedAssetOwners?.[placeholder]
224
- let owner = root
225
- if (identity && Path.isAbsolute(identity)) {
226
- owner = Path.dirname(identity)
227
- for (;;) {
228
- try {
229
- await Fs.access(Path.join(owner, 'package.json'))
230
- break
231
- } catch (error) {
232
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
233
- const parent = Path.dirname(owner)
234
- if (parent === owner)
235
- throw new Error('Packed assets require an owning package.json.')
236
- owner = parent
237
- }
238
- }
239
- }
240
-
241
- const relative = Path.relative(await Fs.realpath(owner), file)
242
- if (
243
- !identity ||
244
- relative === '..' ||
245
- relative.startsWith(`..${Path.sep}`) ||
246
- Path.isAbsolute(relative)
247
- )
248
- throw new Error('Asset path escapes its owning package.')
249
-
250
- context.addDependency(file)
251
- assets.set(
252
- placeholder,
253
- Path.relative(
254
- options.bundler === 'turbopack'
255
- ? Path.dirname(context.resourcePath)
256
- : directory,
257
- file,
258
- )
259
- .split(Path.sep)
260
- .map(encodeURIComponent)
261
- .join('/') + target.slice(raw.length),
262
- )
67
+ const key = JSON.stringify([
68
+ root,
69
+ options.bundler,
70
+ options.reset ?? false,
71
+ options.development ?? false,
72
+ ])
73
+ let project = projects.get(key)
74
+ if (!project) {
75
+ project = Project.create(options)
76
+ projects.set(key, project)
263
77
  }
78
+ const graph = await project.compile(context, source)
79
+ const output =
80
+ graph.modules[
81
+ `app/${Path.relative(root, context.resourcePath).split(Path.sep).join('/')}`
82
+ ]
264
83
 
265
- const shared = graph.sharedCss
266
- ? AtRules.transform({
267
- code: Buffer.from(graph.sharedCss),
268
- filename: 'shared.css',
269
- inputSourceMap: JSON.stringify(graph.sharedCssMap),
270
- sourceMap: true,
271
- visitor: {
272
- Url: (url) =>
273
- assets.has(url.url)
274
- ? { ...url, url: assets.get(url.url)! }
275
- : undefined,
276
- },
277
- })
278
- : undefined
279
- if (options.mode === 'shared')
280
- return {
281
- code: shared?.code.toString() ?? '',
282
- map: shared?.map
283
- ? (JSON.parse(shared.map.toString()) as object)
284
- : undefined,
285
- }
286
84
  if (!output) throw new Error('Missing Next.js source compilation.')
287
85
  // Bundlers resolve map sources relative to the loader resource, not graph identities.
288
86
  const sources = (names: readonly (string | null)[]) =>
@@ -295,63 +93,180 @@ async function compile(context: Context, source: string) {
295
93
  sources: sources(output.map.sources),
296
94
  }
297
95
  const cssMap = { ...output.cssMap, sources: sources(output.cssMap.sources) }
298
- if (options.mode === 'style') return { code: output.css, map: cssMap }
299
96
 
300
- if (options.bundler === 'turbopack') {
301
- const sharedFile = Path.relative(
302
- Path.dirname(context.resourcePath),
303
- Path.join(directory, 'shared.css'),
304
- )
305
- .split(Path.sep)
306
- .join('/')
307
- // Turbopack appends the output extension to loader-transformed resource paths.
308
- const resource = await Fs.stat(context.resourcePath).then(
309
- () => context.resourcePath,
310
- async (error: NodeJS.ErrnoException) => {
311
- if (error.code !== 'ENOENT') throw error
312
- const original = context.resourcePath.slice(
313
- 0,
314
- -Path.extname(context.resourcePath).length,
315
- )
316
- await Fs.access(original)
317
- return original
318
- },
319
- )
320
- const requests = [
321
- ...(graph.sharedCss
322
- ? [
323
- `import ${JSON.stringify(sharedFile.startsWith('../') ? sharedFile : `./${sharedFile}`)};`,
324
- ]
325
- : []),
326
- ...(output.css
327
- ? [
328
- `import ${JSON.stringify(`./${Path.basename(resource)}?zyzz-style`)};`,
329
- ]
330
- : []),
331
- ]
332
- return { code: `${output.code}\n${requests.join('\n')}`, map }
97
+ if (
98
+ output.code === source &&
99
+ !output.css &&
100
+ !/(?:^|[/\\])(?:layout|_app)\.[cm]?[jt]sx?$/.test(context.resourcePath) &&
101
+ !graph.sharedCssMap?.sources.some((name) => name !== 'zyzz/reset.css') &&
102
+ !graph[Stylesheets.packed]?.length
103
+ )
104
+ return { code: source, map }
105
+
106
+ const shared = await compileShared({
107
+ css: graph.sharedCss ?? '',
108
+ map: graph.sharedCssMap!,
109
+ assets: graph.sharedAssets ?? {},
110
+ owners: graph.sharedAssetOwners ?? {},
111
+ })
112
+
113
+ async function compileShared(
114
+ stylesheet: ReturnType<typeof Stylesheets.render>,
115
+ ) {
116
+ const assets = new Map<string, string>()
117
+ for (const [placeholder, target] of Object.entries(stylesheet.assets)) {
118
+ const raw = target.split(/[?#]/)[0]!
119
+ const file = await Fs.realpath(
120
+ target.startsWith('app/')
121
+ ? Path.join(root, decodeURIComponent(raw.slice(4)))
122
+ : decodeURIComponent(raw),
123
+ )
124
+ const identity = stylesheet.owners[placeholder]
125
+ let owner = root
126
+ if (identity && Path.isAbsolute(identity)) {
127
+ owner = Path.dirname(identity)
128
+ for (;;) {
129
+ try {
130
+ await Fs.access(Path.join(owner, 'package.json'))
131
+ break
132
+ } catch (error) {
133
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
134
+ const parent = Path.dirname(owner)
135
+ if (parent === owner)
136
+ throw new Error('Packed assets require an owning package.json.')
137
+ owner = parent
138
+ }
139
+ }
140
+ }
141
+
142
+ const relative = Path.relative(await Fs.realpath(owner), file)
143
+ if (
144
+ !identity ||
145
+ relative === '..' ||
146
+ relative.startsWith(`..${Path.sep}`) ||
147
+ Path.isAbsolute(relative)
148
+ )
149
+ throw new Error('Asset path escapes its owning package.')
150
+
151
+ context.addDependency(file)
152
+ assets.set(
153
+ placeholder,
154
+ Path.relative(directory, file)
155
+ .split(Path.sep)
156
+ .map(encodeURIComponent)
157
+ .join('/') + target.slice(raw.length),
158
+ )
159
+ }
160
+
161
+ return stylesheet.css
162
+ ? AtRules.transform({
163
+ code: Buffer.from(stylesheet.css),
164
+ filename: 'shared.css',
165
+ inputSourceMap: JSON.stringify({
166
+ ...stylesheet.map,
167
+ sources: sources(stylesheet.map.sources),
168
+ }),
169
+ sourceMap: true,
170
+ visitor: {
171
+ Url: (url) =>
172
+ assets.has(url.url)
173
+ ? { ...url, url: assets.get(url.url)! }
174
+ : undefined,
175
+ },
176
+ })
177
+ : undefined
333
178
  }
179
+
334
180
  const styles = [
335
- { css: shared?.code.toString(), map: shared?.map?.toString() },
336
- { css: output.css, map: JSON.stringify(cssMap) },
181
+ ...(await Promise.all(
182
+ (graph[Stylesheets.packed] ?? []).map(async (resource) => {
183
+ const output = await compileShared(resource)
184
+ return {
185
+ css: output?.code.toString(),
186
+ map: output?.map?.toString(),
187
+ id: resource.id,
188
+ }
189
+ }),
190
+ )),
191
+ {
192
+ id: undefined,
193
+ css: shared?.code.toString(),
194
+ map: shared?.map?.toString(),
195
+ },
196
+ { id: undefined, css: output.css, map: JSON.stringify(cssMap) },
337
197
  ]
338
198
  const requests: string[] = []
339
199
  await Fs.mkdir(directory, { recursive: true })
340
- for (const stylesheet of styles) {
200
+ try {
201
+ await Fs.writeFile(
202
+ Path.join(directory, 'package.json'),
203
+ JSON.stringify({ sideEffects: true, type: 'module' }),
204
+ { flag: 'wx' },
205
+ )
206
+ } catch (error) {
207
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
208
+ }
209
+ for (const [index, stylesheet] of styles.entries()) {
341
210
  if (!stylesheet.css) continue
342
211
  const css =
343
212
  stylesheet.css +
344
213
  (stylesheet.map
345
214
  ? `\n/*# sourceMappingURL=data:application/json;base64,${Buffer.from(stylesheet.map).toString('base64')} */`
346
215
  : '')
347
- const hash = Crypto.createHash('sha256').update(css).digest('hex')
216
+ const hash = Crypto.createHash('sha256')
217
+ .update(
218
+ options.development &&
219
+ !(
220
+ index === (graph[Stylesheets.packed]?.length ?? 0) &&
221
+ graph.sharedCssMap?.sources.every(
222
+ (name) => name === 'zyzz/reset.css',
223
+ )
224
+ )
225
+ ? (stylesheet.id ??
226
+ JSON.stringify([context.resourcePath, index, graph.dependencies]))
227
+ : css,
228
+ )
229
+ .digest('hex')
348
230
  const file = Path.join(directory, `${hash}.css`)
349
- try {
350
- await Fs.writeFile(file, css, { flag: 'wx' })
351
- } catch (error) {
352
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
231
+ if (options.development) {
232
+ const previous = await Fs.readFile(file, 'utf8').catch(
233
+ (error: NodeJS.ErrnoException) => {
234
+ if (error.code !== 'ENOENT') throw error
235
+ return undefined
236
+ },
237
+ )
238
+ if (previous !== css) {
239
+ const temporary = `${file}.${Crypto.randomUUID()}.tmp`
240
+ await Fs.writeFile(temporary, css)
241
+ await Fs.rename(temporary, file)
242
+ }
243
+ } else {
244
+ try {
245
+ await Fs.writeFile(file, css, { flag: 'wx' })
246
+ } catch (error) {
247
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
248
+ }
249
+ }
250
+ let request = file
251
+ if (options.development) {
252
+ // Server-component CSS needs a client graph edge for Next's native CSS HMR.
253
+ request = `${file}.js`
254
+ const stylesheet =
255
+ options.bundler === 'turbopack'
256
+ ? `./style.css?zyzz=${hash}`
257
+ : `./${hash}.css`
258
+ const code = `'use client';import ${JSON.stringify(stylesheet)};`
259
+ const existing = await Fs.readFile(request, 'utf8').catch(
260
+ (error: NodeJS.ErrnoException) => {
261
+ if (error.code !== 'ENOENT') throw error
262
+ return undefined
263
+ },
264
+ )
265
+ if (existing !== code) await Fs.writeFile(request, code)
266
+ } else if (options.bundler === 'turbopack') {
267
+ request = Path.join(directory, 'style.css') + `?zyzz=${hash}`
353
268
  }
354
- const relative = Path.relative(Path.dirname(context.resourcePath), file)
269
+ const relative = Path.relative(Path.dirname(context.resourcePath), request)
355
270
  .split(Path.sep)
356
271
  .join('/')
357
272
  requests.push(
@@ -361,38 +276,3 @@ async function compile(context: Context, source: string) {
361
276
 
362
277
  return { code: `${output.code}\n${requests.join('\n')}`, map }
363
278
  }
364
-
365
- /** Whether an installed dependency links back to the directory or one of its ancestors. */
366
- async function loops(directory: string): Promise<boolean> {
367
- const installed = Path.join(directory, 'node_modules')
368
- const entries = await Fs.readdir(installed, { withFileTypes: true }).catch(
369
- () => [],
370
- )
371
- const candidates = (
372
- await Promise.all(
373
- entries.map(async (entry) => {
374
- if (!entry.name.startsWith('@'))
375
- return [Path.join(installed, entry.name)]
376
-
377
- const scoped = Path.join(installed, entry.name)
378
-
379
- return (await Fs.readdir(scoped).catch(() => [])).map((name) =>
380
- Path.join(scoped, name),
381
- )
382
- }),
383
- )
384
- ).flat()
385
-
386
- for (const candidate of candidates) {
387
- const stat = await Fs.lstat(candidate).catch(() => undefined)
388
- if (!stat?.isSymbolicLink()) continue
389
-
390
- const target = await Fs.realpath(candidate).catch(() => undefined)
391
- if (target === undefined) continue
392
-
393
- const relative = Path.relative(target, directory)
394
- if (!relative.startsWith('..') && !Path.isAbsolute(relative)) return true
395
- }
396
-
397
- return false
398
- }
package/src/node/Host.ts CHANGED
@@ -13,6 +13,7 @@ import * as Graph from '../compiler/Graph.js'
13
13
  import * as AtRules from '../compiler/internal/AtRules.js'
14
14
  import * as Catalogs from '../compiler/internal/Catalogs.js'
15
15
  import * as Relative from '../compiler/internal/Relative.js'
16
+ import * as Snapshot from './internal/Snapshot.js'
16
17
  import * as Syntax from '../compiler/internal/Syntax.js'
17
18
  import * as Source from '../compiler/Source.js'
18
19
  import * as Transform from '../compiler/Transform.js'
@@ -124,6 +125,7 @@ export async function create(options: create.Options): Promise<Runtime> {
124
125
  const manifestPath = Path.join(outDir, '.zyzz.json')
125
126
 
126
127
  const compiler = Graph.create()
128
+ const snapshot = Snapshot.create()
127
129
  const stylesheets = new WeakMap<Transform.compile.ReturnType, Stylesheet>()
128
130
  let closed = false
129
131
  let closing: Promise<void> | undefined
@@ -250,7 +252,7 @@ export async function create(options: create.Options): Promise<Runtime> {
250
252
  `Source path conflicts with host control files: ${name}`,
251
253
  )
252
254
 
253
- sources[name] = await Fs.readFile(input, 'utf8')
255
+ sources[name] = await snapshot.read(input)
254
256
  }
255
257
 
256
258
  const modules = Object.fromEntries(
@@ -306,7 +308,7 @@ export async function create(options: create.Options): Promise<Runtime> {
306
308
 
307
309
  observe(`${file}.zyzz.json`)
308
310
  try {
309
- contracts[file] = await Fs.readFile(`${file}.zyzz.json`, 'utf8')
311
+ contracts[file] = await snapshot.read(`${file}.zyzz.json`)
310
312
  } catch (error) {
311
313
  if (
312
314
  !required &&
@@ -363,7 +365,7 @@ export async function create(options: create.Options): Promise<Runtime> {
363
365
  const file = Path.join(root, name)
364
366
  const resolutions = (imports[moduleId] = Object.create(null))
365
367
 
366
- for (const node of Syntax.parse({ moduleId, source }).program.body) {
368
+ for (const node of snapshot.parse({ moduleId, source }).program.body) {
367
369
  if (
368
370
  (node.type !== 'ImportDeclaration' &&
369
371
  node.type !== 'ExportNamedDeclaration' &&
@@ -439,6 +441,7 @@ export async function create(options: create.Options): Promise<Runtime> {
439
441
  }
440
442
 
441
443
  const graph = compiler.compile({
444
+ [Syntax.cache]: snapshot.programs(modules),
442
445
  compiler: options.compiler,
443
446
  native,
444
447
  modules,
@@ -446,6 +449,14 @@ export async function create(options: create.Options): Promise<Runtime> {
446
449
  imports,
447
450
  })
448
451
 
452
+ snapshot.retain(
453
+ new Set([
454
+ ...inputs,
455
+ ...Object.keys(contracts).map((file) => `${file}.zyzz.json`),
456
+ ]),
457
+ modules,
458
+ )
459
+
449
460
  const artifactNames = [
450
461
  'zyzz.css',
451
462
  'zyzz.css.map',
@@ -818,6 +829,7 @@ export async function create(options: create.Options): Promise<Runtime> {
818
829
 
819
830
  closing = (async () => {
820
831
  await tail
832
+ snapshot.clear()
821
833
  await lock.close()
822
834
  await Fs.rm(lockPath)
823
835
  })()