@fuzionx/framework 0.1.54 → 0.1.56
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/cli/templates/make/app-spa/views/default/spa/package.json +1 -1
- package/cli/templates/make/app-ssr/public/js/fx-player.umd.js +7 -0
- package/cli/templates/make/app-ssr/views/default/pages/live/room.html +1 -1
- package/cli/templates/make/app-ssr/views/default/pages/live/watch.html +1 -1
- package/lib/core/Application.js +0 -5
- package/lib/core/Config.js +1 -29
- package/package.json +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var FuzionXPlayer=(()=>{var S=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var T=(h,e)=>{for(var t in e)S(h,t,{get:e[t],enumerable:!0})},R=(h,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of I(e))!k.call(h,o)&&o!==t&&S(h,o,{get:()=>e[o],enumerable:!(i=E(e,o))||i.enumerable});return h};var y=h=>R(S({},"__esModule",{value:!0}),h);var O={};T(O,{CODEC:()=>C,DEFAULT_ICE_SERVERS:()=>u,FuzionXPublisher:()=>g,FuzionXSignaling:()=>_,FuzionXViewer:()=>m,MAX_SLOTS:()=>f,RECONNECT:()=>p,SessionMode:()=>d,SignalType:()=>c});var u=[{urls:"stun:stun.l.google.com:19302"},{urls:"stun:stun1.l.google.com:19302"}],c={JOIN:"join",OFFER:"offer",ANSWER:"answer",CANDIDATE:"candidate",PLI:"pli",LEAVE:"leave",SLOT_INFO:"slot_info",CHAT:"chat",ERROR:"error"},d={BROADCAST:"broadcast",VIDEOCHAT:"videochat"},f={[d.BROADCAST]:1,[d.VIDEOCHAT]:9},p={MAX_RETRIES:5,BASE_DELAY_MS:1e3,MAX_DELAY_MS:3e4},C={VIDEO_MIME:"video/H264",AUDIO_MIME:"audio/opus",VIDEO_CLOCK:9e4,AUDIO_CLOCK:48e3};var _=class{constructor(e){this.url=e.url,this.onMessage=e.onMessage||(()=>{}),this.onOpen=e.onOpen||(()=>{}),this.onClose=e.onClose||(()=>{}),this.onError=e.onError||(()=>{}),this.autoReconnect=e.autoReconnect!==!1,this._ws=null,this._retryCount=0,this._reconnectTimer=null,this._intentionalClose=!1}connect(){this._intentionalClose=!1,this._doConnect()}_doConnect(){try{this._ws=new WebSocket(this.url)}catch(e){this.onError(e),this._scheduleReconnect();return}this._ws.onopen=()=>{this._retryCount=0,this.onOpen()},this._ws.onmessage=e=>{try{let t=JSON.parse(e.data);this.onMessage(t)}catch{console.warn("[FuzionX] Invalid JSON:",e.data)}},this._ws.onclose=e=>{this.onClose(e),!this._intentionalClose&&this.autoReconnect&&this._scheduleReconnect()},this._ws.onerror=e=>{this.onError(e)}}send(e){return this._ws&&this._ws.readyState===WebSocket.OPEN?(this._ws.send(JSON.stringify(e)),!0):!1}sendJoin(e,t,i={}){return this.send({type:c.JOIN,peer_id:e,channel_id:t,nickname:i.nickname||null,token:i.token||null,mode:i.mode||null})}sendOffer(e){return this.send({type:c.OFFER,sdp:e})}sendAnswer(e){return this.send({type:c.ANSWER,sdp:e})}sendCandidate(e){return this.send({type:c.CANDIDATE,candidate:e.candidate,sdp_mid:e.sdpMid,sdp_m_line_index:e.sdpMLineIndex})}sendChat(e,t){return this.send({type:c.CHAT,text:e,nickname:t||null,peer_id:null})}sendPLI(){return this.send({type:c.PLI})}sendLeave(){return this.send({type:c.LEAVE})}disconnect(){this._intentionalClose=!0,clearTimeout(this._reconnectTimer),this._ws&&(this._ws.close(),this._ws=null)}get connected(){return this._ws&&this._ws.readyState===WebSocket.OPEN}_scheduleReconnect(){if(this._retryCount>=p.MAX_RETRIES){console.error("[FuzionX] Max reconnect retries reached."),this.onError(new Error("Max reconnect retries"));return}let e=Math.min(p.BASE_DELAY_MS*Math.pow(2,this._retryCount),p.MAX_DELAY_MS);this._retryCount++,console.log(`[FuzionX] Reconnecting in ${e}ms (${this._retryCount}/${p.MAX_RETRIES})`),this._reconnectTimer=setTimeout(()=>this._doConnect(),e)}};var m=class h{constructor(e){this.url=e.url||null,this.hubUrl=e.hubUrl||null,this.channelId=e.channelId,this.mode=e.mode||d.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`viewer-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._listeners={},this._slots=new Map,this._maxSlots=f[this.mode]||1,this._candidateQueue=[],this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(i=>i(...t))}async connect(){if(!this.url&&this.hubUrl)try{let e=await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);if(!e.ok)throw new Error(`Channel not found: ${this.channelId}`);let t=await e.json();if(t.ws_url){let i=this.hubUrl.startsWith("https");this.url=t.ws_url.replace(/^ws(s?):/,i?"wss:":"ws:")}else{let o=this.hubUrl.startsWith("https")?"wss":"ws";this.url=`${o}://${t.media_ip}:${t.webrtc_port}`}}catch(e){this._emit("error",e);return}if(!this.url){this._emit("error",new Error("url \uB610\uB294 hubUrl\uC744 \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."));return}this._signaling=new _({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>this._onSignalingClose(e),onError:e=>this._emit("error",e)}),this._signaling.connect(),this._beforeUnloadHandler=()=>{this._signaling&&this._signaling.connected&&this._signaling.sendLeave(),this._closePeerConnection()},window.addEventListener("beforeunload",this._beforeUnloadHandler)}async disconnect(){this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),this._signaling&&(this._signaling.sendLeave(),await new Promise(e=>setTimeout(e,100)),this._signaling.disconnect()),this._closePeerConnection(),this._slots.clear(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}requestKeyframe(){this._signaling&&this._signaling.sendPLI()}get slots(){return this._slots}_onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),this._createPeerConnection().catch(e=>this._emit("error",e))}_onSignalingMessage(e){switch(e.type){case c.ANSWER:this._handleAnswer(e);break;case c.CANDIDATE:this._handleCandidate(e);break;case c.SLOT_INFO:this._handleSlotInfo(e);break;case c.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case c.ERROR:this._emit("error",new Error(e.message));break}}_onSignalingClose(e){this._closePeerConnection(),this._connected=!1,this._emit("close",e)}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch(i){console.warn("[FuzionX] ICE candidate error:",i)}else this._candidateQueue.push(t)}_handleSlotInfo(e){let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){let o=this._slots.get(t);o&&this._slots.set(t,{slotIndex:t,stream:o.stream}),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let i=this._slots.get(t)||{};this._slots.set(t,{...i,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}async _createPeerConnection(){this._pc=new RTCPeerConnection(this.rtcConfig);let e=0;this._pc.ontrack=o=>{let n=o.track;if(n.kind==="video"){let s=e++,r=o.streams[0];r||(r=new MediaStream,r.addTrack(n));let a=this._slots.get(s)||{slotIndex:s};a.stream=r,this._slots.set(s,a),this._emit("stream",r,s),this._connected||(this._connected=!0,this._emit("connected"),this._signaling&&this._signaling.sendPLI())}},this._pc.onconnectionstatechange=()=>{let o=this._pc?.connectionState;(o==="failed"||o==="disconnected")&&this._emit("error",new Error(`PeerConnection ${o}`))};for(let o=0;o<this._maxSlots;o++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});let t=await this._pc.createOffer();t.sdp=h._forceCodecs(t.sdp),await this._pc.setLocalDescription(t),await this._waitForIceGathering();let i=this._pc.localDescription?.sdp;i&&this._signaling.sendOffer(i)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},150)})}static _forceCodecs(e){let t=e.split(`\r
|
|
2
|
+
`),i=t.findIndex(n=>n.startsWith("m=video"));if(i!==-1){let n=[],s=new Map;if(t.forEach(r=>{let a=r.match(/a=rtpmap:(\d+) H264\/90000/);a&&(n.push(a[1]),s.set(a[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let a=r.split(" ")[0].split(":")[1];s.has(a)&&(r.includes("profile-level-id=42e01f")?s.set(a,100):r.includes("profile-level-id=42001f")&&s.set(a,80),r.includes("packetization-mode=1")&&s.set(a,(s.get(a)||0)+10))}}),n.length>0){n.sort((l,w)=>s.get(w)-s.get(l));let r=t[i].split(" "),a=r.slice(3).filter(l=>!n.includes(l));t[i]=[...r.slice(0,3),...n,...a].join(" ")}}let o=t.findIndex(n=>n.startsWith("m=audio"));if(o!==-1){let n=[];if(t.forEach(s=>{let r=s.match(/a=rtpmap:(\d+) opus\/48000/);r&&n.push(r[1])}),n.length>0){let s=t[o].split(" "),r=s.slice(3).filter(a=>!n.includes(a));t[o]=[...s.slice(0,3),...n,...r].join(" ")}}return t.join(`\r
|
|
3
|
+
`)}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};var g=class h{constructor(e){this.whipUrl=e.whipUrl||null,this.url=e.url||null,this.hubUrl=e.hubUrl||null,this.channelId=e.channelId||null,this.mode=e.mode||d.BROADCAST,this.nickname=e.nickname||null,this.token=e.token||null,this.peerId=e.peerId||`pub-${Math.random().toString(36).slice(2,10)}`,this.autoReconnect=e.autoReconnect!==!1,this.mediaConstraints=e.media||{video:!0,audio:!0},this._externalStream=e.stream||null,this.rtcConfig=e.rtcConfig||{iceServers:u,bundlePolicy:"max-bundle",rtcpMuxPolicy:"require"},this._signaling=null,this._pc=null,this._localStream=null,this._listeners={},this._candidateQueue=[],this._whipResourceUrl=null,this._maxSlots=f[this.mode]||1,this._slots=new Map,this._connected=!1}on(e,t){return this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t),this}_emit(e,...t){(this._listeners[e]||[]).forEach(i=>i(...t))}async connect(){try{if(!this.url&&!this.whipUrl&&this.hubUrl&&this.channelId){let e=await fetch(`${this.hubUrl}/api/channels`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:this.channelId,source_type:"webrtc"})}),t;if(e.status===409){let i=await fetch(`${this.hubUrl}/api/channels/${this.channelId}`);if(!i.ok)throw new Error(`Channel not found: ${this.channelId}`);t=await i.json()}else if(e.ok)t=await e.json();else throw new Error(`Failed to create channel: ${this.channelId}`);if(t.ws_url){let i=this.hubUrl.startsWith("https");this.url=t.ws_url.replace(/^ws(s?):/,i?"wss:":"ws:")}else{let o=this.hubUrl.startsWith("https")?"wss":"ws";this.url=`${o}://${t.media_ip}:${t.webrtc_port}`}}if(await this._acquireMedia(),this.whipUrl)await this._connectWhip();else if(this.url)this._connectWebSocket();else throw new Error("url, hubUrl, \uB610\uB294 whipUrl \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.");this._beforeUnloadHandler=()=>{this._signaling&&this._signaling.connected&&this._signaling.sendLeave(),this._closePeerConnection(),this._stopMedia()},window.addEventListener("beforeunload",this._beforeUnloadHandler)}catch(e){this._emit("error",e)}}async disconnect(){if(this._beforeUnloadHandler&&(window.removeEventListener("beforeunload",this._beforeUnloadHandler),this._beforeUnloadHandler=null),this.whipUrl&&this._whipResourceUrl){try{let e=new URL(this.whipUrl).origin;await fetch(`${e}${this._whipResourceUrl}`,{method:"DELETE"})}catch(e){console.warn("[FuzionX] WHIP DELETE error:",e)}this._whipResourceUrl=null}this._signaling&&(this._signaling.sendLeave(),await new Promise(e=>setTimeout(e,100)),this._signaling.disconnect()),this._closePeerConnection(),this._stopMedia(),this._connected=!1}chat(e){this._signaling&&this._signaling.sendChat(e,this.nickname)}get localStream(){return this._localStream}get slots(){return this._slots}async _acquireMedia(){this._externalStream?this._localStream=this._externalStream:this._localStream=await navigator.mediaDevices.getUserMedia(this.mediaConstraints),this._emit("media",this._localStream)}_stopMedia(){this._localStream&&!this._externalStream&&this._localStream.getTracks().forEach(e=>e.stop()),this._localStream=null}async _connectWhip(){this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(s=>{this._pc.addTrack(s,this._localStream)});let e=await this._pc.createOffer();e.sdp=h._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await new Promise(s=>{this._pc.iceGatheringState==="complete"?s():(this._pc.onicegatheringstatechange=()=>{this._pc.iceGatheringState==="complete"&&s()},setTimeout(s,150))});let t=this._pc.localDescription,i=this.whipUrl;this.token&&!i.includes("token=")&&(i+=(i.includes("?")?"&":"?")+`token=${this.token}`);let o=await fetch(i,{method:"POST",headers:{"Content-Type":"application/sdp"},body:t.sdp});if(o.status!==201)throw new Error(`WHIP failed: ${o.status} ${await o.text()}`);let n=await o.text();this._whipResourceUrl=o.headers.get("location"),await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:n})),this._pc.onconnectionstatechange=()=>{let s=this._pc?.connectionState;s==="connected"?(this._connected=!0,this._emit("ready")):(s==="failed"||s==="disconnected")&&this._emit("error",new Error(`WHIP PeerConnection ${s}`))},this._emit("ready")}_connectWebSocket(){this._signaling=new _({url:this.url,autoReconnect:this.autoReconnect,onOpen:()=>this._onSignalingOpen(),onMessage:e=>this._onSignalingMessage(e),onClose:e=>{this._closePeerConnection(),this._connected=!1,this._emit("close",e)},onError:e=>this._emit("error",e)}),this._signaling.connect()}async _onSignalingOpen(){this._signaling.sendJoin(this.peerId,this.channelId,{nickname:this.nickname,token:this.token,mode:this.mode}),await this._createPeerConnection()}async _createPeerConnection(){if(this._pc=new RTCPeerConnection(this.rtcConfig),this._localStream.getTracks().forEach(i=>{this._pc.addTrack(i,this._localStream)}),this.mode===d.VIDEOCHAT){for(let n=0;n<this._maxSlots;n++)this._pc.addTransceiver("video",{direction:"recvonly"}),this._pc.addTransceiver("audio",{direction:"recvonly"});this._pc.getTransceivers().forEach(n=>{n.sender.track&&n.direction==="recvonly"&&(n.direction="sendrecv")});let o=0;this._pc.ontrack=n=>{let s=n.track;if(s.kind==="video"){let r=o++,a=n.streams[0];a||(a=new MediaStream,a.addTrack(s));let l=this._slots.get(r)||{slotIndex:r};l.stream=a,this._slots.set(r,l),this._emit("stream",a,r)}}}this._pc.onconnectionstatechange=()=>{let i=this._pc?.connectionState;i==="connected"&&!this._connected?(this._connected=!0,this._emit("ready")):(i==="failed"||i==="disconnected")&&this._emit("error",new Error(`PeerConnection ${i}`))};let e=await this._pc.createOffer();e.sdp=h._forceCodecs(e.sdp),await this._pc.setLocalDescription(e),await this._waitForIceGathering();let t=this._pc.localDescription?.sdp;t&&this._signaling.sendOffer(t)}_waitForIceGathering(){return new Promise(e=>{if(this._pc.iceGatheringState==="complete")return e();let t=()=>{this._pc?.iceGatheringState==="complete"&&(this._pc.removeEventListener("icegatheringstatechange",t),e())};this._pc.addEventListener("icegatheringstatechange",t),setTimeout(()=>{this._pc&&this._pc.removeEventListener("icegatheringstatechange",t),e()},150)})}static _forceCodecs(e){let t=e.split(`\r
|
|
4
|
+
`),i=t.findIndex(n=>n.startsWith("m=video"));if(i!==-1){let n=[],s=new Map;if(t.forEach(r=>{let a=r.match(/a=rtpmap:(\d+) H264\/90000/);a&&(n.push(a[1]),s.set(a[1],0))}),t.forEach(r=>{if(r.startsWith("a=fmtp:")){let a=r.split(" ")[0].split(":")[1];s.has(a)&&(r.includes("profile-level-id=42e01f")?s.set(a,100):r.includes("profile-level-id=42001f")&&s.set(a,80),r.includes("packetization-mode=1")&&s.set(a,(s.get(a)||0)+10))}}),n.length>0){n.sort((l,w)=>s.get(w)-s.get(l));let r=t[i].split(" "),a=r.slice(3).filter(l=>!n.includes(l));t[i]=[...r.slice(0,3),...n,...a].join(" ")}}let o=t.findIndex(n=>n.startsWith("m=audio"));if(o!==-1){let n=[];if(t.forEach(s=>{let r=s.match(/a=rtpmap:(\d+) opus\/48000/);r&&n.push(r[1])}),n.length>0){let s=t[o].split(" "),r=s.slice(3).filter(a=>!n.includes(a));t[o]=[...s.slice(0,3),...n,...r].join(" ")}}return t.join(`\r
|
|
5
|
+
`)}_onSignalingMessage(e){switch(e.type){case c.ANSWER:this._handleAnswer(e);break;case c.CANDIDATE:this._handleCandidate(e);break;case c.SLOT_INFO:this._handleSlotInfo(e);break;case c.CHAT:this._emit("chat",{peerId:e.peer_id,nickname:e.nickname,text:e.text});break;case c.ERROR:this._emit("error",new Error(e.message));break}}async _handleAnswer(e){if(this._pc)try{await this._pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:e.sdp}));for(let t of this._candidateQueue)await this._pc.addIceCandidate(t);this._candidateQueue=[]}catch(t){this._emit("error",t)}}async _handleCandidate(e){let t=new RTCIceCandidate({candidate:e.candidate,sdpMid:e.sdp_mid,sdpMLineIndex:e.sdp_m_line_index});if(this._pc&&this._pc.remoteDescription)try{await this._pc.addIceCandidate(t)}catch{}else this._candidateQueue.push(t)}_handleSlotInfo(e){if(e.sender_id===this.peerId)return;let t=parseInt(e.stream_id.replace("stream_",""),10);if(!e.nickname||e.nickname===""){let o=this._slots.get(t);o&&this._slots.set(t,{slotIndex:t,stream:o.stream}),this._emit("slot_remove",{slotIndex:t,senderId:e.sender_id});return}let i=this._slots.get(t)||{};this._slots.set(t,{...i,slotIndex:t,streamId:e.stream_id,nickname:e.nickname,senderId:e.sender_id}),this._emit("slot",this._slots.get(t))}_closePeerConnection(){this._pc&&(this._pc.close(),this._pc=null),this._candidateQueue=[]}};return y(O);})();
|
|
6
|
+
if(typeof module!=="undefined")module.exports=FuzionXPlayer;
|
|
7
|
+
//# sourceMappingURL=fx-player.umd.js.map
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
{% block description %}FuzionX WebRTC video chat room. Up to 9 participants.{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block head %}
|
|
6
|
-
<script src="/public/js/
|
|
6
|
+
<script src="/public/js/fx-player.umd.js"></script>
|
|
7
7
|
<style>
|
|
8
8
|
.live-role-selector{display:flex;gap:1rem;}
|
|
9
9
|
.live-role-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:.4rem;padding:1.25rem;border:2px solid var(--border-color,rgba(255,255,255,.08));border-radius:12px;background:transparent;cursor:pointer;transition:all .2s;}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
{% block description %}Watch live broadcast on FuzionX WebRTC.{% endblock %}
|
|
4
4
|
|
|
5
5
|
{% block head %}
|
|
6
|
-
<script src="/public/js/
|
|
6
|
+
<script src="/public/js/fx-player.umd.js"></script>
|
|
7
7
|
<style>
|
|
8
8
|
.live-watch-layout{display:flex;height:calc(100vh - 60px);overflow:hidden;}
|
|
9
9
|
.live-watch-video{flex:1;display:flex;flex-direction:column;background:#000;}
|
package/lib/core/Application.js
CHANGED
|
@@ -278,11 +278,6 @@ export default class Application {
|
|
|
278
278
|
this._propagateBridge();
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
// 1. .env 로드 (17-config.md)
|
|
282
|
-
this.config.loadEnv(this.baseDir);
|
|
283
|
-
// .env 로드 후 캐시 클리어 (새로운 환경변수 반영)
|
|
284
|
-
this.config._cache?.clear();
|
|
285
|
-
|
|
286
281
|
await this.emit('booting');
|
|
287
282
|
|
|
288
283
|
// 2. i18n 로드 (04-bootstrap-lifecycle.md)
|
package/lib/core/Config.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* fuzionx.yaml의 3개 섹션(bridge, database, app)을 통합 관리.
|
|
5
5
|
* dot-notation으로 접근: config.get('app.auth.secret')
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* ${VAR:default} 시스템 환경변수 치환 지원.
|
|
8
8
|
* YAML 파일 직접 파싱 지원 (외부 의존 없이 내장 파서 사용).
|
|
9
9
|
*
|
|
10
10
|
* @see docs/framework/17-config.md
|
|
@@ -235,34 +235,6 @@ export default class Config {
|
|
|
235
235
|
return raw;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
/**
|
|
239
|
-
* .env 파일 로드 (17-config.md)
|
|
240
|
-
* 부트 시 자동 호출. 이미 설정된 환경변수는 덮어쓰지 않음.
|
|
241
|
-
* @param {string} [baseDir='.']
|
|
242
|
-
*/
|
|
243
|
-
loadEnv(baseDir = '.') {
|
|
244
|
-
const envPath = path.resolve(baseDir, '.env');
|
|
245
|
-
if (!existsSync(envPath)) return;
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
const content = readFileSync(envPath, 'utf-8');
|
|
249
|
-
for (const line of content.split('\n')) {
|
|
250
|
-
const trimmed = line.trim();
|
|
251
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
252
|
-
const eqIdx = trimmed.indexOf('=');
|
|
253
|
-
if (eqIdx === -1) continue;
|
|
254
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
255
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
256
|
-
// .env 인용부호 제거 ("value" → value, 'value' → value)
|
|
257
|
-
value = value.replace(/^(['"])(.*)\1$/, '$2');
|
|
258
|
-
// 시스템 환경변수 우선 (17-config.md)
|
|
259
|
-
if (!(key in process.env)) {
|
|
260
|
-
process.env[key] = value;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
} catch {} // .env 읽기 실패 시 무시
|
|
264
|
-
}
|
|
265
|
-
|
|
266
238
|
/**
|
|
267
239
|
* YAML 객체의 ${VAR} / ${VAR:default} 패턴 치환 (17-config.md)
|
|
268
240
|
* @param {object} obj
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "https://github.com/saytohenry/fuzionx"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@fuzionx/core": "^0.1.
|
|
37
|
+
"@fuzionx/core": "^0.1.56",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|