@schalkneethling/miyagi-core 4.2.0 → 4.3.1

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/dist/js/iframe.js CHANGED
@@ -1 +1 @@
1
- customElements.define("accordion-tabs",class extends HTMLElement{#e;#t;#s;#i;#n;static get observedAttributes(){return["breakpoint","current"]}constructor(){super()}connectedCallback(){if(!this.closest("code")){if(this.content=[],this.hasAttribute("breakpoint")){const e=this.getAttribute("breakpoint"),t=parseInt(e,10);let s;s=e.endsWith("rem")?t*parseInt(window.getComputedStyle(document.documentElement).fontSize,10):e.endsWith("em")?t*parseInt(window.getComputedStyle(this).fontSize,10):t,this.#t=s}window.requestAnimationFrame(()=>{this.details=Array.from(this.children),this.details.forEach((e,t)=>{const s=e.querySelector("summary");e.open&&(this.index=t),this.content.push({title:s.textContent,content:[...s.parentElement.children].filter(e=>1===e.nodeType&&e!==s)})});let e=!1;this.#i=new ResizeObserver(t=>{if(e)for(const e of t)this.#r(e.borderBoxSize[0].inlineSize);e=!0}),this.#r(this.clientWidth,()=>{this.#i.observe(this)})})}}attributeChangedCallback(e,t,s){"current"===e&&(this.index=parseInt(s,10),this.#r(this.clientWidth))}disconnectedCallback(){this.#i&&this.#i.disconnect()}async#r(e,t){!this.#t||e<this.#t?(await this.#o(),this.#s="accordion"):(await this.#a(),this.#s="tabs"),t&&t()}#o(){return this.#e||(this.#e=new e(this)),"tabs"===this.#s?(this.#c(),this.#e.setElements(),this.#e.elements.forEach(e=>this.appendChild(e))):this.#e.render(),!0}async#a(){this.#c(),this.#n?(this.#n.index="number"==typeof this.index?this.index:0,this.#n.setElements()):this.#n=new t(this);const[e,s]=this.#n.elements;return this.appendChild(e),s.forEach(e=>{this.appendChild(e)}),await this.#n.render(!1)}#c(){Array.from(this.children).forEach(e=>this.removeChild(e))}});class e{constructor(e){this.AccordionTabs=e,this.elements=this.AccordionTabs.details,this.elements.forEach((e,t)=>{e.querySelector("summary").addEventListener("click",({target:e})=>{requestAnimationFrame(()=>{this.#d(e.closest("details"),t)})})}),this.render()}setElements(){this.elements.forEach((e,t)=>{this.AccordionTabs.content[t].content.forEach(t=>e.appendChild(t)),e.open=t===this.AccordionTabs.index})}render(){this.elements.forEach((e,t)=>{e.open=t===this.AccordionTabs.index})}#d(e,t){e.open?(this.elements.forEach(t=>{e!==t&&(t.open=!1)}),this.AccordionTabs.index=t):this.AccordionTabs.index=null}}class t{#h;#l=[];#b=[];#u;constructor(e){this.#h=e,this.elements=this.getElements(),this.index="number"==typeof this.#h.index?this.#h.index:0,this.#u=new s(this)}getElements(){const e=document.createElement("ol"),t=[];return this.#h.content.forEach(({title:s,content:i},n)=>{const r=document.createElement("button"),o=document.createElement("li"),a=document.createElement("div"),c=crypto.randomUUID(),d=`tab-${c}`,h=`panel-${c}`;e.setAttribute("role","tablist"),o.setAttribute("role","presentation"),r.textContent=s,r.type="button",r.id=d,r.setAttribute("aria-selected",n===this.index?"true":"false"),r.setAttribute("tabindex",n===this.index?0:-1),r.setAttribute("aria-controls",h),r.setAttribute("role","tab"),this.#l.push(r),o.appendChild(r),e.appendChild(o),i.forEach(e=>{a.appendChild(e)}),a.id=h,a.hidden=this.index!==n,a.setAttribute("role","tabpanel"),a.setAttribute("tabindex","0"),a.setAttribute("aria-labelledby",d),t.push(a),this.#b.push(a)}),[e,t]}setElements(){this.elements[1].forEach((e,t)=>{this.#h.content[t].content.forEach(t=>e.appendChild(t))})}setActiveTab(e){this.#h.index=this.index=e,this.render()}async render(e=!0){return this.elements[1].forEach((e,t)=>{e.hidden=t!==this.index}),await this.#u.render(e)}}class s{#m;#p;constructor(e){this.#m=e,this.#p=Array.from(this.#m.elements[0].querySelectorAll("button")),this.#p.forEach(e=>{e.addEventListener("click",this.#T.bind(this)),e.addEventListener("keydown",this.#A.bind(this))})}#T({currentTarget:e}){this.#m.setActiveTab(this.#p.indexOf(e))}#A(e){const{dir:t}=e.target.closest("[dir]")||document.documentElement;let s=!1;switch(e.key){case"ArrowLeft":"rtl"===t?this.#f():this.#w(),s=!0;break;case"ArrowRight":"rtl"===t?this.#w():this.#f(),s=!0;break;case"Home":this.#m.setActiveTab(0),s=!0;break;case"End":this.#m.setActiveTab(this.#p.length-1),s=!0}s&&(e.stopPropagation(),e.preventDefault())}#f(){this.#m.setActiveTab(this.#m.index===this.#p.length-1?0:this.#m.index+1)}#w(){this.#m.setActiveTab(0===this.#m.index?this.#p.length-1:this.#m.index-1)}render(e=!0){return this.#p.forEach((t,s)=>{s===this.#m.index?(t.setAttribute("aria-selected","true"),t.removeAttribute("tabindex"),e&&t.focus()):(t.setAttribute("aria-selected","false"),t.setAttribute("tabindex",-1))}),new Promise(e=>setTimeout(e,1e3))}}new WebSocket(`ws://${document.location.host}`).onmessage=async e=>{"reloadParent"===e.data?parent.window.location.reload():window.location.reload()};function i(e){return["INPUT","SELECT","TEXTAREA"].includes(e)}window.location.pathname.startsWith("/component?")&&window.location.href.indexOf("&embedded=true")>=0&&window.self===window.top&&(window.location=new URL(window.location).replace("&embedded=true","")),document.addEventListener("DOMContentLoaded",function(){const e=Array.from(document.querySelectorAll(".Component-file")),t=document.querySelector(".Styleguide");e.length>0&&import("./_iframe-links-DdifIr4P.js").then(t=>{t.default(e)}).catch(e=>console.error(e)),document.querySelector(".Code")&&import("./_prism-By3NMwUd.js"),t&&import("./index-BKDKaBC6.js").then(e=>new e.default(t)).catch(e=>console.error(e)),document.querySelector(".js-openMockData")&&import("./_mock-data-Dypo4Bl_.js")}),document.addEventListener("keyup",e=>{const{path:t,originalTarget:s,target:n,key:r}=e,o=t?t[0]:s||n;((e,t)=>!i(e.tagName)&&"f"===t)(o,r)?parent.window.dispatchEvent(new CustomEvent("searchTriggered")):((e,t)=>!i(e.tagName)&&"g"===t)(o,r)&&parent.window.dispatchEvent(new CustomEvent("gotoTriggered"))});
1
+ customElements.define("accordion-tabs",class extends HTMLElement{#e;#t;#n;#i;#s;static get observedAttributes(){return["breakpoint","current"]}constructor(){super()}connectedCallback(){if(!this.closest("code")){if(this.content=[],this.hasAttribute("breakpoint")){const e=this.getAttribute("breakpoint"),t=parseInt(e,10);let n;n=e.endsWith("rem")?t*parseInt(window.getComputedStyle(document.documentElement).fontSize,10):e.endsWith("em")?t*parseInt(window.getComputedStyle(this).fontSize,10):t,this.#t=n}window.requestAnimationFrame(()=>{this.details=Array.from(this.children),this.details.forEach((e,t)=>{const n=e.querySelector("summary");e.open&&(this.index=t),this.content.push({title:n.textContent,content:[...n.parentElement.children].filter(e=>1===e.nodeType&&e!==n)})});let e=!1;this.#i=new ResizeObserver(t=>{if(e)for(const e of t)this.#r(e.borderBoxSize[0].inlineSize);e=!0}),this.#r(this.clientWidth,()=>{this.#i.observe(this)})})}}attributeChangedCallback(e,t,n){"current"===e&&(this.index=parseInt(n,10),this.#r(this.clientWidth))}disconnectedCallback(){this.#i&&this.#i.disconnect()}async#r(e,t){!this.#t||e<this.#t?(await this.#o(),this.#n="accordion"):(await this.#a(),this.#n="tabs"),t&&t()}#o(){return this.#e||(this.#e=new e(this)),"tabs"===this.#n?(this.#c(),this.#e.setElements(),this.#e.elements.forEach(e=>this.appendChild(e))):this.#e.render(),!0}async#a(){this.#c(),this.#s?(this.#s.index="number"==typeof this.index?this.index:0,this.#s.setElements()):this.#s=new t(this);const[e,n]=this.#s.elements;return this.appendChild(e),n.forEach(e=>{this.appendChild(e)}),await this.#s.render(!1)}#c(){Array.from(this.children).forEach(e=>this.removeChild(e))}});class e{constructor(e){this.AccordionTabs=e,this.elements=this.AccordionTabs.details,this.elements.forEach((e,t)=>{e.querySelector("summary").addEventListener("click",({target:e})=>{requestAnimationFrame(()=>{this.#d(e.closest("details"),t)})})}),this.render()}setElements(){this.elements.forEach((e,t)=>{this.AccordionTabs.content[t].content.forEach(t=>e.appendChild(t)),e.open=t===this.AccordionTabs.index})}render(){this.elements.forEach((e,t)=>{e.open=t===this.AccordionTabs.index})}#d(e,t){e.open?(this.elements.forEach(t=>{e!==t&&(t.open=!1)}),this.AccordionTabs.index=t):this.AccordionTabs.index=null}}class t{#h;#l=[];#b=[];#u;constructor(e){this.#h=e,this.elements=this.getElements(),this.index="number"==typeof this.#h.index?this.#h.index:0,this.#u=new n(this)}getElements(){const e=document.createElement("ol"),t=[];return this.#h.content.forEach(({title:n,content:i},s)=>{const r=document.createElement("button"),o=document.createElement("li"),a=document.createElement("div"),c=crypto.randomUUID(),d=`tab-${c}`,h=`panel-${c}`;e.setAttribute("role","tablist"),o.setAttribute("role","presentation"),r.textContent=n,r.type="button",r.id=d,r.setAttribute("aria-selected",s===this.index?"true":"false"),r.setAttribute("tabindex",s===this.index?0:-1),r.setAttribute("aria-controls",h),r.setAttribute("role","tab"),this.#l.push(r),o.appendChild(r),e.appendChild(o),i.forEach(e=>{a.appendChild(e)}),a.id=h,a.hidden=this.index!==s,a.setAttribute("role","tabpanel"),a.setAttribute("tabindex","0"),a.setAttribute("aria-labelledby",d),t.push(a),this.#b.push(a)}),[e,t]}setElements(){this.elements[1].forEach((e,t)=>{this.#h.content[t].content.forEach(t=>e.appendChild(t))})}setActiveTab(e){this.#h.index=this.index=e,this.render()}async render(e=!0){return this.elements[1].forEach((e,t)=>{e.hidden=t!==this.index}),await this.#u.render(e)}}class n{#m;#p;constructor(e){this.#m=e,this.#p=Array.from(this.#m.elements[0].querySelectorAll("button")),this.#p.forEach(e=>{e.addEventListener("click",this.#T.bind(this)),e.addEventListener("keydown",this.#f.bind(this))})}#T({currentTarget:e}){this.#m.setActiveTab(this.#p.indexOf(e))}#f(e){const{dir:t}=e.target.closest("[dir]")||document.documentElement;let n=!1;switch(e.key){case"ArrowLeft":"rtl"===t?this.#w():this.#A(),n=!0;break;case"ArrowRight":"rtl"===t?this.#A():this.#w(),n=!0;break;case"Home":this.#m.setActiveTab(0),n=!0;break;case"End":this.#m.setActiveTab(this.#p.length-1),n=!0}n&&(e.stopPropagation(),e.preventDefault())}#w(){this.#m.setActiveTab(this.#m.index===this.#p.length-1?0:this.#m.index+1)}#A(){this.#m.setActiveTab(0===this.#m.index?this.#p.length-1:this.#m.index-1)}render(e=!0){return this.#p.forEach((t,n)=>{n===this.#m.index?(t.setAttribute("aria-selected","true"),t.removeAttribute("tabindex"),e&&t.focus()):(t.setAttribute("aria-selected","false"),t.setAttribute("tabindex",-1))}),new Promise(e=>setTimeout(e,1e3))}}let i,s=250;function r(e){const t=function(e){return"reloadParent"===e?"parent":"string"==typeof e&&0===e.length?"iframe":null}(e);return t||function(e){try{return JSON.parse(e).scope||"iframe"}catch{return"iframe"}}(e)}function o(){i=new WebSocket(`${"https:"===window.location.protocol?"wss":"ws"}://${window.location.host}/__miyagi_ws`),i.onopen=()=>{s=250},i.onmessage=e=>{"parent"!==r(e.data)?window.location.reload():parent.window.location.reload()},i.onerror=()=>{i.close()},i.onclose=()=>{!function(){const e=Math.floor(100*Math.random()),t=Math.min(s+e,5e3);s=Math.min(2*s,5e3),window.setTimeout(()=>{o()},t)}()}}o();function a(e){return["INPUT","SELECT","TEXTAREA"].includes(e)}window.location.pathname.startsWith("/component?")&&window.location.href.indexOf("&embedded=true")>=0&&window.self===window.top&&(window.location=new URL(window.location).replace("&embedded=true","")),document.addEventListener("DOMContentLoaded",function(){const e=Array.from(document.querySelectorAll(".Component-file")),t=document.querySelector(".Styleguide");e.length>0&&import("./_iframe-links-DdifIr4P.js").then(t=>{t.default(e)}).catch(e=>console.error(e)),document.querySelector(".Code")&&import("./_prism-By3NMwUd.js"),t&&import("./index-BKDKaBC6.js").then(e=>new e.default(t)).catch(e=>console.error(e)),document.querySelector(".js-openMockData")&&import("./_mock-data-Dypo4Bl_.js")}),document.addEventListener("keyup",e=>{const{path:t,originalTarget:n,target:i,key:s}=e,r=t?t[0]:n||i;((e,t)=>!a(e.tagName)&&"f"===t)(r,s)?parent.window.dispatchEvent(new CustomEvent("searchTriggered")):((e,t)=>!a(e.tagName)&&"g"===t)(r,s)&&parent.window.dispatchEvent(new CustomEvent("gotoTriggered"))});
@@ -1,9 +1,82 @@
1
- const ws = new WebSocket(`ws://${document.location.host}`);
1
+ const SOCKET_PATH = "/__miyagi_ws";
2
+ const INITIAL_RETRY_DELAY_MS = 250;
3
+ const MAX_RETRY_DELAY_MS = 5000;
2
4
 
3
- ws.onmessage = async (message) => {
4
- if (message.data === "reloadParent") {
5
+ let retryDelay = INITIAL_RETRY_DELAY_MS;
6
+ let websocket;
7
+
8
+ function getWebSocketUrl() {
9
+ const protocol = window.location.protocol === "https:" ? "wss" : "ws";
10
+ return `${protocol}://${window.location.host}${SOCKET_PATH}`;
11
+ }
12
+
13
+ function triggerReload(scope) {
14
+ if (scope === "parent") {
5
15
  parent.window.location.reload();
6
- } else {
7
- window.location.reload();
16
+ return;
17
+ }
18
+
19
+ window.location.reload();
20
+ }
21
+
22
+ function parseLegacyScope(messageData) {
23
+ if (messageData === "reloadParent") {
24
+ return "parent";
8
25
  }
9
- };
26
+
27
+ if (typeof messageData === "string" && messageData.length === 0) {
28
+ return "iframe";
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ function parseJsonScope(messageData) {
35
+ try {
36
+ const parsed = JSON.parse(messageData);
37
+ return parsed.scope || "iframe";
38
+ } catch {
39
+ return "iframe";
40
+ }
41
+ }
42
+
43
+ function parseScope(messageData) {
44
+ const legacyScope = parseLegacyScope(messageData);
45
+ if (legacyScope) {
46
+ return legacyScope;
47
+ }
48
+
49
+ return parseJsonScope(messageData);
50
+ }
51
+
52
+ function scheduleReconnect() {
53
+ const jitter = Math.floor(Math.random() * 100);
54
+ const delay = Math.min(retryDelay + jitter, MAX_RETRY_DELAY_MS);
55
+ retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY_MS);
56
+
57
+ window.setTimeout(() => {
58
+ connect();
59
+ }, delay);
60
+ }
61
+
62
+ function connect() {
63
+ websocket = new WebSocket(getWebSocketUrl());
64
+
65
+ websocket.onopen = () => {
66
+ retryDelay = INITIAL_RETRY_DELAY_MS;
67
+ };
68
+
69
+ websocket.onmessage = (message) => {
70
+ triggerReload(parseScope(message.data));
71
+ };
72
+
73
+ websocket.onerror = () => {
74
+ websocket.close();
75
+ };
76
+
77
+ websocket.onclose = () => {
78
+ scheduleReconnect();
79
+ };
80
+ }
81
+
82
+ connect();
package/lib/config.js CHANGED
@@ -70,5 +70,29 @@ function getCliArgs(args) {
70
70
 
71
71
  cliArgs.build = buildArgs;
72
72
 
73
+ if (
74
+ cliArgs.watchReport !== undefined ||
75
+ cliArgs.watchReportFormat !== undefined ||
76
+ cliArgs.watchReportNoColor !== undefined
77
+ ) {
78
+ cliArgs.watch = cliArgs.watch || {};
79
+ cliArgs.watch.report = cliArgs.watch.report || {};
80
+
81
+ if (cliArgs.watchReport !== undefined) {
82
+ cliArgs.watch.report.enabled = cliArgs.watchReport;
83
+ delete cliArgs.watchReport;
84
+ }
85
+
86
+ if (cliArgs.watchReportFormat !== undefined) {
87
+ cliArgs.watch.report.format = cliArgs.watchReportFormat;
88
+ delete cliArgs.watchReportFormat;
89
+ }
90
+
91
+ if (cliArgs.watchReportNoColor !== undefined) {
92
+ cliArgs.watch.report.useColors = !cliArgs.watchReportNoColor;
93
+ delete cliArgs.watchReportNoColor;
94
+ }
95
+ }
96
+
73
97
  return cliArgs;
74
98
  }
@@ -97,6 +97,64 @@ export default {
97
97
  },
98
98
  watchConfigFile: true,
99
99
  },
100
+ watch: {
101
+ enabled: true,
102
+ backend: "chokidar",
103
+ sources: [],
104
+ ignore: {
105
+ defaults: true,
106
+ patterns: [],
107
+ },
108
+ behavior: {
109
+ debounceMs: 60,
110
+ coalesceWindowMs: 120,
111
+ awaitWriteFinish: {
112
+ enabled: true,
113
+ stabilityThresholdMs: 200,
114
+ pollIntervalMs: 50,
115
+ },
116
+ },
117
+ reload: {
118
+ enabled: true,
119
+ rules: {
120
+ template: "iframe",
121
+ data: "parent",
122
+ docs: "parent",
123
+ schema: "iframe",
124
+ componentAsset: "none",
125
+ globalCss: "none",
126
+ globalJs: "none",
127
+ unknown: "parent",
128
+ },
129
+ },
130
+ socket: {
131
+ reconnect: {
132
+ enabled: true,
133
+ initialDelayMs: 250,
134
+ maxDelayMs: 5000,
135
+ jitter: true,
136
+ },
137
+ heartbeat: {
138
+ enabled: true,
139
+ intervalMs: 30000,
140
+ },
141
+ },
142
+ report: {
143
+ enabled: true,
144
+ onStart: true,
145
+ format: "summary",
146
+ destination: "stdout",
147
+ useColors: true,
148
+ },
149
+ configFile: {
150
+ enabled: true,
151
+ },
152
+ debug: {
153
+ logEvents: false,
154
+ logDecisions: false,
155
+ logResolvedSources: false,
156
+ },
157
+ },
100
158
  schema: {
101
159
  ajv: AJV,
102
160
  verbose: false,
package/lib/init/args.js CHANGED
@@ -14,6 +14,19 @@ export default yargs(hideBin(process.argv))
14
14
  "Logging additional information — helpful mainly in case of errors.",
15
15
  type: "boolean",
16
16
  },
17
+ "watch-report": {
18
+ description: "Enable watch report output on startup.",
19
+ type: "boolean",
20
+ },
21
+ "watch-report-format": {
22
+ description: "Set watch report format.",
23
+ type: "string",
24
+ choices: ["pretty", "summary", "json"],
25
+ },
26
+ "watch-report-no-color": {
27
+ description: "Disable colors in watch report output.",
28
+ type: "boolean",
29
+ },
17
30
  })
18
31
  .command("build", "Creates a static build of all your components", {
19
32
  folder: {
@@ -39,6 +39,30 @@ function sanitizePath(path) {
39
39
  return sanitizedPath;
40
40
  }
41
41
 
42
+ /**
43
+ * Keep watch paths relative-friendly (do not strip leading slash),
44
+ * but normalize common user input like "./foo/"
45
+ * @param {string} watchPath
46
+ * @returns {string}
47
+ */
48
+ function sanitizeWatchPath(watchPath) {
49
+ if (typeof watchPath !== "string") {
50
+ return watchPath;
51
+ }
52
+
53
+ let sanitizedPath = watchPath;
54
+
55
+ if (sanitizedPath.startsWith("./")) {
56
+ sanitizedPath = sanitizedPath.slice(2);
57
+ }
58
+
59
+ if (sanitizedPath.endsWith("/")) {
60
+ sanitizedPath = sanitizedPath.slice(0, -1);
61
+ }
62
+
63
+ return sanitizedPath;
64
+ }
65
+
42
66
  /**
43
67
  * @param {string|Array} strOrArr - file path or array of file paths
44
68
  * @returns {Array} the given file path in an array or simply the given array
@@ -47,6 +71,195 @@ function arrayfy(strOrArr) {
47
71
  return Array.isArray(strOrArr) ? strOrArr : [strOrArr];
48
72
  }
49
73
 
74
+ /**
75
+ * @param {object} merged
76
+ * @returns {object[]}
77
+ */
78
+ function getDefaultWatchSources(merged) {
79
+ const sources = [];
80
+ const sourceByPath = new Set();
81
+
82
+ const addSource = (source) => {
83
+ if (!source || typeof source.path !== "string" || source.path.length === 0) {
84
+ return;
85
+ }
86
+
87
+ const key = `${source.type}:${source.path}`;
88
+ // Deduplicate watch targets; While calling add on Set will dedup,
89
+ // we need a conditional to avoid adding duplicates to the `sources` array.
90
+ if (sourceByPath.has(key)) {
91
+ return;
92
+ }
93
+
94
+ sourceByPath.add(key);
95
+ sources.push(source);
96
+ };
97
+
98
+ if (merged.components.folder) {
99
+ addSource({
100
+ id: "components",
101
+ type: "dir",
102
+ path: sanitizeWatchPath(merged.components.folder),
103
+ recursive: true,
104
+ });
105
+ }
106
+
107
+ if (merged.docs?.folder) {
108
+ addSource({
109
+ id: "docs",
110
+ type: "dir",
111
+ path: sanitizeWatchPath(merged.docs.folder),
112
+ recursive: true,
113
+ optional: true,
114
+ });
115
+ }
116
+
117
+ for (const folder of merged.assets.folder || []) {
118
+ addSource({
119
+ id: `assets-folder-${folder}`,
120
+ type: "dir",
121
+ path: sanitizeWatchPath(path.join(merged.assets.root, folder)),
122
+ recursive: true,
123
+ optional: true,
124
+ });
125
+ }
126
+
127
+ const localCssFiles = [...(merged.assets.css || []), ...(merged.assets.shared?.css || [])]
128
+ .filter((filePath) => !filePath.startsWith("http://"))
129
+ .filter((filePath) => !filePath.startsWith("https://"))
130
+ .filter((filePath) => !filePath.startsWith("://"));
131
+
132
+ for (const cssFile of localCssFiles) {
133
+ addSource({
134
+ id: `assets-css-${cssFile}`,
135
+ type: "file",
136
+ path: sanitizeWatchPath(path.join(merged.assets.root, cssFile)),
137
+ optional: true,
138
+ });
139
+ }
140
+
141
+ const localJsFiles = [...(merged.assets.js || []), ...(merged.assets.shared?.js || [])]
142
+ .map((entry) => entry.src || entry)
143
+ .filter((filePath) => typeof filePath === "string")
144
+ .filter((filePath) => !filePath.startsWith("http://"))
145
+ .filter((filePath) => !filePath.startsWith("https://"))
146
+ .filter((filePath) => !filePath.startsWith("://"));
147
+
148
+ for (const jsFile of localJsFiles) {
149
+ addSource({
150
+ id: `assets-js-${jsFile}`,
151
+ type: "file",
152
+ path: sanitizeWatchPath(path.join(merged.assets.root, jsFile)),
153
+ optional: true,
154
+ });
155
+ }
156
+
157
+ if (merged.userFileName && merged.watch?.configFile?.enabled) {
158
+ addSource({
159
+ id: "config-file",
160
+ type: "file",
161
+ path: sanitizeWatchPath(merged.userFileName),
162
+ optional: true,
163
+ });
164
+ }
165
+
166
+ return sources;
167
+ }
168
+
169
+ /**
170
+ * @param {object} watchConfig
171
+ * @returns {object[]}
172
+ */
173
+ function normalizeWatchSources(watchConfig) {
174
+ if (!Array.isArray(watchConfig.sources) || watchConfig.sources.length === 0) {
175
+ return [];
176
+ }
177
+
178
+ return watchConfig.sources
179
+ .map((source, index) => {
180
+ if (!source || typeof source.path !== "string" || source.path.length === 0) {
181
+ return null;
182
+ }
183
+
184
+ const type = source.type === "file" ? "file" : "dir";
185
+
186
+ return {
187
+ id: source.id || `source-${index + 1}`,
188
+ type,
189
+ path: sanitizeWatchPath(source.path),
190
+ recursive: source.recursive !== false,
191
+ optional: source.optional === true,
192
+ };
193
+ })
194
+ .filter(Boolean);
195
+ }
196
+
197
+ /**
198
+ * Applies legacy watch-related config keys to watch config
199
+ * while preserving explicit new watch config values.
200
+ * @param {object} merged
201
+ * @param {object} userConfig
202
+ */
203
+ function applyLegacyWatchCompatibility(merged, userConfig) {
204
+ merged.watch.configFile = merged.watch.configFile || { enabled: true };
205
+ merged.watch.configFile.enabled =
206
+ typeof merged.watch.configFile.enabled === "boolean"
207
+ ? merged.watch.configFile.enabled
208
+ : merged.ui.watchConfigFile;
209
+
210
+ merged.watch.reload = merged.watch.reload || {};
211
+ merged.watch.reload.enabled =
212
+ typeof merged.watch.reload.enabled === "boolean"
213
+ ? merged.watch.reload.enabled
214
+ : merged.ui.reload;
215
+
216
+ const userReloadRules = userConfig.watch?.reload?.rules || {};
217
+ merged.watch.reload.rules = {
218
+ ...defaultUserConfig.watch.reload.rules,
219
+ ...(merged.watch.reload.rules || {}),
220
+ componentAsset:
221
+ userReloadRules.componentAsset ??
222
+ (merged.ui.reloadAfterChanges.componentAssets
223
+ ? "iframe"
224
+ : defaultUserConfig.watch.reload.rules.componentAsset),
225
+ globalCss:
226
+ userReloadRules.globalCss ??
227
+ (merged.ui.reloadAfterChanges.componentAssets
228
+ ? "iframe"
229
+ : defaultUserConfig.watch.reload.rules.globalCss),
230
+ globalJs:
231
+ userReloadRules.globalJs ??
232
+ (merged.ui.reloadAfterChanges.componentAssets
233
+ ? "iframe"
234
+ : defaultUserConfig.watch.reload.rules.globalJs),
235
+ };
236
+
237
+ merged.watch.ignore = merged.watch.ignore || {};
238
+ merged.watch.ignore.patterns = [
239
+ ...(merged.watch.ignore.patterns || []),
240
+ ...(merged.components.ignores || []),
241
+ ].filter((entry) => typeof entry === "string");
242
+ merged.watch.ignore.patterns = [...new Set(merged.watch.ignore.patterns)];
243
+ }
244
+
245
+ /**
246
+ * Normalizes and validates new watch config values.
247
+ * @param {object} merged
248
+ */
249
+ function normalizeAndValidateWatchConfig(merged) {
250
+ if (merged.watch?.backend === "node-watch") {
251
+ throw new Error(
252
+ '`watch.backend="node-watch"` is no longer supported. Please use `watch.backend="chokidar"`. See https://docs.miyagi.dev/configuration/options/ for migration details.',
253
+ );
254
+ }
255
+
256
+ merged.watch.backend = "chokidar";
257
+ merged.watch.sources = normalizeWatchSources(merged.watch);
258
+ if (merged.watch.sources.length === 0) {
259
+ merged.watch.sources = getDefaultWatchSources(merged);
260
+ }
261
+ }
262
+
50
263
  /**
51
264
  *
52
265
  * @param {object} root0
@@ -335,6 +548,9 @@ export default (userConfig = {}) => {
335
548
  merged.lint.logLevel = defaultUserConfig.lint.logLevel;
336
549
  }
337
550
 
551
+ applyLegacyWatchCompatibility(merged, config);
552
+ normalizeAndValidateWatchConfig(merged);
553
+
338
554
  return merged;
339
555
  };
340
556