@automateinc/fleet-types 1.0.103 → 1.0.105

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.
@@ -2,13 +2,43 @@
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { createRequire } from "node:module";
7
- import { tmpdir } from "node:os";
8
7
  import path from "node:path";
9
8
  import { loadEnvFile } from "node:process";
10
9
  import { fileURLToPath } from "node:url";
11
10
 
11
+ const SERIALIZED_OUTPUT_TYPES = `type FleetIsAny<T> = 0 extends 1 & T ? true : false;
12
+ type FleetSimplify<T> = { [TKey in keyof T]: T[TKey] } & {};
13
+ type FleetSerializeObject<T extends object> = {
14
+ \t[TKey in keyof T as FleetIsAny<T[TKey]> extends true
15
+ \t\t? TKey
16
+ \t\t: [T[TKey]] extends [undefined]
17
+ \t\t\t? never
18
+ \t\t\t: T[TKey] extends (...args: never[]) => unknown
19
+ \t\t\t\t? never
20
+ \t\t\t\t: TKey]: FleetSerializeOutput<T[TKey]>;
21
+ };
22
+ type FleetSerializeOutput<T> = FleetIsAny<T> extends true
23
+ \t? T
24
+ \t: T extends (...args: never[]) => unknown
25
+ \t\t? never
26
+ \t\t: T extends Date
27
+ \t\t\t? string
28
+ \t\t\t: T extends readonly (infer TItem)[]
29
+ \t\t\t\t? FleetSerializeOutput<TItem>[]
30
+ \t\t\t\t: T extends { toJSON(): infer TJSON }
31
+ \t\t\t\t\t? TJSON extends object
32
+ \t\t\t\t\t\t? FleetSimplify<
33
+ \t\t\t\t\t\t\t\tOmit<FleetSerializeObject<Omit<T, "toJSON">>, keyof TJSON> &
34
+ \t\t\t\t\t\t\t\t\tFleetSerializeOutput<TJSON>
35
+ \t\t\t\t\t\t\t>
36
+ \t\t\t\t\t\t: FleetSerializeOutput<TJSON>
37
+ \t\t\t\t\t: T extends object
38
+ \t\t\t\t\t\t? FleetSerializeObject<T>
39
+ \t\t\t\t\t\t: T;
40
+ type FleetProcedureOutput<T> = { toJSON(): FleetSerializeOutput<T> };`;
41
+
12
42
  const command = process.argv[2];
13
43
 
14
44
  if (!command || command === "--help" || command === "-h") {
@@ -51,184 +81,224 @@ async function generateTypes() {
51
81
  const apiRoot = path.resolve(callerRoot, fleetApiPath);
52
82
  const outputPath = path.resolve(callerRoot, fleetApiTypesPath);
53
83
  const routerSourcePath = path.join(apiRoot, "src/routers/trpc/index.ts");
54
- const prismaTypesPath = path.join(apiRoot, "prisma/types.d.ts");
84
+ const apiTsconfigPath = path.join(apiRoot, "tsconfig.json");
55
85
  const executableExtension = process.platform === "win32" ? ".cmd" : "";
56
- const apiTscPath = path.join(apiRoot, `node_modules/.bin/tsc${executableExtension}`);
57
86
  const callerBiomePath = path.join(callerRoot, `node_modules/.bin/biome${executableExtension}`);
58
87
  const packageBiomePath = path.join(packageRoot, `node_modules/.bin/biome${executableExtension}`);
59
88
  const biomePath = existsSync(callerBiomePath) ? callerBiomePath : packageBiomePath;
60
- const temporaryDirectory = await mkdtemp(path.join(tmpdir(), "fleet-trpc-types-"));
61
- const declarationOutputDirectory = path.join(temporaryDirectory, "declarations");
62
- const declarationTsconfigPath = path.join(temporaryDirectory, "tsconfig.json");
63
- const prismaJsonShimPath = path.join(temporaryDirectory, "prisma-json.d.ts");
64
- const serviceProviderShimPath = path.join(temporaryDirectory, "service-provider.d.ts");
65
89
  const requireFromPackage = createRequire(path.join(packageRoot, "package.json"));
66
90
  const ts = requireFromPackage("typescript");
91
+ const configResult = ts.readConfigFile(apiTsconfigPath, ts.sys.readFile);
67
92
 
68
- try {
69
- const prismaTypesContent = await readFile(prismaTypesPath, "utf8");
70
- const prismaTypesSource = ts.createSourceFile(prismaTypesPath, prismaTypesContent, ts.ScriptTarget.Latest, true);
71
- let coordsType;
72
- const findCoordsType = node => {
73
- if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "PrismaJson") {
74
- const body = node.body;
75
- if (body && ts.isModuleBlock(body)) {
76
- coordsType = body.statements.find(
77
- statement => ts.isTypeAliasDeclaration(statement) && statement.name.text === "Coords",
78
- );
79
- }
80
- }
81
- ts.forEachChild(node, findCoordsType);
82
- };
83
- findCoordsType(prismaTypesSource);
84
- if (!coordsType) {
85
- throw new Error(`PrismaJson.Coords was not found in ${prismaTypesPath}.`);
93
+ if (configResult.error) {
94
+ throw new Error(formatTypeScriptDiagnostics(ts, [configResult.error], apiRoot));
95
+ }
96
+
97
+ const parsedConfig = ts.parseJsonConfigFileContent(
98
+ configResult.config,
99
+ ts.sys,
100
+ apiRoot,
101
+ {
102
+ declaration: true,
103
+ declarationMap: false,
104
+ emitDeclarationOnly: true,
105
+ incremental: false,
106
+ noEmit: false,
107
+ noEmitOnError: false,
108
+ },
109
+ apiTsconfigPath,
110
+ );
111
+
112
+ if (parsedConfig.errors.length > 0) {
113
+ throw new Error(formatTypeScriptDiagnostics(ts, parsedConfig.errors, apiRoot));
114
+ }
115
+
116
+ const program = ts.createProgram({
117
+ options: parsedConfig.options,
118
+ rootNames: parsedConfig.fileNames,
119
+ });
120
+ const routerSource = program.getSourceFile(routerSourcePath);
121
+
122
+ if (!routerSource) {
123
+ throw new Error(`Unable to load AppRouter source at ${routerSourcePath}.`);
124
+ }
125
+
126
+ const sourceDiagnostics = [
127
+ ...program.getSyntacticDiagnostics(routerSource),
128
+ ...program.getSemanticDiagnostics(routerSource),
129
+ ].filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error);
130
+
131
+ if (sourceDiagnostics.length > 0) {
132
+ throw new Error(formatTypeScriptDiagnostics(ts, sourceDiagnostics, apiRoot));
133
+ }
134
+
135
+ let emittedDeclaration;
136
+ const emitResult = program.emit(
137
+ routerSource,
138
+ (fileName, content) => {
139
+ if (fileName.endsWith(".d.ts")) emittedDeclaration = content;
140
+ },
141
+ undefined,
142
+ true,
143
+ );
144
+ const emitDiagnostics = emitResult.diagnostics.filter(
145
+ diagnostic => diagnostic.category === ts.DiagnosticCategory.Error,
146
+ );
147
+
148
+ if (emitDiagnostics.length > 0) {
149
+ throw new Error(formatTypeScriptDiagnostics(ts, emitDiagnostics, apiRoot));
150
+ }
151
+ if (!emittedDeclaration) {
152
+ throw new Error(`TypeScript did not emit a declaration for ${routerSourcePath}.`);
153
+ }
154
+
155
+ const emittedPath = routerSourcePath.replace(/\.ts$/, ".d.ts");
156
+ const sourceFile = ts.createSourceFile(emittedPath, emittedDeclaration, ts.ScriptTarget.Latest, true);
157
+ const sanitizedSourceFile = sanitizeBackendTypes(ts, sourceFile);
158
+ const transformedSourceFile = transformProcedureOutputs(ts, sanitizedSourceFile);
159
+ const statements = transformedSourceFile.statements.filter(
160
+ statement =>
161
+ ts.isImportDeclaration(statement) ||
162
+ ts.isImportEqualsDeclaration(statement) ||
163
+ (ts.isVariableStatement(statement) &&
164
+ statement.declarationList.declarations.some(
165
+ declaration => ts.isIdentifier(declaration.name) && declaration.name.text === "appRouter",
166
+ )) ||
167
+ (ts.isTypeAliasDeclaration(statement) && statement.name.text === "AppRouter"),
168
+ );
169
+
170
+ if (!statements.some(statement => ts.isTypeAliasDeclaration(statement))) {
171
+ throw new Error("AppRouter was not found in the emitted declaration.");
172
+ }
173
+
174
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
175
+ const appRouterDeclaration = statements
176
+ .map(statement => printer.printNode(ts.EmitHint.Unspecified, statement, transformedSourceFile))
177
+ .join("\n")
178
+ .replace(/^(?: {4})+/gm, indentation => "\t".repeat(indentation.length / 4));
179
+
180
+ await mkdir(path.dirname(outputPath), { recursive: true });
181
+ await writeFile(
182
+ outputPath,
183
+ `// Generated by \`npx @automateinc/fleet-types generate\`. Do not edit manually.\n${SERIALIZED_OUTPUT_TYPES}\n${appRouterDeclaration}\n`,
184
+ );
185
+
186
+ if (existsSync(biomePath)) {
187
+ const biomeResult = spawnSync(biomePath, ["format", "--write", outputPath], {
188
+ cwd: callerRoot,
189
+ stdio: "inherit",
190
+ });
191
+ if (biomeResult.error) {
192
+ throw biomeResult.error;
86
193
  }
87
- const coordsTypeDeclaration = ts
88
- .createPrinter({ newLine: ts.NewLineKind.LineFeed })
89
- .printNode(ts.EmitHint.Unspecified, coordsType, prismaTypesSource);
90
-
91
- await writeFile(
92
- prismaJsonShimPath,
93
- `export {};\ndeclare global { namespace PrismaJson { ${coordsTypeDeclaration} } }\n`,
94
- );
95
- await writeFile(
96
- serviceProviderShimPath,
97
- `interface Permission {
98
- name: string;
99
- }
194
+ if (biomeResult.status !== 0) {
195
+ throw new Error(`Biome formatting failed with exit code ${biomeResult.status}.`);
196
+ }
197
+ }
100
198
 
101
- interface AuthenticatedUser {
102
- id: number;
103
- dailyHours: number | null;
104
- permissionGroups: { permissions: Permission[] }[];
105
- permissions: Permission[];
106
- team: { folderKey: string | null; id: number } | null;
199
+ console.log(`Generated ${path.relative(callerRoot, outputPath)} from ${routerSourcePath}`);
107
200
  }
108
201
 
109
- interface DecodedToken {
110
- permissions: string[];
111
- type: "USER" | "EMPLOYEE";
112
- user: { id: string; role: string };
113
- }
202
+ function transformProcedureOutputs(ts, sourceFile) {
203
+ const procedureTypes = new Set(["TRPCMutationProcedure", "TRPCQueryProcedure", "TRPCSubscriptionProcedure"]);
204
+ const transformation = ts.transform(sourceFile, [
205
+ context => {
206
+ const visit = node => {
207
+ if (
208
+ ts.isImportTypeNode(node) &&
209
+ ts.isIdentifier(node.qualifier) &&
210
+ procedureTypes.has(node.qualifier.text) &&
211
+ node.typeArguments?.length
212
+ ) {
213
+ const [procedureDefinition, ...remainingTypeArguments] = node.typeArguments;
114
214
 
115
- interface CacheOptions {
116
- tags?: string[];
117
- ttl?: number;
118
- }
215
+ if (ts.isTypeLiteralNode(procedureDefinition)) {
216
+ const members = procedureDefinition.members.map(member => {
217
+ if (
218
+ ts.isPropertySignature(member) &&
219
+ member.type &&
220
+ ts.isIdentifier(member.name) &&
221
+ member.name.text === "output"
222
+ ) {
223
+ return ts.factory.updatePropertySignature(
224
+ member,
225
+ member.modifiers,
226
+ member.name,
227
+ member.questionToken,
228
+ ts.factory.createTypeReferenceNode("FleetProcedureOutput", [member.type]),
229
+ );
230
+ }
119
231
 
120
- export declare class ServiceProvider {
121
- static getAuthenticationService(): {
122
- decodeToken(token: string): DecodedToken;
123
- login(email: string, password: string, tokenExpiry?: string): Promise<string>;
124
- };
125
- static getCachingService(): {
126
- fnc<T>(key: string, fn: Promise<T> | (() => Promise<T>), options?: CacheOptions): Promise<T>;
127
- };
128
- static getDatabaseService(): {
129
- getPrisma(): {
130
- user: {
131
- findFirst(args: unknown): Promise<AuthenticatedUser | null>;
132
- };
133
- userAttendance: {
134
- findFirst(args: unknown): Promise<{ id: number } | null>;
232
+ return member;
233
+ });
234
+ const updatedDefinition = ts.factory.updateTypeLiteralNode(procedureDefinition, members);
235
+
236
+ return ts.factory.updateImportTypeNode(
237
+ node,
238
+ node.argument,
239
+ node.attributes,
240
+ node.qualifier,
241
+ [updatedDefinition, ...remainingTypeArguments],
242
+ node.isTypeOf,
243
+ );
244
+ }
245
+ }
246
+
247
+ return ts.visitEachChild(node, visit, context);
135
248
  };
136
- };
137
- };
138
- static getEncryptionService(): {
139
- decodeId(encodedId: string, prefix: string): number;
140
- };
249
+
250
+ return rootNode => ts.visitNode(rootNode, visit);
251
+ },
252
+ ]);
253
+ const transformedSourceFile = transformation.transformed[0];
254
+
255
+ transformation.dispose();
256
+
257
+ return transformedSourceFile;
141
258
  }
142
- `,
143
- );
144
- await writeFile(
145
- declarationTsconfigPath,
146
- `${JSON.stringify(
147
- {
148
- compilerOptions: {
149
- declaration: true,
150
- declarationMap: false,
151
- emitDeclarationOnly: true,
152
- incremental: true,
153
- noEmit: false,
154
- outDir: declarationOutputDirectory,
155
- paths: {
156
- "@/*": [path.join(apiRoot, "src/*")],
157
- "@/providers": [serviceProviderShimPath],
158
- "@/providers/service.provider": [serviceProviderShimPath],
159
- },
160
- tsBuildInfoFile: path.join(temporaryDirectory, "tsconfig.tsbuildinfo"),
161
- typeRoots: [path.join(apiRoot, "node_modules/@types")],
162
- },
163
- extends: path.join(apiRoot, "tsconfig.json"),
164
- files: [routerSourcePath, prismaJsonShimPath],
165
- },
166
- null,
167
- 2,
168
- )}\n`,
169
- );
170
-
171
- const typeScriptResult = spawnSync(apiTscPath, ["--project", declarationTsconfigPath, "--pretty"], {
172
- cwd: apiRoot,
173
- stdio: "inherit",
174
- });
175
259
 
176
- if (typeScriptResult.error) {
177
- throw typeScriptResult.error;
178
- }
179
- if (typeScriptResult.status !== 0) {
180
- throw new Error(`TypeScript declaration generation failed with exit code ${typeScriptResult.status}.`);
181
- }
260
+ function sanitizeBackendTypes(ts, sourceFile) {
261
+ const transformation = ts.transform(sourceFile, [
262
+ context => {
263
+ const visit = node => {
264
+ if (ts.isTypeReferenceNode(node)) {
265
+ let typeName = node.typeName;
182
266
 
183
- const emittedPath = path
184
- .join(declarationOutputDirectory, path.relative(path.join(apiRoot, "src"), routerSourcePath))
185
- .replace(/\.ts$/, ".d.ts");
186
- const emittedDeclaration = await readFile(emittedPath, "utf8");
187
- const sourceFile = ts.createSourceFile(emittedPath, emittedDeclaration, ts.ScriptTarget.Latest, true);
188
- const statements = sourceFile.statements.filter(
189
- statement =>
190
- ts.isImportDeclaration(statement) ||
191
- ts.isImportEqualsDeclaration(statement) ||
192
- (ts.isVariableStatement(statement) &&
193
- statement.declarationList.declarations.some(
194
- declaration => ts.isIdentifier(declaration.name) && declaration.name.text === "appRouter",
195
- )) ||
196
- (ts.isTypeAliasDeclaration(statement) && statement.name.text === "AppRouter"),
197
- );
198
-
199
- if (!statements.some(statement => ts.isTypeAliasDeclaration(statement))) {
200
- throw new Error("AppRouter was not found in the emitted declaration.");
201
- }
267
+ while (ts.isQualifiedName(typeName)) typeName = typeName.left;
202
268
 
203
- const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
204
- const appRouterDeclaration = statements
205
- .map(statement => printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile))
206
- .join("\n")
207
- .replace(/^(?: {4})+/gm, indentation => "\t".repeat(indentation.length / 4));
208
-
209
- await mkdir(path.dirname(outputPath), { recursive: true });
210
- await writeFile(
211
- outputPath,
212
- `// Generated by \`npx @automateinc/fleet-types generate\`. Do not edit manually.\n${appRouterDeclaration}\n`,
213
- );
214
-
215
- if (existsSync(biomePath)) {
216
- const biomeResult = spawnSync(biomePath, ["format", "--write", outputPath], {
217
- cwd: callerRoot,
218
- stdio: "inherit",
219
- });
220
- if (biomeResult.error) {
221
- throw biomeResult.error;
222
- }
223
- if (biomeResult.status !== 0) {
224
- throw new Error(`Biome formatting failed with exit code ${biomeResult.status}.`);
225
- }
226
- }
269
+ if (ts.isIdentifier(typeName) && typeName.text === "PrismaJson") {
270
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
271
+ }
272
+ }
227
273
 
228
- console.log(`Generated ${path.relative(callerRoot, outputPath)} from ${routerSourcePath}`);
229
- } finally {
230
- await rm(temporaryDirectory, { force: true, recursive: true });
231
- }
274
+ if (
275
+ ts.isImportTypeNode(node) &&
276
+ ts.isLiteralTypeNode(node.argument) &&
277
+ ts.isStringLiteral(node.argument.literal) &&
278
+ node.argument.literal.text.startsWith("@/")
279
+ ) {
280
+ return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
281
+ }
282
+
283
+ return ts.visitEachChild(node, visit, context);
284
+ };
285
+
286
+ return rootNode => ts.visitNode(rootNode, visit);
287
+ },
288
+ ]);
289
+ const transformedSourceFile = transformation.transformed[0];
290
+
291
+ transformation.dispose();
292
+
293
+ return transformedSourceFile;
294
+ }
295
+
296
+ function formatTypeScriptDiagnostics(ts, diagnostics, currentDirectory) {
297
+ return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
298
+ getCanonicalFileName: fileName => fileName,
299
+ getCurrentDirectory: () => currentDirectory,
300
+ getNewLine: () => "\n",
301
+ });
232
302
  }
233
303
 
234
304
  function printUsage() {
package/package.json CHANGED
@@ -57,5 +57,5 @@
57
57
  "test": "echo \"Error: no test specified\" && exit 1"
58
58
  },
59
59
  "types": "dist/types/index.d.ts",
60
- "version": "1.0.103"
60
+ "version": "1.0.105"
61
61
  }
@@ -1,54 +0,0 @@
1
- // Generated by `npx @automateinc/fleet-types generate`. Do not edit manually.
2
- export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<
3
- {
4
- ctx: {
5
- client: "FLEET_MOBILE" | "FLEET_MOBILE_USER" | "FLEET_WEB" | undefined;
6
- regionId: string | undefined;
7
- request: import("express").Request<
8
- import("express-serve-static-core").ParamsDictionary,
9
- any,
10
- any,
11
- import("qs").ParsedQs,
12
- Record<string, any>
13
- >;
14
- token: string | undefined;
15
- };
16
- meta: object;
17
- errorShape: import("@trpc/server").TRPCDefaultErrorShape;
18
- transformer: false;
19
- },
20
- import("@trpc/server").TRPCDecorateCreateRouterOptions<{
21
- auth: import("@trpc/server").TRPCBuiltRouter<
22
- {
23
- ctx: {
24
- client: "FLEET_MOBILE" | "FLEET_MOBILE_USER" | "FLEET_WEB" | undefined;
25
- regionId: string | undefined;
26
- request: import("express").Request<
27
- import("express-serve-static-core").ParamsDictionary,
28
- any,
29
- any,
30
- import("qs").ParsedQs,
31
- Record<string, any>
32
- >;
33
- token: string | undefined;
34
- };
35
- meta: object;
36
- errorShape: import("@trpc/server").TRPCDefaultErrorShape;
37
- transformer: false;
38
- },
39
- import("@trpc/server").TRPCDecorateCreateRouterOptions<{
40
- login: import("@trpc/server").TRPCMutationProcedure<{
41
- input: {
42
- email: string;
43
- password: string;
44
- };
45
- output: {
46
- token: string;
47
- };
48
- meta: object;
49
- }>;
50
- }>
51
- >;
52
- }>
53
- >;
54
- export type AppRouter = typeof appRouter;