appflare 0.0.26 → 0.0.28
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,121 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
export function extractClientConfig(configPath: string): string | null {
|
|
5
|
+
if (!fs.existsSync(configPath)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const sourceCode = fs.readFileSync(configPath, "utf-8");
|
|
10
|
+
const sourceFile = ts.createSourceFile(
|
|
11
|
+
"appflare.config.ts",
|
|
12
|
+
sourceCode,
|
|
13
|
+
ts.ScriptTarget.Latest,
|
|
14
|
+
true,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
let clientOptionsNode: ts.Expression | undefined;
|
|
18
|
+
|
|
19
|
+
// 1. Find the default export
|
|
20
|
+
// 2. Find the 'auth' property in the object literal
|
|
21
|
+
// 3. Find the 'clientOptions' property in the 'auth' object literal
|
|
22
|
+
|
|
23
|
+
function findClientOptions(node: ts.Node) {
|
|
24
|
+
if (ts.isExportAssignment(node)) {
|
|
25
|
+
const expr = node.expression;
|
|
26
|
+
if (ts.isObjectLiteralExpression(expr)) {
|
|
27
|
+
const authProp = expr.properties.find(
|
|
28
|
+
(p) =>
|
|
29
|
+
ts.isPropertyAssignment(p) &&
|
|
30
|
+
ts.isIdentifier(p.name) &&
|
|
31
|
+
p.name.text === "auth",
|
|
32
|
+
) as ts.PropertyAssignment | undefined;
|
|
33
|
+
|
|
34
|
+
if (authProp && ts.isObjectLiteralExpression(authProp.initializer)) {
|
|
35
|
+
const clientOptionsProp = authProp.initializer.properties.find(
|
|
36
|
+
(p) =>
|
|
37
|
+
ts.isPropertyAssignment(p) &&
|
|
38
|
+
ts.isIdentifier(p.name) &&
|
|
39
|
+
p.name.text === "clientOptions",
|
|
40
|
+
) as ts.PropertyAssignment | undefined;
|
|
41
|
+
|
|
42
|
+
if (clientOptionsProp) {
|
|
43
|
+
clientOptionsNode = clientOptionsProp.initializer;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
ts.forEachChild(node, findClientOptions);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
findClientOptions(sourceFile);
|
|
52
|
+
|
|
53
|
+
if (!clientOptionsNode) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const clientOptionsText = clientOptionsNode.getText(sourceFile);
|
|
58
|
+
|
|
59
|
+
// 4. Identify identifiers used in clientOptionsText to find necessary imports
|
|
60
|
+
const usedIdentifiers = new Set<string>();
|
|
61
|
+
|
|
62
|
+
function findIdentifiers(node: ts.Node) {
|
|
63
|
+
if (ts.isIdentifier(node)) {
|
|
64
|
+
usedIdentifiers.add(node.text);
|
|
65
|
+
}
|
|
66
|
+
ts.forEachChild(node, findIdentifiers);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
findIdentifiers(clientOptionsNode);
|
|
70
|
+
|
|
71
|
+
// 5. Scan top-level imports to find matching named imports
|
|
72
|
+
const importsToKeep: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const statement of sourceFile.statements) {
|
|
75
|
+
if (ts.isImportDeclaration(statement)) {
|
|
76
|
+
const importClause = statement.importClause;
|
|
77
|
+
if (!importClause) continue;
|
|
78
|
+
|
|
79
|
+
const moduleSpecifier = statement.moduleSpecifier.getText(sourceFile);
|
|
80
|
+
|
|
81
|
+
// Check for named imports
|
|
82
|
+
if (
|
|
83
|
+
importClause.namedBindings &&
|
|
84
|
+
ts.isNamedImports(importClause.namedBindings)
|
|
85
|
+
) {
|
|
86
|
+
const keepElements: string[] = [];
|
|
87
|
+
for (const element of importClause.namedBindings.elements) {
|
|
88
|
+
if (usedIdentifiers.has(element.name.text)) {
|
|
89
|
+
keepElements.push(element.getText(sourceFile));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (keepElements.length > 0) {
|
|
93
|
+
importsToKeep.push(
|
|
94
|
+
`import { ${keepElements.join(", ")} } from ${moduleSpecifier};`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for default import
|
|
100
|
+
if (importClause.name && usedIdentifiers.has(importClause.name.text)) {
|
|
101
|
+
importsToKeep.push(
|
|
102
|
+
`import ${importClause.name.text} from ${moduleSpecifier};`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for namespace import
|
|
107
|
+
if (
|
|
108
|
+
importClause.namedBindings &&
|
|
109
|
+
ts.isNamespaceImport(importClause.namedBindings)
|
|
110
|
+
) {
|
|
111
|
+
if (usedIdentifiers.has(importClause.namedBindings.name.text)) {
|
|
112
|
+
importsToKeep.push(
|
|
113
|
+
`import * as ${importClause.namedBindings.name.text} from ${moduleSpecifier};`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `${importsToKeep.join("\n")}\n\nexport const clientOptions = ${clientOptionsText};`;
|
|
121
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
generateInternalTypeLines,
|
|
16
16
|
} from "./types";
|
|
17
17
|
import { renderObjectKey } from "./utils";
|
|
18
|
+
import { extractClientConfig } from "./extract-configuration";
|
|
18
19
|
|
|
19
20
|
const HEADER_TEMPLATE = `/* eslint-disable */
|
|
20
21
|
/**
|
|
@@ -833,7 +834,7 @@ export function generateApiClient(params: {
|
|
|
833
834
|
authBasePath?: string;
|
|
834
835
|
authEnabled?: boolean;
|
|
835
836
|
configPathAbs?: string;
|
|
836
|
-
}): string {
|
|
837
|
+
}): { apiTs: string; clientConfigTs: string | null } {
|
|
837
838
|
const { importLines, importAliasBySource } = generateImports(params);
|
|
838
839
|
const {
|
|
839
840
|
queriesByFile,
|
|
@@ -892,15 +893,17 @@ export function generateApiClient(params: {
|
|
|
892
893
|
buildUrl(baseUrl, authBasePath),
|
|
893
894
|
});`;
|
|
894
895
|
|
|
896
|
+
const clientConfigTs =
|
|
897
|
+
params.authEnabled && params.configPathAbs
|
|
898
|
+
? extractClientConfig(params.configPathAbs)
|
|
899
|
+
: null;
|
|
900
|
+
|
|
895
901
|
if (params.authEnabled && params.configPathAbs) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
params.configPathAbs,
|
|
899
|
-
);
|
|
900
|
-
configImport = `\nimport __appflareConfig from ${JSON.stringify(configImportPath)};`;
|
|
902
|
+
if (clientConfigTs) {
|
|
903
|
+
configImport = `\nimport { clientOptions } from "./client.config";`;
|
|
901
904
|
|
|
902
|
-
|
|
903
|
-
|
|
905
|
+
// Use a factory function pattern to properly infer the client type from clientOptions
|
|
906
|
+
authClientTypeDefinitions = `const __getAppflareAuthClientOptions = () => (clientOptions ?? {}) as const;
|
|
904
907
|
type AppflareAuthClientOptions = ReturnType<typeof __getAppflareAuthClientOptions>;
|
|
905
908
|
const __createTypedAuthClient = (baseURL: string) => createAuthClient({
|
|
906
909
|
...__getAppflareAuthClientOptions(),
|
|
@@ -908,37 +911,63 @@ const __createTypedAuthClient = (baseURL: string) => createAuthClient({
|
|
|
908
911
|
});
|
|
909
912
|
type AppflareAuthClient = ReturnType<typeof __createTypedAuthClient>;`;
|
|
910
913
|
|
|
911
|
-
|
|
914
|
+
authClientInit = ` const auth = createAuthClient({
|
|
912
915
|
...__getAppflareAuthClientOptions(),
|
|
913
916
|
...(options.auth ?? {}),
|
|
914
917
|
baseURL:
|
|
915
918
|
(options.auth as any)?.baseURL ??
|
|
916
919
|
buildUrl(baseUrl, authBasePath),
|
|
917
920
|
});`;
|
|
921
|
+
} else {
|
|
922
|
+
const configImportPath = toImportPathFromGeneratedSrc(
|
|
923
|
+
params.outDirAbs,
|
|
924
|
+
params.configPathAbs,
|
|
925
|
+
);
|
|
926
|
+
configImport = `\nimport __appflareConfig from ${JSON.stringify(configImportPath)};`;
|
|
927
|
+
|
|
928
|
+
// Use a factory function pattern to properly infer the client type from clientOptions
|
|
929
|
+
authClientTypeDefinitions = `const __getAppflareAuthClientOptions = () => (__appflareConfig.auth?.clientOptions ?? {}) as const;
|
|
930
|
+
type AppflareAuthClientOptions = ReturnType<typeof __getAppflareAuthClientOptions>;
|
|
931
|
+
const __createTypedAuthClient = (baseURL: string) => createAuthClient({
|
|
932
|
+
...__getAppflareAuthClientOptions(),
|
|
933
|
+
baseURL,
|
|
934
|
+
});
|
|
935
|
+
type AppflareAuthClient = ReturnType<typeof __createTypedAuthClient>;`;
|
|
936
|
+
|
|
937
|
+
authClientInit = ` const auth = createAuthClient({
|
|
938
|
+
...__getAppflareAuthClientOptions(),
|
|
939
|
+
...(options.auth ?? {}),
|
|
940
|
+
baseURL:
|
|
941
|
+
(options.auth as any)?.baseURL ??
|
|
942
|
+
buildUrl(baseUrl, authBasePath),
|
|
943
|
+
});`;
|
|
944
|
+
}
|
|
918
945
|
}
|
|
919
946
|
|
|
920
947
|
const typeBlocks = generateTypeBlocks(params.handlers, importAliasBySource);
|
|
921
948
|
|
|
922
|
-
return
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
.replace("{{
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
949
|
+
return {
|
|
950
|
+
apiTs:
|
|
951
|
+
HEADER_TEMPLATE.replace("{{configImport}}", configImport) +
|
|
952
|
+
importLines.join("\n") +
|
|
953
|
+
TYPE_DEFINITIONS_TEMPLATE +
|
|
954
|
+
typeBlocks.join("\n\n") +
|
|
955
|
+
INTERNAL_TEMPLATE.replace(
|
|
956
|
+
"{{internalQueriesTypeDef}}",
|
|
957
|
+
internalQueriesTypeDef,
|
|
958
|
+
)
|
|
959
|
+
.replace("{{internalMutationsTypeDef}}", internalMutationsTypeDef)
|
|
960
|
+
.replace("{{internalInit}}", internalInit)
|
|
961
|
+
.replace("{{internalQueriesMeta}}", internalQueriesMeta)
|
|
962
|
+
.replace("{{internalMutationsMeta}}", internalMutationsMeta) +
|
|
963
|
+
CLIENT_TYPES_TEMPLATE.replace("{{queriesTypeDef}}", queriesTypeDef)
|
|
964
|
+
.replace("{{mutationsTypeDef}}", mutationsTypeDef)
|
|
965
|
+
.replace("{{queriesInit}}", queriesInit)
|
|
966
|
+
.replace("{{mutationsInit}}", mutationsInit)
|
|
967
|
+
.replace("{{authBasePath}}", authBasePathLiteral)
|
|
968
|
+
.replace("{{authClientTypeDefinitions}}", authClientTypeDefinitions)
|
|
969
|
+
.replace("{{authClientInit}}", authClientInit) +
|
|
970
|
+
UTILITY_FUNCTIONS_TEMPLATE,
|
|
971
|
+
clientConfigTs,
|
|
972
|
+
};
|
|
944
973
|
}
|
package/cli/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import chokidar, { FSWatcher } from "chokidar";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { promises as fs } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { pathToFileURL } from "node:url";
|
|
8
9
|
import {
|
|
@@ -82,13 +83,59 @@ async function main(): Promise<void> {
|
|
|
82
83
|
await program.parseAsync(process.argv);
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Regex that matches ES import lines pulling in React Native / Expo native
|
|
88
|
+
* modules (e.g. `import * as SecureStore from "expo-secure-store"`).
|
|
89
|
+
* These cannot be transpiled by Bun and are only needed at client runtime.
|
|
90
|
+
*/
|
|
91
|
+
const NATIVE_IMPORT_RE =
|
|
92
|
+
/^import\s+(?:(?:\*\s+as\s+(\w+))|(?:\{[^}]*\})|(?:(\w+)(?:\s*,\s*\{[^}]*\})?))?\s*from\s*["'](expo-[^"']+)["'];?\s*$/gm;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Strip native-module imports from a config source and replace them with
|
|
96
|
+
* harmless stub declarations so the CLI can evaluate the config object
|
|
97
|
+
* without triggering Bun transpilation errors on native code.
|
|
98
|
+
*/
|
|
99
|
+
function sanitizeConfigSource(source: string): string {
|
|
100
|
+
const stubs: string[] = [];
|
|
101
|
+
const sanitized = source.replace(
|
|
102
|
+
NATIVE_IMPORT_RE,
|
|
103
|
+
(_match, starAs, defaultImport, _mod) => {
|
|
104
|
+
const name = starAs || defaultImport;
|
|
105
|
+
if (name) {
|
|
106
|
+
stubs.push(`const ${name} = {} as any;`);
|
|
107
|
+
}
|
|
108
|
+
return ""; // remove the original import line
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
return stubs.length > 0 ? stubs.join("\n") + "\n" + sanitized : sanitized;
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
async function loadConfig(
|
|
86
115
|
configPathAbs: string,
|
|
87
116
|
): Promise<{ config: AppflareConfig; configDirAbs: string }> {
|
|
88
117
|
await assertFileExists(configPathAbs, `Config not found: ${configPathAbs}`);
|
|
89
118
|
const configDirAbs = path.dirname(configPathAbs);
|
|
90
119
|
|
|
91
|
-
|
|
120
|
+
// Read the config source and strip native-module imports (e.g. expo-secure-store)
|
|
121
|
+
// that Bun cannot transpile. Write the sanitized source to a temp file and import that.
|
|
122
|
+
const raw = await fs.readFile(configPathAbs, "utf-8");
|
|
123
|
+
const sanitized = sanitizeConfigSource(raw);
|
|
124
|
+
|
|
125
|
+
let mod: Record<string, unknown>;
|
|
126
|
+
if (sanitized !== raw) {
|
|
127
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "appflare-cfg-"));
|
|
128
|
+
const tmpFile = path.join(tmpDir, path.basename(configPathAbs));
|
|
129
|
+
await fs.writeFile(tmpFile, sanitized);
|
|
130
|
+
try {
|
|
131
|
+
mod = await import(pathToFileURL(tmpFile).href);
|
|
132
|
+
} finally {
|
|
133
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
mod = await import(pathToFileURL(configPathAbs).href);
|
|
137
|
+
}
|
|
138
|
+
|
|
92
139
|
const config = (mod?.default ?? mod) as Partial<AppflareConfig>;
|
|
93
140
|
if (!config || typeof config !== "object") {
|
|
94
141
|
throw new Error(
|
|
@@ -172,7 +219,7 @@ export default schema;
|
|
|
172
219
|
configPathAbs,
|
|
173
220
|
});
|
|
174
221
|
|
|
175
|
-
const apiTs = generateApiClient({
|
|
222
|
+
const { apiTs, clientConfigTs } = generateApiClient({
|
|
176
223
|
handlers,
|
|
177
224
|
outDirAbs,
|
|
178
225
|
authBasePath:
|
|
@@ -183,6 +230,12 @@ export default schema;
|
|
|
183
230
|
configPathAbs,
|
|
184
231
|
});
|
|
185
232
|
await fs.writeFile(path.join(outDirAbs, "src", "api.ts"), apiTs);
|
|
233
|
+
if (clientConfigTs) {
|
|
234
|
+
await fs.writeFile(
|
|
235
|
+
path.join(outDirAbs, "src", "client.config.ts"),
|
|
236
|
+
clientConfigTs,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
186
239
|
|
|
187
240
|
const serverTs = generateHonoServer({
|
|
188
241
|
handlers,
|