@dokebi/template-inertia 0.1.1 → 0.1.2
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/AI.md +11 -3
- package/README.md +5 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +76 -12
- package/dist/vite-dev.d.ts +13 -0
- package/dist/vite-dev.js +42 -0
- package/package.json +1 -1
package/AI.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# AI 작업 안내 — @dokebi/template-inertia 0.1.
|
|
1
|
+
# AI 작업 안내 — @dokebi/template-inertia 0.1.2
|
|
2
2
|
|
|
3
3
|
Inertia(v3) 서버 어댑터다. 첫 요청은 HTML(+ `script[data-page]` JSON), 이후 `X-Inertia` 요청은 page JSON으로 답한다. Vite manifest를 읽어 스크립트·스타일·버전을 채우고 빌드 파일을 직접 내보낸다. 선택적으로 첫 HTML을 같은 프로세스에서 서버 렌더링(SSR, D-0039)한다. Vue·Vite는 서버 의존성이 아니다(앱의 개발 의존성). Node >=24.0.0, core ^0.2.0, @dokebi/web(peer). 세부는 [README](README.md)다.
|
|
4
4
|
|
|
@@ -21,7 +21,7 @@ npm run build # dist/(브라우저) + dist
|
|
|
21
21
|
| `rootId` | 루트 요소 id(기본 `app`, `^[A-Za-z][A-Za-z0-9_-]*$`) |
|
|
22
22
|
| `version` · `scripts` · `styles` | 자산 버전과 URL(`/`로 시작하는 로컬 URL). 없으면 `INERTIA_VERSION_DEFAULT` 경고 |
|
|
23
23
|
| `vite: { manifest, entry, base? }` | manifest에서 scripts·styles·version 계산. 위 셋과 함께 쓰면 거부 |
|
|
24
|
-
| `ssr: { bundle, failure? }` | 서버 렌더링. `failure`: `fallback`(기본) · `error` |
|
|
24
|
+
| `ssr: { bundle, failure?, entry? }` | 서버 렌더링. `failure`: `fallback`(기본) · `error`. `entry`는 개발 모드에서 쓰는 번들의 원본 모듈(§5.1) |
|
|
25
25
|
|
|
26
26
|
## 3. 페이지 렌더
|
|
27
27
|
|
|
@@ -59,6 +59,14 @@ route.post('/orders', async (request, reply, page) => {
|
|
|
59
59
|
- 브라우저 진입은 `el.hasAttribute('data-server-rendered')`면 `createSSRApp`으로 hydration한다. 서버·브라우저가 같은 레이아웃을 고르도록 페이지 모듈을 바꾸지 말고 `layout` 옵션을 쓴다(동시에 여러 템플릿을 그리므로).
|
|
60
60
|
- `ssrStats()` → `{ renders, failures }`(health·대시보드용). `scope({ ssr: false })`로 앱별로 끌 수 있다.
|
|
61
61
|
|
|
62
|
+
## 5.1 개발 모드(HMR, D-0045)
|
|
63
|
+
|
|
64
|
+
- 환경 변수 `DOKEBI_VITE_DEV=1`(`dokebi dev`가 넣는다)이면 manifest·빌드 없이 **프로젝트의 Vite를 미들웨어 모드로** 한 프로세스에 하나 띄운다. 페이지 스크립트는 `<base>@vite/client`와 `<base><vite.entry>`, 스타일 링크 없음, version `dev`. `<base>*` 요청은 Vite가 답한다.
|
|
65
|
+
- HMR 소켓은 프로세스마다 빈 포트 하나를 따로 쓴다(앱 포트는 @dokebi/websocket이 upgrade를 모두 받으므로). 브라우저는 페이지와 같은 호스트로 붙는다.
|
|
66
|
+
- SSR은 요청마다 `ssrLoadModule(<원본 진입>)`. 원본 진입은 `ssr.entry`(예: `src/apps/admin/views/ssr.js`), 없으면 `vite.entry`와 같은 폴더의 `ssr.js`. 둘 다 없으면 시작 실패.
|
|
67
|
+
- Vite가 프로젝트에 없으면 시작 실패(`npm install --save-dev vite`). Vite는 `localhost`·`*.localhost`·IP 말고 다른 호스트(예: hosts 파일의 `admin.a.com`)를 막으므로, 그런 도메인으로 개발하면 vite.config에 `server.allowedHosts`를 넣는다.
|
|
68
|
+
- 운영(`DOKEBI_VITE_DEV` 없음)에는 영향이 없다. 운영 서버에 이 변수를 넣지 않는다.
|
|
69
|
+
|
|
62
70
|
## 6. 범위(scope)
|
|
63
71
|
|
|
64
72
|
`await api.scope({ title?, rootId?, version?, scripts?, styles?, vite?, ssr?, shared? })` → 그 설정의 렌더러. 정하지 않은 값은 플러그인 옵션, `shared`는 플러그인 shared 뒤에 합쳐진다. @dokebi/app이 앱마다 부른다.
|
|
@@ -71,4 +79,4 @@ route.post('/orders', async (request, reply, page) => {
|
|
|
71
79
|
- deferred·merge·once props, Precognition, session flash는 없다. 있다고 가정하지 않는다.
|
|
72
80
|
- 앱 인스턴스마다 새 플러그인. 전체 drain 뒤에 responder가 닫힌다.
|
|
73
81
|
|
|
74
|
-
근거: `examples/24-template-inertia`(실제 Chrome: CSR·SSR·cluster, 측정), `examples/26-scaffold`(CLI가 만든 SSR 앱의 빌드·서버 렌더), `examples/25-playground`, D-0027·D-0031·D-0039.
|
|
82
|
+
근거: `examples/24-template-inertia`(실제 Chrome: CSR·SSR·cluster, 측정), `examples/26-scaffold`(CLI가 만든 SSR 앱의 빌드·서버 렌더), `examples/25-playground`, D-0027·D-0031·D-0039·D-0045.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @dokebi/template-inertia 0.1.
|
|
1
|
+
# @dokebi/template-inertia 0.1.2
|
|
2
2
|
|
|
3
3
|
Node >=24.0.0, core ^0.2.0, 선택 패키지다(npm 공개). ID는 `dokebi-template-inertia`이며 `dokebi-web`에 의존한다. `@dokebi/web`은 peer 의존성이다(예전의 고정 버전 dependency가 아니다). EJS·Vue·Vite를 서버 패키지의 필수 의존성으로 설치하지 않는다.
|
|
4
4
|
|
|
@@ -64,3 +64,7 @@ Vue 페이지 이동·초기 props는 Inertia, 이후 조회·수정은 `fetch('
|
|
|
64
64
|
- `scope({ ssr })`: 앱마다 번들을 정한다. 생략하면 플러그인 옵션을 따르고 `false`면 끈다. 같은 파일은 한 번만 import한다.
|
|
65
65
|
|
|
66
66
|
측정(예제 24, 같은 프로세스, 순차 400회): CSR 0.20~0.24ms, SSR 0.24~0.28ms(작은 페이지), SSR 450장 뒤 heap +1.1MB.
|
|
67
|
+
|
|
68
|
+
## 개발 모드(HMR, [D-0045](../../docs/decisions/D-0045-inertia-vite-dev.md))
|
|
69
|
+
|
|
70
|
+
`DOKEBI_VITE_DEV=1`(`dokebi dev`가 넣는다)이면 빌드 없이 프로젝트의 Vite를 미들웨어 모드로 띄워 `<base>*`를 Vite가 답하고, 페이지는 `@vite/client`와 원본 진입을 불러온다. `.vue`를 고치면 새로고침 없이 바뀐다(HMR 소켓은 프로세스마다 따로 잡은 빈 포트). SSR은 요청마다 Vite로 원본(`ssr.entry`, 없으면 `vite.entry` 옆 `ssr.js`)을 그린다. 운영에는 영향이 없다.
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface InertiaSsrOptions {
|
|
|
18
18
|
* `error` fails the request, which is easier to notice while developing.
|
|
19
19
|
*/
|
|
20
20
|
readonly failure?: 'fallback' | 'error';
|
|
21
|
+
/**
|
|
22
|
+
* Source module of the bundle (for example `src/apps/admin/views/ssr.js`), rendered through
|
|
23
|
+
* Vite in development (D-0045). Default: `ssr.js` beside the `vite.entry` when it exists.
|
|
24
|
+
*/
|
|
25
|
+
readonly entry?: string;
|
|
21
26
|
}
|
|
22
27
|
/** Counts since start, for health checks and dashboards. */
|
|
23
28
|
export interface InertiaSsrStats {
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { readFile, stat } from 'node:fs/promises';
|
|
3
4
|
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
4
5
|
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import { types } from 'node:util';
|
|
6
7
|
import { definePlugin, providerDependency, } from '@dokebi/core';
|
|
7
8
|
import { InertiaResponder, normalizeInertiaOptions, } from './responder.js';
|
|
9
|
+
import { startViteDev, viteDevRequested } from './vite-dev.js';
|
|
8
10
|
const MANIFEST_MAX_BYTES = 4 * 1024 * 1024;
|
|
9
11
|
function parseVite(input) {
|
|
10
12
|
if (input === null || typeof input !== 'object' || Array.isArray(input) || types.isProxy(input))
|
|
@@ -25,13 +27,14 @@ function parseSsr(input) {
|
|
|
25
27
|
return undefined;
|
|
26
28
|
if (input === null || typeof input !== 'object' || Array.isArray(input) || types.isProxy(input))
|
|
27
29
|
throw new TypeError('Invalid Inertia ssr options');
|
|
28
|
-
const { bundle, failure = 'fallback', ...rest } = input;
|
|
30
|
+
const { bundle, failure = 'fallback', entry, ...rest } = input;
|
|
29
31
|
if (Object.keys(rest).length ||
|
|
30
32
|
typeof bundle !== 'string' ||
|
|
31
33
|
!bundle ||
|
|
32
|
-
(failure !== 'fallback' && failure !== 'error')
|
|
34
|
+
(failure !== 'fallback' && failure !== 'error') ||
|
|
35
|
+
(entry !== undefined && (typeof entry !== 'string' || !entry || entry.includes('..'))))
|
|
33
36
|
throw new TypeError('Invalid Inertia ssr options');
|
|
34
|
-
return Object.freeze({ bundle, failure });
|
|
37
|
+
return Object.freeze({ bundle, failure, ...(entry ? { entry: entry } : {}) });
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
40
|
* Imports the SSR bundle. A missing file or a bundle without `render` stops the start: SSR that
|
|
@@ -194,9 +197,52 @@ class InertiaRuntime {
|
|
|
194
197
|
code: 'INERTIA_VERSION_DEFAULT',
|
|
195
198
|
});
|
|
196
199
|
const server = context.getDependency(web);
|
|
200
|
+
// Development (D-0045): one Vite dev server for every scope, started by the first `vite`.
|
|
201
|
+
const develop = viteDevRequested();
|
|
202
|
+
let devServer;
|
|
203
|
+
const devBases = new Set();
|
|
204
|
+
// Registered now: scopes are created later, from other plugins' setup.
|
|
205
|
+
context.defer(async () => {
|
|
206
|
+
if (devServer)
|
|
207
|
+
await (await devServer).close();
|
|
208
|
+
});
|
|
209
|
+
const viteDev = (base) => {
|
|
210
|
+
if (!devServer) {
|
|
211
|
+
devServer = startViteDev();
|
|
212
|
+
context.logger.log('info', 'Inertia views are served by the Vite dev server', {
|
|
213
|
+
code: 'INERTIA_VITE_DEV',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const current = devServer;
|
|
217
|
+
if (!devBases.has(base)) {
|
|
218
|
+
devBases.add(base);
|
|
219
|
+
// Modules, pre-bundled dependencies and @vite/client, under the Vite base.
|
|
220
|
+
server.route({
|
|
221
|
+
method: 'GET', // Fastify adds HEAD
|
|
222
|
+
url: `${base}*`,
|
|
223
|
+
handler: async (request, reply) => {
|
|
224
|
+
const vite = await current;
|
|
225
|
+
reply.hijack();
|
|
226
|
+
vite.middlewares(request.raw, reply.raw, () => {
|
|
227
|
+
reply.raw.statusCode = 404;
|
|
228
|
+
reply.raw.end();
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return current;
|
|
234
|
+
};
|
|
197
235
|
// Built files are served at their exact URL (no wildcard), once even if apps share chunks.
|
|
198
236
|
const served = new Set();
|
|
199
237
|
const built = async (options) => {
|
|
238
|
+
if (develop) {
|
|
239
|
+
await viteDev(options.base);
|
|
240
|
+
return {
|
|
241
|
+
scripts: [`${options.base}@vite/client`, `${options.base}${options.entry}`],
|
|
242
|
+
styles: [],
|
|
243
|
+
version: 'dev',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
200
246
|
const { files, ...assets } = await viteAssets(options);
|
|
201
247
|
for (const [url, file] of files) {
|
|
202
248
|
if (served.has(url))
|
|
@@ -223,14 +269,31 @@ class InertiaRuntime {
|
|
|
223
269
|
// One import per bundle file, shared by the scopes that name it.
|
|
224
270
|
const bundles = new Map();
|
|
225
271
|
const stats = { renders: 0, failures: 0 };
|
|
226
|
-
const serverRendering = async (ssr) => {
|
|
272
|
+
const serverRendering = async (ssr, vite) => {
|
|
227
273
|
if (!ssr)
|
|
228
274
|
return undefined;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
let render;
|
|
276
|
+
if (develop) {
|
|
277
|
+
// The source module through Vite on every render: an edit shows in the next first HTML.
|
|
278
|
+
const fallback = vite ? `${dirname(vite.entry)}/ssr.js` : undefined;
|
|
279
|
+
const source = ssr.entry ?? (fallback && existsSync(resolve(fallback)) ? fallback : undefined);
|
|
280
|
+
if (!source || !vite)
|
|
281
|
+
throw new Error(`Inertia SSR in development needs the source of ${ssr.bundle}: set ssr.entry`);
|
|
282
|
+
const dev = viteDev(vite.base);
|
|
283
|
+
render = Promise.resolve(async (page) => {
|
|
284
|
+
const module = await (await dev).ssrLoadModule(`/${source}`);
|
|
285
|
+
if (typeof module.render !== 'function')
|
|
286
|
+
throw new Error(`Inertia SSR entry ${source} does not export render(page)`);
|
|
287
|
+
return module.render(page);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const path = resolve(ssr.bundle);
|
|
292
|
+
render = bundles.get(path);
|
|
293
|
+
if (!render) {
|
|
294
|
+
render = loadBundle(ssr.bundle);
|
|
295
|
+
bundles.set(path, render);
|
|
296
|
+
}
|
|
234
297
|
}
|
|
235
298
|
return Object.freeze({
|
|
236
299
|
render: await render,
|
|
@@ -263,7 +326,7 @@ class InertiaRuntime {
|
|
|
263
326
|
location: responder.location.bind(responder),
|
|
264
327
|
});
|
|
265
328
|
};
|
|
266
|
-
const root = api(new InertiaResponder(document, this.shared, await serverRendering(this.settings.ssr)));
|
|
329
|
+
const root = api(new InertiaResponder(document, this.shared, await serverRendering(this.settings.ssr, this.settings.vite)));
|
|
267
330
|
const shared = this.shared;
|
|
268
331
|
return Object.freeze({
|
|
269
332
|
...root,
|
|
@@ -283,14 +346,15 @@ class InertiaRuntime {
|
|
|
283
346
|
const merged = shared && extra
|
|
284
347
|
? async (request) => ({ ...(await shared(request)), ...(await extra(request)) })
|
|
285
348
|
: (extra ?? shared);
|
|
286
|
-
|
|
349
|
+
const scopedVite = vite === undefined ? this.settings.vite : parseVite(vite);
|
|
350
|
+
return api(new InertiaResponder(scoped, merged, await serverRendering(ssr, scopedVite)));
|
|
287
351
|
},
|
|
288
352
|
});
|
|
289
353
|
}
|
|
290
354
|
}
|
|
291
355
|
export const createPlugin = definePlugin({
|
|
292
356
|
name: 'dokebi-template-inertia',
|
|
293
|
-
version: '0.1.
|
|
357
|
+
version: '0.1.2',
|
|
294
358
|
core: '^0.2.0',
|
|
295
359
|
options: parseOptions,
|
|
296
360
|
entry: parseEntry,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The part of Vite's dev server used here. */
|
|
2
|
+
export interface ViteDevServer {
|
|
3
|
+
middlewares(request: unknown, response: unknown, next: (error?: unknown) => void): void;
|
|
4
|
+
ssrLoadModule(url: string): Promise<Record<string, unknown>>;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare const viteDevRequested: () => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Starts the project's Vite (its vite.config.*) in middleware mode. The HMR socket gets a free
|
|
10
|
+
* port of its own: @dokebi/websocket answers every upgrade on the app's HTTP server and closes
|
|
11
|
+
* the ones that are not its routes, so HMR cannot share that server.
|
|
12
|
+
*/
|
|
13
|
+
export declare function startViteDev(root?: string): Promise<ViteDevServer>;
|
package/dist/vite-dev.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Development mode (D-0045): with DOKEBI_VITE_DEV=1 (set by `dokebi dev`) pages load their code
|
|
2
|
+
// from the project's Vite in middleware mode instead of a build, so a .vue edit reaches the
|
|
3
|
+
// browser by hot module replacement. Vite is the project's dev dependency, found from the
|
|
4
|
+
// working directory; this package does not depend on it.
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { createServer } from 'node:net';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
export const viteDevRequested = () => process.env.DOKEBI_VITE_DEV === '1';
|
|
10
|
+
function freePort() {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const server = createServer();
|
|
13
|
+
server.once('error', reject);
|
|
14
|
+
server.listen(0, '127.0.0.1', () => {
|
|
15
|
+
const address = server.address();
|
|
16
|
+
server.close(() => typeof address === 'object' && address ? resolve(address.port) : reject(new Error('port')));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Starts the project's Vite (its vite.config.*) in middleware mode. The HMR socket gets a free
|
|
22
|
+
* port of its own: @dokebi/websocket answers every upgrade on the app's HTTP server and closes
|
|
23
|
+
* the ones that are not its routes, so HMR cannot share that server.
|
|
24
|
+
*/
|
|
25
|
+
export async function startViteDev(root = process.cwd()) {
|
|
26
|
+
let entry;
|
|
27
|
+
try {
|
|
28
|
+
const require = createRequire(join(root, 'package.json'));
|
|
29
|
+
entry = join(dirname(require.resolve('vite/package.json')), 'dist/node/index.js');
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
throw new Error('DOKEBI_VITE_DEV=1 needs Vite in the project (npm install --save-dev vite)', {
|
|
33
|
+
cause,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const vite = (await import(pathToFileURL(entry).href));
|
|
37
|
+
return vite.createServer({
|
|
38
|
+
root,
|
|
39
|
+
appType: 'custom',
|
|
40
|
+
server: { middlewareMode: true, hmr: { port: await freePort() } },
|
|
41
|
+
});
|
|
42
|
+
}
|