@zntc/core 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/bin/cli-flags.mjs +8 -0
- package/bin/rn-dev-input.mjs +6 -0
- package/bin/zntc.mjs +789 -45
- package/dist/core/index.d.cts +260 -3
- package/dist/core/index.d.ts +260 -3
- package/dist/core/src/schema-allowlists.d.ts +1 -1
- package/dist/index.cjs +231 -44
- package/dist/index.js +252 -33
- package/package.json +11 -11
package/bin/zntc.mjs
CHANGED
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
* Watch/Serve는 JS 레이어에서 구현.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
mkdirSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from 'node:fs';
|
|
11
20
|
import { resolve, dirname, basename, extname, join, sep } from 'node:path';
|
|
12
21
|
import { createServer } from 'node:http';
|
|
13
22
|
import { createServer as createHttpsServer } from 'node:https';
|
|
@@ -59,6 +68,7 @@ const {
|
|
|
59
68
|
build,
|
|
60
69
|
buildAppSync,
|
|
61
70
|
buildSync,
|
|
71
|
+
watch,
|
|
62
72
|
envToDefine,
|
|
63
73
|
filterWorkspaces,
|
|
64
74
|
findConfigPath,
|
|
@@ -109,6 +119,7 @@ function usageLines(command) {
|
|
|
109
119
|
' --host [host] Host to listen on (default: localhost)',
|
|
110
120
|
' --port <port> Port to listen on (default: 12300)',
|
|
111
121
|
' --open Open the app URL in the browser',
|
|
122
|
+
' --lazy Compile dynamic import() chunks on demand',
|
|
112
123
|
' --mode <mode> Load mode-specific config and .env files',
|
|
113
124
|
' --base <path> Base public path',
|
|
114
125
|
' --entry-html <path> HTML entry file',
|
|
@@ -247,7 +258,7 @@ function parseArgs(argv) {
|
|
|
247
258
|
bundle: false,
|
|
248
259
|
watch: false,
|
|
249
260
|
watchJson: false,
|
|
250
|
-
watchDelay:
|
|
261
|
+
watchDelay: 16,
|
|
251
262
|
serve: false,
|
|
252
263
|
serveDir: '.',
|
|
253
264
|
port: undefined,
|
|
@@ -288,6 +299,7 @@ function parseArgs(argv) {
|
|
|
288
299
|
entryNames: undefined,
|
|
289
300
|
chunkNames: undefined,
|
|
290
301
|
assetNames: undefined,
|
|
302
|
+
cssNames: undefined,
|
|
291
303
|
jsx: undefined,
|
|
292
304
|
jsxDev: false,
|
|
293
305
|
jsxFactory: undefined,
|
|
@@ -393,6 +405,16 @@ function parseArgs(argv) {
|
|
|
393
405
|
opts.serve = true;
|
|
394
406
|
opts.bundle = true;
|
|
395
407
|
opts.watch = true;
|
|
408
|
+
// #3793 — `zntc dev` 는 incremental HMR 활성 (__zntc_apply_update / __esm register
|
|
409
|
+
// 주입) 이 필수. 명시 안 set 시 initial bundle 이 production 모드로 빌드돼 HMR
|
|
410
|
+
// 런타임 누락 → broadcast 된 Update 가 client 에서 fallback reload. user 가
|
|
411
|
+
// 명시적으로 `--dev=false` 줘서 override 하기 전엔 dev 모드 default 보장.
|
|
412
|
+
opts.devMode = true;
|
|
413
|
+
// RFC_LAZY_DEV_MODULE_HMR PR-5 / web React Fast Refresh: dev 는 reactRefresh 기본 on.
|
|
414
|
+
// 번들러는 React 컴포넌트가 있는 모듈에만 $RefreshReg$/accept 를 emit(비-React 무해).
|
|
415
|
+
// 브라우저 런타임 preamble 주입은 runServe 가 react 존재 시에만 한다(비-React 노이즈 0).
|
|
416
|
+
// 명시 flag(`--react-refresh=false`)는 아래 flag 파싱이 덮어쓴다.
|
|
417
|
+
if (opts.reactRefresh === undefined) opts.reactRefresh = true;
|
|
396
418
|
} else if (appCommand === 'build') {
|
|
397
419
|
opts.bundle = true;
|
|
398
420
|
} else if (appCommand === 'preview') {
|
|
@@ -662,16 +684,137 @@ function getAutoConfigSearchDir(opts) {
|
|
|
662
684
|
return process.cwd();
|
|
663
685
|
}
|
|
664
686
|
|
|
687
|
+
/**
|
|
688
|
+
* RFC #3833 v3 D1a'' (caller-side pre-warm) helper — runAppBuild/runAppDev 가
|
|
689
|
+
* 공유. 사용자 explicit `plugins: [css({postcss:{...}})]` 의 옵션을 추출해
|
|
690
|
+
* prepare 단계의 `postcssOverride` 로 전달. buildAppSync 의 sync dispatcher ×
|
|
691
|
+
* async cssOnLoad 충돌 회피 — Vite/esbuild 의 main thread pre-process → sync
|
|
692
|
+
* bundle 패턴.
|
|
693
|
+
*
|
|
694
|
+
* 분기:
|
|
695
|
+
* 1. disabled:true → explicit PostCSS 끄기 (override={plugins:[]} 로
|
|
696
|
+
* auto-discover 도 차단, prepare 가 length 0 skip)
|
|
697
|
+
* 2. postcss 명시 → presence check, plugins ?? [] 정규화. options-only
|
|
698
|
+
* override 도 explicit no-op (Vite inline override
|
|
699
|
+
* 시맨틱)
|
|
700
|
+
* 3. 둘 다 없으면 → override=null → prepare 가 auto-discover path
|
|
701
|
+
*
|
|
702
|
+
* **findLast**: 미래 default `css()` prepend 와 user override 가 동시 존재 시
|
|
703
|
+
* 마지막 등록이 winner (Vite plugins 순서 의미). sentinel `__cssOptions` 는
|
|
704
|
+
* runtime 위장 방어 0 — 의도 매치용.
|
|
705
|
+
*
|
|
706
|
+
* @param {Array<{name?:string,__cssOptions?:object}>} plugins
|
|
707
|
+
* @returns {{plugins: unknown[], options: Record<string,unknown> | undefined} | null}
|
|
708
|
+
*/
|
|
709
|
+
/**
|
|
710
|
+
* sentinel `__cssOptions` 보유 css plugin 의 raw options 추출. 매치 안 되면 null.
|
|
711
|
+
* extractCssPostcssOverride / extractCssAutoDiscoverRoot 의 공통 helper.
|
|
712
|
+
*/
|
|
713
|
+
function extractCssOptions(plugins) {
|
|
714
|
+
const cssPlugin = plugins.findLast(
|
|
715
|
+
// 두 항 모두 optional chain — predicate 순서 변경 시 null/undefined deref 회귀
|
|
716
|
+
// 차단 (/code-review max #1 latent finding).
|
|
717
|
+
(p) => p?.name === '@zntc/web/css' && p?.__cssOptions !== undefined,
|
|
718
|
+
);
|
|
719
|
+
return cssPlugin?.__cssOptions ?? null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function extractCssPostcssOverride(plugins) {
|
|
723
|
+
const opts = extractCssOptions(plugins);
|
|
724
|
+
if (!opts) return null;
|
|
725
|
+
if (opts.disabled === true) return { plugins: [], options: undefined };
|
|
726
|
+
if (opts.postcss) {
|
|
727
|
+
return {
|
|
728
|
+
plugins: opts.postcss.plugins ?? [],
|
|
729
|
+
options: opts.postcss.options,
|
|
730
|
+
// issue #3851 — css({root}) 의 root 가 caller-pre-warm path 에서 silent
|
|
731
|
+
// ignore 였던 회귀 fix. override path 의 postcss require base 로 routing.
|
|
732
|
+
// mode 는 override.plugins 명시 시 loadPostcssConfig 미호출이라 무의미 —
|
|
733
|
+
// routing 안 함 (사용자가 root 와 mode 둘 다 명시한 경우 mode 는 onLoad
|
|
734
|
+
// 의 dispatcher path 에서만 의미 있었음, caller-pre-warm 에선 dead).
|
|
735
|
+
root: opts.root,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* issue #3857 — `css({root})` 단독 명시 (postcss override 없이) 시 root 가
|
|
743
|
+
* auto-discover path 의 findPostcssConfig 시작 base 로 사용되게 caller 가 전달.
|
|
744
|
+
* monorepo edge: postcss.config 가 monorepo root 에 있고 app 이 sub-package
|
|
745
|
+
* 인 경우 사용자가 root='/monorepo-root' 명시.
|
|
746
|
+
*
|
|
747
|
+
* - opts.disabled 면 null (자동발견 차단은 disabled true 의 책임)
|
|
748
|
+
* - opts.postcss truthy 면 null (override path 가 root 직접 routing)
|
|
749
|
+
* - opts.root 만 있으면 그것 반환
|
|
750
|
+
*/
|
|
751
|
+
function extractCssAutoDiscoverRoot(plugins) {
|
|
752
|
+
const opts = extractCssOptions(plugins);
|
|
753
|
+
if (!opts || opts.disabled === true) return null;
|
|
754
|
+
if (opts.postcss) return null;
|
|
755
|
+
// /code-review max #3/#4: type/empty guard — non-string 또는 빈 string 거부.
|
|
756
|
+
// findPostcssConfig(non-string) → TypeError, findPostcssConfig('') → cwd 기준
|
|
757
|
+
// wrong-base search. 사용자 invalid 입력은 silent null (auto-discover skip).
|
|
758
|
+
if (typeof opts.root !== 'string' || opts.root.length === 0) return null;
|
|
759
|
+
return opts.root;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* RFC #3833 v3 D1a'' — caller-pre-warm sentinel (`__cssOptions !== undefined`)
|
|
764
|
+
* 가진 css plugin 을 native dispatcher plugin chain 에서 제거.
|
|
765
|
+
*
|
|
766
|
+
* Caller paths:
|
|
767
|
+
* - **runAppBuild**: buildAppSync 의 sync dispatcher 가 async onLoad 받으면
|
|
768
|
+
* syncPluginPromiseFailure → BundleFailed. 본 helper 로 dispatch 차단.
|
|
769
|
+
* - **buildBundleOptions** (runBundle/watch/runServe): native async dispatcher
|
|
770
|
+
* 가 onLoad 호출 → prepare 와 같은 PostCSS 두 번 실행 (double-pass). 본
|
|
771
|
+
* helper 로 dispatch 차단.
|
|
772
|
+
*
|
|
773
|
+
* **runAppDev 는 본 helper 미경유** — controller (createAppDevController) 에
|
|
774
|
+
* postcssOverride 만 전달, plugin chain 자체는 runServe → buildBundleOptions
|
|
775
|
+
* 경로에서 처리. 따라서 dev path 의 filter 는 buildBundleOptions 가 cover.
|
|
776
|
+
*
|
|
777
|
+
* extractCssPostcssOverride 와 동일 sentinel match 조건 (predicate 일관) —
|
|
778
|
+
* drift 위험 차단.
|
|
779
|
+
*
|
|
780
|
+
* @param {Array<{name?:string,__cssOptions?:object}>} plugins
|
|
781
|
+
* @returns {Array<unknown>} caller-pre-warm 활성 css plugin 제거된 새 array
|
|
782
|
+
*/
|
|
783
|
+
function dropCallerPreWarmedCssPlugin(plugins) {
|
|
784
|
+
return plugins.filter((p) => !(p?.name === '@zntc/web/css' && p?.__cssOptions !== undefined));
|
|
785
|
+
}
|
|
786
|
+
|
|
665
787
|
async function runAppBuild(opts, config, configEnv, _dotenvVars) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
788
|
+
// JS plugin 로드 — bundle pipeline 의 buildBundleOptions 와 동일 패턴. app
|
|
789
|
+
// pipeline 도 plugin dispatcher 통과 (#2538 4-4 PR-1).
|
|
790
|
+
const appPlugins = [];
|
|
791
|
+
if (config && Array.isArray(config.plugins)) {
|
|
792
|
+
appPlugins.push(...config.plugins);
|
|
793
|
+
}
|
|
794
|
+
for (const pluginPath of opts.pluginPaths) {
|
|
795
|
+
const absPath = resolve(pluginPath);
|
|
796
|
+
const cfg = await importAndResolveDefault(absPath);
|
|
797
|
+
if (Array.isArray(cfg.plugins)) {
|
|
798
|
+
appPlugins.push(...cfg.plugins);
|
|
799
|
+
} else if (typeof cfg.setup === 'function') {
|
|
800
|
+
appPlugins.push(cfg);
|
|
801
|
+
}
|
|
670
802
|
}
|
|
671
803
|
const web = await loadWebModule();
|
|
672
804
|
const root = resolve(opts.appRoot ?? '.');
|
|
673
805
|
const outdir = resolve(opts.outdir ?? join(root, 'dist'));
|
|
674
806
|
if (opts.clean) rmSync(outdir, { recursive: true, force: true });
|
|
807
|
+
// RFC #3833 v3 D1a'' caller-side pre-warm — extractCssPostcssOverride helper 참조.
|
|
808
|
+
const postcssOverride = extractCssPostcssOverride(appPlugins);
|
|
809
|
+
// issue #3857 — css({root}) 단독 명시 시 findPostcssConfig search base
|
|
810
|
+
// (monorepo edge: app 이 sub-package, postcss.config 가 monorepo root).
|
|
811
|
+
const cssAutoDiscoverRoot = extractCssAutoDiscoverRoot(appPlugins);
|
|
812
|
+
// dropCallerPreWarmedCssPlugin: sentinel 가진 css plugin 을 항상 dispatcher 에서
|
|
813
|
+
// 제거 (buildBundleOptions 와 동일 무조건 적용). /code-review max #5: 조건부
|
|
814
|
+
// (postcssOverride truthy 시만) 분기는 비대칭 — 사용자가 sentinel 만 가진
|
|
815
|
+
// plugin (예: `__cssOptions:{someFutureKey}` — extract null) 등록 시
|
|
816
|
+
// BundleFailed 회귀 가능. 무조건 drop 으로 future-key 안전 + 양쪽 path 일관.
|
|
817
|
+
const dispatchPlugins = dropCallerPreWarmedCssPlugin(appPlugins);
|
|
675
818
|
let pipelineRoot = null;
|
|
676
819
|
try {
|
|
677
820
|
const pipeline = await web.prepareAppCssPipelineRoot(
|
|
@@ -681,6 +824,7 @@ async function runAppBuild(opts, config, configEnv, _dotenvVars) {
|
|
|
681
824
|
opts.logLevel,
|
|
682
825
|
'build',
|
|
683
826
|
{ fallbackRequire: requireFromCli, cliNodeModules },
|
|
827
|
+
{ postcssOverride, cssAutoDiscoverRoot },
|
|
684
828
|
);
|
|
685
829
|
pipelineRoot = pipeline?.tempRoot ?? null;
|
|
686
830
|
const result = buildAppSync({
|
|
@@ -701,6 +845,7 @@ async function runAppBuild(opts, config, configEnv, _dotenvVars) {
|
|
|
701
845
|
jsxFactory: opts.jsxFactory,
|
|
702
846
|
jsxFragment: opts.jsxFragment,
|
|
703
847
|
compiler: config?.compiler,
|
|
848
|
+
plugins: dispatchPlugins.length > 0 ? dispatchPlugins : undefined,
|
|
704
849
|
});
|
|
705
850
|
const htmlEnv = loadEnv(
|
|
706
851
|
configEnv.mode,
|
|
@@ -731,14 +876,40 @@ async function runAppDev(opts, config, configEnv, _dotenvVars) {
|
|
|
731
876
|
const web = await loadWebModule();
|
|
732
877
|
const root = resolve(opts.appRoot ?? '.');
|
|
733
878
|
opts.outdir = opts.outdir || join(root, '.zntc-dev');
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
879
|
+
// RFC #3833 v3 D1a'' Phase 2: build path 와 동일 plugin walk + helper 추출.
|
|
880
|
+
const devUserPlugins = [];
|
|
881
|
+
if (config && Array.isArray(config.plugins)) devUserPlugins.push(...config.plugins);
|
|
882
|
+
for (const pluginPath of opts.pluginPaths) {
|
|
883
|
+
const absPath = resolve(pluginPath);
|
|
884
|
+
const cfg = await importAndResolveDefault(absPath);
|
|
885
|
+
if (Array.isArray(cfg.plugins)) devUserPlugins.push(...cfg.plugins);
|
|
886
|
+
else if (typeof cfg.setup === 'function') devUserPlugins.push(cfg);
|
|
887
|
+
}
|
|
888
|
+
const appDev = web.createAppDevController(
|
|
889
|
+
{
|
|
890
|
+
...opts,
|
|
891
|
+
postcssOverride: extractCssPostcssOverride(devUserPlugins),
|
|
892
|
+
// issue #3857 — css({root}) 단독 명시 (postcss override 없이) 시 root 를
|
|
893
|
+
// findPostcssConfig 의 search base 로 전달. monorepo 의 sub-package app
|
|
894
|
+
// 이 monorepo root 의 postcss.config 참조하는 시나리오.
|
|
895
|
+
cssAutoDiscoverRoot: extractCssAutoDiscoverRoot(devUserPlugins),
|
|
896
|
+
},
|
|
897
|
+
root,
|
|
898
|
+
configEnv,
|
|
899
|
+
{ fallbackRequire: requireFromCli, cliNodeModules },
|
|
900
|
+
);
|
|
738
901
|
const prepared = await appDev.prepare();
|
|
739
902
|
|
|
740
903
|
opts.entryPoints = [prepared.entryPath];
|
|
741
904
|
opts.serveDir = opts.outdir;
|
|
905
|
+
// issue #3858 — runServe 의 watchFolders 자동 set 위해 app root 를 stash.
|
|
906
|
+
// opts.serveDir 은 outdir(`.zntc-dev`) 이라 사용자 source code root 가 아님 —
|
|
907
|
+
// 별도 channel 필요.
|
|
908
|
+
opts._appWatchRoot = root;
|
|
909
|
+
// issue #3852 — runAppDev 가 collect 한 plugin 을 stash → runServe 의
|
|
910
|
+
// buildBundleOptions 가 재import 안 함. ESM cache hit 라 시맨틱 회귀는 0 였지만
|
|
911
|
+
// perf + cache invalidate edge 안전.
|
|
912
|
+
opts._resolvedPlugins = devUserPlugins;
|
|
742
913
|
|
|
743
914
|
return runServe(opts, config, { appDev });
|
|
744
915
|
}
|
|
@@ -762,6 +933,17 @@ async function loadWebModule() {
|
|
|
762
933
|
console.error('help: install with `bun add -D @zntc/web` 또는 `npm i -D @zntc/web`.');
|
|
763
934
|
process.exit(1);
|
|
764
935
|
}
|
|
936
|
+
// @zntc/web 의 module 평가가 dev-overlay-client.raw.js 를 readFileSync 한다
|
|
937
|
+
// (#2538 4-3). dist 가 incomplete 한 경우 (build:bundle 의 copy 누락 / 일부만
|
|
938
|
+
// 추출된 tarball) 가 ENOENT 로 throw — 친절 핸들 분기.
|
|
939
|
+
if (code === 'ENOENT' && /dev-overlay-client\.raw\.js/.test(message)) {
|
|
940
|
+
console.error('error: @zntc/web 의 dist/dev-overlay-client.raw.js 가 누락됐습니다.');
|
|
941
|
+
console.error('');
|
|
942
|
+
console.error(
|
|
943
|
+
'help: `bun --cwd <repo>/packages/web run build` 로 재빌드하거나 @zntc/web 을 재설치하세요.',
|
|
944
|
+
);
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
765
947
|
throw err;
|
|
766
948
|
}
|
|
767
949
|
})();
|
|
@@ -1145,6 +1327,7 @@ function mergeConfigIntoOpts(opts, config) {
|
|
|
1145
1327
|
'entryNames',
|
|
1146
1328
|
'chunkNames',
|
|
1147
1329
|
'assetNames',
|
|
1330
|
+
'cssNames',
|
|
1148
1331
|
'jsx',
|
|
1149
1332
|
'jsxFactory',
|
|
1150
1333
|
'jsxFragment',
|
|
@@ -1299,29 +1482,54 @@ function mergeCliRuntimeTargets(runtimePolyfills, runtimeTargetQueries) {
|
|
|
1299
1482
|
return runtimePolyfills;
|
|
1300
1483
|
}
|
|
1301
1484
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
plugins.push(
|
|
1485
|
+
/**
|
|
1486
|
+
* `runBundle` / `startBundleWatch` 가 공유하는 NAPI BuildOptions 생성 helper.
|
|
1487
|
+
* plugins 머지 + applySingleFileDynamicImportDefault + 옵션 매핑을 한 곳에 모아
|
|
1488
|
+
* runBundle (single-shot) 와 watch (incremental HMR, #3779) 의 옵션 drift 차단.
|
|
1489
|
+
*/
|
|
1490
|
+
async function buildBundleOptions(opts, config, { filterCallerPreWarmCss = false } = {}) {
|
|
1491
|
+
// issue #3852 — caller (runAppDev) 가 이미 plugin walk 했으면 `_resolvedPlugins`
|
|
1492
|
+
// 로 stash → 재import skip. ESM cache hit 이라 시맨틱 회귀 없지만 perf +
|
|
1493
|
+
// cache invalidate edge 안전.
|
|
1494
|
+
let plugins;
|
|
1495
|
+
if (Array.isArray(opts._resolvedPlugins)) {
|
|
1496
|
+
plugins = [...opts._resolvedPlugins];
|
|
1497
|
+
} else {
|
|
1498
|
+
plugins = [];
|
|
1499
|
+
if (config && Array.isArray(config.plugins)) {
|
|
1500
|
+
plugins.push(...config.plugins);
|
|
1501
|
+
}
|
|
1502
|
+
for (const pluginPath of opts.pluginPaths) {
|
|
1503
|
+
const absPath = resolve(pluginPath);
|
|
1504
|
+
// importAndResolveDefault 는 pathToFileURL 으로 Windows 경로를 안전하게 처리하고
|
|
1505
|
+
// ENOENT/객체 검증을 통일한다 (config-loader 와 공유).
|
|
1506
|
+
const cfg = await importAndResolveDefault(absPath);
|
|
1507
|
+
if (Array.isArray(cfg.plugins)) {
|
|
1508
|
+
plugins.push(...cfg.plugins);
|
|
1509
|
+
} else if (typeof cfg.setup === 'function') {
|
|
1510
|
+
plugins.push(cfg);
|
|
1511
|
+
}
|
|
1318
1512
|
}
|
|
1319
1513
|
}
|
|
1514
|
+
// RFC #3833 v3 D1a'' caller-side pre-warm — dropCallerPreWarmedCssPlugin helper
|
|
1515
|
+
// 로 sentinel 가진 css plugin 을 dispatcher chain 에서 제거. **app caller
|
|
1516
|
+
// (runServe with appDev) 만 적용** — bundle/transpile 모드 사용자가 css()
|
|
1517
|
+
// 명시한 경우 무조건 drop 하면 PostCSS 효과 0 silent regression (review #2).
|
|
1518
|
+
// app 모드만 caller-pre-warm 로 prepare 가 처리하므로 dispatcher dispatch 차단
|
|
1519
|
+
// 필요, bundle 모드는 native async dispatcher 가 onLoad 호출 (단 caller-pre-warm
|
|
1520
|
+
// 없어 PostCSS 적용 안 되지만 사용자 의도 그대로 dispatcher 에 전달).
|
|
1521
|
+
const dispatchPlugins = filterCallerPreWarmCss ? dropCallerPreWarmedCssPlugin(plugins) : plugins;
|
|
1320
1522
|
|
|
1321
1523
|
applySingleFileDynamicImportDefault(opts);
|
|
1322
1524
|
|
|
1323
|
-
|
|
1525
|
+
return {
|
|
1324
1526
|
entryPoints: opts.entryPoints.map((e) => resolve(e)),
|
|
1527
|
+
// #3795/#3796 — watch handle 이 outdir 알아야 worker thread 의 createFile 가 정확한 위치에
|
|
1528
|
+
// 출력. `prepareNapiOptions` 가 build/buildSync 케이스에서 outdir 를 delete 하지만 watch()
|
|
1529
|
+
// wrapper (index.ts:3358) 가 명시 outdir 받아 NAPI 로 restore. 그러므로 BuildOptions 의
|
|
1530
|
+
// outdir/outfile 필드 자체는 enrich 해서 보내야 함.
|
|
1531
|
+
outdir: opts.outdir,
|
|
1532
|
+
outfile: opts.outfile,
|
|
1325
1533
|
format: opts.format,
|
|
1326
1534
|
platform: opts.platform,
|
|
1327
1535
|
target: opts.target,
|
|
@@ -1402,6 +1610,7 @@ async function runBundle(opts, config) {
|
|
|
1402
1610
|
entryNames: opts.entryNames,
|
|
1403
1611
|
chunkNames: opts.chunkNames,
|
|
1404
1612
|
assetNames: opts.assetNames,
|
|
1613
|
+
cssNames: opts.cssNames,
|
|
1405
1614
|
jsx: opts.jsx,
|
|
1406
1615
|
jsxDev: opts.jsxDev,
|
|
1407
1616
|
jsxFactory: opts.jsxFactory,
|
|
@@ -1409,6 +1618,9 @@ async function runBundle(opts, config) {
|
|
|
1409
1618
|
jsxImportSource: opts.jsxImportSource,
|
|
1410
1619
|
inject: opts.inject.map((p) => resolve(p)),
|
|
1411
1620
|
devMode: opts.devMode,
|
|
1621
|
+
// web React Fast Refresh: native 빌드(options.zig:reactRefresh)에 전파 → 컴포넌트에
|
|
1622
|
+
// $RefreshReg$/accept emit. dev 블록이 기본 on(비-React 무해).
|
|
1623
|
+
reactRefresh: opts.reactRefresh,
|
|
1412
1624
|
globalIdentifiers: opts.globalIdentifiers,
|
|
1413
1625
|
// --polyfill / --run-before-main / --watch-folder 는 경로 → abs 변환 (--inject 와 동일).
|
|
1414
1626
|
// --watch-include / --watch-exclude 는 루트 기준 glob 이므로 변환 안 함.
|
|
@@ -1421,7 +1633,7 @@ async function runBundle(opts, config) {
|
|
|
1421
1633
|
watchExclude: opts.watchExclude?.length ? opts.watchExclude : undefined,
|
|
1422
1634
|
jobs: opts.jobs,
|
|
1423
1635
|
outbase: opts.outbase,
|
|
1424
|
-
plugins:
|
|
1636
|
+
plugins: dispatchPlugins.length > 0 ? dispatchPlugins : undefined,
|
|
1425
1637
|
// compiler.styledComponents / compiler.emotion 도 bundle 모드에서 forward.
|
|
1426
1638
|
// 누락 시 `zntc.config.json` 의 `compiler` 설정이 silently drop 돼 1st-party transform
|
|
1427
1639
|
// (autoLabel 등) 이 활성화 안 됨.
|
|
@@ -1431,8 +1643,12 @@ async function runBundle(opts, config) {
|
|
|
1431
1643
|
// (native CLI 만 zntc.config.json mf 를 직접 읽어 동작했던 갭).
|
|
1432
1644
|
mf: config?.mf,
|
|
1433
1645
|
};
|
|
1646
|
+
}
|
|
1434
1647
|
|
|
1435
|
-
|
|
1648
|
+
async function runBundle(opts, config) {
|
|
1649
|
+
const buildOpts = await buildBundleOptions(opts, config);
|
|
1650
|
+
const hasPlugins = Array.isArray(buildOpts.plugins) && buildOpts.plugins.length > 0;
|
|
1651
|
+
const result = hasPlugins ? await build(buildOpts) : buildSync(buildOpts);
|
|
1436
1652
|
|
|
1437
1653
|
printResultDiagnostics(result, opts.logLevel);
|
|
1438
1654
|
|
|
@@ -1657,6 +1873,65 @@ async function emitRestartAfter(opts, reason, beforeSpawn) {
|
|
|
1657
1873
|
|
|
1658
1874
|
// ─── Serve 모드 ───
|
|
1659
1875
|
|
|
1876
|
+
/**
|
|
1877
|
+
* #3858 — raw root .css 의 diff 기반 reconcile. 이전 scan 결과와 비교해
|
|
1878
|
+
* **사라진 path** 만 outdir 에서 unlink. 이전 design (raw vs outdir set diff)
|
|
1879
|
+
* 은 outdir 의 bundler/sass/css-modules 가 emit 한 transient file (chunk.css,
|
|
1880
|
+
* Button.module.zntc.css 등) 을 stale 오판 → unlink 회귀 (/code-review max #4/#5).
|
|
1881
|
+
*
|
|
1882
|
+
* caller 가 closure 로 prevRawSet 보관 — 매 cycle current scan + diff:
|
|
1883
|
+
* - removed = prev ∖ current → outdir 의 동일 rel path unlink
|
|
1884
|
+
* - prev := current
|
|
1885
|
+
*
|
|
1886
|
+
* .scss/.sass 는 sass pipeline 이 별도 outdir 에 emit, raw root 의 .scss 자체
|
|
1887
|
+
* 삭제 시 sass 산출물도 dev_controller 가 cleanup. reconcile 은 plain raw .css
|
|
1888
|
+
* 만 cover (사용자가 직접 만든 .css 파일).
|
|
1889
|
+
*
|
|
1890
|
+
* cost: O(raw .css count) walk per rebuild — 일반적 small (dozens).
|
|
1891
|
+
*/
|
|
1892
|
+
function createReconcileOutdirCss(rawRoot, outdir) {
|
|
1893
|
+
let prevRawSet = new Set();
|
|
1894
|
+
|
|
1895
|
+
function scanRawCss() {
|
|
1896
|
+
const set = new Set();
|
|
1897
|
+
function walk(dir, relBase) {
|
|
1898
|
+
let entries;
|
|
1899
|
+
try {
|
|
1900
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1901
|
+
} catch {
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
for (const e of entries) {
|
|
1905
|
+
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
1906
|
+
if (e.name.startsWith('.zntc-')) continue;
|
|
1907
|
+
const rel = relBase ? `${relBase}/${e.name}` : e.name;
|
|
1908
|
+
if (e.isDirectory()) walk(join(dir, e.name), rel);
|
|
1909
|
+
else if (e.isFile() && e.name.endsWith('.css')) set.add(rel);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
walk(rawRoot, '');
|
|
1913
|
+
return set;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// 첫 호출 시 prev 를 현재 raw scan 으로 초기화 — 첫 reconcile cycle 에서
|
|
1917
|
+
// 모든 outdir .css 가 stale 로 오판되는 것 회피.
|
|
1918
|
+
prevRawSet = scanRawCss();
|
|
1919
|
+
|
|
1920
|
+
return function reconcile() {
|
|
1921
|
+
const current = scanRawCss();
|
|
1922
|
+
for (const rel of prevRawSet) {
|
|
1923
|
+
if (current.has(rel)) continue;
|
|
1924
|
+
const target = join(outdir, rel);
|
|
1925
|
+
try {
|
|
1926
|
+
unlinkSync(target);
|
|
1927
|
+
} catch {
|
|
1928
|
+
// best-effort — file 이 없거나 race
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
prevRawSet = current;
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1660
1935
|
async function runServe(opts, config, { appDev = null } = {}) {
|
|
1661
1936
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
1662
1937
|
// appDev 모드에서만 web 모듈 (HMR_MSG / APP_DEV_HMR_*_PATH / createHmrChannel /
|
|
@@ -1669,7 +1944,47 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1669
1944
|
const APP_DEV_HMR_CLIENT = web?.APP_DEV_HMR_CLIENT;
|
|
1670
1945
|
const APP_DEV_HMR_CLIENT_PATH = web?.APP_DEV_HMR_CLIENT_PATH;
|
|
1671
1946
|
const APP_DEV_HMR_WS_PATH = web?.APP_DEV_HMR_WS_PATH;
|
|
1947
|
+
const APP_DEV_REACT_REFRESH_PATH = web?.APP_DEV_REACT_REFRESH_PATH;
|
|
1948
|
+
// React Fast Refresh preamble (react-refresh 런타임 글로벌 노출 + injectIntoGlobalHook).
|
|
1949
|
+
// reactRefresh on + react 설치 시에만 non-null(=비-React 앱은 주입/서빙/경고 0). lazy 1회 캐시.
|
|
1950
|
+
const reactRefreshAppRoot = opts.appRoot ? resolve(opts.appRoot) : process.cwd();
|
|
1951
|
+
let reactRefreshPreamble; // undefined=미계산, null=스킵, string=서빙
|
|
1952
|
+
const getReactRefreshPreamble = () => {
|
|
1953
|
+
if (reactRefreshPreamble === undefined) {
|
|
1954
|
+
reactRefreshPreamble =
|
|
1955
|
+
web && opts.reactRefresh ? web.buildReactRefreshPreamble(reactRefreshAppRoot) : null;
|
|
1956
|
+
}
|
|
1957
|
+
return reactRefreshPreamble;
|
|
1958
|
+
};
|
|
1672
1959
|
let serverHandle = null;
|
|
1960
|
+
// #3779 follow-up — restart 시 stop 호출용. opts.bundle+watch+appDev+hmr 분기 안에서만 할당.
|
|
1961
|
+
let nativeWatchHandle = null;
|
|
1962
|
+
// #4062 — JS dev 서버 lazy on-demand 라우트. 게이트 = `--lazy` CLI 플래그(PR-C-3) 또는
|
|
1963
|
+
// env `ZNTC_LAZY=1`(동치 fallback). D105 접근1: native lazy 프리미티브(#4069/#4070, watch
|
|
1964
|
+
// lazySeeds + build lazyForceParse) 위에 JS 서버가 얇게 on-demand 라우팅을 얹는다. lazy 동적
|
|
1965
|
+
// 청크는 emit-skip 되므로(디스크에 없음) 브라우저가 `/<stem>-<8hex>.js` 를 요청하면 그 seed 만
|
|
1966
|
+
// force-parse 한 단발 build() 로 즉석 생성·캐시해서 서빙한다. 게이트 OFF 면 아래 전부 무시 → 0 영향.
|
|
1967
|
+
const lazyMode = opts.lazy === true || process.env.ZNTC_LAZY === '1';
|
|
1968
|
+
// pathHash(8 hex) → seed 절대경로. watch onReady/onRebuild 의 event.lazySeeds 로 갱신.
|
|
1969
|
+
const lazySeedMap = new Map();
|
|
1970
|
+
// pathHash → { body, type }. on-demand 빌드 결과 캐시. rebuild 마다 무효화(seed 본문 변경 가능).
|
|
1971
|
+
const lazyChunkCache = new Map();
|
|
1972
|
+
// pathHash → in-flight build Promise. 같은 청크 동시 요청을 coalesce(중복 build 회피).
|
|
1973
|
+
const lazyInflight = new Map();
|
|
1974
|
+
// on-demand build() 직렬화 tail. 페이지 로드 시 여러 lazy 청크가 동시 요청돼도 native build()
|
|
1975
|
+
// 를 한 번에 하나씩만 돌려 watch worker 와의 동시 재진입 위험을 없앤다(/code-review Q2).
|
|
1976
|
+
let lazyBuildTail = Promise.resolve();
|
|
1977
|
+
// #4062 PR-C-2 — 캐시 세대(epoch). rebuild(captureLazyState)가 캐시를 무효화할 때마다 +1.
|
|
1978
|
+
// on-demand build 가 시작 시 epoch 를 캡처하고 완료 후 비교해, 빌드 도중 rebuild 가 끼면
|
|
1979
|
+
// (epoch 변동) 그 결과를 캐시에 넣지 않는다(옛 소스로 만든 stale 바이트가 비워진 캐시를
|
|
1980
|
+
// 재오염하는 것을 막음 — native LazyState.epoch PR-4-iii 패턴 이식).
|
|
1981
|
+
let lazyEpoch = 0;
|
|
1982
|
+
// lazy entry 청크의 디스크 경로(`<entrystem>.js`). served index.html 은 prepareDev 가 non-split
|
|
1983
|
+
// 으로 `/bundle.js` 를 참조하게 rewrite 했지만, watch lazy 빌드의 entry 청크는 stem 이름이라
|
|
1984
|
+
// mismatch → `/bundle.js` 를 이 파일로 alias.
|
|
1985
|
+
let lazyEntryFile = null;
|
|
1986
|
+
// on-demand 단발 build() 옵션 템플릿(watchBuildOpts 와 동일 lazy 설정, callback/outdir 제거).
|
|
1987
|
+
let lazyOnDemandOpts = null;
|
|
1673
1988
|
const mimeTypes = {
|
|
1674
1989
|
'.html': 'text/html',
|
|
1675
1990
|
'.js': 'application/javascript',
|
|
@@ -1686,29 +2001,40 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1686
2001
|
'.map': 'application/json',
|
|
1687
2002
|
};
|
|
1688
2003
|
|
|
1689
|
-
//
|
|
2004
|
+
// #3796/#3798 root-cause — cold-start 시 runBundle 호출 제거. native watch 가 initial
|
|
2005
|
+
// 빌드 + outdir 출력 + appDev hooks 전부 담당. plugin.setup() 1회만 invoke (cold-start
|
|
2006
|
+
// 시점). race 회피는 HTTP server listen 을 watch.onReady 까지 wait — `napi_tsfn_blocking`
|
|
2007
|
+
// 모드라 worker 가 outdir 출력 완료 후 ready_event firing, JS callback 안에서 markWatchReady.
|
|
1690
2008
|
if (opts.bundle && opts.entryPoints.length > 0) {
|
|
1691
2009
|
opts.outdir = opts.outdir || join(opts.serveDir, '.zntc-serve');
|
|
1692
|
-
const bundleResult = await runBundle(opts, config);
|
|
1693
|
-
if (appDev) {
|
|
1694
|
-
if (bundleResult.errors.length > 0) {
|
|
1695
|
-
hmr?.reportError(bundleResult.errors);
|
|
1696
|
-
} else {
|
|
1697
|
-
hmr?.clearError();
|
|
1698
|
-
appDev.injectBundleCssLinks(bundleResult);
|
|
1699
|
-
await appDev.afterBundle();
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// watch도 같이
|
|
1704
2010
|
if (!opts.watch) {
|
|
1705
2011
|
opts.watch = true;
|
|
1706
2012
|
}
|
|
1707
2013
|
}
|
|
1708
2014
|
|
|
2015
|
+
// #3796 — watch.onReady 까지 HTTP listen 이 wait 할 수 있게 promise. appDev + hmr 모드만
|
|
2016
|
+
// 의미 — 그 외 모드는 markWatchReady 즉시 호출 → promise 가 처음부터 resolved.
|
|
2017
|
+
let watchReadyResolve;
|
|
2018
|
+
const watchReadyPromise = new Promise((r) => {
|
|
2019
|
+
watchReadyResolve = r;
|
|
2020
|
+
});
|
|
2021
|
+
let watchReadyResolved = false;
|
|
2022
|
+
const markWatchReady = () => {
|
|
2023
|
+
if (!watchReadyResolved) {
|
|
2024
|
+
watchReadyResolved = true;
|
|
2025
|
+
watchReadyResolve();
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
if (!(opts.bundle && opts.watch && appDev && hmr)) markWatchReady();
|
|
2029
|
+
|
|
1709
2030
|
const serveDir = resolve(opts.outdir || opts.serveDir);
|
|
1710
2031
|
const base = normalizeBase(opts.base ?? '/');
|
|
1711
2032
|
|
|
2033
|
+
// #3799 — HMR Update modules 의 sourceMappingURL 이 가리키는 lazy sourcemap endpoint.
|
|
2034
|
+
// dev_overlay_client.js 의 __zntc_apply_update 가 eval 한 module code 끝의 주석을 DevTools
|
|
2035
|
+
// 가 fetch 함. RN bridge 의 `/__zntc_hmr_map/<id>` 와 동일 path.
|
|
2036
|
+
const HMR_MAP_PATH = '/__zntc_hmr_map/';
|
|
2037
|
+
|
|
1712
2038
|
function handleRequest(reqUrl, accept = '') {
|
|
1713
2039
|
let pathname = new URL(reqUrl, 'http://localhost').pathname;
|
|
1714
2040
|
if (appDev && pathname === APP_DEV_HMR_CLIENT_PATH) {
|
|
@@ -1718,6 +2044,27 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1718
2044
|
type: 'application/javascript',
|
|
1719
2045
|
};
|
|
1720
2046
|
}
|
|
2047
|
+
// React Fast Refresh preamble — 앱 번들보다 먼저 실행되는 classic <script> 본문.
|
|
2048
|
+
if (appDev && opts.reactRefresh && pathname === APP_DEV_REACT_REFRESH_PATH) {
|
|
2049
|
+
const body = getReactRefreshPreamble();
|
|
2050
|
+
if (body != null) return { status: 200, body, type: 'application/javascript' };
|
|
2051
|
+
}
|
|
2052
|
+
// #3799 — HMR module 별 sourcemap. nativeWatchHandle 이 lazy cache 한 V3 JSON 을
|
|
2053
|
+
// moduleId 로 조회. handle 가 stop 됐거나 module 미수집 시 null → 404. 사용자
|
|
2054
|
+
// app 의 routing 우선순위 (base prefix 처리) 보다 앞에 위치 — `__zntc_hmr_map/`
|
|
2055
|
+
// 는 internal prefix 라 base 영향 무관.
|
|
2056
|
+
if (appDev && pathname.startsWith(HMR_MAP_PATH) && nativeWatchHandle) {
|
|
2057
|
+
const moduleId = decodeURIComponent(pathname.slice(HMR_MAP_PATH.length));
|
|
2058
|
+
try {
|
|
2059
|
+
const sm = nativeWatchHandle.getHmrSourceMap(moduleId);
|
|
2060
|
+
if (sm) {
|
|
2061
|
+
return { status: 200, body: sm, type: 'application/json' };
|
|
2062
|
+
}
|
|
2063
|
+
} catch {
|
|
2064
|
+
// handle stop 또는 unwrap 실패 — 404
|
|
2065
|
+
}
|
|
2066
|
+
return { status: 404, body: 'Not Found', type: 'text/plain' };
|
|
2067
|
+
}
|
|
1721
2068
|
if (base && base !== '/' && pathname.startsWith(base)) {
|
|
1722
2069
|
pathname = '/' + pathname.slice(base.length);
|
|
1723
2070
|
}
|
|
@@ -1744,16 +2091,323 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1744
2091
|
return { status: 200, body, type };
|
|
1745
2092
|
}
|
|
1746
2093
|
|
|
2094
|
+
// #4062 PR-B-2 — lazy 상태 갱신. watch onReady/onRebuild event 의 lazySeeds 로 pathHash→seed
|
|
2095
|
+
// 맵을 다시 채우고, entry 청크 파일을 식별하고, 캐시를 무효화한다(seed 본문 변경 가능).
|
|
2096
|
+
// event.outputs 는 디스크 경로 목록. entry 청크 = entry stem(`<name>.js`)과 basename 이 일치하는
|
|
2097
|
+
// 출력(splitting 의 entry 청크 네이밍). dev 모드는 content-hash off 라 stem 그대로.
|
|
2098
|
+
function captureLazyState(event) {
|
|
2099
|
+
// 실패한 rebuild(event.success===false)는 무시 — 직전 성공 빌드의 seed/cache 를 유지한다
|
|
2100
|
+
// (clear 하면 다음 요청이 또 실패할 빌드를 돌려 last-good 청크를 잃는다). onReady event 는
|
|
2101
|
+
// success 필드가 없어(undefined) 통과한다.
|
|
2102
|
+
if (!lazyMode || !event || event.success === false) return;
|
|
2103
|
+
// 정밀 캐시 무효화(#4062 후속) — 매 rebuild 전체 clear 하면 무관한 편집에도 멀쩡한 lazy
|
|
2104
|
+
// 청크가 버려져 다음 요청이 cold build()(store 없는 단발이라 ~수초)를 돈다. 대신 변경 파일
|
|
2105
|
+
// (event.changed, 절대경로)이 *닿은* 청크만 버린다 — 캐시 항목의 moduleIds(청크 내 모듈
|
|
2106
|
+
// 절대경로)와 교집합이 있을 때만. graph 모양이 바뀐 rebuild(graphChanged=모듈 추가/제거)는
|
|
2107
|
+
// moduleIds 가 stale 일 수 있어(신규 파일이 옛 moduleIds 에 없음) 전체 clear 로 fallback.
|
|
2108
|
+
// changed 가 없으면(onReady 등) 보수적으로 전체 clear.
|
|
2109
|
+
const changedAbs =
|
|
2110
|
+
Array.isArray(event.changed) && event.changed.length > 0
|
|
2111
|
+
? new Set(event.changed.map((p) => resolve(p)))
|
|
2112
|
+
: null;
|
|
2113
|
+
if (event.graphChanged || !changedAbs) {
|
|
2114
|
+
lazyChunkCache.clear();
|
|
2115
|
+
} else {
|
|
2116
|
+
for (const [hash, entry] of lazyChunkCache) {
|
|
2117
|
+
const ids = entry.moduleIds;
|
|
2118
|
+
// moduleIds 미보유(방어) 또는 변경 파일과 교집합 → 버림.
|
|
2119
|
+
if (!Array.isArray(ids) || ids.some((id) => changedAbs.has(resolve(id)))) {
|
|
2120
|
+
lazyChunkCache.delete(hash);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
// epoch 증가는 항상 — 진행 중 on-demand build 가 이 무효화를 넘겨 stale 바이트를 재캐시하지
|
|
2125
|
+
// 못하게(in-flight race backstop). 정밀 delete 와 직교(어떤 항목을 버리든 race 가드는 유지).
|
|
2126
|
+
lazyEpoch++;
|
|
2127
|
+
// seed 맵은 event.lazySeeds 가 *실제로 올 때만* 교체한다. 일반 편집(동적 import 를 가진
|
|
2128
|
+
// 모듈이 cache-hit)은 native 가 graph.lazy_seeds 를 다시 안 쌓아 lazySeeds 가 undefined 로
|
|
2129
|
+
// 온다 — 무조건 clear 하면 직전 유효 맵을 날려(PR-B-2 보다 나쁨) on-demand 라우트가 죽는다.
|
|
2130
|
+
// 새 seed 집합(동적 import 추가/제거 = importer 재파싱)일 때만 clear+repopulate.
|
|
2131
|
+
// (lazyEntryFile 의 `if (entry)` 보존과 같은 원리.)
|
|
2132
|
+
if (Array.isArray(event.lazySeeds)) {
|
|
2133
|
+
lazySeedMap.clear();
|
|
2134
|
+
for (const s of event.lazySeeds) {
|
|
2135
|
+
if (s && typeof s.pathHash === 'string' && typeof s.path === 'string') {
|
|
2136
|
+
lazySeedMap.set(s.pathHash, s.path);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
const entryStem = basename(opts.entryPoints[0] ?? '', extname(opts.entryPoints[0] ?? ''));
|
|
2141
|
+
// event.outputs 는 outdir 기준 bare 파일명("main.js") — serveDir 로 절대화한다(이미 절대면 그대로).
|
|
2142
|
+
const outputs = (Array.isArray(event.outputs) ? event.outputs : [])
|
|
2143
|
+
.filter((p) => typeof p === 'string')
|
|
2144
|
+
.map((p) => resolve(serveDir, p));
|
|
2145
|
+
let entry = outputs.find((p) => basename(p, '.js') === entryStem) ?? null;
|
|
2146
|
+
// fallback: entry stem 매칭 실패 시 `__zntc_load_chunk` 를 포함한 .js 출력(동적 import 보유 청크).
|
|
2147
|
+
if (!entry) {
|
|
2148
|
+
for (const p of outputs) {
|
|
2149
|
+
if (!p.endsWith('.js')) continue;
|
|
2150
|
+
try {
|
|
2151
|
+
if (readFileSync(p, 'utf8').includes('__zntc_load_chunk(')) {
|
|
2152
|
+
entry = p;
|
|
2153
|
+
break;
|
|
2154
|
+
}
|
|
2155
|
+
} catch {
|
|
2156
|
+
// 읽기 실패 — skip
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
// dev rebuild 는 skip_bundle_output 라 event.outputs 가 비어있을 수 있다(#3796). entry 를
|
|
2161
|
+
// 못 찾았으면 직전 값을 유지(entry 청크 파일명은 stem 고정이라 안정 — 매 요청 readFileSync 라
|
|
2162
|
+
// 내용은 자동 fresh). 찾았을 때만 갱신.
|
|
2163
|
+
if (entry) lazyEntryFile = entry;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// #4062 PR-B-2 — lazy on-demand 라우트. 반환 null = lazy 라우트가 처리 안 함(기존 handleRequest 로).
|
|
2167
|
+
// ① `/bundle.js` (served index.html 이 참조) → lazy entry 청크 alias.
|
|
2168
|
+
// ② `/<stem>-<8hex>.js` → seed 역참조 후 그 seed 만 force-parse 한 단발 build() 로 동적 청크 생성.
|
|
2169
|
+
async function tryServeLazy(reqUrl) {
|
|
2170
|
+
if (!lazyMode) return null;
|
|
2171
|
+
let pathname = new URL(reqUrl, 'http://localhost').pathname;
|
|
2172
|
+
if (base && base !== '/' && pathname.startsWith(base)) {
|
|
2173
|
+
pathname = '/' + pathname.slice(base.length);
|
|
2174
|
+
}
|
|
2175
|
+
// ① entry alias — served index.html 의 `/bundle.js` 를 lazy entry 청크로.
|
|
2176
|
+
if ((pathname === '/bundle.js' || pathname === '/index.js') && lazyEntryFile) {
|
|
2177
|
+
try {
|
|
2178
|
+
return {
|
|
2179
|
+
status: 200,
|
|
2180
|
+
body: readFileSync(lazyEntryFile),
|
|
2181
|
+
type: 'application/javascript',
|
|
2182
|
+
};
|
|
2183
|
+
} catch {
|
|
2184
|
+
return null; // 파일 사라짐 — 정적 fallback
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
// ② on-demand 동적 청크.
|
|
2188
|
+
const m = pathname.match(/-([0-9a-f]{8})\.js$/);
|
|
2189
|
+
if (!m) return null;
|
|
2190
|
+
const pathHash = m[1];
|
|
2191
|
+
const seedPath = lazySeedMap.get(pathHash);
|
|
2192
|
+
if (!seedPath) return null; // 알 수 없는 hash — 정적 자산일 수 있어 fallback
|
|
2193
|
+
// #4079 PR-3 (dev materialize) — 이 seed 가 방문됐으니 watch worker 가 그것을 force-parse 해
|
|
2194
|
+
// 정식 청크로 emit(이후 정적 서빙) + transitive deps 를 감시(깊은 편집 HMR + warm 재빌드)하게
|
|
2195
|
+
// 한다. native 가 dedup 하므로 매 요청 호출 무해. PR-1 의 path-hash 안정 이름 덕에 on-demand
|
|
2196
|
+
// 청크 URL ↔ watch emit 청크 URL 이 동일해 전환이 매끄럽다. 아래 on-demand build 는 첫 요청
|
|
2197
|
+
// 즉시성용(watch rebuild 는 ≤200ms 비동기). 구 native 바이너리면 메서드 부재 → 옵셔널 no-op.
|
|
2198
|
+
nativeWatchHandle?.requestLazySeed?.(seedPath);
|
|
2199
|
+
const cached = lazyChunkCache.get(pathHash);
|
|
2200
|
+
if (cached) return cached;
|
|
2201
|
+
if (!lazyOnDemandOpts) return null;
|
|
2202
|
+
// 같은 청크 동시 요청은 진행 중 build 를 공유(coalesce).
|
|
2203
|
+
const existing = lazyInflight.get(pathHash);
|
|
2204
|
+
if (existing) return existing;
|
|
2205
|
+
// tail 에 체인 → on-demand build 들을 직렬화(동시 native build()+watch worker 재진입 회피).
|
|
2206
|
+
const job = lazyBuildTail.then(async () => {
|
|
2207
|
+
// 큐 대기 중 cache 가 채워졌으면(다른 동일 요청이 먼저 완료) 그대로 재사용.
|
|
2208
|
+
const c = lazyChunkCache.get(pathHash);
|
|
2209
|
+
if (c) return c;
|
|
2210
|
+
// build 시작 직전 세대 캡처 — 완료 후 변동(=빌드 도중 rebuild) 시 캐시 오염 방지.
|
|
2211
|
+
const epoch = lazyEpoch;
|
|
2212
|
+
try {
|
|
2213
|
+
const r = await build({ ...lazyOnDemandOpts, lazyForceParse: [seedPath] });
|
|
2214
|
+
if (r.errors && r.errors.length > 0) return null;
|
|
2215
|
+
// seed 모듈을 포함한 청크를 moduleIds 로 찾는다(force-parse 라 그 seed 가 어느 청크에 인라인됨).
|
|
2216
|
+
const chunk = (r.outputFiles ?? []).find(
|
|
2217
|
+
(f) => Array.isArray(f.moduleIds) && f.moduleIds.includes(seedPath),
|
|
2218
|
+
);
|
|
2219
|
+
if (!chunk) return null;
|
|
2220
|
+
const result = {
|
|
2221
|
+
status: 200,
|
|
2222
|
+
body: chunk.contents,
|
|
2223
|
+
type: 'application/javascript',
|
|
2224
|
+
// 정밀 무효화용 — 이 청크에 들어간 모듈 절대경로 집합. rebuild 시 event.changed 와
|
|
2225
|
+
// 교집합이 있을 때만 이 항목을 버린다(무관한 편집엔 보존 → cold 재빌드 회피).
|
|
2226
|
+
moduleIds: Array.isArray(chunk.moduleIds) ? chunk.moduleIds : [],
|
|
2227
|
+
};
|
|
2228
|
+
// 빌드 도중 rebuild 가 끼지 않았을 때만 캐시(epoch 불변). 끼었으면 이 결과는 옛 소스
|
|
2229
|
+
// 기반이라 캐시하지 않고(다음 요청이 fresh 재빌드) 이번 응답으로만 반환.
|
|
2230
|
+
if (lazyEpoch === epoch) lazyChunkCache.set(pathHash, result);
|
|
2231
|
+
return result;
|
|
2232
|
+
} catch (err) {
|
|
2233
|
+
console.error('[serve] lazy chunk build failed:', err);
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
lazyBuildTail = job.catch(() => {}); // tail 은 reject 흡수(다음 build 가 멈추지 않게).
|
|
2238
|
+
lazyInflight.set(pathHash, job);
|
|
2239
|
+
try {
|
|
2240
|
+
return await job;
|
|
2241
|
+
} finally {
|
|
2242
|
+
lazyInflight.delete(pathHash);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
1747
2246
|
const useTls = opts.certfile && opts.keyfile;
|
|
1748
2247
|
|
|
2248
|
+
// #3796/#3798 root-cause — watch handle 을 HTTP listen 전에 띄움. cold-start runBundle 제거
|
|
2249
|
+
// 후 watch worker 가 outdir 출력 + appDev hooks 담당. HTTP listen 은 watchReadyPromise 까지
|
|
2250
|
+
// wait → server listen 시점에 outdir 채워진 상태 (race-free).
|
|
2251
|
+
if (opts.bundle && opts.watch && appDev && hmr) {
|
|
2252
|
+
// app dev path (appDev 활성) — caller-pre-warm 으로 prepare 가 처리한
|
|
2253
|
+
// css plugin 을 dispatcher 에서 제거 필요. bundle 모드 (appDev=null) 는
|
|
2254
|
+
// filter false (사용자 명시 css() 가 dispatcher 에 정상 전달).
|
|
2255
|
+
const watchBuildOpts = await buildBundleOptions(opts, config, {
|
|
2256
|
+
filterCallerPreWarmCss: true,
|
|
2257
|
+
});
|
|
2258
|
+
watchBuildOpts.devMode = true;
|
|
2259
|
+
// #4062 PR-B-2 — lazy on-demand 활성화. lazy 동적 청크는 (a) code splitting 으로 분리되고
|
|
2260
|
+
// (b) IIFE registry(`__zntc_require`/`__zntc_load_chunk`) 로 로드되어야 한다. dev 단일파일
|
|
2261
|
+
// 기본(applySingleFileDynamicImportDefault 가 splitting 미설정 시 inlineDynamicImports=true)
|
|
2262
|
+
// 을 명시적으로 끄고 splitting+iife 를 강제한다. on-demand 단발 build() 템플릿도 동일 설정으로
|
|
2263
|
+
// 준비(watch callback/outdir 제거 — build() 는 outdir 없으면 in-memory outputFiles 반환).
|
|
2264
|
+
if (lazyMode) {
|
|
2265
|
+
watchBuildOpts.splitting = true;
|
|
2266
|
+
watchBuildOpts.inlineDynamicImports = false;
|
|
2267
|
+
watchBuildOpts.lazyCompilation = true;
|
|
2268
|
+
watchBuildOpts.format = 'iife';
|
|
2269
|
+
lazyOnDemandOpts = {
|
|
2270
|
+
...watchBuildOpts,
|
|
2271
|
+
splitting: true,
|
|
2272
|
+
inlineDynamicImports: false,
|
|
2273
|
+
lazyCompilation: true,
|
|
2274
|
+
format: 'iife',
|
|
2275
|
+
};
|
|
2276
|
+
delete lazyOnDemandOpts.onReady;
|
|
2277
|
+
delete lazyOnDemandOpts.onRebuild;
|
|
2278
|
+
// outdir/outfile 둘 다 제거 → build() 가 write skip(in-memory outputFiles)해 watch 의
|
|
2279
|
+
// 디스크 출력을 매 요청마다 clobber 하지 않게 한다(/code-review: outfile 누락 footgun).
|
|
2280
|
+
delete lazyOnDemandOpts.outdir;
|
|
2281
|
+
delete lazyOnDemandOpts.outfile;
|
|
2282
|
+
delete lazyOnDemandOpts.watch;
|
|
2283
|
+
}
|
|
2284
|
+
// issue #3858 — appDev path 에서 watchFolders 자동 = app root. 사용자가 직접
|
|
2285
|
+
// .css 파일을 import 없이 만들었을 때 (graph 외) 신규 file 의 add 감지를
|
|
2286
|
+
// native watcher (TrackedFileSet 의 dir-watch) 가 처리하도록 root_dir 등록.
|
|
2287
|
+
// opts._appWatchRoot 는 runAppDev 가 stash 한 사용자 source code root (outdir
|
|
2288
|
+
// 아닌 진짜 src/ 컨테이너). 사용자 명시 watchFolders 가 있으면 union — 우선순위 유지.
|
|
2289
|
+
let reconcileOutdir = null;
|
|
2290
|
+
if (opts._appWatchRoot) {
|
|
2291
|
+
const autoWatchRoot = resolve(opts._appWatchRoot);
|
|
2292
|
+
const existing = Array.isArray(watchBuildOpts.watchFolders)
|
|
2293
|
+
? watchBuildOpts.watchFolders
|
|
2294
|
+
: [];
|
|
2295
|
+
if (!existing.includes(autoWatchRoot)) {
|
|
2296
|
+
watchBuildOpts.watchFolders = [...existing, autoWatchRoot];
|
|
2297
|
+
}
|
|
2298
|
+
// issue #3858 — reconcileOutdir factory 생성 (closure 가 prev/current raw
|
|
2299
|
+
// .css set 추적). onRebuild 마다 호출 — sass/.module/.chunk emit 영향 0.
|
|
2300
|
+
if (opts.outdir) {
|
|
2301
|
+
reconcileOutdir = createReconcileOutdirCss(autoWatchRoot, resolve(opts.outdir));
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
watchBuildOpts.onReady = async (event) => {
|
|
2305
|
+
try {
|
|
2306
|
+
// #3799 root-cause — initial build 의 diagnostics 의 error 도 reportError.
|
|
2307
|
+
if (event && event.errors && event.errors.length > 0) {
|
|
2308
|
+
hmr.reportError(
|
|
2309
|
+
event.errors.map((e) => ({ text: e.message, location: { file: e.file } })),
|
|
2310
|
+
);
|
|
2311
|
+
} else {
|
|
2312
|
+
hmr.clearError();
|
|
2313
|
+
}
|
|
2314
|
+
captureLazyState(event); // #4062 — lazySeed 맵 + entry 청크 식별 (lazyMode 아니면 no-op)
|
|
2315
|
+
// #3796 — event.outputs (path 목록) 를 BundleResult shape 으로 변환해 injectBundleCssLinks.
|
|
2316
|
+
const mockResult = {
|
|
2317
|
+
outputFiles: (event && event.outputs ? event.outputs : []).map((p) => ({ path: p })),
|
|
2318
|
+
};
|
|
2319
|
+
appDev.injectBundleCssLinks(mockResult);
|
|
2320
|
+
await appDev.afterBundle();
|
|
2321
|
+
} catch (err) {
|
|
2322
|
+
console.error('[serve] initial appDev hooks failed:', err);
|
|
2323
|
+
} finally {
|
|
2324
|
+
markWatchReady();
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
watchBuildOpts.onRebuild = (event) => {
|
|
2328
|
+
// dev_mode + collect_module_codes 인 incremental rebuild 는 skip_bundle_output 자동
|
|
2329
|
+
// 활성이라 outdir 갱신 안 함. graphChanged 시 추가 runBundle 호출 (plugin.setup 2회
|
|
2330
|
+
// 시점 — cold-start 는 watch 1회만).
|
|
2331
|
+
void (async () => {
|
|
2332
|
+
try {
|
|
2333
|
+
// #4062 PR-C-1 — rebuild 마다 lazy 상태 갱신: seed 맵을 event.lazySeeds 로 다시 채우고
|
|
2334
|
+
// (신규 동적 import 추가/제거 반영) 청크 캐시를 무효화한다. captureLazyState 가 실패한
|
|
2335
|
+
// rebuild 는 skip, outputs 가 비면(skip_bundle_output) entry 는 유지한다. (PR-B-2 는
|
|
2336
|
+
// onRebuild 가 lazySeeds 미노출이라 cache.clear 만 했음 — PR-C-1 에서 native 가 노출.)
|
|
2337
|
+
captureLazyState(event);
|
|
2338
|
+
// issue #3858 — native onRebuild 의 cssChanges 분기는 PR #3859 머지로
|
|
2339
|
+
// 도입됐으나, drain (fs.watch) 가 이미 같은 fs event 받아 rebuildAppDevCss
|
|
2340
|
+
// (syncDirty + afterBundle, PostCSS incremental) 로 처리. dual watch race
|
|
2341
|
+
// 의 source 였음 + prepare 호출이 PostCSS 를 tempRoot 전체 reprocess →
|
|
2342
|
+
// "processed N" 회귀 (dev-hmr/postcss test fail). drain 단일 처리로 통합.
|
|
2343
|
+
// graph 외 .css ADD case (#3858 핵심) 는 drain 의 fs.watch (recursive)
|
|
2344
|
+
// 가 신규 add event 받아 처리 — native 의 cssChanges 분기 redundant.
|
|
2345
|
+
// 단 graphChanged 시 outdir 갱신은 아래 runBundle 분기가 cover.
|
|
2346
|
+
// issue #3858 — 매 rebuild 마다 outdir reconcile (raw root .css diff
|
|
2347
|
+
// 후 사라진 path 만 outdir unlink). closure factory 의 prev/current
|
|
2348
|
+
// diff 로 outdir 의 sass/.module/.chunk emit 영향 0.
|
|
2349
|
+
if (event && event.success && reconcileOutdir) {
|
|
2350
|
+
reconcileOutdir();
|
|
2351
|
+
}
|
|
2352
|
+
if (event && event.success && event.graphChanged) {
|
|
2353
|
+
try {
|
|
2354
|
+
const r = await runBundle(opts, config);
|
|
2355
|
+
if (r.errors.length === 0) appDev.injectBundleCssLinks(r);
|
|
2356
|
+
} catch (cssErr) {
|
|
2357
|
+
console.error('[serve] graph-change outdir rebuild failed:', cssErr);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const annotated =
|
|
2361
|
+
event && event.success && event.updates && event.updates.length > 0
|
|
2362
|
+
? {
|
|
2363
|
+
...event,
|
|
2364
|
+
updates: event.updates.map((u) => ({
|
|
2365
|
+
...u,
|
|
2366
|
+
code: `${u.code}\n//# sourceMappingURL=${HMR_MAP_PATH}${encodeURIComponent(u.id)}\n`,
|
|
2367
|
+
})),
|
|
2368
|
+
}
|
|
2369
|
+
: event;
|
|
2370
|
+
const outcome = web.broadcastRebuildEvent(hmr, annotated);
|
|
2371
|
+
// #4079: lazy(splitting) dev 는 분할 청크가 dev HMR 모듈 레지스트리에 없어 module-level
|
|
2372
|
+
// HMR 이 불가하다(dev init lowering 이 production init 로 fallback). 그래서 lazy 청크
|
|
2373
|
+
// 안의 모듈을 편집하면 rebuild 는 변경을 감지(event.changed)하지만 module update 를
|
|
2374
|
+
// 못 만들어 broadcastRebuildEvent 가 'noop' 으로 끝나 화면이 안 갱신된다 → full reload 로 갈음.
|
|
2375
|
+
// 단 (a) 'update'(메인 번들 모듈 변경 — 진짜 HMR 적용 가능)와 'full-reload'(graphChanged)
|
|
2376
|
+
// /'error' 는 제외, (b) CSS 변경은 drain 의 CSS-only HMR(live <link> swap)이 처리하므로
|
|
2377
|
+
// *비-CSS* 변경이 하나라도 있을 때만 full reload(전부 CSS 면 CSS HMR 보존).
|
|
2378
|
+
// errors 가 있으면(partial build) overlay 가 latch 된 상태 — full reload 는 overlay 를
|
|
2379
|
+
// 숨기고 깨진 상태로 리로드하므로 skip(에러는 그대로 보여줌, 다음 성공 빌드가 갱신).
|
|
2380
|
+
if (lazyMode && event.success && outcome === 'noop' && !event.errors?.length) {
|
|
2381
|
+
const hasNonCss = (event.changed ?? []).some(
|
|
2382
|
+
(p) => !/\.(css|scss|sass|less|styl|pcss|postcss)$/i.test(p),
|
|
2383
|
+
);
|
|
2384
|
+
if (hasNonCss) hmr.broadcast({ type: web.HMR_MSG.FullReload, timestamp: Date.now() });
|
|
2385
|
+
}
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
console.error('[serve] hmr broadcast error:', err);
|
|
2388
|
+
}
|
|
2389
|
+
})();
|
|
2390
|
+
};
|
|
2391
|
+
try {
|
|
2392
|
+
nativeWatchHandle = watch(watchBuildOpts);
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
console.error('[serve] native watch failed to start (incremental HMR disabled):', err);
|
|
2395
|
+
markWatchReady();
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// #3796 — HTTP listen 을 watch.onReady 까지 wait (5초 timeout 으로 deadlock 방어).
|
|
2400
|
+
await Promise.race([watchReadyPromise, new Promise((r) => setTimeout(r, 5000))]);
|
|
2401
|
+
|
|
1749
2402
|
if (isBun) {
|
|
1750
2403
|
// Bun.serve
|
|
1751
2404
|
const serveOpts = {
|
|
1752
2405
|
port: opts.port,
|
|
1753
2406
|
hostname: opts.host,
|
|
1754
|
-
fetch(req, server) {
|
|
2407
|
+
async fetch(req, server) {
|
|
1755
2408
|
const url = new URL(req.url);
|
|
1756
2409
|
// /__hmr WebSocket upgrade — Bun-native API 사용 (Node 분기는 server.on('upgrade')).
|
|
2410
|
+
// upgrade 는 첫 await 전에 동기 실행돼 async fetch 여도 안전.
|
|
1757
2411
|
if (hmr && url.pathname === APP_DEV_HMR_WS_PATH) {
|
|
1758
2412
|
if (server.upgrade(req)) return undefined;
|
|
1759
2413
|
return new Response('Upgrade required', { status: 426 });
|
|
@@ -1765,6 +2419,18 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1765
2419
|
}
|
|
1766
2420
|
}
|
|
1767
2421
|
|
|
2422
|
+
// #4062 PR-B-2 — lazy 라우트(entry alias + on-demand 동적 청크). null = 정적 처리로.
|
|
2423
|
+
const lazy = await tryServeLazy(req.url);
|
|
2424
|
+
if (lazy) {
|
|
2425
|
+
return new Response(lazy.body, {
|
|
2426
|
+
status: lazy.status,
|
|
2427
|
+
headers: {
|
|
2428
|
+
'Content-Type': lazy.type,
|
|
2429
|
+
'Access-Control-Allow-Origin': '*',
|
|
2430
|
+
},
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
|
|
1768
2434
|
const { status, body, type } = handleRequest(req.url, req.headers.get('accept') ?? '');
|
|
1769
2435
|
return new Response(body, {
|
|
1770
2436
|
status,
|
|
@@ -1819,6 +2485,17 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1819
2485
|
}
|
|
1820
2486
|
}
|
|
1821
2487
|
|
|
2488
|
+
// #4062 PR-B-2 — lazy 라우트(entry alias + on-demand 동적 청크). null = 정적 처리로.
|
|
2489
|
+
const lazy = await tryServeLazy(req.url);
|
|
2490
|
+
if (lazy) {
|
|
2491
|
+
res.writeHead(lazy.status, {
|
|
2492
|
+
'Content-Type': lazy.type,
|
|
2493
|
+
'Access-Control-Allow-Origin': '*',
|
|
2494
|
+
});
|
|
2495
|
+
res.end(Buffer.isBuffer(lazy.body) ? lazy.body : Buffer.from(lazy.body));
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
1822
2499
|
const { status, body, type } = handleRequest(req.url, req.headers.accept ?? '');
|
|
1823
2500
|
res.writeHead(status, {
|
|
1824
2501
|
'Content-Type': type,
|
|
@@ -1863,6 +2540,20 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1863
2540
|
}
|
|
1864
2541
|
|
|
1865
2542
|
async function closeServerForRestart() {
|
|
2543
|
+
// #3779 follow-up — native watch worker thread 가 child process spawn 후에도 살아남으면
|
|
2544
|
+
// outdir 출력이 부모/자식 두 곳에서 일어나 race. emitRestartAfter 의 child spawn 전에 stop.
|
|
2545
|
+
// stop 자체가 throw 해도 server.close 는 시도 (HTTP 포트 해제 우선).
|
|
2546
|
+
// #3803 — stop() throw 시 nativeWatchHandle 을 null 하지 않고 유지 → 다음 호출에서 retry
|
|
2547
|
+
// 가능. 정상 path 에서만 null 로 갱신. idempotent stop (#3794 의 napi_remove_wrap) 라
|
|
2548
|
+
// 다음 시도도 안전.
|
|
2549
|
+
if (nativeWatchHandle) {
|
|
2550
|
+
try {
|
|
2551
|
+
nativeWatchHandle.stop();
|
|
2552
|
+
nativeWatchHandle = null;
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
console.error('[serve] native watch stop failed (will retry on next invocation):', err);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
1866
2557
|
if (!serverHandle) return;
|
|
1867
2558
|
if (typeof serverHandle.stop === 'function') {
|
|
1868
2559
|
await serverHandle.stop();
|
|
@@ -1889,8 +2580,26 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1889
2580
|
let rebuilding = false;
|
|
1890
2581
|
const dirty = new Set();
|
|
1891
2582
|
|
|
2583
|
+
// #3796 — native watch handle 은 HTTP listen 전에 띄움 (위쪽 코드). 이 블록은
|
|
2584
|
+
// fsWatch + drain (CSS / sass / postcss / restart) 만 — JS 변경은 native watch 가 단독 처리.
|
|
2585
|
+
|
|
1892
2586
|
async function rebuildAppDevCss(changedPath) {
|
|
1893
|
-
|
|
2587
|
+
// issue #3861 — drain (fs.watch) 가 prepare skip 시 tempRoot 가 raw root
|
|
2588
|
+
// 와 동기 안 되어 afterBundle 의 mirror 가 stale .css 를 outdir 에 다시
|
|
2589
|
+
// write → reconcileOutdirCss 의 unlink 무효화 (dual watch race).
|
|
2590
|
+
// syncDirty 는 prepare 의 syncDirtyFilesIntoTempRoot 만 호출 — PostCSS
|
|
2591
|
+
// 재실행 skip (prepare 가 full PostCSS reprocess 라 단일 CSS modify 시
|
|
2592
|
+
// 전체 .css 처리 → "processed N" 회귀, dev-hmr/postcss test fail). PostCSS
|
|
2593
|
+
// incremental 처리는 아래 afterBundle 의 changedPath 분기가 cover.
|
|
2594
|
+
appDev.syncDirty([changedPath]);
|
|
2595
|
+
try {
|
|
2596
|
+
await appDev.afterBundle({ changedPath });
|
|
2597
|
+
} catch (cssErr) {
|
|
2598
|
+
if (opts.logLevel !== 'silent') {
|
|
2599
|
+
console.error('[serve] css afterBundle failed:', cssErr);
|
|
2600
|
+
}
|
|
2601
|
+
throw cssErr; // drain outer catch (line 2460) 가 hmr.reportThrownError.
|
|
2602
|
+
}
|
|
1894
2603
|
hmr?.clearError();
|
|
1895
2604
|
hmr?.broadcast({
|
|
1896
2605
|
type: HMR_MSG.CssUpdate,
|
|
@@ -1900,6 +2609,9 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1900
2609
|
if (opts.logLevel !== 'silent') console.error('[serve] css updated');
|
|
1901
2610
|
}
|
|
1902
2611
|
|
|
2612
|
+
// #3779 — sass partial / 다중 sass 변경 (drain 의 sass 분기) 전용. SCSS dependents
|
|
2613
|
+
// 까지 재컴파일해야 하므로 full pipeline + FullReload. JS module 변경은 native watch
|
|
2614
|
+
// handle 이 단독 처리 — 이 함수는 더 이상 JS 분기에서 호출되지 않는다.
|
|
1903
2615
|
async function rebuildAppDevFull(dirtyPaths = null) {
|
|
1904
2616
|
const prepared = await appDev.prepare(dirtyPaths);
|
|
1905
2617
|
opts.entryPoints = [prepared.entryPath];
|
|
@@ -1947,6 +2659,12 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1947
2659
|
} else {
|
|
1948
2660
|
await rebuildAppDevFull();
|
|
1949
2661
|
}
|
|
2662
|
+
} else if (paths.some((p) => p.endsWith('.scss') || p.endsWith('.sass'))) {
|
|
2663
|
+
// #71: sass 변경인데 fast-path 자격 없음(다른 root scss 가 @import 하는 partial,
|
|
2664
|
+
// 또는 다중 sass 변경) → full pipeline rebuild 로 dirty 의 transitive dependents 까지
|
|
2665
|
+
// 재컴파일. CSS-only 분기(afterBundle = postcss only)는 sass 를 재컴파일하지 않아
|
|
2666
|
+
// partial 변경 시 root scss 가 stale 로 남는다(code-review max 적발).
|
|
2667
|
+
await rebuildAppDevFull(paths);
|
|
1950
2668
|
} else if (cssChanges.length === 1 && paths.length === 1) {
|
|
1951
2669
|
await rebuildAppDevCss(cssChanges[0]);
|
|
1952
2670
|
} else {
|
|
@@ -1956,7 +2674,33 @@ async function runServe(opts, config, { appDev = null } = {}) {
|
|
|
1956
2674
|
if (opts.logLevel !== 'silent') console.error('[serve] css updated');
|
|
1957
2675
|
}
|
|
1958
2676
|
} else {
|
|
1959
|
-
|
|
2677
|
+
// #3797 — paths 의 종류에 따라 3분기:
|
|
2678
|
+
// - 전부 CSS-derived (CSS Module / Sass Module / postcss): native watch 의 module
|
|
2679
|
+
// graph 밖이라 incremental update 트리거 안 됨 → full pipeline + FullReload.
|
|
2680
|
+
// - JS module (.ts/.tsx/.js 등) 섞임: native watch 가 단독 처리. drain noop
|
|
2681
|
+
// (중복 컴파일/output race 회피).
|
|
2682
|
+
// - 그 외 (HTML / JSON / static asset 등 native watch graph 밖 + non-CSS): fallback
|
|
2683
|
+
// FullReload broadcast. pre-#3779 의 unconditional rebuildAppDevFull 회귀 가드.
|
|
2684
|
+
// #3801 — appDev 의 isCssLikeChange 가 단일 진실 소스. inline literal endsWith
|
|
2685
|
+
// 가 `.less` (미지원) 포함하거나 `.styl/.pcss` 누락하던 drift 회귀 방지.
|
|
2686
|
+
const isJsModule = (p) =>
|
|
2687
|
+
p.endsWith('.ts') ||
|
|
2688
|
+
p.endsWith('.tsx') ||
|
|
2689
|
+
p.endsWith('.js') ||
|
|
2690
|
+
p.endsWith('.jsx') ||
|
|
2691
|
+
p.endsWith('.mjs') ||
|
|
2692
|
+
p.endsWith('.cjs');
|
|
2693
|
+
const allCssDerived = paths.every((p) => appDev.isCssLikeChange(p));
|
|
2694
|
+
const hasJs = paths.some(isJsModule);
|
|
2695
|
+
if (allCssDerived) {
|
|
2696
|
+
await rebuildAppDevFull(paths);
|
|
2697
|
+
} else if (!hasJs) {
|
|
2698
|
+
// HTML/JSON/asset 변경 — native watch 가 안 보는 변경에 대한 reload 신호.
|
|
2699
|
+
hmr?.clearError();
|
|
2700
|
+
hmr?.broadcast({ type: HMR_MSG.FullReload, timestamp: Date.now() });
|
|
2701
|
+
if (opts.logLevel !== 'silent') console.error('[serve] file changed, full reload');
|
|
2702
|
+
}
|
|
2703
|
+
// hasJs=true: native watch handle 의 onRebuild 가 단독 broadcast. drain noop.
|
|
1960
2704
|
}
|
|
1961
2705
|
}
|
|
1962
2706
|
} catch (err) {
|