@wular/pnext 0.0.21 → 0.0.23
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/package.json +1 -1
- package/src/cli/create.ts +25 -40
- package/src/cli/serve/entry.ts +14 -4
- package/src/cli/serve/pipeline.ts +4 -0
- package/src/cli/serve/ui.ts +1 -1
- package/src/cli/start.ts +9 -0
- package/src/cli/template/README.md +10 -0
- package/src/cli/template/app/client/page.tsx +30 -0
- package/src/cli/template/app/globals.css +322 -0
- package/src/cli/template/app/layout.tsx +33 -0
- package/src/cli/template/app/page.tsx +29 -0
- package/src/cli/template/app/server/page.tsx +27 -0
- package/src/cli/template/gitignore +2 -0
- package/src/cli/template/package.json +18 -0
- package/src/cli/template/pnext.config.ts +2 -0
- package/src/cli/template/tsconfig.json +10 -0
- package/src/cli/typegen.ts +4 -1
- package/src/client/router/runtime.ts +20 -6
- package/src/compat/metadata.ts +44 -2
- package/src/compat/next/config-loader.ts +7 -1
- package/src/compat/register/typed-routes.ts +4 -0
- package/src/compat/typecheck/check.ts +5 -1
- package/src/config.ts +7 -0
- package/src/dev/server.ts +16 -4
- package/src/extensions.ts +7 -1
- package/src/render/metadata.ts +53 -31
- package/src/render/renderer.ts +33 -10
- package/src/resolve/imports.ts +7 -1
- package/src/resolve/scan-facts.ts +7 -1
- package/src/runtime/fetch-host.ts +46 -2
- package/src/types.ts +84 -67
- package/tsconfig.json +1 -1
package/package.json
CHANGED
package/src/cli/create.ts
CHANGED
|
@@ -27,11 +27,11 @@ export async function createApp(dir: string | undefined, options: CreateAppOptio
|
|
|
27
27
|
const name = sanitizePackageName(path.basename(target))
|
|
28
28
|
console.log(`${cyan('▲')} ${bold('pnext create')} ${dim(`— scaffolding ${name}`)}\n`)
|
|
29
29
|
|
|
30
|
-
const files = scaffoldFiles(name, await latestPnextVersionRange('create'))
|
|
30
|
+
const files = await scaffoldFiles(name, await latestPnextVersionRange('create'))
|
|
31
31
|
await Promise.all(
|
|
32
32
|
Object.entries(files).map(([file, source]) => Bun.write(path.join(target, file), source)),
|
|
33
33
|
)
|
|
34
|
-
for (const file of Object.keys(files)) console.log(` ${green('+')} ${dim(file)}`)
|
|
34
|
+
for (const file of Object.keys(files).sort()) console.log(` ${green('+')} ${dim(file)}`)
|
|
35
35
|
|
|
36
36
|
let installed = false
|
|
37
37
|
if (options.install) {
|
|
@@ -112,43 +112,28 @@ function printNextSteps(dir: string, installed: boolean) {
|
|
|
112
112
|
console.log(dim(`└${'─'.repeat(width + 2)}┘`))
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
'pnext.config.ts': `// See node_modules/@wular/pnext/reference/config.md for available options.\nexport default {};\n`,
|
|
138
|
-
'tsconfig.json': `${JSON.stringify(
|
|
139
|
-
{
|
|
140
|
-
extends: '@wular/pnext/config/ts/react.json',
|
|
141
|
-
compilerOptions: { jsxImportSource: 'preact', paths: { '#gen/*': ['./.pnext/types/*'] } },
|
|
142
|
-
include: ['**/*.ts', '**/*.tsx', '.pnext/types/**/*.ts'],
|
|
143
|
-
},
|
|
144
|
-
null,
|
|
145
|
-
2,
|
|
146
|
-
)}\n`,
|
|
147
|
-
'app/layout.tsx': `import type { LayoutProps } from '@wular/pnext';\nimport './globals.css';\n\nexport const metadata = {\n title: '${name}',\n};\n\nexport default function RootLayout({ children }: LayoutProps) {\n return (\n <html>\n <body>{children}</body>\n </html>\n );\n}\n`,
|
|
148
|
-
'app/page.tsx': `import Counter from './counter';\n\nexport default async function Home() {\n return (\n <>\n <h1>Welcome to ${name}</h1>\n <p>Edit app/page.tsx to get started.</p>\n <Counter />\n </>\n );\n}\n`,
|
|
149
|
-
'app/counter.tsx': `'use client';\n\nimport { useState } from 'preact/hooks';\n\nexport default function Counter() {\n const [count, setCount] = useState(0);\n return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;\n}\n`,
|
|
150
|
-
'app/globals.css': `body {\n margin: 0;\n font-family: system-ui, sans-serif;\n}\n`,
|
|
151
|
-
'.gitignore': `node_modules\n.pnext\n`,
|
|
152
|
-
'README.md': `# ${name}\n\nA pnext app.\n\n\`\`\`\nbun install\nbun dev\n\`\`\`\n\nDocs: node_modules/@wular/pnext/reference/getting-started.md\n`,
|
|
115
|
+
// The template is a real runnable app in src/cli/template; files are copied verbatim with the
|
|
116
|
+
// placeholder name swapped, and package.json gets the app name and pnext version stamped in.
|
|
117
|
+
const TEMPLATE_DIR = path.join(import.meta.dir, 'template')
|
|
118
|
+
const PLACEHOLDER_NAME = 'pnext-app'
|
|
119
|
+
|
|
120
|
+
async function scaffoldFiles(name: string, pnextRange: string): Promise<Record<string, string>> {
|
|
121
|
+
const files: Record<string, string> = {}
|
|
122
|
+
for (const entry of await readdir(TEMPLATE_DIR, { recursive: true, withFileTypes: true })) {
|
|
123
|
+
if (!entry.isFile()) continue
|
|
124
|
+
const rel = path.relative(TEMPLATE_DIR, path.join(entry.parentPath, entry.name))
|
|
125
|
+
const source = await Bun.file(path.join(TEMPLATE_DIR, rel)).text()
|
|
126
|
+
// npm strips .gitignore from published tarballs, so the template stores it unprefixed.
|
|
127
|
+
if (rel === 'gitignore') {
|
|
128
|
+
files['.gitignore'] = source
|
|
129
|
+
} else if (rel === 'package.json') {
|
|
130
|
+
const pkg = JSON.parse(source) as { name: string; devDependencies: Record<string, string> }
|
|
131
|
+
pkg.name = name
|
|
132
|
+
pkg.devDependencies['@wular/pnext'] = pnextRange
|
|
133
|
+
files[rel] = `${JSON.stringify(pkg, null, 2)}\n`
|
|
134
|
+
} else {
|
|
135
|
+
files[rel] = source.replaceAll(PLACEHOLDER_NAME, name)
|
|
136
|
+
}
|
|
153
137
|
}
|
|
138
|
+
return files
|
|
154
139
|
}
|
package/src/cli/serve/entry.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import { pathToFileURL } from 'node:url'
|
|
9
9
|
import type { Plugin as EsbuildPlugin } from 'esbuild'
|
|
10
|
+
import { frameworkFingerprint } from '../../runtime/fingerprint'
|
|
10
11
|
|
|
11
12
|
const frameworkRoot = path.resolve(import.meta.dirname, '..', '..', '..')
|
|
12
13
|
|
|
@@ -20,8 +21,17 @@ function frameworkVersion(): string {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Version AND framework fingerprint: the version alone let an edited `src` keep serving the bundle
|
|
26
|
+
* built from the previous source, so a fix appeared to have no effect until the app was rebuilt.
|
|
27
|
+
* The fingerprint is the same one every other compiled artifact is keyed on, taken through the
|
|
28
|
+
* cache-root record so a restart pays one stat per file rather than re-reading the tree.
|
|
29
|
+
*/
|
|
30
|
+
export function serverEntryDir(outPath: string): string {
|
|
31
|
+
// Cache root spelled inline, not imported: this module is parsed before anything else on the
|
|
32
|
+
// start path, and pulling in the module cache for one path.join would drag its whole graph along.
|
|
33
|
+
const generation = frameworkFingerprint(path.join(outPath, 'cache', 'server'))
|
|
34
|
+
return path.join(outPath, 'server', `bundle-${frameworkVersion()}-${generation}`)
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
function serverEntryFile(outPath: string): string {
|
|
@@ -30,7 +40,7 @@ function serverEntryFile(outPath: string): string {
|
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* The prebuilt entry for a project root, or undefined when there is none to
|
|
33
|
-
* use (no build, a custom `outDir`, a version mismatch, or the opt-out). The
|
|
43
|
+
* use (no build, a custom `outDir`, a version or source-generation mismatch, or the opt-out). The
|
|
34
44
|
* caller then falls back to importing src/cli/start.ts directly, so a miss only
|
|
35
45
|
* costs speed. Deliberately cheap: one existsSync and one package.json read,
|
|
36
46
|
* because it runs before anything else on the start path.
|
|
@@ -43,7 +53,7 @@ export function prebuiltServerEntry(root = process.cwd()): string | undefined {
|
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
|
-
* Bundle src/cli/start.ts under `<outPath>/server/bundle-<version>/`. Bare
|
|
56
|
+
* Bundle src/cli/start.ts under `<outPath>/server/bundle-<version>-<fingerprint>/`. Bare
|
|
47
57
|
* specifiers stay external (the framework's deps are already plain JS), so only
|
|
48
58
|
* the framework's own source is inlined. Splitting is on so the graph behind a
|
|
49
59
|
* dynamic import (compat, dev, build) stays in its own chunk — folding it into
|
|
@@ -15,6 +15,7 @@ import type { ResolvedConfig } from '../../config'
|
|
|
15
15
|
import { bootstrapCompat } from '../../compat-bootstrap'
|
|
16
16
|
import { registerServerRuntime, serverBundleTargetForRuntime } from '../../runtime/loader'
|
|
17
17
|
import { importModuleOnce } from '../../runtime/modules'
|
|
18
|
+
import { abortUpstreamFetchOnDisconnect } from '../../runtime/fetch-host'
|
|
18
19
|
import { applyProxyResponse, createProxyRunner } from '../../routing/proxy'
|
|
19
20
|
import {
|
|
20
21
|
isPprShellUpgradeEligible,
|
|
@@ -287,6 +288,9 @@ export async function createRequestHandler(
|
|
|
287
288
|
},
|
|
288
289
|
)
|
|
289
290
|
const response = maybeCloseNodeFetchConnection(finalized, request)
|
|
291
|
+
// Wired before the close-observer wraps the body: the stream a handler
|
|
292
|
+
// passed through from fetch is still the one the abort map knows.
|
|
293
|
+
abortUpstreamFetchOnDisconnect(response.body, request.signal)
|
|
290
294
|
return flushWorkUnitOnClose(response, unit, request.signal)
|
|
291
295
|
} catch (error) {
|
|
292
296
|
// A dropped promise here would close the socket without a response
|
package/src/cli/serve/ui.ts
CHANGED
|
@@ -138,7 +138,7 @@ export function openBrowser(url: string) {
|
|
|
138
138
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
139
139
|
const browser = process.env.BROWSER
|
|
140
140
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
141
|
-
if (browser === 'none' || process.env.CI) return
|
|
141
|
+
if (browser === 'none' || process.env.CI || process.env.NODE_ENV === 'test') return
|
|
142
142
|
|
|
143
143
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
144
144
|
const browserArgs = process.env.BROWSER_ARGS?.split(/\s+/).filter(Boolean) ?? []
|
package/src/cli/start.ts
CHANGED
|
@@ -130,6 +130,15 @@ async function guardUnservableOutputMode(root?: string): Promise<ResolvedConfig>
|
|
|
130
130
|
)
|
|
131
131
|
process.exit(1)
|
|
132
132
|
}
|
|
133
|
+
// Next warns on `next start` with standalone output; its standalone launcher re-execs this path
|
|
134
|
+
// with __NEXT_PRIVATE_STANDALONE_CONFIG set to suppress the warning. Mirror both.
|
|
135
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
136
|
+
if (mode === 'standalone' && !process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) {
|
|
137
|
+
console.warn(
|
|
138
|
+
'"next start" does not work with "output: standalone" configuration. ' +
|
|
139
|
+
'Use "node .next/standalone/server.js" instead.',
|
|
140
|
+
)
|
|
141
|
+
}
|
|
133
142
|
return config
|
|
134
143
|
}
|
|
135
144
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'preact/hooks'
|
|
4
|
+
|
|
5
|
+
export default function ClientPage() {
|
|
6
|
+
const [text, setText] = useState('')
|
|
7
|
+
const words = text.trim() ? text.trim().split(/\s+/).length : 0
|
|
8
|
+
return (
|
|
9
|
+
<div class="page">
|
|
10
|
+
<span class="chip">client component</span>
|
|
11
|
+
<h1>Running in the browser</h1>
|
|
12
|
+
<input
|
|
13
|
+
class="field"
|
|
14
|
+
placeholder="Type something…"
|
|
15
|
+
value={text}
|
|
16
|
+
onInput={e => setText(e.currentTarget.value)}
|
|
17
|
+
/>
|
|
18
|
+
<dl class="panel">
|
|
19
|
+
<div>
|
|
20
|
+
<dt>Characters</dt>
|
|
21
|
+
<dd>{text.length}</dd>
|
|
22
|
+
</div>
|
|
23
|
+
<div>
|
|
24
|
+
<dt>Words</dt>
|
|
25
|
+
<dd>{words}</dd>
|
|
26
|
+
</div>
|
|
27
|
+
</dl>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #ffffff;
|
|
3
|
+
--ink: #111111;
|
|
4
|
+
--muted: #6b6b6b;
|
|
5
|
+
--line: #e4e4e4;
|
|
6
|
+
--card: #f7f7f7;
|
|
7
|
+
--accent: #4f46e5;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #0a0a0a;
|
|
13
|
+
--ink: #ededed;
|
|
14
|
+
--muted: #8f8f8f;
|
|
15
|
+
--line: #262626;
|
|
16
|
+
--card: #141414;
|
|
17
|
+
--accent: #8583ff;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* {
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
background: var(--bg);
|
|
31
|
+
color: var(--ink);
|
|
32
|
+
font-family: system-ui, sans-serif;
|
|
33
|
+
line-height: 1.5;
|
|
34
|
+
overflow-x: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* One quiet accent glow, top center. */
|
|
38
|
+
body::before {
|
|
39
|
+
content: '';
|
|
40
|
+
position: fixed;
|
|
41
|
+
z-index: -1;
|
|
42
|
+
top: -24rem;
|
|
43
|
+
left: 50%;
|
|
44
|
+
width: 64rem;
|
|
45
|
+
height: 48rem;
|
|
46
|
+
transform: translateX(-50%);
|
|
47
|
+
border-radius: 50%;
|
|
48
|
+
background: radial-gradient(
|
|
49
|
+
ellipse,
|
|
50
|
+
color-mix(in srgb, var(--accent) 10%, transparent),
|
|
51
|
+
transparent 70%
|
|
52
|
+
);
|
|
53
|
+
filter: blur(60px);
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.container {
|
|
58
|
+
width: min(72rem, 100% - 4rem);
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.site-header {
|
|
63
|
+
border-bottom: 1px solid var(--line);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.bar {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
padding: 1rem 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.brand {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 0.5rem;
|
|
77
|
+
font-weight: 650;
|
|
78
|
+
letter-spacing: -0.01em;
|
|
79
|
+
text-decoration: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.mark {
|
|
83
|
+
color: var(--accent);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.site-header nav {
|
|
87
|
+
display: flex;
|
|
88
|
+
gap: 1.75rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.site-header nav a {
|
|
92
|
+
color: var(--muted);
|
|
93
|
+
text-decoration: none;
|
|
94
|
+
font-size: 0.92rem;
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
transition: color 130ms ease;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.site-header nav a:hover {
|
|
100
|
+
color: var(--ink);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main {
|
|
104
|
+
flex: 1;
|
|
105
|
+
display: grid;
|
|
106
|
+
align-content: center;
|
|
107
|
+
padding: 3rem 0 5rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@keyframes rise {
|
|
111
|
+
from {
|
|
112
|
+
opacity: 0;
|
|
113
|
+
transform: translateY(10px);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
to {
|
|
117
|
+
opacity: 1;
|
|
118
|
+
transform: translateY(0);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.hero {
|
|
123
|
+
display: grid;
|
|
124
|
+
grid-template-columns: 1.4fr 1fr;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 4rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.hero > * {
|
|
130
|
+
animation: rise 500ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.hero > :last-child {
|
|
134
|
+
animation-delay: 120ms;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@media (max-width: 52rem) {
|
|
138
|
+
.hero {
|
|
139
|
+
grid-template-columns: 1fr;
|
|
140
|
+
gap: 2.5rem;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
h1 {
|
|
145
|
+
margin: 0 0 1rem;
|
|
146
|
+
font-size: clamp(2.2rem, 4.5vw, 3.4rem);
|
|
147
|
+
font-weight: 700;
|
|
148
|
+
letter-spacing: -0.03em;
|
|
149
|
+
line-height: 1.08;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.lede {
|
|
153
|
+
margin: 0 0 2rem;
|
|
154
|
+
color: var(--muted);
|
|
155
|
+
font-size: 1.15rem;
|
|
156
|
+
max-width: 32rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
a {
|
|
160
|
+
color: inherit;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.button-link,
|
|
164
|
+
.field {
|
|
165
|
+
font: inherit;
|
|
166
|
+
font-weight: 500;
|
|
167
|
+
padding: 0.55rem 1.3rem;
|
|
168
|
+
border-radius: 8px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.button-link {
|
|
172
|
+
display: inline-block;
|
|
173
|
+
text-decoration: none;
|
|
174
|
+
background: var(--ink);
|
|
175
|
+
color: var(--bg);
|
|
176
|
+
border: 1px solid var(--ink);
|
|
177
|
+
transition:
|
|
178
|
+
background 150ms ease,
|
|
179
|
+
border-color 150ms ease;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.button-link:hover {
|
|
183
|
+
background: var(--accent);
|
|
184
|
+
border-color: var(--accent);
|
|
185
|
+
color: #ffffff;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.panel {
|
|
189
|
+
margin: 0;
|
|
190
|
+
background: var(--card);
|
|
191
|
+
border: 1px solid var(--line);
|
|
192
|
+
border-radius: 12px;
|
|
193
|
+
max-width: 26rem;
|
|
194
|
+
width: 100%;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.panel > div,
|
|
199
|
+
.panel > a {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
gap: 1.5rem;
|
|
204
|
+
padding: 0.85rem 1.2rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.panel > * + * {
|
|
208
|
+
border-top: 1px solid var(--line);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.panel > a {
|
|
212
|
+
text-decoration: none;
|
|
213
|
+
transition: background 130ms ease;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.panel > a:hover {
|
|
217
|
+
background: color-mix(in srgb, var(--accent) 8%, var(--card));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.panel .path {
|
|
221
|
+
display: block;
|
|
222
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
223
|
+
font-size: 0.92rem;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.panel .desc {
|
|
227
|
+
display: block;
|
|
228
|
+
margin-top: 0.15rem;
|
|
229
|
+
color: var(--muted);
|
|
230
|
+
font-size: 0.85rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.panel .arrow {
|
|
234
|
+
color: var(--muted);
|
|
235
|
+
transition:
|
|
236
|
+
transform 130ms ease,
|
|
237
|
+
color 130ms ease;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.panel > a:hover .arrow {
|
|
241
|
+
color: var(--accent);
|
|
242
|
+
transform: translateX(3px);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.panel dt {
|
|
246
|
+
color: var(--muted);
|
|
247
|
+
font-size: 0.95rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.panel dd {
|
|
251
|
+
margin: 0;
|
|
252
|
+
font-size: 0.92rem;
|
|
253
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
254
|
+
font-variant-numeric: tabular-nums;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.page {
|
|
258
|
+
max-width: 36rem;
|
|
259
|
+
animation: rise 500ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.chip {
|
|
263
|
+
display: inline-block;
|
|
264
|
+
margin-bottom: 1.1rem;
|
|
265
|
+
padding: 0.18rem 0.7rem;
|
|
266
|
+
border-radius: 999px;
|
|
267
|
+
border: 1px solid var(--line);
|
|
268
|
+
background: var(--card);
|
|
269
|
+
color: var(--muted);
|
|
270
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
271
|
+
font-size: 0.72rem;
|
|
272
|
+
text-transform: uppercase;
|
|
273
|
+
letter-spacing: 0.12em;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.page h1 {
|
|
278
|
+
margin-bottom: 1.75rem;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.page .panel {
|
|
282
|
+
margin-top: 1.25rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.field {
|
|
286
|
+
display: block;
|
|
287
|
+
width: 100%;
|
|
288
|
+
max-width: 24rem;
|
|
289
|
+
background: var(--card);
|
|
290
|
+
color: var(--ink);
|
|
291
|
+
border: 1px solid var(--line);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.field:focus {
|
|
295
|
+
border-color: var(--accent);
|
|
296
|
+
outline: none;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.field::placeholder {
|
|
300
|
+
color: var(--muted);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.hint {
|
|
304
|
+
margin: 0.9rem 0 0;
|
|
305
|
+
min-height: 1.5em;
|
|
306
|
+
color: var(--muted);
|
|
307
|
+
font-size: 0.95rem;
|
|
308
|
+
font-variant-numeric: tabular-nums;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
a:focus-visible {
|
|
312
|
+
outline: 2px solid var(--accent);
|
|
313
|
+
outline-offset: 3px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@media (prefers-reduced-motion: reduce) {
|
|
317
|
+
*,
|
|
318
|
+
*::before {
|
|
319
|
+
animation: none !important;
|
|
320
|
+
transition: none !important;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LayoutProps } from '@wular/pnext'
|
|
2
|
+
import './globals.css'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
title: 'pnext-app',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
9
|
+
return (
|
|
10
|
+
<html>
|
|
11
|
+
<body>
|
|
12
|
+
<header class="site-header">
|
|
13
|
+
<div class="container bar">
|
|
14
|
+
<a class="brand" href="/">
|
|
15
|
+
<span class="mark">▲</span> pnext-app
|
|
16
|
+
</a>
|
|
17
|
+
<nav>
|
|
18
|
+
<a href="/server">Server component</a>
|
|
19
|
+
<a href="/client">Client component</a>
|
|
20
|
+
<a href="https://www.pnext.dev/docs" target="_blank" rel="noopener">
|
|
21
|
+
Docs
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://github.com/muzam1l/pnext" target="_blank" rel="noopener">
|
|
24
|
+
GitHub
|
|
25
|
+
</a>
|
|
26
|
+
</nav>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
<main class="container">{children}</main>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default async function Home() {
|
|
2
|
+
return (
|
|
3
|
+
<div class="hero">
|
|
4
|
+
<div>
|
|
5
|
+
<h1>Hello, world.</h1>
|
|
6
|
+
<p class="lede">Your app is up and running.</p>
|
|
7
|
+
<a class="button-link" href="https://www.pnext.dev/docs" target="_blank" rel="noopener">
|
|
8
|
+
Read the docs
|
|
9
|
+
</a>
|
|
10
|
+
</div>
|
|
11
|
+
<nav class="panel">
|
|
12
|
+
<a href="/server">
|
|
13
|
+
<span>
|
|
14
|
+
<span class="path">/server</span>
|
|
15
|
+
<span class="desc">Server component, rendered per request</span>
|
|
16
|
+
</span>
|
|
17
|
+
<span class="arrow">→</span>
|
|
18
|
+
</a>
|
|
19
|
+
<a href="/client">
|
|
20
|
+
<span>
|
|
21
|
+
<span class="path">/client</span>
|
|
22
|
+
<span class="desc">Client component, interactive in the browser</span>
|
|
23
|
+
</span>
|
|
24
|
+
<span class="arrow">→</span>
|
|
25
|
+
</a>
|
|
26
|
+
</nav>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const metadata = {
|
|
2
|
+
title: 'Server component',
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export default async function ServerPage() {
|
|
6
|
+
const renderedAt = new Date().toLocaleTimeString('en-US', { hour12: false })
|
|
7
|
+
return (
|
|
8
|
+
<div class="page">
|
|
9
|
+
<span class="chip">server component</span>
|
|
10
|
+
<h1>Rendered on the server</h1>
|
|
11
|
+
<dl class="panel">
|
|
12
|
+
<div>
|
|
13
|
+
<dt>Rendered at</dt>
|
|
14
|
+
<dd>{renderedAt}</dd>
|
|
15
|
+
</div>
|
|
16
|
+
<div>
|
|
17
|
+
<dt>Runtime</dt>
|
|
18
|
+
<dd>Bun {Bun.version}</dd>
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<dt>Client JS</dt>
|
|
22
|
+
<dd>none</dd>
|
|
23
|
+
</div>
|
|
24
|
+
</dl>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pnext-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "pnext dev",
|
|
6
|
+
"build": "pnext build",
|
|
7
|
+
"start": "pnext start",
|
|
8
|
+
"analyze": "pnext analyze"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"preact": "^10"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@wular/pnext": "latest",
|
|
15
|
+
"typescript": "^5",
|
|
16
|
+
"@types/bun": "^1"
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/cli/typegen.ts
CHANGED
|
@@ -57,7 +57,10 @@ function routeEntry(route: RouteManifestEntry) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async function writeChecks(config: ResolvedConfig, routes: RouteManifestEntry[]) {
|
|
60
|
-
const checksDir =
|
|
60
|
+
const checksDir = config.checksPath
|
|
61
|
+
// Checks used to live under typesPath, which the app's own tsconfig includes; a stale
|
|
62
|
+
// copy there fails the app's compiler on pnext internals, so heal that on every generate.
|
|
63
|
+
await rm(path.join(config.typesPath, 'checks'), { recursive: true, force: true })
|
|
61
64
|
await rm(checksDir, { recursive: true, force: true })
|
|
62
65
|
await ensureDir(checksDir)
|
|
63
66
|
|