@tailorkit/vite 0.0.0-stage → 0.1.0-beta.32

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/index.mjs ADDED
@@ -0,0 +1,788 @@
1
+ import { a as createDefineViewMatcher, c as getBuildEnvironment, d as createTailorKitUploadManifest, f as tailorkitUploadManifestSchema, i as inferFileView, n as fileRoutesPlugin, o as checkAppSchema, s as isMissingFile, t as createFileRouteEntry, u as appDatabasePaths } from "./file-routes-CjOIw7fH.mjs";
2
+ import { loadTailorKitConfig } from "./config-loader.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { z } from "zod";
5
+ import { execFile } from "node:child_process";
6
+ import { cp, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { promisify } from "node:util";
9
+ import { build, parseSync, transformWithOxc } from "vite";
10
+ import { createHash } from "node:crypto";
11
+ //#region ../asset-delivery/dist/logo-validation.mjs
12
+ const maxLogoBytes = 262144;
13
+ const maxLogoDimension = 2048;
14
+ const readPngDimensions = (content) => {
15
+ if (content.length < 33 || ![
16
+ 137,
17
+ 80,
18
+ 78,
19
+ 71,
20
+ 13,
21
+ 10,
22
+ 26,
23
+ 10
24
+ ].every((byte, index) => content[index] === byte)) throw new Error("Logo content is not a valid PNG file.");
25
+ const view = new DataView(content.buffer, content.byteOffset, content.byteLength);
26
+ const isIhdr = content[12] === 73 && content[13] === 72 && content[14] === 68 && content[15] === 82;
27
+ if (view.getUint32(8) !== 13 || !isIhdr) throw new Error("Logo content is not a valid PNG file.");
28
+ return {
29
+ height: view.getUint32(20),
30
+ width: view.getUint32(16)
31
+ };
32
+ };
33
+ const readWebpDimensions = (content) => {
34
+ const text = (start, end) => new TextDecoder().decode(content.subarray(start, end));
35
+ if (content.length < 30 || text(0, 4) !== "RIFF" || text(8, 12) !== "WEBP") throw new Error("Logo content is not a valid WebP file.");
36
+ const view = new DataView(content.buffer, content.byteOffset, content.byteLength);
37
+ const chunk = text(12, 16);
38
+ if (chunk === "VP8X") return {
39
+ height: 1 + view.getUint16(27, true) + (content[29] ?? 0) * 65536,
40
+ width: 1 + view.getUint16(24, true) + (content[26] ?? 0) * 65536
41
+ };
42
+ if (chunk === "VP8L" && content[20] === 47) {
43
+ const bits = view.getUint32(21, true);
44
+ return {
45
+ height: Math.floor(bits / 16384) % 16384 + 1,
46
+ width: bits % 16384 + 1
47
+ };
48
+ }
49
+ if (chunk === "VP8 " && content[23] === 157 && content[24] === 1 && content[25] === 42) return {
50
+ height: view.getUint16(28, true) % 16384,
51
+ width: view.getUint16(26, true) % 16384
52
+ };
53
+ throw new Error("Logo content is not a supported WebP file.");
54
+ };
55
+ const validateSvg = (content) => {
56
+ const svg = new TextDecoder("utf-8", {
57
+ fatal: true,
58
+ ignoreBOM: false
59
+ }).decode(content);
60
+ if (!/<svg(?:\s|>)/iu.test(svg)) throw new Error("Logo content is not a valid SVG file.");
61
+ if (/<(?:script|foreignObject|iframe|object|embed)(?:\s|>)/iu.test(svg) || /\son[a-z]+\s*=/iu.test(svg) || /(?:href|src)\s*=\s*["'](?!#)/iu.test(svg) || /(?:@import|url\(\s*(?!["']?#))/iu.test(svg) || /<!DOCTYPE|<!ENTITY|<\?xml-stylesheet/iu.test(svg)) throw new Error("SVG logos cannot contain scripts or external resources.");
62
+ };
63
+ const validateLogoAsset = (content, contentType) => {
64
+ if (content.byteLength < 1 || content.byteLength > 262144) throw new Error(`Logo must be between 1 and ${maxLogoBytes} bytes.`);
65
+ if (contentType === "image/svg+xml") {
66
+ validateSvg(content);
67
+ return {};
68
+ }
69
+ const dimensions = contentType === "image/png" ? readPngDimensions(content) : readWebpDimensions(content);
70
+ if (dimensions.width < 1 || dimensions.height < 1 || dimensions.width > 2048 || dimensions.height > 2048) throw new Error(`Raster logos cannot exceed ${maxLogoDimension} by ${maxLogoDimension} pixels.`);
71
+ return dimensions;
72
+ };
73
+ //#endregion
74
+ //#region src/builder/client-views.ts
75
+ const inspectClient = `
76
+ import { readFile } from "node:fs/promises";
77
+ const source = await readFile(process.argv[1], "utf8");
78
+ const { default: client } = await import("data:text/javascript;base64," + Buffer.from(source).toString("base64"));
79
+ if (!client?.slots || typeof client.slots !== "object") throw new Error("Invalid generated app client: missing slots");
80
+ const instanceResolvers = [];
81
+ const views = Object.entries(client.slots).flatMap(([slot, definitions]) =>
82
+ Object.entries(definitions).flatMap(([path, view]) => {
83
+ if (view === false) return [{ slot, path, disabled: true }];
84
+ const resolver = view?.instances?.resolver;
85
+ if (resolver !== undefined) instanceResolvers.push({ slot, path, resolver });
86
+ return [{ slot, path, ...(resolver !== undefined ? { instances: true } : {}) }];
87
+ })
88
+ );
89
+ process.stdout.write("\\nTAILORKIT_VIEWS=" + JSON.stringify({ views, instanceResolvers }));
90
+ process.exit(0);
91
+ `;
92
+ const readClientManifest = async (filename) => {
93
+ try {
94
+ const { stdout } = await promisify(execFile)(process.execPath, [
95
+ "--input-type=module",
96
+ "--eval",
97
+ inspectClient,
98
+ filename
99
+ ], {
100
+ maxBuffer: 2097152,
101
+ timeout: 1e4
102
+ });
103
+ const result = stdout.slice(stdout.lastIndexOf("\nTAILORKIT_VIEWS=") + 17);
104
+ return z.object({
105
+ instanceResolvers: z.array(z.object({
106
+ path: z.string(),
107
+ resolver: z.string().regex(/^i[0-9a-f]{24}$/u),
108
+ slot: z.string()
109
+ })),
110
+ views: tailorkitUploadManifestSchema.shape.views.unwrap()
111
+ }).parse(JSON.parse(result));
112
+ } catch (error) {
113
+ throw new Error("Unable to read app views during the build. Keep browser-only side effects inside components or effects. Check the default exports of your roots, layouts and .view.tsx files.", { cause: error });
114
+ }
115
+ };
116
+ //#endregion
117
+ //#region src/builder/instances.ts
118
+ const isStringValue = (value) => typeof value === "string";
119
+ const requireValue$1 = (value) => {
120
+ if (value === null || value === void 0) throw new Error("Expected a defined value.");
121
+ return value;
122
+ };
123
+ const suffix = "?tailorkit-instances";
124
+ const browserGlobals = /* @__PURE__ */ new Set([
125
+ "window",
126
+ "document",
127
+ "navigator",
128
+ "localStorage",
129
+ "sessionStorage"
130
+ ]);
131
+ const isNode = (value) => value !== void 0 && value !== null && value !== false && value !== "" && value !== 0 && !Number.isNaN(value) && value !== 0n && typeof value === "object" && "type" in value;
132
+ const children = (node) => Object.values(node).flatMap((value) => (() => {
133
+ if (Array.isArray(value)) return value.filter(isNode);
134
+ if (isNode(value)) return [value];
135
+ return [];
136
+ })());
137
+ const walk = (node, visit, ancestors = []) => {
138
+ visit(node, ancestors);
139
+ for (const child of children(node)) walk(child, visit, [...ancestors, node]);
140
+ };
141
+ const boundNames = (node) => {
142
+ if (!node) return [];
143
+ switch (node.type) {
144
+ case "Identifier": return [node.name];
145
+ case "RestElement": return boundNames(node.argument);
146
+ case "AssignmentPattern": return boundNames(node.left);
147
+ case "ArrayPattern": return node.elements.flatMap(boundNames);
148
+ case "ObjectPattern": return node.properties.flatMap((p) => boundNames(p.type === "Property" ? p.value : p.argument));
149
+ default: return [];
150
+ }
151
+ };
152
+ const declarations = (node) => {
153
+ if (node.type === "VariableDeclaration") return node.declarations.flatMap((d) => boundNames(d.id));
154
+ if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") return boundNames(node.id);
155
+ return [];
156
+ };
157
+ const isFunction = (node) => [
158
+ "FunctionDeclaration",
159
+ "FunctionExpression",
160
+ "ArrowFunctionExpression"
161
+ ].includes(node.type);
162
+ /** Lexical references, excluding property names and bindings local to a function/block. */
163
+ const freeNames = (root) => {
164
+ const references = /* @__PURE__ */ new Set();
165
+ const visit = (node, parent) => {
166
+ if (node.type === "Identifier") {
167
+ if (!parent.has(node.name)) references.add(node.name);
168
+ return;
169
+ }
170
+ if (node.type === "ImportDeclaration") return;
171
+ if (node.type === "ExportSpecifier") {
172
+ visit(node.local, parent);
173
+ return;
174
+ }
175
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
176
+ const scope = /* @__PURE__ */ new Set([
177
+ ...parent,
178
+ ...boundNames(node.id),
179
+ ...node.params.flatMap(boundNames)
180
+ ]);
181
+ const hoisted = (body) => {
182
+ if (isFunction(body)) return;
183
+ if (body.type === "VariableDeclaration" && body.kind === "var") for (const name of declarations(body)) scope.add(name);
184
+ for (const child of children(body)) hoisted(child);
185
+ };
186
+ if (node.body) hoisted(node.body);
187
+ for (const p of node.params) pattern(p, scope);
188
+ if (node.body) visit(node.body, scope);
189
+ return;
190
+ }
191
+ if (node.type === "BlockStatement" || node.type === "Program") {
192
+ const scope = /* @__PURE__ */ new Set([...parent, ...node.body.flatMap(declarations)]);
193
+ for (const statement of node.body) visit(statement, scope);
194
+ return;
195
+ }
196
+ if (node.type === "VariableDeclarator") {
197
+ pattern(node.id, parent);
198
+ if (node.init) visit(node.init, parent);
199
+ return;
200
+ }
201
+ if (node.type === "Property" || node.type === "MethodDefinition" || node.type === "PropertyDefinition") {
202
+ if (node.computed) visit(node.key, parent);
203
+ if (node.value) visit(node.value, parent);
204
+ return;
205
+ }
206
+ if (node.type === "MemberExpression") {
207
+ visit(node.object, parent);
208
+ if (node.computed) visit(node.property, parent);
209
+ return;
210
+ }
211
+ if (node.type === "CatchClause") {
212
+ const scope = /* @__PURE__ */ new Set([...parent, ...boundNames(node.param)]);
213
+ if (node.param) pattern(node.param, scope);
214
+ visit(node.body, scope);
215
+ return;
216
+ }
217
+ if (node.type === "ForStatement" || node.type === "ForInStatement" || node.type === "ForOfStatement") {
218
+ const declaration = node.type === "ForStatement" ? node.init : node.left;
219
+ const scope = /* @__PURE__ */ new Set([...parent, ...declaration ? declarations(declaration) : []]);
220
+ for (const child of children(node)) visit(child, scope);
221
+ return;
222
+ }
223
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
224
+ const scope = /* @__PURE__ */ new Set([...parent, ...boundNames(node.id)]);
225
+ if (node.superClass) visit(node.superClass, scope);
226
+ visit(node.body, scope);
227
+ return;
228
+ }
229
+ if (node.type === "LabeledStatement") {
230
+ visit(node.body, parent);
231
+ return;
232
+ }
233
+ if (node.type === "BreakStatement" || node.type === "ContinueStatement") return;
234
+ for (const child of children(node)) visit(child, parent);
235
+ };
236
+ const pattern = (node, scope) => {
237
+ if (node.type === "AssignmentPattern") {
238
+ visit(node.right, scope);
239
+ pattern(node.left, scope);
240
+ } else if (node.type === "ObjectPattern") for (const p of node.properties) if (p.type === "Property") {
241
+ if (p.computed) visit(p.key, scope);
242
+ pattern(p.value, scope);
243
+ } else pattern(p.argument, scope);
244
+ else if (node.type === "ArrayPattern") {
245
+ for (const element of node.elements) if (element) pattern(element, scope);
246
+ } else if (node.type === "RestElement") pattern(node.argument, scope);
247
+ };
248
+ visit(root, /* @__PURE__ */ new Set());
249
+ return references;
250
+ };
251
+ const property = (node, name) => node.properties.find((p) => p.type === "Property" && !p.computed && (p.key.type === "Identifier" ? p.key.name === name : p.key.type === "Literal" && p.key.value === name));
252
+ const parse = (filename, source) => {
253
+ const result = parseSync(filename, source);
254
+ if (result.errors.length) throw new Error(`Unable to parse ${filename}: ${result.errors[0]?.message}`);
255
+ return result.program;
256
+ };
257
+ const units = (program, source) => {
258
+ const imports = [];
259
+ const statements = [];
260
+ for (const statement of program.body) {
261
+ if (statement.type === "ImportDeclaration") {
262
+ imports.push(statement);
263
+ continue;
264
+ }
265
+ const exported = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration";
266
+ const node = statement.type === "ExportNamedDeclaration" && statement.declaration ? statement.declaration : statement;
267
+ if (node.type === "VariableDeclaration") for (const d of node.declarations) statements.push({
268
+ code: `${exported ? "export " : ""}${node.kind} ${source.slice(d.start, d.end)};`,
269
+ exported,
270
+ names: boundNames(d.id),
271
+ node: d
272
+ });
273
+ else statements.push({
274
+ code: source.slice(statement.start, statement.end),
275
+ exported,
276
+ names: declarations(node),
277
+ node
278
+ });
279
+ }
280
+ return {
281
+ imports,
282
+ statements
283
+ };
284
+ };
285
+ const closure = (roots, statements) => {
286
+ const bindings = new Map(statements.flatMap((u) => u.names.map((name) => [name, u])));
287
+ const names = /* @__PURE__ */ new Set();
288
+ const selected = /* @__PURE__ */ new Set();
289
+ const add = (name) => {
290
+ if (names.has(name)) return;
291
+ names.add(name);
292
+ const unit = bindings.get(name);
293
+ if (!unit || selected.has(unit)) return;
294
+ selected.add(unit);
295
+ for (const ref of freeNames(unit.node)) add(ref);
296
+ };
297
+ for (const root of roots) add(root);
298
+ return {
299
+ names,
300
+ selected
301
+ };
302
+ };
303
+ const renderImports = (imports, needed, source, sideEffects) => imports.flatMap((i) => {
304
+ if (!i.specifiers.length) return sideEffects ? [source.slice(i.start, i.end)] : [];
305
+ const parts = i.specifiers.filter((s) => needed(s.local.name));
306
+ if (!parts.length) return [];
307
+ const defaultImport = parts.find((s) => s.type === "ImportDefaultSpecifier");
308
+ const namespace = parts.find((s) => s.type === "ImportNamespaceSpecifier");
309
+ const named = parts.filter((s) => s.type === "ImportSpecifier");
310
+ return [`import ${[
311
+ defaultImport?.local.name,
312
+ namespace ? `* as ${namespace.local.name}` : void 0,
313
+ named.length ? `{ ${named.map((s) => `${source.slice(s.imported.start, s.imported.end)} as ${s.local.name}`).join(", ")} }` : void 0
314
+ ].filter(Boolean).join(", ")} from ${JSON.stringify(i.source.value)};`];
315
+ }).join("\n");
316
+ const extractInstances = async (filename, initialSource, root, includedNames) => {
317
+ let source = initialSource;
318
+ source = inferFileView(source, filename, root);
319
+ source = (await transformWithOxc(source, filename, {
320
+ jsx: { importSource: "preact" },
321
+ sourcemap: false
322
+ })).code;
323
+ const program = parse(filename.replace(/\.[^.]+$/u, ".js"), source);
324
+ const isDefineViewCall = createDefineViewMatcher(program);
325
+ let resolvers = [];
326
+ walk(program, (node, ancestors) => {
327
+ if (!isDefineViewCall(node)) return;
328
+ const [options] = node.arguments;
329
+ if (options?.type !== "ObjectExpression") return;
330
+ const instances = property(options, "instances");
331
+ if (!instances) return;
332
+ const fail = (message) => {
333
+ throw new Error(`${filename}: ${message}`);
334
+ };
335
+ if (ancestors.some(isFunction)) fail("defineView with instances must be declared at module scope.");
336
+ const owner = ancestors.findLast((n) => n.type === "VariableDeclarator");
337
+ const viewName = owner?.type === "VariableDeclarator" && owner.id.type === "Identifier" ? owner.id.name : "default";
338
+ const view = property(options, "view")?.value;
339
+ if (view?.type !== "Literal" || !isStringValue(view.value)) fail("Instance view paths must be string literals.");
340
+ if (instances.value.type !== "ObjectExpression") fail("instances must be an inline object with dataSchema and resolve.");
341
+ const config = instances.value;
342
+ if (config.properties.some((p) => p.type !== "Property" || p.computed || p.kind !== "init")) fail("Instance options cannot contain spreads, getters or computed properties.");
343
+ const resolve = property(config, "resolve");
344
+ const schema = property(config, "dataSchema");
345
+ if (!resolve || !schema || resolve.method || !["ArrowFunctionExpression", "FunctionExpression"].includes(resolve.value.type)) fail("instances requires dataSchema and an inline resolve function.");
346
+ const name = `i${createHash("sha256").update(`${path.relative(root, filename).split(path.sep).join("/")}\0${viewName}\0${view.value}`).digest("hex").slice(0, 24)}`;
347
+ if (resolvers.some((r) => r.name === name)) fail("Instance resolvers must have distinct module-level variable names.");
348
+ resolvers.push({
349
+ call: node,
350
+ instances,
351
+ name,
352
+ resolve: requireValue$1(resolve),
353
+ schema: requireValue$1(schema),
354
+ view: view.value
355
+ });
356
+ });
357
+ if (includedNames) {
358
+ resolvers = resolvers.filter((r) => includedNames.includes(r.name));
359
+ if (resolvers.length !== includedNames.length) throw new Error(`${filename}: Instance definitions changed; rebuild the app client.`);
360
+ }
361
+ if (!resolvers.length) return;
362
+ const original = units(program, source);
363
+ const server = closure(resolvers.flatMap((r) => [...freeNames(r.resolve.value), ...freeNames(r.schema.value)]), original.statements);
364
+ for (const name of server.names) if (browserGlobals.has(name)) throw new Error(`${filename}: Instance resolvers cannot access browser global ${name}.`);
365
+ for (const unit of server.selected) if (resolvers.some((r) => unit.node.start <= r.call.start && unit.node.end >= r.call.end)) throw new Error(`${filename}: Instance resolvers cannot capture a view definition. Use the supplied context and queries.`);
366
+ const serverSource = [
367
+ renderImports(original.imports, (name) => server.names.has(name), source, false),
368
+ ...original.statements.filter((u) => server.selected.has(u)).map((u) => u.code.replace(/^export /u, "")),
369
+ `export const resolvers = { ${resolvers.map((r) => `${r.name}: { dataSchema: ${source.slice(r.schema.value.start, r.schema.value.end)}, resolve: ${source.slice(r.resolve.value.start, r.resolve.value.end)} }`).join(", ")} };`
370
+ ].join("\n");
371
+ let browserSource = source;
372
+ for (const r of [...resolvers].toSorted((a, b) => b.instances.value.start - a.instances.value.start)) browserSource = `${browserSource.slice(0, r.instances.value.start)}{ resolver: ${JSON.stringify(r.name)} }${browserSource.slice(r.instances.value.end)}`;
373
+ const browser = units(parse("browser.js", browserSource), browserSource);
374
+ const serverBindings = new Set([...server.selected].flatMap((u) => u.names));
375
+ const browserRoots = browser.statements.filter((u) => u.exported || !u.names.length || !u.names.some((name) => serverBindings.has(name)));
376
+ const browserNeeded = closure(browserRoots.flatMap((u) => [...freeNames(u.node), ...u.names]), browser.statements);
377
+ return {
378
+ browser: [renderImports(browser.imports, (name) => !server.names.has(name) || browserNeeded.names.has(name), browserSource, true), ...browser.statements.filter((u) => browserRoots.includes(u) || browserNeeded.selected.has(u)).map((u) => u.code)].join("\n"),
379
+ names: resolvers.map((r) => r.name),
380
+ server: serverSource
381
+ };
382
+ };
383
+ const instanceExtractionPlugin = (root, serverConfigured, modules) => ({
384
+ enforce: "pre",
385
+ name: "tailorkit-instance-extraction",
386
+ transform: async (source, filename) => {
387
+ if (filename.includes("node_modules") || !/\.[cm]?[jt]sx?$/u.test(filename) || !source.includes("instances") && !modules.has(filename)) return;
388
+ const result = await extractInstances(filename, source, root());
389
+ if (!result) {
390
+ modules.delete(filename);
391
+ return;
392
+ }
393
+ if (!serverConfigured()) throw new Error("View instances require server configuration in tailorkit.config.ts.");
394
+ modules.set(filename, {
395
+ filename,
396
+ names: result.names
397
+ });
398
+ return {
399
+ code: result.browser,
400
+ map: null
401
+ };
402
+ }
403
+ });
404
+ const instanceServerPlugin = (root, modules) => ({
405
+ enforce: "pre",
406
+ async load(id) {
407
+ if (!id.endsWith(suffix)) return;
408
+ const filename = id.slice(0, -20);
409
+ this.addWatchFile(filename);
410
+ const names = modules.find((module) => module.filename === filename)?.names;
411
+ const result = await extractInstances(filename, await readFile(filename, "utf-8"), root, names);
412
+ if (!result) throw new Error(`Instance resolver missing from ${filename}; rebuild the app client.`);
413
+ return result.server;
414
+ },
415
+ name: "tailorkit-instance-server-modules",
416
+ resolveId: (id) => id.endsWith(suffix) ? id : void 0
417
+ });
418
+ const instanceServerImports = (modules) => modules.map((m, i) => `import { resolvers as instances${i} } from ${JSON.stringify(m.filename + suffix)};`).join("\n");
419
+ const instanceServerBindings = (modules, registrations) => `[${registrations.map(({ slot, path: viewPath, resolver }) => {
420
+ const index = modules.findIndex((module) => module.names.includes(resolver));
421
+ if (index === -1) throw new Error("Instance resolver was not extracted by the app build.");
422
+ return `{ slot: ${JSON.stringify(slot)}, path: ${JSON.stringify(viewPath)}, ...instances${index}[${JSON.stringify(resolver)}] }`;
423
+ }).join(", ")}]`;
424
+ //#endregion
425
+ //#region src/builder/migrations.ts
426
+ /** Read the timestamped migration folders produced by the workspace's Drizzle Kit version. */
427
+ const readAppMigrations = async (directory, optional = false) => {
428
+ const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
429
+ if (isMissingFile(error)) {
430
+ if (optional) return [];
431
+ throw new Error(`App migrations directory is missing: ${directory}. Run tailorkit db generate and commit migrations before building.`, { cause: error });
432
+ }
433
+ throw error;
434
+ });
435
+ const migrations = [];
436
+ const files = [directory];
437
+ const names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).toSorted();
438
+ for (const name of names) {
439
+ if (!/^\d{14}_[\w-]+$/u.test(name)) throw new Error(`Unsupported app migration folder: ${name}. Use Drizzle Kit v1 migrations.`);
440
+ const file = path.join(directory, name, "migration.sql");
441
+ const sql = await readFile(file, "utf-8");
442
+ const statements = sql.split("--> statement-breakpoint").map((part) => part.trim()).filter(Boolean);
443
+ if (!statements.length) throw new Error(`App migration is empty: ${name}`);
444
+ migrations.push({
445
+ hash: createHash("sha256").update(sql).digest("hex"),
446
+ id: name,
447
+ statements
448
+ });
449
+ files.push(file);
450
+ }
451
+ return {
452
+ files,
453
+ migrations
454
+ };
455
+ };
456
+ //#endregion
457
+ //#region src/builder/server.ts
458
+ const containsImplementation = (parent, child) => {
459
+ const relative = path.relative(parent, child);
460
+ return relative === "" || !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`);
461
+ };
462
+ const overlaps = (source, destination) => {
463
+ const contains = containsImplementation;
464
+ return contains(source, destination) || contains(destination, source);
465
+ };
466
+ /** Check before either build can empty an output directory. */
467
+ const validateServerBuildOutput = (loaded, output) => {
468
+ if (!loaded.config.server) return;
469
+ const migrationsDirectory = appDatabasePaths(loaded.root, loaded.config.server.migrations).migrations;
470
+ for (const name of [
471
+ "server",
472
+ "client",
473
+ "migrations",
474
+ "tmp"
475
+ ]) if (overlaps(migrationsDirectory, path.join(output, name))) throw new Error("App build output must not overlap migration source");
476
+ };
477
+ /** Emitted by this SDK's builder; breaking interface changes increment apiVersion. */
478
+ const runtimeManifest = {
479
+ apiVersion: 1,
480
+ requires: [
481
+ "database",
482
+ "actions",
483
+ "database-relations",
484
+ "tools"
485
+ ]
486
+ };
487
+ /** Bundle the private app backend and migrations. */
488
+ const buildServer = async (loaded, watch = false, buildOutDir, instances = [], registrations = [], viteConfig = {}) => {
489
+ const config = loaded.config.server;
490
+ if (!config) return;
491
+ const output = buildOutDir ?? path.resolve(loaded.root, ".tailorkit");
492
+ const directory = path.join(output, "server");
493
+ const migrationsDirectory = appDatabasePaths(loaded.root, config.migrations).migrations;
494
+ const migrationsOutput = path.join(output, "migrations");
495
+ const temporaryRoot = path.join(output, "tmp");
496
+ validateServerBuildOutput(loaded, output);
497
+ await rm(directory, {
498
+ force: true,
499
+ recursive: true
500
+ });
501
+ await mkdir(directory, { recursive: true });
502
+ await mkdir(temporaryRoot, { recursive: true });
503
+ const temporary = await mkdtemp(path.join(temporaryRoot, "server-"));
504
+ const entry = path.join(temporary, "entry.mjs");
505
+ let watching = false;
506
+ let ready;
507
+ const initialBuild = watch ? (async () => {
508
+ const { promise: pendingPromise, resolve, reject: rejectPending } = Promise.withResolvers();
509
+ try {
510
+ ready = resolve;
511
+ } catch (error) {
512
+ rejectPending(error);
513
+ }
514
+ await pendingPromise;
515
+ })() : void 0;
516
+ try {
517
+ await writeFile(entry, `${instances.length ? `import app from ${JSON.stringify(path.resolve(loaded.root, config.entry ?? "src/server.ts"))};\nimport { withInstanceResolvers } from "tailorkit/server";\n${instanceServerImports(instances)}\nexport default withInstanceResolvers(app, ${instanceServerBindings(instances, registrations)});` : `export { default } from ${JSON.stringify(path.resolve(loaded.root, config.entry ?? "src/server.ts"))};`}
518
+ export { default as migrations } from "tailorkit:migrations";
519
+ export const runtimeManifest = ${JSON.stringify(runtimeManifest)};
520
+ `);
521
+ const result = await build({
522
+ build: {
523
+ emptyOutDir: false,
524
+ minify: "oxc",
525
+ outDir: directory,
526
+ rolldownOptions: {
527
+ external: [],
528
+ output: {
529
+ entryFileNames: "server.js",
530
+ inlineDynamicImports: true,
531
+ minify: true,
532
+ minifyInternalExports: true
533
+ }
534
+ },
535
+ ssr: entry,
536
+ target: "es2022",
537
+ watch: watch ? {} : null
538
+ },
539
+ configFile: false,
540
+ define: viteConfig.define,
541
+ mode: viteConfig.mode,
542
+ plugins: [
543
+ ...viteConfig.plugins ?? [],
544
+ instanceServerPlugin(loaded.root, instances),
545
+ {
546
+ closeWatcher: async () => {
547
+ await rm(temporary, {
548
+ force: true,
549
+ recursive: true
550
+ });
551
+ },
552
+ async load(id) {
553
+ if (id !== "\0tailorkit:migrations") return;
554
+ const { migrations, files } = await readAppMigrations(migrationsDirectory, config.migrations === void 0);
555
+ await checkAppSchema(loaded.root, migrationsDirectory);
556
+ this.addWatchFile(appDatabasePaths(loaded.root).schema);
557
+ for (const file of files) this.addWatchFile(file);
558
+ await rm(migrationsOutput, {
559
+ force: true,
560
+ recursive: true
561
+ });
562
+ await mkdir(migrationsOutput, { recursive: true });
563
+ await cp(migrationsDirectory, migrationsOutput, { recursive: true }).catch((error) => {
564
+ if (config.migrations === void 0 && isMissingFile(error)) return;
565
+ throw error;
566
+ });
567
+ return `export default ${JSON.stringify(migrations)};`;
568
+ },
569
+ name: "tailorkit-app-migrations",
570
+ resolveId: (id) => id === "tailorkit:migrations" ? "\0tailorkit:migrations" : void 0
571
+ }
572
+ ],
573
+ resolve: viteConfig.resolve,
574
+ root: loaded.root,
575
+ ssr: { noExternal: true }
576
+ });
577
+ watching = watch;
578
+ if (watch && "on" in result && initialBuild) try {
579
+ await Promise.race([initialBuild, (async () => {
580
+ const { promise: pendingPromise, resolve: _, reject } = Promise.withResolvers();
581
+ try {
582
+ result.on("event", (event) => {
583
+ if (event.code === "ERROR") reject(event.error);
584
+ if (event.code === "END") ready?.();
585
+ });
586
+ } catch (error) {
587
+ reject(error);
588
+ }
589
+ return await pendingPromise;
590
+ })()]);
591
+ } catch (error) {
592
+ await result.close();
593
+ throw error;
594
+ }
595
+ return result;
596
+ } finally {
597
+ if (!watching) await rm(temporary, {
598
+ force: true,
599
+ recursive: true
600
+ });
601
+ }
602
+ };
603
+ const parsePreactVersion = (version) => {
604
+ const match = /^(?<major>\d+)\./u.exec(version);
605
+ if (!(match?.[1] !== void 0 && match?.[1] !== null && match?.[1] !== "")) throw new Error(`Unable to parse Preact version "${version}".`);
606
+ return {
607
+ major: Math.trunc(Number(match[1])),
608
+ version
609
+ };
610
+ };
611
+ const assertSupportedPreactVersion = (version) => {
612
+ const check = parsePreactVersion(version);
613
+ if (check.major < 11) throw new Error(`TailorKit requires Preact 11.0.0 or newer, but found ${version}.`);
614
+ return check;
615
+ };
616
+ //#endregion
617
+ //#region src/index.ts
618
+ const requireValue = (value) => {
619
+ if (value === null || value === void 0) throw new Error("Expected a defined value.");
620
+ return value;
621
+ };
622
+ const preactPackageJson = "preact/package.json";
623
+ const preactPackageJsonModuleId = "\0tailorkit-preact-package-json";
624
+ const flattenPlugins = async (option) => {
625
+ const plugin = await option;
626
+ if (!(plugin !== void 0 && plugin !== null && plugin !== false)) return [];
627
+ if (Array.isArray(plugin)) return (await Promise.all(plugin.map(flattenPlugins))).flat();
628
+ return [plugin];
629
+ };
630
+ const getInstalledPreactVersion = (root) => {
631
+ const require = createRequire(path.join(root, "package.json"));
632
+ try {
633
+ const packageJsonPath = require.resolve("preact/package.json");
634
+ const packageJson = z.object({ version: z.string().optional() }).parse(require(packageJsonPath));
635
+ if (packageJson.version !== void 0) return packageJson.version;
636
+ } catch (error) {
637
+ throw new Error(`TailorKit requires Preact to build an app. Install preact@^11 and try again.`, { cause: error });
638
+ }
639
+ throw new Error("Unable to read the installed Preact version.");
640
+ };
641
+ /** Build the app client, private backend, migrations and deployment manifest. */
642
+ const tailorkit = (options = {}) => {
643
+ let loaded;
644
+ let serverEntry;
645
+ let preactVersion;
646
+ let outputDirectory;
647
+ let resolvedOutDir;
648
+ let clientOutDir;
649
+ let watching = false;
650
+ let fileEntry;
651
+ let serverWatcher;
652
+ let resolved;
653
+ let customPlugins = [];
654
+ const instanceModules = /* @__PURE__ */ new Map();
655
+ const cleanup = async () => {
656
+ if (serverWatcher && "close" in serverWatcher) await serverWatcher.close();
657
+ await fileEntry?.close();
658
+ };
659
+ const writeBuildExtras = async () => {
660
+ const { views, instanceResolvers } = await readClientManifest(path.join(clientOutDir, "client.js"));
661
+ const resolverNames = new Set(instanceResolvers.map((registration) => registration.resolver));
662
+ const activeModules = [...instanceModules.values()].map((module) => ({
663
+ ...module,
664
+ names: module.names.filter((name) => resolverNames.has(name))
665
+ })).filter((module) => module.names.length).toSorted((a, b) => a.filename.localeCompare(b.filename));
666
+ if (activeModules.reduce((count, module) => count + module.names.length, 0) !== resolverNames.size) throw new Error("Client references an instance resolver that was not extracted by the app build.");
667
+ if (serverWatcher && "close" in serverWatcher) await serverWatcher.close();
668
+ serverWatcher = await buildServer(loaded, watching, resolvedOutDir, activeModules, instanceResolvers, {
669
+ define: resolved.define,
670
+ mode: resolved.mode,
671
+ plugins: customPlugins,
672
+ resolve: resolved.resolve
673
+ });
674
+ const logoManifest = {};
675
+ for (const variant of ["light", "dark"]) {
676
+ const configuredPath = loaded.config.logos?.[variant];
677
+ if (!(configuredPath !== void 0 && configuredPath !== null && configuredPath !== "")) continue;
678
+ const extension = path.extname(configuredPath).toLowerCase().slice(1);
679
+ const contentType = (/* @__PURE__ */ new Map([
680
+ ["png", "image/png"],
681
+ ["svg", "image/svg+xml"],
682
+ ["webp", "image/webp"]
683
+ ])).get(extension);
684
+ if (!contentType) throw new Error(`The ${variant} logo must be an SVG, PNG, or WebP file.`);
685
+ const content = await readFile(path.resolve(loaded.root, configuredPath));
686
+ validateLogoAsset(content, contentType);
687
+ const filename = `logo-${variant}.${extension}`;
688
+ await writeFile(path.join(clientOutDir, filename), content);
689
+ logoManifest[variant] = `client/${filename}`;
690
+ }
691
+ await writeFile(path.join(clientOutDir, "views.json"), `${JSON.stringify(views)}\n`);
692
+ await writeFile(path.join(resolvedOutDir, "tailorkit-upload.json"), `${JSON.stringify(createTailorKitUploadManifest(logoManifest, Boolean(loaded.config.server), views), null, 2)}\n`, "utf-8");
693
+ };
694
+ const plugins = [
695
+ {
696
+ buildEnd: async (error) => {
697
+ if (error && !watching) await cleanup();
698
+ },
699
+ closeBundle: async () => {
700
+ if (!watching) await cleanup();
701
+ },
702
+ closeWatcher: cleanup,
703
+ config: async (config) => {
704
+ customPlugins = (await flattenPlugins(config.plugins ?? [])).filter((plugin) => !plugins.includes(plugin));
705
+ watching = Boolean(config.build?.watch);
706
+ outputDirectory = config.build?.outDir ?? ".tailorkit";
707
+ return {
708
+ build: {
709
+ emptyOutDir: true,
710
+ lib: {
711
+ entry: "tailorkit:client-entry",
712
+ fileName: "client",
713
+ formats: ["es"]
714
+ },
715
+ minify: "oxc",
716
+ outDir: path.join(outputDirectory, "client"),
717
+ rolldownOptions: { output: {
718
+ comments: {
719
+ annotation: false,
720
+ jsdoc: false,
721
+ legal: false
722
+ },
723
+ inlineDynamicImports: true,
724
+ minify: true,
725
+ minifyInternalExports: true
726
+ } }
727
+ },
728
+ define: {
729
+ "process.env.NODE_ENV": JSON.stringify(getBuildEnvironment().NODE_ENV ?? "production"),
730
+ ...config.define
731
+ },
732
+ oxc: { jsx: { importSource: "preact" } }
733
+ };
734
+ },
735
+ configResolved: async (config) => {
736
+ const root = config.configFile !== void 0 && config.configFile !== null && config.configFile !== "" ? path.dirname(config.configFile) : config.root;
737
+ loaded = await loadTailorKitConfig(path.resolve(root, options.configPath ?? "tailorkit.config.ts"));
738
+ loaded.root = await realpath(root);
739
+ serverEntry = path.resolve(loaded.root, loaded.config.server?.entry ?? "src/server.ts");
740
+ preactVersion = getInstalledPreactVersion(loaded.root);
741
+ assertSupportedPreactVersion(preactVersion);
742
+ resolvedOutDir = path.resolve(loaded.root, outputDirectory);
743
+ validateServerBuildOutput(loaded, resolvedOutDir);
744
+ clientOutDir = path.join(resolvedOutDir, "client");
745
+ fileEntry = await createFileRouteEntry(loaded.root, resolvedOutDir, watching);
746
+ for (const build of [config.build, requireValue(config.environments.client).build]) {
747
+ build.outDir = clientOutDir;
748
+ if (build.lib !== false) build.lib.entry = fileEntry.entry;
749
+ }
750
+ resolved = config;
751
+ },
752
+ enforce: "pre",
753
+ name: "tailorkit",
754
+ writeBundle: async () => {
755
+ await writeBuildExtras();
756
+ }
757
+ },
758
+ fileRoutesPlugin(() => loaded.root),
759
+ instanceExtractionPlugin(() => loaded.root, () => Boolean(loaded.config.server), instanceModules),
760
+ {
761
+ enforce: "pre",
762
+ name: "tailorkit-browser-server-boundary",
763
+ async resolveId(id, importer) {
764
+ if (/^(?:@tailorkit\/app\/server$|tailorkit\/(?:server|app\/server)$|effect(?:\/|$))/u.test(id) || importer !== void 0 && importer !== null && importer !== "" && path.resolve(path.dirname(importer), id) === serverEntry) throw new Error("App server implementations cannot be imported into a browser bundle. Import api from #tailorkit and server types with import type.");
765
+ if (importer !== void 0 && importer !== null && importer !== "") {
766
+ if ((await this.resolve(id, importer, { skipSelf: true }))?.id.split("?")[0] === serverEntry) throw new Error("App server implementations cannot be imported into a browser bundle. Import api from #tailorkit and server types with import type.");
767
+ }
768
+ return null;
769
+ }
770
+ },
771
+ {
772
+ enforce: "pre",
773
+ load: (id) => {
774
+ if (id === preactPackageJsonModuleId) {
775
+ const version = JSON.stringify(preactVersion);
776
+ return `export const version = ${version}; export default { version: ${version} };`;
777
+ }
778
+ return null;
779
+ },
780
+ name: "tailorkit-preact-package-json",
781
+ resolveId: (id) => id === preactPackageJson ? preactPackageJsonModuleId : null
782
+ }
783
+ ];
784
+ for (const plugin of plugins) plugin.apply = "build";
785
+ return plugins;
786
+ };
787
+ //#endregion
788
+ export { tailorkit };