@nestjs-ssr/react 0.1.7 → 0.1.9

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