@nestjs-ssr/react 0.1.6 → 0.1.8

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.
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
4
+ import { join, resolve, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { consola } from 'consola';
7
+ import { defineCommand, runMain } from 'citty';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ const main = defineCommand({
13
+ meta: {
14
+ name: 'nestjs-ssr',
15
+ description: 'Initialize @nestjs-ssr/react in your NestJS project',
16
+ version: '0.1.6',
17
+ },
18
+ args: {
19
+ force: {
20
+ type: 'boolean',
21
+ description: 'Overwrite existing files',
22
+ alias: 'f',
23
+ },
24
+ views: {
25
+ type: 'string',
26
+ description: 'Views directory path',
27
+ default: 'src/views',
28
+ },
29
+ },
30
+ async run({ args }) {
31
+ const cwd = process.cwd();
32
+ const viewsDir = args.views;
33
+
34
+ consola.box('@nestjs-ssr/react initialization');
35
+ consola.start('Setting up your NestJS SSR React project...\n');
36
+
37
+ // Find template files - check both src/ (dev) and dist/ (production) locations
38
+ const templateLocations = [
39
+ resolve(__dirname, '../../src/templates'), // Development (ts-node/tsx)
40
+ resolve(__dirname, '../templates'), // Built package (dist/cli -> dist/templates)
41
+ ];
42
+ const templateDir = templateLocations.find(loc => existsSync(join(loc, 'entry-client.tsx')));
43
+
44
+ if (!templateDir) {
45
+ consola.error('Failed to locate template files');
46
+ consola.info('Searched:', templateLocations);
47
+ process.exit(1);
48
+ }
49
+
50
+ // Find global.d.ts - check both src/ (dev) and package root (production)
51
+ const globalTypesLocations = [
52
+ resolve(__dirname, '../../src/global.d.ts'), // Development
53
+ resolve(__dirname, '../global.d.ts'), // Built (dist/cli -> dist/../src/global.d.ts)
54
+ ];
55
+ const globalTypesSrc = globalTypesLocations.find(loc => existsSync(loc));
56
+
57
+ if (!globalTypesSrc) {
58
+ consola.error('Failed to locate global.d.ts');
59
+ consola.info('Searched:', globalTypesLocations);
60
+ process.exit(1);
61
+ }
62
+
63
+ // Check that tsconfig.json exists - we don't create it
64
+ const tsconfigPath = join(cwd, 'tsconfig.json');
65
+ if (!existsSync(tsconfigPath)) {
66
+ consola.error('No tsconfig.json found in project root');
67
+ consola.info('Please create a tsconfig.json file first');
68
+ process.exit(1);
69
+ }
70
+
71
+ // 1. Copy entry-client.tsx to views directory
72
+ consola.start('Creating entry-client.tsx...');
73
+ const entryClientSrc = join(templateDir, 'entry-client.tsx');
74
+ const entryClientDest = join(cwd, viewsDir, 'entry-client.tsx');
75
+
76
+ // Create views directory if it doesn't exist
77
+ mkdirSync(join(cwd, viewsDir), { recursive: true });
78
+
79
+ if (existsSync(entryClientDest) && !args.force) {
80
+ consola.warn(`${viewsDir}/entry-client.tsx already exists (use --force to overwrite)`);
81
+ } else {
82
+ copyFileSync(entryClientSrc, entryClientDest);
83
+ consola.success(`Created ${viewsDir}/entry-client.tsx`);
84
+ }
85
+
86
+ // 2. Copy entry-server.tsx to views directory
87
+ consola.start('Creating entry-server.tsx...');
88
+ const entryServerSrc = join(templateDir, 'entry-server.tsx');
89
+ const entryServerDest = join(cwd, viewsDir, 'entry-server.tsx');
90
+
91
+ if (existsSync(entryServerDest) && !args.force) {
92
+ consola.warn(`${viewsDir}/entry-server.tsx already exists (use --force to overwrite)`);
93
+ } else {
94
+ copyFileSync(entryServerSrc, entryServerDest);
95
+ consola.success(`Created ${viewsDir}/entry-server.tsx`);
96
+ }
97
+
98
+ // 2. Copy global.d.ts
99
+ consola.start('Creating global.d.ts...');
100
+ const globalTypesDest = join(cwd, 'src/global.d.ts');
101
+
102
+ if (existsSync(globalTypesDest) && !args.force) {
103
+ consola.warn('src/global.d.ts already exists (use --force to overwrite)');
104
+ } else {
105
+ copyFileSync(globalTypesSrc, globalTypesDest);
106
+ consola.success('Created src/global.d.ts');
107
+ }
108
+
109
+ // 4. Update/create vite.config.js
110
+ consola.start('Configuring vite.config.js...');
111
+ const viteConfigPath = join(cwd, 'vite.config.js');
112
+ const viteConfigTs = join(cwd, 'vite.config.ts');
113
+ const useTypeScript = existsSync(viteConfigTs);
114
+ const configPath = useTypeScript ? viteConfigTs : viteConfigPath;
115
+
116
+ if (existsSync(configPath)) {
117
+ consola.warn(`${useTypeScript ? 'vite.config.ts' : 'vite.config.js'} already exists`);
118
+ consola.info('Please manually add to your Vite config:');
119
+ consola.log(' import { resolve } from \'path\';');
120
+ consola.log(' build: {');
121
+ consola.log(' rollupOptions: {');
122
+ consola.log(` input: { client: resolve(__dirname, '${viewsDir}/entry-client.tsx') }`);
123
+ consola.log(' }');
124
+ consola.log(' }');
125
+ } else {
126
+ const viteConfig = `import { defineConfig } from 'vite';
127
+ import react from '@vitejs/plugin-react';
128
+ import { resolve } from 'path';
129
+
130
+ export default defineConfig({
131
+ plugins: [react()],
132
+ resolve: {
133
+ alias: {
134
+ '@': resolve(__dirname, 'src'),
135
+ },
136
+ },
137
+ server: {
138
+ port: 5173,
139
+ strictPort: true,
140
+ hmr: { port: 5173 },
141
+ },
142
+ build: {
143
+ outDir: 'dist/client',
144
+ manifest: true,
145
+ rollupOptions: {
146
+ input: {
147
+ client: resolve(__dirname, '${viewsDir}/entry-client.tsx'),
148
+ },
149
+ },
150
+ },
151
+ });
152
+ `;
153
+ writeFileSync(viteConfigPath, viteConfig);
154
+ consola.success('Created vite.config.js');
155
+ }
156
+
157
+ // 5. Update tsconfig.json
158
+ consola.start('Configuring tsconfig.json...');
159
+ try {
160
+ const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf-8'));
161
+
162
+ let updated = false;
163
+
164
+ if (!tsconfig.compilerOptions) {
165
+ tsconfig.compilerOptions = {};
166
+ }
167
+
168
+ // Ensure jsx is set
169
+ if (tsconfig.compilerOptions.jsx !== 'react-jsx') {
170
+ tsconfig.compilerOptions.jsx = 'react-jsx';
171
+ updated = true;
172
+ }
173
+
174
+ // Ensure paths includes @ alias
175
+ if (!tsconfig.compilerOptions.paths) {
176
+ tsconfig.compilerOptions.paths = {};
177
+ }
178
+ if (!tsconfig.compilerOptions.paths['@/*']) {
179
+ tsconfig.compilerOptions.paths['@/*'] = ['./src/*'];
180
+ updated = true;
181
+ }
182
+
183
+ // Ensure types includes vite/client
184
+ if (!tsconfig.compilerOptions.types) {
185
+ tsconfig.compilerOptions.types = [];
186
+ }
187
+ if (!tsconfig.compilerOptions.types.includes('vite/client')) {
188
+ tsconfig.compilerOptions.types.push('vite/client');
189
+ updated = true;
190
+ }
191
+
192
+ if (updated) {
193
+ writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
194
+ consola.success('Updated tsconfig.json');
195
+ } else {
196
+ consola.info('tsconfig.json already configured');
197
+ }
198
+ } catch (error) {
199
+ consola.error('Failed to update tsconfig.json:', error);
200
+ }
201
+
202
+ // 6. Setup build scripts
203
+ consola.start('Configuring build scripts...');
204
+ const packageJsonPath = join(cwd, 'package.json');
205
+
206
+ if (!existsSync(packageJsonPath)) {
207
+ consola.warn('No package.json found, skipping build script setup');
208
+ } else {
209
+ try {
210
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
211
+
212
+ if (!packageJson.scripts) {
213
+ packageJson.scripts = {};
214
+ }
215
+
216
+ const existingBuild = packageJson.scripts.build;
217
+ const defaultNestBuild = 'nest build';
218
+ const ssrBuildPrefix = 'vite build && ';
219
+
220
+ let shouldUpdate = false;
221
+ let newBuildScript = '';
222
+
223
+ if (!existingBuild) {
224
+ // No build script exists, create one
225
+ newBuildScript = `${ssrBuildPrefix}${defaultNestBuild}`;
226
+ shouldUpdate = true;
227
+ } else if (existingBuild.includes('vite build')) {
228
+ // Already has vite build
229
+ consola.info('Build script already includes vite build');
230
+ } else if (existingBuild === defaultNestBuild) {
231
+ // Default nest build, prepend vite build
232
+ newBuildScript = `${ssrBuildPrefix}${existingBuild}`;
233
+ shouldUpdate = true;
234
+ } else {
235
+ // Custom build script, ask user
236
+ consola.warn(`Found custom build script: "${existingBuild}"`);
237
+ consola.info('SSR requires running "vite build" before your build command');
238
+ consola.info(`Recommended: ${ssrBuildPrefix}${existingBuild}`);
239
+ consola.info('Please manually update your build script in package.json');
240
+ }
241
+
242
+ if (shouldUpdate) {
243
+ packageJson.scripts.build = newBuildScript;
244
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
245
+ consola.success(`Updated build script to: "${newBuildScript}"`);
246
+ }
247
+ } catch (error) {
248
+ consola.error('Failed to update package.json:', error);
249
+ }
250
+ }
251
+
252
+ consola.success('\nInitialization complete!');
253
+ consola.box('Next steps');
254
+ consola.info(`1. Create your first view component in ${viewsDir}/`);
255
+ consola.info('2. Render it from a NestJS controller using render.render()');
256
+ consola.info('3. Run your dev server with: pnpm start:dev');
257
+ },
258
+ });
259
+
260
+ runMain(main);
@@ -1,4 +1,4 @@
1
- import { StrictMode } from 'react';
1
+ import React, { StrictMode } from 'react';
2
2
  import { hydrateRoot } from 'react-dom/client';
3
3
 
4
4
  const componentName = window.__COMPONENT_NAME__;
@@ -9,19 +9,69 @@ const renderContext = window.__CONTEXT__ || {};
9
9
  // @ts-ignore - Vite-specific API
10
10
  const modules: Record<string, { default: React.ComponentType<any> }> = import.meta.glob('@/views/**/*.tsx', { eager: true });
11
11
 
12
- // Find the component by matching its display name or function name
12
+ // Build a map of components with their metadata
13
+ // Filter out entry files and modules without default exports
14
+ const componentMap = Object.entries(modules)
15
+ .filter(([path, module]) => {
16
+ // Skip entry-client and entry-server files
17
+ const filename = path.split('/').pop();
18
+ if (filename === 'entry-client.tsx' || filename === 'entry-server.tsx') {
19
+ return false;
20
+ }
21
+ // Only include modules with a default export
22
+ return module.default !== undefined;
23
+ })
24
+ .map(([path, module]) => {
25
+ const component = module.default;
26
+ const name = component.displayName || component.name;
27
+ const filename = path.split('/').pop()?.replace('.tsx', '');
28
+ const normalizedFilename = filename ? filename.charAt(0).toUpperCase() + filename.slice(1) : undefined;
29
+
30
+ return { path, component, name, filename, normalizedFilename };
31
+ });
32
+
33
+ // Find the component by matching in this order:
34
+ // 1. Exact match by displayName or function name
35
+ // 2. Match by normalized filename (e.g., "home.tsx" -> "Home")
36
+ // 3. For minified names (default_N), match the Nth component with name "default"
37
+ // 4. If only one component exists, use it (regardless of name)
13
38
  let ViewComponent: React.ComponentType<any> | undefined;
14
- for (const module of Object.values(modules)) {
15
- const component = module.default;
16
- const name = component.displayName || component.name;
17
- if (name === componentName) {
18
- ViewComponent = component;
19
- break;
39
+
40
+ // Try exact name match first
41
+ ViewComponent = componentMap.find(
42
+ (c) => c.name === componentName || c.normalizedFilename === componentName || c.filename === componentName.toLowerCase()
43
+ )?.component;
44
+
45
+ // If no match found and component name looks like a generic/minified name (default, default_1, etc.)
46
+ if (!ViewComponent && /^default(_\d+)?$/.test(componentName)) {
47
+ // If there's only one component, use it regardless of name
48
+ if (componentMap.length === 1) {
49
+ ViewComponent = componentMap[0].component;
50
+ } else {
51
+ // Handle minified anonymous functions: default_1, default_2, etc.
52
+ // Extract the index from the name (default_1 -> 1, default_2 -> 2, default -> 0)
53
+ const match = componentName.match(/^default_(\d+)$/);
54
+ const index = match ? parseInt(match[1], 10) - 1 : 0;
55
+
56
+ // Get all components with name "default" (anonymous functions), sorted by path for consistency
57
+ const defaultComponents = componentMap
58
+ .filter(c => c.name === 'default')
59
+ .sort((a, b) => a.path.localeCompare(b.path));
60
+
61
+ // Try to match by index
62
+ if (defaultComponents[index]) {
63
+ ViewComponent = defaultComponents[index].component;
64
+ }
20
65
  }
21
66
  }
22
67
 
23
68
  if (!ViewComponent) {
24
- throw new Error(`Component "${componentName}" not found in views directory. Available components: ${Object.values(modules).map(m => m.default.displayName || m.default.name).join(', ')}`);
69
+ const availableComponents = Object.entries(modules).map(([path, m]) => {
70
+ const filename = path.split('/').pop()?.replace('.tsx', '');
71
+ const name = m.default.displayName || m.default.name;
72
+ return `${filename} (${name})`;
73
+ }).join(', ');
74
+ throw new Error(`Component "${componentName}" not found in views directory. Available: ${availableComponents}`);
25
75
  }
26
76
 
27
77
  hydrateRoot(
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { renderToString } from 'react-dom/server';
2
3
 
3
4
  export function renderComponent(