@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
@@ -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_MUI': JSON.stringify(depVersion('@mui/material')),
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('@mui')) return 'vendor-mui';
87
69
  if (id.includes('@emotion')) return 'vendor-emotion';
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-mui',
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-mui',
104
- provider: 'v8' as const,
105
- },
106
- },
74
+ test: commonTestConfig('client-mui', '../../../coverage/apps/templates/client-mui'),
107
75
  }));
@@ -3,9 +3,16 @@ import fs from 'node:fs';
3
3
  import { defineConfig } from 'vite';
4
4
  import react from '@vitejs/plugin-react';
5
5
  import tailwindcss from '@tailwindcss/vite';
6
- import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
6
+ import { tanstackRouter } from '@tanstack/router-plugin/vite';
7
7
  import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
8
8
  import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
9
+ import {
10
+ commonDefines,
11
+ commonManualChunks,
12
+ commonTestConfig,
13
+ injectAppVersionPlugin,
14
+ noServerModulesPlugin,
15
+ } from '@icore/vite-plugins';
9
16
 
10
17
  const rootPackageJsonPath = new URL('../../../package.json', import.meta.url);
11
18
  const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')) as {
@@ -15,11 +22,7 @@ const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')
15
22
  };
16
23
 
17
24
  function depVersion(name: string): string {
18
- return (
19
- rootPackageJson.dependencies?.[name] ??
20
- rootPackageJson.devDependencies?.[name] ??
21
- '?'
22
- );
25
+ return rootPackageJson.dependencies?.[name] ?? rootPackageJson.devDependencies?.[name] ?? '?';
23
26
  }
24
27
 
25
28
  export default defineConfig(() => ({
@@ -34,22 +37,11 @@ export default defineConfig(() => ({
34
37
  host: 'localhost',
35
38
  },
36
39
  define: {
37
- 'import.meta.env.VITE_APP_VERSION': JSON.stringify(rootPackageJson.version),
38
- // Dep versions injected at build time so routes don't need JSON imports
39
- 'import.meta.env.VITE_DEP_REACT': JSON.stringify(depVersion('react')),
40
- 'import.meta.env.VITE_DEP_VITE': JSON.stringify(depVersion('vite')),
40
+ ...commonDefines(rootPackageJson),
41
41
  'import.meta.env.VITE_DEP_TAILWINDCSS': JSON.stringify(depVersion('tailwindcss')),
42
- 'import.meta.env.VITE_DEP_TANSTACK_ROUTER': JSON.stringify(
43
- depVersion('@tanstack/react-router'),
44
- ),
45
- 'import.meta.env.VITE_DEP_TANSTACK_QUERY': JSON.stringify(
46
- depVersion('@tanstack/react-query'),
47
- ),
48
- 'import.meta.env.VITE_DEP_ZUSTAND': JSON.stringify(depVersion('zustand')),
49
- 'import.meta.env.VITE_DEP_CASL': JSON.stringify(depVersion('@casl/ability')),
50
42
  },
51
43
  plugins: [
52
- TanStackRouterVite({
44
+ tanstackRouter({
53
45
  target: 'react',
54
46
  autoCodeSplitting: true,
55
47
  routeFileIgnorePattern: '(__tests__|\\.test\\.(t|j)sx?$)',
@@ -58,12 +50,8 @@ export default defineConfig(() => ({
58
50
  tailwindcss(),
59
51
  nxViteTsPaths(),
60
52
  nxCopyAssetsPlugin(['*.md']),
61
- {
62
- name: 'inject-app-version-meta',
63
- transformIndexHtml(html: string) {
64
- return html.replace('%APP_VERSION%', rootPackageJson.version);
65
- },
66
- },
53
+ noServerModulesPlugin(),
54
+ injectAppVersionPlugin(rootPackageJson),
67
55
  ],
68
56
  // Uncomment this if you are using workers.
69
57
  // worker: {
@@ -78,31 +66,11 @@ export default defineConfig(() => ({
78
66
  },
79
67
  rolldownOptions: {
80
68
  output: {
81
- manualChunks: (id: string) => {
82
- if (!id.includes('node_modules')) return;
83
- if (id.includes('/react/') || id.includes('/react-dom/') || id.includes('scheduler'))
84
- return 'vendor-react';
85
- if (id.includes('@tanstack')) return 'vendor-tanstack';
86
- if (id.includes('i18next') || id.includes('react-i18next')) return 'vendor-i18n';
87
- if (id.includes('@casl')) return 'vendor-casl';
69
+ manualChunks: commonManualChunks((id) => {
88
70
  if (id.includes('lucide-react') || id.includes('@radix-ui')) return 'vendor-ui';
89
- if (id.includes('zustand')) return 'vendor-state';
90
- if (id.includes('@idevconn')) return 'vendor-idevconn';
91
- return 'vendor-core';
92
- },
71
+ }),
93
72
  },
94
73
  },
95
74
  },
96
- test: {
97
- name: 'client-shadcn',
98
- watch: false,
99
- globals: true,
100
- environment: 'jsdom',
101
- include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
102
- reporters: ['default'],
103
- coverage: {
104
- reportsDirectory: '../../../coverage/apps/templates/client-shadcn',
105
- provider: 'v8' as const,
106
- },
107
- },
75
+ test: commonTestConfig('client-shadcn', '../../../coverage/apps/templates/client-shadcn'),
108
76
  }));
@@ -5,6 +5,16 @@
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "require": "./src/index.js",
11
+ "types": "./src/index.d.ts"
12
+ },
13
+ "./client": {
14
+ "require": "./src/client.js",
15
+ "types": "./src/client.d.ts"
16
+ }
17
+ },
8
18
  "dependencies": {
9
19
  "@casl/ability": "^7.0.0",
10
20
  "@nestjs/microservices": "^11.0.0",
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Cross-boundary dependency guard.
3
+ *
4
+ * Rule 1 — CLIENT boundary: no file reachable from @icore/shared/client
5
+ * may import @nestjs/* (server-only).
6
+ *
7
+ * Rule 2 — SERVER boundary: no file under apps/microservices/** or
8
+ * libs/*-client/** (NestJS modules) may import react or other
9
+ * browser-only packages.
10
+ *
11
+ * These are static source-level checks — they run fast in Vitest without
12
+ * needing a full build.
13
+ */
14
+ import { readdir, readFile, stat } from 'node:fs/promises';
15
+ import { join, resolve } from 'node:path';
16
+ import { describe, expect, it } from 'vitest';
17
+
18
+ const ROOT = resolve(__dirname, '../../../../..');
19
+
20
+ // ── helpers ────────────────────────────────────────────────────────────────
21
+
22
+ async function walkTs(dir: string): Promise<string[]> {
23
+ const files: string[] = [];
24
+ let entries: Awaited<ReturnType<typeof readdir>>;
25
+ try {
26
+ entries = await readdir(dir, { withFileTypes: true });
27
+ } catch {
28
+ return files;
29
+ }
30
+ for (const e of entries) {
31
+ const full = join(dir, e.name);
32
+ if (e.isDirectory() && e.name !== 'node_modules' && e.name !== '__tests__') {
33
+ files.push(...(await walkTs(full)));
34
+ } else if (e.isFile() && /\.(ts|tsx)$/.test(e.name) && !e.name.endsWith('.d.ts')) {
35
+ files.push(full);
36
+ }
37
+ }
38
+ return files;
39
+ }
40
+
41
+ async function importsIn(file: string): Promise<string[]> {
42
+ const src = await readFile(file, 'utf8');
43
+ const matches = [...src.matchAll(/from\s+['"]([^'"]+)['"]/g)];
44
+ return matches.map((m) => m[1] ?? '');
45
+ }
46
+
47
+ // ── Rule 1: client boundary ────────────────────────────────────────────────
48
+
49
+ describe('client boundary — no @nestjs/* in browser-safe exports', () => {
50
+ const CLIENT_ROOTS = [
51
+ join(ROOT, 'libs/shared/src/client.ts'),
52
+ join(ROOT, 'libs/shared/src/abilities'),
53
+ join(ROOT, 'libs/shared/src/types'),
54
+ join(ROOT, 'libs/template-shared/src'),
55
+ ];
56
+
57
+ it('no @nestjs/* import found in client-side source files', async () => {
58
+ const violations: string[] = [];
59
+
60
+ for (const root of CLIENT_ROOTS) {
61
+ const s = await stat(root).catch(() => null);
62
+ if (!s) continue;
63
+ const files = s.isFile() ? [root] : await walkTs(root);
64
+
65
+ for (const file of files) {
66
+ const imports = await importsIn(file);
67
+ for (const imp of imports) {
68
+ if (/^(@nestjs\/|firebase-admin|bullmq|ioredis)/.test(imp)) {
69
+ violations.push(`${file.replace(ROOT + '/', '')} → ${imp}`);
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ expect(
76
+ violations,
77
+ `Server-only imports found in client code:\n${violations.join('\n')}`,
78
+ ).toEqual([]);
79
+ });
80
+ });
81
+
82
+ // ── Rule 2: server boundary ────────────────────────────────────────────────
83
+
84
+ describe('server boundary — no browser-only packages in NestJS modules', () => {
85
+ const SERVER_ROOTS = [
86
+ join(ROOT, 'apps/microservices'),
87
+ join(ROOT, 'libs/auth-client/src'),
88
+ join(ROOT, 'libs/upload-client/src'),
89
+ join(ROOT, 'libs/notes-client/src'),
90
+ join(ROOT, 'libs/payment-client/src'),
91
+ join(ROOT, 'libs/jobs-client/src'),
92
+ ];
93
+
94
+ // Regex matches the start of a bare import specifier
95
+ const BROWSER_ONLY =
96
+ /^(react(-dom|-router)?|@tanstack\/react-|antd|@mui\/|@radix-ui\/|lucide-react|sonner|@ant-design\/)/;
97
+
98
+ it('no browser-only import found in server-side source files', async () => {
99
+ const violations: string[] = [];
100
+
101
+ for (const root of SERVER_ROOTS) {
102
+ const s = await stat(root).catch(() => null);
103
+ if (!s) continue;
104
+ const files = s.isFile() ? [root] : await walkTs(root);
105
+
106
+ for (const file of files) {
107
+ const imports = await importsIn(file);
108
+ for (const imp of imports) {
109
+ if (BROWSER_ONLY.test(imp)) {
110
+ violations.push(`${file.replace(ROOT + '/', '')} → ${imp}`);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ expect(
117
+ violations,
118
+ `Browser-only imports found in server code:\n${violations.join('\n')}`,
119
+ ).toEqual([]);
120
+ });
121
+ });
@@ -0,0 +1,5 @@
1
+ // Browser-safe subset of @icore/shared.
2
+ // Import from '@icore/shared/client' in client-side code to avoid pulling
3
+ // in NestJS / Node.js-only modules (transport, strategies, contracts).
4
+ export * from './abilities';
5
+ export * from './types';
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import type {
3
2
  AuthSession,
4
3
  AuthStrategy,
@@ -33,7 +32,7 @@ export class FakeAuthStrategy implements AuthStrategy {
33
32
 
34
33
  async signUp(email: string, password: string): Promise<AuthSession> {
35
34
  if (this.users.has(email)) throw new Error('user_exists');
36
- const user: StoredUser = { id: randomUUID(), email, password };
35
+ const user: StoredUser = { id: globalThis.crypto.randomUUID(), email, password };
37
36
  this.users.set(email, user);
38
37
  return this.issueSession(user);
39
38
  }
@@ -72,10 +71,10 @@ export class FakeAuthStrategy implements AuthStrategy {
72
71
  async sendMagicLink(req: MagicLinkRequest): Promise<void> {
73
72
  let user = this.users.get(req.email);
74
73
  if (!user) {
75
- user = { id: randomUUID(), email: req.email, password: '' };
74
+ user = { id: globalThis.crypto.randomUUID(), email: req.email, password: '' };
76
75
  this.users.set(req.email, user);
77
76
  }
78
- const token = randomUUID();
77
+ const token = globalThis.crypto.randomUUID();
79
78
  this.magicLinkTokens.set(token, user.id);
80
79
  this.magicLinkByEmail.set(req.email, token);
81
80
  }
@@ -95,7 +94,7 @@ export class FakeAuthStrategy implements AuthStrategy {
95
94
  }
96
95
 
97
96
  async startOAuth(provider: OAuthProvider, callbackUrl: string): Promise<OAuthStartResult> {
98
- const state = randomUUID();
97
+ const state = globalThis.crypto.randomUUID();
99
98
  this.lastOAuthState = state;
100
99
  const url = new URL(`https://fake-${provider}.example.com/authorize`);
101
100
  url.searchParams.set('redirect_uri', callbackUrl);
@@ -111,7 +110,7 @@ export class FakeAuthStrategy implements AuthStrategy {
111
110
  this.oauthCodes.delete(code);
112
111
  let user = this.users.get(pending.email);
113
112
  if (!user) {
114
- user = { id: randomUUID(), email: pending.email, password: '' };
113
+ user = { id: globalThis.crypto.randomUUID(), email: pending.email, password: '' };
115
114
  this.users.set(pending.email, user);
116
115
  }
117
116
  return this.issueSession(user);
@@ -123,7 +122,7 @@ export class FakeAuthStrategy implements AuthStrategy {
123
122
  getLastOAuthChallenge(provider: OAuthProvider, email: string): { code: string; state: string } {
124
123
  if (!this.lastOAuthState) throw new Error('no startOAuth called yet');
125
124
  const state = this.lastOAuthState;
126
- const code = randomUUID();
125
+ const code = globalThis.crypto.randomUUID();
127
126
  this.oauthStates.set(state, { provider, email });
128
127
  this.oauthCodes.set(code, state);
129
128
  return { code, state };
@@ -137,8 +136,8 @@ export class FakeAuthStrategy implements AuthStrategy {
137
136
  }
138
137
 
139
138
  private issueSession(user: StoredUser): AuthSession {
140
- const accessToken = randomUUID();
141
- const refreshToken = randomUUID();
139
+ const accessToken = globalThis.crypto.randomUUID();
140
+ const refreshToken = globalThis.crypto.randomUUID();
142
141
  this.tokensToUid.set(accessToken, user.id);
143
142
  this.refreshToUid.set(refreshToken, user.id);
144
143
  return {
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  import type { FileInput, StorageRef, StorageStrategy } from '../storage';
3
2
 
4
3
  interface StoredFile {
@@ -13,7 +12,7 @@ export class FakeStorageStrategy implements StorageStrategy {
13
12
  private readonly files = new Map<string, StoredFile>();
14
13
 
15
14
  async upload(userId: string, file: FileInput): Promise<StorageRef> {
16
- const path = `${userId}/${randomUUID()}-${file.filename}`;
15
+ const path = `${userId}/${globalThis.crypto.randomUUID()}-${file.filename}`;
17
16
  const ref: StorageRef = { bucket: this.bucket, path };
18
17
  this.files.set(this.key(ref), {
19
18
  ownerId: userId,
@@ -1,7 +1,7 @@
1
1
  import { AbilityProvider as CaslAbilityProvider, Can } from '@casl/react';
2
2
  import type { ReactNode } from 'react';
3
3
  import { useMemo } from 'react';
4
- import { defineAbilitiesFor, type AppAbility } from '@icore/shared';
4
+ import { defineAbilitiesFor, type AppAbility } from '@icore/shared/client';
5
5
  import { useAuthStore } from '../stores/auth.store.js';
6
6
 
7
7
  export { Can };
@@ -0,0 +1,7 @@
1
+ # vite-plugins
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build vite-plugins` to build the library.
@@ -0,0 +1,19 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
12
+ },
13
+ ],
14
+ },
15
+ languageOptions: {
16
+ parser: await import('jsonc-eslint-parser'),
17
+ },
18
+ },
19
+ ];
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@icore/vite-plugins",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./src/index.mjs",
7
+ "types": "./src/index.d.mts",
8
+ "peerDependencies": {
9
+ "tslib": "^2.3.0",
10
+ "vite": "^8.0.0",
11
+ "vitest": "~4.1.0"
12
+ },
13
+ "peerDependenciesMeta": {
14
+ "vite": {
15
+ "optional": false
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "vite-plugins",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/vite-plugins/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/js:tsc",
10
+ "outputs": ["{options.outputPath}"],
11
+ "options": {
12
+ "outputPath": "dist/libs/vite-plugins",
13
+ "main": "libs/vite-plugins/src/index.ts",
14
+ "tsConfig": "libs/vite-plugins/tsconfig.lib.json",
15
+ "assets": ["libs/vite-plugins/*.md"]
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { UserConfig } from 'vitest/config';
3
+
4
+ export declare function noServerModulesPlugin(): Plugin;
5
+
6
+ export declare function injectAppVersionPlugin(pkg: { version: string }): Plugin;
7
+
8
+ export declare function commonDefines(pkg: {
9
+ version: string;
10
+ dependencies?: Record<string, string>;
11
+ devDependencies?: Record<string, string>;
12
+ }): Record<string, string>;
13
+
14
+ export declare function commonManualChunks(
15
+ uiChunkFn?: (id: string) => string | undefined,
16
+ ): (id: string) => string | undefined;
17
+
18
+ export declare function commonTestConfig(
19
+ name: string,
20
+ coverageDir: string,
21
+ ): NonNullable<UserConfig['test']>;
@@ -0,0 +1,106 @@
1
+ // @icore/vite-plugins — shared Vite plugin helpers for iCore client templates.
2
+ // Plain ESM (no TypeScript syntax) so vite.config.mts can import it directly.
3
+
4
+ const SERVER_ONLY_RE = /^(@nestjs\/|firebase-admin$|bullmq$|ioredis$)/;
5
+
6
+ /**
7
+ * Fails the Vite build if server-only modules are imported in client code.
8
+ * @returns {import('vite').Plugin}
9
+ */
10
+ export function noServerModulesPlugin() {
11
+ return {
12
+ name: 'no-server-modules',
13
+ enforce: 'pre',
14
+ resolveId(id, importer) {
15
+ if (SERVER_ONLY_RE.test(id)) {
16
+ throw new Error(
17
+ `Server-only module "${id}" imported in client code` +
18
+ (importer ? ` (from ${importer})` : '') +
19
+ `. Use @icore/shared/client instead of @icore/shared for browser-safe imports.`,
20
+ );
21
+ }
22
+ },
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Replaces %APP_VERSION% in index.html with the root package.json version.
28
+ * @param {{ version: string }} pkg
29
+ * @returns {import('vite').Plugin}
30
+ */
31
+ export function injectAppVersionPlugin(pkg) {
32
+ return {
33
+ name: 'inject-app-version-meta',
34
+ transformIndexHtml(html) {
35
+ return html.replace('%APP_VERSION%', pkg.version);
36
+ },
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Returns Vite `define` entries shared by all iCore client templates.
42
+ * Each template spreads this and adds its own UI-lib-specific entry.
43
+ *
44
+ * @param {{ version: string, dependencies?: Record<string,string>, devDependencies?: Record<string,string> }} pkg
45
+ * @returns {Record<string, string>}
46
+ */
47
+ export function commonDefines(pkg) {
48
+ const dep = (name) =>
49
+ JSON.stringify(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? '?');
50
+ return {
51
+ 'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version),
52
+ 'import.meta.env.VITE_DEP_REACT': dep('react'),
53
+ 'import.meta.env.VITE_DEP_VITE': dep('vite'),
54
+ 'import.meta.env.VITE_DEP_TANSTACK_ROUTER': dep('@tanstack/react-router'),
55
+ 'import.meta.env.VITE_DEP_TANSTACK_QUERY': dep('@tanstack/react-query'),
56
+ 'import.meta.env.VITE_DEP_ZUSTAND': dep('zustand'),
57
+ 'import.meta.env.VITE_DEP_CASL': dep('@casl/ability'),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Returns a manualChunks function with the common vendor splits pre-applied.
63
+ * Pass a `uiChunkFn` to add UI-library-specific splits before the fallback.
64
+ *
65
+ * @param {(id: string) => string | undefined} [uiChunkFn]
66
+ * @returns {(id: string) => string | undefined}
67
+ */
68
+ export function commonManualChunks(uiChunkFn) {
69
+ return (id) => {
70
+ if (!id.includes('node_modules')) return undefined;
71
+ if (id.includes('/react/') || id.includes('/react-dom/') || id.includes('scheduler'))
72
+ return 'vendor-react';
73
+ if (id.includes('@tanstack')) return 'vendor-tanstack';
74
+ if (id.includes('i18next') || id.includes('react-i18next')) return 'vendor-i18n';
75
+ if (id.includes('@casl')) return 'vendor-casl';
76
+ if (uiChunkFn) {
77
+ const chunk = uiChunkFn(id);
78
+ if (chunk) return chunk;
79
+ }
80
+ if (id.includes('zustand')) return 'vendor-state';
81
+ if (id.includes('@idevconn')) return 'vendor-idevconn';
82
+ return 'vendor-core';
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Returns a vitest `test` configuration block shared by all iCore client templates.
88
+ *
89
+ * @param {string} name - project name (e.g. 'client-shadcn')
90
+ * @param {string} coverageDir - relative path to coverage output dir
91
+ * @returns {import('vitest/config').UserConfig['test']}
92
+ */
93
+ export function commonTestConfig(name, coverageDir) {
94
+ return {
95
+ name,
96
+ watch: false,
97
+ globals: true,
98
+ environment: 'jsdom',
99
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
100
+ reporters: ['default'],
101
+ coverage: {
102
+ reportsDirectory: coverageDir,
103
+ provider: 'v8',
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "forceConsistentCasingInFileNames": true,
6
+ "strict": true,
7
+ "importHelpers": true,
8
+ "noImplicitOverride": true,
9
+ "noImplicitReturns": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "noPropertyAccessFromIndexSignature": true
12
+ },
13
+ "files": [],
14
+ "include": [],
15
+ "references": [
16
+ {
17
+ "path": "./tsconfig.lib.json"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }