@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.
- package/dist/bun-dev-server.d.ts +36 -1
- package/dist/bun-dev-server.js +352 -23
- package/dist/bun-plugin/index.d.ts +73 -1
- package/dist/bun-plugin/index.js +964 -17
- package/dist/dom-shim/index.js +1 -1
- package/dist/index.d.ts +84 -12
- package/dist/index.js +127 -41
- package/dist/shared/{chunk-98972e43.js → chunk-969qgkdf.js} +186 -23
- package/dist/shared/{chunk-n1arq9xq.js → chunk-9jjdzz8c.js} +2 -2
- package/dist/shared/chunk-gggnhyqj.js +57 -0
- package/dist/ssr/index.d.ts +116 -9
- package/dist/ssr/index.js +79 -2
- package/package.json +8 -5
package/dist/bun-plugin/index.js
CHANGED
|
@@ -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
|
|
18
|
-
import { Project, ts as
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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:
|
|
955
|
+
jsx: ts4.JsxEmit.Preserve,
|
|
174
956
|
strict: true
|
|
175
957
|
}
|
|
176
958
|
});
|
|
177
|
-
const hydrationSourceFile = hydrationProject.createSourceFile(args.path,
|
|
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
|
|
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
|
|
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 =
|
|
203
|
-
|
|
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
|