@idevconn/create-icore 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/cli.js +109 -45
  2. package/dist/index.cjs +109 -45
  3. package/dist/index.d.cts +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +109 -45
  6. package/package.json +1 -1
  7. package/templates/apps/api/.env.example +20 -0
  8. package/templates/apps/api/package.json +1 -0
  9. package/templates/apps/api/tsconfig.json +6 -1
  10. package/templates/apps/api/webpack.config.js +20 -0
  11. package/templates/apps/microservices/auth/.env.example +5 -0
  12. package/templates/apps/microservices/auth/tsconfig.json +6 -1
  13. package/templates/apps/microservices/auth/webpack.config.js +30 -0
  14. package/templates/apps/microservices/jobs/tsconfig.json +6 -1
  15. package/templates/apps/microservices/jobs/webpack.config.js +30 -0
  16. package/templates/apps/microservices/notes/.env.example +5 -0
  17. package/templates/apps/microservices/notes/tsconfig.json +6 -1
  18. package/templates/apps/microservices/notes/webpack.config.js +30 -0
  19. package/templates/apps/microservices/notes-e2e/src/support/global.d.ts +6 -0
  20. package/templates/apps/microservices/payment/.env.example +5 -0
  21. package/templates/apps/microservices/payment/tsconfig.json +6 -1
  22. package/templates/apps/microservices/payment/webpack.config.js +30 -0
  23. package/templates/apps/microservices/upload/.env.example +5 -0
  24. package/templates/apps/microservices/upload/tsconfig.json +6 -1
  25. package/templates/apps/microservices/upload/webpack.config.js +30 -0
  26. package/templates/apps/templates/client-antd/src/components/AccessDeniedPage.tsx +1 -1
  27. package/templates/apps/templates/client-antd/src/components/layout/LayoutHeader.tsx +1 -1
  28. package/templates/apps/templates/client-antd/src/components/layout/LayoutSider.tsx +3 -3
  29. package/templates/apps/templates/client-antd/src/routes/_dashboard/dashboard.tsx +2 -2
  30. package/templates/apps/templates/client-antd/src/routes/_dashboard/notes.tsx +2 -2
  31. package/templates/apps/templates/client-antd/src/routes/_dashboard/profile.tsx +2 -2
  32. package/templates/apps/templates/client-antd/src/routes/auth.callback.tsx +1 -1
  33. package/templates/apps/templates/client-antd/src/routes/auth.oauth.callback.tsx +1 -1
  34. package/templates/apps/templates/client-antd/src/routes/login.tsx +1 -1
  35. package/templates/apps/templates/client-antd/tsconfig.json +6 -1
  36. package/templates/apps/templates/client-antd-e2e/src/icore.spec.ts +2 -2
  37. package/templates/apps/templates/client-mui/src/components/AccessDeniedPage.tsx +1 -1
  38. package/templates/apps/templates/client-mui/src/components/layout/LayoutHeader.tsx +1 -1
  39. package/templates/apps/templates/client-mui/src/components/layout/LayoutSider.tsx +3 -15
  40. package/templates/apps/templates/client-mui/src/routes/_dashboard/dashboard.tsx +2 -6
  41. package/templates/apps/templates/client-mui/src/routes/_dashboard/notes.tsx +2 -2
  42. package/templates/apps/templates/client-mui/src/routes/_dashboard/profile.tsx +3 -3
  43. package/templates/apps/templates/client-mui/src/routes/auth.callback.tsx +1 -1
  44. package/templates/apps/templates/client-mui/src/routes/auth.oauth.callback.tsx +1 -1
  45. package/templates/apps/templates/client-mui/src/routes/login.tsx +3 -3
  46. package/templates/apps/templates/client-mui/tsconfig.json +6 -1
  47. package/templates/apps/templates/client-mui-e2e/src/icore.spec.ts +2 -2
  48. package/templates/apps/templates/client-shadcn/src/components/AccessDeniedPage.tsx +1 -1
  49. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +3 -3
  50. package/templates/apps/templates/client-shadcn/src/components/notes/DeleteNoteConfirm.tsx +1 -1
  51. package/templates/apps/templates/client-shadcn/src/components/notes/NoteDialog.tsx +3 -3
  52. package/templates/apps/templates/client-shadcn/src/components/notes/NotesTable.tsx +1 -1
  53. package/templates/apps/templates/client-shadcn/src/components/ui/button.tsx +1 -1
  54. package/templates/apps/templates/client-shadcn/src/components/ui/card.tsx +1 -1
  55. package/templates/apps/templates/client-shadcn/src/components/ui/input.tsx +1 -1
  56. package/templates/apps/templates/client-shadcn/src/components/ui/label.tsx +1 -1
  57. package/templates/apps/templates/client-shadcn/src/routes/_dashboard/dashboard.tsx +3 -9
  58. package/templates/apps/templates/client-shadcn/src/routes/_dashboard/notes.tsx +6 -6
  59. package/templates/apps/templates/client-shadcn/src/routes/_dashboard/profile.tsx +7 -7
  60. package/templates/apps/templates/client-shadcn/src/routes/auth.callback.tsx +1 -1
  61. package/templates/apps/templates/client-shadcn/src/routes/auth.oauth.callback.tsx +1 -1
  62. package/templates/apps/templates/client-shadcn/src/routes/login.tsx +4 -4
  63. package/templates/apps/templates/client-shadcn/tsconfig.json +6 -1
  64. package/templates/libs/auth-client/package.json +1 -1
  65. package/templates/libs/auth-strategies/firebase/package.json +1 -1
  66. package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.contract.unit.test.ts +6 -3
  67. package/templates/libs/auth-strategies/supabase/package.json +1 -1
  68. package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.contract.unit.test.ts +5 -2
  69. package/templates/libs/db-strategies/firestore/package.json +1 -1
  70. package/templates/libs/db-strategies/firestore/src/lib/__tests__/firestore-db.contract.unit.test.ts +1 -2
  71. package/templates/libs/db-strategies/supabase/package.json +1 -1
  72. package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.contract.unit.test.ts +1 -2
  73. package/templates/libs/firebase-admin/package.json +1 -1
  74. package/templates/libs/jobs-client/package.json +1 -1
  75. package/templates/libs/notes-client/package.json +1 -1
  76. package/templates/libs/payment-client/package.json +1 -1
  77. package/templates/libs/shared/package.json +3 -3
  78. package/templates/libs/shared/src/__tests__/cross-boundary.unit.test.ts +2 -1
  79. package/templates/libs/shared/src/__tests__/transport.unit.test.ts +47 -8
  80. package/templates/libs/shared/src/abilities/subjects.ts +12 -1
  81. package/templates/libs/shared/src/strategies/__tests__/fake-auth.contract.unit.test.ts +2 -2
  82. package/templates/libs/shared/src/strategies/__tests__/fake-db.contract.unit.test.ts +2 -2
  83. package/templates/libs/shared/src/strategies/__tests__/fake-storage.contract.unit.test.ts +2 -2
  84. package/templates/libs/shared/src/transport.ts +41 -0
  85. package/templates/libs/storage-strategies/cloudinary/package.json +1 -1
  86. package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.contract.unit.test.ts +1 -2
  87. package/templates/libs/storage-strategies/firebase/package.json +1 -1
  88. package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.contract.unit.test.ts +1 -2
  89. package/templates/libs/storage-strategies/supabase/package.json +1 -1
  90. package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.contract.unit.test.ts +1 -2
  91. package/templates/libs/template-shared/package.json +1 -1
  92. package/templates/libs/upload-client/package.json +1 -1
  93. package/templates/libs/vite-plugins/src/index.d.mts +5 -7
  94. package/templates/libs/vite-plugins/src/index.mjs +1 -1
  95. package/templates/libs/vite-plugins/tsconfig.json +2 -1
  96. package/templates/package.json +2 -1
  97. package/templates/tools/create-icore/_template-shell/package.json +2 -1
  98. package/templates/.yarn/releases/yarn-4.5.0.cjs +0 -925
package/dist/cli.js CHANGED
@@ -195,7 +195,10 @@ Re-run with @latest to refresh:
195
195
  options: [
196
196
  { value: "tcp", label: "TCP (default, no broker required)" },
197
197
  { value: "redis", label: "Redis" },
198
- { value: "nats", label: "NATS" }
198
+ { value: "nats", label: "NATS" },
199
+ { value: "mqtt", label: "MQTT" },
200
+ { value: "rmq", label: "RabbitMQ" },
201
+ { value: "kafka", label: "Kafka" }
199
202
  ],
200
203
  initialValue: "tcp"
201
204
  });
@@ -265,6 +268,24 @@ async function copyTree(src, dest) {
265
268
  else if (entry.isFile()) await copyFile(s, d);
266
269
  }
267
270
  }
271
+ var TRANSPORT_ENV_TOKEN = {
272
+ redis: "REDIS",
273
+ nats: "NATS",
274
+ mqtt: "MQTT",
275
+ rmq: "RMQ",
276
+ kafka: "KAFKA"
277
+ };
278
+ var TRANSPORT_DEPS = {
279
+ nats: { nats: "^2.29.3" },
280
+ mqtt: { mqtt: "^5.15.1" },
281
+ rmq: { amqplib: "^2.0.1", "amqp-connection-manager": "^5.0.0" },
282
+ kafka: { kafkajs: "^2.2.4" }
283
+ };
284
+ function uncommentTransportEnv(text2, prefix, transport) {
285
+ const token = TRANSPORT_ENV_TOKEN[transport];
286
+ if (!token) return text2;
287
+ return text2.replace(new RegExp(`^# (${prefix}_${token}_[A-Z0-9_]*=)`, "gm"), "$1");
288
+ }
268
289
  async function rewriteRootPackageJson(targetDir, opts) {
269
290
  const pkgPath = join2(targetDir, "package.json");
270
291
  const raw = await readFile2(pkgPath, "utf8");
@@ -273,38 +294,31 @@ async function rewriteRootPackageJson(targetDir, opts) {
273
294
  pkg["version"] = "0.0.1";
274
295
  pkg["private"] = true;
275
296
  delete pkg.description;
276
- if (opts.transport === "nats") {
297
+ const transportDeps = TRANSPORT_DEPS[opts.transport];
298
+ if (transportDeps) {
277
299
  const deps = pkg["dependencies"] ??= {};
278
- deps["nats"] = "^2.29.3";
300
+ Object.assign(deps, transportDeps);
279
301
  }
280
302
  if (opts.packageManager !== "yarn") {
281
303
  delete pkg.packageManager;
304
+ } else {
305
+ try {
306
+ const yarnrc = await readFile2(join2(targetDir, ".yarnrc.yml"), "utf8");
307
+ const match = yarnrc.match(/^yarnPath:\s*.+yarn-(\d+\.\d+\.\d+)\.cjs/m);
308
+ if (match?.[1]) {
309
+ pkg["packageManager"] = `yarn@${match[1]}`;
310
+ }
311
+ } catch {
312
+ }
282
313
  }
283
- if (opts.packageManager === "pnpm") {
284
- pkg["pnpm"] = {
285
- onlyBuiltDependencies: [
286
- "@firebase/util",
287
- "@nestjs/core",
288
- "@parcel/watcher",
289
- "@scarf/scarf",
290
- "@swc/core",
291
- "less",
292
- "msgpackr-extract",
293
- "nx",
294
- "protobufjs",
295
- "unrs-resolver"
296
- ]
297
- };
298
- }
314
+ delete pkg.pnpm;
299
315
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
300
316
  }
301
317
  async function writeAuthEnv(targetDir, opts) {
302
318
  const envExample = join2(targetDir, "apps/microservices/auth/.env.example");
303
319
  const env = await readFile2(envExample, "utf8");
304
320
  let next = env.replace(/^AUTH_PROVIDER=.*$/m, `AUTH_PROVIDER=${opts.authProvider}`).replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`);
305
- if (opts.transport !== "tcp") {
306
- next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1");
307
- }
321
+ next = uncommentTransportEnv(next, "AUTH", opts.transport);
308
322
  await writeFile(join2(targetDir, "apps/microservices/auth/.env"), next);
309
323
  }
310
324
  async function writeUploadEnv(targetDir, opts) {
@@ -312,9 +326,7 @@ async function writeUploadEnv(targetDir, opts) {
312
326
  const envExample = join2(targetDir, "apps/microservices/upload/.env.example");
313
327
  const env = await readFile2(envExample, "utf8");
314
328
  let next = env.replace(/^STORAGE_PROVIDER=.*$/m, `STORAGE_PROVIDER=${opts.upload}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`);
315
- if (opts.transport !== "tcp") {
316
- next = next.replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1");
317
- }
329
+ next = uncommentTransportEnv(next, "UPLOAD", opts.transport);
318
330
  await writeFile(join2(targetDir, "apps/microservices/upload/.env"), next);
319
331
  }
320
332
  async function writeNotesEnv(targetDir, opts) {
@@ -323,9 +335,7 @@ async function writeNotesEnv(targetDir, opts) {
323
335
  try {
324
336
  const env = await readFile2(envExample, "utf8");
325
337
  let next = env.replace(/^NOTES_TRANSPORT=.*$/m, `NOTES_TRANSPORT=${opts.transport}`);
326
- if (opts.transport !== "tcp") {
327
- next = next.replace(/^# (NOTES_(?:REDIS|NATS)_URL=)/m, "$1");
328
- }
338
+ next = uncommentTransportEnv(next, "NOTES", opts.transport);
329
339
  await writeFile(join2(targetDir, "apps/microservices/notes/.env"), next);
330
340
  } catch {
331
341
  }
@@ -334,8 +344,8 @@ async function writeGatewayEnv(targetDir, opts) {
334
344
  const envExample = join2(targetDir, "apps/api/.env.example");
335
345
  const env = await readFile2(envExample, "utf8");
336
346
  let next = env.replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`).replace(/^NOTES_TRANSPORT=.*$/m, `NOTES_TRANSPORT=${opts.transport}`).replace(/^PAYMENT_TRANSPORT=.*$/m, `PAYMENT_TRANSPORT=${opts.transport}`);
337
- if (opts.transport !== "tcp") {
338
- next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (NOTES_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (PAYMENT_(?:REDIS|NATS)_URL=)/m, "$1");
347
+ for (const prefix of ["AUTH", "UPLOAD", "NOTES", "PAYMENT"]) {
348
+ next = uncommentTransportEnv(next, prefix, opts.transport);
339
349
  }
340
350
  await writeFile(join2(targetDir, "apps/api/.env"), next);
341
351
  }
@@ -373,9 +383,7 @@ async function writePaymentEnv(targetDir, opts) {
373
383
  try {
374
384
  const env = await readFile2(envExample, "utf8");
375
385
  let next = env.replace(/^PAYMENT_PROVIDER=.*$/m, `PAYMENT_PROVIDER=${opts.payment}`).replace(/^PAYMENT_TRANSPORT=.*$/m, `PAYMENT_TRANSPORT=${opts.transport}`);
376
- if (opts.transport !== "tcp") {
377
- next = next.replace(/^# (PAYMENT_(?:REDIS|NATS)_URL=)/m, "$1");
378
- }
386
+ next = uncommentTransportEnv(next, "PAYMENT", opts.transport);
379
387
  await writeFile(join2(targetDir, "apps/microservices/payment/.env"), next);
380
388
  } catch {
381
389
  }
@@ -464,7 +472,10 @@ async function removeNotesStack(targetDir) {
464
472
  await writeFile(appModulePath, next);
465
473
  } catch {
466
474
  }
467
- await stripDeps(join2(targetDir, "apps/api/package.json"), ["@icore/notes-client"]);
475
+ await stripDeps(join2(targetDir, "apps/api/package.json"), [
476
+ "@icore/notes-client",
477
+ "@casl/ability"
478
+ ]);
468
479
  await stripGatewayTransport(targetDir, "NOTES");
469
480
  const tsconfigPath = join2(targetDir, "tsconfig.base.json");
470
481
  try {
@@ -476,21 +487,16 @@ async function removeNotesStack(targetDir) {
476
487
  const siderPath = join2(targetDir, "apps/client/src/components/layout/LayoutSider.tsx");
477
488
  try {
478
489
  const src = await readFile2(siderPath, "utf8");
479
- const next = src.replace(", StickyNote", "").replace(/\n {8}<Link\n {10}to="\/_dashboard\/notes"[\s\S]*?<\/Link>/, "").replace(", FileTextOutlined", "").replace(
490
+ const next = src.replace(", StickyNote", "").replace(/\n {8}<Link\n {10}to="\/(?:_dashboard\/)?notes"[\s\S]*?<\/Link>/, "").replace(", FileTextOutlined", "").replace(
480
491
  "const selectedKey = pathname.includes('/notes')\n ? 'notes'\n : pathname.includes('/profile')",
481
492
  "const selectedKey = pathname.includes('/profile')"
482
493
  ).replace(
483
- `
484
- {
485
- key: 'notes',
486
- icon: <FileTextOutlined />,
487
- label: <Link to="/_dashboard/notes">{t('notes.title')}</Link>,
488
- },`,
494
+ /\n {4}\{\n {6}key: 'notes',\n {6}icon: <FileTextOutlined \/>,\n {6}label: <Link to="\/(?:_dashboard\/)?notes">\{t\('notes\.title'\)\}<\/Link>,\n {4}\},/,
489
495
  ""
490
496
  ).replace("import NoteOutlinedIcon from '@mui/icons-material/NoteOutlined';\n", "").replace(
491
- /\n {8}<ListItemButton\n {10}component=\{Link\}\n {10}to="\/_dashboard\/notes"[\s\S]*?<\/ListItemButton>/,
497
+ /\n {8}<ListItemButton\n {10}component=\{Link\}\n {10}to="\/(?:_dashboard\/)?notes"[\s\S]*?<\/ListItemButton>/,
492
498
  ""
493
- ).replace(/\n\s*<Link to="\/_dashboard\/notes">[\s\S]*?<\/Link>/m, "");
499
+ ).replace(/\n\s*<Link to="\/(?:_dashboard\/)?notes">[\s\S]*?<\/Link>/m, "");
494
500
  await writeFile(siderPath, next);
495
501
  } catch {
496
502
  }
@@ -633,7 +639,7 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
633
639
  await stripTsconfigPath(targetDir, "@icore/db-supabase");
634
640
  try {
635
641
  const src = await readFile2(modulePath, "utf8");
636
- const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(/\nfunction makeSupabaseDB[\s\S]*?\n}\n/m, "").replace(
642
+ const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(/\nfunction makeSupabaseDB[\s\S]*?\n}\n/m, "").replace(/\nfunction requireEnv[\s\S]*?\n}\n/m, "").replace(
637
643
  /if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
638
644
  "return makeFirestoreDB(cfg);"
639
645
  );
@@ -766,11 +772,69 @@ async function scaffold(opts, templatesDir2) {
766
772
  await rm(join2(opts.targetDir, ".yarn"), { recursive: true, force: true });
767
773
  await rm(join2(opts.targetDir, ".yarnrc.yml"), { force: true });
768
774
  }
775
+ if (opts.packageManager === "pnpm") {
776
+ await writePnpmWorkspace(opts.targetDir);
777
+ await rewritePnpmWorkspaceDeps(opts.targetDir);
778
+ }
769
779
  await patchGitignoreForPm(opts.targetDir, opts.packageManager);
770
780
  await writeAiFiles(opts.targetDir, opts);
771
781
  if (opts.install) runInstall(opts.targetDir, opts.packageManager);
772
782
  if (opts.initGit) gitInit(opts.targetDir, opts.projectName);
773
783
  }
784
+ async function writePnpmWorkspace(targetDir) {
785
+ const pkgPath = join2(targetDir, "package.json");
786
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
787
+ const workspaces = pkg.workspaces ?? [];
788
+ const packagesBlock = workspaces.map((p3) => ` - '${p3}'`).join("\n");
789
+ const allowBuilds = [
790
+ "@firebase/util",
791
+ "@nestjs/core",
792
+ "@parcel/watcher",
793
+ "@scarf/scarf",
794
+ "@swc/core",
795
+ "less",
796
+ "msgpackr-extract",
797
+ "nx",
798
+ "protobufjs",
799
+ "unrs-resolver"
800
+ ].map((p3) => ` '${p3}': true`).join("\n");
801
+ const content = `packages:
802
+ ${packagesBlock}
803
+
804
+ allowBuilds:
805
+ ${allowBuilds}
806
+ `;
807
+ await writeFile(join2(targetDir, "pnpm-workspace.yaml"), content);
808
+ }
809
+ async function rewritePnpmWorkspaceDeps(targetDir) {
810
+ const { readdir: rd } = await import("fs/promises");
811
+ async function walk(dir) {
812
+ const found = [];
813
+ let entries;
814
+ try {
815
+ entries = await rd(dir, { withFileTypes: true });
816
+ } catch {
817
+ return found;
818
+ }
819
+ for (const e of entries) {
820
+ if (e.isDirectory() && e.name !== "node_modules") {
821
+ found.push(...await walk(join2(dir, e.name)));
822
+ } else if (e.isFile() && e.name === "package.json") {
823
+ found.push(join2(dir, e.name));
824
+ }
825
+ }
826
+ return found;
827
+ }
828
+ const pkgFiles = await walk(targetDir);
829
+ for (const f of pkgFiles) {
830
+ try {
831
+ const raw = await readFile2(f, "utf8");
832
+ const next = raw.replace(/"(@icore\/[^"]+)":\s*"\*"/g, '"$1": "workspace:*"');
833
+ if (next !== raw) await writeFile(f, next);
834
+ } catch {
835
+ }
836
+ }
837
+ }
774
838
  async function patchGitignoreForPm(targetDir, pm) {
775
839
  const giPath = join2(targetDir, ".gitignore");
776
840
  try {
@@ -874,7 +938,7 @@ Apache-2.0
874
938
 
875
939
  - **Branch strategy**: \`dev\` is default. Cut \`feature/<name>\` or \`bug/<name>\` from dev. PRs only target dev. Never push directly to main.
876
940
  - **No code without approval**: Propose changes first, wait for go-ahead.
877
- - **\u0417\u0410\u041A\u041E\u041D \u2014 no crash on missing .env**: MS factories must catch config errors, print a boxed banner with ALL missing vars, and return a Fake strategy in dev. In prod (\`NODE_ENV=production\`) throw the same banner. The \`formatEnvBanner\` + \`missingEnv\` helpers from \`@icore/shared\` handle this.
941
+ - **RULE \u2014 no crash on missing .env**: MS factories must catch config errors, print a boxed banner with ALL missing vars, and return a Fake strategy in dev. In prod (\`NODE_ENV=production\`) throw the same banner. The \`formatEnvBanner\` + \`missingEnv\` helpers from \`@icore/shared\` handle this.
878
942
  - **Post-coding routine**: \`npx prettier --write <files>\` \u2192 \`${nx} lint <project>\` \u2192 \`${nx} build <project>\` \u2014 all green before committing.
879
943
  - **Nx generators only**: never hand-write \`project.json\` / tsconfig stacks. Use \`${nx} g @nx/<plugin>:<schematic>\`.
880
944
 
package/dist/index.cjs CHANGED
@@ -74,6 +74,24 @@ async function copyTree(src, dest) {
74
74
  else if (entry.isFile()) await (0, import_promises.copyFile)(s, d);
75
75
  }
76
76
  }
77
+ var TRANSPORT_ENV_TOKEN = {
78
+ redis: "REDIS",
79
+ nats: "NATS",
80
+ mqtt: "MQTT",
81
+ rmq: "RMQ",
82
+ kafka: "KAFKA"
83
+ };
84
+ var TRANSPORT_DEPS = {
85
+ nats: { nats: "^2.29.3" },
86
+ mqtt: { mqtt: "^5.15.1" },
87
+ rmq: { amqplib: "^2.0.1", "amqp-connection-manager": "^5.0.0" },
88
+ kafka: { kafkajs: "^2.2.4" }
89
+ };
90
+ function uncommentTransportEnv(text2, prefix, transport) {
91
+ const token = TRANSPORT_ENV_TOKEN[transport];
92
+ if (!token) return text2;
93
+ return text2.replace(new RegExp(`^# (${prefix}_${token}_[A-Z0-9_]*=)`, "gm"), "$1");
94
+ }
77
95
  async function rewriteRootPackageJson(targetDir, opts) {
78
96
  const pkgPath = (0, import_node_path.join)(targetDir, "package.json");
79
97
  const raw = await (0, import_promises.readFile)(pkgPath, "utf8");
@@ -82,38 +100,31 @@ async function rewriteRootPackageJson(targetDir, opts) {
82
100
  pkg["version"] = "0.0.1";
83
101
  pkg["private"] = true;
84
102
  delete pkg.description;
85
- if (opts.transport === "nats") {
103
+ const transportDeps = TRANSPORT_DEPS[opts.transport];
104
+ if (transportDeps) {
86
105
  const deps = pkg["dependencies"] ??= {};
87
- deps["nats"] = "^2.29.3";
106
+ Object.assign(deps, transportDeps);
88
107
  }
89
108
  if (opts.packageManager !== "yarn") {
90
109
  delete pkg.packageManager;
110
+ } else {
111
+ try {
112
+ const yarnrc = await (0, import_promises.readFile)((0, import_node_path.join)(targetDir, ".yarnrc.yml"), "utf8");
113
+ const match = yarnrc.match(/^yarnPath:\s*.+yarn-(\d+\.\d+\.\d+)\.cjs/m);
114
+ if (match?.[1]) {
115
+ pkg["packageManager"] = `yarn@${match[1]}`;
116
+ }
117
+ } catch {
118
+ }
91
119
  }
92
- if (opts.packageManager === "pnpm") {
93
- pkg["pnpm"] = {
94
- onlyBuiltDependencies: [
95
- "@firebase/util",
96
- "@nestjs/core",
97
- "@parcel/watcher",
98
- "@scarf/scarf",
99
- "@swc/core",
100
- "less",
101
- "msgpackr-extract",
102
- "nx",
103
- "protobufjs",
104
- "unrs-resolver"
105
- ]
106
- };
107
- }
120
+ delete pkg.pnpm;
108
121
  await (0, import_promises.writeFile)(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
109
122
  }
110
123
  async function writeAuthEnv(targetDir, opts) {
111
124
  const envExample = (0, import_node_path.join)(targetDir, "apps/microservices/auth/.env.example");
112
125
  const env = await (0, import_promises.readFile)(envExample, "utf8");
113
126
  let next = env.replace(/^AUTH_PROVIDER=.*$/m, `AUTH_PROVIDER=${opts.authProvider}`).replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`);
114
- if (opts.transport !== "tcp") {
115
- next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1");
116
- }
127
+ next = uncommentTransportEnv(next, "AUTH", opts.transport);
117
128
  await (0, import_promises.writeFile)((0, import_node_path.join)(targetDir, "apps/microservices/auth/.env"), next);
118
129
  }
119
130
  async function writeUploadEnv(targetDir, opts) {
@@ -121,9 +132,7 @@ async function writeUploadEnv(targetDir, opts) {
121
132
  const envExample = (0, import_node_path.join)(targetDir, "apps/microservices/upload/.env.example");
122
133
  const env = await (0, import_promises.readFile)(envExample, "utf8");
123
134
  let next = env.replace(/^STORAGE_PROVIDER=.*$/m, `STORAGE_PROVIDER=${opts.upload}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`);
124
- if (opts.transport !== "tcp") {
125
- next = next.replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1");
126
- }
135
+ next = uncommentTransportEnv(next, "UPLOAD", opts.transport);
127
136
  await (0, import_promises.writeFile)((0, import_node_path.join)(targetDir, "apps/microservices/upload/.env"), next);
128
137
  }
129
138
  async function writeNotesEnv(targetDir, opts) {
@@ -132,9 +141,7 @@ async function writeNotesEnv(targetDir, opts) {
132
141
  try {
133
142
  const env = await (0, import_promises.readFile)(envExample, "utf8");
134
143
  let next = env.replace(/^NOTES_TRANSPORT=.*$/m, `NOTES_TRANSPORT=${opts.transport}`);
135
- if (opts.transport !== "tcp") {
136
- next = next.replace(/^# (NOTES_(?:REDIS|NATS)_URL=)/m, "$1");
137
- }
144
+ next = uncommentTransportEnv(next, "NOTES", opts.transport);
138
145
  await (0, import_promises.writeFile)((0, import_node_path.join)(targetDir, "apps/microservices/notes/.env"), next);
139
146
  } catch {
140
147
  }
@@ -143,8 +150,8 @@ async function writeGatewayEnv(targetDir, opts) {
143
150
  const envExample = (0, import_node_path.join)(targetDir, "apps/api/.env.example");
144
151
  const env = await (0, import_promises.readFile)(envExample, "utf8");
145
152
  let next = env.replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`).replace(/^NOTES_TRANSPORT=.*$/m, `NOTES_TRANSPORT=${opts.transport}`).replace(/^PAYMENT_TRANSPORT=.*$/m, `PAYMENT_TRANSPORT=${opts.transport}`);
146
- if (opts.transport !== "tcp") {
147
- next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (NOTES_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (PAYMENT_(?:REDIS|NATS)_URL=)/m, "$1");
153
+ for (const prefix of ["AUTH", "UPLOAD", "NOTES", "PAYMENT"]) {
154
+ next = uncommentTransportEnv(next, prefix, opts.transport);
148
155
  }
149
156
  await (0, import_promises.writeFile)((0, import_node_path.join)(targetDir, "apps/api/.env"), next);
150
157
  }
@@ -182,9 +189,7 @@ async function writePaymentEnv(targetDir, opts) {
182
189
  try {
183
190
  const env = await (0, import_promises.readFile)(envExample, "utf8");
184
191
  let next = env.replace(/^PAYMENT_PROVIDER=.*$/m, `PAYMENT_PROVIDER=${opts.payment}`).replace(/^PAYMENT_TRANSPORT=.*$/m, `PAYMENT_TRANSPORT=${opts.transport}`);
185
- if (opts.transport !== "tcp") {
186
- next = next.replace(/^# (PAYMENT_(?:REDIS|NATS)_URL=)/m, "$1");
187
- }
192
+ next = uncommentTransportEnv(next, "PAYMENT", opts.transport);
188
193
  await (0, import_promises.writeFile)((0, import_node_path.join)(targetDir, "apps/microservices/payment/.env"), next);
189
194
  } catch {
190
195
  }
@@ -273,7 +278,10 @@ async function removeNotesStack(targetDir) {
273
278
  await (0, import_promises.writeFile)(appModulePath, next);
274
279
  } catch {
275
280
  }
276
- await stripDeps((0, import_node_path.join)(targetDir, "apps/api/package.json"), ["@icore/notes-client"]);
281
+ await stripDeps((0, import_node_path.join)(targetDir, "apps/api/package.json"), [
282
+ "@icore/notes-client",
283
+ "@casl/ability"
284
+ ]);
277
285
  await stripGatewayTransport(targetDir, "NOTES");
278
286
  const tsconfigPath = (0, import_node_path.join)(targetDir, "tsconfig.base.json");
279
287
  try {
@@ -285,21 +293,16 @@ async function removeNotesStack(targetDir) {
285
293
  const siderPath = (0, import_node_path.join)(targetDir, "apps/client/src/components/layout/LayoutSider.tsx");
286
294
  try {
287
295
  const src = await (0, import_promises.readFile)(siderPath, "utf8");
288
- const next = src.replace(", StickyNote", "").replace(/\n {8}<Link\n {10}to="\/_dashboard\/notes"[\s\S]*?<\/Link>/, "").replace(", FileTextOutlined", "").replace(
296
+ const next = src.replace(", StickyNote", "").replace(/\n {8}<Link\n {10}to="\/(?:_dashboard\/)?notes"[\s\S]*?<\/Link>/, "").replace(", FileTextOutlined", "").replace(
289
297
  "const selectedKey = pathname.includes('/notes')\n ? 'notes'\n : pathname.includes('/profile')",
290
298
  "const selectedKey = pathname.includes('/profile')"
291
299
  ).replace(
292
- `
293
- {
294
- key: 'notes',
295
- icon: <FileTextOutlined />,
296
- label: <Link to="/_dashboard/notes">{t('notes.title')}</Link>,
297
- },`,
300
+ /\n {4}\{\n {6}key: 'notes',\n {6}icon: <FileTextOutlined \/>,\n {6}label: <Link to="\/(?:_dashboard\/)?notes">\{t\('notes\.title'\)\}<\/Link>,\n {4}\},/,
298
301
  ""
299
302
  ).replace("import NoteOutlinedIcon from '@mui/icons-material/NoteOutlined';\n", "").replace(
300
- /\n {8}<ListItemButton\n {10}component=\{Link\}\n {10}to="\/_dashboard\/notes"[\s\S]*?<\/ListItemButton>/,
303
+ /\n {8}<ListItemButton\n {10}component=\{Link\}\n {10}to="\/(?:_dashboard\/)?notes"[\s\S]*?<\/ListItemButton>/,
301
304
  ""
302
- ).replace(/\n\s*<Link to="\/_dashboard\/notes">[\s\S]*?<\/Link>/m, "");
305
+ ).replace(/\n\s*<Link to="\/(?:_dashboard\/)?notes">[\s\S]*?<\/Link>/m, "");
303
306
  await (0, import_promises.writeFile)(siderPath, next);
304
307
  } catch {
305
308
  }
@@ -442,7 +445,7 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
442
445
  await stripTsconfigPath(targetDir, "@icore/db-supabase");
443
446
  try {
444
447
  const src = await (0, import_promises.readFile)(modulePath, "utf8");
445
- const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(/\nfunction makeSupabaseDB[\s\S]*?\n}\n/m, "").replace(
448
+ const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(/\nfunction makeSupabaseDB[\s\S]*?\n}\n/m, "").replace(/\nfunction requireEnv[\s\S]*?\n}\n/m, "").replace(
446
449
  /if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
447
450
  "return makeFirestoreDB(cfg);"
448
451
  );
@@ -575,11 +578,69 @@ async function scaffold(opts, templatesDir) {
575
578
  await (0, import_promises.rm)((0, import_node_path.join)(opts.targetDir, ".yarn"), { recursive: true, force: true });
576
579
  await (0, import_promises.rm)((0, import_node_path.join)(opts.targetDir, ".yarnrc.yml"), { force: true });
577
580
  }
581
+ if (opts.packageManager === "pnpm") {
582
+ await writePnpmWorkspace(opts.targetDir);
583
+ await rewritePnpmWorkspaceDeps(opts.targetDir);
584
+ }
578
585
  await patchGitignoreForPm(opts.targetDir, opts.packageManager);
579
586
  await writeAiFiles(opts.targetDir, opts);
580
587
  if (opts.install) runInstall(opts.targetDir, opts.packageManager);
581
588
  if (opts.initGit) gitInit(opts.targetDir, opts.projectName);
582
589
  }
590
+ async function writePnpmWorkspace(targetDir) {
591
+ const pkgPath = (0, import_node_path.join)(targetDir, "package.json");
592
+ const pkg = JSON.parse(await (0, import_promises.readFile)(pkgPath, "utf8"));
593
+ const workspaces = pkg.workspaces ?? [];
594
+ const packagesBlock = workspaces.map((p2) => ` - '${p2}'`).join("\n");
595
+ const allowBuilds = [
596
+ "@firebase/util",
597
+ "@nestjs/core",
598
+ "@parcel/watcher",
599
+ "@scarf/scarf",
600
+ "@swc/core",
601
+ "less",
602
+ "msgpackr-extract",
603
+ "nx",
604
+ "protobufjs",
605
+ "unrs-resolver"
606
+ ].map((p2) => ` '${p2}': true`).join("\n");
607
+ const content = `packages:
608
+ ${packagesBlock}
609
+
610
+ allowBuilds:
611
+ ${allowBuilds}
612
+ `;
613
+ await (0, import_promises.writeFile)((0, import_node_path.join)(targetDir, "pnpm-workspace.yaml"), content);
614
+ }
615
+ async function rewritePnpmWorkspaceDeps(targetDir) {
616
+ const { readdir: rd } = await import("fs/promises");
617
+ async function walk(dir) {
618
+ const found = [];
619
+ let entries;
620
+ try {
621
+ entries = await rd(dir, { withFileTypes: true });
622
+ } catch {
623
+ return found;
624
+ }
625
+ for (const e of entries) {
626
+ if (e.isDirectory() && e.name !== "node_modules") {
627
+ found.push(...await walk((0, import_node_path.join)(dir, e.name)));
628
+ } else if (e.isFile() && e.name === "package.json") {
629
+ found.push((0, import_node_path.join)(dir, e.name));
630
+ }
631
+ }
632
+ return found;
633
+ }
634
+ const pkgFiles = await walk(targetDir);
635
+ for (const f of pkgFiles) {
636
+ try {
637
+ const raw = await (0, import_promises.readFile)(f, "utf8");
638
+ const next = raw.replace(/"(@icore\/[^"]+)":\s*"\*"/g, '"$1": "workspace:*"');
639
+ if (next !== raw) await (0, import_promises.writeFile)(f, next);
640
+ } catch {
641
+ }
642
+ }
643
+ }
583
644
  async function patchGitignoreForPm(targetDir, pm) {
584
645
  const giPath = (0, import_node_path.join)(targetDir, ".gitignore");
585
646
  try {
@@ -683,7 +744,7 @@ Apache-2.0
683
744
 
684
745
  - **Branch strategy**: \`dev\` is default. Cut \`feature/<name>\` or \`bug/<name>\` from dev. PRs only target dev. Never push directly to main.
685
746
  - **No code without approval**: Propose changes first, wait for go-ahead.
686
- - **\u0417\u0410\u041A\u041E\u041D \u2014 no crash on missing .env**: MS factories must catch config errors, print a boxed banner with ALL missing vars, and return a Fake strategy in dev. In prod (\`NODE_ENV=production\`) throw the same banner. The \`formatEnvBanner\` + \`missingEnv\` helpers from \`@icore/shared\` handle this.
747
+ - **RULE \u2014 no crash on missing .env**: MS factories must catch config errors, print a boxed banner with ALL missing vars, and return a Fake strategy in dev. In prod (\`NODE_ENV=production\`) throw the same banner. The \`formatEnvBanner\` + \`missingEnv\` helpers from \`@icore/shared\` handle this.
687
748
  - **Post-coding routine**: \`npx prettier --write <files>\` \u2192 \`${nx} lint <project>\` \u2192 \`${nx} build <project>\` \u2014 all green before committing.
688
749
  - **Nx generators only**: never hand-write \`project.json\` / tsconfig stacks. Use \`${nx} g @nx/<plugin>:<schematic>\`.
689
750
 
@@ -980,7 +1041,10 @@ Re-run with @latest to refresh:
980
1041
  options: [
981
1042
  { value: "tcp", label: "TCP (default, no broker required)" },
982
1043
  { value: "redis", label: "Redis" },
983
- { value: "nats", label: "NATS" }
1044
+ { value: "nats", label: "NATS" },
1045
+ { value: "mqtt", label: "MQTT" },
1046
+ { value: "rmq", label: "RabbitMQ" },
1047
+ { value: "kafka", label: "Kafka" }
984
1048
  ],
985
1049
  initialValue: "tcp"
986
1050
  });
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ type PaymentProvider = 'paypal' | 'none';
5
5
  type JobsProvider = 'bullmq' | 'none';
6
6
  type ExampleMode = 'notes' | 'none';
7
7
  type UiLibrary = 'shadcn' | 'antd' | 'mui';
8
- type MsTransport = 'tcp' | 'redis' | 'nats';
8
+ type MsTransport = 'tcp' | 'redis' | 'nats' | 'mqtt' | 'rmq' | 'kafka';
9
9
  type PackageManager = 'yarn' | 'npm' | 'pnpm';
10
10
  /**
11
11
  * Returns the correct invocation for a package.json script.
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ type PaymentProvider = 'paypal' | 'none';
5
5
  type JobsProvider = 'bullmq' | 'none';
6
6
  type ExampleMode = 'notes' | 'none';
7
7
  type UiLibrary = 'shadcn' | 'antd' | 'mui';
8
- type MsTransport = 'tcp' | 'redis' | 'nats';
8
+ type MsTransport = 'tcp' | 'redis' | 'nats' | 'mqtt' | 'rmq' | 'kafka';
9
9
  type PackageManager = 'yarn' | 'npm' | 'pnpm';
10
10
  /**
11
11
  * Returns the correct invocation for a package.json script.