@nexus_js/cli 0.7.2 → 0.7.4
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/dist/bin.js +220 -21
- package/dist/bin.js.map +1 -1
- package/dist/config.d.ts +56 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/create.d.ts +5 -3
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +1161 -59
- package/dist/create.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/load-app-config.d.ts +3 -0
- package/dist/load-app-config.d.ts.map +1 -0
- package/dist/load-app-config.js +24 -0
- package/dist/load-app-config.js.map +1 -0
- package/dist/security-report.d.ts +16 -0
- package/dist/security-report.d.ts.map +1 -0
- package/dist/security-report.js +114 -0
- package/dist/security-report.js.map +1 -0
- package/dist/studio.d.ts +39 -2
- package/dist/studio.d.ts.map +1 -1
- package/dist/studio.js +270 -3
- package/dist/studio.js.map +1 -1
- package/package.json +7 -5
package/dist/create.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* create-nexus — scaffolding CLI for new Nexus projects.
|
|
4
4
|
* Usage: npm create @nexus_js/nexus@latest my-app
|
|
5
|
-
* npx @nexus_js/create-nexus my-app
|
|
6
|
-
* npm exec --package=@nexus_js/cli -- create-nexus my-app
|
|
5
|
+
* npx @nexus_js/create-nexus my-app [--template minimal|full]
|
|
6
|
+
* npm exec --package=@nexus_js/cli -- create-nexus my-app -t minimal
|
|
7
|
+
* create-nexus --yes (defaults, no prompts)
|
|
7
8
|
*/
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
10
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
10
11
|
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
13
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
14
|
import { fileURLToPath } from 'node:url';
|
|
12
15
|
/** Same version as the published @nexus_js/cli (peer @nexus_js/* packages stay in sync). */
|
|
13
16
|
function getPublishedCliVersion() {
|
|
@@ -17,6 +20,7 @@ function getPublishedCliVersion() {
|
|
|
17
20
|
}
|
|
18
21
|
const CYAN = '\x1b[36m';
|
|
19
22
|
const GREEN = '\x1b[32m';
|
|
23
|
+
const RED = '\x1b[31m';
|
|
20
24
|
const YELLOW = '\x1b[33m';
|
|
21
25
|
const RESET = '\x1b[0m';
|
|
22
26
|
const BOLD = '\x1b[1m';
|
|
@@ -27,48 +31,213 @@ const BANNER = `
|
|
|
27
31
|
The Definitive Full-Stack Framework
|
|
28
32
|
Islands × Runes × Server Actions
|
|
29
33
|
`;
|
|
34
|
+
const TEMPLATE_HINT = {
|
|
35
|
+
minimal: 'Minimal — one landing page, no i18n (build almost from scratch)',
|
|
36
|
+
full: 'Full — i18n (en/es/pt), islands + blog examples',
|
|
37
|
+
};
|
|
38
|
+
/** Official Nexus mark — keep in sync with docs/assets/nexus-logo.svg */
|
|
39
|
+
const NEXUS_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none" aria-hidden="true">
|
|
40
|
+
<defs>
|
|
41
|
+
<linearGradient id="nxOrbitGrad" x1="40" y1="36" x2="216" y2="220" gradientUnits="userSpaceOnUse">
|
|
42
|
+
<stop offset="0%" stop-color="#2DD4BF"/>
|
|
43
|
+
<stop offset="50%" stop-color="#7C3AED"/>
|
|
44
|
+
<stop offset="100%" stop-color="#F472B6"/>
|
|
45
|
+
</linearGradient>
|
|
46
|
+
<radialGradient id="nxMarkBg" cx="32%" cy="28%" r="85%">
|
|
47
|
+
<stop offset="0%" stop-color="#1e293b"/>
|
|
48
|
+
<stop offset="100%" stop-color="#020617"/>
|
|
49
|
+
</radialGradient>
|
|
50
|
+
<radialGradient id="nxNucleusFill" cx="50%" cy="50%" r="50%">
|
|
51
|
+
<stop offset="0%" stop-color="#020617"/>
|
|
52
|
+
<stop offset="72%" stop-color="#0f172a"/>
|
|
53
|
+
<stop offset="100%" stop-color="#1e1b4a"/>
|
|
54
|
+
</radialGradient>
|
|
55
|
+
</defs>
|
|
56
|
+
<rect x="20" y="20" width="216" height="216" rx="60" ry="60" fill="url(#nxMarkBg)"/>
|
|
57
|
+
<rect x="20" y="20" width="216" height="216" rx="60" ry="60" stroke="#475569" stroke-width="1" fill="none" opacity="0.45"/>
|
|
58
|
+
<circle cx="128" cy="86" r="46" fill="url(#nxOrbitGrad)" opacity="0.88"/>
|
|
59
|
+
<circle cx="172" cy="152" r="40" fill="url(#nxOrbitGrad)" opacity="0.78"/>
|
|
60
|
+
<circle cx="84" cy="152" r="34" fill="url(#nxOrbitGrad)" opacity="0.72"/>
|
|
61
|
+
<circle cx="128" cy="128" r="54" fill="url(#nxNucleusFill)"/>
|
|
62
|
+
<circle cx="128" cy="128" r="54" fill="none" stroke="url(#nxOrbitGrad)" stroke-width="2" opacity="0.55"/>
|
|
63
|
+
<circle cx="128" cy="128" r="48" fill="none" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
|
64
|
+
<path d="M 108 90 L 108 166 M 108 90 L 148 166 M 148 90 L 148 166" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
65
|
+
</svg>`;
|
|
66
|
+
const TEMPLATE_DETAIL = {
|
|
67
|
+
minimal: 'Single +page.nx + layout, no i18n, no /islands or /blog examples',
|
|
68
|
+
full: 'i18n (en/es/pt), islands guide, blog — like the my-nexus-app reference',
|
|
69
|
+
};
|
|
70
|
+
function parseCreateArgs(argv) {
|
|
71
|
+
let projectNamePositional;
|
|
72
|
+
let template;
|
|
73
|
+
let help = false;
|
|
74
|
+
let useDefaults = false;
|
|
75
|
+
for (let i = 2; i < argv.length; i++) {
|
|
76
|
+
const a = argv[i];
|
|
77
|
+
if (a === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (a === '--help' || a === '-h') {
|
|
80
|
+
help = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (a === '--yes' || a === '-y' || a === '--defaults') {
|
|
84
|
+
useDefaults = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (a === '--template' || a === '-t') {
|
|
88
|
+
const v = argv[++i];
|
|
89
|
+
if (v === 'minimal' || v === 'full')
|
|
90
|
+
template = v;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (a.startsWith('-'))
|
|
94
|
+
continue;
|
|
95
|
+
if (!projectNamePositional)
|
|
96
|
+
projectNamePositional = a;
|
|
97
|
+
}
|
|
98
|
+
return { projectNamePositional, template, help, useDefaults };
|
|
99
|
+
}
|
|
100
|
+
/** Safe folder name for ./name (no path segments). */
|
|
101
|
+
function normalizeProjectName(raw, fallback) {
|
|
102
|
+
const t = raw.trim().replace(/[/\\]+/g, '').replace(/^\.+/, '');
|
|
103
|
+
if (!t || t.includes('..'))
|
|
104
|
+
return fallback;
|
|
105
|
+
const safe = t.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^-+|-+$/g, '');
|
|
106
|
+
return safe.length > 0 ? safe.slice(0, 214) : fallback;
|
|
107
|
+
}
|
|
108
|
+
async function runInteractiveWizard(opts) {
|
|
109
|
+
const rl = createInterface({ input, output });
|
|
110
|
+
try {
|
|
111
|
+
const defaultName = normalizeProjectName(opts.nameHint ?? '', 'my-nexus-app');
|
|
112
|
+
console.log(`\n ${BOLD}◇ Configure your Nexus project${RESET}\n`);
|
|
113
|
+
const nameAns = await rl.question(` ${CYAN}?${RESET} Project directory ${DIM}./${defaultName}${RESET} ${DIM}(press Enter for default)${RESET}\n` +
|
|
114
|
+
` ${DIM}│${RESET}\n ${DIM}└${RESET} `);
|
|
115
|
+
const projectName = normalizeProjectName(nameAns.length > 0 ? nameAns : defaultName, defaultName);
|
|
116
|
+
let template;
|
|
117
|
+
if (opts.templateFromFlag) {
|
|
118
|
+
template = opts.templateFromFlag;
|
|
119
|
+
console.log(`\n ${DIM}Starter:${RESET} ${BOLD}${template}${RESET} ${DIM}(--template)${RESET}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.log(`\n ${BOLD}?${RESET} Which starter do you want?\n`);
|
|
123
|
+
console.log(` ${DIM}❯${RESET} ${DIM}1${RESET} ${TEMPLATE_HINT.minimal}`);
|
|
124
|
+
console.log(` ${DIM}2${RESET} ${TEMPLATE_HINT.full}\n`);
|
|
125
|
+
console.log(` ${DIM}${TEMPLATE_DETAIL.minimal}${RESET}`);
|
|
126
|
+
console.log(` ${DIM}${TEMPLATE_DETAIL.full}${RESET}\n`);
|
|
127
|
+
const tAns = (await rl.question(` ${CYAN}?${RESET} Pick ${DIM}[1-2, default 2]${RESET} `)).trim().toLowerCase();
|
|
128
|
+
if (tAns === '1' || tAns === 'minimal')
|
|
129
|
+
template = 'minimal';
|
|
130
|
+
else
|
|
131
|
+
template = 'full';
|
|
132
|
+
}
|
|
133
|
+
console.log(`\n ${BOLD}◇ Summary${RESET}\n`);
|
|
134
|
+
console.log(` ${DIM}·${RESET} ${DIM}Location${RESET} ${BOLD}./${projectName}${RESET}`);
|
|
135
|
+
console.log(` ${DIM}·${RESET} ${DIM}Starter${RESET} ${BOLD}${template}${RESET} — ${TEMPLATE_DETAIL[template]}\n`);
|
|
136
|
+
const confirm = (await rl.question(` ${CYAN}?${RESET} Create project? ${DIM}[Y/n]${RESET} `)).trim().toLowerCase();
|
|
137
|
+
if (confirm === 'n' || confirm === 'no') {
|
|
138
|
+
console.log(`\n ${DIM}Aborted.${RESET}\n`);
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
return { projectName, template };
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
rl.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
30
147
|
async function main() {
|
|
148
|
+
const { projectNamePositional, template: templateFlag, help, useDefaults } = parseCreateArgs(process.argv);
|
|
149
|
+
if (help) {
|
|
150
|
+
console.log(BANNER);
|
|
151
|
+
console.log(` ${BOLD}Usage:${RESET} create-nexus [directory] [options]\n`);
|
|
152
|
+
console.log(` ${BOLD}Options:${RESET}`);
|
|
153
|
+
console.log(` ${DIM}--template, -t${RESET} ${DIM}minimal${RESET} | ${DIM}full${RESET} ${DIM}(starter; skips template step in the wizard)${RESET}`);
|
|
154
|
+
console.log(` ${DIM}--yes, -y${RESET} ${DIM}Skip prompts${RESET} — use defaults (full starter, name from argv or ${DIM}my-nexus-app${RESET})`);
|
|
155
|
+
console.log(` ${DIM}--defaults${RESET} ${DIM}Same as --yes${RESET}`);
|
|
156
|
+
console.log(` ${DIM}--help, -h${RESET} ${DIM}Show this help${RESET}\n`);
|
|
157
|
+
console.log(` ${BOLD}Interactive mode${RESET} ${DIM}(terminal):${RESET} asks for directory name, starter, and confirmation.`);
|
|
158
|
+
console.log(` ${BOLD}Non-interactive${RESET} ${DIM}(CI, pipe):${RESET} uses ${DIM}--yes${RESET} or defaults ${DIM}(full, my-nexus-app)${RESET}.\n`);
|
|
159
|
+
console.log(` ${BOLD}Templates:${RESET}`);
|
|
160
|
+
console.log(` ${DIM}minimal${RESET} One ${CYAN}+page.nx${RESET} + simple layout, no i18n, no example routes.`);
|
|
161
|
+
console.log(` ${DIM}full${RESET} i18n, islands presentation, blog — same as the reference app.\n`);
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
31
164
|
console.log(BANNER);
|
|
32
|
-
|
|
165
|
+
let projectName;
|
|
166
|
+
let template;
|
|
167
|
+
if (useDefaults) {
|
|
168
|
+
projectName = normalizeProjectName(projectNamePositional ?? '', 'my-nexus-app');
|
|
169
|
+
template = templateFlag ?? 'full';
|
|
170
|
+
}
|
|
171
|
+
else if (input.isTTY) {
|
|
172
|
+
const w = await runInteractiveWizard({
|
|
173
|
+
nameHint: projectNamePositional,
|
|
174
|
+
templateFromFlag: templateFlag,
|
|
175
|
+
});
|
|
176
|
+
projectName = w.projectName;
|
|
177
|
+
template = w.template;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
projectName = normalizeProjectName(projectNamePositional ?? '', 'my-nexus-app');
|
|
181
|
+
template = templateFlag ?? 'full';
|
|
182
|
+
}
|
|
33
183
|
const targetDir = resolve(process.cwd(), projectName);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
184
|
+
if (existsSync(targetDir)) {
|
|
185
|
+
console.error(`\n ${RED}✖${RESET} Directory already exists: ${BOLD}${projectName}${RESET}\n` +
|
|
186
|
+
` ${DIM}Choose another name, remove the folder, or run from a different directory.${RESET}\n`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
console.log(`\n Creating ${BOLD}${projectName}${RESET} ${DIM}(${template})${RESET}\n`);
|
|
190
|
+
const dirs = template === 'full'
|
|
191
|
+
? [
|
|
192
|
+
'src/routes',
|
|
193
|
+
'src/routes/islands',
|
|
194
|
+
'src/routes/blog/[slug]',
|
|
195
|
+
'src/components',
|
|
196
|
+
'src/islands',
|
|
197
|
+
'src/lib',
|
|
198
|
+
'public',
|
|
199
|
+
'scripts',
|
|
200
|
+
]
|
|
201
|
+
: ['src/routes', 'src/components', 'src/islands', 'src/lib', 'public', 'scripts'];
|
|
44
202
|
for (const dir of dirs) {
|
|
45
203
|
await mkdir(join(targetDir, dir), { recursive: true });
|
|
46
204
|
}
|
|
47
|
-
|
|
48
|
-
await writeProjectFiles(targetDir, projectName);
|
|
205
|
+
await writeProjectFiles(targetDir, projectName, template);
|
|
49
206
|
console.log(` ${GREEN}✓${RESET} Project created at ${BOLD}${projectName}/${RESET}\n`);
|
|
50
207
|
console.log(` Next steps:\n`);
|
|
51
|
-
console.log(` ${DIM}cd${RESET} ${projectName}`);
|
|
52
|
-
console.log(` ${DIM}npm install${RESET} ${DIM}(or pnpm / yarn / bun install)${RESET}`);
|
|
53
|
-
console.log(` ${DIM}npm run dev${RESET} ${DIM}(or pnpm dev, yarn dev, bun run dev)${RESET}\n`);
|
|
208
|
+
console.log(` ${DIM}1.${RESET} ${DIM}cd${RESET} ${projectName}`);
|
|
209
|
+
console.log(` ${DIM}2.${RESET} ${DIM}npm install${RESET} ${DIM}(required — or pnpm / yarn / bun install)${RESET}`);
|
|
210
|
+
console.log(` ${DIM}3.${RESET} ${DIM}npm run dev${RESET} ${DIM}(or pnpm dev, yarn dev, bun run dev)${RESET}\n`);
|
|
54
211
|
console.log(` ${CYAN}◆${RESET} Docs: ${BOLD}https://nexusjs.dev${RESET}\n`);
|
|
55
212
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
213
|
+
function buildMinimalScaffoldFiles(name, range, nexusCli, ensureDeps) {
|
|
214
|
+
return {
|
|
215
|
+
'scripts/check-node-modules.mjs': `import { access } from 'node:fs/promises';
|
|
216
|
+
import { join } from 'node:path';
|
|
217
|
+
|
|
218
|
+
const marker = join(process.cwd(), 'node_modules/@nexus_js/cli/package.json');
|
|
219
|
+
try {
|
|
220
|
+
await access(marker);
|
|
221
|
+
} catch {
|
|
222
|
+
console.error(
|
|
223
|
+
'\\n Dependencies are missing. Run npm install (or pnpm / yarn / bun install), then try again.\\n',
|
|
224
|
+
);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
`,
|
|
63
228
|
'package.json': JSON.stringify({
|
|
64
229
|
name,
|
|
65
230
|
version: '0.1.0',
|
|
66
231
|
private: true,
|
|
67
232
|
type: 'module',
|
|
68
233
|
scripts: {
|
|
234
|
+
predev: ensureDeps,
|
|
69
235
|
dev: `${nexusCli} dev`,
|
|
236
|
+
prebuild: ensureDeps,
|
|
70
237
|
build: `${nexusCli} build`,
|
|
238
|
+
prestart: ensureDeps,
|
|
71
239
|
start: `${nexusCli} start`,
|
|
240
|
+
precheck: ensureDeps,
|
|
72
241
|
check: `${nexusCli} check`,
|
|
73
242
|
},
|
|
74
243
|
dependencies: {
|
|
@@ -95,21 +264,17 @@ async function writeProjectFiles(dir, name) {
|
|
|
95
264
|
'nexus.config.ts': `import type { NexusConfig } from '@nexus_js/cli';
|
|
96
265
|
|
|
97
266
|
export default {
|
|
98
|
-
// Islands hydration strategy defaults
|
|
99
267
|
defaultHydration: 'client:visible',
|
|
100
268
|
|
|
101
|
-
// Image optimization
|
|
102
269
|
images: {
|
|
103
270
|
formats: ['avif', 'webp'],
|
|
104
271
|
sizes: [640, 1280, 1920],
|
|
105
272
|
},
|
|
106
273
|
|
|
107
|
-
// Server options
|
|
108
274
|
server: {
|
|
109
275
|
port: 3000,
|
|
110
276
|
},
|
|
111
277
|
|
|
112
|
-
// Build output
|
|
113
278
|
build: {
|
|
114
279
|
outDir: '.nexus/output',
|
|
115
280
|
sourcemap: false,
|
|
@@ -117,7 +282,6 @@ export default {
|
|
|
117
282
|
} satisfies NexusConfig;
|
|
118
283
|
`,
|
|
119
284
|
'src/routes/+layout.nx': `---
|
|
120
|
-
// Root layout — server-only
|
|
121
285
|
const appName = "My Nexus App";
|
|
122
286
|
---
|
|
123
287
|
|
|
@@ -125,7 +289,7 @@ const appName = "My Nexus App";
|
|
|
125
289
|
<head>
|
|
126
290
|
<meta charset="UTF-8">
|
|
127
291
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
128
|
-
<meta name="description" content="Built with Nexus —
|
|
292
|
+
<meta name="description" content="Built with Nexus — add your own copy in +layout.nx.">
|
|
129
293
|
<title>{appName}</title>
|
|
130
294
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
131
295
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
@@ -140,8 +304,6 @@ const appName = "My Nexus App";
|
|
|
140
304
|
<span>{appName}</span>
|
|
141
305
|
</a>
|
|
142
306
|
<nav class="nx-nav" aria-label="Main">
|
|
143
|
-
<a href="/">Home</a>
|
|
144
|
-
<a href="/blog">Blog</a>
|
|
145
307
|
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Docs</a>
|
|
146
308
|
</nav>
|
|
147
309
|
</header>
|
|
@@ -149,7 +311,606 @@ const appName = "My Nexus App";
|
|
|
149
311
|
<!--nexus:slot-->
|
|
150
312
|
</main>
|
|
151
313
|
<footer class="nx-footer">
|
|
152
|
-
<p>
|
|
314
|
+
<p>Built with <a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Nexus</a></p>
|
|
315
|
+
</footer>
|
|
316
|
+
</body>
|
|
317
|
+
</html>
|
|
318
|
+
|
|
319
|
+
<style>
|
|
320
|
+
:global(:root) {
|
|
321
|
+
--nx-bg0: #07080c;
|
|
322
|
+
--nx-surface: rgba(255, 255, 255, 0.04);
|
|
323
|
+
--nx-border: rgba(255, 255, 255, 0.08);
|
|
324
|
+
--nx-text: #f1f3f7;
|
|
325
|
+
--nx-muted: #8b93a7;
|
|
326
|
+
--nx-accent: #8b7cf8;
|
|
327
|
+
--nx-radius: 14px;
|
|
328
|
+
--nx-font: "DM Sans", system-ui, -apple-system, sans-serif;
|
|
329
|
+
--nx-display: "Outfit", var(--nx-font);
|
|
330
|
+
}
|
|
331
|
+
:global(.nx-body) {
|
|
332
|
+
margin: 0;
|
|
333
|
+
min-height: 100vh;
|
|
334
|
+
font-family: var(--nx-font);
|
|
335
|
+
color: var(--nx-text);
|
|
336
|
+
background: var(--nx-bg0);
|
|
337
|
+
line-height: 1.5;
|
|
338
|
+
-webkit-font-smoothing: antialiased;
|
|
339
|
+
}
|
|
340
|
+
:global(.nx-bg) {
|
|
341
|
+
position: fixed;
|
|
342
|
+
inset: 0;
|
|
343
|
+
z-index: -1;
|
|
344
|
+
background:
|
|
345
|
+
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(139, 124, 248, 0.22), transparent),
|
|
346
|
+
var(--nx-bg0);
|
|
347
|
+
}
|
|
348
|
+
:global(.nx-header) {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
justify-content: space-between;
|
|
352
|
+
gap: 1rem;
|
|
353
|
+
padding: 1rem 1.5rem;
|
|
354
|
+
max-width: 1100px;
|
|
355
|
+
margin: 0 auto;
|
|
356
|
+
border-bottom: 1px solid var(--nx-border);
|
|
357
|
+
backdrop-filter: blur(12px);
|
|
358
|
+
background: rgba(7, 8, 12, 0.75);
|
|
359
|
+
position: sticky;
|
|
360
|
+
top: 0;
|
|
361
|
+
z-index: 10;
|
|
362
|
+
}
|
|
363
|
+
:global(.nx-brand) {
|
|
364
|
+
display: inline-flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
gap: 0.5rem;
|
|
367
|
+
font-family: var(--nx-display);
|
|
368
|
+
font-weight: 700;
|
|
369
|
+
font-size: 1.1rem;
|
|
370
|
+
color: var(--nx-text);
|
|
371
|
+
text-decoration: none;
|
|
372
|
+
}
|
|
373
|
+
:global(.nx-brand-mark) { color: var(--nx-accent); font-size: 1.25rem; line-height: 1; }
|
|
374
|
+
:global(.nx-nav a) {
|
|
375
|
+
color: var(--nx-muted);
|
|
376
|
+
text-decoration: none;
|
|
377
|
+
font-size: 0.925rem;
|
|
378
|
+
font-weight: 500;
|
|
379
|
+
}
|
|
380
|
+
:global(.nx-nav a:hover) { color: var(--nx-text); }
|
|
381
|
+
:global(.nx-main) {
|
|
382
|
+
max-width: 1100px;
|
|
383
|
+
margin: 0 auto;
|
|
384
|
+
padding: 2rem 1.5rem 4rem;
|
|
385
|
+
}
|
|
386
|
+
:global(.nx-footer) {
|
|
387
|
+
max-width: 1100px;
|
|
388
|
+
margin: 0 auto;
|
|
389
|
+
padding: 2rem 1.5rem 3rem;
|
|
390
|
+
border-top: 1px solid var(--nx-border);
|
|
391
|
+
text-align: center;
|
|
392
|
+
}
|
|
393
|
+
:global(.nx-footer p) {
|
|
394
|
+
margin: 0;
|
|
395
|
+
font-size: 0.875rem;
|
|
396
|
+
color: var(--nx-muted);
|
|
397
|
+
}
|
|
398
|
+
:global(.nx-footer a) { color: var(--nx-accent); text-decoration: none; }
|
|
399
|
+
:global(.nx-footer a:hover) { text-decoration: underline; }
|
|
400
|
+
</style>
|
|
401
|
+
`,
|
|
402
|
+
'src/routes/+page.nx': `---
|
|
403
|
+
|
|
404
|
+
<section class="landing">
|
|
405
|
+
<p class="landing-kicker">Nexus</p>
|
|
406
|
+
<h1 class="landing-title">Start here</h1>
|
|
407
|
+
<p class="landing-lead">
|
|
408
|
+
This is the <strong>minimal</strong> template: one presentation page, no i18n, no example blog or islands route.
|
|
409
|
+
Edit <code class="landing-code">src/routes/+page.nx</code> and add routes under <code class="landing-code">src/routes/</code>.
|
|
410
|
+
</p>
|
|
411
|
+
<p class="landing-hint">
|
|
412
|
+
Want i18n, demos, and blog stubs? Create a new project with <code class="landing-code">--template full</code>.
|
|
413
|
+
</p>
|
|
414
|
+
<a class="landing-btn" href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Documentation</a>
|
|
415
|
+
</section>
|
|
416
|
+
|
|
417
|
+
<style>
|
|
418
|
+
.landing {
|
|
419
|
+
max-width: 36rem;
|
|
420
|
+
padding: 2rem 0 3rem;
|
|
421
|
+
}
|
|
422
|
+
.landing-kicker {
|
|
423
|
+
margin: 0 0 0.75rem;
|
|
424
|
+
font-size: 0.75rem;
|
|
425
|
+
font-weight: 600;
|
|
426
|
+
letter-spacing: 0.14em;
|
|
427
|
+
text-transform: uppercase;
|
|
428
|
+
color: var(--nx-accent);
|
|
429
|
+
}
|
|
430
|
+
.landing-title {
|
|
431
|
+
margin: 0 0 1rem;
|
|
432
|
+
font-family: var(--nx-display);
|
|
433
|
+
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
|
434
|
+
font-weight: 700;
|
|
435
|
+
letter-spacing: -0.02em;
|
|
436
|
+
}
|
|
437
|
+
.landing-lead {
|
|
438
|
+
margin: 0 0 1rem;
|
|
439
|
+
color: var(--nx-muted);
|
|
440
|
+
line-height: 1.65;
|
|
441
|
+
font-size: 1.05rem;
|
|
442
|
+
}
|
|
443
|
+
.landing-hint {
|
|
444
|
+
margin: 0 0 1.75rem;
|
|
445
|
+
font-size: 0.9rem;
|
|
446
|
+
color: var(--nx-muted);
|
|
447
|
+
line-height: 1.55;
|
|
448
|
+
}
|
|
449
|
+
.landing-code {
|
|
450
|
+
font-family: ui-monospace, monospace;
|
|
451
|
+
font-size: 0.88em;
|
|
452
|
+
padding: 0.1em 0.35em;
|
|
453
|
+
border-radius: 6px;
|
|
454
|
+
background: var(--nx-surface);
|
|
455
|
+
border: 1px solid var(--nx-border);
|
|
456
|
+
color: var(--nx-accent);
|
|
457
|
+
}
|
|
458
|
+
.landing-btn {
|
|
459
|
+
display: inline-flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
padding: 0.65rem 1.25rem;
|
|
462
|
+
border-radius: 10px;
|
|
463
|
+
font-size: 0.9375rem;
|
|
464
|
+
font-weight: 600;
|
|
465
|
+
text-decoration: none;
|
|
466
|
+
background: linear-gradient(135deg, var(--nx-accent), #6366f1);
|
|
467
|
+
color: #fff;
|
|
468
|
+
box-shadow: 0 4px 24px rgba(139, 124, 248, 0.15);
|
|
469
|
+
}
|
|
470
|
+
.landing-btn:hover { filter: brightness(1.06); }
|
|
471
|
+
</style>
|
|
472
|
+
`,
|
|
473
|
+
'src/lib/db.ts': `// Database client placeholder
|
|
474
|
+
// Replace with your preferred ORM (Prisma, Drizzle, etc.)
|
|
475
|
+
|
|
476
|
+
export const db = {
|
|
477
|
+
user: {
|
|
478
|
+
async findFirst() {
|
|
479
|
+
return { id: 1, name: 'Demo User', email: 'demo@nexusjs.dev' };
|
|
480
|
+
},
|
|
481
|
+
async findMany() {
|
|
482
|
+
return [{ id: 1, name: 'Demo User', email: 'demo@nexusjs.dev' }];
|
|
483
|
+
},
|
|
484
|
+
async update(args: { where?: unknown; data: unknown }) {
|
|
485
|
+
return { ...(args.data as object) };
|
|
486
|
+
},
|
|
487
|
+
async create(args: { data: unknown }) {
|
|
488
|
+
return args.data;
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
`,
|
|
493
|
+
'public/favicon.svg': NEXUS_LOGO_SVG,
|
|
494
|
+
'.gitignore': `node_modules/
|
|
495
|
+
.nexus/
|
|
496
|
+
dist/
|
|
497
|
+
*.js.map
|
|
498
|
+
`,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function buildFullScaffoldFiles(name, range, nexusCli, ensureDeps) {
|
|
502
|
+
return {
|
|
503
|
+
'scripts/check-node-modules.mjs': `import { access } from 'node:fs/promises';
|
|
504
|
+
import { join } from 'node:path';
|
|
505
|
+
|
|
506
|
+
const marker = join(process.cwd(), 'node_modules/@nexus_js/cli/package.json');
|
|
507
|
+
try {
|
|
508
|
+
await access(marker);
|
|
509
|
+
} catch {
|
|
510
|
+
console.error(
|
|
511
|
+
'\\n Dependencies are missing. Run npm install (or pnpm / yarn / bun install), then try again.\\n',
|
|
512
|
+
);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
`,
|
|
516
|
+
'package.json': JSON.stringify({
|
|
517
|
+
name,
|
|
518
|
+
version: '0.1.0',
|
|
519
|
+
private: true,
|
|
520
|
+
type: 'module',
|
|
521
|
+
scripts: {
|
|
522
|
+
predev: ensureDeps,
|
|
523
|
+
dev: `${nexusCli} dev`,
|
|
524
|
+
prebuild: ensureDeps,
|
|
525
|
+
build: `${nexusCli} build`,
|
|
526
|
+
prestart: ensureDeps,
|
|
527
|
+
start: `${nexusCli} start`,
|
|
528
|
+
precheck: ensureDeps,
|
|
529
|
+
check: `${nexusCli} check`,
|
|
530
|
+
},
|
|
531
|
+
dependencies: {
|
|
532
|
+
'@nexus_js/runtime': range,
|
|
533
|
+
},
|
|
534
|
+
devDependencies: {
|
|
535
|
+
'@nexus_js/cli': range,
|
|
536
|
+
'@nexus_js/compiler': range,
|
|
537
|
+
typescript: '^5.5.0',
|
|
538
|
+
},
|
|
539
|
+
}, null, 2),
|
|
540
|
+
'tsconfig.json': JSON.stringify({
|
|
541
|
+
compilerOptions: {
|
|
542
|
+
target: 'ES2022',
|
|
543
|
+
module: 'NodeNext',
|
|
544
|
+
moduleResolution: 'NodeNext',
|
|
545
|
+
strict: true,
|
|
546
|
+
skipLibCheck: true,
|
|
547
|
+
noEmit: true,
|
|
548
|
+
paths: { '$lib/*': ['./src/lib/*'] },
|
|
549
|
+
},
|
|
550
|
+
include: ['nexus.config.ts', 'src/**/*.ts'],
|
|
551
|
+
}, null, 2),
|
|
552
|
+
'src/lib/i18n.ts': `/**
|
|
553
|
+
* i18n — aligned with nexus.config.ts \`i18n.locales\`.
|
|
554
|
+
* Resolve locale per request: ?lang= → cookie nx-lang → Accept-Language → default.
|
|
555
|
+
*/
|
|
556
|
+
|
|
557
|
+
export type Locale = 'en' | 'es' | 'pt';
|
|
558
|
+
|
|
559
|
+
export const LOCALES: Locale[] = ['en', 'es', 'pt'];
|
|
560
|
+
export const DEFAULT_LOCALE: Locale = 'en';
|
|
561
|
+
|
|
562
|
+
type CtxLike = {
|
|
563
|
+
url: URL;
|
|
564
|
+
getCookie: (name: string) => string | undefined;
|
|
565
|
+
request: Request;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
function isLocale(s: string | undefined | null): s is Locale {
|
|
569
|
+
return s === 'en' || s === 'es' || s === 'pt';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Active locale for this request (use in templates: getLocale(ctx)). */
|
|
573
|
+
export function getLocale(ctx: CtxLike): Locale {
|
|
574
|
+
const q = ctx.url.searchParams.get('lang') ?? ctx.url.searchParams.get('locale');
|
|
575
|
+
if (isLocale(q)) return q;
|
|
576
|
+
const ck = ctx.getCookie('nx-lang');
|
|
577
|
+
if (isLocale(ck)) return ck;
|
|
578
|
+
const al = ctx.request.headers.get('accept-language');
|
|
579
|
+
if (al) {
|
|
580
|
+
const first = al.split(',')[0]?.trim().split('-')[0]?.toLowerCase();
|
|
581
|
+
if (first === 'es' || first === 'pt') return first;
|
|
582
|
+
}
|
|
583
|
+
return DEFAULT_LOCALE;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Same path + query, with \`lang\` set (preserves other search params). */
|
|
587
|
+
export function langHref(ctx: CtxLike, locale: Locale): string {
|
|
588
|
+
const u = new URL(ctx.url.href);
|
|
589
|
+
u.searchParams.set('lang', locale);
|
|
590
|
+
return u.pathname + u.search;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Internal link with current locale in \`lang\` (e.g. \`/islands?lang=es\`). */
|
|
594
|
+
export function pathWithLang(ctx: CtxLike, pathname: string): string {
|
|
595
|
+
const u = new URL(ctx.url.href);
|
|
596
|
+
u.pathname = pathname;
|
|
597
|
+
u.searchParams.set('lang', getLocale(ctx));
|
|
598
|
+
return u.pathname + u.search;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function layoutCopy(locale: Locale) {
|
|
602
|
+
const t = {
|
|
603
|
+
en: {
|
|
604
|
+
appName: 'My Nexus App',
|
|
605
|
+
metaDescription:
|
|
606
|
+
'Built with Nexus — full-stack framework with islands, Svelte 5 runes, and server actions.',
|
|
607
|
+
navHome: 'Home',
|
|
608
|
+
navIslands: 'Islands',
|
|
609
|
+
navBlog: 'Blog',
|
|
610
|
+
navDocs: 'Docs',
|
|
611
|
+
navAria: 'Main',
|
|
612
|
+
langAria: 'Language',
|
|
613
|
+
footerTagline: 'Islands · Runes · Server Actions',
|
|
614
|
+
footerMade: 'Built with',
|
|
615
|
+
},
|
|
616
|
+
es: {
|
|
617
|
+
appName: 'Mi app Nexus',
|
|
618
|
+
metaDescription:
|
|
619
|
+
'Hecho con Nexus — framework full stack con islas, runes de Svelte 5 y server actions.',
|
|
620
|
+
navHome: 'Inicio',
|
|
621
|
+
navIslands: 'Islas',
|
|
622
|
+
navBlog: 'Blog',
|
|
623
|
+
navDocs: 'Docs',
|
|
624
|
+
navAria: 'Principal',
|
|
625
|
+
langAria: 'Idioma',
|
|
626
|
+
footerTagline: 'Islas · Runes · Server Actions',
|
|
627
|
+
footerMade: 'Hecho con',
|
|
628
|
+
},
|
|
629
|
+
pt: {
|
|
630
|
+
appName: 'Meu app Nexus',
|
|
631
|
+
metaDescription:
|
|
632
|
+
'Feito com Nexus — framework full stack com ilhas, runes Svelte 5 e server actions.',
|
|
633
|
+
navHome: 'Início',
|
|
634
|
+
navIslands: 'Ilhas',
|
|
635
|
+
navBlog: 'Blog',
|
|
636
|
+
navDocs: 'Docs',
|
|
637
|
+
navAria: 'Principal',
|
|
638
|
+
langAria: 'Idioma',
|
|
639
|
+
footerTagline: 'Ilhas · Runes · Server Actions',
|
|
640
|
+
footerMade: 'Feito com',
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
return t[locale];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function langActiveClass(ctx: CtxLike, locale: Locale): string {
|
|
647
|
+
return getLocale(ctx) === locale ? 'nx-lang-btn--on' : '';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function homeCopy(locale: Locale) {
|
|
651
|
+
const t = {
|
|
652
|
+
en: {
|
|
653
|
+
kicker: "You're running Nexus",
|
|
654
|
+
greeting: 'Ship less JavaScript.',
|
|
655
|
+
sub: 'Nexus combines islands architecture, Svelte 5 runes, and server actions — so your default page weight stays tiny.',
|
|
656
|
+
ctaDocs: 'Documentation',
|
|
657
|
+
ctaIslands: 'Islands guide',
|
|
658
|
+
ctaBlog: 'Example blog',
|
|
659
|
+
featTitle: 'Why Nexus',
|
|
660
|
+
features: [
|
|
661
|
+
{ icon: '◇', title: 'Islands', desc: 'HTML-first pages; JS only where you opt in with client:visible and friends.' },
|
|
662
|
+
{ icon: '⚡', title: 'Runes', desc: 'Fine-grained reactivity with Svelte 5 — no legacy stores required.' },
|
|
663
|
+
{ icon: '↯', title: 'Server actions', desc: 'Mutations colocated with routes; type-safe, SSR-friendly.' },
|
|
664
|
+
],
|
|
665
|
+
demoTitle: 'Interactive island',
|
|
666
|
+
demoHint:
|
|
667
|
+
'The counter below is a small client island — the rest of this page can stay server-rendered.',
|
|
668
|
+
demoLabel: 'Hydrated in the browser',
|
|
669
|
+
counterAria: 'Increment counter',
|
|
670
|
+
},
|
|
671
|
+
es: {
|
|
672
|
+
kicker: 'Estás ejecutando Nexus',
|
|
673
|
+
greeting: 'Envía menos JavaScript.',
|
|
674
|
+
sub: 'Nexus combina arquitectura de islas, runes de Svelte 5 y server actions — el peso por defecto de la página se mantiene bajo.',
|
|
675
|
+
ctaDocs: 'Documentación',
|
|
676
|
+
ctaIslands: 'Guía de islas',
|
|
677
|
+
ctaBlog: 'Blog de ejemplo',
|
|
678
|
+
featTitle: 'Por qué Nexus',
|
|
679
|
+
features: [
|
|
680
|
+
{ icon: '◇', title: 'Islas', desc: 'Páginas HTML primero; JS solo donde eliges con client:visible y similares.' },
|
|
681
|
+
{ icon: '⚡', title: 'Runes', desc: 'Reactividad fina con Svelte 5 — sin stores legacy.' },
|
|
682
|
+
{ icon: '↯', title: 'Server actions', desc: 'Mutaciones junto a las rutas; tipadas y amigables con SSR.' },
|
|
683
|
+
],
|
|
684
|
+
demoTitle: 'Isla interactiva',
|
|
685
|
+
demoHint:
|
|
686
|
+
'El contador es una isla pequeña — el resto de la página puede seguir renderizado en el servidor.',
|
|
687
|
+
demoLabel: 'Hidratado en el navegador',
|
|
688
|
+
counterAria: 'Incrementar contador',
|
|
689
|
+
},
|
|
690
|
+
pt: {
|
|
691
|
+
kicker: 'Você está rodando Nexus',
|
|
692
|
+
greeting: 'Envie menos JavaScript.',
|
|
693
|
+
sub: 'Nexus combina ilhas, runes Svelte 5 e server actions — o peso padrão da página permanece baixo.',
|
|
694
|
+
ctaDocs: 'Documentação',
|
|
695
|
+
ctaIslands: 'Guia de ilhas',
|
|
696
|
+
ctaBlog: 'Blog de exemplo',
|
|
697
|
+
featTitle: 'Por que Nexus',
|
|
698
|
+
features: [
|
|
699
|
+
{ icon: '◇', title: 'Ilhas', desc: 'HTML primeiro; JS só onde você marca com client:visible e afins.' },
|
|
700
|
+
{ icon: '⚡', title: 'Runes', desc: 'Reatividade fina com Svelte 5 — sem stores legados.' },
|
|
701
|
+
{ icon: '↯', title: 'Server actions', desc: 'Mutações junto às rotas; tipadas e SSR-friendly.' },
|
|
702
|
+
],
|
|
703
|
+
demoTitle: 'Ilha interativa',
|
|
704
|
+
demoHint: 'O contador abaixo é uma ilha pequena — o restante pode ficar no servidor.',
|
|
705
|
+
demoLabel: 'Hidratado no navegador',
|
|
706
|
+
counterAria: 'Incrementar contador',
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
return t[locale];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function islandsCopy(locale: Locale) {
|
|
713
|
+
const t = {
|
|
714
|
+
en: {
|
|
715
|
+
pageTitle: 'Islands & components',
|
|
716
|
+
lead: 'How Nexus sends HTML first and adds JavaScript only where you mark it with client:*.',
|
|
717
|
+
presKicker: 'Mini brief',
|
|
718
|
+
s1h: '1 · What is an island?',
|
|
719
|
+
s1p:
|
|
720
|
+
'The server paints the full page in HTML. Only the block wrapped with a directive like client:load or client:visible downloads a small bundle that the browser hydrates (runes, clicks, state).',
|
|
721
|
+
diagramAria: 'Flow: server sends HTML; only the island hydrates',
|
|
722
|
+
flowSrv: 'SSR',
|
|
723
|
+
flowHtml: 'HTML + marked island',
|
|
724
|
+
flowJs: 'Island JS',
|
|
725
|
+
flowOk: 'Interactivity',
|
|
726
|
+
s2h: '2 · Hydration directives',
|
|
727
|
+
thDirective: 'Directive',
|
|
728
|
+
thWhen: 'When',
|
|
729
|
+
thUse: 'Typical use',
|
|
730
|
+
r1: ['client:load', 'On page load', 'Critical UI — nav, modal'],
|
|
731
|
+
r2: ['client:idle', 'When the browser is idle', 'Secondary widgets'],
|
|
732
|
+
r3: ['client:visible', 'When entering the viewport', 'Below-the-fold content'],
|
|
733
|
+
r4: ['client:media="…"', 'When the media query matches', 'Mobile-only or desktop-only'],
|
|
734
|
+
r5: ['server:only', 'Never hydrates', 'Heavy tables, admin without JS'],
|
|
735
|
+
s3h: '3 · Structure of a .nx file',
|
|
736
|
+
l1: 'Frontmatter — server only: data, import, await.',
|
|
737
|
+
l2: 'script — runes ($state, $derived, $effect) for the client.',
|
|
738
|
+
l3: 'HTML template — interpolations and client:* on the interactive root.',
|
|
739
|
+
l4: 'style — scoped CSS for this file.',
|
|
740
|
+
s4h: '4 · Componentization',
|
|
741
|
+
s4p:
|
|
742
|
+
'Split reusable pieces into src/components/MyName.nx (same blocks). In a route, import in the frontmatter and use <MyName /> in the template when the compiler resolves it.',
|
|
743
|
+
s4muted: 'Import resolution and preloads follow the compiler conventions.',
|
|
744
|
+
s5h: '5 · Live demo (client:visible)',
|
|
745
|
+
s5p: 'Scroll down to hydrate the island; the button updates reactive state.',
|
|
746
|
+
demoBtnAria: 'Add one',
|
|
747
|
+
refh: 'References',
|
|
748
|
+
refp: 'In the repo: docs/ISLANDS.md · Official site:',
|
|
749
|
+
},
|
|
750
|
+
es: {
|
|
751
|
+
pageTitle: 'Islas y componentes',
|
|
752
|
+
lead: 'Cómo Nexus envía HTML primero y añade JavaScript solo donde lo marcas con client:*.',
|
|
753
|
+
presKicker: 'Mini presentación',
|
|
754
|
+
s1h: '1 · ¿Qué es una isla?',
|
|
755
|
+
s1p:
|
|
756
|
+
'El servidor pinta toda la página en HTML. Solo el bloque con client:load, client:visible, etc. genera un bundle pequeño que el navegador hidrata (runes, clics, estado).',
|
|
757
|
+
diagramAria: 'Flujo: servidor entrega HTML; el navegador hidrata solo la isla',
|
|
758
|
+
flowSrv: 'SSR',
|
|
759
|
+
flowHtml: 'HTML + isla marcada',
|
|
760
|
+
flowJs: 'JS de la isla',
|
|
761
|
+
flowOk: 'Interactividad',
|
|
762
|
+
s2h: '2 · Directivas de hidratación',
|
|
763
|
+
thDirective: 'Directiva',
|
|
764
|
+
thWhen: 'Cuándo',
|
|
765
|
+
thUse: 'Uso típico',
|
|
766
|
+
r1: ['client:load', 'Al cargar la página', 'UI crítica — nav, modal'],
|
|
767
|
+
r2: ['client:idle', 'Cuando el navegador está libre', 'Widgets secundarios'],
|
|
768
|
+
r3: ['client:visible', 'Al entrar en viewport', 'Contenido bajo el fold'],
|
|
769
|
+
r4: ['client:media="…"', 'Si coincide la media query', 'Solo móvil o solo desktop'],
|
|
770
|
+
r5: ['server:only', 'Nunca hidrata', 'Tablas pesadas, admin sin JS'],
|
|
771
|
+
s3h: '3 · Estructura de un archivo .nx',
|
|
772
|
+
l1: 'Frontmatter — solo servidor: datos, import, await.',
|
|
773
|
+
l2: 'script — runes ($state, $derived, $effect) para la parte cliente.',
|
|
774
|
+
l3: 'Plantilla HTML — interpolaciones y client:* en el contenedor interactivo.',
|
|
775
|
+
l4: 'style — CSS con alcance al archivo.',
|
|
776
|
+
s4h: '4 · Componentizar',
|
|
777
|
+
s4p:
|
|
778
|
+
'Separa piezas en src/components/MiNombre.nx (mismo formato). En una ruta, importa en el frontmatter y usa <MiNombre /> en la plantilla cuando el compilador lo resuelva.',
|
|
779
|
+
s4muted: 'La resolución de imports y preloads sigue las convenciones del compilador.',
|
|
780
|
+
s5h: '5 · Demo en vivo (client:visible)',
|
|
781
|
+
s5p: 'Al hacer scroll hasta aquí, la isla se hidrata; el botón incrementa el estado reactivo.',
|
|
782
|
+
demoBtnAria: 'Sumar uno',
|
|
783
|
+
refh: 'Referencias',
|
|
784
|
+
refp: 'En el repo: docs/ISLANDS.md · Sitio oficial:',
|
|
785
|
+
},
|
|
786
|
+
pt: {
|
|
787
|
+
pageTitle: 'Ilhas e componentes',
|
|
788
|
+
lead: 'Como o Nexus envia HTML primeiro e só adiciona JS onde você marca com client:*.',
|
|
789
|
+
presKicker: 'Mini apresentação',
|
|
790
|
+
s1h: '1 · O que é uma ilha?',
|
|
791
|
+
s1p:
|
|
792
|
+
'O servidor renderiza a página inteira em HTML. Só o bloco com client:load, client:visible, etc. baixa um bundle pequeno que o navegador hidrata.',
|
|
793
|
+
diagramAria: 'Fluxo: servidor entrega HTML; só a ilha hidrata',
|
|
794
|
+
flowSrv: 'SSR',
|
|
795
|
+
flowHtml: 'HTML + ilha marcada',
|
|
796
|
+
flowJs: 'JS da ilha',
|
|
797
|
+
flowOk: 'Interatividade',
|
|
798
|
+
s2h: '2 · Diretivas de hidratação',
|
|
799
|
+
thDirective: 'Diretiva',
|
|
800
|
+
thWhen: 'Quando',
|
|
801
|
+
thUse: 'Uso típico',
|
|
802
|
+
r1: ['client:load', 'Ao carregar a página', 'UI crítica — nav, modal'],
|
|
803
|
+
r2: ['client:idle', 'Quando o navegador está ocioso', 'Widgets secundários'],
|
|
804
|
+
r3: ['client:visible', 'Ao entrar na viewport', 'Abaixo da dobra'],
|
|
805
|
+
r4: ['client:media="…"', 'Se a media query casar', 'Só mobile ou só desktop'],
|
|
806
|
+
r5: ['server:only', 'Nunca hidrata', 'Tabelas pesadas, admin sem JS'],
|
|
807
|
+
s3h: '3 · Estrutura de um arquivo .nx',
|
|
808
|
+
l1: 'Frontmatter — só servidor: dados, import, await.',
|
|
809
|
+
l2: 'script — runes ($state, $derived, $effect) para o cliente.',
|
|
810
|
+
l3: 'Template HTML — interpolações e client:* na raiz interativa.',
|
|
811
|
+
l4: 'style — CSS escopado ao arquivo.',
|
|
812
|
+
s4h: '4 · Componentizar',
|
|
813
|
+
s4p:
|
|
814
|
+
'Separe em src/components/Nome.nx. Na rota, importe no frontmatter e use <Nome /> no template quando o compilador resolver.',
|
|
815
|
+
s4muted: 'Imports e preloads seguem as convenções do compilador.',
|
|
816
|
+
s5h: '5 · Demo ao vivo (client:visible)',
|
|
817
|
+
s5p: 'Role até aqui para hidratar a ilha; o botão atualiza o estado.',
|
|
818
|
+
demoBtnAria: 'Mais um',
|
|
819
|
+
refh: 'Referências',
|
|
820
|
+
refp: 'No repositório: docs/ISLANDS.md · Site oficial:',
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
return t[locale];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function pageHomeCopy(ctx: CtxLike) {
|
|
827
|
+
return homeCopy(getLocale(ctx));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export function pageIslandsCopy(ctx: CtxLike) {
|
|
831
|
+
return islandsCopy(getLocale(ctx));
|
|
832
|
+
}
|
|
833
|
+
`,
|
|
834
|
+
'nexus.config.ts': `import type { NexusConfig } from '@nexus_js/cli';
|
|
835
|
+
|
|
836
|
+
export default {
|
|
837
|
+
i18n: {
|
|
838
|
+
defaultLocale: 'en',
|
|
839
|
+
locales: ['en', 'es', 'pt'],
|
|
840
|
+
},
|
|
841
|
+
|
|
842
|
+
// Islands hydration strategy defaults
|
|
843
|
+
defaultHydration: 'client:visible',
|
|
844
|
+
|
|
845
|
+
// Image optimization
|
|
846
|
+
images: {
|
|
847
|
+
formats: ['avif', 'webp'],
|
|
848
|
+
sizes: [640, 1280, 1920],
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
// Server options
|
|
852
|
+
server: {
|
|
853
|
+
port: 3000,
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
// Build output
|
|
857
|
+
build: {
|
|
858
|
+
outDir: '.nexus/output',
|
|
859
|
+
sourcemap: false,
|
|
860
|
+
},
|
|
861
|
+
} satisfies NexusConfig;
|
|
862
|
+
`,
|
|
863
|
+
'src/routes/+layout.nx': `---
|
|
864
|
+
import {
|
|
865
|
+
getLocale,
|
|
866
|
+
langHref,
|
|
867
|
+
langActiveClass,
|
|
868
|
+
layoutCopy,
|
|
869
|
+
pathWithLang,
|
|
870
|
+
} from '$lib/i18n';
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
<html lang="{getLocale(ctx)}">
|
|
874
|
+
<head>
|
|
875
|
+
<meta charset="UTF-8">
|
|
876
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
877
|
+
<meta name="description" content="{layoutCopy(getLocale(ctx)).metaDescription}">
|
|
878
|
+
<title>{layoutCopy(getLocale(ctx)).appName}</title>
|
|
879
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
880
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
881
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
882
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400..700;1,9..40,400&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
|
|
883
|
+
</head>
|
|
884
|
+
<body class="nx-body">
|
|
885
|
+
<div class="nx-bg" aria-hidden="true"></div>
|
|
886
|
+
<header class="nx-header">
|
|
887
|
+
<a class="nx-brand" href="{pathWithLang(ctx, '/')}">
|
|
888
|
+
<span class="nx-brand-mark" aria-hidden="true">◆</span>
|
|
889
|
+
<span>{layoutCopy(getLocale(ctx)).appName}</span>
|
|
890
|
+
</a>
|
|
891
|
+
<div class="nx-header-actions">
|
|
892
|
+
<nav class="nx-nav" aria-label="{layoutCopy(getLocale(ctx)).navAria}">
|
|
893
|
+
<a href="{pathWithLang(ctx, '/')}">{layoutCopy(getLocale(ctx)).navHome}</a>
|
|
894
|
+
<a href="{pathWithLang(ctx, '/islands')}">{layoutCopy(getLocale(ctx)).navIslands}</a>
|
|
895
|
+
<a href="{pathWithLang(ctx, '/blog')}">{layoutCopy(getLocale(ctx)).navBlog}</a>
|
|
896
|
+
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">{layoutCopy(getLocale(ctx)).navDocs}</a>
|
|
897
|
+
</nav>
|
|
898
|
+
<div class="nx-lang" role="group" aria-label="{layoutCopy(getLocale(ctx)).langAria}">
|
|
899
|
+
<a class="nx-lang-btn {langActiveClass(ctx, 'en')}" href="{langHref(ctx, 'en')}">EN</a>
|
|
900
|
+
<a class="nx-lang-btn {langActiveClass(ctx, 'es')}" href="{langHref(ctx, 'es')}">ES</a>
|
|
901
|
+
<a class="nx-lang-btn {langActiveClass(ctx, 'pt')}" href="{langHref(ctx, 'pt')}">PT</a>
|
|
902
|
+
</div>
|
|
903
|
+
</div>
|
|
904
|
+
</header>
|
|
905
|
+
<main class="nx-main">
|
|
906
|
+
<!--nexus:slot-->
|
|
907
|
+
</main>
|
|
908
|
+
<footer class="nx-footer">
|
|
909
|
+
<p>
|
|
910
|
+
{layoutCopy(getLocale(ctx)).footerMade}
|
|
911
|
+
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">Nexus</a>
|
|
912
|
+
· {layoutCopy(getLocale(ctx)).footerTagline}
|
|
913
|
+
</p>
|
|
153
914
|
</footer>
|
|
154
915
|
</body>
|
|
155
916
|
</html>
|
|
@@ -201,6 +962,13 @@ const appName = "My Nexus App";
|
|
|
201
962
|
top: 0;
|
|
202
963
|
z-index: 10;
|
|
203
964
|
}
|
|
965
|
+
:global(.nx-header-actions) {
|
|
966
|
+
display: flex;
|
|
967
|
+
flex-wrap: wrap;
|
|
968
|
+
align-items: center;
|
|
969
|
+
gap: 0.75rem 1.25rem;
|
|
970
|
+
justify-content: flex-end;
|
|
971
|
+
}
|
|
204
972
|
:global(.nx-brand) {
|
|
205
973
|
display: inline-flex;
|
|
206
974
|
align-items: center;
|
|
@@ -232,6 +1000,31 @@ const appName = "My Nexus App";
|
|
|
232
1000
|
:global(.nx-nav a:hover) {
|
|
233
1001
|
color: var(--nx-text);
|
|
234
1002
|
}
|
|
1003
|
+
:global(.nx-lang) {
|
|
1004
|
+
display: inline-flex;
|
|
1005
|
+
gap: 0.2rem;
|
|
1006
|
+
padding: 0.2rem;
|
|
1007
|
+
border-radius: 10px;
|
|
1008
|
+
border: 1px solid var(--nx-border);
|
|
1009
|
+
background: rgba(0, 0, 0, 0.2);
|
|
1010
|
+
}
|
|
1011
|
+
:global(.nx-lang-btn) {
|
|
1012
|
+
padding: 0.25rem 0.5rem;
|
|
1013
|
+
border-radius: 7px;
|
|
1014
|
+
font-size: 0.7rem;
|
|
1015
|
+
font-weight: 700;
|
|
1016
|
+
letter-spacing: 0.04em;
|
|
1017
|
+
color: var(--nx-muted);
|
|
1018
|
+
text-decoration: none;
|
|
1019
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
1020
|
+
}
|
|
1021
|
+
:global(.nx-lang-btn:hover) {
|
|
1022
|
+
color: var(--nx-text);
|
|
1023
|
+
}
|
|
1024
|
+
:global(.nx-lang-btn--on) {
|
|
1025
|
+
background: rgba(139, 124, 248, 0.25);
|
|
1026
|
+
color: var(--nx-text);
|
|
1027
|
+
}
|
|
235
1028
|
:global(.nx-main) {
|
|
236
1029
|
max-width: 1100px;
|
|
237
1030
|
margin: 0 auto;
|
|
@@ -259,13 +1052,7 @@ const appName = "My Nexus App";
|
|
|
259
1052
|
</style>
|
|
260
1053
|
`,
|
|
261
1054
|
'src/routes/+page.nx': `---
|
|
262
|
-
|
|
263
|
-
const sub = "Nexus combines islands architecture, Svelte 5 runes, and server actions — so your default page weight stays tiny.";
|
|
264
|
-
const features = [
|
|
265
|
-
{ icon: "◇", title: "Islands", desc: "HTML-first pages; JS only where you opt in with client:visible and friends." },
|
|
266
|
-
{ icon: "⚡", title: "Runes", desc: "Fine-grained reactivity with Svelte 5 — no legacy stores required." },
|
|
267
|
-
{ icon: "↯", title: "Server actions", desc: "Mutations colocated with routes; type-safe, SSR-friendly." },
|
|
268
|
-
];
|
|
1055
|
+
import { pageHomeCopy, pathWithLang } from '$lib/i18n';
|
|
269
1056
|
---
|
|
270
1057
|
|
|
271
1058
|
<script>
|
|
@@ -274,19 +1061,20 @@ const features = [
|
|
|
274
1061
|
</script>
|
|
275
1062
|
|
|
276
1063
|
<section class="hero">
|
|
277
|
-
<p class="hero-kicker">
|
|
278
|
-
<h1 class="hero-title">{greeting}</h1>
|
|
279
|
-
<p class="hero-lead">{sub}</p>
|
|
1064
|
+
<p class="hero-kicker">{pageHomeCopy(ctx).kicker}</p>
|
|
1065
|
+
<h1 class="hero-title">{pageHomeCopy(ctx).greeting}</h1>
|
|
1066
|
+
<p class="hero-lead">{pageHomeCopy(ctx).sub}</p>
|
|
280
1067
|
<div class="hero-actions">
|
|
281
|
-
<a class="btn btn-primary" href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">
|
|
282
|
-
<a class="btn btn-ghost" href="/
|
|
1068
|
+
<a class="btn btn-primary" href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">{pageHomeCopy(ctx).ctaDocs}</a>
|
|
1069
|
+
<a class="btn btn-ghost" href="{pathWithLang(ctx, '/islands')}">{pageHomeCopy(ctx).ctaIslands}</a>
|
|
1070
|
+
<a class="btn btn-ghost" href="{pathWithLang(ctx, '/blog')}">{pageHomeCopy(ctx).ctaBlog}</a>
|
|
283
1071
|
</div>
|
|
284
1072
|
</section>
|
|
285
1073
|
|
|
286
1074
|
<section class="features" aria-labelledby="feat-heading">
|
|
287
|
-
<h2 id="feat-heading" class="section-title">
|
|
1075
|
+
<h2 id="feat-heading" class="section-title">{pageHomeCopy(ctx).featTitle}</h2>
|
|
288
1076
|
<ul class="feature-grid">
|
|
289
|
-
{#each features as f}
|
|
1077
|
+
{#each pageHomeCopy(ctx).features as f}
|
|
290
1078
|
<li class="card">
|
|
291
1079
|
<span class="card-icon" aria-hidden="true">{f.icon}</span>
|
|
292
1080
|
<h3 class="card-title">{f.title}</h3>
|
|
@@ -297,13 +1085,13 @@ const features = [
|
|
|
297
1085
|
</section>
|
|
298
1086
|
|
|
299
1087
|
<section class="demo" aria-labelledby="demo-heading">
|
|
300
|
-
<h2 id="demo-heading" class="section-title">
|
|
301
|
-
<p class="demo-hint">
|
|
1088
|
+
<h2 id="demo-heading" class="section-title">{pageHomeCopy(ctx).demoTitle}</h2>
|
|
1089
|
+
<p class="demo-hint">{pageHomeCopy(ctx).demoHint}</p>
|
|
302
1090
|
<div class="counter-wrap" client:visible>
|
|
303
1091
|
<div class="counter">
|
|
304
|
-
<p class="counter-label">
|
|
1092
|
+
<p class="counter-label">{pageHomeCopy(ctx).demoLabel}</p>
|
|
305
1093
|
<div class="counter-row">
|
|
306
|
-
<button id="counter-btn" type="button" class="btn-counter" onclick={() => count++} aria-label="
|
|
1094
|
+
<button id="counter-btn" type="button" class="btn-counter" onclick={() => count++} aria-label="{pageHomeCopy(ctx).counterAria}">
|
|
307
1095
|
+1
|
|
308
1096
|
</button>
|
|
309
1097
|
<output class="counter-out" for="counter-btn">
|
|
@@ -509,8 +1297,314 @@ const features = [
|
|
|
509
1297
|
color: var(--nx-muted);
|
|
510
1298
|
}
|
|
511
1299
|
</style>
|
|
1300
|
+
`,
|
|
1301
|
+
'src/routes/islands/+page.nx': `---
|
|
1302
|
+
import { pageIslandsCopy } from '$lib/i18n';
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
<script>
|
|
1306
|
+
let demo = $state(0);
|
|
1307
|
+
let doubled = $derived(demo * 2);
|
|
1308
|
+
</script>
|
|
1309
|
+
|
|
1310
|
+
<section class="pres-hero">
|
|
1311
|
+
<p class="pres-kicker">{pageIslandsCopy(ctx).presKicker}</p>
|
|
1312
|
+
<h1 class="pres-title">{pageIslandsCopy(ctx).pageTitle}</h1>
|
|
1313
|
+
<p class="pres-lead">{pageIslandsCopy(ctx).lead}</p>
|
|
1314
|
+
</section>
|
|
1315
|
+
|
|
1316
|
+
<section class="pres-section" aria-labelledby="s1">
|
|
1317
|
+
<h2 id="s1" class="pres-h2">{pageIslandsCopy(ctx).s1h}</h2>
|
|
1318
|
+
<p class="pres-p">
|
|
1319
|
+
{pageIslandsCopy(ctx).s1p}
|
|
1320
|
+
</p>
|
|
1321
|
+
<div class="pres-diagram" role="img" aria-label="{pageIslandsCopy(ctx).diagramAria}">
|
|
1322
|
+
<div class="pres-flow">
|
|
1323
|
+
<span class="pres-box pres-box--srv">{pageIslandsCopy(ctx).flowSrv}</span>
|
|
1324
|
+
<span class="pres-arrow" aria-hidden="true">→</span>
|
|
1325
|
+
<span class="pres-box">{pageIslandsCopy(ctx).flowHtml}</span>
|
|
1326
|
+
<span class="pres-arrow" aria-hidden="true">→</span>
|
|
1327
|
+
<span class="pres-box pres-box--js">{pageIslandsCopy(ctx).flowJs}</span>
|
|
1328
|
+
<span class="pres-arrow" aria-hidden="true">→</span>
|
|
1329
|
+
<span class="pres-box pres-box--ok">{pageIslandsCopy(ctx).flowOk}</span>
|
|
1330
|
+
</div>
|
|
1331
|
+
</div>
|
|
1332
|
+
</section>
|
|
1333
|
+
|
|
1334
|
+
<section class="pres-section" aria-labelledby="s2">
|
|
1335
|
+
<h2 id="s2" class="pres-h2">{pageIslandsCopy(ctx).s2h}</h2>
|
|
1336
|
+
<div class="pres-table-wrap">
|
|
1337
|
+
<table class="pres-table">
|
|
1338
|
+
<thead>
|
|
1339
|
+
<tr>
|
|
1340
|
+
<th>{pageIslandsCopy(ctx).thDirective}</th>
|
|
1341
|
+
<th>{pageIslandsCopy(ctx).thWhen}</th>
|
|
1342
|
+
<th>{pageIslandsCopy(ctx).thUse}</th>
|
|
1343
|
+
</tr>
|
|
1344
|
+
</thead>
|
|
1345
|
+
<tbody>
|
|
1346
|
+
<tr>
|
|
1347
|
+
<td><code>client:load</code></td>
|
|
1348
|
+
<td>{pageIslandsCopy(ctx).r1[1]}</td>
|
|
1349
|
+
<td>{pageIslandsCopy(ctx).r1[2]}</td>
|
|
1350
|
+
</tr>
|
|
1351
|
+
<tr>
|
|
1352
|
+
<td><code>client:idle</code></td>
|
|
1353
|
+
<td>{pageIslandsCopy(ctx).r2[1]}</td>
|
|
1354
|
+
<td>{pageIslandsCopy(ctx).r2[2]}</td>
|
|
1355
|
+
</tr>
|
|
1356
|
+
<tr>
|
|
1357
|
+
<td><code>client:visible</code></td>
|
|
1358
|
+
<td>{pageIslandsCopy(ctx).r3[1]}</td>
|
|
1359
|
+
<td>{pageIslandsCopy(ctx).r3[2]}</td>
|
|
1360
|
+
</tr>
|
|
1361
|
+
<tr>
|
|
1362
|
+
<td><code>client:media="…"</code></td>
|
|
1363
|
+
<td>{pageIslandsCopy(ctx).r4[1]}</td>
|
|
1364
|
+
<td>{pageIslandsCopy(ctx).r4[2]}</td>
|
|
1365
|
+
</tr>
|
|
1366
|
+
<tr>
|
|
1367
|
+
<td><code>server:only</code></td>
|
|
1368
|
+
<td>{pageIslandsCopy(ctx).r5[1]}</td>
|
|
1369
|
+
<td>{pageIslandsCopy(ctx).r5[2]}</td>
|
|
1370
|
+
</tr>
|
|
1371
|
+
</tbody>
|
|
1372
|
+
</table>
|
|
1373
|
+
</div>
|
|
1374
|
+
</section>
|
|
1375
|
+
|
|
1376
|
+
<section class="pres-section" aria-labelledby="s3">
|
|
1377
|
+
<h2 id="s3" class="pres-h2">{pageIslandsCopy(ctx).s3h}</h2>
|
|
1378
|
+
<ol class="pres-list">
|
|
1379
|
+
<li>{pageIslandsCopy(ctx).l1}</li>
|
|
1380
|
+
<li>{pageIslandsCopy(ctx).l2}</li>
|
|
1381
|
+
<li>{pageIslandsCopy(ctx).l3}</li>
|
|
1382
|
+
<li>{pageIslandsCopy(ctx).l4}</li>
|
|
1383
|
+
</ol>
|
|
1384
|
+
</section>
|
|
1385
|
+
|
|
1386
|
+
<section class="pres-section" aria-labelledby="s4">
|
|
1387
|
+
<h2 id="s4" class="pres-h2">{pageIslandsCopy(ctx).s4h}</h2>
|
|
1388
|
+
<p class="pres-p">
|
|
1389
|
+
{pageIslandsCopy(ctx).s4p}
|
|
1390
|
+
</p>
|
|
1391
|
+
<p class="pres-p pres-muted">
|
|
1392
|
+
{pageIslandsCopy(ctx).s4muted}
|
|
1393
|
+
</p>
|
|
1394
|
+
</section>
|
|
1395
|
+
|
|
1396
|
+
<section class="pres-section" aria-labelledby="s5">
|
|
1397
|
+
<h2 id="s5" class="pres-h2">{pageIslandsCopy(ctx).s5h}</h2>
|
|
1398
|
+
<p class="pres-p">{pageIslandsCopy(ctx).s5p}</p>
|
|
1399
|
+
<div class="pres-demo" client:visible>
|
|
1400
|
+
<div class="pres-demo-inner">
|
|
1401
|
+
<button type="button" class="pres-demo-btn" id="pres-demo-btn" onclick={() => demo++} aria-label="{pageIslandsCopy(ctx).demoBtnAria}">
|
|
1402
|
+
+1
|
|
1403
|
+
</button>
|
|
1404
|
+
<div class="pres-demo-out">
|
|
1405
|
+
<span class="pres-demo-val">{demo}</span>
|
|
1406
|
+
<span class="pres-demo-meta">×2 = {doubled}</span>
|
|
1407
|
+
</div>
|
|
1408
|
+
</div>
|
|
1409
|
+
</div>
|
|
1410
|
+
</section>
|
|
1411
|
+
|
|
1412
|
+
<section class="pres-section pres-outro">
|
|
1413
|
+
<h2 class="pres-h2">{pageIslandsCopy(ctx).refh}</h2>
|
|
1414
|
+
<p class="pres-p">
|
|
1415
|
+
{pageIslandsCopy(ctx).refp}
|
|
1416
|
+
<a href="https://nexusjs.dev" target="_blank" rel="noopener noreferrer">nexusjs.dev</a>
|
|
1417
|
+
</p>
|
|
1418
|
+
</section>
|
|
1419
|
+
|
|
1420
|
+
<style>
|
|
1421
|
+
.pres-hero {
|
|
1422
|
+
padding: 1rem 0 2rem;
|
|
1423
|
+
border-bottom: 1px solid var(--nx-border);
|
|
1424
|
+
margin-bottom: 2rem;
|
|
1425
|
+
}
|
|
1426
|
+
.pres-kicker {
|
|
1427
|
+
margin: 0 0 0.5rem;
|
|
1428
|
+
font-size: 0.75rem;
|
|
1429
|
+
font-weight: 600;
|
|
1430
|
+
letter-spacing: 0.1em;
|
|
1431
|
+
text-transform: uppercase;
|
|
1432
|
+
color: var(--nx-accent);
|
|
1433
|
+
}
|
|
1434
|
+
.pres-title {
|
|
1435
|
+
margin: 0 0 0.75rem;
|
|
1436
|
+
font-family: var(--nx-display);
|
|
1437
|
+
font-size: clamp(1.75rem, 4vw, 2.25rem);
|
|
1438
|
+
font-weight: 700;
|
|
1439
|
+
letter-spacing: -0.02em;
|
|
1440
|
+
}
|
|
1441
|
+
.pres-lead {
|
|
1442
|
+
margin: 0;
|
|
1443
|
+
max-width: 40rem;
|
|
1444
|
+
color: var(--nx-muted);
|
|
1445
|
+
line-height: 1.6;
|
|
1446
|
+
font-size: 1.05rem;
|
|
1447
|
+
}
|
|
1448
|
+
.pres-section {
|
|
1449
|
+
margin-bottom: 2.5rem;
|
|
1450
|
+
}
|
|
1451
|
+
.pres-h2 {
|
|
1452
|
+
margin: 0 0 1rem;
|
|
1453
|
+
font-family: var(--nx-display);
|
|
1454
|
+
font-size: 1.2rem;
|
|
1455
|
+
font-weight: 600;
|
|
1456
|
+
}
|
|
1457
|
+
.pres-p {
|
|
1458
|
+
margin: 0 0 1rem;
|
|
1459
|
+
color: var(--nx-text);
|
|
1460
|
+
line-height: 1.65;
|
|
1461
|
+
max-width: 46rem;
|
|
1462
|
+
}
|
|
1463
|
+
.pres-muted {
|
|
1464
|
+
color: var(--nx-muted);
|
|
1465
|
+
font-size: 0.9rem;
|
|
1466
|
+
}
|
|
1467
|
+
.pres-code {
|
|
1468
|
+
font-family: ui-monospace, monospace;
|
|
1469
|
+
font-size: 0.88em;
|
|
1470
|
+
background: var(--nx-surface);
|
|
1471
|
+
padding: 0.12em 0.35em;
|
|
1472
|
+
border-radius: 6px;
|
|
1473
|
+
border: 1px solid var(--nx-border);
|
|
1474
|
+
}
|
|
1475
|
+
.pres-diagram {
|
|
1476
|
+
margin-top: 1.25rem;
|
|
1477
|
+
padding: 1.25rem;
|
|
1478
|
+
border-radius: var(--nx-radius);
|
|
1479
|
+
background: var(--nx-surface);
|
|
1480
|
+
border: 1px solid var(--nx-border);
|
|
1481
|
+
overflow-x: auto;
|
|
1482
|
+
}
|
|
1483
|
+
.pres-flow {
|
|
1484
|
+
display: flex;
|
|
1485
|
+
flex-wrap: wrap;
|
|
1486
|
+
align-items: center;
|
|
1487
|
+
gap: 0.5rem 0.75rem;
|
|
1488
|
+
font-size: 0.9rem;
|
|
1489
|
+
}
|
|
1490
|
+
.pres-box {
|
|
1491
|
+
padding: 0.4rem 0.75rem;
|
|
1492
|
+
border-radius: 8px;
|
|
1493
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1494
|
+
border: 1px solid var(--nx-border);
|
|
1495
|
+
}
|
|
1496
|
+
.pres-box--srv {
|
|
1497
|
+
border-color: rgba(56, 189, 248, 0.35);
|
|
1498
|
+
}
|
|
1499
|
+
.pres-box--js {
|
|
1500
|
+
border-color: rgba(139, 124, 248, 0.45);
|
|
1501
|
+
}
|
|
1502
|
+
.pres-box--ok {
|
|
1503
|
+
border-color: rgba(52, 211, 153, 0.4);
|
|
1504
|
+
}
|
|
1505
|
+
.pres-arrow {
|
|
1506
|
+
color: var(--nx-muted);
|
|
1507
|
+
}
|
|
1508
|
+
.pres-table-wrap {
|
|
1509
|
+
overflow-x: auto;
|
|
1510
|
+
border-radius: var(--nx-radius);
|
|
1511
|
+
border: 1px solid var(--nx-border);
|
|
1512
|
+
}
|
|
1513
|
+
.pres-table {
|
|
1514
|
+
width: 100%;
|
|
1515
|
+
border-collapse: collapse;
|
|
1516
|
+
font-size: 0.9rem;
|
|
1517
|
+
}
|
|
1518
|
+
.pres-table th,
|
|
1519
|
+
.pres-table td {
|
|
1520
|
+
padding: 0.65rem 1rem;
|
|
1521
|
+
text-align: left;
|
|
1522
|
+
border-bottom: 1px solid var(--nx-border);
|
|
1523
|
+
}
|
|
1524
|
+
.pres-table th {
|
|
1525
|
+
background: rgba(0, 0, 0, 0.2);
|
|
1526
|
+
font-weight: 600;
|
|
1527
|
+
font-family: var(--nx-display);
|
|
1528
|
+
}
|
|
1529
|
+
.pres-table tr:last-child td {
|
|
1530
|
+
border-bottom: none;
|
|
1531
|
+
}
|
|
1532
|
+
.pres-table code {
|
|
1533
|
+
font-family: ui-monospace, monospace;
|
|
1534
|
+
font-size: 0.85em;
|
|
1535
|
+
color: var(--nx-accent);
|
|
1536
|
+
}
|
|
1537
|
+
.pres-list {
|
|
1538
|
+
margin: 0;
|
|
1539
|
+
padding-left: 1.25rem;
|
|
1540
|
+
max-width: 46rem;
|
|
1541
|
+
line-height: 1.75;
|
|
1542
|
+
color: var(--nx-text);
|
|
1543
|
+
}
|
|
1544
|
+
.pres-list li {
|
|
1545
|
+
margin-bottom: 0.5rem;
|
|
1546
|
+
}
|
|
1547
|
+
.pres-demo {
|
|
1548
|
+
display: flex;
|
|
1549
|
+
justify-content: flex-start;
|
|
1550
|
+
margin-top: 1rem;
|
|
1551
|
+
}
|
|
1552
|
+
.pres-demo-inner {
|
|
1553
|
+
display: flex;
|
|
1554
|
+
align-items: center;
|
|
1555
|
+
gap: 1.25rem;
|
|
1556
|
+
flex-wrap: wrap;
|
|
1557
|
+
padding: 1.25rem 1.5rem;
|
|
1558
|
+
border-radius: var(--nx-radius);
|
|
1559
|
+
background: linear-gradient(145deg, rgba(139, 124, 248, 0.1), var(--nx-surface));
|
|
1560
|
+
border: 1px solid var(--nx-border);
|
|
1561
|
+
}
|
|
1562
|
+
.pres-demo-btn {
|
|
1563
|
+
min-width: 3rem;
|
|
1564
|
+
min-height: 2.75rem;
|
|
1565
|
+
padding: 0 1rem;
|
|
1566
|
+
border-radius: 10px;
|
|
1567
|
+
border: 1px solid var(--nx-border);
|
|
1568
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1569
|
+
color: var(--nx-text);
|
|
1570
|
+
font-weight: 700;
|
|
1571
|
+
font-family: inherit;
|
|
1572
|
+
cursor: pointer;
|
|
1573
|
+
}
|
|
1574
|
+
.pres-demo-btn:hover {
|
|
1575
|
+
background: rgba(139, 124, 248, 0.2);
|
|
1576
|
+
border-color: rgba(139, 124, 248, 0.45);
|
|
1577
|
+
}
|
|
1578
|
+
.pres-demo-out {
|
|
1579
|
+
display: flex;
|
|
1580
|
+
flex-direction: column;
|
|
1581
|
+
gap: 0.2rem;
|
|
1582
|
+
font-family: var(--nx-display);
|
|
1583
|
+
font-size: 1.35rem;
|
|
1584
|
+
font-weight: 700;
|
|
1585
|
+
font-variant-numeric: tabular-nums;
|
|
1586
|
+
}
|
|
1587
|
+
.pres-demo-meta {
|
|
1588
|
+
font-size: 0.85rem;
|
|
1589
|
+
font-weight: 500;
|
|
1590
|
+
color: var(--nx-muted);
|
|
1591
|
+
}
|
|
1592
|
+
.pres-outro {
|
|
1593
|
+
padding-top: 1rem;
|
|
1594
|
+
border-top: 1px solid var(--nx-border);
|
|
1595
|
+
}
|
|
1596
|
+
.pres-outro a {
|
|
1597
|
+
color: var(--nx-accent);
|
|
1598
|
+
text-decoration: none;
|
|
1599
|
+
}
|
|
1600
|
+
.pres-outro a:hover {
|
|
1601
|
+
text-decoration: underline;
|
|
1602
|
+
}
|
|
1603
|
+
</style>
|
|
512
1604
|
`,
|
|
513
1605
|
'src/routes/blog/+page.nx': `---
|
|
1606
|
+
import { pathWithLang } from '$lib/i18n';
|
|
1607
|
+
|
|
514
1608
|
const posts = [
|
|
515
1609
|
{ slug: "hello-nexus", title: "Hello Nexus", date: "2026-04-03" },
|
|
516
1610
|
{ slug: "islands-arch", title: "Islands Architecture Deep Dive", date: "2026-04-01" },
|
|
@@ -522,7 +1616,7 @@ const posts = [
|
|
|
522
1616
|
<ul class="blog-list">
|
|
523
1617
|
{#each posts as post}
|
|
524
1618
|
<li class="blog-item">
|
|
525
|
-
<a class="blog-link" href="/blog/
|
|
1619
|
+
<a class="blog-link" href="{pathWithLang(ctx, '/blog/' + post.slug)}">{post.title}</a>
|
|
526
1620
|
<time class="blog-date" datetime={post.date}>{post.date}</time>
|
|
527
1621
|
</li>
|
|
528
1622
|
{/each}
|
|
@@ -557,11 +1651,12 @@ const posts = [
|
|
|
557
1651
|
</style>
|
|
558
1652
|
`,
|
|
559
1653
|
'src/routes/blog/[slug]/+page.nx': `---
|
|
1654
|
+
import { pathWithLang } from '$lib/i18n';
|
|
560
1655
|
// Use ctx in the template only — frontmatter runs at module load before ctx exists.
|
|
561
1656
|
---
|
|
562
1657
|
|
|
563
1658
|
<article class="blog-post">
|
|
564
|
-
<a class="blog-back" href="/blog">← Blog</a>
|
|
1659
|
+
<a class="blog-back" href="{pathWithLang(ctx, '/blog')}">← Blog</a>
|
|
565
1660
|
<h1 class="blog-post-title">Post: {ctx.params.slug}</h1>
|
|
566
1661
|
<p class="blog-post-body">Placeholder article for <strong>{ctx.params.slug}</strong>. Wire this to your CMS or <code class="inline-code">load()</code> data.</p>
|
|
567
1662
|
</article>
|
|
@@ -609,7 +1704,7 @@ export const db = {
|
|
|
609
1704
|
return [{ id: 1, name: 'Demo User', email: 'demo@nexusjs.dev' }];
|
|
610
1705
|
},
|
|
611
1706
|
async update(args: { where?: unknown; data: unknown }) {
|
|
612
|
-
return { ...args.data };
|
|
1707
|
+
return { ...(args.data as object) };
|
|
613
1708
|
},
|
|
614
1709
|
async create(args: { data: unknown }) {
|
|
615
1710
|
return args.data;
|
|
@@ -617,15 +1712,22 @@ export const db = {
|
|
|
617
1712
|
},
|
|
618
1713
|
};
|
|
619
1714
|
`,
|
|
620
|
-
'public/favicon.svg':
|
|
621
|
-
<text y="28" font-size="28">◆</text>
|
|
622
|
-
</svg>`,
|
|
1715
|
+
'public/favicon.svg': NEXUS_LOGO_SVG,
|
|
623
1716
|
'.gitignore': `node_modules/
|
|
624
1717
|
.nexus/
|
|
625
1718
|
dist/
|
|
626
1719
|
*.js.map
|
|
627
1720
|
`,
|
|
628
1721
|
};
|
|
1722
|
+
}
|
|
1723
|
+
async function writeProjectFiles(dir, name, template) {
|
|
1724
|
+
const v = getPublishedCliVersion();
|
|
1725
|
+
const range = `^${v}`;
|
|
1726
|
+
const nexusCli = 'node ./node_modules/@nexus_js/cli/dist/bin.js';
|
|
1727
|
+
const ensureDeps = 'node ./scripts/check-node-modules.mjs';
|
|
1728
|
+
const files = template === 'full'
|
|
1729
|
+
? buildFullScaffoldFiles(name, range, nexusCli, ensureDeps)
|
|
1730
|
+
: buildMinimalScaffoldFiles(name, range, nexusCli, ensureDeps);
|
|
629
1731
|
for (const [filepath, content] of Object.entries(files)) {
|
|
630
1732
|
const fullPath = join(dir, filepath);
|
|
631
1733
|
await writeFile(fullPath, content, 'utf-8');
|