@tailorkit/app 0.1.0-beta.18 → 0.1.0-beta.19

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.
@@ -0,0 +1,16 @@
1
+ import type { InstanceRegistration } from "./instances";
2
+ export declare function readClientViews(filename: string): Promise<{
3
+ slot: string;
4
+ path: string;
5
+ instances?: true | undefined;
6
+ disabled?: true | undefined;
7
+ }[]>;
8
+ export declare function readClientManifest(filename: string): Promise<{
9
+ views: {
10
+ slot: string;
11
+ path: string;
12
+ instances?: true | undefined;
13
+ disabled?: true | undefined;
14
+ }[];
15
+ instanceResolvers: InstanceRegistration[];
16
+ }>;
@@ -0,0 +1,19 @@
1
+ import type { Plugin } from "vite";
2
+ export interface InstanceModule {
3
+ filename: string;
4
+ names: string[];
5
+ }
6
+ export interface InstanceRegistration {
7
+ slot: string;
8
+ path: string;
9
+ resolver: string;
10
+ }
11
+ export declare function extractInstances(filename: string, source: string, root: string, includedNames?: string[]): Promise<{
12
+ names: string[];
13
+ server: string;
14
+ browser: string;
15
+ } | undefined>;
16
+ export declare function instanceExtractionPlugin(root: string, serverConfigured: boolean, modules: Map<string, InstanceModule>): Plugin;
17
+ export declare function instanceServerPlugin(root: string, modules: InstanceModule[]): Plugin;
18
+ export declare function instanceServerImports(modules: InstanceModule[]): string;
19
+ export declare function instanceServerBindings(modules: InstanceModule[], registrations: InstanceRegistration[]): string;
@@ -1,3 +1,6 @@
1
1
  import type { LoadedTailorKitConfig } from "../config/loader";
2
+ import type { InstanceModule, InstanceRegistration } from "./instances";
2
3
  /** Bundle the private app backend and migrations. */
3
- export declare function buildServer(loaded: LoadedTailorKitConfig, watch?: boolean, buildOutDir?: string): Promise<import("@voidzero-dev/vite-plus-core/rolldown").RolldownOutput[] | import("@voidzero-dev/vite-plus-core/rolldown").RolldownOutput | import("@voidzero-dev/vite-plus-core/rolldown").RolldownWatcher | undefined>;
4
+ export declare function buildServer(loaded: LoadedTailorKitConfig, watch?: boolean, buildOutDir?: string, instances?: InstanceModule[], registrations?: InstanceRegistration[]): Promise<import("@voidzero-dev/vite-plus-core/rolldown").RolldownOutput[] | import("@voidzero-dev/vite-plus-core/rolldown").RolldownOutput | import("@voidzero-dev/vite-plus-core/rolldown").RolldownWatcher | undefined>;
5
+ /** Check before either build can empty an output directory. */
6
+ export declare function validateServerBuildOutput(loaded: LoadedTailorKitConfig, output: string): void;
@@ -8,7 +8,13 @@ export declare const tailorkitUploadManifestSchema: z.ZodObject<{
8
8
  light: z.ZodOptional<z.ZodString>;
9
9
  }, z.core.$strip>>;
10
10
  }, z.core.$strip>;
11
+ views: z.ZodOptional<z.ZodArray<z.ZodObject<{
12
+ slot: z.ZodString;
13
+ path: z.ZodString;
14
+ instances: z.ZodOptional<z.ZodLiteral<true>>;
15
+ disabled: z.ZodOptional<z.ZodLiteral<true>>;
16
+ }, z.core.$strip>>>;
11
17
  version: z.ZodLiteral<1>;
12
18
  }, z.core.$strip>;
13
19
  export type TailorKitUploadManifest = z.output<typeof tailorkitUploadManifestSchema>;
14
- export declare const createTailorKitUploadManifest: (logos?: TailorKitUploadManifest["assets"]["logos"], server?: boolean) => TailorKitUploadManifest;
20
+ export declare const createTailorKitUploadManifest: (logos?: TailorKitUploadManifest["assets"]["logos"], server?: boolean, views?: TailorKitUploadManifest["views"]) => TailorKitUploadManifest;
package/dist/builder.js CHANGED
@@ -4,9 +4,9 @@ import {
4
4
  import "./chunk-GVB2PAK7.js";
5
5
 
6
6
  // src/builder/index.ts
7
- import path6 from "path";
7
+ import path7 from "path";
8
8
  import { createRequire as createRequire2 } from "module";
9
- import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
9
+ import { readFile as readFile4, realpath, writeFile as writeFile2 } from "fs/promises";
10
10
 
11
11
  // ../asset-delivery/dist/logo-validation.mjs
12
12
  var maxLogoBytes = 262144;
@@ -96,9 +96,88 @@ var assertSupportedPreactVersion = (version) => {
96
96
  return check;
97
97
  };
98
98
 
99
+ // src/builder/client-views.ts
100
+ import { execFile } from "child_process";
101
+ import { promisify } from "util";
102
+
103
+ // src/builder/upload-manifest.ts
104
+ import { z } from "zod";
105
+ var tailorkitUploadManifestSchema = z.object({
106
+ assets: z.object({
107
+ client: z.literal("client/client.js"),
108
+ server: z.literal("server/server.js").optional(),
109
+ logos: z.object({
110
+ dark: z.string().regex(/^client\/logo-dark\.(?:png|svg|webp)$/u).optional(),
111
+ light: z.string().regex(/^client\/logo-light\.(?:png|svg|webp)$/u).optional()
112
+ }).optional()
113
+ }),
114
+ views: z.array(
115
+ z.object({
116
+ slot: z.string().min(1).max(255),
117
+ path: z.string().startsWith("/").max(1024),
118
+ instances: z.literal(true).optional(),
119
+ disabled: z.literal(true).optional()
120
+ })
121
+ ).max(1e3).optional(),
122
+ version: z.literal(1)
123
+ });
124
+ var createTailorKitUploadManifest = (logos, server = false, views) => tailorkitUploadManifestSchema.parse({
125
+ assets: {
126
+ client: "client/client.js",
127
+ ...server ? { server: "server/server.js" } : {},
128
+ ...logos && Object.keys(logos).length > 0 ? { logos } : {}
129
+ },
130
+ views,
131
+ version: 1
132
+ });
133
+
134
+ // src/builder/client-views.ts
135
+ var inspectClient = `
136
+ import { readFile } from "node:fs/promises";
137
+ const source = await readFile(process.argv[1], "utf8");
138
+ const { default: client } = await import("data:text/javascript;base64," + Buffer.from(source).toString("base64"));
139
+ if (!client?.slots || typeof client.slots !== "object") throw new Error("Client must default-export defineClient(...)");
140
+ const instanceResolvers = [];
141
+ const views = Object.entries(client.slots).flatMap(([slot, definitions]) =>
142
+ Object.entries(definitions).flatMap(([path, view]) => {
143
+ if (view === false) return [{ slot, path, disabled: true }];
144
+ const resolver = view?.instances?.resolver;
145
+ if (resolver !== undefined) instanceResolvers.push({ slot, path, resolver });
146
+ return [{ slot, path, ...(resolver !== undefined ? { instances: true } : {}) }];
147
+ })
148
+ );
149
+ process.stdout.write("\\nTAILORKIT_VIEWS=" + JSON.stringify({ views, instanceResolvers }));
150
+ process.exit(0);
151
+ `;
152
+ async function readClientManifest(filename) {
153
+ try {
154
+ const { stdout } = await promisify(execFile)(
155
+ process.execPath,
156
+ ["--input-type=module", "--eval", inspectClient, filename],
157
+ { timeout: 1e4, maxBuffer: 2 * 1024 * 1024 }
158
+ );
159
+ const result = stdout.slice(
160
+ stdout.lastIndexOf("\nTAILORKIT_VIEWS=") + "\nTAILORKIT_VIEWS=".length
161
+ );
162
+ const manifest = JSON.parse(result);
163
+ const views = tailorkitUploadManifestSchema.shape.views.unwrap().parse(manifest.views);
164
+ if (!Array.isArray(manifest.instanceResolvers) || manifest.instanceResolvers.some(
165
+ (ref) => !ref || typeof ref !== "object" || typeof ref.slot !== "string" || typeof ref.path !== "string" || typeof ref.resolver !== "string" || !/^i[0-9a-f]{24}$/u.test(ref.resolver)
166
+ )) {
167
+ throw new Error("Invalid instance resolver references");
168
+ }
169
+ return { views, instanceResolvers: manifest.instanceResolvers };
170
+ } catch (error) {
171
+ throw new Error(
172
+ "Unable to read app views during the build. Keep browser-only side effects inside components or effects, and default-export defineClient(...).",
173
+ { cause: error }
174
+ );
175
+ }
176
+ }
177
+
99
178
  // src/builder/server.ts
100
179
  import { mkdir, mkdtemp as mkdtemp2, writeFile, cp, rm as rm2 } from "fs/promises";
101
- import path4 from "path";
180
+ import path5 from "path";
102
181
  import { build } from "vite";
103
182
 
104
183
  // src/builder/migrations.ts
@@ -144,11 +223,11 @@ var runtimeManifest = {
144
223
  };
145
224
 
146
225
  // src/builder/schema-check.ts
147
- import { execFile } from "child_process";
226
+ import { execFile as execFile2 } from "child_process";
148
227
  import { access as access2, mkdtemp, readFile as readFile2, readdir as readdir2, rm } from "fs/promises";
149
228
  import { tmpdir } from "os";
150
229
  import path3 from "path";
151
- import { promisify } from "util";
230
+ import { promisify as promisify2 } from "util";
152
231
 
153
232
  // src/builder/drizzle.ts
154
233
  import { access } from "fs/promises";
@@ -179,7 +258,7 @@ async function drizzleGenerateArguments(root, output, name) {
179
258
  }
180
259
 
181
260
  // src/builder/schema-check.ts
182
- var run = promisify(execFile);
261
+ var run = promisify2(execFile2);
183
262
  async function snapshot(directory) {
184
263
  const entries = await readdir2(directory, { withFileTypes: true }).catch((error) => {
185
264
  if (error.code === "ENOENT") return [];
@@ -247,37 +326,428 @@ async function checkAppSchema(root, migrationsDirectory) {
247
326
  }
248
327
  }
249
328
 
329
+ // src/builder/instances.ts
330
+ import { createHash as createHash2 } from "crypto";
331
+ import { readFile as readFile3 } from "fs/promises";
332
+ import path4 from "path";
333
+ import { parseSync, transformWithOxc } from "vite";
334
+ var suffix = "?tailorkit-instances";
335
+ var clientImports = /* @__PURE__ */ new Set([
336
+ "tailorkit/client",
337
+ "tailorkit/app/client",
338
+ "@tailorkit/app/client"
339
+ ]);
340
+ var browserGlobals = /* @__PURE__ */ new Set([
341
+ "window",
342
+ "document",
343
+ "navigator",
344
+ "localStorage",
345
+ "sessionStorage"
346
+ ]);
347
+ function isNode(value) {
348
+ return !!value && typeof value === "object" && "type" in value;
349
+ }
350
+ function children(node) {
351
+ return Object.values(node).flatMap(
352
+ (value) => Array.isArray(value) ? value.filter(isNode) : isNode(value) ? [value] : []
353
+ );
354
+ }
355
+ function walk(node, visit, ancestors = []) {
356
+ visit(node, ancestors);
357
+ for (const child of children(node)) walk(child, visit, [...ancestors, node]);
358
+ }
359
+ function boundNames(node) {
360
+ if (!node) return [];
361
+ switch (node.type) {
362
+ case "Identifier":
363
+ return [node.name];
364
+ case "RestElement":
365
+ return boundNames(node.argument);
366
+ case "AssignmentPattern":
367
+ return boundNames(node.left);
368
+ case "ArrayPattern":
369
+ return node.elements.flatMap(boundNames);
370
+ case "ObjectPattern":
371
+ return node.properties.flatMap(
372
+ (p) => boundNames(p.type === "Property" ? p.value : p.argument)
373
+ );
374
+ default:
375
+ return [];
376
+ }
377
+ }
378
+ function declarations(node) {
379
+ if (node.type === "VariableDeclaration")
380
+ return node.declarations.flatMap((d) => boundNames(d.id));
381
+ if (node.type === "FunctionDeclaration" || node.type === "ClassDeclaration")
382
+ return boundNames(node.id);
383
+ return [];
384
+ }
385
+ function isFunction(node) {
386
+ return ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(
387
+ node.type
388
+ );
389
+ }
390
+ function freeNames(root) {
391
+ const references = /* @__PURE__ */ new Set();
392
+ function pattern(node, scope) {
393
+ if (node.type === "AssignmentPattern") {
394
+ visit(node.right, scope);
395
+ pattern(node.left, scope);
396
+ } else if (node.type === "ObjectPattern") {
397
+ for (const p of node.properties) {
398
+ if (p.type === "Property") {
399
+ if (p.computed) visit(p.key, scope);
400
+ pattern(p.value, scope);
401
+ } else pattern(p.argument, scope);
402
+ }
403
+ } else if (node.type === "ArrayPattern") {
404
+ for (const element of node.elements) if (element) pattern(element, scope);
405
+ } else if (node.type === "RestElement") pattern(node.argument, scope);
406
+ }
407
+ function visit(node, parent) {
408
+ if (node.type === "Identifier") {
409
+ if (!parent.has(node.name)) references.add(node.name);
410
+ return;
411
+ }
412
+ if (node.type === "ImportDeclaration") return;
413
+ if (node.type === "ExportSpecifier") {
414
+ visit(node.local, parent);
415
+ return;
416
+ }
417
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
418
+ let hoisted2 = function(body) {
419
+ if (isFunction(body)) return;
420
+ if (body.type === "VariableDeclaration" && body.kind === "var") {
421
+ for (const name of declarations(body)) scope.add(name);
422
+ }
423
+ for (const child of children(body)) hoisted2(child);
424
+ };
425
+ var hoisted = hoisted2;
426
+ const scope = /* @__PURE__ */ new Set([
427
+ ...parent,
428
+ ...boundNames(node.id),
429
+ ...node.params.flatMap(boundNames)
430
+ ]);
431
+ if (node.body) hoisted2(node.body);
432
+ for (const p of node.params) pattern(p, scope);
433
+ if (node.body) visit(node.body, scope);
434
+ return;
435
+ }
436
+ if (node.type === "BlockStatement" || node.type === "Program") {
437
+ const scope = /* @__PURE__ */ new Set([...parent, ...node.body.flatMap(declarations)]);
438
+ for (const statement of node.body) visit(statement, scope);
439
+ return;
440
+ }
441
+ if (node.type === "VariableDeclarator") {
442
+ pattern(node.id, parent);
443
+ if (node.init) visit(node.init, parent);
444
+ return;
445
+ }
446
+ if (node.type === "Property" || node.type === "MethodDefinition" || node.type === "PropertyDefinition") {
447
+ if (node.computed) visit(node.key, parent);
448
+ if (node.value) visit(node.value, parent);
449
+ return;
450
+ }
451
+ if (node.type === "MemberExpression") {
452
+ visit(node.object, parent);
453
+ if (node.computed) visit(node.property, parent);
454
+ return;
455
+ }
456
+ if (node.type === "CatchClause") {
457
+ const scope = /* @__PURE__ */ new Set([...parent, ...boundNames(node.param)]);
458
+ if (node.param) pattern(node.param, scope);
459
+ visit(node.body, scope);
460
+ return;
461
+ }
462
+ if (node.type === "ForStatement" || node.type === "ForInStatement" || node.type === "ForOfStatement") {
463
+ const declaration = node.type === "ForStatement" ? node.init : node.left;
464
+ const scope = /* @__PURE__ */ new Set([...parent, ...declaration ? declarations(declaration) : []]);
465
+ for (const child of children(node)) visit(child, scope);
466
+ return;
467
+ }
468
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
469
+ const scope = /* @__PURE__ */ new Set([...parent, ...boundNames(node.id)]);
470
+ if (node.superClass) visit(node.superClass, scope);
471
+ visit(node.body, scope);
472
+ return;
473
+ }
474
+ if (node.type === "LabeledStatement") {
475
+ visit(node.body, parent);
476
+ return;
477
+ }
478
+ if (node.type === "BreakStatement" || node.type === "ContinueStatement") return;
479
+ for (const child of children(node)) visit(child, parent);
480
+ }
481
+ visit(root, /* @__PURE__ */ new Set());
482
+ return references;
483
+ }
484
+ function property(node, name) {
485
+ return node.properties.find(
486
+ (p) => p.type === "Property" && !p.computed && (p.key.type === "Identifier" ? p.key.name === name : p.key.type === "Literal" && p.key.value === name)
487
+ );
488
+ }
489
+ function parse(filename, source) {
490
+ const result = parseSync(filename, source);
491
+ if (result.errors.length)
492
+ throw new Error(`Unable to parse ${filename}: ${result.errors[0]?.message}`);
493
+ return result.program;
494
+ }
495
+ function units(program, source) {
496
+ const imports = [];
497
+ const statements = [];
498
+ for (const statement of program.body) {
499
+ if (statement.type === "ImportDeclaration") {
500
+ imports.push(statement);
501
+ continue;
502
+ }
503
+ const exported = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration";
504
+ const node = statement.type === "ExportNamedDeclaration" && statement.declaration ? statement.declaration : statement;
505
+ if (node.type === "VariableDeclaration") {
506
+ for (const d of node.declarations)
507
+ statements.push({
508
+ node: d,
509
+ names: boundNames(d.id),
510
+ code: `${exported ? "export " : ""}${node.kind} ${source.slice(d.start, d.end)};`,
511
+ exported
512
+ });
513
+ } else
514
+ statements.push({
515
+ node,
516
+ names: declarations(node),
517
+ code: source.slice(statement.start, statement.end),
518
+ exported
519
+ });
520
+ }
521
+ return { imports, statements };
522
+ }
523
+ function closure(roots, statements) {
524
+ const bindings = new Map(statements.flatMap((u) => u.names.map((name) => [name, u])));
525
+ const names = /* @__PURE__ */ new Set();
526
+ const selected = /* @__PURE__ */ new Set();
527
+ function add(name) {
528
+ if (names.has(name)) return;
529
+ names.add(name);
530
+ const unit = bindings.get(name);
531
+ if (!unit || selected.has(unit)) return;
532
+ selected.add(unit);
533
+ for (const ref of freeNames(unit.node)) add(ref);
534
+ }
535
+ for (const root of roots) add(root);
536
+ return { names, selected };
537
+ }
538
+ function renderImports(imports, needed, source, sideEffects) {
539
+ return imports.flatMap((i) => {
540
+ if (!i.specifiers.length) return sideEffects ? [source.slice(i.start, i.end)] : [];
541
+ const parts = i.specifiers.filter((s) => needed(s.local.name));
542
+ if (!parts.length) return [];
543
+ const defaultImport = parts.find((s) => s.type === "ImportDefaultSpecifier");
544
+ const namespace = parts.find((s) => s.type === "ImportNamespaceSpecifier");
545
+ const named = parts.filter((s) => s.type === "ImportSpecifier");
546
+ const clauses = [
547
+ defaultImport?.local.name,
548
+ namespace ? `* as ${namespace.local.name}` : void 0,
549
+ named.length ? `{ ${named.map((s) => `${source.slice(s.imported.start, s.imported.end)} as ${s.local.name}`).join(", ")} }` : void 0
550
+ ].filter(Boolean);
551
+ return [`import ${clauses.join(", ")} from ${JSON.stringify(i.source.value)};`];
552
+ }).join("\n");
553
+ }
554
+ async function extractInstances(filename, source, root, includedNames) {
555
+ const transformed = await transformWithOxc(source, filename, {
556
+ jsx: { importSource: "preact" },
557
+ sourcemap: false
558
+ });
559
+ source = transformed.code;
560
+ const program = parse(filename.replace(/\.[^.]+$/u, ".js"), source);
561
+ const aliases = /* @__PURE__ */ new Set();
562
+ for (const statement of program.body) {
563
+ if (statement.type !== "ImportDeclaration" || !clientImports.has(String(statement.source.value)))
564
+ continue;
565
+ for (const s of statement.specifiers) {
566
+ if (s.type === "ImportSpecifier" && s.imported.type === "Identifier" && s.imported.name === "createView")
567
+ aliases.add(s.local.name);
568
+ }
569
+ }
570
+ let resolvers = [];
571
+ walk(program, (node, ancestors) => {
572
+ if (node.type !== "CallExpression" || node.callee.type !== "Identifier" || !aliases.has(node.callee.name))
573
+ return;
574
+ const options = node.arguments[1];
575
+ if (options?.type !== "ObjectExpression") return;
576
+ const instances = property(options, "instances");
577
+ if (!instances) return;
578
+ const fail = (message) => {
579
+ throw new Error(`${filename}: ${message}`);
580
+ };
581
+ if (ancestors.some(isFunction))
582
+ fail("createView with instances must be declared at module scope.");
583
+ const owner = ancestors.findLast((n) => n.type === "VariableDeclarator");
584
+ const viewName = owner?.type === "VariableDeclarator" && owner.id.type === "Identifier" ? owner.id.name : "default";
585
+ const view = node.arguments[0];
586
+ if (view?.type !== "Literal" || typeof view.value !== "string")
587
+ fail("Instance view paths must be string literals.");
588
+ if (instances.value.type !== "ObjectExpression")
589
+ fail("instances must be an inline object with dataSchema and resolve.");
590
+ const config = instances.value;
591
+ if (config.properties.some((p) => p.type !== "Property" || p.computed || p.kind !== "init"))
592
+ fail("Instance options cannot contain spreads, getters or computed properties.");
593
+ const resolve = property(config, "resolve");
594
+ const schema = property(config, "dataSchema");
595
+ if (!resolve || !schema || resolve.method || !["ArrowFunctionExpression", "FunctionExpression"].includes(resolve.value.type))
596
+ fail("instances requires dataSchema and an inline resolve function.");
597
+ const name = `i${createHash2("sha256").update(
598
+ `${path4.relative(root, filename).split(path4.sep).join("/")}\0${viewName}\0${view.value}`
599
+ ).digest("hex").slice(0, 24)}`;
600
+ if (resolvers.some((r) => r.name === name))
601
+ fail("Instance resolvers must have distinct module-level variable names.");
602
+ resolvers.push({
603
+ name,
604
+ view: view.value,
605
+ call: node,
606
+ instances,
607
+ resolve,
608
+ schema
609
+ });
610
+ });
611
+ if (includedNames) {
612
+ resolvers = resolvers.filter((r) => includedNames.includes(r.name));
613
+ if (resolvers.length !== includedNames.length)
614
+ throw new Error(`${filename}: Instance definitions changed; rebuild the app client.`);
615
+ }
616
+ if (!resolvers.length) return;
617
+ const original = units(program, source);
618
+ const server = closure(
619
+ resolvers.flatMap((r) => [...freeNames(r.resolve.value), ...freeNames(r.schema.value)]),
620
+ original.statements
621
+ );
622
+ for (const name of server.names) {
623
+ if (browserGlobals.has(name))
624
+ throw new Error(`${filename}: Instance resolvers cannot access browser global ${name}.`);
625
+ }
626
+ for (const unit of server.selected) {
627
+ if (resolvers.some((r) => unit.node.start <= r.call.start && unit.node.end >= r.call.end))
628
+ throw new Error(
629
+ `${filename}: Instance resolvers cannot capture a view definition. Use the supplied context and queries.`
630
+ );
631
+ }
632
+ const serverSource = [
633
+ renderImports(original.imports, (name) => server.names.has(name), source, false),
634
+ ...original.statements.filter((u) => server.selected.has(u)).map((u) => u.code.replace(/^export /u, "")),
635
+ `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(", ")} };`
636
+ ].join("\n");
637
+ let browserSource = source;
638
+ for (const r of [...resolvers].sort(
639
+ (a, b) => b.instances.value.start - a.instances.value.start
640
+ )) {
641
+ browserSource = browserSource.slice(0, r.instances.value.start) + `{ resolver: ${JSON.stringify(r.name)} }` + browserSource.slice(r.instances.value.end);
642
+ }
643
+ const browser = units(parse("browser.js", browserSource), browserSource);
644
+ const serverBindings = new Set([...server.selected].flatMap((u) => u.names));
645
+ const browserRoots = browser.statements.filter(
646
+ (u) => u.exported || !u.names.length || !u.names.some((name) => serverBindings.has(name))
647
+ );
648
+ const browserNeeded = closure(
649
+ browserRoots.flatMap((u) => [...freeNames(u.node), ...u.names]),
650
+ browser.statements
651
+ );
652
+ return {
653
+ names: resolvers.map((r) => r.name),
654
+ server: serverSource,
655
+ browser: [
656
+ renderImports(
657
+ browser.imports,
658
+ (name) => !server.names.has(name) || browserNeeded.names.has(name),
659
+ browserSource,
660
+ true
661
+ ),
662
+ ...browser.statements.filter((u) => browserRoots.includes(u) || browserNeeded.selected.has(u)).map((u) => u.code)
663
+ ].join("\n")
664
+ };
665
+ }
666
+ function instanceExtractionPlugin(root, serverConfigured, modules) {
667
+ return {
668
+ name: "tailorkit-instance-extraction",
669
+ enforce: "pre",
670
+ async transform(source, filename) {
671
+ if (filename.includes("node_modules") || !/\.[cm]?[jt]sx?$/u.test(filename) || !source.includes("instances") && !modules.has(filename))
672
+ return;
673
+ const result = await extractInstances(filename, source, root);
674
+ if (!result) {
675
+ modules.delete(filename);
676
+ return;
677
+ }
678
+ if (!serverConfigured)
679
+ throw new Error("View instances require server configuration in tailorkit.config.ts.");
680
+ modules.set(filename, { filename, names: result.names });
681
+ return { code: result.browser, map: null };
682
+ }
683
+ };
684
+ }
685
+ function instanceServerPlugin(root, modules) {
686
+ return {
687
+ name: "tailorkit-instance-server-modules",
688
+ enforce: "pre",
689
+ resolveId(id) {
690
+ if (id.endsWith(suffix)) return id;
691
+ },
692
+ async load(id) {
693
+ if (!id.endsWith(suffix)) return;
694
+ const filename = id.slice(0, -suffix.length);
695
+ this.addWatchFile(filename);
696
+ const names = modules.find((module) => module.filename === filename)?.names;
697
+ const result = await extractInstances(
698
+ filename,
699
+ await readFile3(filename, "utf8"),
700
+ root,
701
+ names
702
+ );
703
+ if (!result)
704
+ throw new Error(`Instance resolver missing from ${filename}; rebuild the app client.`);
705
+ return result.server;
706
+ }
707
+ };
708
+ }
709
+ function instanceServerImports(modules) {
710
+ return modules.map(
711
+ (m, i) => `import { resolvers as instances${i} } from ${JSON.stringify(m.filename + suffix)};`
712
+ ).join("\n");
713
+ }
714
+ function instanceServerBindings(modules, registrations) {
715
+ return `[${registrations.map(({ slot, path: viewPath, resolver }) => {
716
+ const index = modules.findIndex((module) => module.names.includes(resolver));
717
+ if (index < 0) throw new Error("Instance resolver was not extracted by the app build.");
718
+ return `{ slot: ${JSON.stringify(slot)}, path: ${JSON.stringify(viewPath)}, ...instances${index}[${JSON.stringify(resolver)}] }`;
719
+ }).join(", ")}]`;
720
+ }
721
+
250
722
  // src/builder/server.ts
251
- async function buildServer(loaded, watch = false, buildOutDir) {
723
+ async function buildServer(loaded, watch = false, buildOutDir, instances = [], registrations = []) {
252
724
  const config = loaded.config.server;
253
725
  if (!config) {
254
726
  return;
255
727
  }
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
- }
728
+ const output = buildOutDir ?? path5.resolve(loaded.root, loaded.config.build?.outDir ?? ".tailorkit");
729
+ const directory = path5.join(output, "server");
730
+ const migrationsDirectory = path5.resolve(loaded.root, config.migrations ?? "./migrations");
731
+ const migrationsOutput = path5.join(output, "migrations");
732
+ const temporaryRoot = path5.join(output, "tmp");
733
+ validateServerBuildOutput(loaded, output);
271
734
  await rm2(directory, { recursive: true, force: true });
272
735
  await mkdir(directory, { recursive: true });
273
736
  await mkdir(temporaryRoot, { recursive: true });
274
- const temporary = await mkdtemp2(path4.join(temporaryRoot, "server-"));
275
- const entry = path4.join(temporary, "entry.mjs");
737
+ const temporary = await mkdtemp2(path5.join(temporaryRoot, "server-"));
738
+ const entry = path5.join(temporary, "entry.mjs");
276
739
  let watching = false;
740
+ let ready;
741
+ const initialBuild = watch ? new Promise((resolve) => {
742
+ ready = resolve;
743
+ }) : void 0;
277
744
  try {
278
745
  await writeFile(
279
746
  entry,
280
- `export { default } from ${JSON.stringify(path4.resolve(loaded.root, config.entry ?? "src/server.ts"))};
747
+ `${instances.length ? `import app from ${JSON.stringify(path5.resolve(loaded.root, config.entry ?? "src/server.ts"))};
748
+ import { withInstanceResolvers } from "tailorkit/server";
749
+ ${instanceServerImports(instances)}
750
+ export default withInstanceResolvers(app, ${instanceServerBindings(instances, registrations)});` : `export { default } from ${JSON.stringify(path5.resolve(loaded.root, config.entry ?? "src/server.ts"))};`}
281
751
  export { default as migrations } from "tailorkit:migrations";
282
752
  export const runtimeManifest = ${JSON.stringify(runtimeManifest)};
283
753
  `
@@ -286,6 +756,7 @@ export const runtimeManifest = ${JSON.stringify(runtimeManifest)};
286
756
  root: loaded.root,
287
757
  configFile: false,
288
758
  plugins: [
759
+ instanceServerPlugin(loaded.root, instances),
289
760
  {
290
761
  name: "tailorkit-app-migrations",
291
762
  closeWatcher: () => rm2(temporary, { recursive: true, force: true }),
@@ -303,7 +774,7 @@ export const runtimeManifest = ${JSON.stringify(runtimeManifest)};
303
774
  config.migrations === void 0
304
775
  );
305
776
  await checkAppSchema(loaded.root, migrationsDirectory);
306
- this.addWatchFile(path4.join(loaded.root, "src/schema.ts"));
777
+ this.addWatchFile(path5.join(loaded.root, "src/schema.ts"));
307
778
  for (const file of files) {
308
779
  this.addWatchFile(file);
309
780
  }
@@ -327,55 +798,65 @@ export const runtimeManifest = ${JSON.stringify(runtimeManifest)};
327
798
  emptyOutDir: false,
328
799
  target: "es2022",
329
800
  watch: watch ? {} : null,
801
+ minify: "oxc",
330
802
  rollupOptions: {
331
803
  external: [],
332
- output: { entryFileNames: "server.js", inlineDynamicImports: true }
804
+ output: {
805
+ entryFileNames: "server.js",
806
+ inlineDynamicImports: true,
807
+ minify: true,
808
+ minifyInternalExports: true
809
+ }
333
810
  }
334
811
  },
335
812
  ssr: { noExternal: true }
336
813
  });
337
814
  watching = watch;
815
+ if (watch && "on" in result && initialBuild) {
816
+ try {
817
+ await Promise.race([
818
+ initialBuild,
819
+ new Promise(
820
+ (_, reject) => result.on("event", (event) => {
821
+ if (event.code === "ERROR") reject(event.error);
822
+ if (event.code === "END") ready?.();
823
+ })
824
+ )
825
+ ]);
826
+ } catch (error) {
827
+ await result.close();
828
+ throw error;
829
+ }
830
+ }
338
831
  return result;
339
832
  } finally {
340
- if (!watching) {
341
- await rm2(temporary, { recursive: true, force: true });
833
+ if (!watching) await rm2(temporary, { recursive: true, force: true });
834
+ }
835
+ }
836
+ function validateServerBuildOutput(loaded, output) {
837
+ if (!loaded.config.server) return;
838
+ const migrationsDirectory = path5.resolve(
839
+ loaded.root,
840
+ loaded.config.server.migrations ?? "./migrations"
841
+ );
842
+ for (const name of ["server", "client", "migrations", "tmp"]) {
843
+ if (overlaps(migrationsDirectory, path5.join(output, name))) {
844
+ throw new Error("App build output must not overlap migration source");
342
845
  }
343
846
  }
344
847
  }
345
848
  function overlaps(source, destination) {
346
849
  const contains = (parent, child) => {
347
- const relative = path4.relative(parent, child);
348
- return relative === "" || !path4.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path4.sep}`);
850
+ const relative = path5.relative(parent, child);
851
+ return relative === "" || !path5.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path5.sep}`);
349
852
  };
350
853
  return contains(source, destination) || contains(destination, source);
351
854
  }
352
855
 
353
- // src/builder/upload-manifest.ts
354
- import { z } from "zod";
355
- var tailorkitUploadManifestSchema = z.object({
356
- assets: z.object({
357
- client: z.literal("client/client.js"),
358
- server: z.literal("server/server.js").optional(),
359
- logos: z.object({
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()
362
- }).optional()
363
- }),
364
- version: z.literal(1)
365
- });
366
- var createTailorKitUploadManifest = (logos, server = false) => tailorkitUploadManifestSchema.parse({
367
- assets: {
368
- client: "client/client.js",
369
- ...server ? { server: "server/server.js" } : {},
370
- ...logos && Object.keys(logos).length > 0 ? { logos } : {}
371
- },
372
- version: 1
373
- });
374
-
375
856
  // src/builder/generate-migrations.ts
376
857
  import { spawn } from "child_process";
377
858
  import { access as access3 } from "fs/promises";
378
- import path5 from "path";
859
+ import path6 from "path";
379
860
  async function generateAppMigrations(options = {}) {
380
861
  const loaded = await loadTailorKitConfig(options.configPath, options.cwd);
381
862
  if (!loaded.config.server) {
@@ -383,13 +864,13 @@ async function generateAppMigrations(options = {}) {
383
864
  "Enable server: {} in tailorkit.config.ts before generating database migrations."
384
865
  );
385
866
  }
386
- await access3(path5.join(loaded.root, "src/schema.ts")).catch((error) => {
867
+ await access3(path6.join(loaded.root, "src/schema.ts")).catch((error) => {
387
868
  throw new Error(
388
869
  "Define the app database schema in src/schema.ts before generating migrations.",
389
870
  { cause: error }
390
871
  );
391
872
  });
392
- const directory = path5.resolve(loaded.root, loaded.config.server.migrations ?? "./migrations");
873
+ const directory = path6.resolve(loaded.root, loaded.config.server.migrations ?? "./migrations");
393
874
  const args = await drizzleGenerateArguments(loaded.root, directory, options.name);
394
875
  await new Promise((resolve, reject) => {
395
876
  const child = spawn(process.execPath, args, { cwd: loaded.root, stdio: "inherit" });
@@ -408,35 +889,61 @@ var preactPackageJson = "preact/package.json";
408
889
  var preactPackageJsonModuleId = "\0tailorkit-preact-package-json";
409
890
  var buildApp = async (options = {}) => {
410
891
  const loaded = await loadTailorKitConfig(options.configPath, options.cwd);
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");
892
+ loaded.root = await realpath(loaded.root);
893
+ const entry = path7.resolve(loaded.root, loaded.config.client?.entry ?? "src/client.ts");
894
+ const serverEntry = path7.resolve(loaded.root, loaded.config.server?.entry ?? "src/server.ts");
413
895
  const outDir = options.outDir ?? loaded.config.build?.outDir ?? ".tailorkit";
414
896
  const preactVersion = getInstalledPreactVersion(loaded.root);
415
897
  assertSupportedPreactVersion(preactVersion);
416
- const resolvedOutDir = path6.resolve(loaded.root, outDir);
417
- const clientOutDir = path6.join(resolvedOutDir, "client");
418
- const serverWatcher = await buildServer(loaded, options.watch, resolvedOutDir);
898
+ const resolvedOutDir = path7.resolve(loaded.root, outDir);
899
+ validateServerBuildOutput(loaded, resolvedOutDir);
900
+ const clientOutDir = path7.join(resolvedOutDir, "client");
901
+ const instanceModules = /* @__PURE__ */ new Map();
902
+ let serverWatcher;
419
903
  const writeBuildExtras = async () => {
904
+ const { views, instanceResolvers } = await readClientManifest(
905
+ path7.join(clientOutDir, "client.js")
906
+ );
907
+ const resolverNames = new Set(instanceResolvers.map((registration) => registration.resolver));
908
+ const activeModules = [...instanceModules.values()].map((module) => ({
909
+ ...module,
910
+ names: module.names.filter((name) => resolverNames.has(name))
911
+ })).filter((module) => module.names.length).sort((a, b) => a.filename.localeCompare(b.filename));
912
+ if (activeModules.reduce((count, module) => count + module.names.length, 0) !== resolverNames.size) {
913
+ throw new Error(
914
+ "Client references an instance resolver that was not extracted by the app build."
915
+ );
916
+ }
917
+ if (serverWatcher && "close" in serverWatcher) await serverWatcher.close();
918
+ serverWatcher = await buildServer(
919
+ loaded,
920
+ options.watch,
921
+ resolvedOutDir,
922
+ activeModules,
923
+ instanceResolvers
924
+ );
420
925
  const logoManifest = {};
421
926
  for (const variant of ["light", "dark"]) {
422
927
  const configuredPath = loaded.config.logos?.[variant];
423
928
  if (!configuredPath) {
424
929
  continue;
425
930
  }
426
- const extension = path6.extname(configuredPath).toLowerCase().slice(1);
931
+ const extension = path7.extname(configuredPath).toLowerCase().slice(1);
427
932
  const contentType = { png: "image/png", svg: "image/svg+xml", webp: "image/webp" }[extension];
428
933
  if (!contentType) {
429
934
  throw new Error(`The ${variant} logo must be an SVG, PNG, or WebP file.`);
430
935
  }
431
- const content = await readFile3(path6.resolve(loaded.root, configuredPath));
936
+ const content = await readFile4(path7.resolve(loaded.root, configuredPath));
432
937
  validateLogoAsset(content, contentType);
433
938
  const filename = `logo-${variant}.${extension}`;
434
- await writeFile2(path6.join(clientOutDir, filename), content);
939
+ await writeFile2(path7.join(clientOutDir, filename), content);
435
940
  logoManifest[variant] = `client/${filename}`;
436
941
  }
942
+ await writeFile2(path7.join(clientOutDir, "views.json"), `${JSON.stringify(views)}
943
+ `);
437
944
  await writeFile2(
438
- path6.join(resolvedOutDir, "tailorkit-upload.json"),
439
- `${JSON.stringify(createTailorKitUploadManifest(logoManifest, Boolean(loaded.config.server)), null, 2)}
945
+ path7.join(resolvedOutDir, "tailorkit-upload.json"),
946
+ `${JSON.stringify(createTailorKitUploadManifest(logoManifest, Boolean(loaded.config.server), views), null, 2)}
440
947
  `,
441
948
  "utf-8"
442
949
  );
@@ -451,6 +958,15 @@ var buildApp = async (options = {}) => {
451
958
  void firstBuild.catch(() => {
452
959
  });
453
960
  }
961
+ const completeBuild = async () => {
962
+ try {
963
+ await writeBuildExtras();
964
+ firstBuildDone?.();
965
+ } catch (error) {
966
+ firstBuildFailed?.(error instanceof Error ? error : new Error(String(error)));
967
+ throw error;
968
+ }
969
+ };
454
970
  const result = await viteBuild({
455
971
  build: {
456
972
  emptyOutDir: true,
@@ -478,13 +994,14 @@ var buildApp = async (options = {}) => {
478
994
  mode: options.mode,
479
995
  oxc: { jsx: { importSource: "preact" } },
480
996
  plugins: [
997
+ instanceExtractionPlugin(loaded.root, Boolean(loaded.config.server), instanceModules),
481
998
  {
482
999
  name: "tailorkit-browser-server-boundary",
483
1000
  enforce: "pre",
484
1001
  async resolveId(id, importer) {
485
1002
  if (/^(?:@tailorkit\/app\/server$|tailorkit\/(?:server|app\/server)$|effect(?:\/|$))/u.test(
486
1003
  id
487
- ) || importer && path6.resolve(path6.dirname(importer), id) === serverEntry) {
1004
+ ) || importer && path7.resolve(path7.dirname(importer), id) === serverEntry) {
488
1005
  throw new Error(
489
1006
  "App server implementations cannot be imported into a browser bundle. Import api from #tailorkit and server types with import type."
490
1007
  );
@@ -503,13 +1020,7 @@ var buildApp = async (options = {}) => {
503
1020
  {
504
1021
  name: "tailorkit-preview-build-ready",
505
1022
  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
- }
1023
+ await completeBuild();
513
1024
  }
514
1025
  },
515
1026
  {
@@ -532,9 +1043,7 @@ var buildApp = async (options = {}) => {
532
1043
  ],
533
1044
  root: loaded.root
534
1045
  }).catch(async (error) => {
535
- if (serverWatcher && typeof serverWatcher === "object" && "close" in serverWatcher) {
536
- await serverWatcher.close();
537
- }
1046
+ if (serverWatcher && "close" in serverWatcher) await serverWatcher.close();
538
1047
  throw error;
539
1048
  });
540
1049
  if (options.watch) {
@@ -551,26 +1060,24 @@ var buildApp = async (options = {}) => {
551
1060
  if (result && typeof result === "object" && "close" in result) {
552
1061
  await result.close();
553
1062
  }
554
- if (serverWatcher && typeof serverWatcher === "object" && "close" in serverWatcher) {
555
- await serverWatcher.close();
556
- }
1063
+ if (serverWatcher && "close" in serverWatcher) await serverWatcher.close();
557
1064
  throw error;
558
1065
  }
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
- };
1066
+ if (result && "close" in result && serverWatcher && "close" in serverWatcher) {
1067
+ const clientWatcher = result;
1068
+ return {
1069
+ on: clientWatcher.on.bind(clientWatcher),
1070
+ async close() {
1071
+ await clientWatcher.close();
1072
+ if (serverWatcher && "close" in serverWatcher) await serverWatcher.close();
1073
+ }
1074
+ };
1075
+ }
569
1076
  }
570
1077
  return result;
571
1078
  };
572
1079
  function getInstalledPreactVersion(root) {
573
- const require2 = createRequire2(path6.join(root, "package.json"));
1080
+ const require2 = createRequire2(path7.join(root, "package.json"));
574
1081
  try {
575
1082
  const packageJsonPath = require2.resolve("preact/package.json");
576
1083
  const packageJson = require2(packageJsonPath);
@@ -8,26 +8,46 @@ import { createContext, h, render } from "preact";
8
8
  import { useContext } from "preact/hooks";
9
9
  var preactVersion = "10.29.8";
10
10
  var createView = (path, options) => {
11
- const Context2 = createContext(null);
11
+ const Context2 = createContext(
12
+ null
13
+ );
14
+ const instances = options.instances;
15
+ if (instances && !instances.resolver) {
16
+ throw new Error("View instance resolvers must be compiled with the TailorKit app build.");
17
+ }
12
18
  const View = (props) => {
13
19
  if (props.status !== "ready") {
14
20
  return null;
15
21
  }
22
+ if (instances && !props.instance) {
23
+ throw new Error(
24
+ `View "${path}" requires a selected instance. Pass instanceKey to Slot or instance to Slot.Controlled.`
25
+ );
26
+ }
16
27
  return h(
17
28
  Context2.Provider,
18
- { value: props.context },
19
- h(options.component, {})
29
+ { value: { context: props.context, instance: props.instance } },
30
+ h(options.component, { key: props.instance?.key })
20
31
  );
21
32
  };
22
33
  return {
23
34
  component: View,
24
35
  path,
36
+ ...instances?.resolver ? { instances: { resolver: instances.resolver } } : {},
25
37
  useContext: () => {
26
38
  const context = useContext(Context2);
27
39
  if (context === null) {
28
40
  throw new Error(`View context is only available while rendering "${path}".`);
29
41
  }
30
- return context;
42
+ return context.context;
43
+ },
44
+ useInstance: () => {
45
+ const value = useContext(Context2);
46
+ if (!value?.instance)
47
+ throw new Error(
48
+ `View instance is only available while rendering an instance of "${path}".`
49
+ );
50
+ return value.instance;
31
51
  }
32
52
  };
33
53
  };
@@ -1,3 +1,7 @@
1
+ import {
2
+ AppError
3
+ } from "./chunk-IJBQ77AH.js";
4
+
1
5
  // src/server/functions.ts
2
6
  function query(definition) {
3
7
  return Object.freeze({ ...definition, kind: "query" });
@@ -59,6 +63,78 @@ var tk = Object.freeze({
59
63
  action: new FunctionBuilder("action", z.undefined(), void 0, {})
60
64
  });
61
65
 
66
+ // src/server/instances.ts
67
+ import { z as z2 } from "zod";
68
+ var addressSchema = z2.object({
69
+ slot: z2.string().min(1).max(255),
70
+ path: z2.string().startsWith("/").max(1024)
71
+ });
72
+ function registeredQueries(functions) {
73
+ const queries = {};
74
+ for (const [name, fn] of Object.entries(functions)) {
75
+ if (isFunction(fn)) {
76
+ if (fn.kind === "query") queries[name] = fn;
77
+ } else {
78
+ const nested = registeredQueries(fn);
79
+ if (Object.keys(nested).length) queries[name] = nested;
80
+ }
81
+ }
82
+ return queries;
83
+ }
84
+ function withInstanceResolvers(app, resolvers) {
85
+ if (Object.hasOwn(app.functions, "_tailorkit")) {
86
+ throw new Error("The _tailorkit function namespace is reserved by TailorKit.");
87
+ }
88
+ const queries = registeredQueries(app.functions);
89
+ const registry = /* @__PURE__ */ new Map();
90
+ for (const resolver of resolvers) {
91
+ addressSchema.parse(resolver);
92
+ let paths = registry.get(resolver.slot);
93
+ if (!paths) {
94
+ paths = /* @__PURE__ */ new Map();
95
+ registry.set(resolver.slot, paths);
96
+ }
97
+ if (paths.has(resolver.path)) {
98
+ throw new Error(
99
+ `Duplicate instance resolver for slot "${resolver.slot}" and path "${resolver.path}".`
100
+ );
101
+ }
102
+ const result = z2.array(
103
+ z2.object({
104
+ key: z2.string().min(1),
105
+ metadata: z2.record(z2.string(), z2.unknown()),
106
+ data: resolver.dataSchema
107
+ })
108
+ ).refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
109
+ message: "Instance keys must be unique"
110
+ });
111
+ paths.set(resolver.path, { resolver, result });
112
+ }
113
+ const instances = {
114
+ resolve: action({
115
+ functions: queries,
116
+ args: addressSchema.extend({ context: z2.record(z2.string(), z2.unknown()) }),
117
+ async handler({ args, identity, signal, queries: queries2 }) {
118
+ const entry = registry.get(args.slot)?.get(args.path);
119
+ if (!entry) throw new AppError("NOT_FOUND", "Instance resolver not found");
120
+ const value = await entry.resolver.resolve({
121
+ context: args.context,
122
+ identity,
123
+ signal,
124
+ queries: queries2
125
+ });
126
+ const result = entry.result.safeParse(value);
127
+ if (!result.success) throw new AppError("INTERNAL_SERVER_ERROR", "Invalid function result");
128
+ return result.data;
129
+ }
130
+ })
131
+ };
132
+ const internal = defineServer({ instances });
133
+ return Object.freeze({
134
+ functions: { ...app.functions, _tailorkit: internal.functions }
135
+ });
136
+ }
137
+
62
138
  // src/database/schema.ts
63
139
  import {
64
140
  sqliteTable,
@@ -98,6 +174,7 @@ export {
98
174
  action,
99
175
  defineServer,
100
176
  tk,
177
+ withInstanceResolvers,
101
178
  table,
102
179
  text,
103
180
  integer,
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  useAction,
11
11
  useMutation,
12
12
  useQuery
13
- } from "./chunk-3XA36UOQ.js";
13
+ } from "./chunk-43L6XVW6.js";
14
14
  import {
15
15
  AppError,
16
16
  appError
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  useAction,
11
11
  useMutation,
12
12
  useQuery
13
- } from "./chunk-3XA36UOQ.js";
13
+ } from "./chunk-43L6XVW6.js";
14
14
  import {
15
15
  action,
16
16
  and,
@@ -35,8 +35,9 @@ import {
35
35
  real,
36
36
  table,
37
37
  text,
38
- tk
39
- } from "./chunk-46CTW3XJ.js";
38
+ tk,
39
+ withInstanceResolvers
40
+ } from "./chunk-7NOEYJOB.js";
40
41
  import {
41
42
  AppError,
42
43
  appError
@@ -78,5 +79,6 @@ export {
78
79
  tk,
79
80
  useAction,
80
81
  useMutation,
81
- useQuery
82
+ useQuery,
83
+ withInstanceResolvers
82
84
  };
package/dist/protocol.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { oc, type } from "@orpc/contract";
3
3
  import { z } from "zod";
4
4
  var invocationSchema = z.strictObject({
5
- name: z.string().max(512).regex(/^[a-zA-Z][a-zA-Z0-9_]{0,63}(?:\.[a-zA-Z][a-zA-Z0-9_]{0,63})*$/u),
5
+ name: z.string().max(512).regex(/^(?:[a-zA-Z][a-zA-Z0-9_]{0,63}|_tailorkit)(?:\.[a-zA-Z][a-zA-Z0-9_]{0,63})*$/u),
6
6
  args: z.unknown().optional()
7
7
  });
8
8
  var procedure = oc.errors({
@@ -14,7 +14,7 @@ export type FunctionResult<D> = D extends {
14
14
  } ? z.output<R> : D extends {
15
15
  handler: (context: never) => infer O;
16
16
  } ? Awaited<O> : never;
17
- type FunctionCalls<F, K extends "query" | "mutation"> = {
17
+ export type FunctionCalls<F, K extends "query" | "mutation"> = {
18
18
  readonly [N in keyof F as F[N] extends {
19
19
  kind: FunctionKind;
20
20
  } ? (F[N] extends {
@@ -87,4 +87,3 @@ export declare function defineServer<const F extends Functions>(functions: F): R
87
87
  functions: F;
88
88
  }>;
89
89
  export type AppDefinition = ReturnType<typeof defineServer>;
90
- export {};
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import type { AppDefinition, Identity } from "./functions";
3
+ interface InstanceResolver {
4
+ slot: string;
5
+ path: string;
6
+ dataSchema: z.ZodType;
7
+ resolve: (context: {
8
+ context: Record<string, unknown>;
9
+ identity: Identity;
10
+ signal: AbortSignal;
11
+ queries: unknown;
12
+ }) => unknown;
13
+ }
14
+ /** Build-generated actions run through the existing authenticated action runtime. */
15
+ export declare function withInstanceResolvers(app: AppDefinition, resolvers: readonly InstanceResolver[]): AppDefinition;
16
+ export {};
package/dist/server.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { defineServer, query, mutation, action } from "./server/functions";
2
2
  export { tk } from "./server/builder";
3
+ export { withInstanceResolvers } from "./server/instances";
3
4
  export type { QueryDatabase, MutationDatabase } from "./database/types";
4
5
  export type { Identity, FunctionDefinition, AppDefinition, ActionContext, FunctionKind, } from "./server/functions";
5
6
  export { table, text, integer, real, boolean, eq, ne, gt, gte, lt, lte, and, or, asc, desc, count, isNull, isNotNull, inArray, } from "./database/schema";
package/dist/server.js CHANGED
@@ -22,8 +22,9 @@ import {
22
22
  real,
23
23
  table,
24
24
  text,
25
- tk
26
- } from "./chunk-46CTW3XJ.js";
25
+ tk,
26
+ withInstanceResolvers
27
+ } from "./chunk-7NOEYJOB.js";
27
28
  import {
28
29
  AppError
29
30
  } from "./chunk-IJBQ77AH.js";
@@ -52,5 +53,6 @@ export {
52
53
  real,
53
54
  table,
54
55
  text,
55
- tk
56
+ tk,
57
+ withInstanceResolvers
56
58
  };
package/dist/views.d.ts CHANGED
@@ -1,9 +1,29 @@
1
1
  import { h } from "preact";
2
2
  import type { ComponentChild, ComponentChildren, ComponentType, VNode } from "preact";
3
+ import type { z } from "zod";
4
+ import type { FunctionCalls, Identity } from "./server/functions";
3
5
  export interface TailorKitViews {
4
6
  }
5
7
  export interface TailorKitSlots {
6
8
  }
9
+ export interface TailorKitServerFunctions {
10
+ }
11
+ export interface ViewInstance<TData = unknown> {
12
+ key: string;
13
+ metadata: Record<string, unknown>;
14
+ data: TData;
15
+ }
16
+ export interface InstanceResolverContext<TPath extends AppViewPath> {
17
+ context: ViewContext<TPath>;
18
+ identity: Identity;
19
+ signal: AbortSignal;
20
+ queries: FunctionCalls<TailorKitServerFunctions, "query">;
21
+ }
22
+ export interface ViewInstances<TPath extends AppViewPath, TSchema extends z.ZodType> {
23
+ dataSchema: TSchema;
24
+ /** Runs on the app server. Only registered queries are available. */
25
+ resolve: (context: InstanceResolverContext<TPath>) => ViewInstance<z.input<TSchema>>[] | Promise<ViewInstance<z.input<TSchema>>[]>;
26
+ }
7
27
  export type SlotName = keyof TailorKitSlots & string;
8
28
  export type ViewPath = Extract<keyof TailorKitViews & string, `/${string}`>;
9
29
  export type AppViewPath = ViewPath;
@@ -13,23 +33,32 @@ export type ViewContext<TPath extends AppViewPath> = ViewPropsForPath<TPath> ext
13
33
  context: infer TContext;
14
34
  } ? TContext : Record<string, never>;
15
35
  export type View<TProps extends object = Record<string, never>> = (props: TProps) => ComponentChild;
16
- export type ViewRuntimeProps<TPath extends AppViewPath> = {
36
+ export type ViewRuntimeProps<TPath extends AppViewPath, TData = unknown> = {
17
37
  context: ViewContext<TPath>;
18
38
  view: TPath;
19
39
  status: "ready";
40
+ instance?: ViewInstance<TData>;
20
41
  } | {
21
42
  context?: never;
22
43
  view: TPath;
23
44
  status: "loading";
45
+ instance?: never;
24
46
  } | {
25
47
  context?: never;
26
48
  view: TPath;
27
49
  status: "error";
50
+ instance?: never;
28
51
  };
29
- export interface ViewDefinition<TPath extends AppViewPath = AppViewPath> {
52
+ export interface ViewDefinition<TPath extends AppViewPath = AppViewPath, TData = unknown> {
30
53
  component: View<ViewRuntimeProps<TPath>>;
31
54
  path: TPath;
32
55
  useContext: () => ViewContext<TPath>;
56
+ /** The selected instance, with data inferred from instances.dataSchema. */
57
+ useInstance: () => ViewInstance<TData>;
58
+ /** Build-only lookup key connecting this view to its extracted server implementation. */
59
+ instances?: {
60
+ resolver: string;
61
+ };
33
62
  }
34
63
  type RegisteredViews = {
35
64
  [TPath in AppViewPath]: ViewDefinition<TPath>;
@@ -72,9 +101,10 @@ export type TailorKitClientWithMeta<TViews extends SlotDefinitions = SlotDefinit
72
101
  $meta: TailorKitClientMeta;
73
102
  $runtime: TailorKitClientRuntime;
74
103
  };
75
- export declare const createView: <const TPath extends AppViewPath>(path: TPath, options: {
104
+ export declare const createView: <const TPath extends AppViewPath, TSchema extends z.ZodType>(path: TPath, options: {
76
105
  component: View<Record<string, never>>;
77
- }) => ViewDefinition<TPath>;
106
+ instances?: ViewInstances<TPath, TSchema>;
107
+ }) => ViewDefinition<TPath, z.output<TSchema>>;
78
108
  export declare const defineClient: <const TSlots extends SlotDefinitions>(client: TailorKitClient<TSlots> & {
79
109
  slots: { [V in keyof TSlots]: TSlots[V] & RequireViewPaths<TSlots[V]> & RequireMatchingViewKeys<TSlots[V]> & RequireSlotViews<V, TSlots[V]>; };
80
110
  } & (Exclude<keyof TSlots, SlotName> extends never ? unknown : {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailorkit/app",
3
- "version": "0.1.0-beta.18",
3
+ "version": "0.1.0-beta.19",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",