@idevconn/create-icore 0.3.0 → 0.4.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.
Files changed (27) hide show
  1. package/dist/cli.js +193 -7
  2. package/dist/index.cjs +182 -5
  3. package/dist/index.d.cts +3 -1
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +182 -5
  6. package/package.json +1 -1
  7. package/templates/apps/microservices/auth/src/app/app.module.ts +33 -19
  8. package/templates/apps/microservices/notes/src/app/app.module.ts +39 -23
  9. package/templates/apps/microservices/upload/src/app/app.module.ts +41 -25
  10. package/templates/apps/templates/client-antd/vite.config.mts +16 -48
  11. package/templates/apps/templates/client-mui/vite.config.mts +16 -48
  12. package/templates/apps/templates/client-shadcn/vite.config.mts +16 -48
  13. package/templates/libs/shared/package.json +10 -0
  14. package/templates/libs/shared/src/__tests__/cross-boundary.unit.test.ts +121 -0
  15. package/templates/libs/shared/src/client.ts +5 -0
  16. package/templates/libs/shared/src/strategies/fakes/fake-auth.ts +8 -9
  17. package/templates/libs/shared/src/strategies/fakes/fake-storage.ts +1 -2
  18. package/templates/libs/template-shared/src/lib/abilities/ability-provider.tsx +1 -1
  19. package/templates/libs/vite-plugins/README.md +7 -0
  20. package/templates/libs/vite-plugins/eslint.config.mjs +19 -0
  21. package/templates/libs/vite-plugins/package.json +18 -0
  22. package/templates/libs/vite-plugins/project.json +19 -0
  23. package/templates/libs/vite-plugins/src/index.d.mts +21 -0
  24. package/templates/libs/vite-plugins/src/index.mjs +106 -0
  25. package/templates/libs/vite-plugins/tsconfig.json +20 -0
  26. package/templates/libs/vite-plugins/tsconfig.lib.json +9 -0
  27. package/templates/tsconfig.base.json +3 -1
package/dist/index.js CHANGED
@@ -34,6 +34,9 @@ async function rewriteRootPackageJson(targetDir, opts) {
34
34
  pkg["version"] = "0.0.1";
35
35
  pkg["private"] = true;
36
36
  delete pkg.description;
37
+ if (opts.packageManager !== "yarn") {
38
+ delete pkg.packageManager;
39
+ }
37
40
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
38
41
  }
39
42
  async function writeAuthEnv(targetDir, opts) {
@@ -55,6 +58,19 @@ async function writeUploadEnv(targetDir, opts) {
55
58
  }
56
59
  await writeFile(join(targetDir, "apps/microservices/upload/.env"), next);
57
60
  }
61
+ async function writeNotesEnv(targetDir, opts) {
62
+ if (opts.example === "none") return;
63
+ const envExample = join(targetDir, "apps/microservices/notes/.env.example");
64
+ try {
65
+ const env = await readFile(envExample, "utf8");
66
+ let next = env.replace(/^NOTES_TRANSPORT=.*$/m, `NOTES_TRANSPORT=${opts.transport}`);
67
+ if (opts.transport !== "tcp") {
68
+ next = next.replace(/^# (NOTES_(?:REDIS|NATS)_URL=)/m, "$1");
69
+ }
70
+ await writeFile(join(targetDir, "apps/microservices/notes/.env"), next);
71
+ } catch {
72
+ }
73
+ }
58
74
  async function writeGatewayEnv(targetDir, opts) {
59
75
  const envExample = join(targetDir, "apps/api/.env.example");
60
76
  const env = await readFile(envExample, "utf8");
@@ -206,6 +222,145 @@ async function removeNotesStack(targetDir) {
206
222
  } catch {
207
223
  }
208
224
  }
225
+ async function stripTsconfigPath(targetDir, alias) {
226
+ const tsconfigPath = join(targetDir, "tsconfig.base.json");
227
+ try {
228
+ const src = await readFile(tsconfigPath, "utf8");
229
+ const escaped = alias.replace(/[@/]/g, (c) => c === "@" ? "@" : "\\/");
230
+ const pretty = src.replace(new RegExp(`^\\s*"${escaped}": \\[[^\\]]*\\],?\\n`, "m"), "");
231
+ if (pretty !== src) {
232
+ await writeFile(tsconfigPath, pretty);
233
+ return;
234
+ }
235
+ const parsed = JSON.parse(src);
236
+ if (parsed.compilerOptions?.paths) {
237
+ delete parsed.compilerOptions.paths[alias];
238
+ }
239
+ await writeFile(tsconfigPath, JSON.stringify(parsed));
240
+ } catch {
241
+ }
242
+ }
243
+ async function removeUnusedAuthStrategies(targetDir, authProvider) {
244
+ const modulePath = join(targetDir, "apps/microservices/auth/src/app/app.module.ts");
245
+ if (authProvider === "supabase") {
246
+ await rm(join(targetDir, "libs/auth-strategies/firebase"), { recursive: true, force: true });
247
+ await stripDeps(join(targetDir, "apps/microservices/auth/package.json"), [
248
+ "@icore/auth-firebase"
249
+ ]);
250
+ await stripTsconfigPath(targetDir, "@icore/auth-firebase");
251
+ try {
252
+ const src = await readFile(modulePath, "utf8");
253
+ const next = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(/^import \{[^}]*FirebaseAuthStrategy[^}]*\} from '@icore\/auth-firebase';\n/m, "").replace(/^function makeFirebaseStrategy\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'firebase':\n *return makeFirebaseStrategy\(cfg\);\n/, "");
254
+ await writeFile(modulePath, next);
255
+ } catch {
256
+ }
257
+ }
258
+ if (authProvider === "firebase") {
259
+ await rm(join(targetDir, "libs/auth-strategies/supabase"), { recursive: true, force: true });
260
+ await stripDeps(join(targetDir, "apps/microservices/auth/package.json"), [
261
+ "@icore/auth-supabase"
262
+ ]);
263
+ await stripTsconfigPath(targetDir, "@icore/auth-supabase");
264
+ try {
265
+ const src = await readFile(modulePath, "utf8");
266
+ const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseAuthStrategy[^}]*\} from '@icore\/auth-supabase';\n/m, "").replace(
267
+ /\n {10}case 'supabase': \{[\s\S]*?return new SupabaseAuthStrategy\(\{ client \}\);\n {10}\}\n/m,
268
+ ""
269
+ );
270
+ await writeFile(modulePath, next);
271
+ } catch {
272
+ }
273
+ }
274
+ }
275
+ async function removeUnusedStorageStrategies(targetDir, uploadProvider) {
276
+ if (uploadProvider === "none") return;
277
+ const modulePath = join(targetDir, "apps/microservices/upload/src/app/app.module.ts");
278
+ if (uploadProvider !== "firebase") {
279
+ await rm(join(targetDir, "libs/storage-strategies/firebase"), { recursive: true, force: true });
280
+ await stripDeps(join(targetDir, "apps/microservices/upload/package.json"), [
281
+ "@icore/storage-firebase"
282
+ ]);
283
+ await stripTsconfigPath(targetDir, "@icore/storage-firebase");
284
+ }
285
+ if (uploadProvider !== "cloudinary") {
286
+ await rm(join(targetDir, "libs/storage-strategies/cloudinary"), {
287
+ recursive: true,
288
+ force: true
289
+ });
290
+ await stripDeps(join(targetDir, "apps/microservices/upload/package.json"), [
291
+ "@icore/storage-cloudinary"
292
+ ]);
293
+ await stripTsconfigPath(targetDir, "@icore/storage-cloudinary");
294
+ }
295
+ if (uploadProvider !== "supabase") {
296
+ await rm(join(targetDir, "libs/storage-strategies/supabase"), { recursive: true, force: true });
297
+ await stripDeps(join(targetDir, "apps/microservices/upload/package.json"), [
298
+ "@icore/storage-supabase"
299
+ ]);
300
+ await stripTsconfigPath(targetDir, "@icore/storage-supabase");
301
+ }
302
+ try {
303
+ let src = await readFile(modulePath, "utf8");
304
+ if (uploadProvider !== "firebase") {
305
+ src = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(
306
+ /^import \{[^}]*FirebaseStorageStrategy[^}]*\} from '@icore\/storage-firebase';\n/m,
307
+ ""
308
+ ).replace(/^function makeFirebaseStorage\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'firebase':\n *return makeFirebaseStorage\(cfg\);\n/, "");
309
+ }
310
+ if (uploadProvider !== "cloudinary") {
311
+ src = src.replace(/^import \{ v2 as cloudinary \} from 'cloudinary';\n/m, "").replace(
312
+ /^import \{[^}]*CloudinaryStorageStrategy[^}]*\} from '@icore\/storage-cloudinary';\n/m,
313
+ ""
314
+ ).replace(/^function makeCloudinaryStorage\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'cloudinary':\n *return makeCloudinaryStorage\(cfg\);\n/, "");
315
+ }
316
+ if (uploadProvider !== "supabase") {
317
+ src = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(
318
+ /^import \{[^}]*SupabaseStorageStrategy[^}]*\} from '@icore\/storage-supabase';\n/m,
319
+ ""
320
+ ).replace(
321
+ /\n {10}case 'supabase': \{[\s\S]*?bucket: requireEnv\(cfg, 'SUPABASE_STORAGE_BUCKET'\),\n {12}\}\);\n {10}\}\n/m,
322
+ ""
323
+ );
324
+ }
325
+ await writeFile(modulePath, src);
326
+ } catch {
327
+ }
328
+ }
329
+ async function removeUnusedDbStrategies(targetDir, dbProvider) {
330
+ const modulePath = join(targetDir, "apps/microservices/notes/src/app/app.module.ts");
331
+ if (dbProvider === "supabase") {
332
+ await rm(join(targetDir, "libs/db-strategies/firestore"), { recursive: true, force: true });
333
+ await stripDeps(join(targetDir, "apps/microservices/notes/package.json"), [
334
+ "@icore/db-firestore"
335
+ ]);
336
+ await stripTsconfigPath(targetDir, "@icore/db-firestore");
337
+ try {
338
+ const src = await readFile(modulePath, "utf8");
339
+ const next = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(/^import \{[^}]*FirestoreDBStrategy[^}]*\} from '@icore\/db-firestore';\n/m, "").replace(
340
+ /\n {8}if \(provider === 'firestore'[\s\S]*?return new FirestoreDBStrategy\(\{[\s\S]*?\}\);\n {8}\}\n/m,
341
+ ""
342
+ );
343
+ await writeFile(modulePath, next);
344
+ } catch {
345
+ }
346
+ }
347
+ if (dbProvider === "firebase") {
348
+ await rm(join(targetDir, "libs/db-strategies/supabase"), { recursive: true, force: true });
349
+ await stripDeps(join(targetDir, "apps/microservices/notes/package.json"), [
350
+ "@icore/db-supabase"
351
+ ]);
352
+ await stripTsconfigPath(targetDir, "@icore/db-supabase");
353
+ try {
354
+ const src = await readFile(modulePath, "utf8");
355
+ const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(
356
+ /\n {8}if \(provider === 'supabase'\) \{[\s\S]*?return new SupabaseDBStrategy\(\{ client \}\);\n {8}\}\n/m,
357
+ ""
358
+ );
359
+ await writeFile(modulePath, next);
360
+ } catch {
361
+ }
362
+ }
363
+ }
209
364
  async function removeUploadStack(targetDir) {
210
365
  const paths = [
211
366
  "apps/microservices/upload",
@@ -282,14 +437,16 @@ function gitInit(cwd, projectName) {
282
437
  { cwd, stdio: "inherit" }
283
438
  );
284
439
  }
285
- function yarnInstall(cwd) {
286
- spawnSync("yarn", ["install"], { cwd, stdio: "inherit" });
440
+ function runInstall(cwd, pm) {
441
+ const [cmd, ...args] = pm === "npm" ? ["npm", "install"] : pm === "pnpm" ? ["pnpm", "install"] : ["yarn", "install"];
442
+ spawnSync(cmd, args, { cwd, stdio: "inherit" });
287
443
  }
288
444
  async function scaffold(opts, templatesDir) {
289
445
  await copyTree(templatesDir, opts.targetDir);
290
446
  await rewriteRootPackageJson(opts.targetDir, opts);
291
447
  await writeAuthEnv(opts.targetDir, opts);
292
448
  await writeUploadEnv(opts.targetDir, opts);
449
+ await writeNotesEnv(opts.targetDir, opts);
293
450
  await writePaymentEnv(opts.targetDir, opts);
294
451
  await writeGatewayEnv(opts.targetDir, opts);
295
452
  await writeRootEnv(opts.targetDir, opts);
@@ -298,8 +455,13 @@ async function scaffold(opts, templatesDir) {
298
455
  if (opts.payment === "none") await removePaymentStack(opts.targetDir);
299
456
  if (opts.jobs === "none") await removeJobsStack(opts.targetDir);
300
457
  if (opts.example === "none") await removeNotesStack(opts.targetDir);
301
- await writeFile(join(opts.targetDir, "yarn.lock"), "");
302
- if (opts.install) yarnInstall(opts.targetDir);
458
+ await removeUnusedAuthStrategies(opts.targetDir, opts.authProvider);
459
+ await removeUnusedStorageStrategies(opts.targetDir, opts.upload);
460
+ await removeUnusedDbStrategies(opts.targetDir, opts.dbProvider);
461
+ if (opts.packageManager === "yarn") {
462
+ await writeFile(join(opts.targetDir, "yarn.lock"), "");
463
+ }
464
+ if (opts.install) runInstall(opts.targetDir, opts.packageManager);
303
465
  if (opts.initGit) gitInit(opts.targetDir, opts.projectName);
304
466
  }
305
467
 
@@ -309,6 +471,13 @@ import { resolve } from "path";
309
471
  import { readFile as readFile2 } from "fs/promises";
310
472
  import { dirname, join as join2 } from "path";
311
473
  import { fileURLToPath } from "url";
474
+ function detectPackageManager() {
475
+ const ua = process.env["npm_config_user_agent"] ?? "";
476
+ if (ua.startsWith("yarn/")) return "yarn";
477
+ if (ua.startsWith("pnpm/")) return "pnpm";
478
+ if (ua.startsWith("npm/")) return "npm";
479
+ return "yarn";
480
+ }
312
481
  async function readSelfVersion() {
313
482
  try {
314
483
  const here = dirname(fileURLToPath(import.meta.url));
@@ -377,6 +546,9 @@ function parseFlags(argv) {
377
546
  case "transport":
378
547
  out.transport = v;
379
548
  break;
549
+ case "package-manager":
550
+ out.packageManager = v;
551
+ break;
380
552
  case "no-git":
381
553
  out.initGit = false;
382
554
  break;
@@ -486,8 +658,12 @@ Re-run with @latest to refresh:
486
658
  initialValue: "tcp"
487
659
  });
488
660
  if (p.isCancel(transport)) throw new Error("cancelled");
661
+ const packageManager = flags.packageManager ?? detectPackageManager();
489
662
  const initGit = flags.initGit ?? !await p.confirm({ message: "Initialise git repo?", initialValue: true }) === false;
490
- const install = flags.install ?? !await p.confirm({ message: "Run yarn install?", initialValue: true }) === false;
663
+ const install = flags.install ?? !await p.confirm({
664
+ message: `Run ${packageManager} install?`,
665
+ initialValue: true
666
+ }) === false;
491
667
  return {
492
668
  projectName,
493
669
  targetDir: resolve(cwd, projectName),
@@ -499,6 +675,7 @@ Re-run with @latest to refresh:
499
675
  example,
500
676
  ui,
501
677
  transport,
678
+ packageManager,
502
679
  initGit,
503
680
  install
504
681
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idevconn/create-icore",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "iDEVconn",
@@ -5,23 +5,29 @@ import { createClient } from '@supabase/supabase-js';
5
5
  import * as admin from 'firebase-admin';
6
6
  import { SupabaseAuthStrategy } from '@icore/auth-supabase';
7
7
  import { FirebaseAuthStrategy, HttpIdentityToolkitClient } from '@icore/auth-firebase';
8
+ import { FakeAuthStrategy } from '@icore/shared';
8
9
  import type { AuthStrategy } from '@icore/shared';
10
+ import { Logger } from '@nestjs/common';
9
11
  import { AuthController } from './auth.controller';
10
12
 
13
+ function requireEnv(cfg: ConfigService, key: string): string {
14
+ const val = cfg.getOrThrow<string>(key);
15
+ if (!val) throw new Error(`${key} is not set — check apps/microservices/auth/.env`);
16
+ return val;
17
+ }
18
+
11
19
  function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
12
- const projectId = cfg.getOrThrow<string>('FB_ADMIN_PROJECT_ID');
20
+ const projectId = requireEnv(cfg, 'FB_ADMIN_PROJECT_ID');
13
21
  if (admin.apps.length === 0) {
14
22
  admin.initializeApp({
15
23
  credential: admin.credential.cert({
16
24
  projectId,
17
- clientEmail: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_EMAIL'),
18
- privateKey: cfg.getOrThrow<string>('FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
25
+ clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
26
+ privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
19
27
  }),
20
28
  });
21
29
  }
22
- const identityToolkit = new HttpIdentityToolkitClient(
23
- cfg.getOrThrow<string>('FIREBASE_WEB_API_KEY'),
24
- );
30
+ const identityToolkit = new HttpIdentityToolkitClient(requireEnv(cfg, 'FIREBASE_WEB_API_KEY'));
25
31
  return new FirebaseAuthStrategy({
26
32
  identityToolkit,
27
33
  adminAuth: admin.auth(),
@@ -43,20 +49,28 @@ function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
43
49
  {
44
50
  provide: 'AuthStrategy',
45
51
  useFactory: (cfg: ConfigService): AuthStrategy => {
46
- const provider = cfg.getOrThrow<string>('AUTH_PROVIDER');
47
- switch (provider) {
48
- case 'supabase': {
49
- const client = createClient(
50
- cfg.getOrThrow<string>('SUPABASE_URL'),
51
- cfg.getOrThrow<string>('SUPABASE_SERVICE_ROLE_KEY'),
52
- { auth: { autoRefreshToken: false, persistSession: false } },
53
- );
54
- return new SupabaseAuthStrategy({ client });
52
+ try {
53
+ const provider = requireEnv(cfg, 'AUTH_PROVIDER');
54
+ switch (provider) {
55
+ case 'supabase': {
56
+ const client = createClient(
57
+ requireEnv(cfg, 'SUPABASE_URL'),
58
+ requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
59
+ { auth: { autoRefreshToken: false, persistSession: false } },
60
+ );
61
+ return new SupabaseAuthStrategy({ client });
62
+ }
63
+ case 'firebase':
64
+ return makeFirebaseStrategy(cfg);
65
+ default:
66
+ throw new Error(`Unsupported AUTH_PROVIDER: ${provider}`);
55
67
  }
56
- case 'firebase':
57
- return makeFirebaseStrategy(cfg);
58
- default:
59
- throw new Error(`Unsupported AUTH_PROVIDER: ${provider}`);
68
+ } catch (err) {
69
+ new Logger('AuthStrategy').warn(
70
+ `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
71
+ `Requests will fail until apps/microservices/auth/.env is set.`,
72
+ );
73
+ return new FakeAuthStrategy();
60
74
  }
61
75
  },
62
76
  inject: [ConfigService],
@@ -5,9 +5,17 @@ import { createClient } from '@supabase/supabase-js';
5
5
  import * as admin from 'firebase-admin';
6
6
  import { SupabaseDBStrategy } from '@icore/db-supabase';
7
7
  import { FirestoreDBStrategy } from '@icore/db-firestore';
8
+ import { FakeDBStrategy } from '@icore/shared';
8
9
  import type { DBStrategy } from '@icore/shared';
10
+ import { Logger } from '@nestjs/common';
9
11
  import { NotesController } from './notes.controller';
10
12
 
13
+ function requireEnv(cfg: ConfigService, key: string): string {
14
+ const val = cfg.getOrThrow<string>(key);
15
+ if (!val) throw new Error(`${key} is not set — check apps/microservices/notes/.env`);
16
+ return val;
17
+ }
18
+
11
19
  @Module({
12
20
  imports: [
13
21
  ConfigModule.forRoot({
@@ -23,32 +31,40 @@ import { NotesController } from './notes.controller';
23
31
  {
24
32
  provide: 'DBStrategy',
25
33
  useFactory: (cfg: ConfigService): DBStrategy => {
26
- const provider = cfg.getOrThrow<string>('DB_PROVIDER');
27
- if (provider === 'supabase') {
28
- const client = createClient(
29
- cfg.getOrThrow<string>('SUPABASE_URL'),
30
- cfg.getOrThrow<string>('SUPABASE_SERVICE_ROLE_KEY'),
31
- { auth: { autoRefreshToken: false, persistSession: false } },
32
- );
33
- return new SupabaseDBStrategy({ client });
34
- }
35
- if (provider === 'firestore' || provider === 'firebase') {
36
- if (admin.apps.length === 0) {
37
- admin.initializeApp({
38
- credential: admin.credential.cert({
39
- projectId: cfg.getOrThrow<string>('FB_ADMIN_PROJECT_ID'),
40
- clientEmail: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_EMAIL'),
41
- privateKey: cfg.getOrThrow<string>('FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
42
- }),
34
+ try {
35
+ const provider = requireEnv(cfg, 'DB_PROVIDER');
36
+ if (provider === 'supabase') {
37
+ const client = createClient(
38
+ requireEnv(cfg, 'SUPABASE_URL'),
39
+ requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
40
+ { auth: { autoRefreshToken: false, persistSession: false } },
41
+ );
42
+ return new SupabaseDBStrategy({ client });
43
+ }
44
+ if (provider === 'firestore' || provider === 'firebase') {
45
+ if (admin.apps.length === 0) {
46
+ admin.initializeApp({
47
+ credential: admin.credential.cert({
48
+ projectId: requireEnv(cfg, 'FB_ADMIN_PROJECT_ID'),
49
+ clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
50
+ privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
51
+ }),
52
+ });
53
+ }
54
+ return new FirestoreDBStrategy({
55
+ db: admin.firestore() as unknown as ConstructorParameters<
56
+ typeof FirestoreDBStrategy
57
+ >[0]['db'],
43
58
  });
44
59
  }
45
- return new FirestoreDBStrategy({
46
- db: admin.firestore() as unknown as ConstructorParameters<
47
- typeof FirestoreDBStrategy
48
- >[0]['db'],
49
- });
60
+ throw new Error(`Unsupported DB_PROVIDER: ${provider}`);
61
+ } catch (err) {
62
+ new Logger('DBStrategy').warn(
63
+ `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
64
+ `Requests will fail until apps/microservices/notes/.env is set.`,
65
+ );
66
+ return new FakeDBStrategy();
50
67
  }
51
- throw new Error(`Unsupported DB_PROVIDER: ${provider}`);
52
68
  },
53
69
  inject: [ConfigService],
54
70
  },
@@ -7,17 +7,25 @@ import { v2 as cloudinary } from 'cloudinary';
7
7
  import { SupabaseStorageStrategy } from '@icore/storage-supabase';
8
8
  import { FirebaseStorageStrategy } from '@icore/storage-firebase';
9
9
  import { CloudinaryStorageStrategy, type CloudinaryApiLike } from '@icore/storage-cloudinary';
10
+ import { FakeStorageStrategy } from '@icore/shared';
10
11
  import type { StorageStrategy } from '@icore/shared';
12
+ import { Logger } from '@nestjs/common';
11
13
  import { StorageController } from './storage.controller';
12
14
 
15
+ function requireEnv(cfg: ConfigService, key: string): string {
16
+ const val = cfg.getOrThrow<string>(key);
17
+ if (!val) throw new Error(`${key} is not set — check apps/microservices/upload/.env`);
18
+ return val;
19
+ }
20
+
13
21
  function makeFirebaseStorage(cfg: ConfigService): StorageStrategy {
14
- const bucketName = cfg.getOrThrow<string>('FIREBASE_STORAGE_BUCKET');
22
+ const bucketName = requireEnv(cfg, 'FIREBASE_STORAGE_BUCKET');
15
23
  if (admin.apps.length === 0) {
16
24
  admin.initializeApp({
17
25
  credential: admin.credential.cert({
18
- projectId: cfg.getOrThrow<string>('FB_ADMIN_PROJECT_ID'),
19
- clientEmail: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_EMAIL'),
20
- privateKey: cfg.getOrThrow<string>('FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
26
+ projectId: requireEnv(cfg, 'FB_ADMIN_PROJECT_ID'),
27
+ clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
28
+ privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
21
29
  }),
22
30
  });
23
31
  }
@@ -30,9 +38,9 @@ function makeFirebaseStorage(cfg: ConfigService): StorageStrategy {
30
38
 
31
39
  function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
32
40
  cloudinary.config({
33
- cloud_name: cfg.getOrThrow<string>('CLOUDINARY_CLOUD_NAME'),
34
- api_key: cfg.getOrThrow<string>('CLOUDINARY_API_KEY'),
35
- api_secret: cfg.getOrThrow<string>('CLOUDINARY_API_SECRET'),
41
+ cloud_name: requireEnv(cfg, 'CLOUDINARY_CLOUD_NAME'),
42
+ api_key: requireEnv(cfg, 'CLOUDINARY_API_KEY'),
43
+ api_secret: requireEnv(cfg, 'CLOUDINARY_API_SECRET'),
36
44
  secure: true,
37
45
  });
38
46
 
@@ -89,25 +97,33 @@ function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
89
97
  {
90
98
  provide: 'StorageStrategy',
91
99
  useFactory: (cfg: ConfigService): StorageStrategy => {
92
- const provider = cfg.getOrThrow<string>('STORAGE_PROVIDER');
93
- switch (provider) {
94
- case 'supabase': {
95
- const client = createClient(
96
- cfg.getOrThrow<string>('SUPABASE_URL'),
97
- cfg.getOrThrow<string>('SUPABASE_SERVICE_ROLE_KEY'),
98
- { auth: { autoRefreshToken: false, persistSession: false } },
99
- );
100
- return new SupabaseStorageStrategy({
101
- client,
102
- bucket: cfg.getOrThrow<string>('SUPABASE_STORAGE_BUCKET'),
103
- });
100
+ try {
101
+ const provider = requireEnv(cfg, 'STORAGE_PROVIDER');
102
+ switch (provider) {
103
+ case 'supabase': {
104
+ const client = createClient(
105
+ requireEnv(cfg, 'SUPABASE_URL'),
106
+ requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
107
+ { auth: { autoRefreshToken: false, persistSession: false } },
108
+ );
109
+ return new SupabaseStorageStrategy({
110
+ client,
111
+ bucket: requireEnv(cfg, 'SUPABASE_STORAGE_BUCKET'),
112
+ });
113
+ }
114
+ case 'firebase':
115
+ return makeFirebaseStorage(cfg);
116
+ case 'cloudinary':
117
+ return makeCloudinaryStorage(cfg);
118
+ default:
119
+ throw new Error(`Unsupported STORAGE_PROVIDER: ${provider}`);
104
120
  }
105
- case 'firebase':
106
- return makeFirebaseStorage(cfg);
107
- case 'cloudinary':
108
- return makeCloudinaryStorage(cfg);
109
- default:
110
- throw new Error(`Unsupported STORAGE_PROVIDER: ${provider}`);
121
+ } catch (err) {
122
+ new Logger('StorageStrategy').warn(
123
+ `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
124
+ `Requests will fail until apps/microservices/upload/.env is set.`,
125
+ );
126
+ return new FakeStorageStrategy();
111
127
  }
112
128
  },
113
129
  inject: [ConfigService],
@@ -2,9 +2,16 @@
2
2
  import fs from 'node:fs';
3
3
  import { defineConfig } from 'vite';
4
4
  import react from '@vitejs/plugin-react';
5
- import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
5
+ import { tanstackRouter } from '@tanstack/router-plugin/vite';
6
6
  import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
7
7
  import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
8
+ import {
9
+ commonDefines,
10
+ commonManualChunks,
11
+ commonTestConfig,
12
+ injectAppVersionPlugin,
13
+ noServerModulesPlugin,
14
+ } from '@icore/vite-plugins';
8
15
 
9
16
  const rootPackageJsonPath = new URL('../../../package.json', import.meta.url);
10
17
  const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')) as {
@@ -14,11 +21,7 @@ const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')
14
21
  };
15
22
 
16
23
  function depVersion(name: string): string {
17
- return (
18
- rootPackageJson.dependencies?.[name] ??
19
- rootPackageJson.devDependencies?.[name] ??
20
- '?'
21
- );
24
+ return rootPackageJson.dependencies?.[name] ?? rootPackageJson.devDependencies?.[name] ?? '?';
22
25
  }
23
26
 
24
27
  export default defineConfig(() => ({
@@ -33,22 +36,11 @@ export default defineConfig(() => ({
33
36
  host: 'localhost',
34
37
  },
35
38
  define: {
36
- 'import.meta.env.VITE_APP_VERSION': JSON.stringify(rootPackageJson.version),
37
- // Dep versions injected at build time so routes don't need JSON imports
38
- 'import.meta.env.VITE_DEP_REACT': JSON.stringify(depVersion('react')),
39
+ ...commonDefines(rootPackageJson),
39
40
  'import.meta.env.VITE_DEP_ANTD': JSON.stringify(depVersion('antd')),
40
- 'import.meta.env.VITE_DEP_VITE': JSON.stringify(depVersion('vite')),
41
- 'import.meta.env.VITE_DEP_TANSTACK_ROUTER': JSON.stringify(
42
- depVersion('@tanstack/react-router'),
43
- ),
44
- 'import.meta.env.VITE_DEP_TANSTACK_QUERY': JSON.stringify(
45
- depVersion('@tanstack/react-query'),
46
- ),
47
- 'import.meta.env.VITE_DEP_ZUSTAND': JSON.stringify(depVersion('zustand')),
48
- 'import.meta.env.VITE_DEP_CASL': JSON.stringify(depVersion('@casl/ability')),
49
41
  },
50
42
  plugins: [
51
- TanStackRouterVite({
43
+ tanstackRouter({
52
44
  target: 'react',
53
45
  autoCodeSplitting: true,
54
46
  routeFileIgnorePattern: '(__tests__|\\.test\\.(t|j)sx?$)',
@@ -56,12 +48,8 @@ export default defineConfig(() => ({
56
48
  react(),
57
49
  nxViteTsPaths(),
58
50
  nxCopyAssetsPlugin(['*.md']),
59
- {
60
- name: 'inject-app-version-meta',
61
- transformIndexHtml(html: string) {
62
- return html.replace('%APP_VERSION%', rootPackageJson.version);
63
- },
64
- },
51
+ noServerModulesPlugin(),
52
+ injectAppVersionPlugin(rootPackageJson),
65
53
  ],
66
54
  // Uncomment this if you are using workers.
67
55
  // worker: {
@@ -76,32 +64,12 @@ export default defineConfig(() => ({
76
64
  },
77
65
  rolldownOptions: {
78
66
  output: {
79
- manualChunks: (id: string) => {
80
- if (!id.includes('node_modules')) return;
81
- if (id.includes('/react/') || id.includes('/react-dom/') || id.includes('scheduler'))
82
- return 'vendor-react';
83
- if (id.includes('@tanstack')) return 'vendor-tanstack';
84
- if (id.includes('i18next') || id.includes('react-i18next')) return 'vendor-i18n';
85
- if (id.includes('@casl')) return 'vendor-casl';
67
+ manualChunks: commonManualChunks((id) => {
86
68
  if (id.includes('/antd/') || id.includes('@ant-design') || id.includes('rc-'))
87
69
  return 'vendor-antd';
88
- if (id.includes('zustand')) return 'vendor-state';
89
- if (id.includes('@idevconn')) return 'vendor-idevconn';
90
- return 'vendor-core';
91
- },
70
+ }),
92
71
  },
93
72
  },
94
73
  },
95
- test: {
96
- name: 'client-antd',
97
- watch: false,
98
- globals: true,
99
- environment: 'jsdom',
100
- include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
101
- reporters: ['default'],
102
- coverage: {
103
- reportsDirectory: '../../../coverage/apps/templates/client-antd',
104
- provider: 'v8' as const,
105
- },
106
- },
74
+ test: commonTestConfig('client-antd', '../../../coverage/apps/templates/client-antd'),
107
75
  }));