create-100x-mobile 0.4.11 → 0.5.1
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/dist/cli.js +2 -1
- package/dist/commands/new/args.js +25 -0
- package/dist/commands/new/scaffold.js +18 -8
- package/dist/commands/new/steps.js +160 -33
- package/dist/commands/new.js +160 -41
- package/dist/templates/cloudflare/alchemyRun.js +122 -0
- package/dist/templates/cloudflare/cloudflareStorage.js +108 -0
- package/dist/templates/cloudflare/instantCloudflareLib.js +42 -0
- package/dist/templates/cloudflare/worker.js +762 -0
- package/dist/templates/cloudflare/workerMigration.js +18 -0
- package/dist/templates/cloudflare/workerTsconfig.js +20 -0
- package/dist/templates/config/appJson.js +5 -3
- package/dist/templates/config/envExample.js +31 -1
- package/dist/templates/config/packageJson.js +45 -8
- package/dist/templates/config/readme.js +149 -1
- package/dist/templates/config/tsconfig.js +1 -0
- package/dist/templates/instant/magic/authLayout.js +122 -0
- package/dist/templates/instant/magic/rootLayout.js +24 -0
- package/dist/templates/instant/magic/settingsScreen.js +282 -0
- package/dist/templates/instant/magic/signIn.js +346 -0
- package/dist/templates/instant/magic/tabsLayout.js +143 -0
- package/package.json +4 -3
package/dist/commands/new.js
CHANGED
|
@@ -43,6 +43,18 @@ const rootLayout_2 = require("../templates/instant/rootLayout");
|
|
|
43
43
|
const settingsScreen_2 = require("../templates/instant/settingsScreen");
|
|
44
44
|
const tabsLayout_2 = require("../templates/instant/tabsLayout");
|
|
45
45
|
const todosScreen_2 = require("../templates/instant/todosScreen");
|
|
46
|
+
const authLayout_2 = require("../templates/instant/magic/authLayout");
|
|
47
|
+
const rootLayout_3 = require("../templates/instant/magic/rootLayout");
|
|
48
|
+
const settingsScreen_3 = require("../templates/instant/magic/settingsScreen");
|
|
49
|
+
const signIn_2 = require("../templates/instant/magic/signIn");
|
|
50
|
+
const tabsLayout_3 = require("../templates/instant/magic/tabsLayout");
|
|
51
|
+
// Cloudflare templates
|
|
52
|
+
const alchemyRun_1 = require("../templates/cloudflare/alchemyRun");
|
|
53
|
+
const cloudflareStorage_1 = require("../templates/cloudflare/cloudflareStorage");
|
|
54
|
+
const instantCloudflareLib_1 = require("../templates/cloudflare/instantCloudflareLib");
|
|
55
|
+
const worker_1 = require("../templates/cloudflare/worker");
|
|
56
|
+
const workerMigration_1 = require("../templates/cloudflare/workerMigration");
|
|
57
|
+
const workerTsconfig_1 = require("../templates/cloudflare/workerTsconfig");
|
|
46
58
|
// Component templates
|
|
47
59
|
const addTodoForm_1 = require("../templates/components/addTodoForm");
|
|
48
60
|
const emptyState_1 = require("../templates/components/emptyState");
|
|
@@ -50,32 +62,92 @@ const filterTabs_1 = require("../templates/components/filterTabs");
|
|
|
50
62
|
const todoItem_1 = require("../templates/components/todoItem");
|
|
51
63
|
// Hook templates
|
|
52
64
|
const useFrameworkReady_1 = require("../templates/hooks/useFrameworkReady");
|
|
53
|
-
function buildTemplateFiles(projectName, expoVersion, backend) {
|
|
65
|
+
function buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode = "clerk") {
|
|
54
66
|
if (backend === "instantdb") {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
[
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
const cloudflareFiles = (0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
|
|
68
|
+
? [
|
|
69
|
+
["alchemy.run.ts", (0, alchemyRun_1.alchemyRunTemplate)(projectName)],
|
|
70
|
+
["lib/cloudflareStorage.ts", (0, cloudflareStorage_1.cloudflareStorageTemplate)()],
|
|
71
|
+
["lib/instant.ts", (0, instantCloudflareLib_1.instantCloudflareLibTemplate)()],
|
|
72
|
+
["worker/src/worker.ts", (0, worker_1.storageWorkerTemplate)()],
|
|
73
|
+
["worker/tsconfig.json", (0, workerTsconfig_1.workerTsconfigTemplate)()],
|
|
74
|
+
["worker/migrations/0001_uploads.sql", (0, workerMigration_1.workerMigrationTemplate)()],
|
|
75
|
+
]
|
|
76
|
+
: [];
|
|
77
|
+
const files = (0, scaffold_1.isMagicCodeInstantAuthMode)(instantAuthMode)
|
|
78
|
+
? [
|
|
79
|
+
// Config
|
|
80
|
+
[
|
|
81
|
+
"package.json",
|
|
82
|
+
(0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend, instantAuthMode),
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
"app.json",
|
|
86
|
+
(0, appJson_1.appJsonTemplate)(projectName, backend, instantAuthMode),
|
|
87
|
+
],
|
|
88
|
+
["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
|
|
89
|
+
[".gitignore", (0, gitignore_1.gitignoreTemplate)()],
|
|
90
|
+
[".env.example", (0, envExample_1.envExampleTemplate)(backend, instantAuthMode)],
|
|
91
|
+
["expo-env.d.ts", (0, tsconfig_1.expoEnvDtsTemplate)()],
|
|
92
|
+
[".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
|
|
93
|
+
["eas.json", (0, easJson_1.easJsonTemplate)()],
|
|
94
|
+
[
|
|
95
|
+
"README.md",
|
|
96
|
+
(0, readme_1.readmeTemplate)(projectName, backend, instantAuthMode),
|
|
97
|
+
],
|
|
98
|
+
// Instant app
|
|
99
|
+
["app/_layout.tsx", (0, rootLayout_3.magicRootLayoutTemplate)()],
|
|
100
|
+
["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
|
|
101
|
+
["app/(auth)/_layout.tsx", (0, authLayout_2.magicAuthLayoutTemplate)()],
|
|
102
|
+
["app/(auth)/sign-in.tsx", (0, signIn_2.magicSignInTemplate)()],
|
|
103
|
+
["app/(tabs)/_layout.tsx", (0, tabsLayout_3.magicTabsLayoutTemplate)()],
|
|
104
|
+
["app/(tabs)/index.tsx", (0, todosScreen_2.instantTodosScreenTemplate)()],
|
|
105
|
+
[
|
|
106
|
+
"app/(tabs)/settings.tsx",
|
|
107
|
+
(0, settingsScreen_3.magicSettingsScreenTemplate)((0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
|
|
108
|
+
? "InstantDB + magic code auth + Cloudflare storage"
|
|
109
|
+
: "InstantDB + magic code auth"),
|
|
110
|
+
],
|
|
111
|
+
...((0, scaffold_1.isCloudflareInstantAuthMode)(instantAuthMode)
|
|
112
|
+
? []
|
|
113
|
+
: [["lib/instant.ts", (0, instantLib_1.instantLibTemplate)()]]),
|
|
114
|
+
// Hooks
|
|
115
|
+
["hooks/useFrameworkReady.ts", (0, useFrameworkReady_1.useFrameworkReadyTemplate)()],
|
|
116
|
+
...cloudflareFiles,
|
|
117
|
+
]
|
|
118
|
+
: [
|
|
119
|
+
// Config
|
|
120
|
+
[
|
|
121
|
+
"package.json",
|
|
122
|
+
(0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend, instantAuthMode),
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"app.json",
|
|
126
|
+
(0, appJson_1.appJsonTemplate)(projectName, backend, instantAuthMode),
|
|
127
|
+
],
|
|
128
|
+
["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
|
|
129
|
+
[".gitignore", (0, gitignore_1.gitignoreTemplate)()],
|
|
130
|
+
[".env.example", (0, envExample_1.envExampleTemplate)(backend, instantAuthMode)],
|
|
131
|
+
["expo-env.d.ts", (0, tsconfig_1.expoEnvDtsTemplate)()],
|
|
132
|
+
[".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
|
|
133
|
+
["eas.json", (0, easJson_1.easJsonTemplate)()],
|
|
134
|
+
[
|
|
135
|
+
"README.md",
|
|
136
|
+
(0, readme_1.readmeTemplate)(projectName, backend, instantAuthMode),
|
|
137
|
+
],
|
|
138
|
+
// Instant app
|
|
139
|
+
["app/_layout.tsx", (0, rootLayout_2.instantRootLayoutTemplate)()],
|
|
140
|
+
["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
|
|
141
|
+
["app/providers/AuthProvider.tsx", (0, authProvider_2.instantAuthProviderTemplate)()],
|
|
142
|
+
["app/(auth)/_layout.tsx", (0, authLayout_1.authLayoutTemplate)()],
|
|
143
|
+
["app/(auth)/sign-in.tsx", (0, signIn_1.signInTemplate)()],
|
|
144
|
+
["app/(tabs)/_layout.tsx", (0, tabsLayout_2.instantTabsLayoutTemplate)()],
|
|
145
|
+
["app/(tabs)/index.tsx", (0, todosScreen_2.instantTodosScreenTemplate)()],
|
|
146
|
+
["app/(tabs)/settings.tsx", (0, settingsScreen_2.instantSettingsScreenTemplate)()],
|
|
147
|
+
["lib/instant.ts", (0, instantLib_1.instantLibTemplate)()],
|
|
148
|
+
// Hooks
|
|
149
|
+
["hooks/useFrameworkReady.ts", (0, useFrameworkReady_1.useFrameworkReadyTemplate)()],
|
|
150
|
+
];
|
|
79
151
|
return files.map(([path, content]) => ({ path, content }));
|
|
80
152
|
}
|
|
81
153
|
const files = [
|
|
@@ -166,14 +238,19 @@ async function cmdNew(rawArgs) {
|
|
|
166
238
|
if (backend === "convex" && options.clerkDomain) {
|
|
167
239
|
options.clerkDomain = (0, steps_1.normalizeClerkDomain)(options.clerkDomain);
|
|
168
240
|
}
|
|
241
|
+
const instantAuthMode = backend === "instantdb" ? await (0, steps_1.resolveInstantAuthMode)(options) : "clerk";
|
|
169
242
|
const expoVersion = await (0, steps_1.resolveExpoVersion)(options);
|
|
170
243
|
const clerk = backend === "convex"
|
|
171
244
|
? await (0, steps_1.promptClerkSetup)(options)
|
|
172
245
|
: { publishableKey: "", domain: "", jwtCreated: false };
|
|
173
246
|
const instant = backend === "instantdb"
|
|
174
|
-
? await (0, steps_1.promptInstantSetup)(options)
|
|
175
|
-
:
|
|
176
|
-
if (backend === "instantdb" &&
|
|
247
|
+
? await (0, steps_1.promptInstantSetup)(options, instantAuthMode)
|
|
248
|
+
: null;
|
|
249
|
+
if (backend === "instantdb" &&
|
|
250
|
+
instant &&
|
|
251
|
+
instant.authMode === "clerk" &&
|
|
252
|
+
instant.appId &&
|
|
253
|
+
instant.clerkPublishableKey) {
|
|
177
254
|
prompts_1.log.info("");
|
|
178
255
|
prompts_1.log.info(picocolors_1.default.bold("Instant Auth setup required"));
|
|
179
256
|
prompts_1.log.info(picocolors_1.default.dim("In Instant Dashboard → Auth tab, add a Clerk auth client before running the app."));
|
|
@@ -190,8 +267,8 @@ async function cmdNew(rawArgs) {
|
|
|
190
267
|
const structureSpinner = (0, prompts_1.spinner)();
|
|
191
268
|
currentStep++;
|
|
192
269
|
structureSpinner.start(`Creating project structure ${stepLabel()}`);
|
|
193
|
-
await (0, scaffold_1.createProjectDirectories)(projectDir, backend);
|
|
194
|
-
await (0, scaffold_1.writeScaffoldFiles)(projectDir, buildTemplateFiles(projectName, expoVersion, backend));
|
|
270
|
+
await (0, scaffold_1.createProjectDirectories)(projectDir, backend, instantAuthMode);
|
|
271
|
+
await (0, scaffold_1.writeScaffoldFiles)(projectDir, buildTemplateFiles(projectName, expoVersion, backend, instantAuthMode));
|
|
195
272
|
await (0, scaffold_1.writeDefaultAssetFiles)(projectDir);
|
|
196
273
|
structureSpinner.stop("Project files created.");
|
|
197
274
|
currentStep++;
|
|
@@ -203,7 +280,19 @@ async function cmdNew(rawArgs) {
|
|
|
203
280
|
else {
|
|
204
281
|
prompts_1.log.step(`Configuring InstantDB ${stepLabel()}`);
|
|
205
282
|
await (0, steps_1.writeInstantEnvironment)(projectDir, instant);
|
|
206
|
-
if (instant
|
|
283
|
+
if (instant && (0, scaffold_1.isMagicCodeInstantAuthMode)(instant.authMode)) {
|
|
284
|
+
if (instant.appId) {
|
|
285
|
+
prompts_1.log.success((0, scaffold_1.isCloudflareInstantAuthMode)(instant.authMode)
|
|
286
|
+
? "InstantDB magic code and Cloudflare environment configured."
|
|
287
|
+
: "InstantDB magic code environment configured.");
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
prompts_1.log.info(picocolors_1.default.dim("Set EXPO_PUBLIC_INSTANT_APP_ID in .env.local."));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (instant?.authMode === "clerk" &&
|
|
294
|
+
instant.appId &&
|
|
295
|
+
instant.clerkPublishableKey) {
|
|
207
296
|
prompts_1.log.success("InstantDB and Clerk environment configured.");
|
|
208
297
|
}
|
|
209
298
|
else {
|
|
@@ -222,7 +311,7 @@ async function cmdNew(rawArgs) {
|
|
|
222
311
|
}
|
|
223
312
|
currentStep++;
|
|
224
313
|
prompts_1.log.step(`Installing Expo dependencies ${stepLabel()}`);
|
|
225
|
-
const resolved = await (0, steps_1.resolveExpoDependencies)(projectDir, backend);
|
|
314
|
+
const resolved = await (0, steps_1.resolveExpoDependencies)(projectDir, backend, instantAuthMode);
|
|
226
315
|
if (resolved) {
|
|
227
316
|
prompts_1.log.success("Expo dependencies resolved.");
|
|
228
317
|
}
|
|
@@ -272,7 +361,7 @@ async function cmdNew(rawArgs) {
|
|
|
272
361
|
}
|
|
273
362
|
currentStep++;
|
|
274
363
|
prompts_1.log.step(`Running health check ${stepLabel()}`);
|
|
275
|
-
const healthChecks = await (0, steps_1.runHealthChecks)(projectDir, backend, clerk.publishableKey);
|
|
364
|
+
const healthChecks = await (0, steps_1.runHealthChecks)(projectDir, backend, instantAuthMode, clerk.publishableKey);
|
|
276
365
|
for (const check of healthChecks) {
|
|
277
366
|
if (check.ok) {
|
|
278
367
|
prompts_1.log.info(` ${picocolors_1.default.green("✓")} ${check.label}`);
|
|
@@ -283,9 +372,22 @@ async function cmdNew(rawArgs) {
|
|
|
283
372
|
}
|
|
284
373
|
prompts_1.log.info("");
|
|
285
374
|
if (backend === "instantdb") {
|
|
286
|
-
const
|
|
375
|
+
const isCloudflareMode = instant?.authMode && (0, scaffold_1.isCloudflareInstantAuthMode)(instant.authMode);
|
|
376
|
+
const isMagicCodeMode = instant?.authMode && (0, scaffold_1.isMagicCodeInstantAuthMode)(instant.authMode);
|
|
377
|
+
const instantReady = isMagicCodeMode
|
|
378
|
+
? Boolean(instant?.appId)
|
|
379
|
+
: Boolean(instant?.authMode === "clerk" &&
|
|
380
|
+
instant.appId &&
|
|
381
|
+
instant.clerkPublishableKey);
|
|
287
382
|
if (instantReady) {
|
|
288
|
-
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green(
|
|
383
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green(isCloudflareMode
|
|
384
|
+
? "Your app is ready! Deploy Cloudflare storage next."
|
|
385
|
+
: "Your app is ready!")));
|
|
386
|
+
}
|
|
387
|
+
else if (isMagicCodeMode) {
|
|
388
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green(isCloudflareMode
|
|
389
|
+
? "Project scaffolded with InstantDB magic code + Cloudflare!"
|
|
390
|
+
: "Project scaffolded with InstantDB magic code!")));
|
|
289
391
|
}
|
|
290
392
|
else {
|
|
291
393
|
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded with InstantDB!")));
|
|
@@ -300,18 +402,35 @@ async function cmdNew(rawArgs) {
|
|
|
300
402
|
prompts_1.log.info(` ${picocolors_1.default.cyan("bun install")} ${picocolors_1.default.dim("# or npm install")}`);
|
|
301
403
|
prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
|
|
302
404
|
}
|
|
303
|
-
if (
|
|
405
|
+
if (isCloudflareMode) {
|
|
304
406
|
prompts_1.log.info("");
|
|
305
|
-
prompts_1.log.info(picocolors_1.default.dim("
|
|
306
|
-
prompts_1.log.info(picocolors_1.default.dim("
|
|
307
|
-
|
|
308
|
-
prompts_1.log.info(picocolors_1.default.
|
|
407
|
+
prompts_1.log.info(picocolors_1.default.dim(" Cloudflare setup:"));
|
|
408
|
+
prompts_1.log.info(picocolors_1.default.dim(" INSTANT_APP_ID is written to .env.local for Alchemy deploys."));
|
|
409
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("bun run cloudflare:configure")}`);
|
|
410
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("bun run cloudflare:login")}`);
|
|
411
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("bun run cloudflare:deploy")}`);
|
|
412
|
+
prompts_1.log.info(picocolors_1.default.dim(" Add the printed storageWorkerUrl to EXPO_PUBLIC_STORAGE_WORKER_URL in .env.local."));
|
|
309
413
|
}
|
|
310
|
-
|
|
414
|
+
if (isMagicCodeMode) {
|
|
415
|
+
if (!instantReady) {
|
|
416
|
+
prompts_1.log.info("");
|
|
417
|
+
prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID to .env.local."));
|
|
418
|
+
prompts_1.log.info(picocolors_1.default.dim(" Create one with: npx instant-cli init-without-files --title " +
|
|
419
|
+
projectName));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else if (instant?.authMode === "clerk" && instantReady) {
|
|
311
423
|
prompts_1.log.info("");
|
|
312
424
|
prompts_1.log.info(picocolors_1.default.dim(" In Instant Dashboard → Auth tab, add a Clerk auth client using your publishable key."));
|
|
313
425
|
prompts_1.log.info(picocolors_1.default.dim(` Use client name "${instant.clerkClientName}" (EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME).`));
|
|
314
426
|
}
|
|
427
|
+
else {
|
|
428
|
+
prompts_1.log.info("");
|
|
429
|
+
prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID and EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to .env.local."));
|
|
430
|
+
prompts_1.log.info(picocolors_1.default.dim(" Create one with: npx instant-cli init-without-files --title " +
|
|
431
|
+
projectName));
|
|
432
|
+
prompts_1.log.info(picocolors_1.default.dim(" In Instant Auth tab, add your Clerk app and set EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME."));
|
|
433
|
+
}
|
|
315
434
|
}
|
|
316
435
|
else if (clerk.publishableKey && clerk.domain) {
|
|
317
436
|
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Your app is ready!")));
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.alchemyRunTemplate = alchemyRunTemplate;
|
|
4
|
+
function alchemyRunTemplate(projectName) {
|
|
5
|
+
const serializedProjectName = JSON.stringify(projectName);
|
|
6
|
+
return `import { readFileSync } from "node:fs";
|
|
7
|
+
import alchemy from "alchemy";
|
|
8
|
+
import { D1Database, R2Bucket, Worker } from "alchemy/cloudflare";
|
|
9
|
+
|
|
10
|
+
const appName = ${serializedProjectName};
|
|
11
|
+
const stage = process.env.ALCHEMY_STAGE ?? "dev";
|
|
12
|
+
const resourceName = (suffix: string): string =>
|
|
13
|
+
[appName, stage, suffix].join("-");
|
|
14
|
+
const localEnv = readLocalEnvFile(".env.local");
|
|
15
|
+
const instantAppId =
|
|
16
|
+
process.env.INSTANT_APP_ID ??
|
|
17
|
+
process.env.EXPO_PUBLIC_INSTANT_APP_ID ??
|
|
18
|
+
localEnv.INSTANT_APP_ID ??
|
|
19
|
+
localEnv.EXPO_PUBLIC_INSTANT_APP_ID;
|
|
20
|
+
const instantAdminToken =
|
|
21
|
+
process.env.INSTANT_ADMIN_TOKEN ?? localEnv.INSTANT_ADMIN_TOKEN;
|
|
22
|
+
const allowedOrigins =
|
|
23
|
+
process.env.ALLOWED_ORIGINS ??
|
|
24
|
+
localEnv.ALLOWED_ORIGINS ??
|
|
25
|
+
"http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:19006";
|
|
26
|
+
const maxUploadBytes =
|
|
27
|
+
process.env.MAX_UPLOAD_BYTES ?? localEnv.MAX_UPLOAD_BYTES ?? "26214400";
|
|
28
|
+
const userStorageLimitBytes =
|
|
29
|
+
process.env.USER_STORAGE_LIMIT_BYTES ??
|
|
30
|
+
localEnv.USER_STORAGE_LIMIT_BYTES ??
|
|
31
|
+
"524288000";
|
|
32
|
+
const dailyUploadLimit =
|
|
33
|
+
process.env.DAILY_UPLOAD_LIMIT ?? localEnv.DAILY_UPLOAD_LIMIT ?? "100";
|
|
34
|
+
const allowedContentTypes =
|
|
35
|
+
process.env.ALLOWED_CONTENT_TYPES ??
|
|
36
|
+
localEnv.ALLOWED_CONTENT_TYPES ??
|
|
37
|
+
"image/jpeg,image/png,image/webp,application/pdf,text/plain";
|
|
38
|
+
|
|
39
|
+
interface FileSystemError extends Error {
|
|
40
|
+
code?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readLocalEnvFile(path: string): Record<string, string> {
|
|
44
|
+
try {
|
|
45
|
+
const contents = readFileSync(path, "utf8");
|
|
46
|
+
const entries: [string, string][] = contents
|
|
47
|
+
.split(/\\r?\\n/)
|
|
48
|
+
.map((line) => line.trim())
|
|
49
|
+
.filter((line) => line && !line.startsWith("#"))
|
|
50
|
+
.map((line) => {
|
|
51
|
+
const [key, ...valueParts] = line.split("=");
|
|
52
|
+
return [key.trim(), valueParts.join("=").trim()] as [string, string];
|
|
53
|
+
})
|
|
54
|
+
.filter(([key]) => key.length > 0);
|
|
55
|
+
|
|
56
|
+
return Object.fromEntries(
|
|
57
|
+
entries,
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (isFileSystemError(error) && error.code === "ENOENT") {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isFileSystemError(error: unknown): error is FileSystemError {
|
|
68
|
+
return error instanceof Error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!instantAppId) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Set INSTANT_APP_ID or EXPO_PUBLIC_INSTANT_APP_ID before deploying Cloudflare storage.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!instantAdminToken) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Set INSTANT_ADMIN_TOKEN before deploying Cloudflare storage so the Worker can sync metadata to InstantDB.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const app = await alchemy(appName, {
|
|
84
|
+
stage,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const storageBucket = await R2Bucket("storage", {
|
|
88
|
+
name: resourceName("storage"),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const metadataDatabase = await D1Database("metadata", {
|
|
92
|
+
name: resourceName("metadata"),
|
|
93
|
+
migrationsDir: "./worker/migrations",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const storageApi = await Worker("storage-api", {
|
|
97
|
+
name: resourceName("storage-api"),
|
|
98
|
+
entrypoint: "./worker/src/worker.ts",
|
|
99
|
+
compatibilityDate: "2026-04-24",
|
|
100
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
101
|
+
url: true,
|
|
102
|
+
bindings: {
|
|
103
|
+
STORAGE: storageBucket,
|
|
104
|
+
DB: metadataDatabase,
|
|
105
|
+
INSTANT_APP_ID: instantAppId,
|
|
106
|
+
INSTANT_ADMIN_TOKEN: instantAdminToken,
|
|
107
|
+
ALLOWED_ORIGINS: allowedOrigins,
|
|
108
|
+
MAX_UPLOAD_BYTES: maxUploadBytes,
|
|
109
|
+
USER_STORAGE_LIMIT_BYTES: userStorageLimitBytes,
|
|
110
|
+
DAILY_UPLOAD_LIMIT: dailyUploadLimit,
|
|
111
|
+
ALLOWED_CONTENT_TYPES: allowedContentTypes,
|
|
112
|
+
},
|
|
113
|
+
observability: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log({ storageWorkerUrl: storageApi.url });
|
|
119
|
+
|
|
120
|
+
await app.finalize();
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cloudflareStorageTemplate = cloudflareStorageTemplate;
|
|
4
|
+
function cloudflareStorageTemplate() {
|
|
5
|
+
return `import { instantDb } from "@/lib/instant";
|
|
6
|
+
|
|
7
|
+
const storageWorkerUrl = process.env.EXPO_PUBLIC_STORAGE_WORKER_URL;
|
|
8
|
+
|
|
9
|
+
export interface CloudflareUpload {
|
|
10
|
+
id: string;
|
|
11
|
+
key: string;
|
|
12
|
+
ownerId: string;
|
|
13
|
+
fileName: string;
|
|
14
|
+
contentType: string;
|
|
15
|
+
size: number;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
url: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UploadCloudflareObjectInput {
|
|
21
|
+
body: Blob | ArrayBuffer | Uint8Array;
|
|
22
|
+
contentType: string;
|
|
23
|
+
fileName: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SyncCloudflareUploadInput {
|
|
27
|
+
upload: CloudflareUpload;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isCloudflareStorageConfigured(): boolean {
|
|
31
|
+
return Boolean(storageWorkerUrl);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireStorageWorkerUrl(): string {
|
|
35
|
+
if (!storageWorkerUrl) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Missing EXPO_PUBLIC_STORAGE_WORKER_URL. Deploy the Cloudflare Worker and add its URL to .env.local.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return storageWorkerUrl;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateUploadInput(input: UploadCloudflareObjectInput): void {
|
|
44
|
+
if (!input.fileName.trim()) {
|
|
45
|
+
throw new Error("fileName is required to upload a Cloudflare object.");
|
|
46
|
+
}
|
|
47
|
+
if (!input.contentType.trim()) {
|
|
48
|
+
throw new Error("contentType is required to upload a Cloudflare object.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readErrorResponse(response: Response): Promise<string> {
|
|
53
|
+
const responseBody = await response.text();
|
|
54
|
+
return \`Cloudflare storage request failed. status=\${response.status} body=\${responseBody}\`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function requireInstantRefreshToken(): Promise<string> {
|
|
58
|
+
if (!instantDb) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"InstantDB is not configured. Add EXPO_PUBLIC_INSTANT_APP_ID before using Cloudflare storage.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const user = await instantDb.getAuth();
|
|
65
|
+
const token = user?.refresh_token;
|
|
66
|
+
if (!token) {
|
|
67
|
+
throw new Error("Sign in with InstantDB before using Cloudflare storage.");
|
|
68
|
+
}
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function uploadCloudflareObject(
|
|
73
|
+
input: UploadCloudflareObjectInput,
|
|
74
|
+
): Promise<CloudflareUpload> {
|
|
75
|
+
validateUploadInput(input);
|
|
76
|
+
const workerUrl = requireStorageWorkerUrl();
|
|
77
|
+
const token = await requireInstantRefreshToken();
|
|
78
|
+
const response = await fetch(\`\${workerUrl}/uploads\`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
authorization: \`Bearer \${token}\`,
|
|
82
|
+
"content-type": input.contentType,
|
|
83
|
+
"x-file-name": input.fileName,
|
|
84
|
+
},
|
|
85
|
+
body: input.body,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(await readErrorResponse(response));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (await response.json()) as CloudflareUpload;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function syncCloudflareUploadToInstant(
|
|
96
|
+
input: SyncCloudflareUploadInput,
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
if (!instantDb) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"InstantDB is not configured. Add EXPO_PUBLIC_INSTANT_APP_ID before reading synced Cloudflare uploads.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
requireStorageWorkerUrl();
|
|
105
|
+
return input.upload.id;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instantCloudflareLibTemplate = instantCloudflareLibTemplate;
|
|
4
|
+
function instantCloudflareLibTemplate() {
|
|
5
|
+
return `import "react-native-get-random-values";
|
|
6
|
+
import { init, i, id, type InstaQLEntity } from "@instantdb/react-native";
|
|
7
|
+
|
|
8
|
+
const appId = process.env.EXPO_PUBLIC_INSTANT_APP_ID;
|
|
9
|
+
|
|
10
|
+
export const schema = i.schema({
|
|
11
|
+
entities: {
|
|
12
|
+
todos: i.entity({
|
|
13
|
+
text: i.string(),
|
|
14
|
+
completed: i.boolean(),
|
|
15
|
+
createdAt: i.number().indexed(),
|
|
16
|
+
}),
|
|
17
|
+
cloudflareObjects: i.entity({
|
|
18
|
+
uploadId: i.string().indexed(),
|
|
19
|
+
key: i.string(),
|
|
20
|
+
ownerId: i.string().indexed(),
|
|
21
|
+
fileName: i.string(),
|
|
22
|
+
contentType: i.string(),
|
|
23
|
+
size: i.number(),
|
|
24
|
+
workerUrl: i.string(),
|
|
25
|
+
createdAt: i.number().indexed(),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type InstantTodo = InstaQLEntity<typeof schema, "todos">;
|
|
31
|
+
export type InstantCloudflareObject = InstaQLEntity<
|
|
32
|
+
typeof schema,
|
|
33
|
+
"cloudflareObjects"
|
|
34
|
+
>;
|
|
35
|
+
export const createId = id;
|
|
36
|
+
|
|
37
|
+
export const isInstantConfigured = Boolean(appId);
|
|
38
|
+
export const instantDb = isInstantConfigured
|
|
39
|
+
? init({ appId: appId as string, schema })
|
|
40
|
+
: null;
|
|
41
|
+
`;
|
|
42
|
+
}
|