@zntc/web 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -18
- package/dist/css/index.d.ts +64 -0
- package/dist/css/index.js +69 -0
- package/dist/dev-controller.d.ts +72 -0
- package/dist/dev-overlay-client.raw.js +311 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +338 -352
- package/dist/inject.d.ts +15 -0
- package/dist/react-refresh-preamble.d.ts +8 -0
- package/dist/style/postcss.d.ts +56 -2
- package/dist/style/sass.d.ts +6 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
# @zntc/web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English · **[한국어](./README_KO.md)**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> ZNTC web platform layer — dev server (HTTP/HTTPS + WebSocket HMR), HMR overlay, postcss / sass / lightningcss CSS pipeline, and dev controller (file watcher + module graph).
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@zntc/web)
|
|
8
|
+
[](https://github.com/ohah/zntc/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
`@zntc/web` powers the app-mode side of [ZNTC](https://github.com/ohah/zntc): the `zntc dev` / `zntc preview` / `zntc build` commands. It adds a dev server with WebSocket-based HMR, an error overlay, a CSS pipeline (postcss / sass / lightningcss), and a dev controller that wires a file watcher to the module graph. The `zntc` CLI loads this package automatically, so you rarely import it yourself.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
6
13
|
|
|
7
14
|
```bash
|
|
8
15
|
bun add -D @zntc/web
|
|
9
|
-
#
|
|
16
|
+
# or
|
|
10
17
|
npm i -D @zntc/web
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
`@zntc/core`
|
|
20
|
+
`@zntc/core` (which ships the native NAPI binary and the `zntc` CLI) is installed automatically as a dependency.
|
|
14
21
|
|
|
15
|
-
### Optional
|
|
22
|
+
### Optional — CSS pipeline
|
|
16
23
|
|
|
17
24
|
```bash
|
|
18
25
|
bun add -D postcss postcss-load-config sass
|
|
19
26
|
```
|
|
20
27
|
|
|
21
|
-
- `postcss` / `postcss-load-config` — `postcss.config.{js,ts}`
|
|
22
|
-
- `sass` — `.scss` / `.sass`
|
|
28
|
+
- `postcss` / `postcss-load-config` — auto-discovers `postcss.config.{js,ts}` for Tailwind and other PostCSS plugins
|
|
29
|
+
- `sass` — handles `.scss` / `.sass` files
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
These are optional dependencies; install only what your project needs.
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Dev server + HMR
|
|
36
|
+
|
|
37
|
+
In app mode, the `zntc` CLI dynamically imports `@zntc/web` for you — installing the package is all that's required:
|
|
27
38
|
|
|
28
39
|
```bash
|
|
29
40
|
bunx zntc dev # dev server + HMR
|
|
@@ -31,18 +42,35 @@ bunx zntc build # production app build
|
|
|
31
42
|
bunx zntc preview # production preview server
|
|
32
43
|
```
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
`zntc dev` serves your app over HTTP (or HTTPS when TLS options are set), pushes Hot Module Replacement updates over WebSocket, and surfaces build errors through the HMR overlay in the browser.
|
|
46
|
+
|
|
47
|
+
### postcss / sass
|
|
48
|
+
|
|
49
|
+
When the optional CSS packages are installed, the CSS pipeline activates automatically:
|
|
50
|
+
|
|
51
|
+
- `postcss.config.{js,ts}` is auto-discovered (Tailwind / PostCSS plugins).
|
|
52
|
+
- `.scss` / `.sass` files are compiled through `sass`.
|
|
53
|
+
- lightningcss handles transforms and minification.
|
|
54
|
+
|
|
55
|
+
No extra configuration is needed beyond installing the relevant packages.
|
|
56
|
+
|
|
57
|
+
### Direct import (advanced)
|
|
58
|
+
|
|
59
|
+
`createAppDevController` is the dev controller's main entry point. It consumes the result of `prepareAppDevSync` from `@zntc/core` and exposes a broad option surface for embedding the dev server in custom tooling. The package also exports a `@zntc/web/css` entry for the CSS pipeline. See the [HMR guide](https://ohah.github.io/zntc) for usage examples.
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
35
62
|
|
|
36
|
-
|
|
63
|
+
📚 **Official docs: <https://ohah.github.io/zntc>**
|
|
37
64
|
|
|
38
|
-
|
|
65
|
+
- Monorepo / source: <https://github.com/ohah/zntc>
|
|
66
|
+
- Config reference and HMR details are part of the main ZNTC documentation.
|
|
39
67
|
|
|
40
|
-
|
|
68
|
+
Related packages:
|
|
41
69
|
|
|
42
|
-
- [@zntc/core](https://npmjs.com/package/@zntc/core) —
|
|
43
|
-
- [@zntc/react-native](https://npmjs.com/package/@zntc/react-native) —
|
|
44
|
-
- [@zntc/vite-plugin](https://npmjs.com/package/@zntc/vite-plugin) —
|
|
70
|
+
- [@zntc/core](https://www.npmjs.com/package/@zntc/core) — transpiler / bundler core
|
|
71
|
+
- [@zntc/react-native](https://www.npmjs.com/package/@zntc/react-native) — React Native platform layer
|
|
72
|
+
- [@zntc/vite-plugin](https://www.npmjs.com/package/@zntc/vite-plugin) — apply the ZNTC transform when using Vite
|
|
45
73
|
|
|
46
|
-
##
|
|
74
|
+
## License
|
|
47
75
|
|
|
48
|
-
MIT
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@zntc/web/css` — ZTS dev/build 의 CSS pipeline plugin (#2538 4-4 PR-2).
|
|
3
|
+
*
|
|
4
|
+
* 사용자가 명시 안 해도 controller 가 default 로 등록 (#2538 4-4 PR-3 에서 wiring).
|
|
5
|
+
* 명시적으로 끄거나 옵션 override 하려면 zntc.config 에 `plugins: [css(...)]` 추가.
|
|
6
|
+
*
|
|
7
|
+
* Vite 의 `vite:css` plugin 과 같은 역할 — postcss.config.js 자동 발견 (Vite 정확
|
|
8
|
+
* 패턴) + `css({ postcss: { plugins } })` 명시 override. 둘 다 지원.
|
|
9
|
+
*
|
|
10
|
+
* 현 PR-2 의 scope = factory + ZntcPlugin shape + .css 파일 onLoad PostCSS 통과
|
|
11
|
+
* (minimal). Sass / CSS Modules 는 follow-up commit.
|
|
12
|
+
*/
|
|
13
|
+
import type { ZntcPlugin } from '@zntc/core';
|
|
14
|
+
export interface CssPluginOptions {
|
|
15
|
+
/** plugin 전체 비활성. 사용자가 `plugins:[css()]` 명시했지만 특정 빌드에서 끄고 싶을 때. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* PostCSS override. 미지정 시 ZTS 가 postcss.config.js 자동 발견 (Vite 패턴).
|
|
19
|
+
* 명시 시 자동 발견 무시하고 해당 plugins 사용.
|
|
20
|
+
*/
|
|
21
|
+
postcss?: {
|
|
22
|
+
plugins?: unknown[];
|
|
23
|
+
options?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* postcss.config 검색의 root. 미지정 시 process.cwd().
|
|
27
|
+
* Vite 의 `css.postcss` 또는 `process.cwd()` 와 동일 의미.
|
|
28
|
+
*/
|
|
29
|
+
root?: string;
|
|
30
|
+
/**
|
|
31
|
+
* postcss-load-config 의 `env` 로 전달 — postcss.config.js 가 factory 형식
|
|
32
|
+
* `({ mode }) => ({...})` 일 때 사용. 미지정 시 `process.env.NODE_ENV` 또는
|
|
33
|
+
* `'development'` (dev safer). Vite 의 `css.postcss` env parity.
|
|
34
|
+
*/
|
|
35
|
+
mode?: 'development' | 'production';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* CSS pipeline plugin factory. ZTS dev / build pipeline 의 `.css` 파일 onLoad 시
|
|
39
|
+
* PostCSS pass 적용 (autoprefixer / preset-env 등 postcss.config 의 plugin들).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* // zero-config — postcss.config.js 자동 발견
|
|
44
|
+
* export default defineConfig({ plugins: [css()] });
|
|
45
|
+
*
|
|
46
|
+
* // override
|
|
47
|
+
* export default defineConfig({
|
|
48
|
+
* plugins: [css({ postcss: { plugins: [autoprefixer()] } })],
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // disable
|
|
52
|
+
* export default defineConfig({ plugins: [css({ disabled: true })] });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* ZntcPlugin + caller-side pre-warm sentinel (RFC #3833 v3 D1a'').
|
|
57
|
+
* @internal — caller (runAppBuild) 만 사용. 사용자 type-import 차단 위해 unexported.
|
|
58
|
+
* sentinel 은 string property 라 runtime 위장 방어 0 — 의도 매치용.
|
|
59
|
+
*/
|
|
60
|
+
type CssZntcPlugin = ZntcPlugin & {
|
|
61
|
+
readonly __cssOptions: CssPluginOptions;
|
|
62
|
+
};
|
|
63
|
+
export declare function css(options?: CssPluginOptions): CssZntcPlugin;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readdirSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
var readFileSync$1 = readFileSync;
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var createRequire$1 = createRequire;
|
|
5
|
+
import { join, resolve, basename, dirname, relative } from "node:path";
|
|
6
|
+
var join$1 = join;
|
|
7
|
+
var resolve$1 = resolve;
|
|
8
|
+
//#region loader.ts
|
|
9
|
+
function requireFromAppRoot(root,fallbackRequire,specifier) {
|
|
10
|
+
const requireFromRoot = createRequire(join(root, "package.json"));
|
|
11
|
+
return requireFromAppOrFallback(requireFromRoot, fallbackRequire, specifier);
|
|
12
|
+
}
|
|
13
|
+
function requireFromAppOrFallback(requireFromApp,fallbackRequire,specifier) {
|
|
14
|
+
try {
|
|
15
|
+
return requireFromApp(specifier);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
const code = err?.code;
|
|
18
|
+
if (code !== "MODULE_NOT_FOUND" && code !== "ERR_MODULE_NOT_FOUND")throw err;
|
|
19
|
+
return fallbackRequire(specifier);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region postcss.ts
|
|
24
|
+
async function loadPostcssConfig(root,configEnv,fallbackRequire,requireBase) {
|
|
25
|
+
const reqBase = requireBase ?? root,postcssrc = requireFromAppRoot(reqBase, fallbackRequire, "postcss-load-config"),postcssModule = requireFromAppRoot(reqBase, fallbackRequire, "postcss"),postcss = postcssModule.default ?? postcssModule,config = await postcssrc({ cwd: root, env: configEnv.mode }, root).catch((err) => {
|
|
26
|
+
if (err?.message?.includes("No PostCSS Config found"))return null;
|
|
27
|
+
throw err;
|
|
28
|
+
});
|
|
29
|
+
if (!config)return null;
|
|
30
|
+
const plugins = config.plugins ?? [];
|
|
31
|
+
if (plugins.length === 0)return null;
|
|
32
|
+
return { postcss, plugins, options: config.options ?? {}, configFile: config.file ?? null };
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region index.ts
|
|
36
|
+
function css(options={}) {
|
|
37
|
+
return { name: "@zntc/web/css", __cssOptions: options, setup(build) {
|
|
38
|
+
if (options.disabled)return;
|
|
39
|
+
build.onLoad({ filter: /(?<!\.module)\.css$/ }, async (args) => {
|
|
40
|
+
const input = readFileSync$1(args.path, "utf8"),root = options.root ?? process.cwd(),fallbackRequire = createRequire$1(import.meta.url),mode = options.mode ?? "production";
|
|
41
|
+
let plugins,postcssOptions,postcssModule;
|
|
42
|
+
const overridePlugins = options.postcss?.plugins;
|
|
43
|
+
if (overridePlugins && overridePlugins.length > 0) {
|
|
44
|
+
try {
|
|
45
|
+
postcssModule = (await import("postcss")).default;
|
|
46
|
+
} catch {
|
|
47
|
+
return { contents: input };
|
|
48
|
+
}
|
|
49
|
+
plugins = overridePlugins;
|
|
50
|
+
postcssOptions = options.postcss?.options ?? {};
|
|
51
|
+
} else {
|
|
52
|
+
let loaded;
|
|
53
|
+
try {
|
|
54
|
+
loaded = await loadPostcssConfig(root, { mode }, fallbackRequire);
|
|
55
|
+
} catch {
|
|
56
|
+
return { contents: input };
|
|
57
|
+
}
|
|
58
|
+
if (!loaded)return { contents: input };
|
|
59
|
+
plugins = loaded.plugins;
|
|
60
|
+
postcssOptions = loaded.options ?? {};
|
|
61
|
+
postcssModule = loaded.postcss;
|
|
62
|
+
}
|
|
63
|
+
const result = await postcssModule(plugins).process(input, { ...postcssOptions, from: args.path, to: args.path });
|
|
64
|
+
return { contents: result.css, map: result.map?.toString(), loader: "css" };
|
|
65
|
+
});
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
68
|
+
export { css };
|
|
69
|
+
//#endregion
|
package/dist/dev-controller.d.ts
CHANGED
|
@@ -9,11 +9,31 @@ export interface AppDevControllerOptions {
|
|
|
9
9
|
base?: string | undefined;
|
|
10
10
|
publicPath?: string | undefined;
|
|
11
11
|
appRoot?: string | undefined;
|
|
12
|
+
/** React Fast Refresh preamble(`/__zntc_react_refresh__`) 을 HTML 에 주입할지.
|
|
13
|
+
* react 미설치(=비-React 앱)면 주입 자체를 스킵(404 노이즈 방지). */
|
|
14
|
+
reactRefresh?: boolean | undefined;
|
|
12
15
|
entryHtml?: string | undefined;
|
|
13
16
|
publicDir?: string | false | undefined;
|
|
14
17
|
envDir?: string | undefined;
|
|
15
18
|
envPrefixes?: readonly string[] | undefined;
|
|
16
19
|
logLevel?: string | undefined;
|
|
20
|
+
/** caller-side pre-warm PostCSS override (RFC #3833 v3 D1a'' Phase 2). 사용자
|
|
21
|
+
* explicit `plugins: [css({postcss:{...override}})]` 의 옵션을 runAppDev 가
|
|
22
|
+
* 추출해 controller 에 전달. prepare 의 `postcssOverride` + afterBundle 의
|
|
23
|
+
* `runPostcssForAppDev` override 둘 다에 동일 값. build path 와 dev path 의
|
|
24
|
+
* PostCSS plugin set 일치 (dev/build divergence 해소). */
|
|
25
|
+
postcssOverride?: {
|
|
26
|
+
plugins: unknown[];
|
|
27
|
+
options?: Record<string, unknown>;
|
|
28
|
+
/** issue #3851 — css({root}) override 가 controller path 로 전달될 때 type
|
|
29
|
+
* 보존 (TS 사용자가 명시적으로 root 줄 수 있도록). runtime 은 prepare →
|
|
30
|
+
* runPostcssIfConfigured 가 동일 field name 으로 read. */
|
|
31
|
+
root?: string;
|
|
32
|
+
} | null;
|
|
33
|
+
/** issue #3857 — css({root}) 단독 명시 시 findPostcssConfig search base.
|
|
34
|
+
* controller 가 prepareAppCssPipelineRoot 의 cssAutoDiscoverRoot 옵션으로
|
|
35
|
+
* forward. monorepo edge (app 이 sub-package, postcss.config 가 monorepo root). */
|
|
36
|
+
cssAutoDiscoverRoot?: string | null;
|
|
17
37
|
}
|
|
18
38
|
export interface AppDevControllerDeps {
|
|
19
39
|
/** dev/build 의 NAPI sync wrapper — core 가 이미 세션에서 init 됐다고 가정. */
|
|
@@ -32,11 +52,36 @@ export interface PrepareAppCssPipelineRootOptions {
|
|
|
32
52
|
dirtyPaths?: readonly string[] | null;
|
|
33
53
|
/** 이전 prep 의 stylePipelineFiles + styleSourceFiles — 구조 변화 없으면 재사용. */
|
|
34
54
|
cache?: PipelineCache | null;
|
|
55
|
+
/** sass @import reverse-dep 맵(tempRoot 기준 path): dep → 그 dep 을 import 한 파일들. dev 세션이
|
|
56
|
+
* 소유하고 prep 마다 갱신/조회한다. dirty 한 sass 가 다른 root scss 의 dep 이면 그 root 도 재컴파일
|
|
57
|
+
* 대상에 transitive 추가 — partial(`_x.scss`) 변경 시 stale CSS 방지 (#71). */
|
|
58
|
+
sassReverseDep?: Map<string, Set<string>> | null;
|
|
59
|
+
/** caller-side pre-warm 으로 전달되는 PostCSS override (RFC #3833 v3 D1a''). 사용자
|
|
60
|
+
* explicit `plugins: [css({ postcss: {...override} })]` 의 옵션을 runAppBuild 가
|
|
61
|
+
* 추출해 prepareAppCssPipelineRoot 로 전달. truthy + plugins.length>0 면 자동 발견
|
|
62
|
+
* skip 후 override 직접 사용. sync dispatcher × async onLoad 충돌 회피용 path. */
|
|
63
|
+
postcssOverride?: {
|
|
64
|
+
plugins: unknown[];
|
|
65
|
+
options?: Record<string, unknown>;
|
|
66
|
+
/** issue #3851 — css({root}) override 의 postcss require base. 미지정 시
|
|
67
|
+
* root 인자 fallback. AppDevControllerOptions.postcssOverride.root 와 동일
|
|
68
|
+
* field — controller 가 forward. */
|
|
69
|
+
root?: string;
|
|
70
|
+
} | null;
|
|
71
|
+
/** issue #3857 — css({root}) 단독 명시 (postcss override 없이) 시 root 가
|
|
72
|
+
* auto-discover path 의 findPostcssConfig 시작 base 로 사용되게 caller 가
|
|
73
|
+
* 전달. monorepo edge: app 이 sub-package, postcss.config 가 monorepo root.
|
|
74
|
+
* 미지정 시 root 인자 사용 — 기존 동작 유지. */
|
|
75
|
+
cssAutoDiscoverRoot?: string | null;
|
|
35
76
|
}
|
|
36
77
|
export interface AppCssPipelineResult {
|
|
37
78
|
tempRoot: string;
|
|
38
79
|
generatedCssAbsPaths: string[];
|
|
39
80
|
cache: PipelineCache;
|
|
81
|
+
/** issue #3850 — PostCSS message 의 deps/dirDeps 보존. afterBundle skipPostcssRun
|
|
82
|
+
* path 가 watch trigger 정합 위해 사용 (tailwind `@source` 같은 dir-dep). */
|
|
83
|
+
postcssDeps?: Set<string>;
|
|
84
|
+
postcssDirDeps?: Set<string>;
|
|
40
85
|
}
|
|
41
86
|
export declare function cleanupPostcssTempRoot(tempRoot: string): void;
|
|
42
87
|
/**
|
|
@@ -47,6 +92,10 @@ export declare function cleanupPostcssTempRoot(tempRoot: string): void;
|
|
|
47
92
|
* Incremental — existingTempRoot + dirtyPaths 가 있으면 그 파일만 cp / 삭제 처리,
|
|
48
93
|
* cache 가 있으면 stylePipelineFiles / styleSourceFiles tree walk 도 skip.
|
|
49
94
|
*/
|
|
95
|
+
/** #71: sass `loadedUrls`(전이 @import, 자기 자신 포함)로 reverse-dep 맵(dep → 그것을 import 한
|
|
96
|
+
* 파일들)을 갱신. self/비-file URL 제외. dep 맵은 누적 — 삭제된 import 의 stale entry 는 과잉
|
|
97
|
+
* 재컴파일(느릴 뿐 correctness 안전)이라 정리하지 않는다. */
|
|
98
|
+
export declare function recordSassReverseDep(reverseDep: Map<string, Set<string>>, file: string, loadedUrls: readonly URL[]): void;
|
|
50
99
|
export declare function prepareAppCssPipelineRoot(root: string, outdir: string, configEnv: ConfigEnv, logLevel: string | undefined, phase: string, deps: AppDevControllerDeps, options?: PrepareAppCssPipelineRootOptions): Promise<AppCssPipelineResult | null>;
|
|
51
100
|
export interface AppDevPrepareResult {
|
|
52
101
|
entryPath: string;
|
|
@@ -56,6 +105,15 @@ export interface AppDevController {
|
|
|
56
105
|
readonly outdir: string;
|
|
57
106
|
readonly base: string;
|
|
58
107
|
prepare(dirtyPaths?: readonly string[] | null): Promise<AppDevPrepareResult>;
|
|
108
|
+
/**
|
|
109
|
+
* issue #3861 follow-up — drain (fs.watch) 의 CSS incremental path 가 PostCSS
|
|
110
|
+
* 재실행 없이 tempRoot 만 raw root 와 동기시키기 위한 minimal sync. prepare 가
|
|
111
|
+
* full pipeline (sync + PostCSS reprocess) 라 단일 CSS modify 시 전체 .css
|
|
112
|
+
* reprocess 회귀. PostCSS 는 afterBundle 의 changedPath 분기가 incremental
|
|
113
|
+
* 처리 — sync 만 충분. dirtyPaths 가 modify 면 cpSync, delete 면 rmSync
|
|
114
|
+
* (sync flow 는 prepare 와 동일 invariant).
|
|
115
|
+
*/
|
|
116
|
+
syncDirty(dirtyPaths: readonly string[]): void;
|
|
59
117
|
afterBundle(options?: {
|
|
60
118
|
changedPath?: string | null;
|
|
61
119
|
}): Promise<{
|
|
@@ -65,9 +123,23 @@ export interface AppDevController {
|
|
|
65
123
|
processed: number;
|
|
66
124
|
}>;
|
|
67
125
|
injectBundleCssLinks(bundleResult: BundleResult): void;
|
|
126
|
+
/**
|
|
127
|
+
* #3813 — outdir 의 `.css` 파일을 file system 스캔해 HTML `<link>` 주입.
|
|
128
|
+
* `injectBundleCssLinks` 가 bundleResult 를 받는 것과 달리 native watch onRebuild 의
|
|
129
|
+
* graphChanged 분기처럼 bundleResult 가 없는 경로용. JS 변경이 새 CSS import 추가했을 때
|
|
130
|
+
* stale `<link>` 회귀 가드.
|
|
131
|
+
*/
|
|
132
|
+
injectBundleCssLinksFromOutdir(): void;
|
|
68
133
|
isPostcssConfig(absPath: string): boolean;
|
|
69
134
|
isCssOnlyChange(absPath: string): boolean;
|
|
70
135
|
isSassOnlyChange(absPath: string): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* #3801 — drain else 분기의 CSS-derived 판정 단일 소스. inline literal endsWith 가
|
|
138
|
+
* `.less` 같은 미지원 확장자나 `.styl/.pcss` 누락으로 drift 하던 회귀 방지. CSS / Sass /
|
|
139
|
+
* postcss config / CSS Module / Sass Module 등 native watch graph 밖이라 incremental
|
|
140
|
+
* update 가 트리거되지 않는 변경을 cover.
|
|
141
|
+
*/
|
|
142
|
+
isCssLikeChange(absPath: string): boolean;
|
|
71
143
|
rebuildScssIncremental(absPath: string): Promise<string | null>;
|
|
72
144
|
hrefFor(absPath: string): string;
|
|
73
145
|
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// zntc dev overlay client — single source of truth (src/server/dev_overlay_client.js).
|
|
2
|
+
// Zig dev server (src/server/dev_server.zig) 는 @embedFile 로, @zntc/web 의 JS
|
|
3
|
+
// dev server 는 readFileSync 로 raw mirror (packages/web/runtime/dev-overlay-client.raw.js)
|
|
4
|
+
// 를 읽어 아래 sentinel 들을 @zntc/server/protocol 의 실제 값으로 replaceAll 한 뒤
|
|
5
|
+
// 브라우저로 보낸다. raw 그대로는 동작하지 않음 — 반드시 치환 후 송신.
|
|
6
|
+
//
|
|
7
|
+
// Sentinel 들은 모두 `__ZNTC_HMR_*__` 패턴. 신규 추가 시 dev_server.zig 의
|
|
8
|
+
// substituteOverlayPlaceholders 와 packages/web/runtime/dev-overlay-client.mjs
|
|
9
|
+
// 양쪽 치환 표를 같이 갱신해야 한다.
|
|
10
|
+
|
|
11
|
+
const ZNTC_HMR_WS_PATH = "__ZNTC_HMR_WS_PATH__";
|
|
12
|
+
const ZNTC_HMR_MSG_ERROR = "__ZNTC_HMR_MSG_ERROR__";
|
|
13
|
+
const ZNTC_HMR_MSG_CLEAR_ERROR = "__ZNTC_HMR_MSG_CLEAR_ERROR__";
|
|
14
|
+
const ZNTC_HMR_MSG_UPDATE_START = "__ZNTC_HMR_MSG_UPDATE_START__";
|
|
15
|
+
const ZNTC_HMR_MSG_UPDATE = "__ZNTC_HMR_MSG_UPDATE__";
|
|
16
|
+
const ZNTC_HMR_MSG_UPDATE_DONE = "__ZNTC_HMR_MSG_UPDATE_DONE__";
|
|
17
|
+
const ZNTC_HMR_MSG_FULL_RELOAD = "__ZNTC_HMR_MSG_FULL_RELOAD__";
|
|
18
|
+
const ZNTC_HMR_MSG_CSS_UPDATE = "__ZNTC_HMR_MSG_CSS_UPDATE__";
|
|
19
|
+
|
|
20
|
+
const socketProtocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
21
|
+
let overlay = null;
|
|
22
|
+
let closeOverlayOnEsc = null;
|
|
23
|
+
function hideOverlay() {
|
|
24
|
+
if (closeOverlayOnEsc) document.removeEventListener("keydown", closeOverlayOnEsc);
|
|
25
|
+
closeOverlayOnEsc = null;
|
|
26
|
+
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
27
|
+
overlay = null;
|
|
28
|
+
}
|
|
29
|
+
function normalizeErrors(errors) {
|
|
30
|
+
if (!Array.isArray(errors) || errors.length === 0) {
|
|
31
|
+
return [{ file: "", message: "Unknown build error" }];
|
|
32
|
+
}
|
|
33
|
+
return errors.map(function(error) {
|
|
34
|
+
if (typeof error === "string") return { file: "", message: error };
|
|
35
|
+
return {
|
|
36
|
+
file: error && typeof error.file === "string" ? error.file : "",
|
|
37
|
+
message: error && typeof error.message === "string" ? error.message : String(error)
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function normalizeRuntimeError(error, file) {
|
|
42
|
+
if (error && typeof error.stack === "string" && error.stack) {
|
|
43
|
+
return { file: file || "", message: error.stack };
|
|
44
|
+
}
|
|
45
|
+
if (error && typeof error.message === "string" && error.message) {
|
|
46
|
+
const name = typeof error.name === "string" && error.name ? error.name : "Error";
|
|
47
|
+
return { file: file || "", message: name + ": " + error.message };
|
|
48
|
+
}
|
|
49
|
+
return { file: file || "", message: String(error || "Unknown runtime error") };
|
|
50
|
+
}
|
|
51
|
+
const sourceMapCache = new Map();
|
|
52
|
+
const sourceMapVlqChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
53
|
+
function displaySourceName(source) {
|
|
54
|
+
if (!source) return "";
|
|
55
|
+
const clean = String(source).split("?")[0].split("#")[0];
|
|
56
|
+
const slash = Math.max(clean.lastIndexOf("/"), clean.lastIndexOf("\\"));
|
|
57
|
+
return slash >= 0 ? clean.slice(slash + 1) : clean;
|
|
58
|
+
}
|
|
59
|
+
function decodeSourceMapVlq(segment) {
|
|
60
|
+
const values = [];
|
|
61
|
+
let result = 0;
|
|
62
|
+
let shift = 0;
|
|
63
|
+
for (const ch of segment) {
|
|
64
|
+
let digit = sourceMapVlqChars.indexOf(ch);
|
|
65
|
+
if (digit < 0) return values;
|
|
66
|
+
const continuation = digit & 32;
|
|
67
|
+
digit &= 31;
|
|
68
|
+
result += digit << shift;
|
|
69
|
+
if (continuation) {
|
|
70
|
+
shift += 5;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const negative = result & 1;
|
|
74
|
+
const value = result >> 1;
|
|
75
|
+
values.push(negative ? -value : value);
|
|
76
|
+
result = 0;
|
|
77
|
+
shift = 0;
|
|
78
|
+
}
|
|
79
|
+
return values;
|
|
80
|
+
}
|
|
81
|
+
function parseSourceMapMappings(map) {
|
|
82
|
+
if (map.__zntcParsedMappings) return map.__zntcParsedMappings;
|
|
83
|
+
let source = 0;
|
|
84
|
+
let originalLine = 0;
|
|
85
|
+
let originalColumn = 0;
|
|
86
|
+
let name = 0;
|
|
87
|
+
const parsed = [];
|
|
88
|
+
for (const line of String(map.mappings || "").split(";")) {
|
|
89
|
+
let generatedColumn = 0;
|
|
90
|
+
const segments = [];
|
|
91
|
+
for (const segment of line.split(",")) {
|
|
92
|
+
if (!segment) continue;
|
|
93
|
+
const values = decodeSourceMapVlq(segment);
|
|
94
|
+
if (values.length === 0) continue;
|
|
95
|
+
generatedColumn += values[0];
|
|
96
|
+
if (values.length >= 4) {
|
|
97
|
+
source += values[1];
|
|
98
|
+
originalLine += values[2];
|
|
99
|
+
originalColumn += values[3];
|
|
100
|
+
if (values.length >= 5) name += values[4];
|
|
101
|
+
segments.push({ generatedColumn, source, originalLine, originalColumn });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
parsed.push(segments);
|
|
105
|
+
}
|
|
106
|
+
Object.defineProperty(map, "__zntcParsedMappings", { value: parsed });
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
function findOriginalPosition(map, line, column) {
|
|
110
|
+
const segments = parseSourceMapMappings(map)[line - 1];
|
|
111
|
+
if (!segments || segments.length === 0) return null;
|
|
112
|
+
let lo = 0;
|
|
113
|
+
let hi = segments.length - 1;
|
|
114
|
+
let best = null;
|
|
115
|
+
while (lo <= hi) {
|
|
116
|
+
const mid = (lo + hi) >> 1;
|
|
117
|
+
const segment = segments[mid];
|
|
118
|
+
if (segment.generatedColumn <= column) {
|
|
119
|
+
best = segment;
|
|
120
|
+
lo = mid + 1;
|
|
121
|
+
} else {
|
|
122
|
+
hi = mid - 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
best = best || segments[0];
|
|
126
|
+
const source = map.sources && map.sources[best.source];
|
|
127
|
+
if (!source) return null;
|
|
128
|
+
const columnOffset = Math.max(0, column - best.generatedColumn);
|
|
129
|
+
return {
|
|
130
|
+
source: displaySourceName(source),
|
|
131
|
+
line: best.originalLine + 1,
|
|
132
|
+
column: best.originalColumn + columnOffset,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function loadSourceMapForGeneratedUrl(url) {
|
|
136
|
+
const generatedUrl = new URL(url, location.href).href;
|
|
137
|
+
if (sourceMapCache.has(generatedUrl)) return sourceMapCache.get(generatedUrl);
|
|
138
|
+
async function safeJson(response) {
|
|
139
|
+
try { return await response.json(); } catch (_) { return null; }
|
|
140
|
+
}
|
|
141
|
+
const promise = (async function() {
|
|
142
|
+
const direct = await fetch(generatedUrl + ".map", { cache: "no-store" }).catch(function() { return null; });
|
|
143
|
+
if (direct && direct.ok) return safeJson(direct);
|
|
144
|
+
const jsResponse = await fetch(generatedUrl, { cache: "no-store" }).catch(function() { return null; });
|
|
145
|
+
if (!jsResponse || !jsResponse.ok) return null;
|
|
146
|
+
const code = await jsResponse.text();
|
|
147
|
+
const match =
|
|
148
|
+
code.match(/\/\/[#@]\s*sourceMappingURL=([^\n\r]+)/) ||
|
|
149
|
+
code.match(/\/\*[#@]\s*sourceMappingURL=([^*]+)\*\//);
|
|
150
|
+
if (!match) return null;
|
|
151
|
+
const ref = match[1].trim();
|
|
152
|
+
if (ref.startsWith("data:")) {
|
|
153
|
+
const comma = ref.indexOf(",");
|
|
154
|
+
if (comma < 0) return null;
|
|
155
|
+
const meta = ref.slice(0, comma);
|
|
156
|
+
const data = ref.slice(comma + 1);
|
|
157
|
+
try {
|
|
158
|
+
const json = meta.includes(";base64") ? atob(data) : decodeURIComponent(data);
|
|
159
|
+
return JSON.parse(json);
|
|
160
|
+
} catch (_) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const mapResponse = await fetch(new URL(ref, generatedUrl).href, { cache: "no-store" }).catch(function() { return null; });
|
|
165
|
+
return mapResponse && mapResponse.ok ? safeJson(mapResponse) : null;
|
|
166
|
+
})();
|
|
167
|
+
sourceMapCache.set(generatedUrl, promise);
|
|
168
|
+
return promise;
|
|
169
|
+
}
|
|
170
|
+
async function mapGeneratedLocation(url, line, column) {
|
|
171
|
+
const map = await loadSourceMapForGeneratedUrl(url);
|
|
172
|
+
return map ? findOriginalPosition(map, line, column) : null;
|
|
173
|
+
}
|
|
174
|
+
async function mapLocationText(text) {
|
|
175
|
+
if (!text) return text;
|
|
176
|
+
const match = String(text).match(/(https?:\/\/[^\s)]+):(\d+):(\d+)/);
|
|
177
|
+
if (!match) return text;
|
|
178
|
+
const mapped = await mapGeneratedLocation(match[1], Number(match[2]), Number(match[3]));
|
|
179
|
+
if (!mapped) return text;
|
|
180
|
+
return String(text).replace(match[0], mapped.source + ":" + mapped.line + ":" + mapped.column);
|
|
181
|
+
}
|
|
182
|
+
async function mapStackTrace(stack) {
|
|
183
|
+
if (typeof stack !== "string") return stack;
|
|
184
|
+
const lines = await Promise.all(stack.split("\n").map(mapLocationText));
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
}
|
|
187
|
+
async function normalizeRuntimeErrorWithSourceMap(error, file) {
|
|
188
|
+
const item = normalizeRuntimeError(error, file);
|
|
189
|
+
item.file = await mapLocationText(item.file);
|
|
190
|
+
item.message = await mapStackTrace(item.message);
|
|
191
|
+
return item;
|
|
192
|
+
}
|
|
193
|
+
async function showRuntimeOverlay(error, file) {
|
|
194
|
+
let item;
|
|
195
|
+
try {
|
|
196
|
+
item = await normalizeRuntimeErrorWithSourceMap(error, file);
|
|
197
|
+
} catch (_) {
|
|
198
|
+
item = normalizeRuntimeError(error, file);
|
|
199
|
+
}
|
|
200
|
+
showOverlay([item], "Runtime Error");
|
|
201
|
+
}
|
|
202
|
+
function showOverlay(errors, titleText = "Build Error") {
|
|
203
|
+
hideOverlay();
|
|
204
|
+
const items = normalizeErrors(errors);
|
|
205
|
+
overlay = document.createElement("div");
|
|
206
|
+
overlay.id = "zntc-error-overlay";
|
|
207
|
+
const root = overlay.attachShadow({ mode: "open" });
|
|
208
|
+
const style = document.createElement("style");
|
|
209
|
+
style.textContent = ":host{position:fixed;inset:0;z-index:2147483647;display:block;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--red:#fb7185;--text:#f8fafc;--blue:#93c5fd;--window:#181818;}" +
|
|
210
|
+
".backdrop{position:fixed;inset:0;overflow:auto;padding:32px;box-sizing:border-box;background:rgba(0,0,0,.66);font:14px/1.5 var(--font);color:var(--text);}" +
|
|
211
|
+
".window{max-width:980px;margin:0 auto;background:var(--window);border-top:8px solid var(--red);border-radius:6px 6px 8px 8px;box-shadow:0 19px 38px rgba(0,0,0,.30),0 15px 12px rgba(0,0,0,.22);overflow:hidden;}" +
|
|
212
|
+
".header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:18px 20px;border-bottom:1px solid rgba(255,255,255,.12);}" +
|
|
213
|
+
".title{font-size:18px;font-weight:700;color:#fecdd3;}" +
|
|
214
|
+
".close{width:30px;height:30px;border:1px solid rgba(255,255,255,.25);border-radius:4px;background:#111827;color:var(--text);cursor:pointer;font:18px/1 var(--font);}" +
|
|
215
|
+
".card{padding:18px 20px;border-top:1px solid rgba(255,255,255,.08);}" +
|
|
216
|
+
".file{margin-bottom:10px;color:var(--blue);word-break:break-all;}" +
|
|
217
|
+
".message{margin:0;white-space:pre-wrap;color:#fff;word-break:break-word;font:14px/1.5 var(--font);}";
|
|
218
|
+
const backdrop = document.createElement("div");
|
|
219
|
+
backdrop.className = "backdrop";
|
|
220
|
+
const panel = document.createElement("div");
|
|
221
|
+
panel.className = "window";
|
|
222
|
+
panel.onclick = function(event) { event.stopPropagation(); };
|
|
223
|
+
const header = document.createElement("div");
|
|
224
|
+
header.className = "header";
|
|
225
|
+
const title = document.createElement("div");
|
|
226
|
+
title.className = "title";
|
|
227
|
+
title.textContent = titleText;
|
|
228
|
+
const close = document.createElement("button");
|
|
229
|
+
close.type = "button";
|
|
230
|
+
close.textContent = "x";
|
|
231
|
+
close.className = "close";
|
|
232
|
+
close.setAttribute("aria-label", "Close error overlay");
|
|
233
|
+
close.onclick = hideOverlay;
|
|
234
|
+
header.appendChild(title);
|
|
235
|
+
header.appendChild(close);
|
|
236
|
+
panel.appendChild(header);
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
const card = document.createElement("div");
|
|
239
|
+
card.className = "card";
|
|
240
|
+
if (item.file) {
|
|
241
|
+
const file = document.createElement("div");
|
|
242
|
+
file.className = "file";
|
|
243
|
+
file.textContent = item.file;
|
|
244
|
+
card.appendChild(file);
|
|
245
|
+
}
|
|
246
|
+
const message = document.createElement("pre");
|
|
247
|
+
message.className = "message";
|
|
248
|
+
message.textContent = item.message;
|
|
249
|
+
card.appendChild(message);
|
|
250
|
+
panel.appendChild(card);
|
|
251
|
+
}
|
|
252
|
+
backdrop.appendChild(panel);
|
|
253
|
+
root.appendChild(style);
|
|
254
|
+
root.appendChild(backdrop);
|
|
255
|
+
closeOverlayOnEsc = function(event) {
|
|
256
|
+
if (event.key === "Escape" || event.code === "Escape") hideOverlay();
|
|
257
|
+
};
|
|
258
|
+
document.addEventListener("keydown", closeOverlayOnEsc);
|
|
259
|
+
(document.body || document.documentElement).appendChild(overlay);
|
|
260
|
+
}
|
|
261
|
+
globalThis.__zntc_show_error_overlay = showOverlay;
|
|
262
|
+
globalThis.__zntc_clear_error_overlay = hideOverlay;
|
|
263
|
+
if (!globalThis.__zntc_runtime_listeners_attached) {
|
|
264
|
+
globalThis.__zntc_runtime_listeners_attached = true;
|
|
265
|
+
window.addEventListener("error", function(event) {
|
|
266
|
+
const file = event.filename ? event.filename + ":" + event.lineno + ":" + event.colno : "";
|
|
267
|
+
showRuntimeOverlay(event.error || event.message, file);
|
|
268
|
+
});
|
|
269
|
+
window.addEventListener("unhandledrejection", function(event) {
|
|
270
|
+
showRuntimeOverlay(event.reason, "");
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const socket = new WebSocket(socketProtocol + "//" + location.host + ZNTC_HMR_WS_PATH);
|
|
274
|
+
socket.addEventListener("message", function(event) {
|
|
275
|
+
const msg = JSON.parse(event.data);
|
|
276
|
+
if (msg.type === ZNTC_HMR_MSG_ERROR) { showOverlay(msg.errors); return; }
|
|
277
|
+
if (msg.type === ZNTC_HMR_MSG_CLEAR_ERROR) { hideOverlay(); return; }
|
|
278
|
+
if (msg.type === ZNTC_HMR_MSG_UPDATE_START) return;
|
|
279
|
+
if (msg.type === ZNTC_HMR_MSG_UPDATE_DONE) { hideOverlay(); return; }
|
|
280
|
+
if (msg.type === ZNTC_HMR_MSG_FULL_RELOAD) { hideOverlay(); location.reload(); return; }
|
|
281
|
+
if (msg.type === ZNTC_HMR_MSG_UPDATE) {
|
|
282
|
+
hideOverlay();
|
|
283
|
+
if (typeof __zntc_apply_update === "function") __zntc_apply_update(msg.modules);
|
|
284
|
+
else location.reload();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (msg.type === ZNTC_HMR_MSG_CSS_UPDATE) {
|
|
288
|
+
hideOverlay();
|
|
289
|
+
const targetPath = msg.href || msg.file;
|
|
290
|
+
const stamp = msg.timestamp || Date.now();
|
|
291
|
+
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
|
|
292
|
+
let updated = false;
|
|
293
|
+
for (const link of links) {
|
|
294
|
+
const href = link.getAttribute("href");
|
|
295
|
+
if (!href) continue;
|
|
296
|
+
const current = new URL(href, location.href);
|
|
297
|
+
const target = new URL(targetPath || current.pathname, location.href);
|
|
298
|
+
if (targetPath && current.pathname !== target.pathname) continue;
|
|
299
|
+
const next = new URL(current.href);
|
|
300
|
+
next.searchParams.set("t", String(stamp));
|
|
301
|
+
const replacement = link.cloneNode();
|
|
302
|
+
replacement.href = next.href;
|
|
303
|
+
replacement.onload = function() { link.remove(); };
|
|
304
|
+
replacement.onerror = function() { location.reload(); };
|
|
305
|
+
link.after(replacement);
|
|
306
|
+
updated = true;
|
|
307
|
+
}
|
|
308
|
+
if (!updated) location.reload();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { APP_DEV_HMR_CLIENT_PATH, APP_DEV_HMR_WS_PATH, type BunHmrClient, createHmrChannel, createWatcher, type CreateWatcherOptions, HMR_MSG, type HmrChannel, type HmrConnectedMessage, type HmrCssUpdateMessage, type HmrError, type HmrErrorMessage, type HmrFullReloadMessage, type HmrMessage, type HmrMessageType, type WatchEventType, type WatchFn, type WatchListener, type WatcherHandle, type WatcherInstance, } from '@zntc/server';
|
|
2
|
-
export { type BundleResult, injectAppDevBundleCssLinks, injectAppDevHmrClient, injectAppDevPipelineCssLinks, injectIntoDevHtml, } from './inject.ts';
|
|
1
|
+
export { APP_DEV_HMR_CLIENT_PATH, APP_DEV_HMR_WS_PATH, APP_DEV_REACT_REFRESH_PATH, broadcastRebuildEvent, type BunHmrClient, createHmrChannel, createWatcher, type CreateWatcherOptions, HMR_MSG, type HmrChannel, type HmrConnectedMessage, type HmrCssUpdateMessage, type HmrError, type HmrErrorMessage, type HmrFullReloadMessage, type HmrMessage, type HmrMessageType, type HmrUpdateDoneMessage, type HmrUpdateMessage, type HmrUpdateModule, type HmrUpdateStartMessage, type RebuildBroadcastOutcome, type RebuildEventLike, type WatchEventType, type WatchFn, type WatchListener, type WatcherHandle, type WatcherInstance, } from '@zntc/server';
|
|
2
|
+
export { type BundleResult, injectAppDevBundleCssLinks, injectAppDevBundleCssLinksFromOutdir, injectAppDevHmrClient, injectAppDevPipelineCssLinks, injectAppDevReactRefreshPreamble, injectIntoDevHtml, } from './inject.ts';
|
|
3
|
+
export { buildReactRefreshPreamble } from './react-refresh-preamble.ts';
|
|
3
4
|
export { DEFAULT_HTML_ENV_PREFIX, type TransformHtmlEnvResult, applyHtmlEnvTokens, transformHtmlEnvTokens, } from './html-env.ts';
|
|
4
5
|
export { joinUrl } from './url.ts';
|
|
5
6
|
export { isCssIdent, isCssIdentStart, skipCssString, skipCssUrl, startsWithCssIdent, } from './style/css-parser.ts';
|