appflare 0.0.13 → 0.0.15

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/cli/core/build.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  AppflareConfig,
6
6
  assertDirExists,
7
7
  assertFileExists,
8
+ toImportPathFromGeneratedSrc,
8
9
  } from "../utils/utils";
9
10
  import { getSchemaTableNames, generateSchemaTypes } from "../schema/schema";
10
11
  import {
@@ -38,6 +39,17 @@ export async function buildFromConfig(params: {
38
39
  await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
39
40
  await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
40
41
 
42
+ // Re-export the user schema inside the generated output so downstream code can import it from the build directory.
43
+ const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
44
+ outDirAbs,
45
+ schemaPathAbs
46
+ );
47
+ const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
48
+ export type AppflareGeneratedSchema = typeof schema;
49
+ export default schema;
50
+ `;
51
+ await fs.writeFile(path.join(outDirAbs, "src", "schema.ts"), schemaReexport);
52
+
41
53
  const schemaTypesTs = await generateSchemaTypes({
42
54
  schemaPathAbs,
43
55
  configPathAbs,
@@ -140,17 +152,18 @@ async function writeEmitTsconfig(params: {
140
152
  declaration: true,
141
153
  emitDeclarationOnly: false,
142
154
  outDir: `./${outDirRel}/dist`,
143
- rootDir: `./${outDirRel}/src`,
155
+ rootDir: `.`,
144
156
  sourceMap: false,
145
157
  declarationMap: false,
146
158
  skipLibCheck: true,
147
159
  target: "ES2022",
148
160
  module: "ES2022",
149
161
  moduleResolution: "Bundler",
150
- types: [],
162
+ types: ["node"],
151
163
  },
152
164
  include: [
153
165
  `./${outDirRel}/src/schema-types.ts`,
166
+ `./${outDirRel}/src/schema.ts`,
154
167
  `./${outDirRel}/src/handlers/**/*`,
155
168
  ],
156
169
  };
@@ -29,6 +29,13 @@ export async function discoverHandlers(params: {
29
29
  if (path.resolve(fileAbs) === path.resolve(params.schemaPathAbs)) continue;
30
30
  if (path.resolve(fileAbs) === path.resolve(params.configPathAbs)) continue;
31
31
 
32
+ const relPathRaw = path.relative(params.projectDirAbs, fileAbs);
33
+ const relPath = relPathRaw.replace(/\\/g, "/");
34
+ const rawRoutePath = relPath.replace(/\.ts$/, "");
35
+ const routePath = rawRoutePath.endsWith("/index")
36
+ ? rawRoutePath.slice(0, -"/index".length) || "index"
37
+ : rawRoutePath;
38
+
32
39
  const content = await fs.readFile(fileAbs, "utf8");
33
40
  const cronTriggersByHandler = extractCronTriggers(content);
34
41
  const regex =
@@ -38,6 +45,7 @@ export async function discoverHandlers(params: {
38
45
  const kind = match[2] as HandlerKind;
39
46
  handlers.push({
40
47
  fileName: path.basename(fileAbs, ".ts"),
48
+ routePath,
41
49
  name: match[1],
42
50
  kind,
43
51
  sourceFileAbs: fileAbs,
@@ -50,7 +58,7 @@ export async function discoverHandlers(params: {
50
58
  // De-dupe: keep first occurrence
51
59
  const seen = new Set<string>();
52
60
  const unique = handlers.filter((h) => {
53
- const key = `${h.kind}:${h.fileName}:${h.name}`;
61
+ const key = `${h.kind}:${h.routePath}:${h.name}`;
54
62
  if (seen.has(key)) return false;
55
63
  seen.add(key);
56
64
  return true;
@@ -58,6 +66,8 @@ export async function discoverHandlers(params: {
58
66
 
59
67
  unique.sort((a, b) => {
60
68
  if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
69
+ if (a.routePath !== b.routePath)
70
+ return a.routePath.localeCompare(b.routePath);
61
71
  if (a.fileName !== b.fileName) return a.fileName.localeCompare(b.fileName);
62
72
  return a.name.localeCompare(b.name);
63
73
  });
package/cli/core/index.ts CHANGED
@@ -14,12 +14,20 @@ import {
14
14
  } from "./handlers";
15
15
  import { generateSchemaTypes, getSchemaTableNames } from "../schema/schema";
16
16
  import { runTscEmit, writeEmitTsconfig } from "../utils/tsc";
17
- import { assertDirExists, assertFileExists } from "../utils/utils";
17
+ import {
18
+ assertDirExists,
19
+ assertFileExists,
20
+ toImportPathFromGeneratedSrc,
21
+ } from "../utils/utils";
18
22
 
19
23
  type AppflareConfig = {
20
24
  dir: string;
21
25
  schema: string;
22
26
  outDir: string;
27
+ auth?: {
28
+ enabled?: boolean;
29
+ basePath?: string;
30
+ };
23
31
  };
24
32
 
25
33
  const program = new Command();
@@ -106,6 +114,16 @@ async function buildFromConfig(params: {
106
114
  await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
107
115
  await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
108
116
 
117
+ const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
118
+ outDirAbs,
119
+ schemaPathAbs
120
+ );
121
+ const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
122
+ export type AppflareGeneratedSchema = typeof schema;
123
+ export default schema;
124
+ `;
125
+ await fs.writeFile(path.join(outDirAbs, "src", "schema.ts"), schemaReexport);
126
+
109
127
  const schemaTypesTs = await generateSchemaTypes({
110
128
  schemaPathAbs,
111
129
  configPathAbs,
@@ -3,91 +3,161 @@ import {
3
3
  handlerTypePrefix,
4
4
  normalizeTableName,
5
5
  renderObjectKey,
6
- sortedEntries,
7
6
  } from "./utils";
8
7
 
8
+ type PathTree<T> = {
9
+ leaf?: { path: string; items: T[] };
10
+ children: Map<string, PathTree<T>>;
11
+ };
12
+
13
+ const buildPathTree = <T>(byPath: Map<string, T[]>): PathTree<T> => {
14
+ const root: PathTree<T> = { children: new Map() };
15
+ for (const [path, items] of Array.from(byPath.entries())) {
16
+ const segments = path.split("/").filter(Boolean);
17
+ let node = root;
18
+ for (const segment of segments) {
19
+ if (!node.children.has(segment)) {
20
+ node.children.set(segment, { children: new Map() });
21
+ }
22
+ node = node.children.get(segment)!;
23
+ }
24
+ node.leaf = { path, items };
25
+ }
26
+ return root;
27
+ };
28
+
29
+ const indent = (depth: number): string => "\t".repeat(depth);
30
+
31
+ const renderPathTreeLines = <T>(
32
+ node: PathTree<T>,
33
+ depth: number,
34
+ renderLeaf: (leaf: { path: string; items: T[] }, depth: number) => string[]
35
+ ): string[] => {
36
+ const lines: string[] = [];
37
+ if (node.leaf) {
38
+ lines.push(...renderLeaf(node.leaf, depth));
39
+ }
40
+ const children = Array.from(node.children.entries()).sort((a, b) =>
41
+ a[0].localeCompare(b[0])
42
+ );
43
+ for (const [segment, child] of children) {
44
+ lines.push(`${indent(depth + 1)}${renderObjectKey(segment)}: {`);
45
+ lines.push(...renderPathTreeLines(child, depth + 1, renderLeaf));
46
+ lines.push(`${indent(depth + 1)}}`);
47
+ }
48
+ return lines;
49
+ };
50
+
9
51
  export function generateQueriesClientLines(
10
52
  queriesByFile: Map<string, DiscoveredHandler[]>,
11
53
  importAliasBySource: Map<string, string>
12
54
  ): string {
13
- return sortedEntries(queriesByFile)
14
- .map(([fileName, list]) => {
15
- const fileKey = renderObjectKey(fileName);
16
- const inner = list
17
- .slice()
18
- .sort((a, b) => a.name.localeCompare(b.name))
19
- .map((h) => {
20
- const pascal = handlerTypePrefix(h);
21
- const route = `/queries/${fileName}/${h.name}`;
22
- const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
23
- const handlerAccessor = `${importAlias}.${h.name}`;
24
- return (
25
- `\t\t${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
26
- `\t\t\tasync (args: ${pascal}Args, init) => {\n` +
27
- `\t\t\t\tconst url = buildQueryUrl(baseUrl, ${JSON.stringify(route)}, args);\n` +
28
- `\t\t\t\tconst response = await request(url, {\n` +
29
- `\t\t\t\t\t...(init ?? {}),\n` +
30
- `\t\t\t\t\tmethod: "GET",\n` +
31
- `\t\t\t\t});\n` +
32
- `\t\t\t\treturn parseJson<${pascal}Result>(response);\n` +
33
- `\t\t\t},\n` +
34
- `\t\t\t{\n` +
35
- `\t\t\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
36
- `\t\t\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
37
- `\t\t\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(fileName))},\n` +
38
- `\t\t\t\t\tdefaultHandler: { file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(h.name)} },\n` +
39
- `\t\t\t\t}),\n` +
40
- `\t\t\t\tpath: ${JSON.stringify(route)},\n` +
41
- `\t\t\t}\n` +
42
- `\t\t),`
43
- );
44
- })
45
- .join("\n");
46
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t},`;
47
- })
48
- .join("\n");
55
+ const tree = buildPathTree(queriesByFile);
56
+ const renderLeaf = (
57
+ leaf: { path: string; items: DiscoveredHandler[] },
58
+ depth: number
59
+ ): string[] => {
60
+ const inner = leaf.items
61
+ .slice()
62
+ .sort((a, b) => a.name.localeCompare(b.name))
63
+ .map((h) => {
64
+ const pascal = handlerTypePrefix(h);
65
+ const route = `/queries/${leaf.path}/${h.name}`;
66
+ const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
67
+ const handlerAccessor = `${importAlias}.${h.name}`;
68
+ const pad = indent(depth + 1);
69
+ return (
70
+ `${pad}${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
71
+ `${pad}\tasync (args: ${pascal}Args, init) => {\n` +
72
+ `${pad}\t\tconst url = buildQueryUrl(baseUrl, ${JSON.stringify(route)}, args);\n` +
73
+ `${pad}\t\tconst response = await request(url, {\n` +
74
+ `${pad}\t\t\t...(init ?? {}),\n` +
75
+ `${pad}\t\t\tmethod: "GET",\n` +
76
+ `${pad}\t\t});\n` +
77
+ `${pad}\t\treturn parseJson<${pascal}Result>(response);\n` +
78
+ `${pad}\t},\n` +
79
+ `${pad}\t{\n` +
80
+ `${pad}\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
81
+ `${pad}\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
82
+ `${pad}\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(h.fileName))},\n` +
83
+ `${pad}\t\t\tdefaultHandler: { file: ${JSON.stringify(leaf.path)}, name: ${JSON.stringify(h.name)} },\n` +
84
+ `${pad}\t\t}),\n` +
85
+ `${pad}\t\tpath: ${JSON.stringify(route)},\n` +
86
+ `${pad}\t}\n` +
87
+ `${pad}),`
88
+ );
89
+ })
90
+ .join("\n");
91
+
92
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
93
+ };
94
+
95
+ const lines: string[] = [];
96
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
97
+ a[0].localeCompare(b[0])
98
+ );
99
+ for (const [segment, child] of children) {
100
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
101
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
102
+ lines.push(`${indent(1)}}`);
103
+ }
104
+ return lines.join("\n");
49
105
  }
50
106
 
51
107
  export function generateMutationsClientLines(
52
108
  mutationsByFile: Map<string, DiscoveredHandler[]>,
53
109
  importAliasBySource: Map<string, string>
54
110
  ): string {
55
- return sortedEntries(mutationsByFile)
56
- .map(([fileName, list]) => {
57
- const fileKey = renderObjectKey(fileName);
58
- const inner = list
59
- .slice()
60
- .sort((a, b) => a.name.localeCompare(b.name))
61
- .map((h) => {
62
- const pascal = handlerTypePrefix(h);
63
- const route = `/mutations/${fileName}/${h.name}`;
64
- const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
65
- const handlerAccessor = `${importAlias}.${h.name}`;
66
- return (
67
- `\t\t${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
68
- `\t\t\tasync (args: ${pascal}Args, init) => {\n` +
69
- `\t\t\t\tconst url = buildUrl(baseUrl, ${JSON.stringify(route)});\n` +
70
- `\t\t\t\tconst response = await request(url, {\n` +
71
- `\t\t\t\t\t...(init ?? {}),\n` +
72
- `\t\t\t\t\tmethod: "POST",\n` +
73
- `\t\t\t\t\theaders: ensureJsonHeaders(init?.headers),\n` +
74
- `\t\t\t\t\tbody: JSON.stringify(args),\n` +
75
- `\t\t\t\t});\n` +
76
- `\t\t\t\treturn parseJson<${pascal}Result>(response);\n` +
77
- `\t\t\t},\n` +
78
- `\t\t\t{\n` +
79
- `\t\t\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
80
- `\t\t\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
81
- `\t\t\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(fileName))},\n` +
82
- `\t\t\t\t\tdefaultHandler: { file: ${JSON.stringify(fileName)}, name: ${JSON.stringify(h.name)} },\n` +
83
- `\t\t\t\t}),\n` +
84
- `\t\t\t\tpath: ${JSON.stringify(route)},\n` +
85
- `\t\t\t}\n` +
86
- `\t\t),`
87
- );
88
- })
89
- .join("\n");
90
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t},`;
91
- })
92
- .join("\n");
111
+ const tree = buildPathTree(mutationsByFile);
112
+ const renderLeaf = (
113
+ leaf: { path: string; items: DiscoveredHandler[] },
114
+ depth: number
115
+ ): string[] => {
116
+ const inner = leaf.items
117
+ .slice()
118
+ .sort((a, b) => a.name.localeCompare(b.name))
119
+ .map((h) => {
120
+ const pascal = handlerTypePrefix(h);
121
+ const route = `/mutations/${leaf.path}/${h.name}`;
122
+ const importAlias = importAliasBySource.get(h.sourceFileAbs)!;
123
+ const handlerAccessor = `${importAlias}.${h.name}`;
124
+ const pad = indent(depth + 1);
125
+ return (
126
+ `${pad}${h.name}: withHandlerMetadata<${pascal}Definition>(\n` +
127
+ `${pad}\tasync (args: ${pascal}Args, init) => {\n` +
128
+ `${pad}\t\tconst url = buildUrl(baseUrl, ${JSON.stringify(route)});\n` +
129
+ `${pad}\t\tconst response = await request(url, {\n` +
130
+ `${pad}\t\t\t...(init ?? {}),\n` +
131
+ `${pad}\t\t\tmethod: "POST",\n` +
132
+ `${pad}\t\t\theaders: ensureJsonHeaders(init?.headers),\n` +
133
+ `${pad}\t\t\tbody: JSON.stringify(args),\n` +
134
+ `${pad}\t\t});\n` +
135
+ `${pad}\t\treturn parseJson<${pascal}Result>(response);\n` +
136
+ `${pad}\t},\n` +
137
+ `${pad}\t{\n` +
138
+ `${pad}\t\tschema: createHandlerSchema(${handlerAccessor}.args),\n` +
139
+ `${pad}\t\twebsocket: createHandlerWebsocket<${pascal}Args, ${pascal}Result>(realtime, {\n` +
140
+ `${pad}\t\t\tdefaultTable: ${JSON.stringify(normalizeTableName(h.fileName))},\n` +
141
+ `${pad}\t\t\tdefaultHandler: { file: ${JSON.stringify(leaf.path)}, name: ${JSON.stringify(h.name)} },\n` +
142
+ `${pad}\t\t}),\n` +
143
+ `${pad}\t\tpath: ${JSON.stringify(route)},\n` +
144
+ `${pad}\t}\n` +
145
+ `${pad}),`
146
+ );
147
+ })
148
+ .join("\n");
149
+
150
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
151
+ };
152
+
153
+ const lines: string[] = [];
154
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
155
+ a[0].localeCompare(b[0])
156
+ );
157
+ for (const [segment, child] of children) {
158
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
159
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
160
+ lines.push(`${indent(1)}}`);
161
+ }
162
+ return lines.join("\n");
93
163
  }
@@ -369,6 +369,15 @@ export async function runInternalQuery<
369
369
  args: HandlerArgsFromShape<TArgs>
370
370
  ): Promise<TResult> {
371
371
  const parsed = parseHandlerArgs(handler as any, args as any);
372
+ if (handler.middleware) {
373
+ const middlewareResult = await handler.middleware(
374
+ ctx as any,
375
+ parsed as any
376
+ );
377
+ if (typeof middlewareResult !== "undefined") {
378
+ return middlewareResult as TResult;
379
+ }
380
+ }
372
381
  return handler.handler(ctx as any, parsed as any);
373
382
  }
374
383
 
@@ -381,6 +390,15 @@ export async function runInternalMutation<
381
390
  args: HandlerArgsFromShape<TArgs>
382
391
  ): Promise<TResult> {
383
392
  const parsed = parseHandlerArgs(handler as any, args as any);
393
+ if (handler.middleware) {
394
+ const middlewareResult = await handler.middleware(
395
+ ctx as any,
396
+ parsed as any
397
+ );
398
+ if (typeof middlewareResult !== "undefined") {
399
+ return middlewareResult as TResult;
400
+ }
401
+ }
384
402
  return handler.handler(ctx as any, parsed as any);
385
403
  }
386
404
 
@@ -663,7 +681,7 @@ function generateImports(params: {
663
681
  const importLines: string[] = [];
664
682
  const importAliasBySource = new Map<string, string>();
665
683
  for (const [fileAbs, list] of Array.from(handlerImportsGrouped.entries())) {
666
- const alias = `__appflare_${pascalCase(list[0].fileName)}`;
684
+ const alias = `__appflare_${pascalCase(list[0].routePath)}`;
667
685
  importAliasBySource.set(fileAbs, alias);
668
686
  const importPath = toImportPathFromGeneratedSrc(params.outDirAbs, fileAbs);
669
687
  importLines.push(
@@ -686,10 +704,13 @@ function generateGroupedHandlers(handlers: DiscoveredHandler[]): {
686
704
  (h) => h.kind === "internalMutation"
687
705
  );
688
706
 
689
- const queriesByFile = groupBy(queries, (h) => h.fileName);
690
- const mutationsByFile = groupBy(mutations, (h) => h.fileName);
691
- const internalQueriesByFile = groupBy(internalQueries, (h) => h.fileName);
692
- const internalMutationsByFile = groupBy(internalMutations, (h) => h.fileName);
707
+ const queriesByFile = groupBy(queries, (h) => h.routePath);
708
+ const mutationsByFile = groupBy(mutations, (h) => h.routePath);
709
+ const internalQueriesByFile = groupBy(internalQueries, (h) => h.routePath);
710
+ const internalMutationsByFile = groupBy(
711
+ internalMutations,
712
+ (h) => h.routePath
713
+ );
693
714
 
694
715
  return {
695
716
  queriesByFile,
@@ -1,5 +1,48 @@
1
1
  import { DiscoveredHandler } from "../../utils/utils";
2
- import { handlerTypePrefix, renderObjectKey, sortedEntries } from "./utils";
2
+ import { handlerTypePrefix, renderObjectKey } from "./utils";
3
+
4
+ type PathTree<T> = {
5
+ leaf?: { path: string; items: T[] };
6
+ children: Map<string, PathTree<T>>;
7
+ };
8
+
9
+ const buildPathTree = <T>(byPath: Map<string, T[]>): PathTree<T> => {
10
+ const root: PathTree<T> = { children: new Map() };
11
+ for (const [path, items] of Array.from(byPath.entries())) {
12
+ const segments = path.split("/").filter(Boolean);
13
+ let node = root;
14
+ for (const segment of segments) {
15
+ if (!node.children.has(segment)) {
16
+ node.children.set(segment, { children: new Map() });
17
+ }
18
+ node = node.children.get(segment)!;
19
+ }
20
+ node.leaf = { path, items };
21
+ }
22
+ return root;
23
+ };
24
+
25
+ const indent = (depth: number): string => "\t".repeat(depth);
26
+
27
+ const renderPathTreeLines = <T>(
28
+ node: PathTree<T>,
29
+ depth: number,
30
+ renderLeaf: (leaf: { path: string; items: T[] }, depth: number) => string[]
31
+ ): string[] => {
32
+ const lines: string[] = [];
33
+ if (node.leaf) {
34
+ lines.push(...renderLeaf(node.leaf, depth));
35
+ }
36
+ const children = Array.from(node.children.entries()).sort((a, b) =>
37
+ a[0].localeCompare(b[0])
38
+ );
39
+ for (const [segment, child] of children) {
40
+ lines.push(`${indent(depth + 1)}${renderObjectKey(segment)}: {`);
41
+ lines.push(...renderPathTreeLines(child, depth + 1, renderLeaf));
42
+ lines.push(`${indent(depth + 1)}}`);
43
+ }
44
+ return lines;
45
+ };
3
46
 
4
47
  export function generateTypeBlocks(
5
48
  handlers: DiscoveredHandler[],
@@ -23,57 +66,99 @@ export function generateTypeBlocks(
23
66
  export function generateQueriesTypeLines(
24
67
  queriesByFile: Map<string, DiscoveredHandler[]>
25
68
  ): string {
26
- return sortedEntries(queriesByFile)
27
- .map(([fileName, list]) => {
28
- const fileKey = renderObjectKey(fileName);
29
- const inner = list
30
- .slice()
31
- .sort((a, b) => a.name.localeCompare(b.name))
32
- .map((h) => {
33
- const pascal = handlerTypePrefix(h);
34
- return `\t\t${h.name}: ${pascal}Client;`;
35
- })
36
- .join("\n");
37
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
38
- })
39
- .join("\n");
69
+ const tree = buildPathTree(queriesByFile);
70
+ const renderLeaf = (
71
+ leaf: { path: string; items: DiscoveredHandler[] },
72
+ depth: number
73
+ ): string[] => {
74
+ const inner = leaf.items
75
+ .slice()
76
+ .sort((a, b) => a.name.localeCompare(b.name))
77
+ .map((h) => {
78
+ const pascal = handlerTypePrefix(h);
79
+ return `${indent(depth + 1)}${h.name}: ${pascal}Client;`;
80
+ })
81
+ .join("\n");
82
+
83
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
84
+ };
85
+
86
+ const lines: string[] = [];
87
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
88
+ a[0].localeCompare(b[0])
89
+ );
90
+ for (const [segment, child] of children) {
91
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
92
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
93
+ lines.push(`${indent(1)}}`);
94
+ }
95
+
96
+ return lines.join("\n");
40
97
  }
41
98
 
42
99
  export function generateMutationsTypeLines(
43
100
  mutationsByFile: Map<string, DiscoveredHandler[]>
44
101
  ): string {
45
- return sortedEntries(mutationsByFile)
46
- .map(([fileName, list]) => {
47
- const fileKey = renderObjectKey(fileName);
48
- const inner = list
49
- .slice()
50
- .sort((a, b) => a.name.localeCompare(b.name))
51
- .map((h) => {
52
- const pascal = handlerTypePrefix(h);
53
- return `\t\t${h.name}: ${pascal}Client;`;
54
- })
55
- .join("\n");
56
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
57
- })
58
- .join("\n");
102
+ const tree = buildPathTree(mutationsByFile);
103
+ const renderLeaf = (
104
+ leaf: { path: string; items: DiscoveredHandler[] },
105
+ depth: number
106
+ ): string[] => {
107
+ const inner = leaf.items
108
+ .slice()
109
+ .sort((a, b) => a.name.localeCompare(b.name))
110
+ .map((h) => {
111
+ const pascal = handlerTypePrefix(h);
112
+ return `${indent(depth + 1)}${h.name}: ${pascal}Client;`;
113
+ })
114
+ .join("\n");
115
+
116
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
117
+ };
118
+
119
+ const lines: string[] = [];
120
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
121
+ a[0].localeCompare(b[0])
122
+ );
123
+ for (const [segment, child] of children) {
124
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
125
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
126
+ lines.push(`${indent(1)}}`);
127
+ }
128
+
129
+ return lines.join("\n");
59
130
  }
60
131
 
61
132
  export function generateInternalTypeLines(
62
133
  internalByFile: Map<string, DiscoveredHandler[]>,
63
134
  importAliasBySource: Map<string, string>
64
135
  ): string {
65
- return sortedEntries(internalByFile)
66
- .map(([fileName, list]) => {
67
- const fileKey = renderObjectKey(fileName);
68
- const inner = list
69
- .slice()
70
- .sort((a, b) => a.name.localeCompare(b.name))
71
- .map((h) => {
72
- const alias = importAliasBySource.get(h.sourceFileAbs)!;
73
- return `\t\t${h.name}: typeof ${alias}[${JSON.stringify(h.name)}];`;
74
- })
75
- .join("\n");
76
- return `\t${fileKey}: {\n${inner || "\t\t// (none)"}\n\t};`;
77
- })
78
- .join("\n");
136
+ const tree = buildPathTree(internalByFile);
137
+ const renderLeaf = (
138
+ leaf: { path: string; items: DiscoveredHandler[] },
139
+ depth: number
140
+ ): string[] => {
141
+ const inner = leaf.items
142
+ .slice()
143
+ .sort((a, b) => a.name.localeCompare(b.name))
144
+ .map((h) => {
145
+ const alias = importAliasBySource.get(h.sourceFileAbs)!;
146
+ return `${indent(depth + 1)}${h.name}: typeof ${alias}[${JSON.stringify(h.name)}];`;
147
+ })
148
+ .join("\n");
149
+
150
+ return inner ? [inner] : [`${indent(depth + 1)}// (none)`];
151
+ };
152
+
153
+ const lines: string[] = [];
154
+ const children = Array.from(tree.children.entries()).sort((a, b) =>
155
+ a[0].localeCompare(b[0])
156
+ );
157
+ for (const [segment, child] of children) {
158
+ lines.push(`${indent(1)}${renderObjectKey(segment)}: {`);
159
+ lines.push(...renderPathTreeLines(child, 1, renderLeaf));
160
+ lines.push(`${indent(1)}}`);
161
+ }
162
+
163
+ return lines.join("\n");
79
164
  }
@@ -6,13 +6,17 @@ export const sortedEntries = <T>(map: Map<string, T[]>): Array<[string, T[]]> =>
6
6
  export const renderObjectKey = (key: string): string =>
7
7
  isValidIdentifier(key) ? key : JSON.stringify(key);
8
8
 
9
- export const handlerTypePrefix = (h: any): string =>
10
- h.fileName
11
- .replace(/[^a-zA-Z0-9]/g, "")
12
- .replace(/^./, (c: string) => c.toUpperCase()) +
13
- h.name
14
- .replace(/[^a-zA-Z0-9]/g, "")
15
- .replace(/^./, (c: string) => c.toUpperCase());
9
+ export const handlerTypePrefix = (h: any): string => {
10
+ const base = (h.routePath ?? h.fileName) as string;
11
+ return (
12
+ base
13
+ .replace(/[^a-zA-Z0-9]/g, "")
14
+ .replace(/^./, (c: string) => c.toUpperCase()) +
15
+ (h.name as string)
16
+ .replace(/[^a-zA-Z0-9]/g, "")
17
+ .replace(/^./, (c: string) => c.toUpperCase())
18
+ );
19
+ };
16
20
 
17
21
  export const normalizeTableName = (fileName: string): string =>
18
22
  fileName.endsWith("s") ? fileName : `${fileName}s`;
@@ -14,11 +14,11 @@ export const buildCronHandlerEntries = (params: {
14
14
  return params.handlers
15
15
  .map((handler) => {
16
16
  const local = params.localNameFor(handler);
17
- const task = `${handler.fileName}/${handler.name}`;
17
+ const task = `${handler.routePath}/${handler.name}`;
18
18
  const fallbackTriggers = stringifyTriggers(handler.cronTriggers);
19
19
  return (
20
20
  `\t${JSON.stringify(task)}: {\n` +
21
- `\t\tfile: ${JSON.stringify(handler.fileName)},\n` +
21
+ `\t\tfile: ${JSON.stringify(handler.routePath)},\n` +
22
22
  `\t\tname: ${JSON.stringify(handler.name)},\n` +
23
23
  `\t\tcronTrigger: ${local}.cronTrigger ?? ${fallbackTriggers},\n` +
24
24
  `\t\trun: ${local}.handler,\n` +