@zntc/web 0.1.0 → 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.
@@ -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
@@ -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';