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
- const configImportPath = toImportPathFromGeneratedSrc(
897
- params.outDirAbs,
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
- // Use a factory function pattern to properly infer the client type from clientOptions
903
- authClientTypeDefinitions = `const __getAppflareAuthClientOptions = () => (__appflareConfig.auth?.clientOptions ?? {}) as const;
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
- authClientInit = ` const auth = createAuthClient({
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
- HEADER_TEMPLATE.replace("{{configImport}}", configImport) +
924
- importLines.join("\n") +
925
- TYPE_DEFINITIONS_TEMPLATE +
926
- typeBlocks.join("\n\n") +
927
- INTERNAL_TEMPLATE.replace(
928
- "{{internalQueriesTypeDef}}",
929
- internalQueriesTypeDef,
930
- )
931
- .replace("{{internalMutationsTypeDef}}", internalMutationsTypeDef)
932
- .replace("{{internalInit}}", internalInit)
933
- .replace("{{internalQueriesMeta}}", internalQueriesMeta)
934
- .replace("{{internalMutationsMeta}}", internalMutationsMeta) +
935
- CLIENT_TYPES_TEMPLATE.replace("{{queriesTypeDef}}", queriesTypeDef)
936
- .replace("{{mutationsTypeDef}}", mutationsTypeDef)
937
- .replace("{{queriesInit}}", queriesInit)
938
- .replace("{{mutationsInit}}", mutationsInit)
939
- .replace("{{authBasePath}}", authBasePathLiteral)
940
- .replace("{{authClientTypeDefinitions}}", authClientTypeDefinitions)
941
- .replace("{{authClientInit}}", authClientInit) +
942
- UTILITY_FUNCTIONS_TEMPLATE
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
- const mod = await import(pathToFileURL(configPathAbs).href);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appflare",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "bin": {
5
5
  "appflare": "./cli/index.ts"
6
6
  },