@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.
- package/README.md +39 -494
- package/dist/cli/init.d.mts +1 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +2685 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/init.mjs +2659 -0
- package/dist/cli/init.mjs.map +1 -0
- package/dist/{index-Bpzo1KfR.d.mts → index-BiaVDe9J.d.mts} +0 -1
- package/dist/{index-Bpzo1KfR.d.ts → index-BiaVDe9J.d.ts} +0 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -15
- package/dist/index.mjs.map +1 -1
- package/dist/render/index.d.mts +1 -1
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +7 -15
- package/dist/render/index.js.map +1 -1
- package/dist/render/index.mjs +7 -15
- package/dist/render/index.mjs.map +1 -1
- package/dist/templates/entry-client.tsx +41 -12
- package/dist/templates/entry-server.tsx +1 -0
- package/package.json +8 -7
- package/src/cli/init.ts +313 -0
- package/src/templates/entry-client.tsx +41 -12
- package/src/templates/entry-server.tsx +1 -0
package/src/cli/init.ts
ADDED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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) {
|