@holo-js/cli 0.1.9 → 0.2.0
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/bin/holo.mjs +165 -81
- package/dist/broadcast-WI6PJS5P.mjs +203 -0
- package/dist/broadcast-YWS4N5QU.mjs +203 -0
- package/dist/{cache-ETOIQ5IG.mjs → cache-KWNQECAA.mjs} +6 -6
- package/dist/cache-QARFSW4F.mjs +66 -0
- package/dist/{cache-migrations-2GGI4TJK.mjs → cache-migrations-3OXR4FN5.mjs} +50 -30
- package/dist/cache-migrations-MDFMDVTK.mjs +173 -0
- package/dist/{chunk-IMOGEKB4.mjs → chunk-2DKQKZML.mjs} +188 -106
- package/dist/{chunk-7JR73TOH.mjs → chunk-2RGJTPYF.mjs} +36 -25
- package/dist/{chunk-ASTSSSL2.mjs → chunk-EWYXSN2C.mjs} +75 -122
- package/dist/{chunk-F4MT6GBK.mjs → chunk-FGQ2I2YH.mjs} +1 -1
- package/dist/chunk-I7QBCEV7.mjs +33 -0
- package/dist/{chunk-R6BWRY3E.mjs → chunk-ILU426CF.mjs} +3 -1
- package/dist/{chunk-HB4Q7VYK.mjs → chunk-IUDD5FYL.mjs} +28 -273
- package/dist/{chunk-WRZFATUT.mjs → chunk-KWRIBHC3.mjs} +229 -142
- package/dist/{chunk-57SJ566R.mjs → chunk-LBJAJLKU.mjs} +1 -1
- package/dist/{chunk-BAFQ2GOA.mjs → chunk-LXGQCG56.mjs} +1 -1
- package/dist/{chunk-SRPGIWCF.mjs → chunk-ONKESAQA.mjs} +2 -2
- package/dist/chunk-QA7TP5EO.mjs +448 -0
- package/dist/chunk-UPZH6KCF.mjs +3306 -0
- package/dist/{chunk-5EU32E7X.mjs → chunk-VRGB6DIS.mjs} +116 -12
- package/dist/{config-ARLE6PKR.mjs → config-TWEO2R4N.mjs} +3 -3
- package/dist/{dev-6RG5SSZ7.mjs → dev-2OULECTU.mjs} +7 -7
- package/dist/dev-PJMEGTAC.mjs +42 -0
- package/dist/{discovery-FCVGQQVD.mjs → discovery-7FXND7Y6.mjs} +3 -3
- package/dist/{generators-UI2LJK3O.mjs → generators-4BP7B47W.mjs} +11 -34
- package/dist/generators-Z4XLSMC7.mjs +520 -0
- package/dist/index.mjs +167 -83
- package/dist/{media-migrations-JQSDCC7S.mjs → media-migrations-BFEL7NFG.mjs} +9 -20
- package/dist/media-migrations-VR7DLLR6.mjs +106 -0
- package/dist/{queue-BY3PLH4I.mjs → queue-SVOJPTRO.mjs} +10 -10
- package/dist/queue-YCBQTCYI.mjs +625 -0
- package/dist/{queue-migrations-YZUKEZK7.mjs → queue-migrations-HPXOO3NA.mjs} +13 -12
- package/dist/queue-migrations-X4P7FZKJ.mjs +167 -0
- package/dist/{runtime-BI343WHS.mjs → runtime-CPKR663Y.mjs} +9 -9
- package/dist/runtime-GIE56H47.mjs +57 -0
- package/dist/{runtime-ZKD6URAV.mjs → runtime-GSXF4NB3.mjs} +1 -1
- package/dist/runtime-worker.d.ts +2 -0
- package/dist/runtime-worker.mjs +242 -0
- package/dist/{scaffold-UBOS2NZR.mjs → scaffold-3QPGYQEQ.mjs} +9 -5
- package/dist/scaffold-RGAAHC6I.mjs +139 -0
- package/dist/{security-TYPVOYGF.mjs → security-7H5TNHZY.mjs} +6 -6
- package/dist/security-BZGD6ONY.mjs +71 -0
- package/package.json +9 -7
- package/dist/broadcast-VR46UZEL.mjs +0 -84
- package/dist/chunk-ZXDU7RHU.mjs +0 -9
|
@@ -0,0 +1,3306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadProjectConfig,
|
|
3
|
+
resolveGeneratedSchemaPath
|
|
4
|
+
} from "./chunk-ONKESAQA.mjs";
|
|
5
|
+
import {
|
|
6
|
+
loadGeneratedProjectRegistry,
|
|
7
|
+
relativeImportPath,
|
|
8
|
+
renderAuthProviderRouteFiles,
|
|
9
|
+
renderAuthRouteFiles,
|
|
10
|
+
renderFrameworkFiles,
|
|
11
|
+
renderFrameworkRunner,
|
|
12
|
+
renderGeneratedModelTypes,
|
|
13
|
+
renderNextBroadcastAuthRoute,
|
|
14
|
+
renderNextBroadcastConfigRoute,
|
|
15
|
+
renderNextGeneratedBroadcastAuthRoute,
|
|
16
|
+
renderNextGeneratedBroadcastConfigRoute,
|
|
17
|
+
renderNextGeneratedRealtimeDefinitions,
|
|
18
|
+
renderNextHoloHelper,
|
|
19
|
+
renderNextManagedHostedAuthRouteFiles,
|
|
20
|
+
renderNextRuntimeBootstrap,
|
|
21
|
+
renderSvelteHoloHelper,
|
|
22
|
+
renderSvelteViteConfig
|
|
23
|
+
} from "./chunk-2DKQKZML.mjs";
|
|
24
|
+
import {
|
|
25
|
+
AUTH_CONFIG_FILE_NAMES,
|
|
26
|
+
AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES,
|
|
27
|
+
BROADCAST_CONFIG_FILE_NAMES,
|
|
28
|
+
CACHE_CONFIG_FILE_NAMES,
|
|
29
|
+
CORS_CONFIG_FILE_NAMES,
|
|
30
|
+
DB_DRIVER_PACKAGE_NAMES,
|
|
31
|
+
GENERATED_MODEL_TYPES_PATH,
|
|
32
|
+
MAIL_CONFIG_FILE_NAMES,
|
|
33
|
+
MEDIA_CONFIG_FILE_NAMES,
|
|
34
|
+
NOTIFICATIONS_CONFIG_FILE_NAMES,
|
|
35
|
+
QUEUE_CONFIG_FILE_NAMES,
|
|
36
|
+
REDIS_CONFIG_FILE_NAMES,
|
|
37
|
+
SECURITY_CONFIG_FILE_NAMES,
|
|
38
|
+
SESSION_CONFIG_FILE_NAMES,
|
|
39
|
+
SUPPORTED_AUTH_SOCIAL_PROVIDERS,
|
|
40
|
+
isSupportedCacheInstallerDriver,
|
|
41
|
+
isSupportedQueueInstallerDriver,
|
|
42
|
+
normalizeScaffoldOptionalPackages,
|
|
43
|
+
pathExists,
|
|
44
|
+
readTextFile,
|
|
45
|
+
resolveFirstExistingPath,
|
|
46
|
+
sanitizePackageName,
|
|
47
|
+
writeTextFile
|
|
48
|
+
} from "./chunk-ILU426CF.mjs";
|
|
49
|
+
|
|
50
|
+
// src/project/scaffold.ts
|
|
51
|
+
import { mkdir as mkdir3, readdir as readdir2 } from "fs/promises";
|
|
52
|
+
import { dirname as dirname2, extname as extname2, relative, resolve as resolve4, sep } from "path";
|
|
53
|
+
import { loadConfigDirectory as loadConfigDirectory2 } from "@holo-js/config";
|
|
54
|
+
|
|
55
|
+
// src/project/scaffold/config-renderers.ts
|
|
56
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
57
|
+
import { extname, resolve as resolve2 } from "path";
|
|
58
|
+
import { holoStorageDefaults } from "@holo-js/config";
|
|
59
|
+
|
|
60
|
+
// src/project/scaffold/dependencies.ts
|
|
61
|
+
import { resolve } from "path";
|
|
62
|
+
import { loadConfigDirectory } from "@holo-js/config";
|
|
63
|
+
|
|
64
|
+
// package.json
|
|
65
|
+
var package_default = {
|
|
66
|
+
name: "@holo-js/cli",
|
|
67
|
+
version: "0.2.0",
|
|
68
|
+
description: "Holo-JS Framework - project creation, discovery, and operational CLI",
|
|
69
|
+
type: "module",
|
|
70
|
+
license: "MIT",
|
|
71
|
+
bin: {
|
|
72
|
+
holo: "./dist/bin/holo.mjs",
|
|
73
|
+
"holo-js": "./dist/bin/holo.mjs"
|
|
74
|
+
},
|
|
75
|
+
exports: {
|
|
76
|
+
".": {
|
|
77
|
+
types: "./dist/index.d.ts",
|
|
78
|
+
import: "./dist/index.mjs"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
main: "./dist/index.mjs",
|
|
82
|
+
types: "./dist/index.d.ts",
|
|
83
|
+
files: [
|
|
84
|
+
"dist"
|
|
85
|
+
],
|
|
86
|
+
scripts: {
|
|
87
|
+
build: "node ../../scripts/generate-cli-workspace-catalog.mjs && tsup",
|
|
88
|
+
stub: "node ../../scripts/generate-cli-workspace-catalog.mjs && tsup --watch",
|
|
89
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
90
|
+
test: "vitest --run && HOLO_CLI_INCLUDE_INTEGRATION=1 vitest --run tests/cli.test.ts",
|
|
91
|
+
"test:integration": "HOLO_CLI_INCLUDE_INTEGRATION=1 vitest --run tests/cli.test.ts"
|
|
92
|
+
},
|
|
93
|
+
dependencies: {
|
|
94
|
+
"@clack/prompts": "catalog:",
|
|
95
|
+
"@holo-js/cache-db": "catalog:",
|
|
96
|
+
"@holo-js/config": "catalog:",
|
|
97
|
+
"@holo-js/core": "catalog:",
|
|
98
|
+
"@holo-js/db": "catalog:",
|
|
99
|
+
esbuild: "catalog:",
|
|
100
|
+
inflection: "catalog:"
|
|
101
|
+
},
|
|
102
|
+
devDependencies: {
|
|
103
|
+
"@holo-js/events": "catalog:",
|
|
104
|
+
"@holo-js/queue": "catalog:",
|
|
105
|
+
"@holo-js/queue-db": "catalog:",
|
|
106
|
+
"@types/node": "catalog:",
|
|
107
|
+
tsup: "catalog:",
|
|
108
|
+
typescript: "catalog:",
|
|
109
|
+
vitest: "catalog:"
|
|
110
|
+
},
|
|
111
|
+
engines: {
|
|
112
|
+
node: ">=20.11.0"
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// src/generated/workspaceCatalog.ts
|
|
117
|
+
var WORKSPACE_CATALOG = Object.freeze({
|
|
118
|
+
"@clerk/backend": "^3.4.7",
|
|
119
|
+
"@clack/prompts": "^1.5.1",
|
|
120
|
+
"@eslint/js": "^9.17.0",
|
|
121
|
+
"@holo-js/adapter-next": "^0.2.0",
|
|
122
|
+
"@holo-js/adapter-nuxt": "^0.2.0",
|
|
123
|
+
"@holo-js/adapter-sveltekit": "^0.2.0",
|
|
124
|
+
"@holo-js/auth": "^0.2.0",
|
|
125
|
+
"@holo-js/auth-clerk": "^0.2.0",
|
|
126
|
+
"@holo-js/auth-social": "^0.2.0",
|
|
127
|
+
"@holo-js/auth-social-apple": "^0.2.0",
|
|
128
|
+
"@holo-js/auth-social-discord": "^0.2.0",
|
|
129
|
+
"@holo-js/auth-social-facebook": "^0.2.0",
|
|
130
|
+
"@holo-js/auth-social-github": "^0.2.0",
|
|
131
|
+
"@holo-js/auth-social-google": "^0.2.0",
|
|
132
|
+
"@holo-js/auth-social-linkedin": "^0.2.0",
|
|
133
|
+
"@holo-js/auth-workos": "^0.2.0",
|
|
134
|
+
"@holo-js/authorization": "^0.2.0",
|
|
135
|
+
"@holo-js/broadcast": "^0.2.0",
|
|
136
|
+
"@holo-js/cache": "^0.2.0",
|
|
137
|
+
"@holo-js/cache-db": "^0.2.0",
|
|
138
|
+
"@holo-js/cache-redis": "^0.2.0",
|
|
139
|
+
"@holo-js/cli": "^0.2.0",
|
|
140
|
+
"@holo-js/config": "^0.2.0",
|
|
141
|
+
"@holo-js/core": "^0.2.0",
|
|
142
|
+
"@holo-js/db": "^0.2.0",
|
|
143
|
+
"@holo-js/db-mysql": "^0.2.0",
|
|
144
|
+
"@holo-js/db-postgres": "^0.2.0",
|
|
145
|
+
"@holo-js/db-sqlite": "^0.2.0",
|
|
146
|
+
"@holo-js/events": "^0.2.0",
|
|
147
|
+
"@holo-js/flux": "^0.2.0",
|
|
148
|
+
"@holo-js/flux-react": "^0.2.0",
|
|
149
|
+
"@holo-js/flux-svelte": "^0.2.0",
|
|
150
|
+
"@holo-js/flux-vue": "^0.2.0",
|
|
151
|
+
"@holo-js/forms": "^0.2.0",
|
|
152
|
+
"@holo-js/mail": "^0.2.0",
|
|
153
|
+
"@holo-js/media": "^0.2.0",
|
|
154
|
+
"@holo-js/notifications": "^0.2.0",
|
|
155
|
+
"@holo-js/queue": "^0.2.0",
|
|
156
|
+
"@holo-js/queue-db": "^0.2.0",
|
|
157
|
+
"@holo-js/queue-redis": "^0.2.0",
|
|
158
|
+
"@holo-js/realtime": "^0.2.0",
|
|
159
|
+
"@holo-js/security": "^0.2.0",
|
|
160
|
+
"@holo-js/session": "^0.2.0",
|
|
161
|
+
"@holo-js/storage": "^0.2.0",
|
|
162
|
+
"@holo-js/storage-s3": "^0.2.0",
|
|
163
|
+
"@holo-js/validation": "^0.2.0",
|
|
164
|
+
"@nuxt/kit": "^4.4.4",
|
|
165
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
166
|
+
"@sveltejs/adapter-node": "^5.5.4",
|
|
167
|
+
"@sveltejs/kit": "^2.59.1",
|
|
168
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.0",
|
|
169
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
170
|
+
"@types/node": "^22.10.2",
|
|
171
|
+
"@types/pg": "^8.11.0",
|
|
172
|
+
"@types/react": "^19.2.14",
|
|
173
|
+
"@types/react-dom": "^19.2.3",
|
|
174
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
175
|
+
"@types/ws": "^8.18.1",
|
|
176
|
+
"@vitest/coverage-istanbul": "^4.1.5",
|
|
177
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
178
|
+
"better-sqlite3": "^11.7.0",
|
|
179
|
+
"bullmq": "^5.71.0",
|
|
180
|
+
"create-holo-js": "^0.2.0",
|
|
181
|
+
"esbuild": "^0.27.4",
|
|
182
|
+
"eslint": "^9.17.0",
|
|
183
|
+
"fast-check": "^4.5.3",
|
|
184
|
+
"globals": "^15.14.0",
|
|
185
|
+
"h3": "^1.15.11",
|
|
186
|
+
"inflection": "^3.0.2",
|
|
187
|
+
"ioredis": "^5.4.2",
|
|
188
|
+
"mysql2": "^3.17.1",
|
|
189
|
+
"next": "^16.2.4",
|
|
190
|
+
"nodemailer": "^6.10.1",
|
|
191
|
+
"nuxt": "^4.4.4",
|
|
192
|
+
"pg": "^8.13.0",
|
|
193
|
+
"playwright": "^1.57.0",
|
|
194
|
+
"react": "^19.2.6",
|
|
195
|
+
"react-dom": "^19.2.6",
|
|
196
|
+
"react-test-renderer": "^19.2.6",
|
|
197
|
+
"sharp": "^0.34.4",
|
|
198
|
+
"svelte": "^5.55.5",
|
|
199
|
+
"svelte-check": "^4.4.6",
|
|
200
|
+
"tslib": "^2.8.1",
|
|
201
|
+
"tsup": "^8.3.5",
|
|
202
|
+
"typescript": "^5.7.2",
|
|
203
|
+
"typescript-eslint": "^8.30.1",
|
|
204
|
+
"ulid": "^3.0.1",
|
|
205
|
+
"uuid": "^12.0.0",
|
|
206
|
+
"valibot": "^1.1.0",
|
|
207
|
+
"vite": "^8.0.10",
|
|
208
|
+
"vitepress": "^1.6.3",
|
|
209
|
+
"vitest": "^4.1.5",
|
|
210
|
+
"vue": "^3.5.13",
|
|
211
|
+
"vue-router": "^5.0.4",
|
|
212
|
+
"vue-tsc": "^2.2.0",
|
|
213
|
+
"ws": "^8.18.3"
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// src/metadata.ts
|
|
217
|
+
var HOLO_PACKAGE_VERSION = package_default.version;
|
|
218
|
+
var HOLO_PACKAGE_RANGE = `^${HOLO_PACKAGE_VERSION}`;
|
|
219
|
+
function catalogVersion(packageName) {
|
|
220
|
+
return WORKSPACE_CATALOG[packageName];
|
|
221
|
+
}
|
|
222
|
+
var ESBUILD_PACKAGE_VERSION = catalogVersion("esbuild");
|
|
223
|
+
var SCAFFOLD_PACKAGE_MANAGER_VERSIONS = Object.freeze({
|
|
224
|
+
npm: "npm@latest",
|
|
225
|
+
pnpm: "pnpm@latest",
|
|
226
|
+
yarn: "yarn@stable",
|
|
227
|
+
bun: "bun@1.3.9"
|
|
228
|
+
});
|
|
229
|
+
var SCAFFOLD_FRAMEWORK_VERSIONS = Object.freeze({
|
|
230
|
+
nuxt: catalogVersion("nuxt"),
|
|
231
|
+
next: catalogVersion("next"),
|
|
232
|
+
sveltekit: catalogVersion("@sveltejs/kit")
|
|
233
|
+
});
|
|
234
|
+
var SCAFFOLD_NEXT_REACT_VERSIONS = Object.freeze({
|
|
235
|
+
react: catalogVersion("react"),
|
|
236
|
+
"react-dom": catalogVersion("react-dom"),
|
|
237
|
+
"@types/react": catalogVersion("@types/react"),
|
|
238
|
+
"@types/react-dom": catalogVersion("@types/react-dom")
|
|
239
|
+
});
|
|
240
|
+
var SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS = Object.freeze({
|
|
241
|
+
typescript: catalogVersion("typescript"),
|
|
242
|
+
"@types/node": catalogVersion("@types/node"),
|
|
243
|
+
eslint: catalogVersion("eslint")
|
|
244
|
+
});
|
|
245
|
+
var SCAFFOLD_NUXT_DEPENDENCY_VERSIONS = Object.freeze({
|
|
246
|
+
vue: catalogVersion("vue"),
|
|
247
|
+
"vue-router": catalogVersion("vue-router"),
|
|
248
|
+
"vue-tsc": catalogVersion("vue-tsc")
|
|
249
|
+
});
|
|
250
|
+
var SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS = Object.freeze({
|
|
251
|
+
"@sveltejs/adapter-node": catalogVersion("@sveltejs/adapter-node"),
|
|
252
|
+
"@sveltejs/vite-plugin-svelte": catalogVersion("@sveltejs/vite-plugin-svelte"),
|
|
253
|
+
svelte: catalogVersion("svelte"),
|
|
254
|
+
"svelte-check": catalogVersion("svelte-check"),
|
|
255
|
+
vite: catalogVersion("vite")
|
|
256
|
+
});
|
|
257
|
+
var IOREDIS_PACKAGE_VERSION = catalogVersion("ioredis");
|
|
258
|
+
var SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS = Object.freeze({
|
|
259
|
+
nuxt: HOLO_PACKAGE_RANGE,
|
|
260
|
+
next: HOLO_PACKAGE_RANGE,
|
|
261
|
+
sveltekit: HOLO_PACKAGE_RANGE
|
|
262
|
+
});
|
|
263
|
+
var SCAFFOLD_FRAMEWORK_RUNTIME_VERSIONS = Object.freeze({
|
|
264
|
+
nuxt: {
|
|
265
|
+
"@holo-js/storage": HOLO_PACKAGE_RANGE
|
|
266
|
+
},
|
|
267
|
+
next: {
|
|
268
|
+
"@holo-js/storage": HOLO_PACKAGE_RANGE
|
|
269
|
+
},
|
|
270
|
+
sveltekit: {
|
|
271
|
+
"@holo-js/storage": HOLO_PACKAGE_RANGE
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// src/project/scaffold/dependencies.ts
|
|
276
|
+
function normalizeDependencyMap(value) {
|
|
277
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
return Object.fromEntries(
|
|
281
|
+
Object.entries(value).filter(([, dependencyVersion]) => typeof dependencyVersion === "string").sort(([left], [right]) => left.localeCompare(right))
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
function isWorkspaceDependencyVersion(value) {
|
|
285
|
+
return typeof value === "string" && value.startsWith("workspace:");
|
|
286
|
+
}
|
|
287
|
+
function resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies) {
|
|
288
|
+
const currentPackageVersion = dependencies[packageName] ?? devDependencies[packageName];
|
|
289
|
+
if (isWorkspaceDependencyVersion(currentPackageVersion)) {
|
|
290
|
+
return currentPackageVersion;
|
|
291
|
+
}
|
|
292
|
+
const workspaceVersion = Object.entries({
|
|
293
|
+
...dependencies,
|
|
294
|
+
...devDependencies
|
|
295
|
+
}).find(([dependencyName, dependencyVersion]) => dependencyName.startsWith("@holo-js/") && isWorkspaceDependencyVersion(dependencyVersion))?.[1];
|
|
296
|
+
return workspaceVersion ?? `^${HOLO_PACKAGE_VERSION}`;
|
|
297
|
+
}
|
|
298
|
+
async function readPackageJsonDependencyState(projectRoot) {
|
|
299
|
+
const packageJsonPath = resolve(projectRoot, "package.json");
|
|
300
|
+
const existing = await readTextFile(packageJsonPath);
|
|
301
|
+
if (!existing) {
|
|
302
|
+
throw new Error(`Missing package.json in ${projectRoot}.`);
|
|
303
|
+
}
|
|
304
|
+
let parsed;
|
|
305
|
+
try {
|
|
306
|
+
parsed = JSON.parse(existing);
|
|
307
|
+
} catch {
|
|
308
|
+
throw new Error(`Invalid package.json in ${projectRoot}.`);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
packageJsonPath,
|
|
312
|
+
parsed,
|
|
313
|
+
dependencies: normalizeDependencyMap(parsed.dependencies),
|
|
314
|
+
devDependencies: normalizeDependencyMap(parsed.devDependencies)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies) {
|
|
318
|
+
parsed.dependencies = Object.fromEntries(
|
|
319
|
+
Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right))
|
|
320
|
+
);
|
|
321
|
+
if (Object.keys(devDependencies).length > 0) {
|
|
322
|
+
parsed.devDependencies = Object.fromEntries(
|
|
323
|
+
Object.entries(devDependencies).sort(([left], [right]) => left.localeCompare(right))
|
|
324
|
+
);
|
|
325
|
+
} else {
|
|
326
|
+
delete parsed.devDependencies;
|
|
327
|
+
}
|
|
328
|
+
await writeTextFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
|
|
329
|
+
`);
|
|
330
|
+
}
|
|
331
|
+
function hasLoadedConfigFile(loadedFiles, configName) {
|
|
332
|
+
return loadedFiles.some((filePath) => {
|
|
333
|
+
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
334
|
+
return normalizedPath.endsWith(`/config/${configName}.ts`) || normalizedPath.endsWith(`/config/${configName}.mts`) || normalizedPath.endsWith(`/config/${configName}.js`) || normalizedPath.endsWith(`/config/${configName}.mjs`) || normalizedPath.endsWith(`/config/${configName}.cts`) || normalizedPath.endsWith(`/config/${configName}.cjs`);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function inferDatabaseDriverFromUrl(value) {
|
|
338
|
+
if (!value) {
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
const normalized = value.trim().toLowerCase();
|
|
342
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://")) {
|
|
343
|
+
return "postgres";
|
|
344
|
+
}
|
|
345
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql2://")) {
|
|
346
|
+
return "mysql";
|
|
347
|
+
}
|
|
348
|
+
if (normalized === ":memory:" || normalized.startsWith("file:") || normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../") || normalized.endsWith(".db") || normalized.endsWith(".sqlite") || normalized.endsWith(".sqlite3")) {
|
|
349
|
+
return "sqlite";
|
|
350
|
+
}
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
function inferConnectionDriver(connection) {
|
|
354
|
+
if (typeof connection === "string") {
|
|
355
|
+
return inferDatabaseDriverFromUrl(connection);
|
|
356
|
+
}
|
|
357
|
+
const explicitDriver = connection.driver;
|
|
358
|
+
if (explicitDriver === "sqlite" || explicitDriver === "postgres" || explicitDriver === "mysql") {
|
|
359
|
+
return explicitDriver;
|
|
360
|
+
}
|
|
361
|
+
return inferDatabaseDriverFromUrl(connection.url ?? connection.filename);
|
|
362
|
+
}
|
|
363
|
+
function registryHasJobs(registry) {
|
|
364
|
+
return (registry?.jobs.length ?? 0) > 0;
|
|
365
|
+
}
|
|
366
|
+
function registryHasEvents(registry) {
|
|
367
|
+
return (registry?.events.length ?? 0) > 0 || (registry?.listeners.length ?? 0) > 0;
|
|
368
|
+
}
|
|
369
|
+
function registryHasBroadcastDefinitions(registry) {
|
|
370
|
+
return (registry?.broadcast.length ?? 0) > 0 || (registry?.channels.length ?? 0) > 0;
|
|
371
|
+
}
|
|
372
|
+
function registryHasAuthorizationDefinitions(registry) {
|
|
373
|
+
return (registry?.authorizationPolicies.length ?? 0) > 0 || (registry?.authorizationAbilities.length ?? 0) > 0;
|
|
374
|
+
}
|
|
375
|
+
function authConfigUsesSocialProviders(loaded) {
|
|
376
|
+
return Object.keys(loaded.auth.social).length > 0;
|
|
377
|
+
}
|
|
378
|
+
function authConfigUsesWorkosProviders(loaded) {
|
|
379
|
+
return Object.entries(loaded.auth.workos).some(([name, provider]) => name !== "provider" && typeof provider === "object" && provider !== null);
|
|
380
|
+
}
|
|
381
|
+
function authConfigUsesClerkProviders(loaded) {
|
|
382
|
+
return Object.keys(loaded.auth.clerk).length > 0;
|
|
383
|
+
}
|
|
384
|
+
function mailConfigUsesQueue(loaded) {
|
|
385
|
+
return loaded.mail.queue.queued || Object.values(loaded.mail.mailers).some((mailer) => mailer.queue.queued);
|
|
386
|
+
}
|
|
387
|
+
async function projectHasAuthorizationScaffold(projectRoot) {
|
|
388
|
+
const project = await loadProjectConfig(projectRoot);
|
|
389
|
+
const policiesRoot = resolve(projectRoot, project.config.paths.authorizationPolicies ?? "server/policies");
|
|
390
|
+
const abilitiesRoot = resolve(projectRoot, project.config.paths.authorizationAbilities ?? "server/abilities");
|
|
391
|
+
return await pathExists(policiesRoot) || await pathExists(abilitiesRoot);
|
|
392
|
+
}
|
|
393
|
+
async function projectHasEventsScaffold(projectRoot) {
|
|
394
|
+
const project = await loadProjectConfig(projectRoot);
|
|
395
|
+
const eventsRoot = resolve(projectRoot, project.config.paths.events);
|
|
396
|
+
const listenersRoot = resolve(projectRoot, project.config.paths.listeners);
|
|
397
|
+
return await pathExists(eventsRoot) || await pathExists(listenersRoot);
|
|
398
|
+
}
|
|
399
|
+
async function projectHasRealtimeScaffold(projectRoot) {
|
|
400
|
+
return await pathExists(resolve(projectRoot, "server/realtime"));
|
|
401
|
+
}
|
|
402
|
+
async function syncManagedDriverDependencies(projectRoot, registry) {
|
|
403
|
+
const loaded = await loadConfigDirectory(projectRoot, {
|
|
404
|
+
preferCache: false,
|
|
405
|
+
processEnv: process.env
|
|
406
|
+
});
|
|
407
|
+
const discoveredRegistry = registry ?? await loadGeneratedProjectRegistry(projectRoot);
|
|
408
|
+
const authConfigured = hasLoadedConfigFile(loaded.loadedFiles, "auth");
|
|
409
|
+
const broadcastConfigured = hasLoadedConfigFile(loaded.loadedFiles, "broadcast");
|
|
410
|
+
const cacheConfigured = hasLoadedConfigFile(loaded.loadedFiles, "cache");
|
|
411
|
+
const mailConfigured = hasLoadedConfigFile(loaded.loadedFiles, "mail");
|
|
412
|
+
const mediaConfigured = hasLoadedConfigFile(loaded.loadedFiles, "media");
|
|
413
|
+
const notificationsConfigured = hasLoadedConfigFile(loaded.loadedFiles, "notifications");
|
|
414
|
+
const queueConfigured = hasLoadedConfigFile(loaded.loadedFiles, "queue");
|
|
415
|
+
const securityConfigured = hasLoadedConfigFile(loaded.loadedFiles, "security");
|
|
416
|
+
const sessionConfigured = hasLoadedConfigFile(loaded.loadedFiles, "session");
|
|
417
|
+
const storageConfigured = hasLoadedConfigFile(loaded.loadedFiles, "storage");
|
|
418
|
+
const requiredPackages = /* @__PURE__ */ new Set();
|
|
419
|
+
const hasAuthorizationScaffold = await projectHasAuthorizationScaffold(projectRoot);
|
|
420
|
+
const hasEventsScaffold = await projectHasEventsScaffold(projectRoot);
|
|
421
|
+
const hasRealtimeScaffold = await projectHasRealtimeScaffold(projectRoot);
|
|
422
|
+
const {
|
|
423
|
+
packageJsonPath,
|
|
424
|
+
parsed,
|
|
425
|
+
dependencies,
|
|
426
|
+
devDependencies
|
|
427
|
+
} = await readPackageJsonDependencyState(projectRoot);
|
|
428
|
+
const cachePackageInstalled = typeof dependencies["@holo-js/cache"] !== "undefined" || typeof devDependencies["@holo-js/cache"] !== "undefined";
|
|
429
|
+
const cacheDesired = cacheConfigured || cachePackageInstalled;
|
|
430
|
+
requiredPackages.add("@holo-js/core");
|
|
431
|
+
for (const connection of Object.values(loaded.database.connections)) {
|
|
432
|
+
const inferredDriver = inferConnectionDriver(connection);
|
|
433
|
+
if (inferredDriver) {
|
|
434
|
+
requiredPackages.add(DB_DRIVER_PACKAGE_NAMES[inferredDriver]);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (authConfigured || sessionConfigured) {
|
|
438
|
+
requiredPackages.add("@holo-js/session");
|
|
439
|
+
}
|
|
440
|
+
if (authConfigured || securityConfigured) {
|
|
441
|
+
requiredPackages.add("@holo-js/security");
|
|
442
|
+
}
|
|
443
|
+
if (authConfigured) {
|
|
444
|
+
requiredPackages.add("@holo-js/auth");
|
|
445
|
+
if (authConfigUsesSocialProviders(loaded)) {
|
|
446
|
+
requiredPackages.add("@holo-js/auth-social");
|
|
447
|
+
for (const [providerName, provider] of Object.entries(loaded.auth.social)) {
|
|
448
|
+
if (typeof provider.runtime === "string" && provider.runtime.trim()) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const builtinPackage = AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES[providerName];
|
|
452
|
+
if (builtinPackage) {
|
|
453
|
+
requiredPackages.add(builtinPackage);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (authConfigUsesWorkosProviders(loaded)) {
|
|
458
|
+
requiredPackages.add("@holo-js/auth-workos");
|
|
459
|
+
}
|
|
460
|
+
if (authConfigUsesClerkProviders(loaded)) {
|
|
461
|
+
requiredPackages.add("@holo-js/auth-clerk");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (mailConfigured) {
|
|
465
|
+
requiredPackages.add("@holo-js/mail");
|
|
466
|
+
}
|
|
467
|
+
if (mediaConfigured) {
|
|
468
|
+
requiredPackages.add("@holo-js/media");
|
|
469
|
+
}
|
|
470
|
+
if (cacheDesired) {
|
|
471
|
+
requiredPackages.add("@holo-js/cache");
|
|
472
|
+
}
|
|
473
|
+
if (cacheConfigured) {
|
|
474
|
+
const cacheDrivers = Object.values(loaded.cache.drivers);
|
|
475
|
+
if (cacheDrivers.some((driver) => driver.driver === "redis")) {
|
|
476
|
+
requiredPackages.add("@holo-js/cache-redis");
|
|
477
|
+
}
|
|
478
|
+
if (cacheDrivers.some((driver) => driver.driver === "database")) {
|
|
479
|
+
requiredPackages.add("@holo-js/cache-db");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (notificationsConfigured) {
|
|
483
|
+
requiredPackages.add("@holo-js/notifications");
|
|
484
|
+
}
|
|
485
|
+
if (broadcastConfigured || registryHasBroadcastDefinitions(discoveredRegistry) || hasRealtimeScaffold) {
|
|
486
|
+
requiredPackages.add("@holo-js/broadcast");
|
|
487
|
+
requiredPackages.add("@holo-js/flux");
|
|
488
|
+
const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
|
|
489
|
+
if (framework === "next") {
|
|
490
|
+
requiredPackages.add("@holo-js/flux-react");
|
|
491
|
+
requiredPackages.add("@holo-js/adapter-next");
|
|
492
|
+
} else if (framework === "nuxt") {
|
|
493
|
+
requiredPackages.add("@holo-js/flux-vue");
|
|
494
|
+
requiredPackages.add("@holo-js/adapter-nuxt");
|
|
495
|
+
} else if (framework === "sveltekit") {
|
|
496
|
+
requiredPackages.add("@holo-js/flux-svelte");
|
|
497
|
+
requiredPackages.add("@holo-js/adapter-sveltekit");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (hasRealtimeScaffold) {
|
|
501
|
+
requiredPackages.add("@holo-js/realtime");
|
|
502
|
+
}
|
|
503
|
+
if (registryHasAuthorizationDefinitions(discoveredRegistry) || hasAuthorizationScaffold) {
|
|
504
|
+
requiredPackages.add("@holo-js/authorization");
|
|
505
|
+
}
|
|
506
|
+
if (registryHasEvents(discoveredRegistry) || hasEventsScaffold) {
|
|
507
|
+
requiredPackages.add("@holo-js/events");
|
|
508
|
+
requiredPackages.add("@holo-js/queue");
|
|
509
|
+
}
|
|
510
|
+
if (queueConfigured || registryHasJobs(discoveredRegistry) || mailConfigUsesQueue(loaded)) {
|
|
511
|
+
requiredPackages.add("@holo-js/queue");
|
|
512
|
+
if (queueConfigured) {
|
|
513
|
+
const queueConnections = Object.values(loaded.queue.connections);
|
|
514
|
+
if (queueConnections.some((connection) => connection.driver === "redis")) {
|
|
515
|
+
requiredPackages.add("@holo-js/queue-redis");
|
|
516
|
+
}
|
|
517
|
+
if (queueConnections.some((connection) => connection.driver === "database") || loaded.queue.failed !== false) {
|
|
518
|
+
requiredPackages.add("@holo-js/queue-db");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (Object.values(loaded.cache?.drivers ?? {}).some((driver) => driver.driver === "redis") || loaded.security?.rateLimit?.driver === "redis" || Object.values(loaded.session?.stores ?? {}).some((store) => store.driver === "redis") || loaded.broadcast?.worker != null && loaded.broadcast.worker.scaling !== false) {
|
|
523
|
+
requiredPackages.add("ioredis");
|
|
524
|
+
}
|
|
525
|
+
if (storageConfigured) {
|
|
526
|
+
requiredPackages.add("@holo-js/storage");
|
|
527
|
+
if (Object.values(loaded.storage.disks).some((disk) => disk.driver === "s3")) {
|
|
528
|
+
requiredPackages.add("@holo-js/storage-s3");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
let changed = false;
|
|
532
|
+
const removableManagedPackages = /* @__PURE__ */ new Set([
|
|
533
|
+
"@holo-js/core",
|
|
534
|
+
...Object.values(DB_DRIVER_PACKAGE_NAMES),
|
|
535
|
+
"@holo-js/auth",
|
|
536
|
+
"@holo-js/auth-clerk",
|
|
537
|
+
"@holo-js/auth-social",
|
|
538
|
+
"@holo-js/auth-workos",
|
|
539
|
+
"@holo-js/authorization",
|
|
540
|
+
"@holo-js/broadcast",
|
|
541
|
+
"@holo-js/cache-db",
|
|
542
|
+
"@holo-js/cache-redis",
|
|
543
|
+
"@holo-js/events",
|
|
544
|
+
"@holo-js/flux",
|
|
545
|
+
"@holo-js/flux-react",
|
|
546
|
+
"@holo-js/flux-svelte",
|
|
547
|
+
"@holo-js/flux-vue",
|
|
548
|
+
"@holo-js/mail",
|
|
549
|
+
"@holo-js/media",
|
|
550
|
+
"@holo-js/notifications",
|
|
551
|
+
"@holo-js/queue",
|
|
552
|
+
"@holo-js/queue-db",
|
|
553
|
+
"@holo-js/queue-redis",
|
|
554
|
+
"@holo-js/realtime",
|
|
555
|
+
"@holo-js/security",
|
|
556
|
+
"@holo-js/session",
|
|
557
|
+
"@holo-js/storage",
|
|
558
|
+
"@holo-js/storage-s3",
|
|
559
|
+
...Object.values(AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES),
|
|
560
|
+
"ioredis"
|
|
561
|
+
]);
|
|
562
|
+
if (cacheDesired || cachePackageInstalled) {
|
|
563
|
+
removableManagedPackages.add("@holo-js/cache");
|
|
564
|
+
}
|
|
565
|
+
for (const packageName of requiredPackages) {
|
|
566
|
+
const requiredVersion = packageName === "ioredis" ? IOREDIS_PACKAGE_VERSION : resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies);
|
|
567
|
+
if (dependencies[packageName] !== requiredVersion || typeof devDependencies[packageName] !== "undefined") {
|
|
568
|
+
dependencies[packageName] = requiredVersion;
|
|
569
|
+
delete devDependencies[packageName];
|
|
570
|
+
changed = true;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (const packageName of removableManagedPackages) {
|
|
574
|
+
if (requiredPackages.has(packageName)) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (typeof dependencies[packageName] !== "undefined" || typeof devDependencies[packageName] !== "undefined") {
|
|
578
|
+
delete dependencies[packageName];
|
|
579
|
+
delete devDependencies[packageName];
|
|
580
|
+
changed = true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (!changed) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
async function upsertQueuePackageDependency(projectRoot, driver) {
|
|
590
|
+
const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
591
|
+
const queueConfigPath = await resolveFirstExistingPath(projectRoot, ["config/queue.ts", "config/queue.mts", "config/queue.js", "config/queue.mjs", "config/queue.cts", "config/queue.cjs"]);
|
|
592
|
+
const loadedQueueConfig = queueConfigPath ? loadConfigDirectory(projectRoot, {
|
|
593
|
+
preferCache: false,
|
|
594
|
+
processEnv: process.env
|
|
595
|
+
}).then((config) => config.queue).catch(() => void 0) : Promise.resolve(void 0);
|
|
596
|
+
const nextVersion = resolveManagedHoloPackageVersion("@holo-js/queue", dependencies, devDependencies);
|
|
597
|
+
const nextQueueDbVersion = resolveManagedHoloPackageVersion("@holo-js/queue-db", dependencies, devDependencies);
|
|
598
|
+
const nextQueueRedisVersion = resolveManagedHoloPackageVersion("@holo-js/queue-redis", dependencies, devDependencies);
|
|
599
|
+
const nextEsbuildVersion = ESBUILD_PACKAGE_VERSION;
|
|
600
|
+
const queueConfig = typeof driver === "undefined" ? await loadedQueueConfig : void 0;
|
|
601
|
+
const resolvedQueueDriver = driver && driver !== "sync" ? driver : queueConfig?.connections[queueConfig.default]?.driver ?? driver;
|
|
602
|
+
const requiresQueueDb = resolvedQueueDriver === "database" || (queueConfig?.failed ?? false) !== false || Object.values(queueConfig?.connections ?? {}).some((connection) => connection.driver === "database");
|
|
603
|
+
const requiresQueueRedis = resolvedQueueDriver === "redis" || Object.values(queueConfig?.connections ?? {}).some((connection) => connection.driver === "redis");
|
|
604
|
+
const currentVersion = dependencies["@holo-js/queue"];
|
|
605
|
+
const currentQueueDbVersion = dependencies["@holo-js/queue-db"];
|
|
606
|
+
const currentQueueRedisVersion = dependencies["@holo-js/queue-redis"];
|
|
607
|
+
const currentDevVersion = devDependencies["@holo-js/queue"];
|
|
608
|
+
const currentDevQueueDbVersion = devDependencies["@holo-js/queue-db"];
|
|
609
|
+
const currentDevQueueRedisVersion = devDependencies["@holo-js/queue-redis"];
|
|
610
|
+
const currentEsbuildVersion = dependencies.esbuild;
|
|
611
|
+
const currentDevEsbuildVersion = devDependencies.esbuild;
|
|
612
|
+
if (currentVersion === nextVersion && (requiresQueueDb ? currentQueueDbVersion === nextQueueDbVersion : typeof currentQueueDbVersion === "undefined") && (requiresQueueRedis ? currentQueueRedisVersion === nextQueueRedisVersion : typeof currentQueueRedisVersion === "undefined") && typeof currentDevVersion === "undefined" && typeof currentDevQueueDbVersion === "undefined" && typeof currentDevQueueRedisVersion === "undefined" && currentEsbuildVersion === nextEsbuildVersion && typeof currentDevEsbuildVersion === "undefined") {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
dependencies["@holo-js/queue"] = nextVersion;
|
|
616
|
+
if (requiresQueueDb) {
|
|
617
|
+
dependencies["@holo-js/queue-db"] = nextQueueDbVersion;
|
|
618
|
+
} else {
|
|
619
|
+
delete dependencies["@holo-js/queue-db"];
|
|
620
|
+
}
|
|
621
|
+
if (requiresQueueRedis) {
|
|
622
|
+
dependencies["@holo-js/queue-redis"] = nextQueueRedisVersion;
|
|
623
|
+
} else {
|
|
624
|
+
delete dependencies["@holo-js/queue-redis"];
|
|
625
|
+
}
|
|
626
|
+
dependencies.esbuild = nextEsbuildVersion;
|
|
627
|
+
delete devDependencies["@holo-js/queue"];
|
|
628
|
+
delete devDependencies["@holo-js/queue-db"];
|
|
629
|
+
delete devDependencies["@holo-js/queue-redis"];
|
|
630
|
+
delete devDependencies.esbuild;
|
|
631
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
async function upsertEventsPackageDependency(projectRoot) {
|
|
635
|
+
const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
636
|
+
const nextVersion = resolveManagedHoloPackageVersion("@holo-js/events", dependencies, devDependencies);
|
|
637
|
+
const nextQueueVersion = resolveManagedHoloPackageVersion("@holo-js/queue", dependencies, devDependencies);
|
|
638
|
+
const currentVersion = dependencies["@holo-js/events"];
|
|
639
|
+
const currentDevVersion = devDependencies["@holo-js/events"];
|
|
640
|
+
const currentQueueVersion = dependencies["@holo-js/queue"];
|
|
641
|
+
const currentQueueDevVersion = devDependencies["@holo-js/queue"];
|
|
642
|
+
if (currentVersion === nextVersion && typeof currentDevVersion === "undefined" && currentQueueVersion === nextQueueVersion && typeof currentQueueDevVersion === "undefined") {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
dependencies["@holo-js/events"] = nextVersion;
|
|
646
|
+
dependencies["@holo-js/queue"] = nextQueueVersion;
|
|
647
|
+
delete devDependencies["@holo-js/events"];
|
|
648
|
+
delete devDependencies["@holo-js/queue"];
|
|
649
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
async function upsertManagedPackageDependency(projectRoot, packageName) {
|
|
653
|
+
const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
654
|
+
const nextVersion = resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies);
|
|
655
|
+
const currentVersion = dependencies[packageName];
|
|
656
|
+
const currentDevVersion = devDependencies[packageName];
|
|
657
|
+
if (currentVersion === nextVersion && typeof currentDevVersion === "undefined") {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
dependencies[packageName] = nextVersion;
|
|
661
|
+
delete devDependencies[packageName];
|
|
662
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
async function upsertNotificationsPackageDependency(projectRoot) {
|
|
666
|
+
return await upsertManagedPackageDependency(projectRoot, "@holo-js/notifications");
|
|
667
|
+
}
|
|
668
|
+
async function upsertMailPackageDependency(projectRoot) {
|
|
669
|
+
return await upsertManagedPackageDependency(projectRoot, "@holo-js/mail");
|
|
670
|
+
}
|
|
671
|
+
async function upsertMediaPackageDependency(projectRoot) {
|
|
672
|
+
return await upsertManagedPackageDependency(projectRoot, "@holo-js/media");
|
|
673
|
+
}
|
|
674
|
+
async function upsertSecurityPackageDependency(projectRoot) {
|
|
675
|
+
return await upsertManagedPackageDependency(projectRoot, "@holo-js/security");
|
|
676
|
+
}
|
|
677
|
+
async function upsertCachePackageDependencies(projectRoot, driver = "file") {
|
|
678
|
+
const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
679
|
+
const cacheConfigPath = await resolveFirstExistingPath(projectRoot, CACHE_CONFIG_FILE_NAMES);
|
|
680
|
+
const cacheConfig = cacheConfigPath ? await loadConfigDirectory(projectRoot, {
|
|
681
|
+
preferCache: false,
|
|
682
|
+
processEnv: process.env
|
|
683
|
+
}).then((config) => config.cache).catch(() => void 0) : void 0;
|
|
684
|
+
const nextVersion = resolveManagedHoloPackageVersion("@holo-js/cache", dependencies, devDependencies);
|
|
685
|
+
const nextCacheDbVersion = resolveManagedHoloPackageVersion("@holo-js/cache-db", dependencies, devDependencies);
|
|
686
|
+
const nextCacheRedisVersion = resolveManagedHoloPackageVersion("@holo-js/cache-redis", dependencies, devDependencies);
|
|
687
|
+
const requiresCacheRedis = driver === "redis" || Object.values(cacheConfig?.drivers ?? {}).some((connection) => connection.driver === "redis");
|
|
688
|
+
const requiresCacheDb = driver === "database" || Object.values(cacheConfig?.drivers ?? {}).some((connection) => connection.driver === "database");
|
|
689
|
+
const currentVersion = dependencies["@holo-js/cache"];
|
|
690
|
+
const currentCacheDbVersion = dependencies["@holo-js/cache-db"];
|
|
691
|
+
const currentCacheRedisVersion = dependencies["@holo-js/cache-redis"];
|
|
692
|
+
const currentDevVersion = devDependencies["@holo-js/cache"];
|
|
693
|
+
const currentDevCacheDbVersion = devDependencies["@holo-js/cache-db"];
|
|
694
|
+
const currentDevCacheRedisVersion = devDependencies["@holo-js/cache-redis"];
|
|
695
|
+
if (currentVersion === nextVersion && (requiresCacheDb ? currentCacheDbVersion === nextCacheDbVersion : typeof currentCacheDbVersion === "undefined") && (requiresCacheRedis ? currentCacheRedisVersion === nextCacheRedisVersion : typeof currentCacheRedisVersion === "undefined") && typeof currentDevVersion === "undefined" && typeof currentDevCacheDbVersion === "undefined" && typeof currentDevCacheRedisVersion === "undefined") {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
dependencies["@holo-js/cache"] = nextVersion;
|
|
699
|
+
if (requiresCacheDb) {
|
|
700
|
+
dependencies["@holo-js/cache-db"] = nextCacheDbVersion;
|
|
701
|
+
} else {
|
|
702
|
+
delete dependencies["@holo-js/cache-db"];
|
|
703
|
+
}
|
|
704
|
+
if (requiresCacheRedis) {
|
|
705
|
+
dependencies["@holo-js/cache-redis"] = nextCacheRedisVersion;
|
|
706
|
+
} else {
|
|
707
|
+
delete dependencies["@holo-js/cache-redis"];
|
|
708
|
+
}
|
|
709
|
+
delete devDependencies["@holo-js/cache"];
|
|
710
|
+
delete devDependencies["@holo-js/cache-db"];
|
|
711
|
+
delete devDependencies["@holo-js/cache-redis"];
|
|
712
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
function detectProjectFrameworkFromPackageJson(dependencies, devDependencies) {
|
|
716
|
+
if (dependencies.next || devDependencies.next) {
|
|
717
|
+
return "next";
|
|
718
|
+
}
|
|
719
|
+
if (dependencies.nuxt || devDependencies.nuxt) {
|
|
720
|
+
return "nuxt";
|
|
721
|
+
}
|
|
722
|
+
if (dependencies["@sveltejs/kit"] || devDependencies["@sveltejs/kit"]) {
|
|
723
|
+
return "sveltekit";
|
|
724
|
+
}
|
|
725
|
+
return void 0;
|
|
726
|
+
}
|
|
727
|
+
async function upsertBroadcastPackageDependencies(projectRoot) {
|
|
728
|
+
const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
729
|
+
const nextVersion = resolveManagedHoloPackageVersion("@holo-js/broadcast", dependencies, devDependencies);
|
|
730
|
+
const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
|
|
731
|
+
let changed = false;
|
|
732
|
+
const requestedPackages = /* @__PURE__ */ new Set([
|
|
733
|
+
"@holo-js/broadcast",
|
|
734
|
+
"@holo-js/flux"
|
|
735
|
+
]);
|
|
736
|
+
if (framework === "next") {
|
|
737
|
+
requestedPackages.add("@holo-js/flux-react");
|
|
738
|
+
requestedPackages.add("@holo-js/adapter-next");
|
|
739
|
+
} else if (framework === "nuxt") {
|
|
740
|
+
requestedPackages.add("@holo-js/flux-vue");
|
|
741
|
+
requestedPackages.add("@holo-js/adapter-nuxt");
|
|
742
|
+
} else if (framework === "sveltekit") {
|
|
743
|
+
requestedPackages.add("@holo-js/flux-svelte");
|
|
744
|
+
requestedPackages.add("@holo-js/adapter-sveltekit");
|
|
745
|
+
}
|
|
746
|
+
const frameworkPackages = /* @__PURE__ */ new Set([
|
|
747
|
+
"@holo-js/flux-react",
|
|
748
|
+
"@holo-js/adapter-next",
|
|
749
|
+
"@holo-js/flux-vue",
|
|
750
|
+
"@holo-js/adapter-nuxt",
|
|
751
|
+
"@holo-js/flux-svelte",
|
|
752
|
+
"@holo-js/adapter-sveltekit"
|
|
753
|
+
]);
|
|
754
|
+
const managedPackages = /* @__PURE__ */ new Set([
|
|
755
|
+
...requestedPackages,
|
|
756
|
+
...frameworkPackages
|
|
757
|
+
]);
|
|
758
|
+
for (const packageName of managedPackages) {
|
|
759
|
+
if (!requestedPackages.has(packageName)) {
|
|
760
|
+
if (typeof dependencies[packageName] !== "undefined" || typeof devDependencies[packageName] !== "undefined") {
|
|
761
|
+
delete dependencies[packageName];
|
|
762
|
+
delete devDependencies[packageName];
|
|
763
|
+
changed = true;
|
|
764
|
+
}
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (dependencies[packageName] !== nextVersion || typeof devDependencies[packageName] !== "undefined") {
|
|
768
|
+
dependencies[packageName] = nextVersion;
|
|
769
|
+
delete devDependencies[packageName];
|
|
770
|
+
changed = true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (!changed) {
|
|
774
|
+
return {
|
|
775
|
+
updated: false,
|
|
776
|
+
framework
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
780
|
+
return {
|
|
781
|
+
updated: true,
|
|
782
|
+
framework
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
async function upsertAuthPackageDependencies(projectRoot, features = {}) {
|
|
786
|
+
const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
787
|
+
const socialEnabled = features.social === true || (features.socialProviders?.length ?? 0) > 0;
|
|
788
|
+
const requestedPackages = {
|
|
789
|
+
"@holo-js/auth": true,
|
|
790
|
+
"@holo-js/session": true,
|
|
791
|
+
"@holo-js/security": true,
|
|
792
|
+
"@holo-js/auth-social": socialEnabled,
|
|
793
|
+
"@holo-js/auth-workos": features.workos === true,
|
|
794
|
+
"@holo-js/auth-clerk": features.clerk === true
|
|
795
|
+
};
|
|
796
|
+
const requestedSocialProviders = new Set(features.socialProviders ?? (socialEnabled ? ["google"] : []));
|
|
797
|
+
let changed = false;
|
|
798
|
+
for (const [packageName, enabled] of Object.entries(requestedPackages)) {
|
|
799
|
+
const currentDependency = dependencies[packageName];
|
|
800
|
+
const currentDevDependency = devDependencies[packageName];
|
|
801
|
+
const nextVersion = resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies);
|
|
802
|
+
if (enabled) {
|
|
803
|
+
if (currentDependency !== nextVersion || typeof currentDevDependency !== "undefined") {
|
|
804
|
+
dependencies[packageName] = nextVersion;
|
|
805
|
+
delete devDependencies[packageName];
|
|
806
|
+
changed = true;
|
|
807
|
+
}
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (typeof currentDevDependency !== "undefined") {
|
|
811
|
+
delete devDependencies[packageName];
|
|
812
|
+
changed = true;
|
|
813
|
+
}
|
|
814
|
+
if (typeof currentDependency !== "undefined") {
|
|
815
|
+
delete dependencies[packageName];
|
|
816
|
+
changed = true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (const [providerName, packageName] of Object.entries(AUTH_SOCIAL_PROVIDER_PACKAGE_NAMES)) {
|
|
820
|
+
const enabled = requestedSocialProviders.has(providerName);
|
|
821
|
+
const currentDependency = dependencies[packageName];
|
|
822
|
+
const currentDevDependency = devDependencies[packageName];
|
|
823
|
+
const nextVersion = resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies);
|
|
824
|
+
if (enabled) {
|
|
825
|
+
if (currentDependency !== nextVersion || typeof currentDevDependency !== "undefined") {
|
|
826
|
+
dependencies[packageName] = nextVersion;
|
|
827
|
+
delete devDependencies[packageName];
|
|
828
|
+
changed = true;
|
|
829
|
+
}
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (typeof currentDevDependency !== "undefined") {
|
|
833
|
+
delete devDependencies[packageName];
|
|
834
|
+
changed = true;
|
|
835
|
+
}
|
|
836
|
+
if (typeof currentDependency !== "undefined") {
|
|
837
|
+
delete dependencies[packageName];
|
|
838
|
+
changed = true;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (!changed) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
async function upsertAuthorizationPackageDependency(projectRoot) {
|
|
848
|
+
return await upsertManagedPackageDependency(projectRoot, "@holo-js/authorization");
|
|
849
|
+
}
|
|
850
|
+
async function upsertRealtimePackageDependency(projectRoot) {
|
|
851
|
+
const broadcastResult = await upsertBroadcastPackageDependencies(projectRoot);
|
|
852
|
+
const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
853
|
+
const nextVersion = resolveManagedHoloPackageVersion("@holo-js/realtime", dependencies, devDependencies);
|
|
854
|
+
const currentVersion = dependencies["@holo-js/realtime"];
|
|
855
|
+
const currentDevVersion = devDependencies["@holo-js/realtime"];
|
|
856
|
+
if (currentVersion === nextVersion && typeof currentDevVersion === "undefined") {
|
|
857
|
+
return broadcastResult.updated;
|
|
858
|
+
}
|
|
859
|
+
dependencies["@holo-js/realtime"] = nextVersion;
|
|
860
|
+
delete devDependencies["@holo-js/realtime"];
|
|
861
|
+
await writePackageJsonDependencyState(packageJsonPath, parsed, dependencies, devDependencies);
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/project/scaffold/config-renderers.ts
|
|
866
|
+
function renderStorageConfig() {
|
|
867
|
+
return [
|
|
868
|
+
"import { defineStorageConfig, env } from '@holo-js/config'",
|
|
869
|
+
"",
|
|
870
|
+
"export default defineStorageConfig({",
|
|
871
|
+
` defaultDisk: env('STORAGE_DEFAULT_DISK', '${holoStorageDefaults.defaultDisk}'),`,
|
|
872
|
+
` routePrefix: env('STORAGE_ROUTE_PREFIX', '${holoStorageDefaults.routePrefix}'),`,
|
|
873
|
+
" disks: {",
|
|
874
|
+
" local: {",
|
|
875
|
+
" driver: 'local',",
|
|
876
|
+
" root: './storage/app',",
|
|
877
|
+
" },",
|
|
878
|
+
" public: {",
|
|
879
|
+
" driver: 'public',",
|
|
880
|
+
" root: './storage/app/public',",
|
|
881
|
+
" visibility: 'public',",
|
|
882
|
+
" },",
|
|
883
|
+
" },",
|
|
884
|
+
"})",
|
|
885
|
+
""
|
|
886
|
+
].join("\n");
|
|
887
|
+
}
|
|
888
|
+
function renderMediaConfig() {
|
|
889
|
+
return [
|
|
890
|
+
"import { defineMediaConfig } from '@holo-js/config'",
|
|
891
|
+
"",
|
|
892
|
+
"export default defineMediaConfig({})",
|
|
893
|
+
""
|
|
894
|
+
].join("\n");
|
|
895
|
+
}
|
|
896
|
+
function renderQueueConfig(options = {}) {
|
|
897
|
+
const driver = options.driver ?? "sync";
|
|
898
|
+
const defaultDatabaseConnection = options.defaultDatabaseConnection?.trim() || "default";
|
|
899
|
+
if (driver === "redis") {
|
|
900
|
+
return [
|
|
901
|
+
"import { defineQueueConfig, env } from '@holo-js/config'",
|
|
902
|
+
"",
|
|
903
|
+
"export default defineQueueConfig({",
|
|
904
|
+
" default: 'redis',",
|
|
905
|
+
" failed: false,",
|
|
906
|
+
" connections: {",
|
|
907
|
+
" redis: {",
|
|
908
|
+
" driver: 'redis',",
|
|
909
|
+
" connection: 'default',",
|
|
910
|
+
" queue: 'default',",
|
|
911
|
+
" retryAfter: 90,",
|
|
912
|
+
" blockFor: 5,",
|
|
913
|
+
" },",
|
|
914
|
+
" },",
|
|
915
|
+
"})",
|
|
916
|
+
""
|
|
917
|
+
].join("\n");
|
|
918
|
+
}
|
|
919
|
+
if (driver === "database") {
|
|
920
|
+
return [
|
|
921
|
+
"import { defineQueueConfig } from '@holo-js/config'",
|
|
922
|
+
"",
|
|
923
|
+
"export default defineQueueConfig({",
|
|
924
|
+
" default: 'database',",
|
|
925
|
+
" failed: {",
|
|
926
|
+
" driver: 'database',",
|
|
927
|
+
` connection: '${defaultDatabaseConnection}',`,
|
|
928
|
+
" table: 'failed_jobs',",
|
|
929
|
+
" },",
|
|
930
|
+
" connections: {",
|
|
931
|
+
" database: {",
|
|
932
|
+
" driver: 'database',",
|
|
933
|
+
` connection: '${defaultDatabaseConnection}',`,
|
|
934
|
+
" table: 'jobs',",
|
|
935
|
+
" queue: 'default',",
|
|
936
|
+
" retryAfter: 90,",
|
|
937
|
+
" sleep: 1,",
|
|
938
|
+
" },",
|
|
939
|
+
" },",
|
|
940
|
+
"})",
|
|
941
|
+
""
|
|
942
|
+
].join("\n");
|
|
943
|
+
}
|
|
944
|
+
return [
|
|
945
|
+
"import { defineQueueConfig } from '@holo-js/config'",
|
|
946
|
+
"",
|
|
947
|
+
"export default defineQueueConfig({",
|
|
948
|
+
" default: 'sync',",
|
|
949
|
+
" failed: false,",
|
|
950
|
+
" connections: {",
|
|
951
|
+
" sync: {",
|
|
952
|
+
" driver: 'sync',",
|
|
953
|
+
" queue: 'default',",
|
|
954
|
+
" },",
|
|
955
|
+
" },",
|
|
956
|
+
"})",
|
|
957
|
+
""
|
|
958
|
+
].join("\n");
|
|
959
|
+
}
|
|
960
|
+
function renderCacheConfig(driver = "file", defaultDatabaseConnection = "default", defaultRedisConnection = "default") {
|
|
961
|
+
const lines = [
|
|
962
|
+
"import { defineCacheConfig, env } from '@holo-js/config'",
|
|
963
|
+
"",
|
|
964
|
+
"export default defineCacheConfig({",
|
|
965
|
+
` default: '${driver}',`,
|
|
966
|
+
" prefix: env('CACHE_PREFIX', ''),",
|
|
967
|
+
" drivers: {",
|
|
968
|
+
" file: {",
|
|
969
|
+
" driver: 'file',",
|
|
970
|
+
" path: './storage/framework/cache/data',",
|
|
971
|
+
" },",
|
|
972
|
+
" memory: {",
|
|
973
|
+
" driver: 'memory',",
|
|
974
|
+
" maxEntries: 1000,",
|
|
975
|
+
" },"
|
|
976
|
+
];
|
|
977
|
+
if (driver === "redis") {
|
|
978
|
+
lines.push(
|
|
979
|
+
" redis: {",
|
|
980
|
+
" driver: 'redis',",
|
|
981
|
+
` connection: '${defaultRedisConnection}',`,
|
|
982
|
+
" prefix: 'cache:',",
|
|
983
|
+
" },"
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
if (driver === "database") {
|
|
987
|
+
lines.push(
|
|
988
|
+
" database: {",
|
|
989
|
+
" driver: 'database',",
|
|
990
|
+
` connection: '${defaultDatabaseConnection}',`,
|
|
991
|
+
" table: 'cache',",
|
|
992
|
+
" lockTable: 'cache_locks',",
|
|
993
|
+
" },"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
lines.push(
|
|
997
|
+
" },",
|
|
998
|
+
"})",
|
|
999
|
+
""
|
|
1000
|
+
);
|
|
1001
|
+
return lines.join("\n");
|
|
1002
|
+
}
|
|
1003
|
+
function renderRedisConfig() {
|
|
1004
|
+
return [
|
|
1005
|
+
"import { defineRedisConfig, env } from '@holo-js/config'",
|
|
1006
|
+
"",
|
|
1007
|
+
"export default defineRedisConfig({",
|
|
1008
|
+
" default: 'default',",
|
|
1009
|
+
" connections: {",
|
|
1010
|
+
" default: {",
|
|
1011
|
+
" url: env('REDIS_URL') || undefined,",
|
|
1012
|
+
" host: env('REDIS_HOST', '127.0.0.1'),",
|
|
1013
|
+
" port: env('REDIS_PORT', 6379),",
|
|
1014
|
+
" username: env('REDIS_USERNAME'),",
|
|
1015
|
+
" password: env('REDIS_PASSWORD'),",
|
|
1016
|
+
" db: env('REDIS_DB', 0),",
|
|
1017
|
+
" },",
|
|
1018
|
+
" },",
|
|
1019
|
+
"})",
|
|
1020
|
+
""
|
|
1021
|
+
].join("\n");
|
|
1022
|
+
}
|
|
1023
|
+
async function ensureRedisConfigFile(projectRoot) {
|
|
1024
|
+
const redisConfigPath = await resolveFirstExistingPath(projectRoot, REDIS_CONFIG_FILE_NAMES) ?? resolve2(projectRoot, "config/redis.ts");
|
|
1025
|
+
const redisConfigExists = await pathExists(redisConfigPath);
|
|
1026
|
+
if (!redisConfigExists) {
|
|
1027
|
+
await writeTextFile(redisConfigPath, renderRedisConfig());
|
|
1028
|
+
}
|
|
1029
|
+
return !redisConfigExists;
|
|
1030
|
+
}
|
|
1031
|
+
function renderNotificationsConfig() {
|
|
1032
|
+
return [
|
|
1033
|
+
"import { defineNotificationsConfig } from '@holo-js/config'",
|
|
1034
|
+
"",
|
|
1035
|
+
"export default defineNotificationsConfig({",
|
|
1036
|
+
" table: 'notifications',",
|
|
1037
|
+
" queue: {",
|
|
1038
|
+
" afterCommit: false,",
|
|
1039
|
+
" },",
|
|
1040
|
+
"})",
|
|
1041
|
+
""
|
|
1042
|
+
].join("\n");
|
|
1043
|
+
}
|
|
1044
|
+
function renderMailConfig() {
|
|
1045
|
+
return [
|
|
1046
|
+
"import { defineMailConfig, env } from '@holo-js/config'",
|
|
1047
|
+
"",
|
|
1048
|
+
"export default defineMailConfig({",
|
|
1049
|
+
" default: env('MAIL_MAILER', 'preview'),",
|
|
1050
|
+
" from: {",
|
|
1051
|
+
" email: env('MAIL_FROM_ADDRESS', 'hello@app.test'),",
|
|
1052
|
+
" name: env('MAIL_FROM_NAME', 'Holo App'),",
|
|
1053
|
+
" },",
|
|
1054
|
+
" preview: {",
|
|
1055
|
+
" allowedEnvironments: ['development'],",
|
|
1056
|
+
" },",
|
|
1057
|
+
" mailers: {",
|
|
1058
|
+
" preview: {",
|
|
1059
|
+
" driver: 'preview',",
|
|
1060
|
+
" },",
|
|
1061
|
+
" log: {",
|
|
1062
|
+
" driver: 'log',",
|
|
1063
|
+
" logBodies: env('MAIL_LOG_BODIES', false),",
|
|
1064
|
+
" },",
|
|
1065
|
+
" fake: {",
|
|
1066
|
+
" driver: 'fake',",
|
|
1067
|
+
" },",
|
|
1068
|
+
" smtp: {",
|
|
1069
|
+
" driver: 'smtp',",
|
|
1070
|
+
" host: env('MAIL_HOST', '127.0.0.1'),",
|
|
1071
|
+
" port: env('MAIL_PORT', 1025),",
|
|
1072
|
+
" secure: env('MAIL_SECURE', false),",
|
|
1073
|
+
" user: env('MAIL_USERNAME') || undefined,",
|
|
1074
|
+
" password: env('MAIL_PASSWORD') || undefined,",
|
|
1075
|
+
" },",
|
|
1076
|
+
" },",
|
|
1077
|
+
"})",
|
|
1078
|
+
""
|
|
1079
|
+
].join("\n");
|
|
1080
|
+
}
|
|
1081
|
+
function renderSecurityConfig() {
|
|
1082
|
+
return [
|
|
1083
|
+
`import { defineSecurityConfig, limit } from '@holo-js/security'`,
|
|
1084
|
+
"",
|
|
1085
|
+
"export default defineSecurityConfig({",
|
|
1086
|
+
" csrf: {",
|
|
1087
|
+
" enabled: true,",
|
|
1088
|
+
" field: '_token',",
|
|
1089
|
+
" header: 'X-CSRF-TOKEN',",
|
|
1090
|
+
" cookie: 'XSRF-TOKEN',",
|
|
1091
|
+
" except: [],",
|
|
1092
|
+
" },",
|
|
1093
|
+
" rateLimit: {",
|
|
1094
|
+
" driver: 'file',",
|
|
1095
|
+
" file: {",
|
|
1096
|
+
" path: './storage/framework/rate-limits',",
|
|
1097
|
+
" },",
|
|
1098
|
+
" redis: {",
|
|
1099
|
+
" connection: 'default',",
|
|
1100
|
+
" prefix: 'holo:rate-limit:',",
|
|
1101
|
+
" },",
|
|
1102
|
+
" limiters: {",
|
|
1103
|
+
" login: limit.perMinute(5).define(),",
|
|
1104
|
+
" register: limit.perHour(10).define(),",
|
|
1105
|
+
" },",
|
|
1106
|
+
" },",
|
|
1107
|
+
"})",
|
|
1108
|
+
""
|
|
1109
|
+
].join("\n");
|
|
1110
|
+
}
|
|
1111
|
+
function renderCorsConfig() {
|
|
1112
|
+
return [
|
|
1113
|
+
"import { defineCorsConfig, env } from '@holo-js/config'",
|
|
1114
|
+
"",
|
|
1115
|
+
"export default defineCorsConfig({",
|
|
1116
|
+
" paths: ['/api/*', '/broadcasting/auth'],",
|
|
1117
|
+
" origins: [",
|
|
1118
|
+
" env('FRONTEND_URL', 'http://localhost:3000'),",
|
|
1119
|
+
" ],",
|
|
1120
|
+
" methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],",
|
|
1121
|
+
" headers: ['Content-Type', 'Authorization', 'X-CSRF-TOKEN', 'X-Requested-With'],",
|
|
1122
|
+
" credentials: true,",
|
|
1123
|
+
" maxAge: 7200,",
|
|
1124
|
+
" statefulDomains: [",
|
|
1125
|
+
" env('FRONTEND_DOMAIN', 'localhost:3000'),",
|
|
1126
|
+
" ],",
|
|
1127
|
+
"})",
|
|
1128
|
+
""
|
|
1129
|
+
].join("\n");
|
|
1130
|
+
}
|
|
1131
|
+
async function ensureCorsConfigFile(projectRoot) {
|
|
1132
|
+
const corsConfigPath = await resolveFirstExistingPath(projectRoot, CORS_CONFIG_FILE_NAMES) ?? resolve2(projectRoot, "config/cors.ts");
|
|
1133
|
+
const corsConfigExists = await pathExists(corsConfigPath);
|
|
1134
|
+
if (!corsConfigExists) {
|
|
1135
|
+
await writeTextFile(corsConfigPath, renderCorsConfig());
|
|
1136
|
+
}
|
|
1137
|
+
return !corsConfigExists;
|
|
1138
|
+
}
|
|
1139
|
+
async function ensureRateLimitStorageIgnore(projectRoot) {
|
|
1140
|
+
const rateLimitRoot = resolve2(projectRoot, "storage/framework/rate-limits");
|
|
1141
|
+
const ignorePath = resolve2(rateLimitRoot, ".gitignore");
|
|
1142
|
+
await mkdir(rateLimitRoot, { recursive: true });
|
|
1143
|
+
if (!await pathExists(ignorePath)) {
|
|
1144
|
+
await writeTextFile(ignorePath, "*\n!.gitignore\n");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const currentContents = await readTextFile(ignorePath) ?? "";
|
|
1148
|
+
const existingLines = new Set(currentContents.split(/\r?\n/));
|
|
1149
|
+
const missingLines = [
|
|
1150
|
+
"*",
|
|
1151
|
+
"!.gitignore"
|
|
1152
|
+
].filter((line) => !existingLines.has(line));
|
|
1153
|
+
if (missingLines.length === 0) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
await appendFile(
|
|
1157
|
+
ignorePath,
|
|
1158
|
+
`${currentContents.length > 0 && !currentContents.endsWith("\n") ? "\n" : ""}${missingLines.join("\n")}
|
|
1159
|
+
`,
|
|
1160
|
+
"utf8"
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
function renderBroadcastConfig(moduleFormat, includeAuthEndpoint, useTypeScriptSyntax) {
|
|
1164
|
+
const renderBroadcastScheme = () => {
|
|
1165
|
+
return useTypeScriptSyntax ? "env('BROADCAST_SCHEME') === 'https' ? 'https' : 'http'" : "(process.env.BROADCAST_SCHEME === 'https' ? 'https' : 'http')";
|
|
1166
|
+
};
|
|
1167
|
+
if (moduleFormat === "cjs") {
|
|
1168
|
+
return [
|
|
1169
|
+
"const { defineBroadcastConfig, env } = require('@holo-js/config')",
|
|
1170
|
+
"",
|
|
1171
|
+
`const broadcastScheme = ${renderBroadcastScheme()}`,
|
|
1172
|
+
"",
|
|
1173
|
+
"module.exports = defineBroadcastConfig({",
|
|
1174
|
+
" default: env('BROADCAST_CONNECTION', 'holo'),",
|
|
1175
|
+
" connections: {",
|
|
1176
|
+
" holo: {",
|
|
1177
|
+
" driver: 'holo',",
|
|
1178
|
+
" appId: env('BROADCAST_APP_ID', 'app-id'),",
|
|
1179
|
+
" key: env('BROADCAST_APP_KEY', 'app-key'),",
|
|
1180
|
+
" secret: env('BROADCAST_APP_SECRET', 'app-secret'),",
|
|
1181
|
+
" options: {",
|
|
1182
|
+
" host: env('BROADCAST_HOST', '127.0.0.1'),",
|
|
1183
|
+
" port: env('BROADCAST_PORT', 8080),",
|
|
1184
|
+
" scheme: broadcastScheme,",
|
|
1185
|
+
" useTLS: broadcastScheme === 'https',",
|
|
1186
|
+
" },",
|
|
1187
|
+
...includeAuthEndpoint ? [
|
|
1188
|
+
" clientOptions: {",
|
|
1189
|
+
" authEndpoint: `${env('APP_URL', 'http://localhost:3000')}/broadcasting/auth`,",
|
|
1190
|
+
" },"
|
|
1191
|
+
] : [],
|
|
1192
|
+
" },",
|
|
1193
|
+
" log: {",
|
|
1194
|
+
" driver: 'log',",
|
|
1195
|
+
" },",
|
|
1196
|
+
" null: {",
|
|
1197
|
+
" driver: 'null',",
|
|
1198
|
+
" },",
|
|
1199
|
+
" },",
|
|
1200
|
+
"})",
|
|
1201
|
+
""
|
|
1202
|
+
].join("\n");
|
|
1203
|
+
}
|
|
1204
|
+
return [
|
|
1205
|
+
"import { defineBroadcastConfig, env } from '@holo-js/config'",
|
|
1206
|
+
"",
|
|
1207
|
+
`const broadcastScheme = ${renderBroadcastScheme()}`,
|
|
1208
|
+
"",
|
|
1209
|
+
"export default defineBroadcastConfig({",
|
|
1210
|
+
" default: env('BROADCAST_CONNECTION', 'holo'),",
|
|
1211
|
+
" connections: {",
|
|
1212
|
+
" holo: {",
|
|
1213
|
+
" driver: 'holo',",
|
|
1214
|
+
" appId: env('BROADCAST_APP_ID', 'app-id'),",
|
|
1215
|
+
" key: env('BROADCAST_APP_KEY', 'app-key'),",
|
|
1216
|
+
" secret: env('BROADCAST_APP_SECRET', 'app-secret'),",
|
|
1217
|
+
" options: {",
|
|
1218
|
+
" host: env('BROADCAST_HOST', '127.0.0.1'),",
|
|
1219
|
+
" port: env('BROADCAST_PORT', 8080),",
|
|
1220
|
+
" scheme: broadcastScheme,",
|
|
1221
|
+
" useTLS: broadcastScheme === 'https',",
|
|
1222
|
+
" },",
|
|
1223
|
+
...includeAuthEndpoint ? [
|
|
1224
|
+
" clientOptions: {",
|
|
1225
|
+
" authEndpoint: `${env('APP_URL', 'http://localhost:3000')}/broadcasting/auth`,",
|
|
1226
|
+
" },"
|
|
1227
|
+
] : [],
|
|
1228
|
+
" },",
|
|
1229
|
+
" log: {",
|
|
1230
|
+
" driver: 'log',",
|
|
1231
|
+
" },",
|
|
1232
|
+
" null: {",
|
|
1233
|
+
" driver: 'null',",
|
|
1234
|
+
" },",
|
|
1235
|
+
" },",
|
|
1236
|
+
"})",
|
|
1237
|
+
""
|
|
1238
|
+
].join("\n");
|
|
1239
|
+
}
|
|
1240
|
+
function stripBroadcastAuthEndpointBlock(value) {
|
|
1241
|
+
return value.replace(
|
|
1242
|
+
/(^|\n)\s*clientOptions:\s*\{\n\s*authEndpoint:\s*.*,\n\s*\},/m,
|
|
1243
|
+
""
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
function hasUnaliasedEnvSpecifier(specifiers, aliasToken) {
|
|
1247
|
+
return specifiers.split(",").some((specifier) => {
|
|
1248
|
+
const normalized = specifier.trim().replace(/\s+/g, " ");
|
|
1249
|
+
return normalized === "env" || normalized === `env ${aliasToken} env`;
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
function hasHoloConfigEnvBinding(value) {
|
|
1253
|
+
for (const match of value.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"]@holo-js\/config['"]/g)) {
|
|
1254
|
+
const specifiers = match[1];
|
|
1255
|
+
if (specifiers && hasUnaliasedEnvSpecifier(specifiers, "as")) {
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
for (const match of value.matchAll(/\{([^}]+)\}\s*=\s*require\(['"]@holo-js\/config['"]\)/g)) {
|
|
1260
|
+
const specifiers = match[1];
|
|
1261
|
+
if (specifiers && hasUnaliasedEnvSpecifier(specifiers, ":")) {
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
function injectBroadcastAuthEndpoint(value) {
|
|
1268
|
+
if (value.includes("authEndpoint:")) {
|
|
1269
|
+
return value;
|
|
1270
|
+
}
|
|
1271
|
+
const authEndpointExpression = hasHoloConfigEnvBinding(value) ? "`${env('APP_URL', 'http://localhost:3000')}/broadcasting/auth`" : "`${process.env.APP_URL ?? 'http://localhost:3000'}/broadcasting/auth`";
|
|
1272
|
+
const nextValue = value.replace(
|
|
1273
|
+
/(holo:\s*\{[\s\S]*?options:\s*\{[\s\S]*?\n)([ \t]*)\},/m,
|
|
1274
|
+
(_match, prefix, indent) => {
|
|
1275
|
+
return [
|
|
1276
|
+
`${prefix}${indent}},`,
|
|
1277
|
+
`${indent}clientOptions: {`,
|
|
1278
|
+
`${indent} authEndpoint: ${authEndpointExpression},`,
|
|
1279
|
+
`${indent}},`
|
|
1280
|
+
].join("\n");
|
|
1281
|
+
}
|
|
1282
|
+
);
|
|
1283
|
+
return nextValue === value ? void 0 : nextValue;
|
|
1284
|
+
}
|
|
1285
|
+
function canSafelyRewriteBroadcastConfig(currentContents, moduleFormat, useTypeScriptSyntax) {
|
|
1286
|
+
return stripBroadcastAuthEndpointBlock(currentContents) === stripBroadcastAuthEndpointBlock(
|
|
1287
|
+
renderBroadcastConfig(moduleFormat, false, useTypeScriptSyntax)
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
function resolveBroadcastConfigTargetPath(projectRoot, manifestPath, moduleFormat) {
|
|
1291
|
+
const extension = extname(manifestPath);
|
|
1292
|
+
const targetExtension = extension === ".cjs" || extension === ".cts" || extension === ".mjs" || extension === ".mts" ? extension : moduleFormat === "cjs" ? ".cjs" : extension === ".ts" || extension === ".js" ? extension : ".ts";
|
|
1293
|
+
return resolve2(projectRoot, `config/broadcast${targetExtension}`);
|
|
1294
|
+
}
|
|
1295
|
+
function renderBroadcastEnvFiles() {
|
|
1296
|
+
const env = [
|
|
1297
|
+
"BROADCAST_CONNECTION=holo"
|
|
1298
|
+
];
|
|
1299
|
+
const example = [
|
|
1300
|
+
"BROADCAST_CONNECTION=holo",
|
|
1301
|
+
"BROADCAST_APP_ID=",
|
|
1302
|
+
"BROADCAST_APP_KEY=",
|
|
1303
|
+
"BROADCAST_APP_SECRET="
|
|
1304
|
+
];
|
|
1305
|
+
return {
|
|
1306
|
+
env,
|
|
1307
|
+
example
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
async function syncBroadcastAuthSupportAfterAuthInstall(projectRoot) {
|
|
1311
|
+
const { dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
1312
|
+
const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
|
|
1313
|
+
const canCreateBroadcastAuthRoute = framework === "next" || framework === "nuxt" || framework === "sveltekit";
|
|
1314
|
+
const authConfigPath = await resolveFirstExistingPath(projectRoot, AUTH_CONFIG_FILE_NAMES);
|
|
1315
|
+
const broadcastConfigPath = await resolveFirstExistingPath(projectRoot, BROADCAST_CONFIG_FILE_NAMES);
|
|
1316
|
+
if (!authConfigPath || !broadcastConfigPath || !canCreateBroadcastAuthRoute) {
|
|
1317
|
+
return {
|
|
1318
|
+
updatedBroadcastConfig: false,
|
|
1319
|
+
createdBroadcastAuthRoute: false
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
const currentBroadcastConfig = await readTextFile(broadcastConfigPath);
|
|
1323
|
+
let updatedBroadcastConfig = false;
|
|
1324
|
+
let createdBroadcastAuthRoute = false;
|
|
1325
|
+
if (!currentBroadcastConfig.includes("authEndpoint:")) {
|
|
1326
|
+
const broadcastConfigModuleFormat = resolveConfigModuleFormat(broadcastConfigPath, currentBroadcastConfig);
|
|
1327
|
+
const broadcastConfigIsTypeScript = [".ts", ".mts", ".cts"].includes(extname(broadcastConfigPath));
|
|
1328
|
+
const rewrittenBroadcastConfig = canSafelyRewriteBroadcastConfig(
|
|
1329
|
+
currentBroadcastConfig,
|
|
1330
|
+
broadcastConfigModuleFormat,
|
|
1331
|
+
broadcastConfigIsTypeScript
|
|
1332
|
+
) ? renderBroadcastConfig(broadcastConfigModuleFormat, true, broadcastConfigIsTypeScript) : injectBroadcastAuthEndpoint(currentBroadcastConfig);
|
|
1333
|
+
if (rewrittenBroadcastConfig) {
|
|
1334
|
+
await writeTextFile(
|
|
1335
|
+
broadcastConfigPath,
|
|
1336
|
+
rewrittenBroadcastConfig
|
|
1337
|
+
);
|
|
1338
|
+
updatedBroadcastConfig = true;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (framework === "next") {
|
|
1342
|
+
const authRoutePath = resolve2(projectRoot, "app/broadcasting/auth/route.ts");
|
|
1343
|
+
const holoHelperPath = resolve2(projectRoot, ".holo-js/generated/next/holo.ts");
|
|
1344
|
+
const generatedRoutePath = resolve2(projectRoot, ".holo-js/generated/next/broadcast-auth-route.ts");
|
|
1345
|
+
if (!await pathExists(authRoutePath)) {
|
|
1346
|
+
await writeTextFile(authRoutePath, renderNextBroadcastAuthRoute());
|
|
1347
|
+
createdBroadcastAuthRoute = true;
|
|
1348
|
+
}
|
|
1349
|
+
await writeTextFile(holoHelperPath, renderNextHoloHelper());
|
|
1350
|
+
await writeTextFile(generatedRoutePath, renderNextGeneratedBroadcastAuthRoute());
|
|
1351
|
+
return {
|
|
1352
|
+
updatedBroadcastConfig,
|
|
1353
|
+
createdBroadcastAuthRoute
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
if (framework === "sveltekit") {
|
|
1357
|
+
const holoHelperPath = resolve2(projectRoot, ".holo-js/generated/sveltekit/holo.ts");
|
|
1358
|
+
await writeTextFile(holoHelperPath, renderSvelteHoloHelper());
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
updatedBroadcastConfig,
|
|
1362
|
+
createdBroadcastAuthRoute
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function renderSessionConfig(defaultDatabaseConnection = "default") {
|
|
1366
|
+
return [
|
|
1367
|
+
"import { defineSessionConfig, env } from '@holo-js/config'",
|
|
1368
|
+
"",
|
|
1369
|
+
"const sessionSameSite = env('SESSION_SAME_SITE') === 'strict'",
|
|
1370
|
+
" ? 'strict'",
|
|
1371
|
+
" : env('SESSION_SAME_SITE') === 'none'",
|
|
1372
|
+
" ? 'none'",
|
|
1373
|
+
" : 'lax'",
|
|
1374
|
+
"",
|
|
1375
|
+
"export default defineSessionConfig({",
|
|
1376
|
+
" driver: env('SESSION_DRIVER', 'file'),",
|
|
1377
|
+
" stores: {",
|
|
1378
|
+
" database: {",
|
|
1379
|
+
" driver: 'database',",
|
|
1380
|
+
` connection: env('SESSION_CONNECTION', '${defaultDatabaseConnection}'),`,
|
|
1381
|
+
" table: 'sessions',",
|
|
1382
|
+
" },",
|
|
1383
|
+
" file: {",
|
|
1384
|
+
" driver: 'file',",
|
|
1385
|
+
" path: './storage/framework/sessions',",
|
|
1386
|
+
" },",
|
|
1387
|
+
" },",
|
|
1388
|
+
" cookie: {",
|
|
1389
|
+
" name: env('SESSION_COOKIE', 'holo_session'),",
|
|
1390
|
+
" path: env('SESSION_PATH', '/'),",
|
|
1391
|
+
" domain: env('SESSION_DOMAIN'),",
|
|
1392
|
+
" secure: env('SESSION_SECURE', false),",
|
|
1393
|
+
" httpOnly: true,",
|
|
1394
|
+
" sameSite: sessionSameSite,",
|
|
1395
|
+
" },",
|
|
1396
|
+
" idleTimeout: env('SESSION_IDLE_TIMEOUT', 120),",
|
|
1397
|
+
" absoluteLifetime: env('SESSION_LIFETIME', 120),",
|
|
1398
|
+
" rememberMeLifetime: env('SESSION_REMEMBER_ME_LIFETIME', 43200),",
|
|
1399
|
+
"})",
|
|
1400
|
+
""
|
|
1401
|
+
].join("\n");
|
|
1402
|
+
}
|
|
1403
|
+
function renderAuthConfig(features = {}, moduleFormat = "esm") {
|
|
1404
|
+
const envValue = (name, fallback) => {
|
|
1405
|
+
if (moduleFormat === "cjs") {
|
|
1406
|
+
return typeof fallback === "string" ? `process.env.${name} || ${JSON.stringify(fallback)}` : `process.env.${name}`;
|
|
1407
|
+
}
|
|
1408
|
+
return typeof fallback === "string" ? `env('${name}', ${JSON.stringify(fallback)})` : `env('${name}')`;
|
|
1409
|
+
};
|
|
1410
|
+
const socialEnabled = features.social === true || (features.socialProviders?.length ?? 0) > 0;
|
|
1411
|
+
const socialProviders = features.socialProviders && features.socialProviders.length > 0 ? features.socialProviders : socialEnabled ? ["google"] : [];
|
|
1412
|
+
const lines = [
|
|
1413
|
+
moduleFormat === "cjs" ? "module.exports = {" : "import { defineAuthConfig, env } from '@holo-js/config'",
|
|
1414
|
+
"",
|
|
1415
|
+
...moduleFormat === "cjs" ? [] : ["export default defineAuthConfig({"],
|
|
1416
|
+
" defaults: {",
|
|
1417
|
+
" guard: 'web',",
|
|
1418
|
+
" passwords: 'users',",
|
|
1419
|
+
" },",
|
|
1420
|
+
" guards: {",
|
|
1421
|
+
" web: {",
|
|
1422
|
+
" driver: 'session',",
|
|
1423
|
+
" provider: 'users',",
|
|
1424
|
+
" },",
|
|
1425
|
+
" // admin: {",
|
|
1426
|
+
" // driver: 'session',",
|
|
1427
|
+
" // provider: 'admins',",
|
|
1428
|
+
" // },",
|
|
1429
|
+
" },",
|
|
1430
|
+
" providers: {",
|
|
1431
|
+
" users: {",
|
|
1432
|
+
" model: 'User',",
|
|
1433
|
+
" identifiers: ['email'],",
|
|
1434
|
+
" },",
|
|
1435
|
+
" // admins: {",
|
|
1436
|
+
" // model: 'Admin',",
|
|
1437
|
+
" // identifiers: ['email'],",
|
|
1438
|
+
" // },",
|
|
1439
|
+
" },",
|
|
1440
|
+
" passwords: {",
|
|
1441
|
+
" users: {",
|
|
1442
|
+
" provider: 'users',",
|
|
1443
|
+
" table: 'password_reset_tokens',",
|
|
1444
|
+
" expire: 60,",
|
|
1445
|
+
" throttle: 60,",
|
|
1446
|
+
" route: '/reset-password',",
|
|
1447
|
+
" },",
|
|
1448
|
+
" },",
|
|
1449
|
+
" emailVerification: {",
|
|
1450
|
+
" required: false,",
|
|
1451
|
+
" route: '/verify-email',",
|
|
1452
|
+
" },"
|
|
1453
|
+
];
|
|
1454
|
+
if (socialProviders.length > 0) {
|
|
1455
|
+
lines.push(" social: {");
|
|
1456
|
+
for (const provider of socialProviders) {
|
|
1457
|
+
const upper = provider.toUpperCase();
|
|
1458
|
+
const defaultScopes = provider === "google" ? ["openid", "email", "profile"] : provider === "github" ? ["read:user", "user:email"] : provider === "discord" ? ["identify", "email"] : provider === "facebook" ? ["email", "public_profile"] : provider === "apple" ? ["name", "email"] : ["openid", "profile", "email"];
|
|
1459
|
+
lines.push(
|
|
1460
|
+
` ${provider}: {`,
|
|
1461
|
+
` clientId: ${envValue(`AUTH_${upper}_CLIENT_ID`)},`,
|
|
1462
|
+
` clientSecret: ${envValue(`AUTH_${upper}_CLIENT_SECRET`)},`,
|
|
1463
|
+
` redirectUri: ${envValue(`AUTH_${upper}_REDIRECT_URI`)},`,
|
|
1464
|
+
` scopes: [${defaultScopes.map((scope) => `'${scope}'`).join(", ")}],`,
|
|
1465
|
+
" },"
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
lines.push(" },");
|
|
1469
|
+
}
|
|
1470
|
+
if (features.workos) {
|
|
1471
|
+
lines.push(
|
|
1472
|
+
" workos: {",
|
|
1473
|
+
` provider: ${envValue("AUTH_WORKOS_PROVIDER", "dashboard")},`,
|
|
1474
|
+
" dashboard: {",
|
|
1475
|
+
` clientId: ${envValue("WORKOS_CLIENT_ID")},`,
|
|
1476
|
+
` apiKey: ${envValue("WORKOS_API_KEY")},`,
|
|
1477
|
+
` redirectUri: ${envValue("WORKOS_REDIRECT_URI")},`,
|
|
1478
|
+
" },",
|
|
1479
|
+
" },",
|
|
1480
|
+
" // Add a dedicated guard and provider if WorkOS users should resolve through a different model."
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (features.clerk) {
|
|
1484
|
+
lines.push(
|
|
1485
|
+
" clerk: {",
|
|
1486
|
+
` provider: ${envValue("AUTH_CLERK_PROVIDER", "app")},`,
|
|
1487
|
+
" app: {",
|
|
1488
|
+
` publishableKey: ${envValue("CLERK_PUBLISHABLE_KEY")},`,
|
|
1489
|
+
` secretKey: ${envValue("CLERK_SECRET_KEY")},`,
|
|
1490
|
+
` apiUrl: ${envValue("CLERK_API_URL")},`,
|
|
1491
|
+
` frontendApi: ${envValue("CLERK_FRONTEND_API")},`,
|
|
1492
|
+
` redirectUri: ${envValue("CLERK_REDIRECT_URI")},`,
|
|
1493
|
+
` sessionCookie: ${envValue("CLERK_SESSION_COOKIE", "__session")},`,
|
|
1494
|
+
" },",
|
|
1495
|
+
" },",
|
|
1496
|
+
" // Add a dedicated guard and provider if Clerk users should resolve through a different model."
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
lines.push(moduleFormat === "cjs" ? "}" : "})", "");
|
|
1500
|
+
return lines.join("\n");
|
|
1501
|
+
}
|
|
1502
|
+
function authFeaturesRequireConfigUpdate(features) {
|
|
1503
|
+
return features.workos === true || features.clerk === true || features.social === true || (features.socialProviders?.length ?? 0) > 0;
|
|
1504
|
+
}
|
|
1505
|
+
function detectAuthInstallFeaturesFromConfig(contents) {
|
|
1506
|
+
const socialProviders = SUPPORTED_AUTH_SOCIAL_PROVIDERS.filter((provider) => {
|
|
1507
|
+
const pattern = new RegExp(`\\b${provider}\\s*:\\s*\\{`);
|
|
1508
|
+
return pattern.test(contents);
|
|
1509
|
+
});
|
|
1510
|
+
return Object.freeze({
|
|
1511
|
+
...socialProviders.length > 0 ? { social: true, socialProviders } : {},
|
|
1512
|
+
...contents.includes(" workos: {") ? { workos: true } : {},
|
|
1513
|
+
...contents.includes(" clerk: {") ? { clerk: true } : {}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
function mergeAuthInstallFeatures(current, requested) {
|
|
1517
|
+
const socialProviders = Array.from(/* @__PURE__ */ new Set([
|
|
1518
|
+
...current.socialProviders ?? [],
|
|
1519
|
+
...requested.socialProviders ?? []
|
|
1520
|
+
]));
|
|
1521
|
+
return Object.freeze({
|
|
1522
|
+
...current.social === true || requested.social === true || socialProviders.length > 0 ? { social: true } : {},
|
|
1523
|
+
...socialProviders.length > 0 ? { socialProviders } : {},
|
|
1524
|
+
...current.workos === true || requested.workos === true ? { workos: true } : {},
|
|
1525
|
+
...current.clerk === true || requested.clerk === true ? { clerk: true } : {}
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
function canSafelyRewriteAuthConfig(currentContents, currentFeatures, moduleFormat) {
|
|
1529
|
+
const stripLegacyCurrentUserEndpoint = (value) => value.replace(
|
|
1530
|
+
/(^|\n)\s*currentUserEndpoint:\s*\{\n\s*path:\s*.*,\n\s*\},/m,
|
|
1531
|
+
""
|
|
1532
|
+
);
|
|
1533
|
+
return stripLegacyCurrentUserEndpoint(currentContents) === stripLegacyCurrentUserEndpoint(
|
|
1534
|
+
renderAuthConfig(currentFeatures, moduleFormat)
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
function resolveConfigModuleFormat(filePath, contents) {
|
|
1538
|
+
if (filePath?.endsWith(".cjs") || filePath?.endsWith(".cts") || contents.includes("module.exports =")) {
|
|
1539
|
+
return "cjs";
|
|
1540
|
+
}
|
|
1541
|
+
return "esm";
|
|
1542
|
+
}
|
|
1543
|
+
function mergeInstalledAuthFeatures(current, requested) {
|
|
1544
|
+
return mergeAuthInstallFeatures(current, requested);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/project/scaffold/framework.ts
|
|
1548
|
+
import { mkdir as mkdir2, readdir, writeFile } from "fs/promises";
|
|
1549
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
1550
|
+
import {
|
|
1551
|
+
normalizeHoloProjectConfig,
|
|
1552
|
+
renderGeneratedSchemaPlaceholder
|
|
1553
|
+
} from "@holo-js/db";
|
|
1554
|
+
|
|
1555
|
+
// src/project/scaffold/project-renderers.ts
|
|
1556
|
+
import {
|
|
1557
|
+
createMigrationFileName
|
|
1558
|
+
} from "@holo-js/db";
|
|
1559
|
+
|
|
1560
|
+
// src/project/scaffold/types.ts
|
|
1561
|
+
var AUTH_MIGRATION_SLUGS = [
|
|
1562
|
+
"create_users",
|
|
1563
|
+
"create_sessions",
|
|
1564
|
+
"create_auth_identities",
|
|
1565
|
+
"create_personal_access_tokens",
|
|
1566
|
+
"create_password_reset_tokens",
|
|
1567
|
+
"create_email_verification_tokens"
|
|
1568
|
+
];
|
|
1569
|
+
|
|
1570
|
+
// src/project/scaffold/project-renderers.ts
|
|
1571
|
+
function renderAuthEnvFiles(features = {}, defaultDatabaseConnection = "default") {
|
|
1572
|
+
const socialEnabled = features.social === true || (features.socialProviders?.length ?? 0) > 0;
|
|
1573
|
+
const socialProviders = features.socialProviders && features.socialProviders.length > 0 ? features.socialProviders : socialEnabled ? ["google"] : [];
|
|
1574
|
+
const env = [
|
|
1575
|
+
"AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email",
|
|
1576
|
+
"AUTH_PASSWORD_RESET_ROUTE=/reset-password",
|
|
1577
|
+
"FRONTEND_URL=",
|
|
1578
|
+
"FRONTEND_DOMAIN=",
|
|
1579
|
+
"SESSION_DRIVER=file",
|
|
1580
|
+
`SESSION_CONNECTION=${defaultDatabaseConnection}`,
|
|
1581
|
+
"SESSION_COOKIE=holo_session",
|
|
1582
|
+
"SESSION_PATH=/",
|
|
1583
|
+
"SESSION_DOMAIN=",
|
|
1584
|
+
"SESSION_SECURE=false",
|
|
1585
|
+
"SESSION_SAME_SITE=lax",
|
|
1586
|
+
"SESSION_IDLE_TIMEOUT=120",
|
|
1587
|
+
"SESSION_LIFETIME=120",
|
|
1588
|
+
"SESSION_REMEMBER_ME_LIFETIME=43200"
|
|
1589
|
+
];
|
|
1590
|
+
for (const provider of socialProviders) {
|
|
1591
|
+
const upper = provider.toUpperCase();
|
|
1592
|
+
env.push(
|
|
1593
|
+
`AUTH_${upper}_CLIENT_ID=`,
|
|
1594
|
+
`AUTH_${upper}_CLIENT_SECRET=`,
|
|
1595
|
+
`AUTH_${upper}_REDIRECT_URI=`
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
if (features.workos) {
|
|
1599
|
+
env.push(
|
|
1600
|
+
"AUTH_WORKOS_PROVIDER=dashboard",
|
|
1601
|
+
"WORKOS_CLIENT_ID=",
|
|
1602
|
+
"WORKOS_API_KEY=",
|
|
1603
|
+
"WORKOS_REDIRECT_URI="
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
if (features.clerk) {
|
|
1607
|
+
env.push(
|
|
1608
|
+
"CLERK_PUBLISHABLE_KEY=",
|
|
1609
|
+
"CLERK_SECRET_KEY=",
|
|
1610
|
+
"CLERK_API_URL=",
|
|
1611
|
+
"CLERK_FRONTEND_API=",
|
|
1612
|
+
"CLERK_REDIRECT_URI=",
|
|
1613
|
+
"CLERK_SESSION_COOKIE=__session"
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
return {
|
|
1617
|
+
env,
|
|
1618
|
+
example: env.map((line) => `${line.split("=")[0]}=`)
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function renderAuthUserModel(_generatedSchemaImportPath = "../../.holo-js/generated/schema.generated") {
|
|
1622
|
+
return [
|
|
1623
|
+
"import { defineModel } from '@holo-js/db'",
|
|
1624
|
+
"",
|
|
1625
|
+
"export default defineModel('users', {",
|
|
1626
|
+
" fillable: ['name', 'email', 'password', 'avatar'],",
|
|
1627
|
+
" hidden: ['password'],",
|
|
1628
|
+
"})",
|
|
1629
|
+
""
|
|
1630
|
+
].join("\n");
|
|
1631
|
+
}
|
|
1632
|
+
function renderAuthEmailVerificationNotification() {
|
|
1633
|
+
return [
|
|
1634
|
+
"import { defineNotification } from '@holo-js/notifications'",
|
|
1635
|
+
"",
|
|
1636
|
+
"interface EmailVerificationNotification {",
|
|
1637
|
+
" readonly email: string",
|
|
1638
|
+
" readonly name?: string",
|
|
1639
|
+
" readonly url: string",
|
|
1640
|
+
" readonly expiresAt: Date",
|
|
1641
|
+
"}",
|
|
1642
|
+
"",
|
|
1643
|
+
"export default defineNotification({",
|
|
1644
|
+
" type: 'auth.email-verification',",
|
|
1645
|
+
" via() {",
|
|
1646
|
+
" return ['email']",
|
|
1647
|
+
" },",
|
|
1648
|
+
" build: {",
|
|
1649
|
+
" email(data: EmailVerificationNotification) {",
|
|
1650
|
+
" return {",
|
|
1651
|
+
" subject: 'Verify your email address',",
|
|
1652
|
+
" greeting: data.name ? `Hello ${data.name},` : undefined,",
|
|
1653
|
+
" lines: [",
|
|
1654
|
+
" 'Confirm your account to finish signing in.',",
|
|
1655
|
+
" `This verification link expires at ${data.expiresAt.toUTCString()}.`,",
|
|
1656
|
+
" ],",
|
|
1657
|
+
" action: {",
|
|
1658
|
+
" label: 'Verify email address',",
|
|
1659
|
+
" url: data.url,",
|
|
1660
|
+
" },",
|
|
1661
|
+
" }",
|
|
1662
|
+
" },",
|
|
1663
|
+
" },",
|
|
1664
|
+
"})",
|
|
1665
|
+
""
|
|
1666
|
+
].join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
function renderAuthPasswordResetNotification() {
|
|
1669
|
+
return [
|
|
1670
|
+
"import { defineNotification } from '@holo-js/notifications'",
|
|
1671
|
+
"",
|
|
1672
|
+
"interface PasswordResetNotification {",
|
|
1673
|
+
" readonly email: string",
|
|
1674
|
+
" readonly url: string",
|
|
1675
|
+
" readonly expiresAt: Date",
|
|
1676
|
+
"}",
|
|
1677
|
+
"",
|
|
1678
|
+
"export default defineNotification({",
|
|
1679
|
+
" type: 'auth.password-reset',",
|
|
1680
|
+
" via() {",
|
|
1681
|
+
" return ['email']",
|
|
1682
|
+
" },",
|
|
1683
|
+
" build: {",
|
|
1684
|
+
" email(data: PasswordResetNotification) {",
|
|
1685
|
+
" return {",
|
|
1686
|
+
" subject: 'Reset your password',",
|
|
1687
|
+
" lines: [",
|
|
1688
|
+
" 'Click the link below to choose a new password.',",
|
|
1689
|
+
" `This reset link expires at ${data.expiresAt.toUTCString()}.`,",
|
|
1690
|
+
" ],",
|
|
1691
|
+
" action: {",
|
|
1692
|
+
" label: 'Reset password',",
|
|
1693
|
+
" url: data.url,",
|
|
1694
|
+
" },",
|
|
1695
|
+
" }",
|
|
1696
|
+
" },",
|
|
1697
|
+
" },",
|
|
1698
|
+
"})",
|
|
1699
|
+
""
|
|
1700
|
+
].join("\n");
|
|
1701
|
+
}
|
|
1702
|
+
function renderAuthorizationPoliciesReadme() {
|
|
1703
|
+
return [
|
|
1704
|
+
"# Authorization Policies",
|
|
1705
|
+
"",
|
|
1706
|
+
"Place policy files in this directory.",
|
|
1707
|
+
"Export `definePolicy(...)` definitions from `@holo-js/authorization`.",
|
|
1708
|
+
""
|
|
1709
|
+
].join("\n");
|
|
1710
|
+
}
|
|
1711
|
+
function renderAuthorizationAbilitiesReadme() {
|
|
1712
|
+
return [
|
|
1713
|
+
"# Authorization Abilities",
|
|
1714
|
+
"",
|
|
1715
|
+
"Place ability files in this directory.",
|
|
1716
|
+
"Export `defineAbility(...)` definitions from `@holo-js/authorization`.",
|
|
1717
|
+
""
|
|
1718
|
+
].join("\n");
|
|
1719
|
+
}
|
|
1720
|
+
function resolveAuthUserModelSchemaImportPath(userModelPath, generatedSchemaPath) {
|
|
1721
|
+
return relativeImportPath(userModelPath, generatedSchemaPath);
|
|
1722
|
+
}
|
|
1723
|
+
function renderAuthMigration(slug) {
|
|
1724
|
+
switch (slug) {
|
|
1725
|
+
case "create_users":
|
|
1726
|
+
return [
|
|
1727
|
+
"import { defineMigration, type MigrationContext } from '@holo-js/db'",
|
|
1728
|
+
"",
|
|
1729
|
+
"export default defineMigration({",
|
|
1730
|
+
" async up({ schema }: MigrationContext) {",
|
|
1731
|
+
" await schema.createTable('users', (table) => {",
|
|
1732
|
+
" table.id()",
|
|
1733
|
+
" table.string('name')",
|
|
1734
|
+
" table.string('email').unique()",
|
|
1735
|
+
" table.string('password').nullable()",
|
|
1736
|
+
" table.string('avatar').nullable()",
|
|
1737
|
+
" table.timestamp('email_verified_at').nullable()",
|
|
1738
|
+
" table.timestamps()",
|
|
1739
|
+
" })",
|
|
1740
|
+
" },",
|
|
1741
|
+
" async down({ schema }: MigrationContext) {",
|
|
1742
|
+
" await schema.dropTable('users')",
|
|
1743
|
+
" },",
|
|
1744
|
+
"})",
|
|
1745
|
+
""
|
|
1746
|
+
].join("\n");
|
|
1747
|
+
case "create_sessions":
|
|
1748
|
+
return [
|
|
1749
|
+
"import { defineMigration, type MigrationContext } from '@holo-js/db'",
|
|
1750
|
+
"",
|
|
1751
|
+
"export default defineMigration({",
|
|
1752
|
+
" async up({ schema }: MigrationContext) {",
|
|
1753
|
+
" await schema.createTable('sessions', (table) => {",
|
|
1754
|
+
" table.string('id').primaryKey()",
|
|
1755
|
+
" table.string('store').default('database')",
|
|
1756
|
+
" table.json('data')",
|
|
1757
|
+
" table.timestamp('created_at')",
|
|
1758
|
+
" table.timestamp('last_activity_at')",
|
|
1759
|
+
" table.timestamp('expires_at')",
|
|
1760
|
+
" table.timestamp('invalidated_at').nullable()",
|
|
1761
|
+
" table.string('remember_token_hash').nullable()",
|
|
1762
|
+
" table.index(['expires_at'])",
|
|
1763
|
+
" })",
|
|
1764
|
+
" },",
|
|
1765
|
+
" async down({ schema }: MigrationContext) {",
|
|
1766
|
+
" await schema.dropTable('sessions')",
|
|
1767
|
+
" },",
|
|
1768
|
+
"})",
|
|
1769
|
+
""
|
|
1770
|
+
].join("\n");
|
|
1771
|
+
case "create_auth_identities":
|
|
1772
|
+
return [
|
|
1773
|
+
"import { defineMigration, type MigrationContext } from '@holo-js/db'",
|
|
1774
|
+
"",
|
|
1775
|
+
"export default defineMigration({",
|
|
1776
|
+
" async up({ schema }: MigrationContext) {",
|
|
1777
|
+
" await schema.createTable('auth_identities', (table) => {",
|
|
1778
|
+
" table.id()",
|
|
1779
|
+
" table.string('user_id')",
|
|
1780
|
+
" table.string('guard').default('web')",
|
|
1781
|
+
" table.string('auth_provider').default('users')",
|
|
1782
|
+
" table.string('provider')",
|
|
1783
|
+
" table.string('provider_user_id')",
|
|
1784
|
+
" table.string('email').nullable()",
|
|
1785
|
+
" table.boolean('email_verified').default(false)",
|
|
1786
|
+
" table.json('profile')",
|
|
1787
|
+
" table.json('tokens')",
|
|
1788
|
+
" table.timestamps()",
|
|
1789
|
+
" table.index(['user_id'])",
|
|
1790
|
+
" table.unique(['provider', 'provider_user_id'], 'auth_identities_provider_user_unique')",
|
|
1791
|
+
" })",
|
|
1792
|
+
" },",
|
|
1793
|
+
" async down({ schema }: MigrationContext) {",
|
|
1794
|
+
" await schema.dropTable('auth_identities')",
|
|
1795
|
+
" },",
|
|
1796
|
+
"})",
|
|
1797
|
+
""
|
|
1798
|
+
].join("\n");
|
|
1799
|
+
case "create_personal_access_tokens":
|
|
1800
|
+
return [
|
|
1801
|
+
"import { defineMigration, type MigrationContext } from '@holo-js/db'",
|
|
1802
|
+
"",
|
|
1803
|
+
"export default defineMigration({",
|
|
1804
|
+
" async up({ schema }: MigrationContext) {",
|
|
1805
|
+
" await schema.createTable('personal_access_tokens', (table) => {",
|
|
1806
|
+
" table.uuid('id').primaryKey()",
|
|
1807
|
+
" table.string('provider').default('users')",
|
|
1808
|
+
" table.string('user_id')",
|
|
1809
|
+
" table.string('name')",
|
|
1810
|
+
" table.string('token_hash').unique()",
|
|
1811
|
+
" table.json('abilities')",
|
|
1812
|
+
" table.timestamp('last_used_at').nullable()",
|
|
1813
|
+
" table.timestamp('expires_at').nullable()",
|
|
1814
|
+
" table.timestamps()",
|
|
1815
|
+
" table.index(['provider'])",
|
|
1816
|
+
" table.index(['user_id'])",
|
|
1817
|
+
" })",
|
|
1818
|
+
" },",
|
|
1819
|
+
" async down({ schema }: MigrationContext) {",
|
|
1820
|
+
" await schema.dropTable('personal_access_tokens')",
|
|
1821
|
+
" },",
|
|
1822
|
+
"})",
|
|
1823
|
+
""
|
|
1824
|
+
].join("\n");
|
|
1825
|
+
case "create_password_reset_tokens":
|
|
1826
|
+
return [
|
|
1827
|
+
"import { defineMigration, type MigrationContext } from '@holo-js/db'",
|
|
1828
|
+
"",
|
|
1829
|
+
"export default defineMigration({",
|
|
1830
|
+
" async up({ schema }: MigrationContext) {",
|
|
1831
|
+
" await schema.createTable('password_reset_tokens', (table) => {",
|
|
1832
|
+
" table.uuid('id').primaryKey()",
|
|
1833
|
+
" table.string('provider').default('users')",
|
|
1834
|
+
" table.string('email')",
|
|
1835
|
+
" table.string('token_hash')",
|
|
1836
|
+
" table.timestamp('expires_at')",
|
|
1837
|
+
" table.timestamp('used_at').nullable()",
|
|
1838
|
+
" table.timestamps()",
|
|
1839
|
+
" table.index(['provider'])",
|
|
1840
|
+
" table.index(['email'])",
|
|
1841
|
+
" })",
|
|
1842
|
+
" },",
|
|
1843
|
+
" async down({ schema }: MigrationContext) {",
|
|
1844
|
+
" await schema.dropTable('password_reset_tokens')",
|
|
1845
|
+
" },",
|
|
1846
|
+
"})",
|
|
1847
|
+
""
|
|
1848
|
+
].join("\n");
|
|
1849
|
+
case "create_email_verification_tokens":
|
|
1850
|
+
return [
|
|
1851
|
+
"import { defineMigration, type MigrationContext } from '@holo-js/db'",
|
|
1852
|
+
"",
|
|
1853
|
+
"export default defineMigration({",
|
|
1854
|
+
" async up({ schema }: MigrationContext) {",
|
|
1855
|
+
" await schema.createTable('email_verification_tokens', (table) => {",
|
|
1856
|
+
" table.uuid('id').primaryKey()",
|
|
1857
|
+
" table.string('provider').default('users')",
|
|
1858
|
+
" table.string('user_id')",
|
|
1859
|
+
" table.string('email')",
|
|
1860
|
+
" table.string('token_hash')",
|
|
1861
|
+
" table.timestamp('expires_at')",
|
|
1862
|
+
" table.timestamp('used_at').nullable()",
|
|
1863
|
+
" table.timestamps()",
|
|
1864
|
+
" table.index(['provider'])",
|
|
1865
|
+
" table.index(['user_id'])",
|
|
1866
|
+
" table.index(['email'])",
|
|
1867
|
+
" })",
|
|
1868
|
+
" },",
|
|
1869
|
+
" async down({ schema }: MigrationContext) {",
|
|
1870
|
+
" await schema.dropTable('email_verification_tokens')",
|
|
1871
|
+
" },",
|
|
1872
|
+
"})",
|
|
1873
|
+
""
|
|
1874
|
+
].join("\n");
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function createAuthMigrationFiles(date = /* @__PURE__ */ new Date()) {
|
|
1878
|
+
return AUTH_MIGRATION_SLUGS.map((slug, index) => ({
|
|
1879
|
+
path: createMigrationFileName(slug, new Date(date.getTime() + index * 1e3)),
|
|
1880
|
+
contents: renderAuthMigration(slug)
|
|
1881
|
+
}));
|
|
1882
|
+
}
|
|
1883
|
+
function renderNotificationsMigration() {
|
|
1884
|
+
return [
|
|
1885
|
+
"import { defineMigration, type MigrationContext } from '@holo-js/db'",
|
|
1886
|
+
"",
|
|
1887
|
+
"export default defineMigration({",
|
|
1888
|
+
" async up({ schema }: MigrationContext) {",
|
|
1889
|
+
" await schema.createTable('notifications', (table) => {",
|
|
1890
|
+
" table.string('id').primaryKey()",
|
|
1891
|
+
" table.string('type').nullable()",
|
|
1892
|
+
" table.string('notifiable_type')",
|
|
1893
|
+
" table.string('notifiable_id')",
|
|
1894
|
+
" table.json('data')",
|
|
1895
|
+
" table.timestamp('read_at').nullable()",
|
|
1896
|
+
" table.timestamp('created_at')",
|
|
1897
|
+
" table.timestamp('updated_at')",
|
|
1898
|
+
" table.index(['notifiable_type', 'notifiable_id'])",
|
|
1899
|
+
" table.index(['read_at'])",
|
|
1900
|
+
" })",
|
|
1901
|
+
" },",
|
|
1902
|
+
" async down({ schema }: MigrationContext) {",
|
|
1903
|
+
" await schema.dropTable('notifications')",
|
|
1904
|
+
" },",
|
|
1905
|
+
"})",
|
|
1906
|
+
""
|
|
1907
|
+
].join("\n");
|
|
1908
|
+
}
|
|
1909
|
+
function createNotificationsMigrationFiles(date = /* @__PURE__ */ new Date()) {
|
|
1910
|
+
return [{
|
|
1911
|
+
path: createMigrationFileName("create_notifications", date),
|
|
1912
|
+
contents: renderNotificationsMigration()
|
|
1913
|
+
}];
|
|
1914
|
+
}
|
|
1915
|
+
function resolveFrameworkDefaultUrl(framework) {
|
|
1916
|
+
return framework === "sveltekit" ? "http://localhost:5173" : "http://localhost:3000";
|
|
1917
|
+
}
|
|
1918
|
+
function renderScaffoldAppConfig(projectName, framework) {
|
|
1919
|
+
const defaultUrl = resolveFrameworkDefaultUrl(framework);
|
|
1920
|
+
return [
|
|
1921
|
+
"import { defineAppConfig, env } from '@holo-js/config'",
|
|
1922
|
+
"",
|
|
1923
|
+
"const appEnv = env('APP_ENV') === 'production'",
|
|
1924
|
+
" ? 'production'",
|
|
1925
|
+
" : env('APP_ENV') === 'test'",
|
|
1926
|
+
" ? 'test'",
|
|
1927
|
+
" : 'development'",
|
|
1928
|
+
"",
|
|
1929
|
+
"export default defineAppConfig({",
|
|
1930
|
+
` name: env('APP_NAME', ${JSON.stringify(projectName)}),`,
|
|
1931
|
+
" key: env('APP_KEY'),",
|
|
1932
|
+
` url: env('APP_URL', '${defaultUrl}'),`,
|
|
1933
|
+
" env: appEnv,",
|
|
1934
|
+
" debug: env('APP_DEBUG', true),",
|
|
1935
|
+
" paths: {",
|
|
1936
|
+
" models: 'server/models',",
|
|
1937
|
+
" migrations: 'server/db/migrations',",
|
|
1938
|
+
" seeders: 'server/db/seeders',",
|
|
1939
|
+
" commands: 'server/commands',",
|
|
1940
|
+
" jobs: 'server/jobs',",
|
|
1941
|
+
" events: 'server/events',",
|
|
1942
|
+
" listeners: 'server/listeners',",
|
|
1943
|
+
" generatedSchema: '.holo-js/generated/schema.generated.ts',",
|
|
1944
|
+
" },",
|
|
1945
|
+
"})",
|
|
1946
|
+
""
|
|
1947
|
+
].join("\n");
|
|
1948
|
+
}
|
|
1949
|
+
function renderScaffoldDatabaseConfig(options) {
|
|
1950
|
+
const packageName = sanitizePackageName(options.projectName) || "holo-app";
|
|
1951
|
+
if (options.databaseDriver === "sqlite") {
|
|
1952
|
+
return [
|
|
1953
|
+
"import { defineDatabaseConfig, env } from '@holo-js/config'",
|
|
1954
|
+
"",
|
|
1955
|
+
"export default defineDatabaseConfig({",
|
|
1956
|
+
" defaultConnection: 'main',",
|
|
1957
|
+
" connections: {",
|
|
1958
|
+
" main: {",
|
|
1959
|
+
" driver: 'sqlite',",
|
|
1960
|
+
" url: env('DB_URL', './storage/database.sqlite'),",
|
|
1961
|
+
" },",
|
|
1962
|
+
" },",
|
|
1963
|
+
"})",
|
|
1964
|
+
""
|
|
1965
|
+
].join("\n");
|
|
1966
|
+
}
|
|
1967
|
+
const port = options.databaseDriver === "mysql" ? "3306" : "5432";
|
|
1968
|
+
const username = options.databaseDriver === "mysql" ? "root" : "postgres";
|
|
1969
|
+
const schemaLine = options.databaseDriver === "postgres" ? " schema: env('DB_SCHEMA', 'public')," : void 0;
|
|
1970
|
+
return [
|
|
1971
|
+
"import { defineDatabaseConfig, env } from '@holo-js/config'",
|
|
1972
|
+
"",
|
|
1973
|
+
"export default defineDatabaseConfig({",
|
|
1974
|
+
" defaultConnection: 'main',",
|
|
1975
|
+
" connections: {",
|
|
1976
|
+
" main: {",
|
|
1977
|
+
` driver: '${options.databaseDriver}',`,
|
|
1978
|
+
" host: env('DB_HOST', '127.0.0.1'),",
|
|
1979
|
+
` port: env('DB_PORT', '${port}'),`,
|
|
1980
|
+
` username: env('DB_USERNAME', '${username}'),`,
|
|
1981
|
+
" password: env('DB_PASSWORD'),",
|
|
1982
|
+
` database: env('DB_DATABASE', '${packageName}'),`,
|
|
1983
|
+
...schemaLine ? [schemaLine] : [],
|
|
1984
|
+
" },",
|
|
1985
|
+
" },",
|
|
1986
|
+
"})",
|
|
1987
|
+
""
|
|
1988
|
+
].join("\n");
|
|
1989
|
+
}
|
|
1990
|
+
function resolveDefaultDatabaseUrl(driver) {
|
|
1991
|
+
if (driver === "sqlite") {
|
|
1992
|
+
return "./storage/database.sqlite";
|
|
1993
|
+
}
|
|
1994
|
+
return void 0;
|
|
1995
|
+
}
|
|
1996
|
+
function renderScaffoldEnvFiles(options) {
|
|
1997
|
+
const defaultDatabaseConnection = "main";
|
|
1998
|
+
const defaultUrl = resolveFrameworkDefaultUrl(options.framework);
|
|
1999
|
+
const optionalPackageNames = normalizeScaffoldOptionalPackages(options.optionalPackages);
|
|
2000
|
+
const baseLines = [
|
|
2001
|
+
"APP_NAME=",
|
|
2002
|
+
"APP_KEY=",
|
|
2003
|
+
`APP_URL=${defaultUrl}`,
|
|
2004
|
+
"APP_ENV=development",
|
|
2005
|
+
"APP_DEBUG=true",
|
|
2006
|
+
`DB_DRIVER=${options.databaseDriver}`
|
|
2007
|
+
];
|
|
2008
|
+
const driverLines = options.databaseDriver === "sqlite" ? [
|
|
2009
|
+
`DB_URL=${resolveDefaultDatabaseUrl(options.databaseDriver)}`
|
|
2010
|
+
] : [
|
|
2011
|
+
"DB_HOST=127.0.0.1",
|
|
2012
|
+
`DB_PORT=${options.databaseDriver === "mysql" ? "3306" : "5432"}`,
|
|
2013
|
+
`DB_USERNAME=${options.databaseDriver === "mysql" ? "root" : "postgres"}`,
|
|
2014
|
+
"DB_PASSWORD=",
|
|
2015
|
+
`DB_DATABASE=${sanitizePackageName(options.projectName) || "holo_app"}`,
|
|
2016
|
+
...options.databaseDriver === "postgres" ? ["DB_SCHEMA=public"] : []
|
|
2017
|
+
];
|
|
2018
|
+
const storageLines = optionalPackageNames.includes("storage") ? [
|
|
2019
|
+
`STORAGE_DEFAULT_DISK=${options.storageDefaultDisk}`,
|
|
2020
|
+
"STORAGE_ROUTE_PREFIX=/storage"
|
|
2021
|
+
] : [];
|
|
2022
|
+
const authLines = optionalPackageNames.includes("auth") ? [...renderAuthEnvFiles({}, defaultDatabaseConnection).env] : [];
|
|
2023
|
+
const cacheLines = optionalPackageNames.includes("cache") ? [...renderCacheEnvFiles("file").env] : [];
|
|
2024
|
+
const mailLines = optionalPackageNames.includes("mail") ? [...renderMailEnvFiles().env] : [];
|
|
2025
|
+
const mailExampleLines = optionalPackageNames.includes("mail") ? [...renderMailEnvFiles().example] : [];
|
|
2026
|
+
const envGroups = [
|
|
2027
|
+
baseLines,
|
|
2028
|
+
driverLines,
|
|
2029
|
+
storageLines,
|
|
2030
|
+
authLines,
|
|
2031
|
+
cacheLines,
|
|
2032
|
+
mailLines
|
|
2033
|
+
];
|
|
2034
|
+
const exampleGroups = [
|
|
2035
|
+
baseLines.map(renderEnvExampleLine),
|
|
2036
|
+
driverLines.map(renderEnvExampleLine),
|
|
2037
|
+
storageLines.map(renderEnvExampleLine),
|
|
2038
|
+
authLines.map(renderEnvExampleLine),
|
|
2039
|
+
cacheLines.map(renderEnvExampleLine),
|
|
2040
|
+
mailExampleLines.map(renderEnvExampleLine)
|
|
2041
|
+
];
|
|
2042
|
+
const env = renderEnvGroups(envGroups);
|
|
2043
|
+
const example = [
|
|
2044
|
+
"# Copy this file to .env and fill in your local values.",
|
|
2045
|
+
"# Supported layered env files: .env.local, .env.development, .env.production, .env.prod, .env.test",
|
|
2046
|
+
renderEnvGroups(exampleGroups).trimEnd(),
|
|
2047
|
+
""
|
|
2048
|
+
].join("\n");
|
|
2049
|
+
return { env, example };
|
|
2050
|
+
}
|
|
2051
|
+
function renderEnvGroups(groups) {
|
|
2052
|
+
return `${groups.filter((group) => group.length > 0).map((group) => group.join("\n")).join("\n\n")}
|
|
2053
|
+
`;
|
|
2054
|
+
}
|
|
2055
|
+
function renderEnvExampleLine(line) {
|
|
2056
|
+
const [key] = line.split("=");
|
|
2057
|
+
return `${key ?? line}=`;
|
|
2058
|
+
}
|
|
2059
|
+
function renderMailEnvFiles() {
|
|
2060
|
+
return {
|
|
2061
|
+
env: [
|
|
2062
|
+
"MAIL_MAILER=preview",
|
|
2063
|
+
"MAIL_FROM_ADDRESS=hello@app.test",
|
|
2064
|
+
"MAIL_FROM_NAME=Holo App",
|
|
2065
|
+
"MAIL_LOG_BODIES=false",
|
|
2066
|
+
"MAIL_HOST=127.0.0.1",
|
|
2067
|
+
"MAIL_PORT=1025",
|
|
2068
|
+
"MAIL_SECURE=false",
|
|
2069
|
+
"MAIL_USERNAME=",
|
|
2070
|
+
"MAIL_PASSWORD="
|
|
2071
|
+
],
|
|
2072
|
+
example: [
|
|
2073
|
+
"MAIL_MAILER=",
|
|
2074
|
+
"MAIL_FROM_ADDRESS=",
|
|
2075
|
+
"MAIL_FROM_NAME=",
|
|
2076
|
+
"MAIL_LOG_BODIES=",
|
|
2077
|
+
"MAIL_HOST=",
|
|
2078
|
+
"MAIL_PORT=",
|
|
2079
|
+
"MAIL_SECURE=",
|
|
2080
|
+
"MAIL_USERNAME=",
|
|
2081
|
+
"MAIL_PASSWORD="
|
|
2082
|
+
]
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
function renderRedisConnectionEnvFiles() {
|
|
2086
|
+
return {
|
|
2087
|
+
env: [
|
|
2088
|
+
"REDIS_URL=",
|
|
2089
|
+
"REDIS_HOST=127.0.0.1",
|
|
2090
|
+
"REDIS_PORT=6379",
|
|
2091
|
+
"REDIS_USERNAME=",
|
|
2092
|
+
"REDIS_PASSWORD=",
|
|
2093
|
+
"REDIS_DB=0"
|
|
2094
|
+
],
|
|
2095
|
+
example: [
|
|
2096
|
+
"REDIS_URL=",
|
|
2097
|
+
"REDIS_HOST=",
|
|
2098
|
+
"REDIS_PORT=",
|
|
2099
|
+
"REDIS_USERNAME=",
|
|
2100
|
+
"REDIS_PASSWORD=",
|
|
2101
|
+
"REDIS_DB="
|
|
2102
|
+
]
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
function renderQueueEnvFiles(driver) {
|
|
2106
|
+
if (driver !== "redis") {
|
|
2107
|
+
return {
|
|
2108
|
+
env: [],
|
|
2109
|
+
example: []
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
return renderRedisConnectionEnvFiles();
|
|
2113
|
+
}
|
|
2114
|
+
function renderCacheEnvFiles(driver) {
|
|
2115
|
+
if (driver === "redis") {
|
|
2116
|
+
const redis = renderRedisConnectionEnvFiles();
|
|
2117
|
+
return {
|
|
2118
|
+
env: [
|
|
2119
|
+
"CACHE_PREFIX=",
|
|
2120
|
+
...redis.env
|
|
2121
|
+
],
|
|
2122
|
+
example: [
|
|
2123
|
+
"CACHE_PREFIX=",
|
|
2124
|
+
...redis.example
|
|
2125
|
+
]
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
return {
|
|
2129
|
+
env: [
|
|
2130
|
+
"CACHE_PREFIX="
|
|
2131
|
+
],
|
|
2132
|
+
example: [
|
|
2133
|
+
"CACHE_PREFIX="
|
|
2134
|
+
]
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function parseEnvKey(line) {
|
|
2138
|
+
const trimmed = line.trim();
|
|
2139
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
2140
|
+
return void 0;
|
|
2141
|
+
}
|
|
2142
|
+
const normalized = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
2143
|
+
const separatorIndex = normalized.indexOf("=");
|
|
2144
|
+
if (separatorIndex <= 0) {
|
|
2145
|
+
return void 0;
|
|
2146
|
+
}
|
|
2147
|
+
return normalized.slice(0, separatorIndex).trim();
|
|
2148
|
+
}
|
|
2149
|
+
function renderEnvFileContents(segments) {
|
|
2150
|
+
const normalized = segments.map((segment) => segment.replace(/\n+$/, "")).filter((segment) => segment.length > 0);
|
|
2151
|
+
return normalized.length > 0 ? `${normalized.join("\n")}
|
|
2152
|
+
` : "";
|
|
2153
|
+
}
|
|
2154
|
+
function normalizeScaffoldEnvSegments(segments) {
|
|
2155
|
+
return segments.split("\n").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
2156
|
+
}
|
|
2157
|
+
function upsertEnvContents(existingContents, additions) {
|
|
2158
|
+
if (additions.length === 0) {
|
|
2159
|
+
return {
|
|
2160
|
+
contents: existingContents,
|
|
2161
|
+
changed: false
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
const nextLines = existingContents ? existingContents.replace(/\r\n/g, "\n").split("\n") : [];
|
|
2165
|
+
const existingKeys = new Set(nextLines.map(parseEnvKey).filter((value) => typeof value === "string"));
|
|
2166
|
+
const additionKeys = /* @__PURE__ */ new Set();
|
|
2167
|
+
const missingLines = additions.flatMap((line) => {
|
|
2168
|
+
const normalizedLine = line.trim();
|
|
2169
|
+
if (normalizedLine.length === 0 || normalizedLine.startsWith("#")) {
|
|
2170
|
+
return [];
|
|
2171
|
+
}
|
|
2172
|
+
const key = parseEnvKey(normalizedLine);
|
|
2173
|
+
if (!key || additionKeys.has(key) || existingKeys.has(key)) {
|
|
2174
|
+
return [];
|
|
2175
|
+
}
|
|
2176
|
+
additionKeys.add(key);
|
|
2177
|
+
return [normalizedLine];
|
|
2178
|
+
});
|
|
2179
|
+
if (missingLines.length === 0) {
|
|
2180
|
+
return {
|
|
2181
|
+
contents: existingContents,
|
|
2182
|
+
changed: false
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
if (nextLines.length > 0 && nextLines[nextLines.length - 1]?.trim() !== "") {
|
|
2186
|
+
nextLines.push("");
|
|
2187
|
+
}
|
|
2188
|
+
nextLines.push(...missingLines);
|
|
2189
|
+
return {
|
|
2190
|
+
contents: `${nextLines.join("\n").replace(/\n*$/, "")}
|
|
2191
|
+
`,
|
|
2192
|
+
changed: true
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// src/project/scaffold/workspace-renderers.ts
|
|
2197
|
+
function renderScaffoldGitignore() {
|
|
2198
|
+
return [
|
|
2199
|
+
"# Dependencies",
|
|
2200
|
+
"node_modules",
|
|
2201
|
+
"",
|
|
2202
|
+
"# Environment",
|
|
2203
|
+
".env",
|
|
2204
|
+
".env.local",
|
|
2205
|
+
".env.development",
|
|
2206
|
+
".env.production",
|
|
2207
|
+
".env.prod",
|
|
2208
|
+
".env.test",
|
|
2209
|
+
".env.*.local",
|
|
2210
|
+
"",
|
|
2211
|
+
"# Holo",
|
|
2212
|
+
".holo-js/generated",
|
|
2213
|
+
".holo-js/runtime",
|
|
2214
|
+
".holo-cli",
|
|
2215
|
+
"",
|
|
2216
|
+
"# Nuxt",
|
|
2217
|
+
".nuxt",
|
|
2218
|
+
".output",
|
|
2219
|
+
".nitro",
|
|
2220
|
+
".data",
|
|
2221
|
+
".netlify",
|
|
2222
|
+
".vercel",
|
|
2223
|
+
"",
|
|
2224
|
+
"# Next.js",
|
|
2225
|
+
".next",
|
|
2226
|
+
"out",
|
|
2227
|
+
"next-env.d.ts",
|
|
2228
|
+
"",
|
|
2229
|
+
"# SvelteKit",
|
|
2230
|
+
".svelte-kit",
|
|
2231
|
+
"build",
|
|
2232
|
+
"",
|
|
2233
|
+
"# Build / Misc",
|
|
2234
|
+
"dist",
|
|
2235
|
+
"coverage",
|
|
2236
|
+
"*.log",
|
|
2237
|
+
"*.tsbuildinfo",
|
|
2238
|
+
"",
|
|
2239
|
+
"# Database",
|
|
2240
|
+
"*.db",
|
|
2241
|
+
"*.sqlite",
|
|
2242
|
+
"*.sqlite3",
|
|
2243
|
+
"*.sqlite-wal",
|
|
2244
|
+
"*.sqlite-shm",
|
|
2245
|
+
"",
|
|
2246
|
+
"# Storage",
|
|
2247
|
+
"storage",
|
|
2248
|
+
"",
|
|
2249
|
+
"# OS",
|
|
2250
|
+
".DS_Store",
|
|
2251
|
+
"Thumbs.db",
|
|
2252
|
+
""
|
|
2253
|
+
].join("\n");
|
|
2254
|
+
}
|
|
2255
|
+
function renderScaffoldTsconfig(options) {
|
|
2256
|
+
if (options.framework === "nuxt") {
|
|
2257
|
+
return `${JSON.stringify({
|
|
2258
|
+
extends: "./.nuxt/tsconfig.json"
|
|
2259
|
+
}, null, 2)}
|
|
2260
|
+
`;
|
|
2261
|
+
}
|
|
2262
|
+
if (options.framework === "sveltekit") {
|
|
2263
|
+
return `${JSON.stringify({
|
|
2264
|
+
extends: "./.svelte-kit/tsconfig.json",
|
|
2265
|
+
compilerOptions: {
|
|
2266
|
+
strict: true,
|
|
2267
|
+
noEmit: true,
|
|
2268
|
+
skipLibCheck: true
|
|
2269
|
+
},
|
|
2270
|
+
include: [
|
|
2271
|
+
"src/**/*.ts",
|
|
2272
|
+
"src/**/*.svelte",
|
|
2273
|
+
"server/**/*.ts",
|
|
2274
|
+
"config/**/*.ts",
|
|
2275
|
+
".holo-js/generated/**/*.ts",
|
|
2276
|
+
".holo-js/generated/**/*.d.ts",
|
|
2277
|
+
"vite.config.ts"
|
|
2278
|
+
]
|
|
2279
|
+
}, null, 2)}
|
|
2280
|
+
`;
|
|
2281
|
+
}
|
|
2282
|
+
const include = ["next-env.d.ts", "app/**/*.ts", "app/**/*.tsx", "server/**/*.ts", "config/**/*.ts", ".holo-js/generated/**/*.ts", ".holo-js/generated/**/*.d.ts"];
|
|
2283
|
+
return `${JSON.stringify({
|
|
2284
|
+
compilerOptions: {
|
|
2285
|
+
target: "ES2022",
|
|
2286
|
+
module: "ESNext",
|
|
2287
|
+
moduleResolution: "Bundler",
|
|
2288
|
+
strict: true,
|
|
2289
|
+
noEmit: true,
|
|
2290
|
+
skipLibCheck: true,
|
|
2291
|
+
baseUrl: ".",
|
|
2292
|
+
jsx: "preserve",
|
|
2293
|
+
paths: {
|
|
2294
|
+
"~/*": ["./*"],
|
|
2295
|
+
"@/*": ["./*"]
|
|
2296
|
+
}
|
|
2297
|
+
},
|
|
2298
|
+
include
|
|
2299
|
+
}, null, 2)}
|
|
2300
|
+
`;
|
|
2301
|
+
}
|
|
2302
|
+
function renderVSCodeSettings(options) {
|
|
2303
|
+
if (options.framework !== "nuxt" && options.framework !== "sveltekit") {
|
|
2304
|
+
return void 0;
|
|
2305
|
+
}
|
|
2306
|
+
const settings = {
|
|
2307
|
+
"typescript.tsdk": "node_modules/typescript/lib",
|
|
2308
|
+
"typescript.enablePromptUseWorkspaceTsdk": true
|
|
2309
|
+
};
|
|
2310
|
+
if (options.framework === "nuxt") {
|
|
2311
|
+
settings["vue.server.hybridMode"] = true;
|
|
2312
|
+
}
|
|
2313
|
+
return `${JSON.stringify(settings, null, 2)}
|
|
2314
|
+
`;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// src/project/scaffold/framework.ts
|
|
2318
|
+
function resolvePackageManagerVersion(value) {
|
|
2319
|
+
return SCAFFOLD_PACKAGE_MANAGER_VERSIONS[value];
|
|
2320
|
+
}
|
|
2321
|
+
function renderScaffoldPackageJson(options) {
|
|
2322
|
+
const packageName = sanitizePackageName(options.projectName) || "holo-app";
|
|
2323
|
+
const optionalPackages = normalizeScaffoldOptionalPackages(options.optionalPackages);
|
|
2324
|
+
const dependencies = {
|
|
2325
|
+
"@holo-js/cli": `^${HOLO_PACKAGE_VERSION}`,
|
|
2326
|
+
"@holo-js/config": `^${HOLO_PACKAGE_VERSION}`,
|
|
2327
|
+
"@holo-js/core": `^${HOLO_PACKAGE_VERSION}`,
|
|
2328
|
+
"@holo-js/db": `^${HOLO_PACKAGE_VERSION}`,
|
|
2329
|
+
[DB_DRIVER_PACKAGE_NAMES[options.databaseDriver]]: `^${HOLO_PACKAGE_VERSION}`,
|
|
2330
|
+
esbuild: ESBUILD_PACKAGE_VERSION
|
|
2331
|
+
};
|
|
2332
|
+
const devDependencies = {
|
|
2333
|
+
typescript: SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS.typescript,
|
|
2334
|
+
"@types/node": SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS["@types/node"],
|
|
2335
|
+
eslint: SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS.eslint
|
|
2336
|
+
};
|
|
2337
|
+
if (options.framework === "nuxt") {
|
|
2338
|
+
dependencies.nuxt = SCAFFOLD_FRAMEWORK_VERSIONS.nuxt;
|
|
2339
|
+
dependencies.vue = SCAFFOLD_NUXT_DEPENDENCY_VERSIONS.vue;
|
|
2340
|
+
dependencies["vue-router"] = SCAFFOLD_NUXT_DEPENDENCY_VERSIONS["vue-router"];
|
|
2341
|
+
dependencies["@holo-js/adapter-nuxt"] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.nuxt;
|
|
2342
|
+
devDependencies["vue-tsc"] = SCAFFOLD_NUXT_DEPENDENCY_VERSIONS["vue-tsc"];
|
|
2343
|
+
}
|
|
2344
|
+
if (options.framework === "next") {
|
|
2345
|
+
dependencies.next = SCAFFOLD_FRAMEWORK_VERSIONS.next;
|
|
2346
|
+
dependencies.react = SCAFFOLD_NEXT_REACT_VERSIONS.react;
|
|
2347
|
+
dependencies["react-dom"] = SCAFFOLD_NEXT_REACT_VERSIONS["react-dom"];
|
|
2348
|
+
dependencies["@holo-js/adapter-next"] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.next;
|
|
2349
|
+
devDependencies["@types/react"] = SCAFFOLD_NEXT_REACT_VERSIONS["@types/react"];
|
|
2350
|
+
devDependencies["@types/react-dom"] = SCAFFOLD_NEXT_REACT_VERSIONS["@types/react-dom"];
|
|
2351
|
+
}
|
|
2352
|
+
if (options.framework === "sveltekit") {
|
|
2353
|
+
dependencies["@holo-js/adapter-sveltekit"] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.sveltekit;
|
|
2354
|
+
dependencies["@sveltejs/adapter-node"] = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS["@sveltejs/adapter-node"];
|
|
2355
|
+
dependencies["@sveltejs/kit"] = SCAFFOLD_FRAMEWORK_VERSIONS.sveltekit;
|
|
2356
|
+
dependencies["@sveltejs/vite-plugin-svelte"] = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS["@sveltejs/vite-plugin-svelte"];
|
|
2357
|
+
dependencies.svelte = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS.svelte;
|
|
2358
|
+
dependencies.vite = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS.vite;
|
|
2359
|
+
devDependencies["svelte-check"] = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS["svelte-check"];
|
|
2360
|
+
}
|
|
2361
|
+
if (optionalPackages.includes("storage")) {
|
|
2362
|
+
dependencies["@holo-js/storage"] = SCAFFOLD_FRAMEWORK_RUNTIME_VERSIONS[options.framework]["@holo-js/storage"];
|
|
2363
|
+
}
|
|
2364
|
+
if (optionalPackages.includes("events")) {
|
|
2365
|
+
dependencies["@holo-js/events"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2366
|
+
dependencies["@holo-js/queue"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2367
|
+
}
|
|
2368
|
+
if (optionalPackages.includes("queue")) {
|
|
2369
|
+
dependencies["@holo-js/queue"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2370
|
+
}
|
|
2371
|
+
if (optionalPackages.includes("validation")) {
|
|
2372
|
+
dependencies["@holo-js/validation"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2373
|
+
}
|
|
2374
|
+
if (optionalPackages.includes("forms")) {
|
|
2375
|
+
dependencies["@holo-js/forms"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2376
|
+
}
|
|
2377
|
+
if (optionalPackages.includes("auth")) {
|
|
2378
|
+
dependencies["@holo-js/auth"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2379
|
+
dependencies["@holo-js/session"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2380
|
+
dependencies["@holo-js/security"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2381
|
+
}
|
|
2382
|
+
if (optionalPackages.includes("authorization")) {
|
|
2383
|
+
dependencies["@holo-js/authorization"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2384
|
+
}
|
|
2385
|
+
if (optionalPackages.includes("notifications")) {
|
|
2386
|
+
dependencies["@holo-js/notifications"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2387
|
+
}
|
|
2388
|
+
if (optionalPackages.includes("mail")) {
|
|
2389
|
+
dependencies["@holo-js/mail"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2390
|
+
}
|
|
2391
|
+
if (optionalPackages.includes("broadcast") || optionalPackages.includes("realtime")) {
|
|
2392
|
+
dependencies["@holo-js/broadcast"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2393
|
+
dependencies["@holo-js/flux"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2394
|
+
if (options.framework === "next") {
|
|
2395
|
+
dependencies["@holo-js/flux-react"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2396
|
+
} else if (options.framework === "nuxt") {
|
|
2397
|
+
dependencies["@holo-js/flux-vue"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2398
|
+
} else if (options.framework === "sveltekit") {
|
|
2399
|
+
dependencies["@holo-js/flux-svelte"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (optionalPackages.includes("realtime")) {
|
|
2403
|
+
dependencies["@holo-js/realtime"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2404
|
+
}
|
|
2405
|
+
if (optionalPackages.includes("security")) {
|
|
2406
|
+
dependencies["@holo-js/security"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2407
|
+
}
|
|
2408
|
+
if (optionalPackages.includes("cache")) {
|
|
2409
|
+
dependencies["@holo-js/cache"] = `^${HOLO_PACKAGE_VERSION}`;
|
|
2410
|
+
}
|
|
2411
|
+
return `${JSON.stringify({
|
|
2412
|
+
name: packageName,
|
|
2413
|
+
private: true,
|
|
2414
|
+
type: "module",
|
|
2415
|
+
packageManager: resolvePackageManagerVersion(options.packageManager),
|
|
2416
|
+
scripts: {
|
|
2417
|
+
...options.framework === "nuxt" ? { postinstall: "nuxt prepare" } : {},
|
|
2418
|
+
prepare: "holo key:generate && holo prepare",
|
|
2419
|
+
dev: "holo dev",
|
|
2420
|
+
build: "holo build",
|
|
2421
|
+
lint: options.framework === "nuxt" ? "eslint app config server shared tests *.d.ts --fix --no-warn-ignored --no-error-on-unmatched-pattern" : options.framework === "next" ? "eslint app config server tests --fix --no-warn-ignored --no-error-on-unmatched-pattern" : "eslint src config server tests --fix --no-warn-ignored --no-error-on-unmatched-pattern",
|
|
2422
|
+
typecheck: options.framework === "nuxt" ? "nuxt typecheck" : options.framework === "next" ? "tsc -p tsconfig.json --noEmit" : "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
2423
|
+
["config:cache"]: "holo config:cache",
|
|
2424
|
+
["config:clear"]: "holo config:clear",
|
|
2425
|
+
["holo:dev"]: "node ./.holo-js/framework/run.mjs dev",
|
|
2426
|
+
["holo:build"]: "node ./.holo-js/framework/run.mjs build"
|
|
2427
|
+
},
|
|
2428
|
+
dependencies,
|
|
2429
|
+
devDependencies
|
|
2430
|
+
}, null, 2)}
|
|
2431
|
+
`;
|
|
2432
|
+
}
|
|
2433
|
+
function appendScaffoldEnvGroup(contents, group) {
|
|
2434
|
+
const normalizedGroup = group?.map((line) => line.trim()).filter((line) => line.length > 0) ?? [];
|
|
2435
|
+
if (normalizedGroup.length === 0) {
|
|
2436
|
+
return contents;
|
|
2437
|
+
}
|
|
2438
|
+
const normalizedContents = contents.trimEnd();
|
|
2439
|
+
if (normalizedContents.length === 0) {
|
|
2440
|
+
return `${normalizedGroup.join("\n")}
|
|
2441
|
+
`;
|
|
2442
|
+
}
|
|
2443
|
+
return `${normalizedContents}
|
|
2444
|
+
|
|
2445
|
+
${normalizedGroup.join("\n")}
|
|
2446
|
+
`;
|
|
2447
|
+
}
|
|
2448
|
+
async function scaffoldProject(projectRoot, options) {
|
|
2449
|
+
const existingEntries = await readdir(projectRoot).catch(() => []);
|
|
2450
|
+
if (existingEntries.length > 0) {
|
|
2451
|
+
throw new Error(`Refusing to scaffold into a non-empty directory: ${projectRoot}`);
|
|
2452
|
+
}
|
|
2453
|
+
const { env, example } = renderScaffoldEnvFiles(options);
|
|
2454
|
+
const config = normalizeHoloProjectConfig();
|
|
2455
|
+
const generatedSchemaPath = resolveGeneratedSchemaPath(projectRoot, config);
|
|
2456
|
+
const optionalPackages = normalizeScaffoldOptionalPackages(options.optionalPackages);
|
|
2457
|
+
const storageEnabled = optionalPackages.includes("storage");
|
|
2458
|
+
const queueEnabled = optionalPackages.includes("queue");
|
|
2459
|
+
const eventsEnabled = optionalPackages.includes("events");
|
|
2460
|
+
const authEnabled = optionalPackages.includes("auth");
|
|
2461
|
+
const authorizationEnabled = optionalPackages.includes("authorization");
|
|
2462
|
+
const notificationsEnabled = optionalPackages.includes("notifications");
|
|
2463
|
+
const mailEnabled = optionalPackages.includes("mail");
|
|
2464
|
+
const broadcastEnabled = optionalPackages.includes("broadcast");
|
|
2465
|
+
const realtimeEnabled = optionalPackages.includes("realtime");
|
|
2466
|
+
const securityEnabled = optionalPackages.includes("security");
|
|
2467
|
+
const cacheEnabled = optionalPackages.includes("cache");
|
|
2468
|
+
const broadcastEnvFiles = broadcastEnabled ? renderBroadcastEnvFiles() : void 0;
|
|
2469
|
+
const scaffoldEnv = appendScaffoldEnvGroup(env, broadcastEnvFiles?.env);
|
|
2470
|
+
const scaffoldEnvExample = appendScaffoldEnvGroup(example, broadcastEnvFiles?.example);
|
|
2471
|
+
await mkdir2(projectRoot, { recursive: true });
|
|
2472
|
+
await mkdir2(resolve3(projectRoot, "config"), { recursive: true });
|
|
2473
|
+
await mkdir2(resolve3(projectRoot, ".holo-js", "framework"), { recursive: true });
|
|
2474
|
+
await mkdir2(resolve3(projectRoot, config.paths.models), { recursive: true });
|
|
2475
|
+
await mkdir2(resolve3(projectRoot, config.paths.commands), { recursive: true });
|
|
2476
|
+
if (queueEnabled) {
|
|
2477
|
+
await mkdir2(resolve3(projectRoot, config.paths.jobs), { recursive: true });
|
|
2478
|
+
}
|
|
2479
|
+
if (eventsEnabled) {
|
|
2480
|
+
await mkdir2(resolve3(projectRoot, config.paths.events), { recursive: true });
|
|
2481
|
+
await mkdir2(resolve3(projectRoot, config.paths.listeners), { recursive: true });
|
|
2482
|
+
}
|
|
2483
|
+
if (authorizationEnabled) {
|
|
2484
|
+
await mkdir2(resolve3(projectRoot, "server/policies"), { recursive: true });
|
|
2485
|
+
await mkdir2(resolve3(projectRoot, "server/abilities"), { recursive: true });
|
|
2486
|
+
}
|
|
2487
|
+
if (mailEnabled) {
|
|
2488
|
+
await mkdir2(resolve3(projectRoot, "server/mail"), { recursive: true });
|
|
2489
|
+
}
|
|
2490
|
+
if (broadcastEnabled) {
|
|
2491
|
+
await mkdir2(resolve3(projectRoot, "server/broadcast"), { recursive: true });
|
|
2492
|
+
await mkdir2(resolve3(projectRoot, "server/channels"), { recursive: true });
|
|
2493
|
+
}
|
|
2494
|
+
if (realtimeEnabled) {
|
|
2495
|
+
await mkdir2(resolve3(projectRoot, "server/realtime"), { recursive: true });
|
|
2496
|
+
}
|
|
2497
|
+
await mkdir2(resolve3(projectRoot, "server/db/factories"), { recursive: true });
|
|
2498
|
+
await mkdir2(resolve3(projectRoot, "server/db/migrations"), { recursive: true });
|
|
2499
|
+
await mkdir2(resolve3(projectRoot, "server/db/seeders"), { recursive: true });
|
|
2500
|
+
await mkdir2(resolve3(projectRoot, "server/db/schema"), { recursive: true });
|
|
2501
|
+
await mkdir2(resolve3(projectRoot, config.paths.observers), { recursive: true });
|
|
2502
|
+
await mkdir2(resolve3(projectRoot, "storage"), { recursive: true });
|
|
2503
|
+
if (storageEnabled) {
|
|
2504
|
+
await mkdir2(resolve3(projectRoot, "storage/app/public"), { recursive: true });
|
|
2505
|
+
}
|
|
2506
|
+
await writeFile(resolve3(projectRoot, "package.json"), renderScaffoldPackageJson(options), "utf8");
|
|
2507
|
+
await writeFile(resolve3(projectRoot, ".gitignore"), renderScaffoldGitignore(), "utf8");
|
|
2508
|
+
await writeFile(resolve3(projectRoot, ".env"), scaffoldEnv, "utf8");
|
|
2509
|
+
await writeFile(resolve3(projectRoot, ".env.example"), scaffoldEnvExample, "utf8");
|
|
2510
|
+
await writeFile(resolve3(projectRoot, "config/app.ts"), renderScaffoldAppConfig(options.projectName, options.framework), "utf8");
|
|
2511
|
+
await writeFile(resolve3(projectRoot, "config/database.ts"), renderScaffoldDatabaseConfig(options), "utf8");
|
|
2512
|
+
await writeFile(resolve3(projectRoot, "config/redis.ts"), renderRedisConfig(), "utf8");
|
|
2513
|
+
if (queueEnabled) {
|
|
2514
|
+
await writeFile(resolve3(projectRoot, "config/queue.ts"), renderQueueConfig({
|
|
2515
|
+
driver: "sync",
|
|
2516
|
+
defaultDatabaseConnection: "main"
|
|
2517
|
+
}), "utf8");
|
|
2518
|
+
}
|
|
2519
|
+
if (notificationsEnabled) {
|
|
2520
|
+
await writeFile(resolve3(projectRoot, "config/notifications.ts"), renderNotificationsConfig(), "utf8");
|
|
2521
|
+
for (const migrationFile of createNotificationsMigrationFiles()) {
|
|
2522
|
+
await writeFile(resolve3(projectRoot, config.paths.migrations, migrationFile.path), migrationFile.contents, "utf8");
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (mailEnabled) {
|
|
2526
|
+
await writeFile(resolve3(projectRoot, "config/mail.ts"), renderMailConfig(), "utf8");
|
|
2527
|
+
}
|
|
2528
|
+
if (broadcastEnabled) {
|
|
2529
|
+
await writeFile(resolve3(projectRoot, "config/broadcast.ts"), renderBroadcastConfig("esm", false, true), "utf8");
|
|
2530
|
+
}
|
|
2531
|
+
if (securityEnabled) {
|
|
2532
|
+
await writeFile(resolve3(projectRoot, "config/security.ts"), renderSecurityConfig(), "utf8");
|
|
2533
|
+
await writeFile(resolve3(projectRoot, "config/cors.ts"), renderCorsConfig(), "utf8");
|
|
2534
|
+
await ensureRateLimitStorageIgnore(projectRoot);
|
|
2535
|
+
}
|
|
2536
|
+
if (cacheEnabled) {
|
|
2537
|
+
await writeFile(resolve3(projectRoot, "config/cache.ts"), renderCacheConfig("file", "main"), "utf8");
|
|
2538
|
+
}
|
|
2539
|
+
if (authEnabled) {
|
|
2540
|
+
await writeFile(resolve3(projectRoot, "config/auth.ts"), renderAuthConfig(), "utf8");
|
|
2541
|
+
await writeFile(resolve3(projectRoot, "config/session.ts"), renderSessionConfig("main"), "utf8");
|
|
2542
|
+
if (!securityEnabled) {
|
|
2543
|
+
await writeFile(resolve3(projectRoot, "config/security.ts"), renderSecurityConfig(), "utf8");
|
|
2544
|
+
await writeFile(resolve3(projectRoot, "config/cors.ts"), renderCorsConfig(), "utf8");
|
|
2545
|
+
await ensureRateLimitStorageIgnore(projectRoot);
|
|
2546
|
+
}
|
|
2547
|
+
const userModelPath = resolve3(projectRoot, config.paths.models, "User.ts");
|
|
2548
|
+
await writeFile(
|
|
2549
|
+
userModelPath,
|
|
2550
|
+
renderAuthUserModel(resolveAuthUserModelSchemaImportPath(
|
|
2551
|
+
userModelPath,
|
|
2552
|
+
generatedSchemaPath
|
|
2553
|
+
)),
|
|
2554
|
+
"utf8"
|
|
2555
|
+
);
|
|
2556
|
+
for (const migrationFile of createAuthMigrationFiles()) {
|
|
2557
|
+
await writeFile(resolve3(projectRoot, config.paths.migrations, migrationFile.path), migrationFile.contents, "utf8");
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
if (authEnabled && notificationsEnabled) {
|
|
2561
|
+
await mkdir2(resolve3(projectRoot, "server/notifications/auth"), { recursive: true });
|
|
2562
|
+
await writeFile(
|
|
2563
|
+
resolve3(projectRoot, "server/notifications/auth/email-verification.ts"),
|
|
2564
|
+
renderAuthEmailVerificationNotification(),
|
|
2565
|
+
"utf8"
|
|
2566
|
+
);
|
|
2567
|
+
await writeFile(
|
|
2568
|
+
resolve3(projectRoot, "server/notifications/auth/password-reset.ts"),
|
|
2569
|
+
renderAuthPasswordResetNotification(),
|
|
2570
|
+
"utf8"
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
if (broadcastEnabled && authEnabled) {
|
|
2574
|
+
await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
|
|
2575
|
+
}
|
|
2576
|
+
if (authorizationEnabled) {
|
|
2577
|
+
await writeFile(resolve3(projectRoot, "server/policies/README.md"), renderAuthorizationPoliciesReadme(), "utf8");
|
|
2578
|
+
await writeFile(resolve3(projectRoot, "server/abilities/README.md"), renderAuthorizationAbilitiesReadme(), "utf8");
|
|
2579
|
+
}
|
|
2580
|
+
if (storageEnabled) {
|
|
2581
|
+
await writeFile(resolve3(projectRoot, "config/storage.ts"), renderStorageConfig(), "utf8");
|
|
2582
|
+
}
|
|
2583
|
+
await writeFile(resolve3(projectRoot, ".holo-js/framework/run.mjs"), renderFrameworkRunner(options), "utf8");
|
|
2584
|
+
await writeFile(resolve3(projectRoot, ".holo-js/framework/project.json"), `${JSON.stringify(options, null, 2)}
|
|
2585
|
+
`, "utf8");
|
|
2586
|
+
await writeFile(resolve3(projectRoot, "tsconfig.json"), renderScaffoldTsconfig(options), "utf8");
|
|
2587
|
+
const vscodeSettings = renderVSCodeSettings(options);
|
|
2588
|
+
if (vscodeSettings) {
|
|
2589
|
+
await mkdir2(resolve3(projectRoot, ".vscode"), { recursive: true });
|
|
2590
|
+
await writeFile(resolve3(projectRoot, ".vscode/settings.json"), vscodeSettings, "utf8");
|
|
2591
|
+
}
|
|
2592
|
+
await mkdir2(dirname(generatedSchemaPath), { recursive: true });
|
|
2593
|
+
await writeFile(generatedSchemaPath, renderGeneratedSchemaPlaceholder(), "utf8");
|
|
2594
|
+
await writeFile(resolve3(projectRoot, GENERATED_MODEL_TYPES_PATH), renderGeneratedModelTypes([]), "utf8");
|
|
2595
|
+
for (const file of renderFrameworkFiles(options)) {
|
|
2596
|
+
await writeTextFile(resolve3(projectRoot, file.path), file.contents);
|
|
2597
|
+
}
|
|
2598
|
+
if (options.databaseDriver === "sqlite") {
|
|
2599
|
+
await writeFile(resolve3(projectRoot, "storage/database.sqlite"), "", "utf8");
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// src/project/scaffold.ts
|
|
2604
|
+
async function resolveExistingModelPath(modelsRoot, modelName) {
|
|
2605
|
+
const supportedExtensions = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];
|
|
2606
|
+
for (const extension of supportedExtensions) {
|
|
2607
|
+
const candidate = resolve4(modelsRoot, `${modelName}${extension}`);
|
|
2608
|
+
if (await pathExists(candidate)) {
|
|
2609
|
+
return candidate;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return void 0;
|
|
2613
|
+
}
|
|
2614
|
+
function injectSvelteRealtimeVitePlugin(viteConfigContents) {
|
|
2615
|
+
if (/\bholoSvelteKitRealtime\b/.test(viteConfigContents)) {
|
|
2616
|
+
return void 0;
|
|
2617
|
+
}
|
|
2618
|
+
const adapterImportPattern = /import\s*\{([^}]*)\}\s*from\s*(['"])@holo-js\/adapter-sveltekit\/vite\2/;
|
|
2619
|
+
const svelteKitImportPattern = /(import\s*\{[^}]*\bsveltekit\b[^}]*\}\s*from\s*(['"])@sveltejs\/kit\/vite\2)/;
|
|
2620
|
+
const existingAdapterImport = adapterImportPattern.exec(viteConfigContents);
|
|
2621
|
+
let withImport = viteConfigContents;
|
|
2622
|
+
if (existingAdapterImport) {
|
|
2623
|
+
const imports = existingAdapterImport[1]?.split(",").map((value) => value.trim()).filter(Boolean) ?? [];
|
|
2624
|
+
withImport = viteConfigContents.replace(adapterImportPattern, `import { ${["holoSvelteKitRealtime", ...imports].join(", ")} } from ${existingAdapterImport[2]}@holo-js/adapter-sveltekit/vite${existingAdapterImport[2]}`);
|
|
2625
|
+
} else {
|
|
2626
|
+
withImport = viteConfigContents.replace(
|
|
2627
|
+
svelteKitImportPattern,
|
|
2628
|
+
[
|
|
2629
|
+
"$1",
|
|
2630
|
+
"import { holoSvelteKitRealtime } from '@holo-js/adapter-sveltekit/vite'"
|
|
2631
|
+
].join("\n")
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
if (withImport === viteConfigContents && !existingAdapterImport) {
|
|
2635
|
+
return {
|
|
2636
|
+
error: "Unable to add holoSvelteKitRealtime() to vite.config.ts because the SvelteKit Vite import could not be found."
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
const withPlugin = withImport.replace(/plugins\s*:\s*\[([\s\S]*?)\]/, (_match, plugins) => {
|
|
2640
|
+
const separator = plugins.trim() ? " " : "";
|
|
2641
|
+
return `plugins: [holoSvelteKitRealtime(),${separator}${plugins}]`;
|
|
2642
|
+
});
|
|
2643
|
+
if (withPlugin === withImport) {
|
|
2644
|
+
if (/\bplugins\s*:\s*(?!\[)|\bplugins\s*(?:,|})/.test(withImport)) {
|
|
2645
|
+
return {
|
|
2646
|
+
error: "Unable to add holoSvelteKitRealtime() to vite.config.ts because the plugins option is not an inline array."
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
return {
|
|
2650
|
+
error: "Unable to add holoSvelteKitRealtime() to vite.config.ts because no plugins array was found."
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
return withPlugin;
|
|
2654
|
+
}
|
|
2655
|
+
var realtimeDefinitionExtensions = /* @__PURE__ */ new Set([".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"]);
|
|
2656
|
+
async function collectRealtimeDefinitionFiles(root) {
|
|
2657
|
+
const entries = await readdir2(root, { withFileTypes: true }).catch(() => []);
|
|
2658
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
2659
|
+
const entryPath = resolve4(root, entry.name);
|
|
2660
|
+
if (entry.isDirectory()) {
|
|
2661
|
+
return await collectRealtimeDefinitionFiles(entryPath);
|
|
2662
|
+
}
|
|
2663
|
+
return realtimeDefinitionExtensions.has(extname2(entry.name)) ? [entryPath] : [];
|
|
2664
|
+
}));
|
|
2665
|
+
return files.flat().sort((left, right) => left.localeCompare(right));
|
|
2666
|
+
}
|
|
2667
|
+
async function renderNextRealtimeDefinitions(projectRoot) {
|
|
2668
|
+
const generatedRoot = resolve4(projectRoot, ".holo-js/generated/next");
|
|
2669
|
+
const files = await collectRealtimeDefinitionFiles(resolve4(projectRoot, "server/realtime"));
|
|
2670
|
+
const importPaths = files.map((filePath) => {
|
|
2671
|
+
const withoutExtension = filePath.slice(0, -extname2(filePath).length);
|
|
2672
|
+
const importPath = relative(generatedRoot, withoutExtension).split(sep).join("/");
|
|
2673
|
+
return importPath.startsWith(".") ? importPath : `./${importPath}`;
|
|
2674
|
+
});
|
|
2675
|
+
return renderNextGeneratedRealtimeDefinitions(importPaths);
|
|
2676
|
+
}
|
|
2677
|
+
async function resolveExistingAuthMigrationFiles(migrationsRoot) {
|
|
2678
|
+
const entries = await readdir2(migrationsRoot).catch(() => []);
|
|
2679
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2680
|
+
for (const entry of entries) {
|
|
2681
|
+
for (const slug of AUTH_MIGRATION_SLUGS) {
|
|
2682
|
+
if (entry.endsWith(`_${slug}.ts`) || entry.endsWith(`_${slug}.mts`) || entry.endsWith(`_${slug}.js`) || entry.endsWith(`_${slug}.mjs`) || entry.endsWith(`_${slug}.cts`) || entry.endsWith(`_${slug}.cjs`)) {
|
|
2683
|
+
resolved.set(slug, resolve4(migrationsRoot, entry));
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
return resolved;
|
|
2688
|
+
}
|
|
2689
|
+
async function resolveExistingNotificationsMigrationFiles(migrationsRoot) {
|
|
2690
|
+
const entries = await readdir2(migrationsRoot).catch(() => []);
|
|
2691
|
+
return entries.filter((entry) => entry.endsWith("_create_notifications.ts") || entry.endsWith("_create_notifications.mts") || entry.endsWith("_create_notifications.js") || entry.endsWith("_create_notifications.mjs") || entry.endsWith("_create_notifications.cts") || entry.endsWith("_create_notifications.cjs")).map((entry) => resolve4(migrationsRoot, entry));
|
|
2692
|
+
}
|
|
2693
|
+
var AUTH_NOTIFICATION_FILES = Object.freeze([
|
|
2694
|
+
Object.freeze({
|
|
2695
|
+
path: "server/notifications/auth/email-verification.ts",
|
|
2696
|
+
render: renderAuthEmailVerificationNotification
|
|
2697
|
+
}),
|
|
2698
|
+
Object.freeze({
|
|
2699
|
+
path: "server/notifications/auth/password-reset.ts",
|
|
2700
|
+
render: renderAuthPasswordResetNotification
|
|
2701
|
+
})
|
|
2702
|
+
]);
|
|
2703
|
+
async function writeAuthNotificationFiles(projectRoot) {
|
|
2704
|
+
const createdFiles = [];
|
|
2705
|
+
const skippedFiles = [];
|
|
2706
|
+
await mkdir3(resolve4(projectRoot, "server/notifications/auth"), { recursive: true });
|
|
2707
|
+
for (const file of AUTH_NOTIFICATION_FILES) {
|
|
2708
|
+
const filePath = resolve4(projectRoot, file.path);
|
|
2709
|
+
if (await pathExists(filePath)) {
|
|
2710
|
+
skippedFiles.push(filePath);
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
await writeTextFile(filePath, file.render());
|
|
2714
|
+
createdFiles.push(filePath);
|
|
2715
|
+
}
|
|
2716
|
+
return {
|
|
2717
|
+
createdFiles,
|
|
2718
|
+
skippedFiles
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
async function syncHostedAuthRouteFiles(projectRoot, features) {
|
|
2722
|
+
if (!features.workos && !features.clerk) {
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
const { dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
2726
|
+
const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
|
|
2727
|
+
if (!framework) {
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
for (const file of renderAuthProviderRouteFiles(framework, features)) {
|
|
2731
|
+
const targetPath = resolve4(projectRoot, file.path);
|
|
2732
|
+
if (await pathExists(targetPath)) {
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
await mkdir3(dirname2(targetPath), { recursive: true });
|
|
2736
|
+
await writeTextFile(targetPath, file.contents);
|
|
2737
|
+
}
|
|
2738
|
+
if (framework === "next") {
|
|
2739
|
+
for (const file of renderNextManagedHostedAuthRouteFiles(features)) {
|
|
2740
|
+
await writeTextFile(resolve4(projectRoot, file.path), file.contents);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
async function syncAuthRouteFiles(projectRoot) {
|
|
2745
|
+
const { dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
2746
|
+
const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
|
|
2747
|
+
if (!framework) {
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
for (const file of renderAuthRouteFiles(framework)) {
|
|
2751
|
+
const targetPath = resolve4(projectRoot, file.path);
|
|
2752
|
+
if (await pathExists(targetPath)) {
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
await writeTextFile(targetPath, file.contents);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
async function installAuthIntoProject(projectRoot, features = {}) {
|
|
2759
|
+
const project = await loadProjectConfig(projectRoot);
|
|
2760
|
+
const modelsRoot = resolve4(projectRoot, project.config.paths.models);
|
|
2761
|
+
const migrationsRoot = resolve4(projectRoot, project.config.paths.migrations);
|
|
2762
|
+
const defaultDatabaseConnection = project.config.database?.defaultConnection ?? "default";
|
|
2763
|
+
const authConfigPath = await resolveFirstExistingPath(projectRoot, AUTH_CONFIG_FILE_NAMES);
|
|
2764
|
+
const sessionConfigPath = await resolveFirstExistingPath(projectRoot, SESSION_CONFIG_FILE_NAMES);
|
|
2765
|
+
const securityConfigPath = await resolveFirstExistingPath(projectRoot, SECURITY_CONFIG_FILE_NAMES);
|
|
2766
|
+
const corsConfigPath = await resolveFirstExistingPath(projectRoot, CORS_CONFIG_FILE_NAMES);
|
|
2767
|
+
const userModelPath = await resolveExistingModelPath(modelsRoot, "User");
|
|
2768
|
+
const existingMigrationFiles = await resolveExistingAuthMigrationFiles(migrationsRoot);
|
|
2769
|
+
const hasAllAuthMigrations = AUTH_MIGRATION_SLUGS.every((slug) => existingMigrationFiles.has(slug));
|
|
2770
|
+
const existingAuthArtifacts = [
|
|
2771
|
+
authConfigPath,
|
|
2772
|
+
userModelPath,
|
|
2773
|
+
...AUTH_MIGRATION_SLUGS.map((slug) => existingMigrationFiles.get(slug))
|
|
2774
|
+
].filter((value) => typeof value === "string");
|
|
2775
|
+
if (authConfigPath && userModelPath && hasAllAuthMigrations) {
|
|
2776
|
+
const envPath2 = resolve4(projectRoot, ".env");
|
|
2777
|
+
const envExamplePath2 = resolve4(projectRoot, ".env.example");
|
|
2778
|
+
const currentAuthConfig = await readTextFile(authConfigPath) ?? "";
|
|
2779
|
+
const currentAuthFeatures = detectAuthInstallFeaturesFromConfig(currentAuthConfig);
|
|
2780
|
+
const nextAuthFeatures = mergeInstalledAuthFeatures(currentAuthFeatures, features);
|
|
2781
|
+
const authConfigModuleFormat = resolveConfigModuleFormat(authConfigPath, currentAuthConfig);
|
|
2782
|
+
const nextAuthConfig = renderAuthConfig(nextAuthFeatures, authConfigModuleFormat);
|
|
2783
|
+
const authEnvFiles2 = renderAuthEnvFiles(nextAuthFeatures, defaultDatabaseConnection);
|
|
2784
|
+
const nextEnv2 = upsertEnvContents(await readTextFile(envPath2), authEnvFiles2.env);
|
|
2785
|
+
const nextEnvExample2 = upsertEnvContents(await readTextFile(envExamplePath2), authEnvFiles2.example);
|
|
2786
|
+
const authConfigChanged = authFeaturesRequireConfigUpdate(features) && currentAuthConfig !== nextAuthConfig;
|
|
2787
|
+
if (authConfigChanged) {
|
|
2788
|
+
if (!canSafelyRewriteAuthConfig(currentAuthConfig, currentAuthFeatures, authConfigModuleFormat)) {
|
|
2789
|
+
throw new Error(
|
|
2790
|
+
`Auth support is already installed in ${projectRoot}, but ${authConfigPath} contains manual changes. Refusing to overwrite the existing auth config automatically.`
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
await writeTextFile(authConfigPath, nextAuthConfig);
|
|
2794
|
+
}
|
|
2795
|
+
if (nextEnv2.changed && typeof nextEnv2.contents === "string") {
|
|
2796
|
+
await writeTextFile(envPath2, nextEnv2.contents);
|
|
2797
|
+
}
|
|
2798
|
+
if (nextEnvExample2.changed && typeof nextEnvExample2.contents === "string") {
|
|
2799
|
+
await writeTextFile(envExamplePath2, nextEnvExample2.contents);
|
|
2800
|
+
}
|
|
2801
|
+
let createdSecurityConfig2 = false;
|
|
2802
|
+
if (!securityConfigPath) {
|
|
2803
|
+
await writeTextFile(resolve4(projectRoot, "config/security.ts"), renderSecurityConfig());
|
|
2804
|
+
createdSecurityConfig2 = true;
|
|
2805
|
+
}
|
|
2806
|
+
const createdCorsConfig2 = await ensureCorsConfigFile(projectRoot);
|
|
2807
|
+
await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
|
|
2808
|
+
await syncAuthRouteFiles(projectRoot);
|
|
2809
|
+
await syncHostedAuthRouteFiles(projectRoot, nextAuthFeatures);
|
|
2810
|
+
return {
|
|
2811
|
+
updatedPackageJson: await upsertAuthPackageDependencies(projectRoot, nextAuthFeatures),
|
|
2812
|
+
createdAuthConfig: authConfigChanged,
|
|
2813
|
+
createdSessionConfig: false,
|
|
2814
|
+
createdSecurityConfig: createdSecurityConfig2,
|
|
2815
|
+
createdCorsConfig: createdCorsConfig2,
|
|
2816
|
+
createdUserModel: false,
|
|
2817
|
+
createdMigrationFiles: [],
|
|
2818
|
+
updatedEnv: nextEnv2.changed,
|
|
2819
|
+
updatedEnvExample: nextEnvExample2.changed
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
const collisions = sessionConfigPath && existingAuthArtifacts.length === 0 ? [] : [
|
|
2823
|
+
...existingAuthArtifacts,
|
|
2824
|
+
...sessionConfigPath && existingAuthArtifacts.length > 0 ? [sessionConfigPath] : []
|
|
2825
|
+
];
|
|
2826
|
+
if (collisions.length > 0) {
|
|
2827
|
+
throw new Error(
|
|
2828
|
+
`Auth support is partially installed. Refusing to overwrite existing files in ${projectRoot}: ${collisions.join(", ")}`
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2831
|
+
const authConfigTargetPath = resolve4(projectRoot, "config/auth.ts");
|
|
2832
|
+
const sessionConfigTargetPath = resolve4(projectRoot, "config/session.ts");
|
|
2833
|
+
const userModelTargetPath = resolve4(modelsRoot, "User.ts");
|
|
2834
|
+
const generatedSchemaPath = resolveGeneratedSchemaPath(projectRoot, project.config);
|
|
2835
|
+
const migrationFiles = createAuthMigrationFiles();
|
|
2836
|
+
const authEnvFiles = renderAuthEnvFiles(features, defaultDatabaseConnection);
|
|
2837
|
+
await mkdir3(resolve4(projectRoot, "config"), { recursive: true });
|
|
2838
|
+
await mkdir3(modelsRoot, { recursive: true });
|
|
2839
|
+
await mkdir3(migrationsRoot, { recursive: true });
|
|
2840
|
+
await ensureRedisConfigFile(projectRoot);
|
|
2841
|
+
await writeTextFile(authConfigTargetPath, renderAuthConfig(features));
|
|
2842
|
+
if (!sessionConfigPath) {
|
|
2843
|
+
await writeTextFile(sessionConfigTargetPath, renderSessionConfig(defaultDatabaseConnection));
|
|
2844
|
+
}
|
|
2845
|
+
let createdSecurityConfig = false;
|
|
2846
|
+
if (!securityConfigPath) {
|
|
2847
|
+
await writeTextFile(resolve4(projectRoot, "config/security.ts"), renderSecurityConfig());
|
|
2848
|
+
createdSecurityConfig = true;
|
|
2849
|
+
}
|
|
2850
|
+
const createdCorsConfig = corsConfigPath ? false : await ensureCorsConfigFile(projectRoot);
|
|
2851
|
+
await writeTextFile(
|
|
2852
|
+
userModelTargetPath,
|
|
2853
|
+
renderAuthUserModel(resolveAuthUserModelSchemaImportPath(
|
|
2854
|
+
userModelTargetPath,
|
|
2855
|
+
generatedSchemaPath
|
|
2856
|
+
))
|
|
2857
|
+
);
|
|
2858
|
+
const createdMigrationFiles = [];
|
|
2859
|
+
for (const migrationFile of migrationFiles) {
|
|
2860
|
+
const migrationPath = resolve4(migrationsRoot, migrationFile.path);
|
|
2861
|
+
await writeTextFile(migrationPath, migrationFile.contents);
|
|
2862
|
+
createdMigrationFiles.push(migrationPath);
|
|
2863
|
+
}
|
|
2864
|
+
const envPath = resolve4(projectRoot, ".env");
|
|
2865
|
+
const envExamplePath = resolve4(projectRoot, ".env.example");
|
|
2866
|
+
const nextEnv = upsertEnvContents(await readTextFile(envPath), authEnvFiles.env);
|
|
2867
|
+
const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), authEnvFiles.example);
|
|
2868
|
+
if (nextEnv.changed && typeof nextEnv.contents === "string") {
|
|
2869
|
+
await writeTextFile(envPath, nextEnv.contents);
|
|
2870
|
+
}
|
|
2871
|
+
if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
|
|
2872
|
+
await writeTextFile(envExamplePath, nextEnvExample.contents);
|
|
2873
|
+
}
|
|
2874
|
+
await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
|
|
2875
|
+
await syncAuthRouteFiles(projectRoot);
|
|
2876
|
+
await syncHostedAuthRouteFiles(projectRoot, features);
|
|
2877
|
+
return {
|
|
2878
|
+
updatedPackageJson: await upsertAuthPackageDependencies(projectRoot, features),
|
|
2879
|
+
createdAuthConfig: true,
|
|
2880
|
+
createdSessionConfig: !sessionConfigPath,
|
|
2881
|
+
createdSecurityConfig,
|
|
2882
|
+
createdCorsConfig,
|
|
2883
|
+
createdUserModel: true,
|
|
2884
|
+
createdMigrationFiles,
|
|
2885
|
+
updatedEnv: nextEnv.changed,
|
|
2886
|
+
updatedEnvExample: nextEnvExample.changed
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
async function installAuthorizationIntoProject(projectRoot) {
|
|
2890
|
+
await loadProjectConfig(projectRoot, { required: true });
|
|
2891
|
+
const policiesRoot = resolve4(projectRoot, "server/policies");
|
|
2892
|
+
const abilitiesRoot = resolve4(projectRoot, "server/abilities");
|
|
2893
|
+
const policiesDirectoryExists = await pathExists(policiesRoot);
|
|
2894
|
+
const abilitiesDirectoryExists = await pathExists(abilitiesRoot);
|
|
2895
|
+
const policiesReadmePath = resolve4(policiesRoot, "README.md");
|
|
2896
|
+
const abilitiesReadmePath = resolve4(abilitiesRoot, "README.md");
|
|
2897
|
+
const policiesReadmeExists = await pathExists(policiesReadmePath);
|
|
2898
|
+
const abilitiesReadmeExists = await pathExists(abilitiesReadmePath);
|
|
2899
|
+
await mkdir3(policiesRoot, { recursive: true });
|
|
2900
|
+
await mkdir3(abilitiesRoot, { recursive: true });
|
|
2901
|
+
if (!policiesReadmeExists) {
|
|
2902
|
+
await writeTextFile(policiesReadmePath, renderAuthorizationPoliciesReadme());
|
|
2903
|
+
}
|
|
2904
|
+
if (!abilitiesReadmeExists) {
|
|
2905
|
+
await writeTextFile(abilitiesReadmePath, renderAuthorizationAbilitiesReadme());
|
|
2906
|
+
}
|
|
2907
|
+
return {
|
|
2908
|
+
updatedPackageJson: await upsertAuthorizationPackageDependency(projectRoot),
|
|
2909
|
+
createdPoliciesDirectory: !policiesDirectoryExists,
|
|
2910
|
+
createdAbilitiesDirectory: !abilitiesDirectoryExists,
|
|
2911
|
+
createdPoliciesReadme: !policiesReadmeExists,
|
|
2912
|
+
createdAbilitiesReadme: !abilitiesReadmeExists
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
async function installQueueIntoProject(projectRoot, options = {}) {
|
|
2916
|
+
const driver = options.driver ?? "sync";
|
|
2917
|
+
if (!isSupportedQueueInstallerDriver(driver)) {
|
|
2918
|
+
throw new Error(`Unsupported queue driver: ${driver}.`);
|
|
2919
|
+
}
|
|
2920
|
+
const project = await loadProjectConfig(projectRoot);
|
|
2921
|
+
const defaultDatabaseConnection = project.config.database?.defaultConnection ?? "default";
|
|
2922
|
+
const queueConfigPath = await resolveFirstExistingPath(projectRoot, QUEUE_CONFIG_FILE_NAMES) ?? resolve4(projectRoot, "config/queue.ts");
|
|
2923
|
+
const queueConfigExists = await pathExists(queueConfigPath);
|
|
2924
|
+
const jobsRoot = resolve4(projectRoot, project.config.paths.jobs);
|
|
2925
|
+
const jobsDirectoryExists = await pathExists(jobsRoot);
|
|
2926
|
+
const queueEnvFiles = renderQueueEnvFiles(driver);
|
|
2927
|
+
if (!queueConfigExists) {
|
|
2928
|
+
await writeTextFile(queueConfigPath, renderQueueConfig({
|
|
2929
|
+
driver,
|
|
2930
|
+
defaultDatabaseConnection
|
|
2931
|
+
}));
|
|
2932
|
+
}
|
|
2933
|
+
if (driver === "redis") {
|
|
2934
|
+
await ensureRedisConfigFile(projectRoot);
|
|
2935
|
+
}
|
|
2936
|
+
await mkdir3(jobsRoot, { recursive: true });
|
|
2937
|
+
const updatedPackageJson = await upsertQueuePackageDependency(
|
|
2938
|
+
projectRoot,
|
|
2939
|
+
!queueConfigExists || driver !== "sync" ? driver : void 0
|
|
2940
|
+
);
|
|
2941
|
+
const envPath = resolve4(projectRoot, ".env");
|
|
2942
|
+
const envExamplePath = resolve4(projectRoot, ".env.example");
|
|
2943
|
+
const nextEnv = upsertEnvContents(await readTextFile(envPath), queueEnvFiles.env);
|
|
2944
|
+
const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), queueEnvFiles.example);
|
|
2945
|
+
if (nextEnv.changed && typeof nextEnv.contents === "string") {
|
|
2946
|
+
await writeTextFile(envPath, nextEnv.contents);
|
|
2947
|
+
}
|
|
2948
|
+
if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
|
|
2949
|
+
await writeTextFile(envExamplePath, nextEnvExample.contents);
|
|
2950
|
+
}
|
|
2951
|
+
return {
|
|
2952
|
+
createdQueueConfig: !queueConfigExists,
|
|
2953
|
+
updatedPackageJson,
|
|
2954
|
+
updatedEnv: nextEnv.changed,
|
|
2955
|
+
updatedEnvExample: nextEnvExample.changed,
|
|
2956
|
+
createdJobsDirectory: !jobsDirectoryExists
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
async function installEventsIntoProject(projectRoot) {
|
|
2960
|
+
const project = await loadProjectConfig(projectRoot);
|
|
2961
|
+
const eventsRoot = resolve4(projectRoot, project.config.paths.events);
|
|
2962
|
+
const listenersRoot = resolve4(projectRoot, project.config.paths.listeners);
|
|
2963
|
+
const eventsDirectoryExists = await pathExists(eventsRoot);
|
|
2964
|
+
const listenersDirectoryExists = await pathExists(listenersRoot);
|
|
2965
|
+
await mkdir3(eventsRoot, { recursive: true });
|
|
2966
|
+
await mkdir3(listenersRoot, { recursive: true });
|
|
2967
|
+
return {
|
|
2968
|
+
updatedPackageJson: await upsertEventsPackageDependency(projectRoot),
|
|
2969
|
+
createdEventsDirectory: !eventsDirectoryExists,
|
|
2970
|
+
createdListenersDirectory: !listenersDirectoryExists
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
async function installRealtimeIntoProject(projectRoot) {
|
|
2974
|
+
await loadProjectConfig(projectRoot, { required: true });
|
|
2975
|
+
const realtimeRoot = resolve4(projectRoot, "server/realtime");
|
|
2976
|
+
const realtimeDirectoryExists = await pathExists(realtimeRoot);
|
|
2977
|
+
const { dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
2978
|
+
const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
|
|
2979
|
+
let createdFrameworkSetup = false;
|
|
2980
|
+
await mkdir3(realtimeRoot, { recursive: true });
|
|
2981
|
+
if (framework === "next") {
|
|
2982
|
+
const routes = [
|
|
2983
|
+
{ path: ".holo-js/generated/next/holo.ts", contents: renderNextHoloHelper() },
|
|
2984
|
+
{ path: ".holo-js/generated/next/bootstrap.mjs", contents: renderNextRuntimeBootstrap() },
|
|
2985
|
+
{ path: ".holo-js/generated/next/realtime-definitions.ts", contents: await renderNextRealtimeDefinitions(projectRoot) }
|
|
2986
|
+
];
|
|
2987
|
+
for (const route of routes) {
|
|
2988
|
+
const routePath = resolve4(projectRoot, route.path);
|
|
2989
|
+
if (!await pathExists(routePath)) {
|
|
2990
|
+
createdFrameworkSetup = true;
|
|
2991
|
+
}
|
|
2992
|
+
await writeTextFile(routePath, route.contents);
|
|
2993
|
+
}
|
|
2994
|
+
} else if (framework === "sveltekit") {
|
|
2995
|
+
const viteConfigPath = resolve4(projectRoot, "vite.config.ts");
|
|
2996
|
+
if (await pathExists(viteConfigPath)) {
|
|
2997
|
+
const existingViteConfig = await readTextFile(viteConfigPath);
|
|
2998
|
+
const viteConfigContents = existingViteConfig ? injectSvelteRealtimeVitePlugin(existingViteConfig) : void 0;
|
|
2999
|
+
if (typeof viteConfigContents === "string") {
|
|
3000
|
+
createdFrameworkSetup = true;
|
|
3001
|
+
await writeTextFile(viteConfigPath, viteConfigContents);
|
|
3002
|
+
} else if (viteConfigContents) {
|
|
3003
|
+
throw new Error(viteConfigContents.error);
|
|
3004
|
+
}
|
|
3005
|
+
} else {
|
|
3006
|
+
createdFrameworkSetup = true;
|
|
3007
|
+
await writeTextFile(viteConfigPath, renderSvelteViteConfig(false, true));
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return {
|
|
3011
|
+
updatedPackageJson: await upsertRealtimePackageDependency(projectRoot),
|
|
3012
|
+
createdRealtimeDirectory: !realtimeDirectoryExists,
|
|
3013
|
+
createdFrameworkSetup
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
async function installNotificationsIntoProject(projectRoot) {
|
|
3017
|
+
const project = await loadProjectConfig(projectRoot);
|
|
3018
|
+
const migrationsRoot = resolve4(projectRoot, project.config.paths.migrations);
|
|
3019
|
+
const notificationsConfigPath = await resolveFirstExistingPath(projectRoot, NOTIFICATIONS_CONFIG_FILE_NAMES);
|
|
3020
|
+
const existingMigrationFiles = await resolveExistingNotificationsMigrationFiles(migrationsRoot);
|
|
3021
|
+
await mkdir3(resolve4(projectRoot, "config"), { recursive: true });
|
|
3022
|
+
await mkdir3(migrationsRoot, { recursive: true });
|
|
3023
|
+
if (!notificationsConfigPath) {
|
|
3024
|
+
await writeTextFile(resolve4(projectRoot, "config/notifications.ts"), renderNotificationsConfig());
|
|
3025
|
+
}
|
|
3026
|
+
const createdMigrationFiles = [];
|
|
3027
|
+
if (existingMigrationFiles.length === 0) {
|
|
3028
|
+
for (const migrationFile of createNotificationsMigrationFiles()) {
|
|
3029
|
+
const migrationPath = resolve4(migrationsRoot, migrationFile.path);
|
|
3030
|
+
await writeTextFile(migrationPath, migrationFile.contents);
|
|
3031
|
+
createdMigrationFiles.push(migrationPath);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
return {
|
|
3035
|
+
updatedPackageJson: await upsertNotificationsPackageDependency(projectRoot),
|
|
3036
|
+
createdNotificationsConfig: !notificationsConfigPath,
|
|
3037
|
+
createdMigrationFiles
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
async function publishAuthNotificationsIntoProject(projectRoot) {
|
|
3041
|
+
await loadProjectConfig(projectRoot, { required: true });
|
|
3042
|
+
const dependencies = await readPackageJsonDependencyState(projectRoot);
|
|
3043
|
+
if (!dependencies.dependencies["@holo-js/auth"] && !dependencies.devDependencies["@holo-js/auth"]) {
|
|
3044
|
+
throw new Error(
|
|
3045
|
+
"Auth notification publishing requires @holo-js/auth. Install auth first with `holo install auth`."
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
if (!dependencies.dependencies["@holo-js/notifications"] && !dependencies.devDependencies["@holo-js/notifications"]) {
|
|
3049
|
+
throw new Error(
|
|
3050
|
+
"Auth notification publishing requires @holo-js/notifications. Install notifications first with `holo install notifications`."
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
const result = await writeAuthNotificationFiles(projectRoot);
|
|
3054
|
+
return {
|
|
3055
|
+
...result,
|
|
3056
|
+
hasMailDependency: !!dependencies.dependencies["@holo-js/mail"] || !!dependencies.devDependencies["@holo-js/mail"]
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
async function installMailIntoProject(projectRoot) {
|
|
3060
|
+
await loadProjectConfig(projectRoot, { required: true });
|
|
3061
|
+
const mailConfigPath = await resolveFirstExistingPath(projectRoot, MAIL_CONFIG_FILE_NAMES);
|
|
3062
|
+
const mailRoot = resolve4(projectRoot, "server/mail");
|
|
3063
|
+
const mailDirectoryExists = await pathExists(mailRoot);
|
|
3064
|
+
const envPath = resolve4(projectRoot, ".env");
|
|
3065
|
+
const envExamplePath = resolve4(projectRoot, ".env.example");
|
|
3066
|
+
await mkdir3(resolve4(projectRoot, "config"), { recursive: true });
|
|
3067
|
+
await mkdir3(mailRoot, { recursive: true });
|
|
3068
|
+
if (!mailConfigPath) {
|
|
3069
|
+
await writeTextFile(resolve4(projectRoot, "config/mail.ts"), renderMailConfig());
|
|
3070
|
+
}
|
|
3071
|
+
const mailEnvFiles = renderMailEnvFiles();
|
|
3072
|
+
const nextEnv = upsertEnvContents(await readTextFile(envPath), mailEnvFiles.env);
|
|
3073
|
+
const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), mailEnvFiles.example);
|
|
3074
|
+
if (nextEnv.changed && typeof nextEnv.contents === "string") {
|
|
3075
|
+
await writeTextFile(envPath, nextEnv.contents);
|
|
3076
|
+
}
|
|
3077
|
+
if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
|
|
3078
|
+
await writeTextFile(envExamplePath, nextEnvExample.contents);
|
|
3079
|
+
}
|
|
3080
|
+
return {
|
|
3081
|
+
updatedPackageJson: await upsertMailPackageDependency(projectRoot),
|
|
3082
|
+
createdMailConfig: !mailConfigPath,
|
|
3083
|
+
createdMailDirectory: !mailDirectoryExists,
|
|
3084
|
+
updatedEnv: nextEnv.changed,
|
|
3085
|
+
updatedEnvExample: nextEnvExample.changed
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
async function installMediaIntoProject(projectRoot) {
|
|
3089
|
+
await loadProjectConfig(projectRoot, { required: true });
|
|
3090
|
+
const mediaConfigPath = await resolveFirstExistingPath(projectRoot, MEDIA_CONFIG_FILE_NAMES);
|
|
3091
|
+
await mkdir3(resolve4(projectRoot, "config"), { recursive: true });
|
|
3092
|
+
if (!mediaConfigPath) {
|
|
3093
|
+
await writeTextFile(resolve4(projectRoot, "config/media.ts"), renderMediaConfig());
|
|
3094
|
+
}
|
|
3095
|
+
const { createMediaTableMigration } = await import("./media-migrations-VR7DLLR6.mjs");
|
|
3096
|
+
const migrationFilePath = await createMediaTableMigration(projectRoot, {
|
|
3097
|
+
skipIfExists: true
|
|
3098
|
+
});
|
|
3099
|
+
return {
|
|
3100
|
+
updatedPackageJson: await upsertMediaPackageDependency(projectRoot),
|
|
3101
|
+
createdMediaConfig: !mediaConfigPath,
|
|
3102
|
+
createdMigrationFiles: migrationFilePath ? [migrationFilePath] : []
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
async function installSecurityIntoProject(projectRoot) {
|
|
3106
|
+
await loadProjectConfig(projectRoot, { required: true });
|
|
3107
|
+
const securityConfigPath = await resolveFirstExistingPath(projectRoot, SECURITY_CONFIG_FILE_NAMES);
|
|
3108
|
+
const corsConfigPath = await resolveFirstExistingPath(projectRoot, CORS_CONFIG_FILE_NAMES);
|
|
3109
|
+
await mkdir3(resolve4(projectRoot, "config"), { recursive: true });
|
|
3110
|
+
await ensureRateLimitStorageIgnore(projectRoot);
|
|
3111
|
+
await ensureRedisConfigFile(projectRoot);
|
|
3112
|
+
if (!securityConfigPath) {
|
|
3113
|
+
await writeTextFile(resolve4(projectRoot, "config/security.ts"), renderSecurityConfig());
|
|
3114
|
+
}
|
|
3115
|
+
const createdCorsConfig = corsConfigPath ? false : await ensureCorsConfigFile(projectRoot);
|
|
3116
|
+
return {
|
|
3117
|
+
updatedPackageJson: await upsertSecurityPackageDependency(projectRoot),
|
|
3118
|
+
createdSecurityConfig: !securityConfigPath,
|
|
3119
|
+
createdCorsConfig
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
async function installCacheIntoProject(projectRoot, options = {}) {
|
|
3123
|
+
const project = await loadProjectConfig(projectRoot, { required: true });
|
|
3124
|
+
const driver = options.driver ?? "file";
|
|
3125
|
+
if (!isSupportedCacheInstallerDriver(driver)) {
|
|
3126
|
+
throw new Error(`Unsupported cache driver: ${driver}.`);
|
|
3127
|
+
}
|
|
3128
|
+
const cacheConfigPath = await resolveFirstExistingPath(projectRoot, CACHE_CONFIG_FILE_NAMES);
|
|
3129
|
+
const loadedConfig = await loadConfigDirectory2(projectRoot, {
|
|
3130
|
+
preferCache: false,
|
|
3131
|
+
processEnv: process.env
|
|
3132
|
+
});
|
|
3133
|
+
const defaultDatabaseConnection = project.config.database?.defaultConnection ?? "default";
|
|
3134
|
+
const defaultRedisConnection = loadedConfig.redis.default;
|
|
3135
|
+
const loadedCacheConfig = cacheConfigPath ? loadedConfig.cache : void 0;
|
|
3136
|
+
if (loadedCacheConfig && !Object.values(loadedCacheConfig.drivers).some((entry) => entry.driver === driver)) {
|
|
3137
|
+
throw new Error(
|
|
3138
|
+
`config/cache.ts already exists and does not configure the "${driver}" cache driver. Update your cache config first, then rerun "holo install cache".`
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
await mkdir3(resolve4(projectRoot, "config"), { recursive: true });
|
|
3142
|
+
if (!cacheConfigPath) {
|
|
3143
|
+
await writeTextFile(
|
|
3144
|
+
resolve4(projectRoot, "config/cache.ts"),
|
|
3145
|
+
renderCacheConfig(driver, defaultDatabaseConnection, defaultRedisConnection)
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3148
|
+
let createdRedisConfig = false;
|
|
3149
|
+
if (driver === "redis") {
|
|
3150
|
+
createdRedisConfig = await ensureRedisConfigFile(projectRoot);
|
|
3151
|
+
}
|
|
3152
|
+
const cacheEnvFiles = renderCacheEnvFiles(driver);
|
|
3153
|
+
const envPath = resolve4(projectRoot, ".env");
|
|
3154
|
+
const envExamplePath = resolve4(projectRoot, ".env.example");
|
|
3155
|
+
const nextEnv = upsertEnvContents(await readTextFile(envPath), cacheEnvFiles.env);
|
|
3156
|
+
const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), cacheEnvFiles.example);
|
|
3157
|
+
if (nextEnv.changed && typeof nextEnv.contents === "string") {
|
|
3158
|
+
await writeTextFile(envPath, nextEnv.contents);
|
|
3159
|
+
}
|
|
3160
|
+
if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
|
|
3161
|
+
await writeTextFile(envExamplePath, nextEnvExample.contents);
|
|
3162
|
+
}
|
|
3163
|
+
return {
|
|
3164
|
+
updatedPackageJson: await upsertCachePackageDependencies(projectRoot, driver),
|
|
3165
|
+
createdCacheConfig: !cacheConfigPath,
|
|
3166
|
+
createdRedisConfig,
|
|
3167
|
+
updatedEnv: nextEnv.changed,
|
|
3168
|
+
updatedEnvExample: nextEnvExample.changed,
|
|
3169
|
+
databaseDriver: driver === "database"
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
async function installBroadcastIntoProject(projectRoot) {
|
|
3173
|
+
const project = await loadProjectConfig(projectRoot, { required: true });
|
|
3174
|
+
const manifestPath = project.manifestPath;
|
|
3175
|
+
const manifestContents = await readTextFile(manifestPath);
|
|
3176
|
+
const manifestFormat = resolveConfigModuleFormat(manifestPath, manifestContents);
|
|
3177
|
+
const broadcastConfigTargetPath = resolveBroadcastConfigTargetPath(projectRoot, manifestPath, manifestFormat);
|
|
3178
|
+
const broadcastConfigIsTypeScript = [".ts", ".mts", ".cts"].includes(extname2(broadcastConfigTargetPath));
|
|
3179
|
+
const broadcastConfigPath = await resolveFirstExistingPath(projectRoot, BROADCAST_CONFIG_FILE_NAMES);
|
|
3180
|
+
const authConfigPath = await resolveFirstExistingPath(projectRoot, AUTH_CONFIG_FILE_NAMES);
|
|
3181
|
+
const { dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot);
|
|
3182
|
+
const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies);
|
|
3183
|
+
const canCreateBroadcastAuthRoute = framework === "next" || framework === "nuxt" || framework === "sveltekit";
|
|
3184
|
+
const broadcastRoot = resolve4(projectRoot, "server/broadcast");
|
|
3185
|
+
const channelsRoot = resolve4(projectRoot, "server/channels");
|
|
3186
|
+
const broadcastDirectoryExists = await pathExists(broadcastRoot);
|
|
3187
|
+
const channelsDirectoryExists = await pathExists(channelsRoot);
|
|
3188
|
+
await mkdir3(resolve4(projectRoot, "config"), { recursive: true });
|
|
3189
|
+
await mkdir3(broadcastRoot, { recursive: true });
|
|
3190
|
+
await mkdir3(channelsRoot, { recursive: true });
|
|
3191
|
+
await ensureRedisConfigFile(projectRoot);
|
|
3192
|
+
if (!broadcastConfigPath) {
|
|
3193
|
+
await writeTextFile(
|
|
3194
|
+
broadcastConfigTargetPath,
|
|
3195
|
+
renderBroadcastConfig(
|
|
3196
|
+
manifestFormat,
|
|
3197
|
+
Boolean(authConfigPath) && canCreateBroadcastAuthRoute,
|
|
3198
|
+
broadcastConfigIsTypeScript
|
|
3199
|
+
)
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
const broadcastEnvFiles = renderBroadcastEnvFiles();
|
|
3203
|
+
const envPath = resolve4(projectRoot, ".env");
|
|
3204
|
+
const envExamplePath = resolve4(projectRoot, ".env.example");
|
|
3205
|
+
const nextEnv = upsertEnvContents(await readTextFile(envPath), broadcastEnvFiles.env);
|
|
3206
|
+
const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), broadcastEnvFiles.example);
|
|
3207
|
+
const dependencyResult = await upsertBroadcastPackageDependencies(projectRoot);
|
|
3208
|
+
let createdFrameworkSetup = false;
|
|
3209
|
+
if (framework === "next") {
|
|
3210
|
+
const holoHelperPath = resolve4(projectRoot, ".holo-js/generated/next/holo.ts");
|
|
3211
|
+
const broadcastConfigRoutePath = resolve4(projectRoot, "app/broadcasting/config/route.ts");
|
|
3212
|
+
const generatedBroadcastConfigRoutePath = resolve4(projectRoot, ".holo-js/generated/next/broadcast-config-route.ts");
|
|
3213
|
+
if (!await pathExists(holoHelperPath)) {
|
|
3214
|
+
await writeTextFile(holoHelperPath, renderNextHoloHelper());
|
|
3215
|
+
createdFrameworkSetup = true;
|
|
3216
|
+
}
|
|
3217
|
+
if (!await pathExists(broadcastConfigRoutePath)) {
|
|
3218
|
+
await writeTextFile(broadcastConfigRoutePath, renderNextBroadcastConfigRoute());
|
|
3219
|
+
createdFrameworkSetup = true;
|
|
3220
|
+
}
|
|
3221
|
+
await writeTextFile(generatedBroadcastConfigRoutePath, renderNextGeneratedBroadcastConfigRoute());
|
|
3222
|
+
} else if (framework === "sveltekit") {
|
|
3223
|
+
const holoHelperPath = resolve4(projectRoot, ".holo-js/generated/sveltekit/holo.ts");
|
|
3224
|
+
if (!await pathExists(holoHelperPath)) {
|
|
3225
|
+
await writeTextFile(holoHelperPath, renderSvelteHoloHelper());
|
|
3226
|
+
createdFrameworkSetup = true;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
const broadcastAuthSupport = await syncBroadcastAuthSupportAfterAuthInstall(projectRoot);
|
|
3230
|
+
if (nextEnv.changed && typeof nextEnv.contents === "string") {
|
|
3231
|
+
await writeTextFile(envPath, nextEnv.contents);
|
|
3232
|
+
}
|
|
3233
|
+
if (nextEnvExample.changed && typeof nextEnvExample.contents === "string") {
|
|
3234
|
+
await writeTextFile(envExamplePath, nextEnvExample.contents);
|
|
3235
|
+
}
|
|
3236
|
+
return {
|
|
3237
|
+
updatedPackageJson: dependencyResult.updated,
|
|
3238
|
+
createdBroadcastConfig: !broadcastConfigPath,
|
|
3239
|
+
createdBroadcastDirectory: !broadcastDirectoryExists,
|
|
3240
|
+
createdChannelsDirectory: !channelsDirectoryExists,
|
|
3241
|
+
createdBroadcastAuthRoute: broadcastAuthSupport.createdBroadcastAuthRoute,
|
|
3242
|
+
createdFrameworkSetup,
|
|
3243
|
+
updatedEnv: nextEnv.changed,
|
|
3244
|
+
updatedEnvExample: nextEnvExample.changed
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
export {
|
|
3249
|
+
hasLoadedConfigFile,
|
|
3250
|
+
inferDatabaseDriverFromUrl,
|
|
3251
|
+
inferConnectionDriver,
|
|
3252
|
+
syncManagedDriverDependencies,
|
|
3253
|
+
upsertEventsPackageDependency,
|
|
3254
|
+
upsertNotificationsPackageDependency,
|
|
3255
|
+
upsertMailPackageDependency,
|
|
3256
|
+
upsertMediaPackageDependency,
|
|
3257
|
+
upsertSecurityPackageDependency,
|
|
3258
|
+
upsertCachePackageDependencies,
|
|
3259
|
+
upsertAuthPackageDependencies,
|
|
3260
|
+
renderStorageConfig,
|
|
3261
|
+
renderMediaConfig,
|
|
3262
|
+
renderQueueConfig,
|
|
3263
|
+
renderCacheConfig,
|
|
3264
|
+
renderRedisConfig,
|
|
3265
|
+
renderNotificationsConfig,
|
|
3266
|
+
renderMailConfig,
|
|
3267
|
+
renderSecurityConfig,
|
|
3268
|
+
renderCorsConfig,
|
|
3269
|
+
injectBroadcastAuthEndpoint,
|
|
3270
|
+
resolveBroadcastConfigTargetPath,
|
|
3271
|
+
renderSessionConfig,
|
|
3272
|
+
renderAuthConfig,
|
|
3273
|
+
authFeaturesRequireConfigUpdate,
|
|
3274
|
+
detectAuthInstallFeaturesFromConfig,
|
|
3275
|
+
renderAuthEnvFiles,
|
|
3276
|
+
renderAuthUserModel,
|
|
3277
|
+
renderAuthMigration,
|
|
3278
|
+
renderNotificationsMigration,
|
|
3279
|
+
renderScaffoldAppConfig,
|
|
3280
|
+
renderScaffoldDatabaseConfig,
|
|
3281
|
+
resolveDefaultDatabaseUrl,
|
|
3282
|
+
renderScaffoldEnvFiles,
|
|
3283
|
+
renderMailEnvFiles,
|
|
3284
|
+
renderQueueEnvFiles,
|
|
3285
|
+
renderCacheEnvFiles,
|
|
3286
|
+
renderEnvFileContents,
|
|
3287
|
+
normalizeScaffoldEnvSegments,
|
|
3288
|
+
renderScaffoldGitignore,
|
|
3289
|
+
renderScaffoldTsconfig,
|
|
3290
|
+
renderVSCodeSettings,
|
|
3291
|
+
resolvePackageManagerVersion,
|
|
3292
|
+
renderScaffoldPackageJson,
|
|
3293
|
+
scaffoldProject,
|
|
3294
|
+
installAuthIntoProject,
|
|
3295
|
+
installAuthorizationIntoProject,
|
|
3296
|
+
installQueueIntoProject,
|
|
3297
|
+
installEventsIntoProject,
|
|
3298
|
+
installRealtimeIntoProject,
|
|
3299
|
+
installNotificationsIntoProject,
|
|
3300
|
+
publishAuthNotificationsIntoProject,
|
|
3301
|
+
installMailIntoProject,
|
|
3302
|
+
installMediaIntoProject,
|
|
3303
|
+
installSecurityIntoProject,
|
|
3304
|
+
installCacheIntoProject,
|
|
3305
|
+
installBroadcastIntoProject
|
|
3306
|
+
};
|