@vertz/ui-server 0.2.15 → 0.2.16

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.
@@ -1,21 +1,28 @@
1
1
  // @bun
2
+ import {
3
+ computeImageOutputPaths,
4
+ resolveImageSrc
5
+ } from "../shared/chunk-gggnhyqj.js";
2
6
  import {
3
7
  __require
4
8
  } from "../shared/chunk-eb80r8e8.js";
5
9
 
6
10
  // src/bun-plugin/plugin.ts
7
- import { mkdirSync, writeFileSync } from "fs";
8
- import { dirname, relative, resolve } from "path";
11
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
12
+ import { dirname, relative, resolve as resolve2 } from "path";
9
13
  import remapping from "@ampproject/remapping";
10
14
  import {
11
15
  ComponentAnalyzer,
12
16
  CSSExtractor,
13
17
  compile,
14
18
  generateAllManifests,
15
- HydrationTransformer
19
+ HydrationTransformer,
20
+ regenerateFileManifest,
21
+ resolveModuleSpecifier,
22
+ transformRouteSplitting
16
23
  } from "@vertz/ui-compiler";
17
- import MagicString from "magic-string";
18
- import { Project, ts as ts2 } from "ts-morph";
24
+ import MagicString3 from "magic-string";
25
+ import { Project as Project2, ts as ts4 } from "ts-morph";
19
26
 
20
27
  // src/bun-plugin/context-stable-ids.ts
21
28
  import { ts } from "ts-morph";
@@ -48,6 +55,22 @@ function injectContextStableIds(source, sourceFile, relFilePath) {
48
55
  }
49
56
  }
50
57
 
58
+ // src/bun-plugin/entity-schema-loader.ts
59
+ import { readFileSync } from "fs";
60
+ function loadEntitySchema(schemaPath) {
61
+ if (!schemaPath)
62
+ return;
63
+ try {
64
+ const content = readFileSync(schemaPath, "utf-8");
65
+ const parsed = JSON.parse(content);
66
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
67
+ return;
68
+ return parsed;
69
+ } catch {
70
+ return;
71
+ }
72
+ }
73
+
51
74
  // src/bun-plugin/fast-refresh-codegen.ts
52
75
  function generateRefreshPreamble(moduleId, contentHash) {
53
76
  const escapedId = moduleId.replace(/['\\]/g, "\\$&");
@@ -95,6 +118,276 @@ function generateRefreshCode(moduleId, components, contentHash) {
95
118
  return { preamble, epilogue };
96
119
  }
97
120
 
121
+ // src/bun-plugin/field-selection-inject.ts
122
+ import { analyzeFieldSelection } from "@vertz/ui-compiler";
123
+ import MagicString from "magic-string";
124
+ function injectFieldSelection(filePath, source, options) {
125
+ const selections = analyzeFieldSelection(filePath, source);
126
+ if (selections.length === 0) {
127
+ return { code: source, injected: false, diagnostics: [] };
128
+ }
129
+ const s = new MagicString(source);
130
+ let injected = false;
131
+ const diagnostics = [];
132
+ for (const selection of selections) {
133
+ if (hasUserSelect(source, selection.descriptorCallStart, selection.descriptorCallEnd)) {
134
+ diagnostics.push({
135
+ queryVar: selection.queryVar,
136
+ singleFileFields: [...selection.fields],
137
+ crossFileFields: [],
138
+ combinedFields: [...selection.fields],
139
+ hasOpaqueAccess: selection.hasOpaqueAccess,
140
+ injected: false
141
+ });
142
+ continue;
143
+ }
144
+ const entityName = selection.inferredEntityName ?? options?.entityType;
145
+ const schema = entityName ? options?.entitySchema?.[entityName] : undefined;
146
+ const crossFileResult = resolveCrossFileFields(filePath, selection.propFlows, options);
147
+ const combinedFields = [...selection.fields, ...crossFileResult.fields];
148
+ const combinedOpaque = selection.hasOpaqueAccess || crossFileResult.hasOpaqueAccess;
149
+ let queryInjected = false;
150
+ if (!combinedOpaque && combinedFields.length > 0) {
151
+ const injectionStr = schema ? buildManifestAwareInjection(combinedFields, selection.nestedAccess, schema) : buildSimpleSelectInjection(combinedFields);
152
+ if (injectionStr) {
153
+ switch (selection.injectionKind) {
154
+ case "insert-arg":
155
+ s.appendLeft(selection.injectionPos, `{ ${injectionStr} }`);
156
+ break;
157
+ case "merge-into-object":
158
+ s.appendLeft(selection.injectionPos, `, ${injectionStr} `);
159
+ break;
160
+ case "append-arg":
161
+ s.appendLeft(selection.injectionPos, `, { ${injectionStr} }`);
162
+ break;
163
+ }
164
+ queryInjected = true;
165
+ injected = true;
166
+ }
167
+ }
168
+ diagnostics.push({
169
+ queryVar: selection.queryVar,
170
+ singleFileFields: [...selection.fields],
171
+ crossFileFields: [...crossFileResult.fields],
172
+ combinedFields: [...new Set(combinedFields)],
173
+ hasOpaqueAccess: combinedOpaque,
174
+ injected: queryInjected
175
+ });
176
+ }
177
+ return {
178
+ code: s.toString(),
179
+ injected,
180
+ diagnostics
181
+ };
182
+ }
183
+ function buildSimpleSelectInjection(fields) {
184
+ const allFields = new Set(["id", ...fields]);
185
+ const sortedFields = [...allFields].sort();
186
+ const selectEntries = sortedFields.map((f) => `${f}: true`).join(", ");
187
+ return `select: { ${selectEntries} }`;
188
+ }
189
+ function buildManifestAwareInjection(fields, nestedAccess, schema) {
190
+ const relationNames = new Set(Object.keys(schema.relations));
191
+ const hiddenFieldSet = new Set(schema.hiddenFields);
192
+ const primaryKey = schema.primaryKey ?? "id";
193
+ const scalarFields = new Set([primaryKey]);
194
+ const relationIncludes = new Map;
195
+ for (const field of new Set(fields)) {
196
+ if (relationNames.has(field)) {
197
+ const nestedForField = nestedAccess.filter((n) => n.field === field);
198
+ if (nestedForField.length > 0) {
199
+ if (!relationIncludes.has(field)) {
200
+ relationIncludes.set(field, new Set);
201
+ }
202
+ for (const nested of nestedForField) {
203
+ const firstSegment = nested.nestedPath[0];
204
+ if (firstSegment !== undefined) {
205
+ relationIncludes.get(field)?.add(firstSegment);
206
+ }
207
+ }
208
+ } else {
209
+ scalarFields.add(field);
210
+ }
211
+ } else {
212
+ scalarFields.add(field);
213
+ }
214
+ }
215
+ for (const hidden of hiddenFieldSet) {
216
+ scalarFields.delete(hidden);
217
+ }
218
+ const sortedScalars = [...scalarFields].sort();
219
+ const selectEntries = sortedScalars.map((f) => `${f}: true`).join(", ");
220
+ const parts = [`select: { ${selectEntries} }`];
221
+ if (relationIncludes.size > 0) {
222
+ const includeEntries = [];
223
+ const sortedRelations = [...relationIncludes.keys()].sort();
224
+ for (const relName of sortedRelations) {
225
+ const relSchema = schema.relations[relName];
226
+ const relFieldSet = relationIncludes.get(relName);
227
+ if (!relFieldSet)
228
+ continue;
229
+ let relFields = [...relFieldSet];
230
+ if (relSchema && Array.isArray(relSchema.selection)) {
231
+ const allowed = new Set(relSchema.selection);
232
+ relFields = relFields.filter((f) => allowed.has(f));
233
+ }
234
+ if (relFields.length > 0) {
235
+ const sortedRelFields = relFields.sort();
236
+ const relSelectEntries = sortedRelFields.map((f) => `${f}: true`).join(", ");
237
+ includeEntries.push(`${relName}: { select: { ${relSelectEntries} } }`);
238
+ }
239
+ }
240
+ if (includeEntries.length > 0) {
241
+ parts.push(`include: { ${includeEntries.join(", ")} }`);
242
+ }
243
+ }
244
+ return parts.join(", ");
245
+ }
246
+ function hasUserSelect(source, callStart, callEnd) {
247
+ const region = source.slice(callStart, callEnd);
248
+ return /\bselect\s*:/.test(region);
249
+ }
250
+ function resolveCrossFileFields(filePath, propFlows, options) {
251
+ if (!options?.manifest || !options.resolveImport || propFlows.length === 0) {
252
+ return { fields: [], hasOpaqueAccess: false };
253
+ }
254
+ const fields = [];
255
+ let hasOpaqueAccess = false;
256
+ for (const flow of propFlows) {
257
+ if (!flow.importSource)
258
+ continue;
259
+ const resolvedPath = options.resolveImport(flow.importSource, filePath);
260
+ if (!resolvedPath) {
261
+ hasOpaqueAccess = true;
262
+ continue;
263
+ }
264
+ const childFields = options.manifest.getResolvedPropFields(resolvedPath, flow.componentName, flow.propName);
265
+ if (childFields) {
266
+ fields.push(...childFields.fields);
267
+ if (childFields.hasOpaqueAccess) {
268
+ hasOpaqueAccess = true;
269
+ }
270
+ }
271
+ }
272
+ return { fields, hasOpaqueAccess };
273
+ }
274
+
275
+ // src/bun-plugin/field-selection-manifest.ts
276
+ import { analyzeComponentPropFields } from "@vertz/ui-compiler";
277
+
278
+ class FieldSelectionManifest {
279
+ fileComponents = new Map;
280
+ importResolver = () => {
281
+ return;
282
+ };
283
+ resolvedCache = new Map;
284
+ setImportResolver(resolver) {
285
+ this.importResolver = resolver;
286
+ }
287
+ registerFile(filePath, sourceText) {
288
+ const components = analyzeComponentPropFields(filePath, sourceText);
289
+ this.fileComponents.set(filePath, components);
290
+ this.resolvedCache.clear();
291
+ }
292
+ updateFile(filePath, sourceText) {
293
+ const oldComponents = this.fileComponents.get(filePath);
294
+ const newComponents = analyzeComponentPropFields(filePath, sourceText);
295
+ const changed = !componentsEqual(oldComponents, newComponents);
296
+ if (changed) {
297
+ this.fileComponents.set(filePath, newComponents);
298
+ this.resolvedCache.clear();
299
+ }
300
+ return { changed };
301
+ }
302
+ deleteFile(filePath) {
303
+ this.fileComponents.delete(filePath);
304
+ this.resolvedCache.clear();
305
+ }
306
+ getComponentPropFields(filePath, componentName, propName) {
307
+ const components = this.fileComponents.get(filePath);
308
+ if (!components)
309
+ return;
310
+ const component = components.find((c) => c.componentName === componentName);
311
+ if (!component)
312
+ return;
313
+ return component.props[propName];
314
+ }
315
+ getResolvedPropFields(filePath, componentName, propName) {
316
+ const cacheKey = `${filePath}::${componentName}::${propName}`;
317
+ if (this.resolvedCache.has(cacheKey)) {
318
+ return this.resolvedCache.get(cacheKey);
319
+ }
320
+ const result = this.resolveFields(filePath, componentName, propName, new Set);
321
+ if (result) {
322
+ this.resolvedCache.set(cacheKey, result);
323
+ }
324
+ return result;
325
+ }
326
+ resolveFields(filePath, componentName, propName, visited) {
327
+ const visitKey = `${filePath}::${componentName}::${propName}`;
328
+ if (visited.has(visitKey))
329
+ return;
330
+ visited.add(visitKey);
331
+ const access = this.getComponentPropFields(filePath, componentName, propName);
332
+ if (!access)
333
+ return;
334
+ const allFields = new Set(access.fields);
335
+ let hasOpaqueAccess = access.hasOpaqueAccess;
336
+ for (const forward of access.forwarded) {
337
+ const targetPath = forward.importSource ? this.importResolver(forward.importSource, filePath) : undefined;
338
+ if (!targetPath) {
339
+ hasOpaqueAccess = true;
340
+ continue;
341
+ }
342
+ const childFields = this.resolveFields(targetPath, forward.componentName, forward.propName, visited);
343
+ if (childFields) {
344
+ for (const field of childFields.fields) {
345
+ allFields.add(field);
346
+ }
347
+ if (childFields.hasOpaqueAccess) {
348
+ hasOpaqueAccess = true;
349
+ }
350
+ }
351
+ }
352
+ return { fields: [...allFields], hasOpaqueAccess };
353
+ }
354
+ }
355
+ function componentsEqual(a, b) {
356
+ if (!a)
357
+ return b.length === 0;
358
+ if (a.length !== b.length)
359
+ return false;
360
+ for (let i = 0;i < a.length; i++) {
361
+ const ac = a[i];
362
+ const bc = b[i];
363
+ if (ac.componentName !== bc.componentName)
364
+ return false;
365
+ const aProps = Object.keys(ac.props).sort();
366
+ const bProps = Object.keys(bc.props).sort();
367
+ if (aProps.length !== bProps.length)
368
+ return false;
369
+ for (let j = 0;j < aProps.length; j++) {
370
+ const aKey = aProps[j];
371
+ const bKey = bProps[j];
372
+ if (aKey !== bKey)
373
+ return false;
374
+ const aProp = ac.props[aKey];
375
+ const bProp = bc.props[bKey];
376
+ if (aProp.hasOpaqueAccess !== bProp.hasOpaqueAccess)
377
+ return false;
378
+ if (aProp.fields.length !== bProp.fields.length)
379
+ return false;
380
+ const aFieldsSorted = [...aProp.fields].sort();
381
+ const bFieldsSorted = [...bProp.fields].sort();
382
+ for (let k = 0;k < aFieldsSorted.length; k++) {
383
+ if (aFieldsSorted[k] !== bFieldsSorted[k])
384
+ return false;
385
+ }
386
+ }
387
+ }
388
+ return true;
389
+ }
390
+
98
391
  // src/bun-plugin/file-path-hash.ts
99
392
  function filePathHash(filePath) {
100
393
  let hash = 5381;
@@ -104,20 +397,459 @@ function filePathHash(filePath) {
104
397
  return hash.toString(36);
105
398
  }
106
399
 
400
+ // src/bun-plugin/image-processor.ts
401
+ import { createHash } from "crypto";
402
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
403
+ import { basename, extname, resolve } from "path";
404
+ import sharp from "sharp";
405
+ var FORMAT_MAP = {
406
+ jpeg: { ext: ".jpg", mime: "image/jpeg" },
407
+ jpg: { ext: ".jpg", mime: "image/jpeg" },
408
+ png: { ext: ".png", mime: "image/png" },
409
+ webp: { ext: ".webp", mime: "image/webp" },
410
+ gif: { ext: ".gif", mime: "image/gif" },
411
+ tiff: { ext: ".tiff", mime: "image/tiff" },
412
+ avif: { ext: ".avif", mime: "image/avif" }
413
+ };
414
+ async function processImage(opts) {
415
+ const { sourcePath, width, height, quality, fit, outputDir } = opts;
416
+ if (!existsSync(sourcePath)) {
417
+ return { ok: false, error: `Image not found: ${sourcePath}` };
418
+ }
419
+ const sourceBuffer = readFileSync2(sourcePath);
420
+ const hash = createHash("sha256").update(sourceBuffer).update(`${width}x${height}q${quality}f${fit}`).digest("hex").slice(0, 12);
421
+ const name = basename(sourcePath, extname(sourcePath));
422
+ const meta = await sharp(sourceBuffer).metadata();
423
+ const sourceFormat = meta.format ?? "jpeg";
424
+ const defaultFormat = { ext: ".jpg", mime: "image/jpeg" };
425
+ const formatInfo = FORMAT_MAP[sourceFormat] ?? defaultFormat;
426
+ const webp1xName = `${name}-${hash}-${width}w.webp`;
427
+ const webp2xName = `${name}-${hash}-${width * 2}w.webp`;
428
+ const fallbackName = `${name}-${hash}-${width * 2}w${formatInfo.ext}`;
429
+ const webp1xPath = resolve(outputDir, webp1xName);
430
+ const webp2xPath = resolve(outputDir, webp2xName);
431
+ const fallbackPath = resolve(outputDir, fallbackName);
432
+ if (existsSync(webp1xPath) && existsSync(webp2xPath) && existsSync(fallbackPath)) {
433
+ return {
434
+ ok: true,
435
+ webp1x: { path: webp1xPath, url: `/__vertz_img/${webp1xName}` },
436
+ webp2x: { path: webp2xPath, url: `/__vertz_img/${webp2xName}` },
437
+ fallback: {
438
+ path: fallbackPath,
439
+ url: `/__vertz_img/${fallbackName}`,
440
+ format: formatInfo.mime
441
+ }
442
+ };
443
+ }
444
+ mkdirSync(outputDir, { recursive: true });
445
+ const sharpFit = fit;
446
+ const [webp1xBuf, webp2xBuf, fallbackBuf] = await Promise.all([
447
+ sharp(sourceBuffer).resize(width, height, { fit: sharpFit }).webp({ quality }).toBuffer(),
448
+ sharp(sourceBuffer).resize(width * 2, height * 2, { fit: sharpFit }).webp({ quality }).toBuffer(),
449
+ sharp(sourceBuffer).resize(width * 2, height * 2, { fit: sharpFit }).toFormat(sourceFormat, { quality }).toBuffer()
450
+ ]);
451
+ writeFileSync(webp1xPath, webp1xBuf);
452
+ writeFileSync(webp2xPath, webp2xBuf);
453
+ writeFileSync(fallbackPath, fallbackBuf);
454
+ return {
455
+ ok: true,
456
+ webp1x: { path: webp1xPath, url: `/__vertz_img/${webp1xName}` },
457
+ webp2x: { path: webp2xPath, url: `/__vertz_img/${webp2xName}` },
458
+ fallback: { path: fallbackPath, url: `/__vertz_img/${fallbackName}`, format: formatInfo.mime }
459
+ };
460
+ }
461
+
462
+ // src/bun-plugin/image-transform.ts
463
+ import MagicString2 from "magic-string";
464
+ import { Project, ts as ts2 } from "ts-morph";
465
+ function escapeAttr(value) {
466
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
467
+ }
468
+ function transformImages(source, filePath, options) {
469
+ if (!source.includes("<Image") && !source.includes("Image")) {
470
+ return { code: source, map: null, transformed: false };
471
+ }
472
+ const localName = findImageImportName(source);
473
+ if (!localName) {
474
+ return { code: source, map: null, transformed: false };
475
+ }
476
+ const project = new Project({ useInMemoryFileSystem: true });
477
+ const sourceFile = project.createSourceFile(filePath, source, { overwrite: true });
478
+ const jsxElements = findImageJsxElements(sourceFile, localName);
479
+ if (jsxElements.length === 0) {
480
+ return { code: source, map: null, transformed: false };
481
+ }
482
+ const s = new MagicString2(source);
483
+ let transformed = false;
484
+ const sorted = [...jsxElements].sort((a, b) => b.getStart() - a.getStart());
485
+ for (const element of sorted) {
486
+ const props = extractStaticProps(element, sourceFile);
487
+ if (!props)
488
+ continue;
489
+ const paths = options.getImageOutputPaths(options.resolveImagePath(props.src, filePath), props.width, props.height, props.quality ?? 80, props.fit ?? "cover");
490
+ const resolvedLoading = props.priority ? "eager" : props.loading ?? "lazy";
491
+ const resolvedDecoding = props.priority ? "sync" : props.decoding ?? "async";
492
+ const resolvedFetchpriority = props.priority ? "high" : props.fetchpriority;
493
+ const imgAttrs = [
494
+ `src="${escapeAttr(paths.fallback)}"`,
495
+ `width="${props.width}"`,
496
+ `height="${props.height}"`,
497
+ `alt="${escapeAttr(props.alt)}"`,
498
+ `loading="${escapeAttr(resolvedLoading)}"`,
499
+ `decoding="${escapeAttr(resolvedDecoding)}"`
500
+ ];
501
+ if (resolvedFetchpriority)
502
+ imgAttrs.push(`fetchpriority="${escapeAttr(resolvedFetchpriority)}"`);
503
+ if (props.class)
504
+ imgAttrs.push(`class="${escapeAttr(props.class)}"`);
505
+ if (props.style)
506
+ imgAttrs.push(`style="${escapeAttr(props.style)}"`);
507
+ for (const attr of props.extraAttrs) {
508
+ imgAttrs.push(attr);
509
+ }
510
+ const pictureOpen = props.pictureClass ? `<picture class="${escapeAttr(props.pictureClass)}">` : "<picture>";
511
+ const replacement = [
512
+ pictureOpen,
513
+ `<source srcset="${escapeAttr(paths.webp1x)} 1x, ${escapeAttr(paths.webp2x)} 2x" type="image/webp" />`,
514
+ `<img ${imgAttrs.join(" ")} />`,
515
+ "</picture>"
516
+ ].join("");
517
+ s.overwrite(element.getStart(), element.getEnd(), replacement);
518
+ transformed = true;
519
+ }
520
+ if (!transformed) {
521
+ return { code: source, map: null, transformed: false };
522
+ }
523
+ return {
524
+ code: s.toString(),
525
+ map: s.generateMap({ source: filePath, hires: true }),
526
+ transformed: true
527
+ };
528
+ }
529
+ function findImageImportName(source) {
530
+ const importMatch = source.match(/import\s*\{[^}]*\bImage\b(?:\s+as\s+(\w+))?[^}]*\}\s*from\s*['"]@vertz\/ui['"]/);
531
+ if (!importMatch)
532
+ return null;
533
+ return importMatch[1] ?? "Image";
534
+ }
535
+ function findImageJsxElements(sourceFile, localName) {
536
+ const results = [];
537
+ function visit(node) {
538
+ if (ts2.isJsxSelfClosingElement(node)) {
539
+ const tagName = node.tagName.getText(sourceFile.compilerNode);
540
+ if (tagName === localName) {
541
+ results.push(node);
542
+ }
543
+ }
544
+ ts2.forEachChild(node, visit);
545
+ }
546
+ visit(sourceFile.compilerNode);
547
+ return results;
548
+ }
549
+ function extractStaticProps(element, sourceFile) {
550
+ const attrs = element.attributes;
551
+ for (const attr of attrs.properties) {
552
+ if (ts2.isJsxSpreadAttribute(attr))
553
+ return null;
554
+ }
555
+ let src = null;
556
+ let width = null;
557
+ let height = null;
558
+ let alt = null;
559
+ let className;
560
+ let pictureClass;
561
+ let style;
562
+ let loading;
563
+ let decoding;
564
+ let fetchpriority;
565
+ let priority = false;
566
+ let quality;
567
+ let fit;
568
+ const extraAttrs = [];
569
+ const KNOWN_PROPS = new Set([
570
+ "src",
571
+ "width",
572
+ "height",
573
+ "alt",
574
+ "class",
575
+ "pictureClass",
576
+ "style",
577
+ "loading",
578
+ "decoding",
579
+ "fetchpriority",
580
+ "priority",
581
+ "quality",
582
+ "fit"
583
+ ]);
584
+ for (const attr of attrs.properties) {
585
+ if (!ts2.isJsxAttribute(attr))
586
+ continue;
587
+ const name = attr.name.getText(sourceFile.compilerNode);
588
+ const value = attr.initializer;
589
+ switch (name) {
590
+ case "src":
591
+ src = extractStaticString(value, sourceFile);
592
+ if (src === null)
593
+ return null;
594
+ break;
595
+ case "width":
596
+ width = extractStaticNumber(value, sourceFile);
597
+ if (width === null)
598
+ return null;
599
+ break;
600
+ case "height":
601
+ height = extractStaticNumber(value, sourceFile);
602
+ if (height === null)
603
+ return null;
604
+ break;
605
+ case "alt":
606
+ alt = extractStaticString(value, sourceFile);
607
+ if (alt === null)
608
+ return null;
609
+ break;
610
+ case "class":
611
+ if (value) {
612
+ className = extractStaticString(value, sourceFile) ?? undefined;
613
+ if (!className)
614
+ return null;
615
+ }
616
+ break;
617
+ case "pictureClass":
618
+ if (value) {
619
+ pictureClass = extractStaticString(value, sourceFile) ?? undefined;
620
+ if (!pictureClass)
621
+ return null;
622
+ }
623
+ break;
624
+ case "style":
625
+ if (value) {
626
+ style = extractStaticString(value, sourceFile) ?? undefined;
627
+ if (!style)
628
+ return null;
629
+ }
630
+ break;
631
+ case "loading":
632
+ loading = extractStaticString(value, sourceFile) ?? undefined;
633
+ break;
634
+ case "decoding":
635
+ decoding = extractStaticString(value, sourceFile) ?? undefined;
636
+ break;
637
+ case "fetchpriority":
638
+ fetchpriority = extractStaticString(value, sourceFile) ?? undefined;
639
+ break;
640
+ case "priority":
641
+ if (!value) {
642
+ priority = true;
643
+ } else {
644
+ const boolVal = extractStaticBoolean(value, sourceFile);
645
+ if (boolVal !== null)
646
+ priority = boolVal;
647
+ }
648
+ break;
649
+ case "quality":
650
+ quality = extractStaticNumber(value, sourceFile) ?? undefined;
651
+ break;
652
+ case "fit":
653
+ fit = extractStaticString(value, sourceFile) ?? undefined;
654
+ break;
655
+ default:
656
+ if (!KNOWN_PROPS.has(name)) {
657
+ const strVal = extractStaticString(value, sourceFile);
658
+ if (strVal !== null) {
659
+ extraAttrs.push(`${name}="${escapeAttr(strVal)}"`);
660
+ }
661
+ }
662
+ break;
663
+ }
664
+ }
665
+ if (!src || width === null || height === null || alt === null) {
666
+ return null;
667
+ }
668
+ return {
669
+ src,
670
+ width,
671
+ height,
672
+ alt,
673
+ class: className,
674
+ pictureClass,
675
+ style,
676
+ loading,
677
+ decoding,
678
+ fetchpriority,
679
+ priority,
680
+ quality,
681
+ fit,
682
+ extraAttrs
683
+ };
684
+ }
685
+ function extractStaticString(value, _sourceFile) {
686
+ if (!value)
687
+ return null;
688
+ if (ts2.isStringLiteral(value)) {
689
+ return value.text;
690
+ }
691
+ if (ts2.isJsxExpression(value) && value.expression) {
692
+ const expr = value.expression;
693
+ if (ts2.isStringLiteral(expr)) {
694
+ return expr.text;
695
+ }
696
+ if (ts2.isNoSubstitutionTemplateLiteral(expr)) {
697
+ return expr.text;
698
+ }
699
+ }
700
+ return null;
701
+ }
702
+ function extractStaticNumber(value, _sourceFile) {
703
+ if (!value)
704
+ return null;
705
+ if (ts2.isJsxExpression(value) && value.expression) {
706
+ const expr = value.expression;
707
+ if (ts2.isNumericLiteral(expr)) {
708
+ return Number(expr.text);
709
+ }
710
+ }
711
+ return null;
712
+ }
713
+ function extractStaticBoolean(value, _sourceFile) {
714
+ if (!value)
715
+ return null;
716
+ if (ts2.isJsxExpression(value) && value.expression) {
717
+ const expr = value.expression;
718
+ if (expr.kind === ts2.SyntaxKind.TrueKeyword)
719
+ return true;
720
+ if (expr.kind === ts2.SyntaxKind.FalseKeyword)
721
+ return false;
722
+ }
723
+ return null;
724
+ }
725
+
726
+ // src/bun-plugin/island-id-inject.ts
727
+ import { ts as ts3 } from "ts-morph";
728
+ function injectIslandIds(source, sourceFile, relFilePath) {
729
+ const originalSource = source.original;
730
+ if (!originalSource.includes("<Island") && !originalSource.includes("Island")) {
731
+ return;
732
+ }
733
+ const localName = findIslandImportName(originalSource);
734
+ if (!localName)
735
+ return;
736
+ const jsxElements = findIslandJsxElements(sourceFile, localName);
737
+ if (jsxElements.length === 0)
738
+ return;
739
+ const escapedPath = relFilePath.replace(/['\\]/g, "\\$&");
740
+ for (const element of jsxElements) {
741
+ if (hasIdProp(element, sourceFile))
742
+ continue;
743
+ const componentName = extractComponentName(element, sourceFile);
744
+ if (!componentName)
745
+ continue;
746
+ const stableId = `${escapedPath}::${componentName}`;
747
+ const tagName = element.tagName;
748
+ const tagEnd = tagName.end;
749
+ source.appendLeft(tagEnd, ` id="${stableId}"`);
750
+ }
751
+ }
752
+ function findIslandImportName(source) {
753
+ const importMatch = source.match(/import\s*\{[^}]*\bIsland\b(?:\s+as\s+(\w+))?[^}]*\}\s*from\s*['"]@vertz\/ui['"]/);
754
+ if (!importMatch)
755
+ return null;
756
+ return importMatch[1] ?? "Island";
757
+ }
758
+ function findIslandJsxElements(sourceFile, localName) {
759
+ const results = [];
760
+ function visit(node) {
761
+ if (ts3.isJsxSelfClosingElement(node)) {
762
+ const tagName = node.tagName.getText(sourceFile.compilerNode);
763
+ if (tagName === localName) {
764
+ results.push(node);
765
+ }
766
+ }
767
+ ts3.forEachChild(node, visit);
768
+ }
769
+ visit(sourceFile.compilerNode);
770
+ return results;
771
+ }
772
+ function hasIdProp(element, sourceFile) {
773
+ for (const attr of element.attributes.properties) {
774
+ if (ts3.isJsxAttribute(attr)) {
775
+ const name = attr.name.getText(sourceFile.compilerNode);
776
+ if (name === "id")
777
+ return true;
778
+ }
779
+ }
780
+ return false;
781
+ }
782
+ function extractComponentName(element, sourceFile) {
783
+ for (const attr of element.attributes.properties) {
784
+ if (!ts3.isJsxAttribute(attr))
785
+ continue;
786
+ const name = attr.name.getText(sourceFile.compilerNode);
787
+ if (name !== "component")
788
+ continue;
789
+ const value = attr.initializer;
790
+ if (!value)
791
+ return null;
792
+ if (ts3.isJsxExpression(value) && value.expression) {
793
+ if (ts3.isIdentifier(value.expression)) {
794
+ return value.expression.text;
795
+ }
796
+ }
797
+ return null;
798
+ }
799
+ return null;
800
+ }
801
+
107
802
  // src/bun-plugin/plugin.ts
803
+ function manifestsEqual(a, b) {
804
+ if (!a)
805
+ return false;
806
+ const aKeys = Object.keys(a.exports);
807
+ const bKeys = Object.keys(b.exports);
808
+ if (aKeys.length !== bKeys.length)
809
+ return false;
810
+ for (const key of aKeys) {
811
+ const aExport = a.exports[key];
812
+ const bExport = b.exports[key];
813
+ if (!aExport || !bExport)
814
+ return false;
815
+ if (aExport.kind !== bExport.kind)
816
+ return false;
817
+ if (aExport.reactivity.type !== bExport.reactivity.type)
818
+ return false;
819
+ if (aExport.reactivity.type === "signal-api" && bExport.reactivity.type === "signal-api") {
820
+ if (!setsEqual(aExport.reactivity.signalProperties, bExport.reactivity.signalProperties)) {
821
+ return false;
822
+ }
823
+ if (!setsEqual(aExport.reactivity.plainProperties, bExport.reactivity.plainProperties)) {
824
+ return false;
825
+ }
826
+ }
827
+ }
828
+ return true;
829
+ }
830
+ function setsEqual(a, b) {
831
+ if (a.size !== b.size)
832
+ return false;
833
+ for (const item of a) {
834
+ if (!b.has(item))
835
+ return false;
836
+ }
837
+ return true;
838
+ }
108
839
  function createVertzBunPlugin(options) {
109
840
  const filter = options?.filter ?? /\.tsx$/;
110
841
  const hmr = options?.hmr ?? true;
111
842
  const fastRefresh = options?.fastRefresh ?? hmr;
843
+ const routeSplitting = options?.routeSplitting ?? false;
112
844
  const projectRoot = options?.projectRoot ?? process.cwd();
113
- const cssOutDir = options?.cssOutDir ?? resolve(projectRoot, ".vertz", "css");
845
+ const cssOutDir = options?.cssOutDir ?? resolve2(projectRoot, ".vertz", "css");
114
846
  const cssExtractor = new CSSExtractor;
115
847
  const componentAnalyzer = new ComponentAnalyzer;
116
848
  const logger = options?.logger;
117
849
  const diagnostics = options?.diagnostics;
118
850
  const fileExtractions = new Map;
119
851
  const cssSidecarMap = new Map;
120
- const srcDir = options?.srcDir ?? resolve(projectRoot, "src");
852
+ const srcDir = options?.srcDir ?? resolve2(projectRoot, "src");
121
853
  const frameworkManifestJson = __require(__require.resolve("@vertz/ui/reactivity.json"));
122
854
  const manifestResult = generateAllManifests({
123
855
  srcDir,
@@ -156,7 +888,31 @@ function createVertzBunPlugin(options) {
156
888
  }
157
889
  }
158
890
  diagnostics?.recordManifestPrepass(manifests.size, Math.round(manifestResult.durationMs), manifestResult.warnings.map((w) => ({ type: w.type, message: w.message })));
159
- mkdirSync(cssOutDir, { recursive: true });
891
+ const fieldSelectionManifest = new FieldSelectionManifest;
892
+ const fieldSelectionResolveImport = (specifier, fromFile) => {
893
+ return resolveModuleSpecifier(specifier, fromFile, {}, srcDir);
894
+ };
895
+ fieldSelectionManifest.setImportResolver(fieldSelectionResolveImport);
896
+ let fieldSelectionFileCount = 0;
897
+ for (const [filePath] of manifests) {
898
+ if (filePath.endsWith(".tsx")) {
899
+ try {
900
+ const sourceText = readFileSync3(filePath, "utf-8");
901
+ fieldSelectionManifest.registerFile(filePath, sourceText);
902
+ fieldSelectionFileCount++;
903
+ } catch {}
904
+ }
905
+ }
906
+ diagnostics?.recordFieldSelectionManifest(fieldSelectionFileCount);
907
+ const entitySchemaPath = options?.entitySchemaPath ?? resolve2(projectRoot, ".vertz", "generated", "entity-schema.json");
908
+ let entitySchema = loadEntitySchema(entitySchemaPath);
909
+ if (logger?.isEnabled("fields") && entitySchema) {
910
+ logger.log("fields", "entity-schema-loaded", {
911
+ path: entitySchemaPath,
912
+ entities: Object.keys(entitySchema).length
913
+ });
914
+ }
915
+ mkdirSync2(cssOutDir, { recursive: true });
160
916
  const plugin = {
161
917
  name: "vertz-bun-plugin",
162
918
  setup(build) {
@@ -166,32 +922,129 @@ function createVertzBunPlugin(options) {
166
922
  const source = await Bun.file(args.path).text();
167
923
  const relPath = relative(projectRoot, args.path);
168
924
  logger?.log("plugin", "onLoad", { file: relPath, bytes: source.length });
169
- const hydrationS = new MagicString(source);
170
- const hydrationProject = new Project({
925
+ let sourceAfterRouteSplit = source;
926
+ let routeSplitMap = null;
927
+ if (routeSplitting) {
928
+ const splitResult = transformRouteSplitting(source, args.path);
929
+ if (splitResult.transformed) {
930
+ sourceAfterRouteSplit = splitResult.code;
931
+ routeSplitMap = splitResult.map;
932
+ if (logger?.isEnabled("plugin")) {
933
+ for (const d of splitResult.diagnostics) {
934
+ logger.log("plugin", "route-split", {
935
+ file: relPath,
936
+ route: d.routePath,
937
+ import: d.importSource,
938
+ symbol: d.symbolName
939
+ });
940
+ }
941
+ for (const s of splitResult.skipped) {
942
+ logger.log("plugin", "route-split-skip", {
943
+ file: relPath,
944
+ route: s.routePath,
945
+ reason: s.reason
946
+ });
947
+ }
948
+ }
949
+ }
950
+ }
951
+ const hydrationS = new MagicString3(sourceAfterRouteSplit);
952
+ const hydrationProject = new Project2({
171
953
  useInMemoryFileSystem: true,
172
954
  compilerOptions: {
173
- jsx: ts2.JsxEmit.Preserve,
955
+ jsx: ts4.JsxEmit.Preserve,
174
956
  strict: true
175
957
  }
176
958
  });
177
- const hydrationSourceFile = hydrationProject.createSourceFile(args.path, source);
959
+ const hydrationSourceFile = hydrationProject.createSourceFile(args.path, sourceAfterRouteSplit);
178
960
  const hydrationTransformer = new HydrationTransformer;
179
961
  hydrationTransformer.transform(hydrationS, hydrationSourceFile);
180
962
  if (fastRefresh) {
181
963
  const relFilePath = relative(projectRoot, args.path);
182
964
  injectContextStableIds(hydrationS, hydrationSourceFile, relFilePath);
183
965
  }
966
+ {
967
+ const relFilePath = relative(projectRoot, args.path);
968
+ injectIslandIds(hydrationS, hydrationSourceFile, relFilePath);
969
+ }
184
970
  const hydratedCode = hydrationS.toString();
185
971
  const hydrationMap = hydrationS.generateMap({
186
972
  source: args.path,
187
973
  includeContent: true
188
974
  });
189
- const compileResult = compile(hydratedCode, {
975
+ const fieldSelectionResult = injectFieldSelection(args.path, hydratedCode, {
976
+ manifest: fieldSelectionManifest,
977
+ resolveImport: fieldSelectionResolveImport,
978
+ entitySchema
979
+ });
980
+ const codeForCompile = fieldSelectionResult.code;
981
+ if (logger?.isEnabled("fields") && fieldSelectionResult.diagnostics.length > 0) {
982
+ for (const diag of fieldSelectionResult.diagnostics) {
983
+ logger.log("fields", "query", {
984
+ file: relPath,
985
+ queryVar: diag.queryVar,
986
+ fields: diag.combinedFields,
987
+ opaque: diag.hasOpaqueAccess,
988
+ injected: diag.injected,
989
+ crossFile: diag.crossFileFields.length
990
+ });
991
+ }
992
+ }
993
+ if (diagnostics && fieldSelectionResult.diagnostics.length > 0) {
994
+ diagnostics.recordFieldSelection(relPath, {
995
+ queries: fieldSelectionResult.diagnostics.map((d) => ({
996
+ queryVar: d.queryVar,
997
+ fields: d.combinedFields,
998
+ hasOpaqueAccess: d.hasOpaqueAccess,
999
+ crossFileFields: d.crossFileFields,
1000
+ injected: d.injected
1001
+ }))
1002
+ });
1003
+ }
1004
+ const imageOutputDir = resolve2(projectRoot, ".vertz", "images");
1005
+ const imageQueue = [];
1006
+ const imageResult = transformImages(codeForCompile, args.path, {
1007
+ projectRoot,
1008
+ resolveImagePath: (src) => resolveImageSrc(src, args.path, projectRoot),
1009
+ getImageOutputPaths: (sourcePath, w, h, q, f) => {
1010
+ const paths = computeImageOutputPaths(sourcePath, w, h, q, f);
1011
+ if (!paths) {
1012
+ return {
1013
+ webp1x: sourcePath,
1014
+ webp2x: sourcePath,
1015
+ fallback: sourcePath,
1016
+ fallbackType: "image/jpeg"
1017
+ };
1018
+ }
1019
+ imageQueue.push({
1020
+ sourcePath,
1021
+ width: w,
1022
+ height: h,
1023
+ quality: q,
1024
+ fit: f,
1025
+ outputDir: imageOutputDir
1026
+ });
1027
+ return paths;
1028
+ }
1029
+ });
1030
+ const codeAfterImageTransform = imageResult.code;
1031
+ if (imageQueue.length > 0) {
1032
+ await Promise.all(imageQueue.map((opts) => processImage({ ...opts, fit: opts.fit })));
1033
+ }
1034
+ const compileResult = compile(codeAfterImageTransform, {
190
1035
  filename: args.path,
191
1036
  target: options?.target,
192
1037
  manifests: getManifestsRecord()
193
1038
  });
194
- const remapped = remapping([compileResult.map, hydrationMap], () => null);
1039
+ const mapsToChain = [compileResult.map];
1040
+ if (imageResult.map) {
1041
+ mapsToChain.push(imageResult.map);
1042
+ }
1043
+ mapsToChain.push(hydrationMap);
1044
+ if (routeSplitMap) {
1045
+ mapsToChain.push(routeSplitMap);
1046
+ }
1047
+ const remapped = remapping(mapsToChain, () => null);
195
1048
  const extraction = cssExtractor.extract(source, args.path);
196
1049
  let cssImportLine = "";
197
1050
  if (extraction.css.length > 0) {
@@ -199,8 +1052,8 @@ function createVertzBunPlugin(options) {
199
1052
  if (hmr) {
200
1053
  const hash = filePathHash(args.path);
201
1054
  const cssFileName = `${hash}.css`;
202
- const cssFilePath = resolve(cssOutDir, cssFileName);
203
- writeFileSync(cssFilePath, extraction.css);
1055
+ const cssFilePath = resolve2(cssOutDir, cssFileName);
1056
+ writeFileSync2(cssFilePath, extraction.css);
204
1057
  cssSidecarMap.set(args.path, cssFilePath);
205
1058
  const relPath2 = relative(dirname(args.path), cssFilePath);
206
1059
  const importPath = relPath2.startsWith(".") ? relPath2 : `./${relPath2}`;
@@ -247,6 +1100,7 @@ import.meta.hot.accept();
247
1100
  if (logger?.isEnabled("plugin")) {
248
1101
  const durationMs = Math.round(performance.now() - startMs);
249
1102
  const stages = [
1103
+ routeSplitting && sourceAfterRouteSplit !== source ? "routeSplit" : null,
250
1104
  "hydration",
251
1105
  fastRefresh ? "stableIds" : null,
252
1106
  "compile",
@@ -266,9 +1120,102 @@ import.meta.hot.accept();
266
1120
  throw err;
267
1121
  }
268
1122
  });
1123
+ if (routeSplitting) {
1124
+ build.onLoad({ filter: /\.ts$/ }, async (args) => {
1125
+ const source = await Bun.file(args.path).text();
1126
+ if (!source.includes("defineRoutes(") || !source.includes("@vertz/ui")) {
1127
+ return { contents: source, loader: "ts" };
1128
+ }
1129
+ const splitResult = transformRouteSplitting(source, args.path);
1130
+ if (splitResult.transformed && logger?.isEnabled("plugin")) {
1131
+ const relPath = relative(projectRoot, args.path);
1132
+ for (const d of splitResult.diagnostics) {
1133
+ logger.log("plugin", "route-split", {
1134
+ file: relPath,
1135
+ route: d.routePath,
1136
+ import: d.importSource,
1137
+ symbol: d.symbolName
1138
+ });
1139
+ }
1140
+ for (const s of splitResult.skipped) {
1141
+ logger.log("plugin", "route-split-skip", {
1142
+ file: relPath,
1143
+ route: s.routePath,
1144
+ reason: s.reason
1145
+ });
1146
+ }
1147
+ }
1148
+ let contents = splitResult.code;
1149
+ if (splitResult.transformed && splitResult.map) {
1150
+ const mapBase64 = Buffer.from(splitResult.map.toString()).toString("base64");
1151
+ contents += `
1152
+ //# sourceMappingURL=data:application/json;base64,${mapBase64}`;
1153
+ }
1154
+ return { contents, loader: "ts" };
1155
+ });
1156
+ }
1157
+ }
1158
+ };
1159
+ function updateManifest(filePath, sourceText) {
1160
+ const oldManifest = manifests.get(filePath);
1161
+ const { manifest: newManifest, warnings } = regenerateFileManifest(filePath, sourceText, manifests, { srcDir });
1162
+ const changed = !manifestsEqual(oldManifest, newManifest);
1163
+ if (changed) {
1164
+ manifestsRecord = null;
1165
+ }
1166
+ if (filePath.endsWith(".tsx")) {
1167
+ fieldSelectionManifest.updateFile(filePath, sourceText);
1168
+ }
1169
+ if (logger?.isEnabled("manifest")) {
1170
+ const exportShapes = {};
1171
+ for (const [name, info] of Object.entries(newManifest.exports)) {
1172
+ exportShapes[name] = info.reactivity.type;
1173
+ }
1174
+ logger.log("manifest", "hmr-update", {
1175
+ file: relative(projectRoot, filePath),
1176
+ changed,
1177
+ exports: exportShapes
1178
+ });
1179
+ for (const warning of warnings) {
1180
+ logger.log("manifest", "warning", { type: warning.type, message: warning.message });
1181
+ }
1182
+ }
1183
+ return { changed };
1184
+ }
1185
+ function deleteManifest(filePath) {
1186
+ const existed = manifests.delete(filePath);
1187
+ if (existed) {
1188
+ manifestsRecord = null;
1189
+ if (logger?.isEnabled("manifest")) {
1190
+ logger.log("manifest", "hmr-delete", {
1191
+ file: relative(projectRoot, filePath)
1192
+ });
1193
+ }
1194
+ }
1195
+ fieldSelectionManifest.deleteFile(filePath);
1196
+ return existed;
1197
+ }
1198
+ function reloadEntitySchema() {
1199
+ const newSchema = loadEntitySchema(entitySchemaPath);
1200
+ const changed = JSON.stringify(newSchema) !== JSON.stringify(entitySchema);
1201
+ entitySchema = newSchema;
1202
+ if (logger?.isEnabled("fields")) {
1203
+ logger.log("fields", "entity-schema-reload", {
1204
+ path: entitySchemaPath,
1205
+ entities: newSchema ? Object.keys(newSchema).length : 0,
1206
+ changed
1207
+ });
269
1208
  }
1209
+ return changed;
1210
+ }
1211
+ return {
1212
+ plugin,
1213
+ fileExtractions,
1214
+ cssSidecarMap,
1215
+ updateManifest,
1216
+ deleteManifest,
1217
+ reloadEntitySchema
270
1218
  };
271
- return { plugin, fileExtractions, cssSidecarMap };
272
1219
  }
273
1220
  export {
274
1221
  createVertzBunPlugin