@tailorkit/app 0.1.0-beta.13 → 0.1.0-beta.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/builder.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  loadTailorKitConfig
3
- } from "./chunk-EKCUETAV.js";
4
- import "./chunk-ZFYKVXDO.js";
3
+ } from "./chunk-DEU6RITM.js";
4
+ import "./chunk-GVB2PAK7.js";
5
5
 
6
6
  // src/builder/index.ts
7
- import path from "path";
8
- import { createRequire } from "module";
9
- import { readFile, writeFile } from "fs/promises";
7
+ import path6 from "path";
8
+ import { createRequire as createRequire2 } from "module";
9
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
10
10
 
11
11
  // ../asset-delivery/dist/logo-validation.mjs
12
12
  var maxLogoBytes = 262144;
@@ -96,45 +96,370 @@ var assertSupportedPreactVersion = (version) => {
96
96
  return check;
97
97
  };
98
98
 
99
+ // src/builder/server.ts
100
+ import { mkdir, mkdtemp as mkdtemp2, writeFile, cp, rm as rm2 } from "fs/promises";
101
+ import path4 from "path";
102
+ import { build } from "vite";
103
+
104
+ // src/builder/migrations.ts
105
+ import { createHash } from "crypto";
106
+ import { readdir, readFile } from "fs/promises";
107
+ import path from "path";
108
+ async function readAppMigrations(directory, optional = false) {
109
+ const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
110
+ if (error.code === "ENOENT") {
111
+ if (optional) return [];
112
+ throw new Error(
113
+ `App migrations directory is missing: ${directory}. Run tailorkit db generate and commit migrations before building.`,
114
+ { cause: error }
115
+ );
116
+ }
117
+ throw error;
118
+ });
119
+ const migrations = [];
120
+ const files = [directory];
121
+ const names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
122
+ for (const name of names) {
123
+ if (!/^\d{14}_[\w-]+$/u.test(name)) {
124
+ throw new Error(`Unsupported app migration folder: ${name}. Use Drizzle Kit v1 migrations.`);
125
+ }
126
+ const file = path.join(directory, name, "migration.sql");
127
+ const sql = await readFile(file, "utf8");
128
+ const statements = sql.split("--> statement-breakpoint").map((part) => part.trim()).filter(Boolean);
129
+ if (!statements.length) throw new Error(`App migration is empty: ${name}`);
130
+ migrations.push({
131
+ id: name,
132
+ hash: createHash("sha256").update(sql).digest("hex"),
133
+ statements
134
+ });
135
+ files.push(file);
136
+ }
137
+ return { migrations, files };
138
+ }
139
+
140
+ // src/server/bundle.ts
141
+ var runtimeManifest = {
142
+ apiVersion: 1,
143
+ requires: ["database", "actions"]
144
+ };
145
+
146
+ // src/builder/schema-check.ts
147
+ import { execFile } from "child_process";
148
+ import { access as access2, mkdtemp, readFile as readFile2, readdir as readdir2, rm } from "fs/promises";
149
+ import { tmpdir } from "os";
150
+ import path3 from "path";
151
+ import { promisify } from "util";
152
+
153
+ // src/builder/drizzle.ts
154
+ import { access } from "fs/promises";
155
+ import { createRequire } from "module";
156
+ import path2 from "path";
157
+ async function drizzleGenerateArguments(root, output, name) {
158
+ const require2 = createRequire(path2.join(root, "package.json"));
159
+ for (const directory of require2.resolve.paths("drizzle-kit") ?? []) {
160
+ const cli = path2.join(directory, "drizzle-kit/bin.cjs");
161
+ if (await access(cli).then(() => true).catch(() => false)) {
162
+ return [
163
+ "--preserve-symlinks-main",
164
+ cli,
165
+ "generate",
166
+ "--dialect",
167
+ "sqlite",
168
+ "--schema",
169
+ path2.join(root, "src/schema.ts"),
170
+ "--out",
171
+ output,
172
+ ...name === void 0 ? [] : ["--name", name]
173
+ ];
174
+ }
175
+ }
176
+ throw new Error(
177
+ "Install drizzle-kit and drizzle-orm as dev dependencies, then run tailorkit db generate."
178
+ );
179
+ }
180
+
181
+ // src/builder/schema-check.ts
182
+ var run = promisify(execFile);
183
+ async function snapshot(directory) {
184
+ const entries = await readdir2(directory, { withFileTypes: true }).catch((error) => {
185
+ if (error.code === "ENOENT") return [];
186
+ throw error;
187
+ });
188
+ const latest = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().at(-1);
189
+ if (!latest) return;
190
+ const filename = path3.join(directory, latest, "snapshot.json");
191
+ const value = JSON.parse(await readFile2(filename, "utf8"));
192
+ if (value.version !== "7" || value.dialect !== "sqlite" || !Array.isArray(value.ddl)) {
193
+ throw new Error(
194
+ `Unsupported app schema snapshot: ${filename}. Use Drizzle Kit v1 SQLite migrations.`
195
+ );
196
+ }
197
+ return value;
198
+ }
199
+ function canonical(value) {
200
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
201
+ if (value && typeof value === "object") {
202
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
203
+ }
204
+ return JSON.stringify(value);
205
+ }
206
+ async function checkAppSchema(root, migrationsDirectory) {
207
+ const schema = path3.join(root, "src/schema.ts");
208
+ try {
209
+ await access2(schema);
210
+ } catch (error) {
211
+ if (error.code === "ENOENT") return;
212
+ throw error;
213
+ }
214
+ const temporary = await mkdtemp(path3.join(tmpdir(), "tailorkit-schema-check-"));
215
+ try {
216
+ const { stdout } = await run(
217
+ process.execPath,
218
+ await drizzleGenerateArguments(root, temporary),
219
+ {
220
+ cwd: root,
221
+ timeout: 3e4,
222
+ maxBuffer: 1024 * 1024,
223
+ env: { ...process.env, CI: "true" }
224
+ }
225
+ ).catch((error) => {
226
+ throw new Error(
227
+ "Could not check the app database schema with Drizzle Kit. Check src/schema.ts and your drizzle-kit/drizzle-orm dependencies.",
228
+ { cause: error }
229
+ );
230
+ });
231
+ const current = await snapshot(temporary);
232
+ if (!current && !stdout.includes("No schema changes")) {
233
+ throw new Error(
234
+ "Drizzle Kit did not produce a schema snapshot. Check src/schema.ts and run tailorkit db generate."
235
+ );
236
+ }
237
+ const saved = await snapshot(migrationsDirectory);
238
+ const expected = (current?.ddl ?? []).map(canonical).sort();
239
+ const actual = (saved?.ddl ?? []).map(canonical).sort();
240
+ if (canonical(expected) !== canonical(actual)) {
241
+ throw new Error(
242
+ `App database migrations are missing or out of sync with src/schema.ts. Run tailorkit db generate, commit the generated files in ${path3.relative(root, migrationsDirectory)}, then build again.`
243
+ );
244
+ }
245
+ } finally {
246
+ await rm(temporary, { recursive: true, force: true });
247
+ }
248
+ }
249
+
250
+ // src/builder/server.ts
251
+ async function buildServer(loaded, watch = false, buildOutDir) {
252
+ const config = loaded.config.server;
253
+ if (!config) {
254
+ return;
255
+ }
256
+ const output = buildOutDir ?? path4.resolve(loaded.root, loaded.config.build?.outDir ?? ".tailorkit");
257
+ const directory = path4.join(output, "server");
258
+ const migrationsDirectory = path4.resolve(loaded.root, config.migrations ?? "./migrations");
259
+ const migrationsOutput = path4.join(output, "migrations");
260
+ const temporaryRoot = path4.join(output, "tmp");
261
+ for (const destination of [
262
+ directory,
263
+ path4.join(output, "client"),
264
+ migrationsOutput,
265
+ temporaryRoot
266
+ ]) {
267
+ if (overlaps(migrationsDirectory, destination)) {
268
+ throw new Error("App build output must not overlap migration source");
269
+ }
270
+ }
271
+ await rm2(directory, { recursive: true, force: true });
272
+ await mkdir(directory, { recursive: true });
273
+ await mkdir(temporaryRoot, { recursive: true });
274
+ const temporary = await mkdtemp2(path4.join(temporaryRoot, "server-"));
275
+ const entry = path4.join(temporary, "entry.mjs");
276
+ let watching = false;
277
+ try {
278
+ await writeFile(
279
+ entry,
280
+ `export { default } from ${JSON.stringify(path4.resolve(loaded.root, config.entry ?? "src/server.ts"))};
281
+ export { default as migrations } from "tailorkit:migrations";
282
+ export const runtimeManifest = ${JSON.stringify(runtimeManifest)};
283
+ `
284
+ );
285
+ const result = await build({
286
+ root: loaded.root,
287
+ configFile: false,
288
+ plugins: [
289
+ {
290
+ name: "tailorkit-app-migrations",
291
+ closeWatcher: () => rm2(temporary, { recursive: true, force: true }),
292
+ resolveId(id) {
293
+ if (id === "tailorkit:migrations") {
294
+ return "\0tailorkit:migrations";
295
+ }
296
+ },
297
+ async load(id) {
298
+ if (id !== "\0tailorkit:migrations") {
299
+ return;
300
+ }
301
+ const { migrations, files } = await readAppMigrations(
302
+ migrationsDirectory,
303
+ config.migrations === void 0
304
+ );
305
+ await checkAppSchema(loaded.root, migrationsDirectory);
306
+ this.addWatchFile(path4.join(loaded.root, "src/schema.ts"));
307
+ for (const file of files) {
308
+ this.addWatchFile(file);
309
+ }
310
+ await rm2(migrationsOutput, { recursive: true, force: true });
311
+ await mkdir(migrationsOutput, { recursive: true });
312
+ await cp(migrationsDirectory, migrationsOutput, { recursive: true }).catch(
313
+ (error) => {
314
+ if (config.migrations === void 0 && error.code === "ENOENT") {
315
+ return;
316
+ }
317
+ throw error;
318
+ }
319
+ );
320
+ return `export default ${JSON.stringify(migrations)};`;
321
+ }
322
+ }
323
+ ],
324
+ build: {
325
+ ssr: entry,
326
+ outDir: directory,
327
+ emptyOutDir: false,
328
+ target: "es2022",
329
+ watch: watch ? {} : null,
330
+ rollupOptions: {
331
+ external: [],
332
+ output: { entryFileNames: "server.js", inlineDynamicImports: true }
333
+ }
334
+ },
335
+ ssr: { noExternal: true }
336
+ });
337
+ watching = watch;
338
+ return result;
339
+ } finally {
340
+ if (!watching) {
341
+ await rm2(temporary, { recursive: true, force: true });
342
+ }
343
+ }
344
+ }
345
+ function overlaps(source, destination) {
346
+ const contains = (parent, child) => {
347
+ const relative = path4.relative(parent, child);
348
+ return relative === "" || !path4.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path4.sep}`);
349
+ };
350
+ return contains(source, destination) || contains(destination, source);
351
+ }
352
+
99
353
  // src/builder/upload-manifest.ts
100
354
  import { z } from "zod";
101
355
  var tailorkitUploadManifestSchema = z.object({
102
356
  assets: z.object({
103
- client: z.literal("client.js"),
357
+ client: z.literal("client/client.js"),
358
+ server: z.literal("server/server.js").optional(),
104
359
  logos: z.object({
105
- dark: z.string().regex(/^logo-dark\.(?:png|svg|webp)$/u).optional(),
106
- light: z.string().regex(/^logo-light\.(?:png|svg|webp)$/u).optional()
360
+ dark: z.string().regex(/^client\/logo-dark\.(?:png|svg|webp)$/u).optional(),
361
+ light: z.string().regex(/^client\/logo-light\.(?:png|svg|webp)$/u).optional()
107
362
  }).optional()
108
363
  }),
109
364
  version: z.literal(1)
110
365
  });
111
- var createTailorKitUploadManifest = (logos) => tailorkitUploadManifestSchema.parse({
366
+ var createTailorKitUploadManifest = (logos, server = false) => tailorkitUploadManifestSchema.parse({
112
367
  assets: {
113
- client: "client.js",
368
+ client: "client/client.js",
369
+ ...server ? { server: "server/server.js" } : {},
114
370
  ...logos && Object.keys(logos).length > 0 ? { logos } : {}
115
371
  },
116
372
  version: 1
117
373
  });
118
374
 
375
+ // src/builder/generate-migrations.ts
376
+ import { spawn } from "child_process";
377
+ import { access as access3 } from "fs/promises";
378
+ import path5 from "path";
379
+ async function generateAppMigrations(options = {}) {
380
+ const loaded = await loadTailorKitConfig(options.configPath, options.cwd);
381
+ if (!loaded.config.server) {
382
+ throw new Error(
383
+ "Enable server: {} in tailorkit.config.ts before generating database migrations."
384
+ );
385
+ }
386
+ await access3(path5.join(loaded.root, "src/schema.ts")).catch((error) => {
387
+ throw new Error(
388
+ "Define the app database schema in src/schema.ts before generating migrations.",
389
+ { cause: error }
390
+ );
391
+ });
392
+ const directory = path5.resolve(loaded.root, loaded.config.server.migrations ?? "./migrations");
393
+ const args = await drizzleGenerateArguments(loaded.root, directory, options.name);
394
+ await new Promise((resolve, reject) => {
395
+ const child = spawn(process.execPath, args, { cwd: loaded.root, stdio: "inherit" });
396
+ child.once("error", reject);
397
+ child.once("close", (code, signal) => {
398
+ if (code === 0) resolve();
399
+ else reject(new Error(`Drizzle migration generation failed (${signal ?? code}).`));
400
+ });
401
+ });
402
+ await checkAppSchema(loaded.root, directory);
403
+ return directory;
404
+ }
405
+
119
406
  // src/builder/index.ts
120
407
  var preactPackageJson = "preact/package.json";
121
408
  var preactPackageJsonModuleId = "\0tailorkit-preact-package-json";
122
409
  var buildApp = async (options = {}) => {
123
410
  const loaded = await loadTailorKitConfig(options.configPath, options.cwd);
124
- const entry = options.entry ?? loaded.config.client?.entry ?? "./src/client.ts";
411
+ const entry = path6.resolve(loaded.root, loaded.config.client?.entry ?? "src/client.ts");
412
+ const serverEntry = path6.resolve(loaded.root, loaded.config.server?.entry ?? "src/server.ts");
125
413
  const outDir = options.outDir ?? loaded.config.build?.outDir ?? ".tailorkit";
126
414
  const preactVersion = getInstalledPreactVersion(loaded.root);
127
415
  assertSupportedPreactVersion(preactVersion);
128
- const resolvedOutDir = path.resolve(loaded.root, outDir);
416
+ const resolvedOutDir = path6.resolve(loaded.root, outDir);
417
+ const clientOutDir = path6.join(resolvedOutDir, "client");
418
+ const serverWatcher = await buildServer(loaded, options.watch, resolvedOutDir);
419
+ const writeBuildExtras = async () => {
420
+ const logoManifest = {};
421
+ for (const variant of ["light", "dark"]) {
422
+ const configuredPath = loaded.config.logos?.[variant];
423
+ if (!configuredPath) {
424
+ continue;
425
+ }
426
+ const extension = path6.extname(configuredPath).toLowerCase().slice(1);
427
+ const contentType = { png: "image/png", svg: "image/svg+xml", webp: "image/webp" }[extension];
428
+ if (!contentType) {
429
+ throw new Error(`The ${variant} logo must be an SVG, PNG, or WebP file.`);
430
+ }
431
+ const content = await readFile3(path6.resolve(loaded.root, configuredPath));
432
+ validateLogoAsset(content, contentType);
433
+ const filename = `logo-${variant}.${extension}`;
434
+ await writeFile2(path6.join(clientOutDir, filename), content);
435
+ logoManifest[variant] = `client/${filename}`;
436
+ }
437
+ await writeFile2(
438
+ path6.join(resolvedOutDir, "tailorkit-upload.json"),
439
+ `${JSON.stringify(createTailorKitUploadManifest(logoManifest, Boolean(loaded.config.server)), null, 2)}
440
+ `,
441
+ "utf-8"
442
+ );
443
+ };
444
+ let firstBuildDone;
445
+ let firstBuildFailed;
446
+ const firstBuild = new Promise((resolve, reject) => {
447
+ firstBuildDone = resolve;
448
+ firstBuildFailed = reject;
449
+ });
450
+ if (!options.watch) {
451
+ void firstBuild.catch(() => {
452
+ });
453
+ }
129
454
  const result = await viteBuild({
130
455
  build: {
131
456
  emptyOutDir: true,
132
457
  lib: {
133
- entry: path.resolve(loaded.root, entry),
458
+ entry,
134
459
  fileName: "client",
135
460
  formats: ["es"]
136
461
  },
137
- outDir: resolvedOutDir,
462
+ outDir: clientOutDir,
138
463
  watch: options.watch ? {} : null,
139
464
  minify: "oxc",
140
465
  rollupOptions: {
@@ -151,7 +476,42 @@ var buildApp = async (options = {}) => {
151
476
  },
152
477
  configFile: false,
153
478
  mode: options.mode,
479
+ oxc: { jsx: { importSource: "preact" } },
154
480
  plugins: [
481
+ {
482
+ name: "tailorkit-browser-server-boundary",
483
+ enforce: "pre",
484
+ async resolveId(id, importer) {
485
+ if (/^(?:@tailorkit\/app\/server$|tailorkit\/(?:server|app\/server)$|effect(?:\/|$))/u.test(
486
+ id
487
+ ) || importer && path6.resolve(path6.dirname(importer), id) === serverEntry) {
488
+ throw new Error(
489
+ "App server implementations cannot be imported into a browser bundle. Import api from #tailorkit and server types with import type."
490
+ );
491
+ }
492
+ if (importer) {
493
+ const resolved = await this.resolve(id, importer, { skipSelf: true });
494
+ if (resolved?.id.split("?")[0] === serverEntry) {
495
+ throw new Error(
496
+ "App server implementations cannot be imported into a browser bundle. Import api from #tailorkit and server types with import type."
497
+ );
498
+ }
499
+ }
500
+ return null;
501
+ }
502
+ },
503
+ {
504
+ name: "tailorkit-preview-build-ready",
505
+ async writeBundle() {
506
+ try {
507
+ await writeBuildExtras();
508
+ firstBuildDone?.();
509
+ } catch (error) {
510
+ firstBuildFailed?.(error instanceof Error ? error : new Error(String(error)));
511
+ throw error;
512
+ }
513
+ }
514
+ },
155
515
  {
156
516
  name: "tailorkit-preact-package-json",
157
517
  enforce: "pre",
@@ -171,38 +531,46 @@ var buildApp = async (options = {}) => {
171
531
  }
172
532
  ],
173
533
  root: loaded.root
534
+ }).catch(async (error) => {
535
+ if (serverWatcher && typeof serverWatcher === "object" && "close" in serverWatcher) {
536
+ await serverWatcher.close();
537
+ }
538
+ throw error;
174
539
  });
175
- const logoManifest = {};
176
- for (const variant of ["light", "dark"]) {
177
- const configuredPath = loaded.config.logos?.[variant];
178
- if (!configuredPath) {
179
- continue;
540
+ if (options.watch) {
541
+ if (result && typeof result === "object" && "on" in result) {
542
+ result.on("event", (event) => {
543
+ if (event.code === "ERROR") {
544
+ firstBuildFailed?.(event.error ?? new Error("Initial app build failed."));
545
+ }
546
+ });
180
547
  }
181
- const extension = path.extname(configuredPath).toLowerCase().slice(1);
182
- const contentType = {
183
- png: "image/png",
184
- svg: "image/svg+xml",
185
- webp: "image/webp"
186
- }[extension];
187
- if (!contentType) {
188
- throw new Error(`The ${variant} logo must be an SVG, PNG, or WebP file.`);
548
+ try {
549
+ await firstBuild;
550
+ } catch (error) {
551
+ if (result && typeof result === "object" && "close" in result) {
552
+ await result.close();
553
+ }
554
+ if (serverWatcher && typeof serverWatcher === "object" && "close" in serverWatcher) {
555
+ await serverWatcher.close();
556
+ }
557
+ throw error;
189
558
  }
190
- const content = await readFile(path.resolve(loaded.root, configuredPath));
191
- validateLogoAsset(content, contentType);
192
- const filename = `logo-${variant}.${extension}`;
193
- await writeFile(path.join(resolvedOutDir, filename), content);
194
- logoManifest[variant] = filename;
195
- }
196
- await writeFile(
197
- path.join(resolvedOutDir, "tailorkit-upload.json"),
198
- `${JSON.stringify(createTailorKitUploadManifest(logoManifest), null, 2)}
199
- `,
200
- "utf-8"
201
- );
559
+ }
560
+ if (options.watch && result && typeof result === "object" && "close" in result && serverWatcher && typeof serverWatcher === "object" && "close" in serverWatcher) {
561
+ const clientWatcher = result;
562
+ return {
563
+ on: clientWatcher.on.bind(clientWatcher),
564
+ async close() {
565
+ await clientWatcher.close();
566
+ await serverWatcher.close();
567
+ }
568
+ };
569
+ }
202
570
  return result;
203
571
  };
204
572
  function getInstalledPreactVersion(root) {
205
- const require2 = createRequire(path.join(root, "package.json"));
573
+ const require2 = createRequire2(path6.join(root, "package.json"));
206
574
  try {
207
575
  const packageJsonPath = require2.resolve("preact/package.json");
208
576
  const packageJson = require2(packageJsonPath);
@@ -220,5 +588,6 @@ function getInstalledPreactVersion(root) {
220
588
  export {
221
589
  buildApp,
222
590
  createTailorKitUploadManifest,
591
+ generateAppMigrations,
223
592
  tailorkitUploadManifestSchema
224
593
  };