@itzworking/devkit 0.0.198-canary.20250707-e7f7621

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.
Files changed (49) hide show
  1. package/README.md +15 -0
  2. package/dist-cjs/generate-api-clients/api-type-spec-utils.js +160 -0
  3. package/dist-cjs/generate-api-clients/copy-api-client.js +17 -0
  4. package/dist-cjs/generate-api-clients/generate-api-client-class.js +161 -0
  5. package/dist-cjs/generate-api-clients/generate-api-clients.js +70 -0
  6. package/dist-cjs/generate-api-clients/generate-hook.js +71 -0
  7. package/dist-cjs/generate-api-clients/handlebars-helpers.js +14 -0
  8. package/dist-cjs/generate-api-clients/index.js +4 -0
  9. package/dist-cjs/generate-api-clients/parse-discovered-apis.js +125 -0
  10. package/dist-cjs/generate-api-clients/sync-api-clients.js +84 -0
  11. package/dist-cjs/generate-api-clients/template-loader.js +24 -0
  12. package/dist-cjs/generate-api-clients/templates/api-client.hbs +25 -0
  13. package/dist-cjs/generate-api-clients/templates/api-client.ts.template +131 -0
  14. package/dist-cjs/generate-api-clients/templates/hook-mutation.hbs +39 -0
  15. package/dist-cjs/generate-api-clients/templates/hook-query.hbs +28 -0
  16. package/dist-cjs/generate-api-clients/types.js +2 -0
  17. package/dist-cjs/generate-api-clients/utils.js +89 -0
  18. package/dist-cjs/index.js +4 -0
  19. package/dist-es/generate-api-clients/api-type-spec-utils.js +155 -0
  20. package/dist-es/generate-api-clients/copy-api-client.js +12 -0
  21. package/dist-es/generate-api-clients/generate-api-client-class.js +156 -0
  22. package/dist-es/generate-api-clients/generate-api-clients.js +65 -0
  23. package/dist-es/generate-api-clients/generate-hook.js +66 -0
  24. package/dist-es/generate-api-clients/handlebars-helpers.js +9 -0
  25. package/dist-es/generate-api-clients/index.js +1 -0
  26. package/dist-es/generate-api-clients/parse-discovered-apis.js +120 -0
  27. package/dist-es/generate-api-clients/sync-api-clients.js +79 -0
  28. package/dist-es/generate-api-clients/template-loader.js +20 -0
  29. package/dist-es/generate-api-clients/templates/api-client.hbs +25 -0
  30. package/dist-es/generate-api-clients/templates/api-client.ts.template +131 -0
  31. package/dist-es/generate-api-clients/templates/hook-mutation.hbs +39 -0
  32. package/dist-es/generate-api-clients/templates/hook-query.hbs +28 -0
  33. package/dist-es/generate-api-clients/types.js +1 -0
  34. package/dist-es/generate-api-clients/utils.js +79 -0
  35. package/dist-es/index.js +1 -0
  36. package/dist-types/generate-api-clients/api-type-spec-utils.d.ts +4 -0
  37. package/dist-types/generate-api-clients/copy-api-client.d.ts +5 -0
  38. package/dist-types/generate-api-clients/generate-api-client-class.d.ts +11 -0
  39. package/dist-types/generate-api-clients/generate-api-clients.d.ts +2 -0
  40. package/dist-types/generate-api-clients/generate-hook.d.ts +11 -0
  41. package/dist-types/generate-api-clients/handlebars-helpers.d.ts +1 -0
  42. package/dist-types/generate-api-clients/index.d.ts +1 -0
  43. package/dist-types/generate-api-clients/parse-discovered-apis.d.ts +8 -0
  44. package/dist-types/generate-api-clients/sync-api-clients.d.ts +1 -0
  45. package/dist-types/generate-api-clients/template-loader.d.ts +5 -0
  46. package/dist-types/generate-api-clients/types.d.ts +113 -0
  47. package/dist-types/generate-api-clients/utils.d.ts +21 -0
  48. package/dist-types/index.d.ts +1 -0
  49. package/package.json +44 -0
@@ -0,0 +1,156 @@
1
+ import { prepareImports, toClassName, toPropertyName } from "./utils";
2
+ import Handlebars from "handlebars";
3
+ import { loadTemplate } from "./template-loader";
4
+ const pathWithQuotes = (apiPath) => {
5
+ return `${apiPath.includes("{") ? "`" : '"'}${apiPath}${apiPath.includes("{") ? "`" : '"'}`.replace(/{/g, "${");
6
+ };
7
+ Handlebars.registerHelper("pathWithQuotes", pathWithQuotes);
8
+ const prepareMethods = (apis) => {
9
+ const methods = [];
10
+ for (const api of apis) {
11
+ const pathParams = (api.fullPath.match(/\{[^}]+}/g) || []).map((p) => p.replace(/[{}]/g, ""));
12
+ let params = "";
13
+ if (api.method === "GET") {
14
+ if (pathParams.length > 0) {
15
+ params += pathParams.map((p) => `${p}: string`).join(", ");
16
+ }
17
+ if (api.inputType) {
18
+ if (params)
19
+ params += ", ";
20
+ params += `body: ${api.inputType}`;
21
+ }
22
+ }
23
+ else {
24
+ if (pathParams.length > 0 || api.inputType) {
25
+ params += "{ ";
26
+ if (pathParams.length > 0) {
27
+ params += pathParams.join(", ") + (api.inputType ? ", " : "");
28
+ }
29
+ if (api.inputType) {
30
+ params += `body`;
31
+ }
32
+ params += " }: { ";
33
+ if (pathParams.length > 0) {
34
+ params +=
35
+ pathParams.map((p) => `${p}: string`).join(", ") +
36
+ (api.inputType ? ", " : "");
37
+ }
38
+ if (api.inputType) {
39
+ params += `body: ${api.inputType.typeName}`;
40
+ }
41
+ params += " }";
42
+ }
43
+ }
44
+ methods.push({
45
+ name: toPropertyName(api.actionName),
46
+ params,
47
+ returnType: api.outputType ? api.outputType.typeName : "void",
48
+ method: api.method.toLowerCase(),
49
+ fullPath: api.fullPath,
50
+ inputType: api.inputType,
51
+ outputType: api.outputType,
52
+ });
53
+ }
54
+ return methods;
55
+ };
56
+ const prepareTypeImports = (apis) => {
57
+ const imports = [];
58
+ for (const api of apis) {
59
+ const typeSpecs = [api.inputType, api.outputType];
60
+ for (const typeSpec of typeSpecs) {
61
+ if (!typeSpec || !typeSpec.importPath)
62
+ continue;
63
+ for (const allowedImport of typeSpec.typeDefinition.allowedImports) {
64
+ let currentImport = imports.find((i) => i.importPath === allowedImport.importPath);
65
+ if (!currentImport) {
66
+ currentImport = {
67
+ importPath: allowedImport.importPath,
68
+ importNames: new Set(),
69
+ };
70
+ imports.push(currentImport);
71
+ }
72
+ if (allowedImport.namespaceImport) {
73
+ const namespaceNames = allowedImport.importNames;
74
+ const conflictingNamespaceNames = [];
75
+ if (namespaceNames.length > 1) {
76
+ conflictingNamespaceNames.push(...namespaceNames);
77
+ }
78
+ if (currentImport.namespaceImport &&
79
+ currentImport.namespaceImport !== namespaceNames[0]) {
80
+ conflictingNamespaceNames.push(currentImport.namespaceImport);
81
+ conflictingNamespaceNames.push(...namespaceNames);
82
+ }
83
+ if (conflictingNamespaceNames.length > 0) {
84
+ throw new Error(`Conflicting namespace imports found for module "${allowedImport.importPath}": ` +
85
+ `[${conflictingNamespaceNames.join(", ")}]. ` +
86
+ `All namespace imports from the same module must use the same name. ` +
87
+ `Please consolidate to use a single namespace import: import * as [name] from "${allowedImport.importPath}"`);
88
+ }
89
+ const namespaceName = namespaceNames[0];
90
+ const conflictingImport = imports.find((imp) => imp.importPath !== allowedImport.importPath &&
91
+ imp.namespaceImport === namespaceName);
92
+ if (conflictingImport) {
93
+ throw new Error(`Namespace name "${namespaceName}" is used by imports from: ` +
94
+ `"${conflictingImport.importPath}" and "${allowedImport.importPath}". ` +
95
+ `Cannot use the same namespace name for different modules. ` +
96
+ `Please use a different namespace name for one of the imports.`);
97
+ }
98
+ currentImport.namespaceImport = namespaceName;
99
+ }
100
+ else {
101
+ for (const importName of allowedImport.importNames) {
102
+ currentImport.importNames.add(importName);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return imports.map((i) => ({
109
+ namespaceImport: i.namespaceImport,
110
+ importPath: i.importPath,
111
+ importNames: Array.from(i.importNames).sort(),
112
+ }));
113
+ };
114
+ export const generateApiClientClass = ({ basePath, apis, }, options) => {
115
+ const className = toClassName(basePath) + "Api";
116
+ const imports = prepareImports(apis, options);
117
+ const methods = prepareMethods(apis);
118
+ const templateSource = loadTemplate("api-client.hbs");
119
+ const template = Handlebars.compile(templateSource);
120
+ let apiTypesFile = "";
121
+ if (options.typeResolutionMode === "inline-types") {
122
+ const typeImports = prepareTypeImports(apis);
123
+ for (const typeImport of typeImports) {
124
+ if (typeImport.namespaceImport) {
125
+ apiTypesFile += `import * as ${typeImport.namespaceImport} from "${typeImport.importPath}";\n`;
126
+ }
127
+ if (typeImport.importNames.length > 0) {
128
+ apiTypesFile += `import { ${typeImport.importNames.join(", ")} } from "${typeImport.importPath}";\n`;
129
+ }
130
+ }
131
+ const inlinedFilePaths = new Set();
132
+ for (const api of apis) {
133
+ const typeSpecs = [api.inputType, api.outputType];
134
+ for (const typeSpec of typeSpecs) {
135
+ if (!typeSpec || !typeSpec.importPath)
136
+ continue;
137
+ for (const inlinedFile of typeSpec.typeDefinition.inlinedFiles) {
138
+ if (inlinedFilePaths.has(inlinedFile.filePath)) {
139
+ continue;
140
+ }
141
+ inlinedFilePaths.add(inlinedFile.filePath);
142
+ apiTypesFile += "\n\n";
143
+ apiTypesFile += inlinedFile.text;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return {
149
+ apiClientClassFile: template({
150
+ className,
151
+ imports,
152
+ methods,
153
+ }),
154
+ apiTypesFile,
155
+ };
156
+ };
@@ -0,0 +1,65 @@
1
+ import { getITzWorkingResources, toFileName, writeFile } from "./utils";
2
+ import { parseDiscoveredApis } from "./parse-discovered-apis";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { generateApiClientClass } from "./generate-api-client-class";
6
+ import { generateHook } from "./generate-hook";
7
+ import { copyApiClient } from "./copy-api-client";
8
+ export const generateApiClients = async (options = {}) => {
9
+ const { tsConfigPath = "tsconfig.json", targetTypeSystem = "preserve-backend", typeResolutionMode = "source-imports", verbose = false, allowOverwrite = false, cleanOutputDir = false, outputDirectory = "out/apis", resourcesFilePath = "cdk.itzworking.json", } = options;
10
+ if (targetTypeSystem !== "preserve-backend") {
11
+ console.error(`Unsupported targetTypeSystem: ${targetTypeSystem}. Only 'preserve-backend' is supported.`);
12
+ process.exit(1);
13
+ }
14
+ const resources = getITzWorkingResources({ resourcesFilePath, verbose });
15
+ const apisByBasePath = parseDiscoveredApis(resources, {
16
+ tsConfigPath,
17
+ typeResolutionMode,
18
+ verbose,
19
+ });
20
+ if (cleanOutputDir) {
21
+ fs.rmSync(outputDirectory, { recursive: true, force: true });
22
+ }
23
+ copyApiClient({ outputDirectory, allowOverwrite, verbose });
24
+ for (const [basePath, apis] of Object.entries(apisByBasePath)) {
25
+ writeFile({
26
+ filePath: path.join(outputDirectory, toFileName(basePath), "index.ts"),
27
+ content: `export * from "./${toFileName(basePath)}-api";`,
28
+ allowOverwrite,
29
+ verbose,
30
+ });
31
+ const { apiClientClassFile, apiTypesFile } = generateApiClientClass({ basePath, apis }, {
32
+ typeResolutionMode,
33
+ verbose,
34
+ });
35
+ writeFile({
36
+ filePath: path.join(outputDirectory, toFileName(basePath), `${toFileName(basePath)}-api.ts`),
37
+ content: apiClientClassFile.replace(/^[\s\S]*?(?=import)/m, ""),
38
+ allowOverwrite,
39
+ verbose,
40
+ });
41
+ if (apiTypesFile) {
42
+ writeFile({
43
+ filePath: path.join(outputDirectory, toFileName(basePath), `types.ts`),
44
+ content: apiTypesFile,
45
+ allowOverwrite,
46
+ verbose,
47
+ });
48
+ fs.appendFileSync(path.join(outputDirectory, toFileName(basePath), "index.ts"), `\nexport * from "./types";`);
49
+ }
50
+ }
51
+ for (const [basePath, apis] of Object.entries(apisByBasePath)) {
52
+ for (const api of apis) {
53
+ const { hookName, hookFile } = generateHook({ basePath, api }, {
54
+ typeResolutionMode,
55
+ verbose,
56
+ });
57
+ writeFile({
58
+ filePath: path.join(outputDirectory, toFileName(basePath), "hooks", `${toFileName(hookName)}.ts`),
59
+ content: hookFile.replace(/^[\s\S]*?(?=import)/m, ""),
60
+ allowOverwrite,
61
+ verbose,
62
+ });
63
+ }
64
+ }
65
+ };
@@ -0,0 +1,66 @@
1
+ import { prepareImports, toClassName, toFileName, toPropertyName, } from "./utils";
2
+ import Handlebars from "handlebars";
3
+ import { loadTemplate } from "./template-loader";
4
+ export const generateHook = ({ basePath, api, }, options) => {
5
+ const apiClassName = toClassName(basePath) + "Api";
6
+ const actionName = toPropertyName(api.actionName);
7
+ const actionClassName = toClassName(api.actionName);
8
+ const hookName = `use${actionClassName}`;
9
+ const pathParams = (api.fullPath.match(/\{[^}]+}/g) || []).map((p) => p.replace(/[{}]/g, ""));
10
+ let mutationParams = "";
11
+ if (api.method !== "GET") {
12
+ if (pathParams.length > 0 || api.inputType) {
13
+ mutationParams += "variables: {\n";
14
+ if (pathParams.length > 0) {
15
+ mutationParams +=
16
+ pathParams.map((p) => ` ${p}: string`).join(",\n") +
17
+ (api.inputType ? ",\n" : "");
18
+ }
19
+ if (api.inputType) {
20
+ mutationParams += ` body: ${api.inputType.typeName}`;
21
+ }
22
+ mutationParams += "\n }";
23
+ }
24
+ }
25
+ let queryKeyParams = "";
26
+ let queryKeyArray = "";
27
+ let queryKeyInput = "";
28
+ if (api.method === "GET") {
29
+ if (pathParams.length > 0) {
30
+ queryKeyParams = pathParams.map((p) => `${p}: string`).join(", ");
31
+ queryKeyInput = pathParams.map((p) => `${p}`).join(", ");
32
+ }
33
+ queryKeyArray = api.fullPath
34
+ .split("/")
35
+ .filter(Boolean)
36
+ .map((p) => {
37
+ if (p.startsWith("{") && p.endsWith("}")) {
38
+ return " `$" + `${p}` + "`";
39
+ }
40
+ return ` "${p}"`;
41
+ })
42
+ .join(",\n");
43
+ }
44
+ const imports = [
45
+ { path: `../${toFileName(apiClassName)}`, names: apiClassName },
46
+ ...prepareImports(api, options).map((i) => i.path === "./types" ? { path: "../types", names: i.names } : i),
47
+ ];
48
+ const data = {
49
+ imports,
50
+ hookName,
51
+ mutationParams,
52
+ apiClassName,
53
+ actionName,
54
+ actionClassName,
55
+ relatedDataName: toPropertyName(api.relatedDataName),
56
+ queryKeyParams,
57
+ queryKeyArray,
58
+ queryKeyInput,
59
+ outputType: api.outputType,
60
+ };
61
+ const templateFile = api.method === "GET" ? "hook-query.hbs" : "hook-mutation.hbs";
62
+ const templateSource = loadTemplate(templateFile);
63
+ const template = Handlebars.compile(templateSource);
64
+ const hookFile = template(data);
65
+ return { hookName, hookFile };
66
+ };
@@ -0,0 +1,9 @@
1
+ import Handlebars from "handlebars";
2
+ export const handlebarsHelpers = () => {
3
+ Handlebars.registerHelper("eq", function (a, b) {
4
+ return a === b;
5
+ });
6
+ Handlebars.registerHelper("lowercase", function (str) {
7
+ return typeof str === "string" ? str.toLowerCase() : str;
8
+ });
9
+ };
@@ -0,0 +1 @@
1
+ export * from "./generate-api-clients";
@@ -0,0 +1,120 @@
1
+ import { Project, SyntaxKind } from "ts-morph";
2
+ import * as path from "path";
3
+ import { extractTypeDefinition, setTypeKind } from "./api-type-spec-utils";
4
+ const INPUT_DECORATOR_NAME = "LambdaInput";
5
+ const OUTPUT_DECORATOR_NAME = "LambdaOutput";
6
+ export const parseDiscoveredApis = (resources, options) => {
7
+ const parsedApis = {};
8
+ const project = new Project({
9
+ tsConfigFilePath: options.tsConfigPath,
10
+ skipAddingFilesFromTsConfig: true,
11
+ });
12
+ for (const stackResources of Object.values(resources.discoveredResources)) {
13
+ for (const api of stackResources.apis) {
14
+ const method = api.id.split("--")[0];
15
+ const fullPath = "/" + api.id.split("--").slice(1).join("/");
16
+ const basePath = fullPath.split("/")[1];
17
+ const handlerPath = path.join(api.folderPath, "handler.ts");
18
+ const sourceFile = project.addSourceFileAtPathIfExists(handlerPath);
19
+ if (!sourceFile)
20
+ continue;
21
+ const handlerVar = sourceFile.getVariableDeclaration("handler");
22
+ if (!handlerVar)
23
+ continue;
24
+ const initializer = handlerVar.getInitializer();
25
+ let className = null;
26
+ if (initializer && initializer.getKind() === SyntaxKind.CallExpression) {
27
+ const callExpr = initializer.asKind(SyntaxKind.CallExpression);
28
+ if (callExpr) {
29
+ const expr = callExpr.getExpression();
30
+ const exprText = expr.getText();
31
+ className = exprText.split(".")[0];
32
+ }
33
+ }
34
+ if (!className)
35
+ continue;
36
+ const actionName = className.replace(/Lambda|lambda|Handler|handler/g, "");
37
+ const relatedDataName = actionName.replace(/^[A-Z][a-z]*/, "");
38
+ const classDecl = sourceFile.getClass(className);
39
+ if (!classDecl)
40
+ continue;
41
+ let inputTypeName = null;
42
+ let outputTypeName = null;
43
+ for (const decorator of classDecl.getDecorators()) {
44
+ const name = decorator.getName();
45
+ const args = decorator.getArguments();
46
+ if (name === INPUT_DECORATOR_NAME && args.length > 0) {
47
+ inputTypeName = args[0].getText().replace(/\W/g, "");
48
+ }
49
+ if (name === OUTPUT_DECORATOR_NAME && args.length > 0) {
50
+ outputTypeName = args[0].getText().replace(/\W/g, "");
51
+ }
52
+ }
53
+ const imports = {};
54
+ let inputImportPath = null;
55
+ let outputImportPath = null;
56
+ for (const importDecl of sourceFile.getImportDeclarations()) {
57
+ const namedImports = importDecl.getNamedImports();
58
+ for (const namedImport of namedImports) {
59
+ const importName = namedImport.getName();
60
+ const importPath = importDecl.getModuleSpecifierValue();
61
+ if (inputTypeName && importName === inputTypeName) {
62
+ inputImportPath = importPath;
63
+ if (!imports[importPath])
64
+ imports[importPath] = [];
65
+ imports[importPath].push(importName);
66
+ }
67
+ if (outputTypeName && importName === outputTypeName) {
68
+ outputImportPath = importPath;
69
+ if (!imports[importPath])
70
+ imports[importPath] = [];
71
+ imports[importPath].push(importName);
72
+ }
73
+ }
74
+ }
75
+ const inputType = inputTypeName
76
+ ? {
77
+ typeName: inputTypeName,
78
+ importPath: inputImportPath,
79
+ typeDefinition: {
80
+ allowedImports: [],
81
+ inlinedFiles: [],
82
+ },
83
+ structure: null,
84
+ kind: "unknown",
85
+ }
86
+ : null;
87
+ const outputType = outputTypeName
88
+ ? {
89
+ typeName: outputTypeName,
90
+ importPath: outputImportPath,
91
+ typeDefinition: {
92
+ allowedImports: [],
93
+ inlinedFiles: [],
94
+ },
95
+ structure: null,
96
+ kind: "unknown",
97
+ }
98
+ : null;
99
+ setTypeKind(inputType, sourceFile);
100
+ setTypeKind(outputType, sourceFile);
101
+ if (options.typeResolutionMode === "inline-types") {
102
+ extractTypeDefinition(inputType, sourceFile);
103
+ extractTypeDefinition(outputType, sourceFile);
104
+ }
105
+ if (!parsedApis[basePath]) {
106
+ parsedApis[basePath] = [];
107
+ }
108
+ parsedApis[basePath].push({
109
+ fullPath,
110
+ method,
111
+ actionName,
112
+ relatedDataName,
113
+ inputType,
114
+ outputType,
115
+ imports,
116
+ });
117
+ }
118
+ }
119
+ return parsedApis;
120
+ };
@@ -0,0 +1,79 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ export const syncApiClients = (webappDataPath) => {
4
+ const sourceDir = path.join(__dirname, "out", "apis");
5
+ const targetDir = path.join(webappDataPath, "apis");
6
+ if (!fs.existsSync(sourceDir)) {
7
+ throw new Error(`Source directory not found: ${sourceDir}`);
8
+ }
9
+ if (!fs.existsSync(targetDir)) {
10
+ fs.mkdirSync(targetDir, { recursive: true });
11
+ }
12
+ const getHooks = (dir) => {
13
+ const hooks = [];
14
+ const services = fs
15
+ .readdirSync(dir, { withFileTypes: true })
16
+ .filter((dirent) => dirent.isDirectory() && dirent.name !== "node_modules");
17
+ for (const service of services) {
18
+ const hooksPath = path.join(dir, service.name, "hooks");
19
+ if (fs.existsSync(hooksPath)) {
20
+ const hookFiles = fs
21
+ .readdirSync(hooksPath)
22
+ .filter((file) => file.endsWith(".ts") && file !== "index.ts");
23
+ for (const hookFile of hookFiles) {
24
+ hooks.push({
25
+ service: service.name,
26
+ name: hookFile.replace(".ts", ""),
27
+ fullPath: path.join(hooksPath, hookFile),
28
+ });
29
+ }
30
+ }
31
+ }
32
+ return hooks;
33
+ };
34
+ const existingHooks = getHooks(targetDir);
35
+ const newHooks = getHooks(sourceDir);
36
+ const addedHooks = newHooks.filter((newHook) => !existingHooks.some((existingHook) => existingHook.service === newHook.service &&
37
+ existingHook.name === newHook.name));
38
+ const removedHooks = existingHooks.filter((existingHook) => !newHooks.some((newHook) => newHook.service === existingHook.service &&
39
+ newHook.name === existingHook.name));
40
+ console.log("Hook changes:");
41
+ if (addedHooks.length === 0 && removedHooks.length === 0) {
42
+ console.log("No changes detected.");
43
+ }
44
+ addedHooks.forEach((hook) => console.log(`+ ${hook.service}/${hook.name}`));
45
+ removedHooks.forEach((hook) => console.log(`- ${hook.service}/${hook.name}`));
46
+ fs.copyFileSync(path.join(sourceDir, "api-client.ts"), path.join(targetDir, "api-client.ts"));
47
+ const services = fs
48
+ .readdirSync(sourceDir, { withFileTypes: true })
49
+ .filter((dirent) => dirent.isDirectory());
50
+ const mainIndexContent = services
51
+ .map((service) => `export * from "./${service.name}";`)
52
+ .join("\n");
53
+ fs.writeFileSync(path.join(targetDir, "index.ts"), mainIndexContent);
54
+ for (const service of services) {
55
+ const sourceServiceDir = path.join(sourceDir, service.name);
56
+ const targetServiceDir = path.join(targetDir, service.name);
57
+ if (!fs.existsSync(targetServiceDir)) {
58
+ fs.mkdirSync(targetServiceDir, { recursive: true });
59
+ }
60
+ fs.copyFileSync(path.join(sourceServiceDir, `${service.name}-api.ts`), path.join(targetServiceDir, `${service.name}-api.ts`));
61
+ fs.copyFileSync(path.join(sourceServiceDir, "index.ts"), path.join(targetServiceDir, "index.ts"));
62
+ const targetHooksDir = path.join(targetServiceDir, "hooks");
63
+ if (!fs.existsSync(targetHooksDir)) {
64
+ fs.mkdirSync(targetHooksDir, { recursive: true });
65
+ }
66
+ const serviceHooks = [...addedHooks, ...existingHooks].filter((hook) => hook.service === service.name);
67
+ for (const hook of serviceHooks) {
68
+ const targetHookPath = path.join(targetHooksDir, `${hook.name}.ts`);
69
+ if (!fs.existsSync(targetHookPath)) {
70
+ fs.copyFileSync(hook.fullPath, targetHookPath);
71
+ }
72
+ }
73
+ const hooksIndexContent = serviceHooks
74
+ .map((hook) => `export * from "./${hook.name}";`)
75
+ .sort()
76
+ .join("\n") + "\n";
77
+ fs.writeFileSync(path.join(targetHooksDir, "index.ts"), hooksIndexContent);
78
+ }
79
+ };
@@ -0,0 +1,20 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ export function loadTemplate(templateName) {
4
+ const possiblePaths = [
5
+ path.join(__dirname, "templates", templateName),
6
+ path.join(__dirname, "templates", templateName),
7
+ path.join(process.cwd(), "node_modules", "@itzworking", "devkit", "dist-cjs", "generate-api-clients", "templates", templateName),
8
+ path.join(process.cwd(), "node_modules", "@itzworking", "devkit", "dist-es", "generate-api-clients", "templates", templateName),
9
+ ];
10
+ for (const templatePath of possiblePaths) {
11
+ try {
12
+ if (fs.existsSync(templatePath)) {
13
+ return fs.readFileSync(templatePath, "utf8");
14
+ }
15
+ }
16
+ catch (error) {
17
+ }
18
+ }
19
+ throw new Error(`Template not found: ${templateName}. Tried paths: ${possiblePaths.join(", ")}`);
20
+ }
@@ -0,0 +1,25 @@
1
+ // prettier-ignore
2
+ /* eslint-disable */
3
+
4
+ import { ApiClientClass } from "../api-client";
5
+ {{#each imports}}
6
+ import { {{names}} } from "{{path}}";
7
+ {{/each}}
8
+
9
+ class {{className}}Class extends ApiClientClass {
10
+ {{#each methods}}
11
+ async {{name}}({{params}}): Promise<{{returnType}}> {
12
+ {{#if outputType}}
13
+ const responseBody = await this.{{method}}({ path: {{{pathWithQuotes fullPath}}}{{#if inputType}}, options: { body }{{/if}} });
14
+ return new {{outputType.typeName}}(responseBody);
15
+ {{else}}
16
+ await this.{{method}}({ path: {{{pathWithQuotes fullPath}}}{{#if inputType}}, options: { body }{{/if}} });
17
+ {{/if}}
18
+ }
19
+ {{#unless @last}}
20
+
21
+ {{/unless}}
22
+ {{/each}}
23
+ }
24
+
25
+ export const {{className}} = new {{className}}Class();
@@ -0,0 +1,131 @@
1
+ import {
2
+ del,
3
+ get,
4
+ head,
5
+ isCancelError,
6
+ patch,
7
+ post,
8
+ put,
9
+ } from "aws-amplify/api";
10
+ import type {
11
+ ApiInput as AmplifyApiInput,
12
+ RestApiOptionsBase as AmplifyRestApiOptionsBase,
13
+ RestApiResponse as AmplifyRestApiResponse,
14
+ } from "@aws-amplify/api-rest/src/types";
15
+ import { fetchAuthSession } from "aws-amplify/auth";
16
+
17
+ type ApiClientInputOption = {
18
+ headers?: Record<string, string>;
19
+ queryParams?: Record<string, string>;
20
+ body?: any;
21
+ };
22
+
23
+ type ApiClientInput = {
24
+ path: string;
25
+ options?: Omit<ApiClientInputOption, "body">;
26
+ };
27
+
28
+ type ApiClientInputWithBody = {
29
+ path: string;
30
+ options?: ApiClientInputOption;
31
+ };
32
+
33
+ export type GetInput = ApiClientInputWithBody;
34
+ export type PostInput = ApiClientInputWithBody;
35
+ export type PutInput = ApiClientInputWithBody;
36
+ export type PatchInput = ApiClientInputWithBody;
37
+ export type DeleteInput = ApiClientInput;
38
+ export type HeadInput = ApiClientInput;
39
+
40
+ export class ApiClientClass {
41
+ private readonly apiName = "default";
42
+
43
+ private async formatInput(
44
+ input: ApiClientInput
45
+ ): Promise<AmplifyApiInput<AmplifyRestApiOptionsBase>> {
46
+ const { idToken, accessToken } = (await fetchAuthSession()).tokens ?? {};
47
+ return {
48
+ ...input,
49
+ apiName: this.apiName,
50
+ options: {
51
+ ...input.options,
52
+ headers: {
53
+ ...input.options?.headers,
54
+ Authorization: `Bearer ${idToken}`,
55
+ },
56
+ },
57
+ };
58
+ }
59
+
60
+ private async formatResponseBody(
61
+ response:
62
+ | Promise<AmplifyRestApiResponse>
63
+ | Promise<Omit<AmplifyRestApiResponse, "body">>
64
+ ): Promise<any> {
65
+ try {
66
+ const res = await response;
67
+ // Handle 204 No Content or empty body
68
+ if (res.statusCode === 204 || !("body" in res) || !res.body) {
69
+ return undefined;
70
+ }
71
+ // Try to parse JSON, but handle empty string
72
+ const text = await res.body.text();
73
+ if (!text) return undefined;
74
+ try {
75
+ const json = JSON.parse(text);
76
+ return json;
77
+ } catch (e) {
78
+ throw new Error("Response must be a JSON object or array");
79
+ }
80
+ } catch (e: any) {
81
+ if (e?._response?.body) {
82
+ let parsed: any;
83
+ try {
84
+ parsed = JSON.parse(e._response.body);
85
+ } catch (parseError) {
86
+ // If JSON parsing fails, continue to throw original error
87
+ }
88
+ if (parsed) {
89
+ throw parsed;
90
+ }
91
+ }
92
+ throw e;
93
+ }
94
+ }
95
+
96
+ async get(input: GetInput) {
97
+ const formattedInput = await this.formatInput(input);
98
+ return this.formatResponseBody(get(formattedInput).response);
99
+ }
100
+
101
+ async put(input: PutInput) {
102
+ const formattedInput = await this.formatInput(input);
103
+ return this.formatResponseBody(put(formattedInput).response);
104
+ }
105
+
106
+ async post(input: PostInput) {
107
+ const formattedInput = await this.formatInput(input);
108
+ return this.formatResponseBody(post(formattedInput).response);
109
+ }
110
+
111
+ async delete(input: DeleteInput) {
112
+ const formattedInput = await this.formatInput(input);
113
+ return this.formatResponseBody(del(formattedInput).response);
114
+ }
115
+
116
+ async head(input: HeadInput) {
117
+ const formattedInput = await this.formatInput(input);
118
+ return this.formatResponseBody(head(formattedInput).response);
119
+ }
120
+
121
+ async patch(input: PatchInput) {
122
+ const formattedInput = await this.formatInput(input);
123
+ return this.formatResponseBody(patch(formattedInput).response);
124
+ }
125
+
126
+ isCancelError(error: unknown) {
127
+ return isCancelError(error);
128
+ }
129
+ }
130
+
131
+ export const ApiClient = new ApiClientClass();