@tailorkit/app 0.1.0-beta.14 → 0.1.0-beta.17

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,36 +96,326 @@ 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);
129
419
  const writeBuildExtras = async () => {
130
420
  const logoManifest = {};
131
421
  for (const variant of ["light", "dark"]) {
@@ -133,20 +423,20 @@ var buildApp = async (options = {}) => {
133
423
  if (!configuredPath) {
134
424
  continue;
135
425
  }
136
- const extension = path.extname(configuredPath).toLowerCase().slice(1);
426
+ const extension = path6.extname(configuredPath).toLowerCase().slice(1);
137
427
  const contentType = { png: "image/png", svg: "image/svg+xml", webp: "image/webp" }[extension];
138
428
  if (!contentType) {
139
429
  throw new Error(`The ${variant} logo must be an SVG, PNG, or WebP file.`);
140
430
  }
141
- const content = await readFile(path.resolve(loaded.root, configuredPath));
431
+ const content = await readFile3(path6.resolve(loaded.root, configuredPath));
142
432
  validateLogoAsset(content, contentType);
143
433
  const filename = `logo-${variant}.${extension}`;
144
- await writeFile(path.join(resolvedOutDir, filename), content);
145
- logoManifest[variant] = filename;
434
+ await writeFile2(path6.join(clientOutDir, filename), content);
435
+ logoManifest[variant] = `client/${filename}`;
146
436
  }
147
- await writeFile(
148
- path.join(resolvedOutDir, "tailorkit-upload.json"),
149
- `${JSON.stringify(createTailorKitUploadManifest(logoManifest), null, 2)}
437
+ await writeFile2(
438
+ path6.join(resolvedOutDir, "tailorkit-upload.json"),
439
+ `${JSON.stringify(createTailorKitUploadManifest(logoManifest, Boolean(loaded.config.server)), null, 2)}
150
440
  `,
151
441
  "utf-8"
152
442
  );
@@ -165,11 +455,11 @@ var buildApp = async (options = {}) => {
165
455
  build: {
166
456
  emptyOutDir: true,
167
457
  lib: {
168
- entry: path.resolve(loaded.root, entry),
458
+ entry,
169
459
  fileName: "client",
170
460
  formats: ["es"]
171
461
  },
172
- outDir: resolvedOutDir,
462
+ outDir: clientOutDir,
173
463
  watch: options.watch ? {} : null,
174
464
  minify: "oxc",
175
465
  rollupOptions: {
@@ -188,9 +478,31 @@ var buildApp = async (options = {}) => {
188
478
  mode: options.mode,
189
479
  oxc: { jsx: { importSource: "preact" } },
190
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
+ },
191
503
  {
192
504
  name: "tailorkit-preview-build-ready",
193
- async closeBundle() {
505
+ async writeBundle() {
194
506
  try {
195
507
  await writeBuildExtras();
196
508
  firstBuildDone?.();
@@ -219,6 +531,11 @@ var buildApp = async (options = {}) => {
219
531
  }
220
532
  ],
221
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;
222
539
  });
223
540
  if (options.watch) {
224
541
  if (result && typeof result === "object" && "on" in result) {
@@ -228,12 +545,32 @@ var buildApp = async (options = {}) => {
228
545
  }
229
546
  });
230
547
  }
231
- await firstBuild;
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;
558
+ }
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
+ };
232
569
  }
233
570
  return result;
234
571
  };
235
572
  function getInstalledPreactVersion(root) {
236
- const require2 = createRequire(path.join(root, "package.json"));
573
+ const require2 = createRequire2(path6.join(root, "package.json"));
237
574
  try {
238
575
  const packageJsonPath = require2.resolve("preact/package.json");
239
576
  const packageJson = require2(packageJsonPath);
@@ -251,5 +588,6 @@ function getInstalledPreactVersion(root) {
251
588
  export {
252
589
  buildApp,
253
590
  createTailorKitUploadManifest,
591
+ generateAppMigrations,
254
592
  tailorkitUploadManifestSchema
255
593
  };