zyzz 0.0.7 → 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 (67) hide show
  1. package/dist/babel/index.d.ts +4 -1
  2. package/dist/babel/index.d.ts.map +1 -1
  3. package/dist/babel/index.js +6 -3
  4. package/dist/babel/index.js.map +1 -1
  5. package/dist/babel/internal/Compilation.d.ts +12 -0
  6. package/dist/babel/internal/Compilation.d.ts.map +1 -0
  7. package/dist/babel/internal/Compilation.js +3 -0
  8. package/dist/babel/internal/Compilation.js.map +1 -0
  9. package/dist/compiler/Graph.d.ts +6 -1
  10. package/dist/compiler/Graph.d.ts.map +1 -1
  11. package/dist/compiler/Graph.js +160 -36
  12. package/dist/compiler/Graph.js.map +1 -1
  13. package/dist/compiler/Transform.d.ts.map +1 -1
  14. package/dist/compiler/Transform.js +2 -1
  15. package/dist/compiler/Transform.js.map +1 -1
  16. package/dist/compiler/internal/Stylesheets.d.ts +8 -0
  17. package/dist/compiler/internal/Stylesheets.d.ts.map +1 -1
  18. package/dist/compiler/internal/Stylesheets.js +4 -0
  19. package/dist/compiler/internal/Stylesheets.js.map +1 -1
  20. package/dist/metro/Graph.d.ts +9 -1
  21. package/dist/metro/Graph.d.ts.map +1 -1
  22. package/dist/metro/Graph.js +5 -4
  23. package/dist/metro/Graph.js.map +1 -1
  24. package/dist/metro/transformer.d.ts +0 -1
  25. package/dist/metro/transformer.d.ts.map +1 -1
  26. package/dist/metro/transformer.js +19 -1
  27. package/dist/metro/transformer.js.map +1 -1
  28. package/dist/next/index.d.ts +3 -3
  29. package/dist/next/index.d.ts.map +1 -1
  30. package/dist/next/index.js +25 -30
  31. package/dist/next/index.js.map +1 -1
  32. package/dist/next/internal/Project.d.ts +4 -4
  33. package/dist/next/internal/Project.d.ts.map +1 -1
  34. package/dist/next/internal/Project.js +153 -107
  35. package/dist/next/internal/Project.js.map +1 -1
  36. package/dist/next/loader.d.ts +3 -1
  37. package/dist/next/loader.d.ts.map +1 -1
  38. package/dist/next/loader.js +152 -99
  39. package/dist/next/loader.js.map +1 -1
  40. package/dist/node/Host.d.ts.map +1 -1
  41. package/dist/node/Host.js +11 -3
  42. package/dist/node/Host.js.map +1 -1
  43. package/dist/node/internal/Snapshot.d.ts +12 -0
  44. package/dist/node/internal/Snapshot.d.ts.map +1 -0
  45. package/dist/node/internal/Snapshot.js +74 -0
  46. package/dist/node/internal/Snapshot.js.map +1 -0
  47. package/dist/unplugin/index.d.ts.map +1 -1
  48. package/dist/unplugin/index.js +10 -5
  49. package/dist/unplugin/index.js.map +1 -1
  50. package/dist/vite/index.d.ts.map +1 -1
  51. package/dist/vite/index.js +19 -4
  52. package/dist/vite/index.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/babel/index.ts +10 -4
  55. package/src/babel/internal/Compilation.ts +13 -0
  56. package/src/compiler/Graph.ts +213 -47
  57. package/src/compiler/Transform.ts +2 -1
  58. package/src/compiler/internal/Stylesheets.ts +9 -0
  59. package/src/metro/Graph.ts +5 -3
  60. package/src/metro/transformer.ts +26 -1
  61. package/src/next/index.ts +26 -31
  62. package/src/next/internal/Project.ts +196 -155
  63. package/src/next/loader.ts +181 -124
  64. package/src/node/Host.ts +15 -3
  65. package/src/node/internal/Snapshot.ts +79 -0
  66. package/src/unplugin/index.ts +10 -5
  67. package/src/vite/index.ts +24 -4
@@ -2,15 +2,13 @@
2
2
  import * as Contract from '../../compiler/internal/Contract.js'
3
3
  import * as Fs from 'node:fs/promises'
4
4
  import * as Graph from '../../compiler/Graph.js'
5
- import type * as NativeFs from 'node:fs'
6
5
  import * as Path from 'node:path'
7
6
  import * as Reset from '../../node/Reset.js'
7
+ import * as Stylesheets from '../../compiler/internal/Stylesheets.js'
8
8
  import * as Syntax from '../../compiler/internal/Syntax.js'
9
9
 
10
10
  /** Resolution and dependency notifications supplied by the active loader invocation. */
11
11
  export type Context = {
12
- /** Track graph membership changes. */
13
- addContextDependency(directory: string): void
14
12
  /** Track source and contract edits. */
15
13
  addDependency(file: string): void
16
14
  /** Resolve with the active bundler's aliases and conditions. */
@@ -28,10 +26,6 @@ export type Context = {
28
26
  /** Creates project state owned by a loader configuration in one worker process. */
29
27
  export function create(options: create.Options) {
30
28
  let compiler = Graph.create()
31
- const directories = new Map<
32
- string,
33
- { items: NativeFs.Dirent[]; track: boolean; version: string }
34
- >()
35
29
  const files = new Map<string, { source: string; version: string }>()
36
30
  const libraries = new Map<
37
31
  string,
@@ -46,18 +40,19 @@ export function create(options: create.Options) {
46
40
 
47
41
  return {
48
42
  /** Compiles a fresh snapshot while retaining unchanged syntax and graph results. */
49
- compile(context: Context, source?: string) {
43
+ compile(context: Context, source: string) {
50
44
  // Loader calls may overlap, but only one immutable snapshot updates the compiler at a time.
51
45
  const result = pending.then(async () => {
46
+ const tracked = new Set<string>()
52
47
  try {
53
- return await compile(context, source)
48
+ return await compile(context, source, tracked)
54
49
  } catch (error) {
55
- // Failed discovery or extraction may leave a partial filesystem snapshot.
50
+ // Failed resolution or extraction may leave a partial filesystem snapshot.
56
51
  compiler = Graph.create()
57
- directories.clear()
58
52
  files.clear()
59
53
  libraries.clear()
60
54
  syntax.clear()
55
+ for (const file of tracked) context.addDependency(file)
61
56
  throw error
62
57
  }
63
58
  })
@@ -72,19 +67,37 @@ export function create(options: create.Options) {
72
67
  },
73
68
  }
74
69
 
75
- async function compile(context: Context, source?: string) {
70
+ async function compile(
71
+ context: Context,
72
+ source: string,
73
+ tracked: Set<string>,
74
+ ) {
76
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
+ }
77
81
  const programs = new Map<string, ReturnType<typeof Syntax.parse>>()
78
- const seenDirectories = new Set<string>()
79
- const seenFiles = new Set<string>()
80
- async function read(file: string) {
81
- seenFiles.add(file)
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) {
82
93
  const version = await stamp(file)
83
94
  let entry = files.get(file)
84
95
  if (entry?.version !== version) {
85
96
  entry = { source: await Fs.readFile(file, 'utf8'), version }
86
- files.set(file, entry)
87
97
  }
98
+ files.delete(file)
99
+ files.set(file, entry)
100
+ if (files.size > 256) files.delete(files.keys().next().value!)
88
101
  return entry.source
89
102
  }
90
103
 
@@ -94,7 +107,21 @@ export function create(options: create.Options) {
94
107
  Record<string, string | null>
95
108
  > = Object.create(null)
96
109
  const contracts: Record<string, string> = Object.create(null)
97
- const resolve = context.getResolve({})
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
+ }
98
125
  const id = (file: string) =>
99
126
  `app/${Path.relative(root, file).split(Path.sep).join('/')}`
100
127
  const eligible = (file: string) =>
@@ -107,70 +134,148 @@ export function create(options: create.Options) {
107
134
  part === '..' || part === 'node_modules' || part.startsWith('.'),
108
135
  )
109
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
+
110
147
  async function visit(file: string, text?: string): Promise<void> {
111
148
  const name = id(file)
112
149
  if (Object.hasOwn(modules, name)) return
113
150
 
114
- context.addDependency(file)
115
- seenFiles.add(file)
151
+ track(file)
116
152
  modules[name] = text ?? (await read(file))
117
153
  const links: Record<string, string | null> = Object.create(null)
118
154
  imports[name] = links
119
- let entry = syntax.get(name)
120
- if (entry?.source !== modules[name]) {
121
- entry = {
122
- source: modules[name]!,
123
- parsed: Syntax.parse({ moduleId: name, source: modules[name]! }),
124
- }
125
- syntax.set(name, entry)
126
- }
127
- programs.set(name, entry.parsed)
128
- const parsed = entry.parsed
155
+ const parsed = parse(name, modules[name]!)
156
+ programs.set(name, parsed)
129
157
 
130
- for (const node of parsed.program.body) {
131
- if (
132
- (node.type !== 'ImportDeclaration' &&
133
- node.type !== 'ExportNamedDeclaration' &&
134
- node.type !== 'ExportAllDeclaration') ||
135
- !node.source
136
- )
137
- continue
138
- if (
139
- node.type === 'ImportDeclaration'
140
- ? node.importKind === 'type'
141
- : node.exportKind === 'type'
142
- )
143
- continue
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
144
173
 
145
- const specifier = node.source.value
146
- links[specifier] = null
147
- // Next owns this virtual module, which has no physical stylesheet contract.
148
- if (specifier === 'next/root-params') continue
149
- if (
150
- specifier === 'zyzz' ||
151
- (specifier.startsWith('zyzz/') && specifier !== 'zyzz/default') ||
152
- specifier.startsWith('node:')
153
- )
154
- continue
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
155
184
 
156
- const resolved = await new Promise<string | false | undefined>(
157
- (accept, reject) => {
158
- resolve(Path.dirname(file), specifier, (error, value) =>
159
- error ? reject(error) : accept(value),
160
- )
161
- },
162
- )
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
163
192
  if (!resolved || !/\.[cm]?[jt]sx?$/.test(resolved)) continue
164
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
+ }
165
271
  links[specifier] = id(resolved)
166
272
  await visit(resolved)
167
273
  continue
168
274
  }
169
-
170
275
  const sidecar = `${resolved}.zyzz.json`
171
276
  try {
172
277
  contracts[resolved] = await read(sidecar)
173
- context.addDependency(sidecar)
278
+ track(sidecar)
174
279
  links[specifier] = resolved
175
280
  } catch (error) {
176
281
  if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
@@ -178,48 +283,7 @@ export function create(options: create.Options) {
178
283
  }
179
284
  }
180
285
 
181
- async function discover(directory: string): Promise<void> {
182
- const version = await stamp(directory)
183
- let cached = directories.get(directory)
184
- if (cached?.version !== version) {
185
- const items = await Fs.readdir(directory, { withFileTypes: true })
186
- cached = {
187
- items,
188
- track:
189
- !items.some((item) => item.name === 'node_modules') ||
190
- !(await loops(directory)),
191
- version,
192
- }
193
- directories.set(directory, cached)
194
- }
195
- seenDirectories.add(directory)
196
- const items = cached.items
197
-
198
- // Linked package roots are tracked through children to avoid recursive dependency loops.
199
- if (cached.track) context.addContextDependency(directory)
200
-
201
- for (const item of items) {
202
- if (
203
- item.name.startsWith('.') ||
204
- [
205
- 'node_modules',
206
- 'dist',
207
- 'build',
208
- 'coverage',
209
- 'test',
210
- 'tests',
211
- '__tests__',
212
- ].includes(item.name)
213
- )
214
- continue
215
- const file = Path.join(directory, item.name)
216
- if (item.isDirectory()) await discover(file)
217
- else if (item.isFile() && eligible(file)) await visit(file)
218
- }
219
- }
220
-
221
- if (source !== undefined) await visit(context.resourcePath, source)
222
- await discover(root)
286
+ await visit(context.resourcePath, source)
223
287
  const loaded = new Set<string>()
224
288
  async function dependencies(file: string): Promise<void> {
225
289
  if (loaded.has(file)) return
@@ -240,13 +304,7 @@ export function create(options: create.Options) {
240
304
  for (const dependency of library.dependencies) {
241
305
  let owner = file
242
306
  for (const specifier of dependency) {
243
- const target = await new Promise<string | false | undefined>(
244
- (accept, reject) => {
245
- resolve(Path.dirname(owner), specifier, (error, value) =>
246
- error ? reject(error) : accept(value),
247
- )
248
- },
249
- )
307
+ const target = await resolve(Path.dirname(owner), specifier)
250
308
  if (!target || !Path.isAbsolute(target))
251
309
  throw new Error('Unable to resolve packed stylesheet dependency.')
252
310
 
@@ -255,7 +313,7 @@ export function create(options: create.Options) {
255
313
  if (!Object.hasOwn(contracts, target)) {
256
314
  const sidecar = `${target}.zyzz.json`
257
315
  contracts[target] = await read(sidecar)
258
- context.addDependency(sidecar)
316
+ track(sidecar)
259
317
  }
260
318
  await dependencies(target)
261
319
  owner = target
@@ -266,20 +324,36 @@ export function create(options: create.Options) {
266
324
 
267
325
  const graph = compiler.compile({
268
326
  [Syntax.cache]: programs,
327
+ [Stylesheets.entry]: id(context.resourcePath),
269
328
  contracts,
329
+ development: options.development,
270
330
  imports,
271
331
  modules,
272
332
  reset,
273
333
  })
274
334
 
275
- for (const file of files.keys())
276
- if (!seenFiles.has(file)) files.delete(file)
277
- for (const file of directories.keys())
278
- if (!seenDirectories.has(file)) directories.delete(file)
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
+
279
355
  for (const file of libraries.keys())
280
356
  if (!Object.hasOwn(contracts, file)) libraries.delete(file)
281
- for (const name of syntax.keys())
282
- if (!programs.has(name)) syntax.delete(name)
283
357
 
284
358
  return graph
285
359
  }
@@ -287,8 +361,10 @@ export function create(options: create.Options) {
287
361
 
288
362
  /** Project cache settings. */
289
363
  export declare namespace create {
290
- /** Isolates source discovery and shared stylesheet options. */
364
+ /** Isolates imported source graphs and shared stylesheet options. */
291
365
  type Options = {
366
+ /** Keep development class names stable across value edits. */
367
+ readonly development?: boolean | undefined
292
368
  /** Include the packaged reset in shared CSS. */
293
369
  readonly reset?: boolean | undefined
294
370
  /** Absolute application directory. */
@@ -300,38 +376,3 @@ async function stamp(file: string) {
300
376
  const stat = await Fs.stat(file, { bigint: true })
301
377
  return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`
302
378
  }
303
-
304
- /** Whether an installed dependency links back to the directory or one of its ancestors. */
305
- async function loops(directory: string): Promise<boolean> {
306
- const installed = Path.join(directory, 'node_modules')
307
- const entries = await Fs.readdir(installed, { withFileTypes: true }).catch(
308
- () => [],
309
- )
310
- const candidates = (
311
- await Promise.all(
312
- entries.map(async (entry) => {
313
- if (!entry.name.startsWith('@'))
314
- return [Path.join(installed, entry.name)]
315
-
316
- const scoped = Path.join(installed, entry.name)
317
-
318
- return (await Fs.readdir(scoped).catch(() => [])).map((name) =>
319
- Path.join(scoped, name),
320
- )
321
- }),
322
- )
323
- ).flat()
324
-
325
- for (const candidate of candidates) {
326
- const stat = await Fs.lstat(candidate).catch(() => undefined)
327
- if (!stat?.isSymbolicLink()) continue
328
-
329
- const target = await Fs.realpath(candidate).catch(() => undefined)
330
- if (target === undefined) continue
331
-
332
- const relative = Path.relative(target, directory)
333
- if (!relative.startsWith('..') && !Path.isAbsolute(relative)) return true
334
- }
335
-
336
- return false
337
- }