zyzz 0.0.5 → 0.0.7

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 (91) 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 +2 -0
  7. package/dist/babel/index.d.ts.map +1 -1
  8. package/dist/babel/index.js +10 -1
  9. package/dist/babel/index.js.map +1 -1
  10. package/dist/compiler/Graph.d.ts +2 -0
  11. package/dist/compiler/Graph.d.ts.map +1 -1
  12. package/dist/compiler/Graph.js +19 -1
  13. package/dist/compiler/Graph.js.map +1 -1
  14. package/dist/compiler/Source.d.ts.map +1 -1
  15. package/dist/compiler/Source.js +5 -2
  16. package/dist/compiler/Source.js.map +1 -1
  17. package/dist/compiler/internal/Contributions.d.ts +1 -1
  18. package/dist/compiler/internal/Contributions.d.ts.map +1 -1
  19. package/dist/compiler/internal/Contributions.js +5 -3
  20. package/dist/compiler/internal/Contributions.js.map +1 -1
  21. package/dist/compiler/internal/Static.d.ts.map +1 -1
  22. package/dist/compiler/internal/Static.js +2 -0
  23. package/dist/compiler/internal/Static.js.map +1 -1
  24. package/dist/internal/Authoring.d.ts.map +1 -1
  25. package/dist/internal/Authoring.js +14 -0
  26. package/dist/internal/Authoring.js.map +1 -1
  27. package/dist/internal/Binding.d.ts +7 -0
  28. package/dist/internal/Binding.d.ts.map +1 -1
  29. package/dist/internal/Binding.js.map +1 -1
  30. package/dist/internal/Configuration.d.ts +1 -1
  31. package/dist/internal/Configuration.d.ts.map +1 -1
  32. package/dist/internal/Configuration.js +7 -6
  33. package/dist/internal/Configuration.js.map +1 -1
  34. package/dist/internal/Theme.d.ts +1 -1
  35. package/dist/internal/Theme.d.ts.map +1 -1
  36. package/dist/internal/Theme.js.map +1 -1
  37. package/dist/internal/Token.d.ts +3 -1
  38. package/dist/internal/Token.d.ts.map +1 -1
  39. package/dist/internal/Token.js +28 -3
  40. package/dist/internal/Token.js.map +1 -1
  41. package/dist/internal/Value.d.ts +22 -0
  42. package/dist/internal/Value.d.ts.map +1 -1
  43. package/dist/internal/Value.js +76 -9
  44. package/dist/internal/Value.js.map +1 -1
  45. package/dist/next/index.d.ts +8 -3
  46. package/dist/next/index.d.ts.map +1 -1
  47. package/dist/next/index.js +21 -10
  48. package/dist/next/index.js.map +1 -1
  49. package/dist/next/internal/Project.d.ts +28 -0
  50. package/dist/next/internal/Project.d.ts.map +1 -0
  51. package/dist/next/internal/Project.js +249 -0
  52. package/dist/next/internal/Project.js.map +1 -0
  53. package/dist/next/loader.d.ts +3 -5
  54. package/dist/next/loader.d.ts.map +1 -1
  55. package/dist/next/loader.js +14 -144
  56. package/dist/next/loader.js.map +1 -1
  57. package/dist/node/Reset.d.ts +3 -0
  58. package/dist/node/Reset.d.ts.map +1 -0
  59. package/dist/node/Reset.js +7 -0
  60. package/dist/node/Reset.js.map +1 -0
  61. package/dist/unplugin/index.d.ts +2 -0
  62. package/dist/unplugin/index.d.ts.map +1 -1
  63. package/dist/unplugin/index.js +2 -0
  64. package/dist/unplugin/index.js.map +1 -1
  65. package/dist/variants.d.ts +1 -1
  66. package/dist/variants.d.ts.map +1 -1
  67. package/dist/variants.js.map +1 -1
  68. package/dist/vite/index.d.ts +2 -0
  69. package/dist/vite/index.d.ts.map +1 -1
  70. package/dist/vite/index.js +38 -9
  71. package/dist/vite/index.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/Style.ts +42 -8
  74. package/src/babel/index.ts +23 -1
  75. package/src/compiler/Graph.ts +22 -0
  76. package/src/compiler/Source.ts +5 -1
  77. package/src/compiler/internal/Contributions.ts +5 -2
  78. package/src/compiler/internal/Static.ts +1 -0
  79. package/src/internal/Authoring.ts +21 -0
  80. package/src/internal/Binding.ts +30 -0
  81. package/src/internal/Configuration.ts +19 -13
  82. package/src/internal/Theme.ts +5 -1
  83. package/src/internal/Token.ts +34 -3
  84. package/src/internal/Value.ts +180 -10
  85. package/src/next/index.ts +34 -12
  86. package/src/next/internal/Project.ts +337 -0
  87. package/src/next/loader.ts +28 -190
  88. package/src/node/Reset.ts +7 -0
  89. package/src/unplugin/index.ts +4 -0
  90. package/src/variants.ts +1 -0
  91. package/src/vite/index.ts +43 -14
@@ -1,31 +1,23 @@
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 AtRules from '../compiler/internal/AtRules.js'
8
- import * as Contract from '../compiler/internal/Contract.js'
6
+ import * as Project from './internal/Project.js'
9
7
 
10
- type Context = {
11
- addContextDependency(directory: string): void
12
- addDependency(file: string): void
8
+ type Context = Project.Context & {
13
9
  async(): (error: Error | null, code?: string, map?: object) => void
14
10
  getOptions(): {
15
11
  bundler: string
16
12
  mode: 'shared' | 'source' | 'style'
13
+ reset?: boolean | undefined
17
14
  root: string
18
15
  }
19
- getResolve(
20
- options: object,
21
- ): (
22
- directory: string,
23
- specifier: string,
24
- callback: (error: Error | null, file?: string | false) => void,
25
- ) => void
26
- resourcePath: string
27
16
  }
28
17
 
18
+ // Next owns loader worker lifetimes. Each project retains only its latest graph in that worker.
19
+ const projects = new Map<string, ReturnType<typeof Project.create>>()
20
+
29
21
  /** Loader-runner boundary used by Webpack and Turbopack. */
30
22
  export default function loader(this: Context, source: string): void {
31
23
  const callback = this.async()
@@ -40,14 +32,6 @@ async function compile(context: Context, source: string) {
40
32
  const options = context.getOptions()
41
33
  const root = options.root
42
34
  const directory = Path.resolve(root, '.zyzz', 'next')
43
- const modules: Record<string, string> = Object.create(null)
44
- const imports: Record<string, Record<string, string | null>> = Object.create(
45
- null,
46
- )
47
- const contracts: Record<string, string> = Object.create(null)
48
- const resolve = context.getResolve({})
49
- const id = (file: string) =>
50
- `app/${Path.relative(root, file).split(Path.sep).join('/')}`
51
35
  const eligible = (file: string) =>
52
36
  /\.[cm]?[jt]sx?$/.test(file) &&
53
37
  !/\.(?:d|test|test-d|bench|bench-d)\.[cm]?[jt]sx?$/.test(file) &&
@@ -58,142 +42,31 @@ async function compile(context: Context, source: string) {
58
42
  part === '..' || part === 'node_modules' || part.startsWith('.'),
59
43
  )
60
44
 
61
- if (options.mode !== 'shared' && !eligible(context.resourcePath))
62
- return { code: source, map: undefined }
63
-
64
- async function visit(file: string, text?: string): Promise<void> {
65
- const name = id(file)
66
- if (Object.hasOwn(modules, name)) return
67
-
68
- context.addDependency(file)
69
- modules[name] = text ?? (await Fs.readFile(file, 'utf8'))
70
- const links: Record<string, string | null> = Object.create(null)
71
- imports[name] = links
72
- const parsed = Parser.parseSync(file, modules[name]!, {
73
- sourceType: 'module',
74
- })
75
-
76
- for (const node of parsed.program.body) {
77
- if (
78
- (node.type !== 'ImportDeclaration' &&
79
- node.type !== 'ExportNamedDeclaration' &&
80
- node.type !== 'ExportAllDeclaration') ||
81
- !node.source
82
- )
83
- continue
84
- if (
85
- node.type === 'ImportDeclaration'
86
- ? node.importKind === 'type'
87
- : node.exportKind === 'type'
88
- )
89
- continue
90
-
91
- const specifier = node.source.value
92
- links[specifier] = null
93
- // Next owns this virtual module, which has no physical stylesheet contract.
94
- if (specifier === 'next/root-params') continue
95
- if (
96
- specifier === 'zyzz' ||
97
- (specifier.startsWith('zyzz/') && specifier !== 'zyzz/default') ||
98
- specifier.startsWith('node:')
99
- )
100
- continue
101
-
102
- const resolved = await new Promise<string | false | undefined>(
103
- (accept, reject) => {
104
- resolve(Path.dirname(file), specifier, (error, value) =>
105
- error ? reject(error) : accept(value),
106
- )
107
- },
108
- )
109
- if (!resolved || !/\.[cm]?[jt]sx?$/.test(resolved)) continue
110
- if (eligible(resolved)) {
111
- links[specifier] = id(resolved)
112
- await visit(resolved)
113
- continue
114
- }
115
-
116
- const sidecar = `${resolved}.zyzz.json`
117
- try {
118
- contracts[resolved] = await Fs.readFile(sidecar, 'utf8')
119
- context.addDependency(sidecar)
120
- links[specifier] = resolved
121
- } catch (error) {
122
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
123
- }
124
- }
125
- }
126
-
127
- async function discover(directory: string): Promise<void> {
128
- const items = await Fs.readdir(directory, { withFileTypes: true })
129
-
130
- // Recursive tracking of a package root follows every installed dependency.
131
- // A dependency symlink back to an ancestor, as a package linked from its
132
- // own repository, is rejected as a loop, so such a root is tracked through
133
- // its files and subdirectories instead.
134
- if (
135
- !items.some((item) => item.name === 'node_modules') ||
136
- !(await loops(directory))
45
+ // Next.js hoists instrumentation CSS out of route stylesheets without loading it.
46
+ if (
47
+ /^(?:src\/)?instrumentation-client\.[cm]?[jt]s$/.test(
48
+ Path.relative(root, context.resourcePath).split(Path.sep).join('/'),
137
49
  )
138
- context.addContextDependency(directory)
139
-
140
- for (const item of items) {
141
- if (
142
- item.name.startsWith('.') ||
143
- [
144
- 'node_modules',
145
- 'dist',
146
- 'build',
147
- 'coverage',
148
- 'test',
149
- 'tests',
150
- '__tests__',
151
- ].includes(item.name)
152
- )
153
- continue
154
- const file = Path.join(directory, item.name)
155
- if (item.isDirectory()) await discover(file)
156
- else if (item.isFile() && eligible(file)) await visit(file)
157
- }
158
- }
159
-
160
- if (options.mode !== 'shared') await visit(context.resourcePath, source)
161
- await discover(root)
162
- const loaded = new Set<string>()
163
- async function dependencies(file: string): Promise<void> {
164
- if (loaded.has(file)) return
165
- loaded.add(file)
50
+ )
51
+ return { code: source, map: undefined }
166
52
 
167
- const metadata = Contract.read(contracts[file]!, new Map(), file)
168
- for (const section of metadata.stylesheets) {
169
- let owner = file
170
- for (const specifier of section.dependency ?? []) {
171
- const target = await new Promise<string | false | undefined>(
172
- (accept, reject) => {
173
- resolve(Path.dirname(owner), specifier, (error, value) =>
174
- error ? reject(error) : accept(value),
175
- )
176
- },
177
- )
178
- if (!target || !Path.isAbsolute(target))
179
- throw new Error('Unable to resolve packed stylesheet dependency.')
53
+ if (options.mode !== 'shared' && !eligible(context.resourcePath))
54
+ return { code: source, map: undefined }
180
55
 
181
- const links = (imports[owner] ??= Object.create(null))
182
- links[specifier] = target
183
- if (!Object.hasOwn(contracts, target)) {
184
- const sidecar = `${target}.zyzz.json`
185
- contracts[target] = await Fs.readFile(sidecar, 'utf8')
186
- context.addDependency(sidecar)
187
- }
188
- await dependencies(target)
189
- owner = target
190
- }
191
- }
56
+ const key = JSON.stringify([root, options.bundler, options.reset ?? false])
57
+ let project = projects.get(key)
58
+ if (!project) {
59
+ project = Project.create(options)
60
+ projects.set(key, project)
192
61
  }
193
- for (const file of Object.keys(contracts)) await dependencies(file)
194
-
195
- const graph = Graph.compile({ contracts, imports, modules })
196
- const output = graph.modules[id(context.resourcePath)]
62
+ const graph = await project.compile(
63
+ context,
64
+ options.mode === 'shared' ? undefined : source,
65
+ )
66
+ const output =
67
+ graph.modules[
68
+ `app/${Path.relative(root, context.resourcePath).split(Path.sep).join('/')}`
69
+ ]
197
70
 
198
71
  const assets = new Map<string, string>()
199
72
  for (const [placeholder, target] of Object.entries(
@@ -346,38 +219,3 @@ async function compile(context: Context, source: string) {
346
219
 
347
220
  return { code: `${output.code}\n${requests.join('\n')}`, map }
348
221
  }
349
-
350
- /** Whether an installed dependency links back to the directory or one of its ancestors. */
351
- async function loops(directory: string): Promise<boolean> {
352
- const installed = Path.join(directory, 'node_modules')
353
- const entries = await Fs.readdir(installed, { withFileTypes: true }).catch(
354
- () => [],
355
- )
356
- const candidates = (
357
- await Promise.all(
358
- entries.map(async (entry) => {
359
- if (!entry.name.startsWith('@'))
360
- return [Path.join(installed, entry.name)]
361
-
362
- const scoped = Path.join(installed, entry.name)
363
-
364
- return (await Fs.readdir(scoped).catch(() => [])).map((name) =>
365
- Path.join(scoped, name),
366
- )
367
- }),
368
- )
369
- ).flat()
370
-
371
- for (const candidate of candidates) {
372
- const stat = await Fs.lstat(candidate).catch(() => undefined)
373
- if (!stat?.isSymbolicLink()) continue
374
-
375
- const target = await Fs.realpath(candidate).catch(() => undefined)
376
- if (target === undefined) continue
377
-
378
- const relative = Path.relative(target, directory)
379
- if (!relative.startsWith('..') && !Path.isAbsolute(relative)) return true
380
- }
381
-
382
- return false
383
- }
@@ -0,0 +1,7 @@
1
+ /** Loads the canonical reset stylesheet for web bundler adapters. @module */
2
+ import * as Fs from 'node:fs'
3
+
4
+ /** Reads the packaged stylesheet without adding a dependency on browser CSS loaders. */
5
+ export function read(): string {
6
+ return Fs.readFileSync(new URL('../reset.css', import.meta.url), 'utf8')
7
+ }
@@ -3,6 +3,7 @@ import * as Mapping from '@jridgewell/gen-mapping'
3
3
  import * as Crypto from 'node:crypto'
4
4
  import * as Fs from 'node:fs/promises'
5
5
  import * as Path from 'node:path'
6
+ import * as Reset from '../node/Reset.js'
6
7
  import * as Parser from 'oxc-parser'
7
8
  import { createUnplugin } from 'unplugin'
8
9
  import type { UnpluginBuildContext } from 'unplugin'
@@ -16,6 +17,8 @@ import * as Vite from '../vite/index.js'
16
17
  export type Options = {
17
18
  /** Rewrite authoring calls. Defaults to true. */
18
19
  readonly compiler?: boolean | undefined
20
+ /** Include the CSS reset. Defaults to false. */
21
+ readonly reset?: boolean | undefined
19
22
  /** Source directory scanned for modules and global contributions. Defaults to the working directory. */
20
23
  readonly root?: string | undefined
21
24
  }
@@ -280,6 +283,7 @@ const portable = createUnplugin<Options | undefined, false>(
280
283
 
281
284
  const result = compiler.compile({
282
285
  compiler: options.compiler,
286
+ reset: options.reset ? Reset.read() : undefined,
283
287
  contracts,
284
288
  imports,
285
289
  modules,
package/src/variants.ts CHANGED
@@ -51,6 +51,7 @@ type CheckedChoice<
51
51
  values: values,
52
52
  ) => CheckedStyles<body, tokens, layers, mappings> &
53
53
  Binding.Checked<body> &
54
+ Binding.TokenSlots<body, values, tokens, mappings> &
54
55
  (string extends keyof values
55
56
  ? never
56
57
  : values extends Record<string, string | number> &
package/src/vite/index.ts CHANGED
@@ -10,6 +10,7 @@ import * as Lightning from 'lightningcss'
10
10
  import * as Crypto from 'node:crypto'
11
11
  import * as Fs from 'node:fs/promises'
12
12
  import * as Path from 'node:path'
13
+ import * as Reset from '../node/Reset.js'
13
14
  import { isBuiltin } from 'node:module'
14
15
  import * as Parser from 'oxc-parser'
15
16
  import * as Walker from 'oxc-walker'
@@ -34,6 +35,8 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
34
35
  options.native.units && Object.freeze({ ...options.native.units }),
35
36
  })
36
37
  : undefined
38
+ if (native && options.reset)
39
+ throw new Error('The CSS reset is only supported by web builds.')
37
40
  if (native && options.compiler === false)
38
41
  throw new Error('Native builds require source compilation.')
39
42
 
@@ -640,6 +643,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
640
643
 
641
644
  const result = entry.compiler.compile({
642
645
  compiler: options.compiler,
646
+ reset: options.reset ? Reset.read() : undefined,
643
647
  native,
644
648
  contracts,
645
649
  development: entry.environment.mode !== 'build',
@@ -762,17 +766,39 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
762
766
  for (const [key, target] of Object.entries(result.sharedAssets ?? {})) {
763
767
  const raw = target.split(/[?#]/)[0]!
764
768
 
765
- const file = await Fs.realpath(
766
- target.startsWith('app/')
767
- ? Path.join(root, decodeURIComponent(raw.slice(4)))
768
- : Path.isAbsolute(raw)
769
- ? decodeURIComponent(raw)
770
- : (() => {
771
- throw new Error('Asset path escapes the Vite graph.')
772
- })(),
773
- )
774
-
775
769
  const identity = result.sharedAssetOwners?.[key]
770
+ const path = target.startsWith('app/')
771
+ ? Path.join(root, decodeURIComponent(raw.slice(4)))
772
+ : Path.isAbsolute(raw)
773
+ ? decodeURIComponent(raw)
774
+ : undefined
775
+ if (!path) throw new Error('Asset path escapes the Vite graph.')
776
+
777
+ const asset = await (async () => {
778
+ try {
779
+ return { file: await Fs.realpath(path), package: false }
780
+ } catch (error) {
781
+ if (
782
+ (error as NodeJS.ErrnoException).code !== 'ENOENT' ||
783
+ !identity?.startsWith('app/')
784
+ )
785
+ throw error
786
+
787
+ const importer = Path.join(root, identity.slice(4))
788
+ const specifier = Path.relative(Path.dirname(importer), path)
789
+ .split(Path.sep)
790
+ .join('/')
791
+ if (specifier.startsWith('.') || Path.isAbsolute(specifier))
792
+ throw error
793
+ const resolved = await host.resolve(specifier, importer)
794
+ if (!resolved || resolved.external) throw error
795
+ return {
796
+ file: await Fs.realpath(resolved.id.split('?')[0]!),
797
+ package: true,
798
+ }
799
+ }
800
+ })()
801
+ const file = asset.file
776
802
 
777
803
  const owner = identity?.startsWith('app/')
778
804
  ? appRoot
@@ -782,9 +808,10 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
782
808
 
783
809
  const relative = owner ? Path.relative(owner, file) : '..'
784
810
  if (
785
- relative === '..' ||
786
- relative.startsWith(`..${Path.sep}`) ||
787
- Path.isAbsolute(relative)
811
+ !asset.package &&
812
+ (relative === '..' ||
813
+ relative.startsWith(`..${Path.sep}`) ||
814
+ Path.isAbsolute(relative))
788
815
  )
789
816
  throw new Error('Asset path escapes its owning package.')
790
817
 
@@ -793,7 +820,7 @@ export function zyzz(options: zyzz.Options = {}): Plugin {
793
820
 
794
821
  let url: string
795
822
 
796
- if (/[?#%]/.test(file)) {
823
+ if (asset.package || /[?#%]/.test(file)) {
797
824
  if (entry.environment.mode === 'build') {
798
825
  url = await host.asset(file)
799
826
 
@@ -1465,6 +1492,8 @@ export declare namespace zyzz {
1465
1492
  readonly compiler?: boolean | undefined
1466
1493
  /** Native context captured at creation. Disables CSS delivery and initialization scripts. */
1467
1494
  readonly native?: Graph.compile.Options['native']
1495
+ /** Include the CSS reset. Defaults to false. */
1496
+ readonly reset?: boolean | undefined
1468
1497
  /**
1469
1498
  * Inline each configuration's `script()` at the start of index.html's head.
1470
1499
  * False skips injection for documents that inline the script themselves.