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
@@ -0,0 +1,378 @@
1
+ /** Retains filesystem snapshots and an incremental graph within a Next.js loader worker. @module */
2
+ import * as Contract from '../../compiler/internal/Contract.js'
3
+ import * as Fs from 'node:fs/promises'
4
+ import * as Graph from '../../compiler/Graph.js'
5
+ import * as Path from 'node:path'
6
+ import * as Reset from '../../node/Reset.js'
7
+ import * as Stylesheets from '../../compiler/internal/Stylesheets.js'
8
+ import * as Syntax from '../../compiler/internal/Syntax.js'
9
+
10
+ /** Resolution and dependency notifications supplied by the active loader invocation. */
11
+ export type Context = {
12
+ /** Track source and contract edits. */
13
+ addDependency(file: string): void
14
+ /** Resolve with the active bundler's aliases and conditions. */
15
+ getResolve(
16
+ options: object,
17
+ ): (
18
+ directory: string,
19
+ specifier: string,
20
+ callback: (error: Error | null, file?: string | false) => void,
21
+ ) => void
22
+ /** Physical loader resource. */
23
+ resourcePath: string
24
+ }
25
+
26
+ /** Creates project state owned by a loader configuration in one worker process. */
27
+ export function create(options: create.Options) {
28
+ let compiler = Graph.create()
29
+ const files = new Map<string, { source: string; version: string }>()
30
+ const libraries = new Map<
31
+ string,
32
+ { dependencies: readonly (readonly string[])[]; source: string }
33
+ >()
34
+ const reset = options.reset ? Reset.read() : undefined
35
+ const syntax = new Map<
36
+ string,
37
+ { parsed: ReturnType<typeof Syntax.parse>; source: string }
38
+ >()
39
+ let pending = Promise.resolve()
40
+
41
+ return {
42
+ /** Compiles a fresh snapshot while retaining unchanged syntax and graph results. */
43
+ compile(context: Context, source: string) {
44
+ // Loader calls may overlap, but only one immutable snapshot updates the compiler at a time.
45
+ const result = pending.then(async () => {
46
+ const tracked = new Set<string>()
47
+ try {
48
+ return await compile(context, source, tracked)
49
+ } catch (error) {
50
+ // Failed resolution or extraction may leave a partial filesystem snapshot.
51
+ compiler = Graph.create()
52
+ files.clear()
53
+ libraries.clear()
54
+ syntax.clear()
55
+ for (const file of tracked) context.addDependency(file)
56
+ throw error
57
+ }
58
+ })
59
+
60
+ // A failed generation must not prevent recovery after the source is corrected.
61
+ pending = result.then(
62
+ () => {},
63
+ () => {},
64
+ )
65
+
66
+ return result
67
+ },
68
+ }
69
+
70
+ async function compile(
71
+ context: Context,
72
+ source: string,
73
+ tracked: Set<string>,
74
+ ) {
75
+ const root = options.root
76
+ const runtime = new Set<string>()
77
+ function track(file: string) {
78
+ if (tracked.has(file)) return
79
+ tracked.add(file)
80
+ }
81
+ const programs = new Map<string, ReturnType<typeof Syntax.parse>>()
82
+ const reads = new Map<string, Promise<string>>()
83
+ function read(file: string) {
84
+ let pending = reads.get(file)
85
+ if (!pending) {
86
+ pending = load(file)
87
+ reads.set(file, pending)
88
+ }
89
+ return pending
90
+ }
91
+
92
+ async function load(file: string) {
93
+ const version = await stamp(file)
94
+ let entry = files.get(file)
95
+ if (entry?.version !== version) {
96
+ entry = { source: await Fs.readFile(file, 'utf8'), version }
97
+ }
98
+ files.delete(file)
99
+ files.set(file, entry)
100
+ if (files.size > 256) files.delete(files.keys().next().value!)
101
+ return entry.source
102
+ }
103
+
104
+ const modules: Record<string, string> = Object.create(null)
105
+ const imports: Record<
106
+ string,
107
+ Record<string, string | null>
108
+ > = Object.create(null)
109
+ const contracts: Record<string, string> = Object.create(null)
110
+ const resolver = context.getResolve({})
111
+ const resolutions = new Map<string, Promise<string | false | undefined>>()
112
+ function resolve(directory: string, specifier: string) {
113
+ const key = JSON.stringify([directory, specifier])
114
+ let pending = resolutions.get(key)
115
+ if (!pending) {
116
+ pending = new Promise<string | false | undefined>((accept, reject) => {
117
+ resolver(directory, specifier, (error, value) =>
118
+ error ? reject(error) : accept(value),
119
+ )
120
+ })
121
+ resolutions.set(key, pending)
122
+ }
123
+ return pending
124
+ }
125
+ const id = (file: string) =>
126
+ `app/${Path.relative(root, file).split(Path.sep).join('/')}`
127
+ const eligible = (file: string) =>
128
+ /\.[cm]?[jt]sx?$/.test(file) &&
129
+ !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(file) &&
130
+ !Path.relative(root, file)
131
+ .split(Path.sep)
132
+ .some(
133
+ (part) =>
134
+ part === '..' || part === 'node_modules' || part.startsWith('.'),
135
+ )
136
+
137
+ function parse(name: string, source: string) {
138
+ let entry = syntax.get(name)
139
+ if (entry?.source !== source)
140
+ entry = { source, parsed: Syntax.parse({ moduleId: name, source }) }
141
+ syntax.delete(name)
142
+ syntax.set(name, entry)
143
+ if (syntax.size > 256) syntax.delete(syntax.keys().next().value!)
144
+ return entry.parsed
145
+ }
146
+
147
+ async function visit(file: string, text?: string): Promise<void> {
148
+ const name = id(file)
149
+ if (Object.hasOwn(modules, name)) return
150
+
151
+ track(file)
152
+ modules[name] = text ?? (await read(file))
153
+ const links: Record<string, string | null> = Object.create(null)
154
+ imports[name] = links
155
+ const parsed = parse(name, modules[name]!)
156
+ programs.set(name, parsed)
157
+
158
+ const resolutions = await Promise.all(
159
+ parsed.program.body.map(async (node) => {
160
+ if (
161
+ (node.type !== 'ImportDeclaration' &&
162
+ node.type !== 'ExportNamedDeclaration' &&
163
+ node.type !== 'ExportAllDeclaration') ||
164
+ !node.source
165
+ )
166
+ return undefined
167
+ if (
168
+ node.type === 'ImportDeclaration'
169
+ ? node.importKind === 'type'
170
+ : node.exportKind === 'type'
171
+ )
172
+ return undefined
173
+
174
+ const specifier = node.source.value
175
+ links[specifier] = null
176
+ // Next owns this virtual module, which has no physical stylesheet contract.
177
+ if (specifier === 'next/root-params') return undefined
178
+ if (
179
+ specifier === 'zyzz' ||
180
+ (specifier.startsWith('zyzz/') && specifier !== 'zyzz/default') ||
181
+ specifier.startsWith('node:')
182
+ )
183
+ return undefined
184
+
185
+ const resolved = await resolve(Path.dirname(file), specifier)
186
+ return { node, specifier, resolved }
187
+ }),
188
+ )
189
+ for (const resolution of resolutions) {
190
+ if (!resolution) continue
191
+ const { node, specifier, resolved } = resolution
192
+ if (!resolved || !/\.[cm]?[jt]sx?$/.test(resolved)) continue
193
+ if (eligible(resolved)) {
194
+ if (node.type === 'ImportDeclaration' && node.specifiers.length) {
195
+ track(resolved)
196
+ const parsed = parse(id(resolved), await read(resolved))
197
+ const functions = new Set<string>()
198
+ let defaultBinding: string | undefined
199
+ for (const statement of parsed.program.body) {
200
+ if (
201
+ statement.type === 'ExportDefaultDeclaration' &&
202
+ [
203
+ 'FunctionDeclaration',
204
+ 'ClassDeclaration',
205
+ 'ArrowFunctionExpression',
206
+ 'FunctionExpression',
207
+ ].includes(statement.declaration.type)
208
+ ) {
209
+ functions.add('default')
210
+ if (
211
+ statement.declaration.type === 'FunctionDeclaration' ||
212
+ statement.declaration.type === 'ClassDeclaration'
213
+ )
214
+ defaultBinding = statement.declaration.id?.name
215
+ }
216
+ if (statement.type !== 'ExportNamedDeclaration') continue
217
+ const declaration = statement.declaration
218
+ if (
219
+ (declaration?.type === 'FunctionDeclaration' ||
220
+ declaration?.type === 'ClassDeclaration') &&
221
+ declaration.id
222
+ )
223
+ functions.add(declaration.id.name)
224
+ if (declaration?.type === 'VariableDeclaration')
225
+ for (const item of declaration.declarations) {
226
+ if (
227
+ item.id.type === 'Identifier' &&
228
+ item.init &&
229
+ [
230
+ 'ArrowFunctionExpression',
231
+ 'FunctionExpression',
232
+ 'ClassExpression',
233
+ ].includes(item.init.type)
234
+ )
235
+ functions.add(item.id.name)
236
+ }
237
+ }
238
+ for (const statement of parsed.program.body) {
239
+ const declaration =
240
+ statement.type === 'ExportNamedDeclaration'
241
+ ? statement.declaration
242
+ : statement
243
+ if (
244
+ declaration?.type === 'TSModuleDeclaration' &&
245
+ declaration.id.type === 'Identifier'
246
+ ) {
247
+ functions.delete(declaration.id.name)
248
+ if (declaration.id.name === defaultBinding)
249
+ functions.delete('default')
250
+ }
251
+ }
252
+ if (
253
+ !parsed.errors.length &&
254
+ node.specifiers.every((specifier) =>
255
+ specifier.type === 'ImportDefaultSpecifier'
256
+ ? functions.has('default')
257
+ : specifier.type === 'ImportSpecifier' &&
258
+ (specifier.importKind === 'type' ||
259
+ functions.has(
260
+ specifier.imported.type === 'Identifier'
261
+ ? specifier.imported.name
262
+ : specifier.imported.value,
263
+ )),
264
+ )
265
+ ) {
266
+ runtime.add(resolved)
267
+ track(resolved)
268
+ continue
269
+ }
270
+ }
271
+ links[specifier] = id(resolved)
272
+ await visit(resolved)
273
+ continue
274
+ }
275
+ const sidecar = `${resolved}.zyzz.json`
276
+ try {
277
+ contracts[resolved] = await read(sidecar)
278
+ track(sidecar)
279
+ links[specifier] = resolved
280
+ } catch (error) {
281
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
282
+ }
283
+ }
284
+ }
285
+
286
+ await visit(context.resourcePath, source)
287
+ const loaded = new Set<string>()
288
+ async function dependencies(file: string): Promise<void> {
289
+ if (loaded.has(file)) return
290
+ loaded.add(file)
291
+
292
+ let library = libraries.get(file)
293
+ if (!library || library.source !== contracts[file]) {
294
+ library = {
295
+ source: contracts[file]!,
296
+ dependencies: Contract.read(
297
+ contracts[file]!,
298
+ new Map(),
299
+ file,
300
+ ).stylesheets.map((section) => section.dependency ?? []),
301
+ }
302
+ libraries.set(file, library)
303
+ }
304
+ for (const dependency of library.dependencies) {
305
+ let owner = file
306
+ for (const specifier of dependency) {
307
+ const target = await resolve(Path.dirname(owner), specifier)
308
+ if (!target || !Path.isAbsolute(target))
309
+ throw new Error('Unable to resolve packed stylesheet dependency.')
310
+
311
+ const links = (imports[owner] ??= Object.create(null))
312
+ links[specifier] = target
313
+ if (!Object.hasOwn(contracts, target)) {
314
+ const sidecar = `${target}.zyzz.json`
315
+ contracts[target] = await read(sidecar)
316
+ track(sidecar)
317
+ }
318
+ await dependencies(target)
319
+ owner = target
320
+ }
321
+ }
322
+ }
323
+ for (const file of Object.keys(contracts)) await dependencies(file)
324
+
325
+ const graph = compiler.compile({
326
+ [Syntax.cache]: programs,
327
+ [Stylesheets.entry]: id(context.resourcePath),
328
+ contracts,
329
+ development: options.development,
330
+ imports,
331
+ modules,
332
+ reset,
333
+ })
334
+
335
+ const entry = id(context.resourcePath)
336
+ const output = graph.modules[entry]!
337
+ // Unchanged runtime modules rely on Next's import graph, not transitive style inputs.
338
+ if (output.code === source && !output.css) {
339
+ context.addDependency(context.resourcePath)
340
+ for (const file of runtime) context.addDependency(file)
341
+ for (const file of tracked)
342
+ if (file.endsWith('.zyzz.json')) context.addDependency(file)
343
+ for (const target of Object.values(imports[entry] ?? {})) {
344
+ if (!target) continue
345
+ context.addDependency(
346
+ target.startsWith('app/')
347
+ ? Path.join(root, target.slice(4))
348
+ : `${target}.zyzz.json`,
349
+ )
350
+ }
351
+ } else {
352
+ for (const file of tracked) context.addDependency(file)
353
+ }
354
+
355
+ for (const file of libraries.keys())
356
+ if (!Object.hasOwn(contracts, file)) libraries.delete(file)
357
+
358
+ return graph
359
+ }
360
+ }
361
+
362
+ /** Project cache settings. */
363
+ export declare namespace create {
364
+ /** Isolates imported source graphs and shared stylesheet options. */
365
+ type Options = {
366
+ /** Keep development class names stable across value edits. */
367
+ readonly development?: boolean | undefined
368
+ /** Include the packaged reset in shared CSS. */
369
+ readonly reset?: boolean | undefined
370
+ /** Absolute application directory. */
371
+ readonly root: string
372
+ }
373
+ }
374
+
375
+ async function stamp(file: string) {
376
+ const stat = await Fs.stat(file, { bigint: true })
377
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`
378
+ }