coralite 0.40.1 → 0.41.0

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/lib/index.js CHANGED
@@ -3460,59 +3460,106 @@ var staticAssetPlugin = (assets = []) => {
3460
3460
  };
3461
3461
 
3462
3462
  // plugins/testing.js
3463
- function traverseAndAddTestId(children) {
3463
+ function traverseAndAddTestId(children, instanceId, { autoTestId = false, counters = {} } = {}) {
3464
3464
  if (!Array.isArray(children)) {
3465
3465
  return;
3466
3466
  }
3467
3467
  for (let i = 0; i < children.length; i++) {
3468
3468
  const node = children[i];
3469
- if (node.type === "tag" && node.attribs?.ref) {
3470
- if (!node.attribs["data-testid"]) {
3471
- node.attribs["data-testid"] = node.attribs.ref;
3469
+ if (node.type === "tag") {
3470
+ if (node.attribs?.ref) {
3471
+ if (!node.attribs["data-testid"]) {
3472
+ const refValue = node.attribs.ref;
3473
+ const prefix = instanceId ? `${instanceId}__` : "";
3474
+ if (instanceId === "page" && refValue.includes("__")) {
3475
+ node.attribs["data-testid"] = refValue;
3476
+ } else {
3477
+ node.attribs["data-testid"] = `${prefix}${refValue}`;
3478
+ }
3479
+ }
3480
+ } else if (autoTestId && instanceId) {
3481
+ const tagName = node.name.toLowerCase();
3482
+ const isInteractive = [
3483
+ "button",
3484
+ "a",
3485
+ "input",
3486
+ "form",
3487
+ "select",
3488
+ "textarea"
3489
+ ].includes(tagName) || node.attribs && (node.attribs.tabindex !== void 0 || node.attribs.role && ["button", "link", "checkbox"].includes(node.attribs.role)) || node.slots;
3490
+ if (isInteractive) {
3491
+ if (!counters[tagName]) {
3492
+ counters[tagName] = 0;
3493
+ }
3494
+ const index = counters[tagName]++;
3495
+ if (!node.attribs) {
3496
+ node.attribs = {};
3497
+ }
3498
+ if (!node.attribs["data-testid"]) {
3499
+ node.attribs["data-testid"] = `${instanceId}__${tagName}-${index}`;
3500
+ }
3501
+ }
3472
3502
  }
3473
3503
  }
3474
3504
  if (node.children?.length > 0) {
3475
- traverseAndAddTestId(node.children);
3505
+ traverseAndAddTestId(node.children, instanceId, {
3506
+ autoTestId,
3507
+ counters
3508
+ });
3476
3509
  }
3477
3510
  }
3478
3511
  }
3479
3512
  var testingPlugin = definePlugin({
3480
3513
  name: "testing",
3481
3514
  server: {
3482
- onBeforeComponentRender: ({ instanceId, refs }) => {
3483
- for (let i = 0; i < refs.length; i++) {
3484
- const ref = refs[i];
3485
- const uniqueRefValue = `${instanceId}__${ref.name}`;
3486
- if (ref.element.attribs) {
3487
- const currentTestId = ref.element.attribs["data-testid"];
3488
- if (!currentTestId || currentTestId === ref.name) {
3489
- ref.element.attribs["data-testid"] = uniqueRefValue;
3490
- }
3491
- }
3515
+ onBeforeBuild: ({ app }) => {
3516
+ if (app.options.mode !== "testing") {
3517
+ return;
3492
3518
  }
3519
+ app.options.externalStyles = app.options.externalStyles || [];
3520
+ const velocityStyle = `
3521
+ *, *::before, *::after {
3522
+ transition: none !important;
3523
+ animation: none !important;
3524
+ scroll-behavior: auto !important;
3525
+ }
3526
+ `.trim();
3527
+ app.options.externalStyles.push(`data:text/css;base64,${Buffer.from(velocityStyle).toString("base64")}`);
3493
3528
  },
3494
- onComponentSet: ({ component }) => {
3495
- const children = component?.template?.children;
3496
- if (children) {
3497
- traverseAndAddTestId(children);
3529
+ onBeforeComponentRender: ({ instanceId, template, app }) => {
3530
+ const isTesting = app.options.mode === "testing";
3531
+ const counters = {};
3532
+ const templateNode = template;
3533
+ if (templateNode && templateNode.children) {
3534
+ traverseAndAddTestId(templateNode.children, instanceId, {
3535
+ autoTestId: isTesting,
3536
+ counters
3537
+ });
3498
3538
  }
3499
3539
  },
3500
- onPageSet: ({ elements }) => {
3501
- const children = elements?.root?.children;
3502
- if (children) {
3503
- traverseAndAddTestId(children);
3504
- }
3540
+ onPageSet: ({ elements, app }) => {
3541
+ const isTesting = app.options.mode === "testing";
3542
+ const counters = {};
3543
+ traverseAndAddTestId(elements?.root?.children, "page", {
3544
+ autoTestId: isTesting,
3545
+ counters
3546
+ });
3505
3547
  }
3506
3548
  },
3507
3549
  client: {
3508
3550
  onBeforeComponentRender: ({ instanceId, refs }) => {
3509
3551
  for (let i = 0; i < refs.length; i++) {
3510
3552
  const ref = refs[i];
3511
- const uniqueRefValue = `${instanceId}__${ref.name}`;
3553
+ const prefix = `${instanceId}__`;
3554
+ const uniqueRefValue = ref.element && ref.element.getAttribute("ref");
3512
3555
  if (ref.element && ref.element.setAttribute) {
3513
3556
  const currentTestId = ref.element.getAttribute("data-testid");
3514
3557
  if (!currentTestId || currentTestId === ref.name) {
3515
- ref.element.setAttribute("data-testid", uniqueRefValue);
3558
+ if (uniqueRefValue && uniqueRefValue.startsWith(prefix)) {
3559
+ ref.element.setAttribute("data-testid", uniqueRefValue);
3560
+ } else {
3561
+ ref.element.setAttribute("data-testid", `${prefix}${ref.name}`);
3562
+ }
3516
3563
  }
3517
3564
  }
3518
3565
  }
@@ -4190,8 +4237,15 @@ function createComponentDefinition({ app }) {
4190
4237
  }
4191
4238
  }
4192
4239
  }
4193
- if (typeof server === "function") {
4194
- const serverResult = await server({
4240
+ let serverToExecute = server;
4241
+ if (app.options.mode === "testing" && app.options.testing?.mocks) {
4242
+ const mock = app.options.testing.mocks[module.id];
4243
+ if (mock && typeof mock.server === "function") {
4244
+ serverToExecute = mock.server;
4245
+ }
4246
+ }
4247
+ if (typeof serverToExecute === "function") {
4248
+ const serverResult = await serverToExecute({
4195
4249
  ...context2,
4196
4250
  ...initialState
4197
4251
  });
@@ -4566,7 +4620,6 @@ function createPageHandlers({
4566
4620
  scriptManager,
4567
4621
  createSession
4568
4622
  }) {
4569
- const { pageCustomElements, childCustomElements } = app._dependencyGraph;
4570
4623
  const onFileSetLocal = async (data) => {
4571
4624
  const rootPath = data.type === "component" ? app.options.path.components : app.options.path.pages;
4572
4625
  const urlPathname = pathToFileURL3(join3("/", relative2(rootPath, data.path.pathname))).pathname;
@@ -4597,34 +4650,11 @@ function createPageHandlers({
4597
4650
  };
4598
4651
  }
4599
4652
  const elements = parseHTML(data.content, app.options.ignoreByAttribute, app.options.skipRenderByAttribute, handleError2);
4600
- if (true) {
4601
- const customElementsList = elements && elements.customElements ? elements.customElements : [];
4602
- for (let i = 0; i < customElementsList.length; i++) {
4603
- const name = customElementsList[i].name;
4604
- if (!pageCustomElements[name]) {
4605
- pageCustomElements[name] = /* @__PURE__ */ new Set();
4606
- app._dependencyGraph.pageCustomElements[name] = pageCustomElements[name];
4607
- const component = app.components.getItem(name);
4608
- if (component && component.result && component.result.customElements && component.result.customElements.length) {
4609
- const stack = [component.result.customElements];
4610
- while (stack.length > 0) {
4611
- const current = stack.pop();
4612
- for (let i2 = 0; i2 < current.length; i2++) {
4613
- const element = current[i2];
4614
- if (!childCustomElements[element.name]) {
4615
- childCustomElements[element.name] = name;
4616
- const comp = app.components.getItem(element.name);
4617
- if (comp && comp.result && comp.result.customElements && comp.result.customElements.length) {
4618
- stack.push(comp.result.customElements);
4619
- }
4620
- }
4621
- }
4622
- }
4623
- }
4624
- }
4625
- const customElements = pageCustomElements[name];
4626
- customElements.add(data.path.pathname);
4627
- }
4653
+ const directPageComponents = app._dependencyGraph.directPageComponents;
4654
+ if (elements && elements.customElements && directPageComponents) {
4655
+ const directComponents = elements.customElements.map((el) => el.name);
4656
+ directPageComponents[data.path.pathname] = directComponents;
4657
+ app._refreshDependencyGraph();
4628
4658
  }
4629
4659
  const mappedContext = await triggerHook("onPageSet", {
4630
4660
  elements,
@@ -4633,8 +4663,8 @@ function createPageHandlers({
4633
4663
  data,
4634
4664
  app
4635
4665
  });
4636
- const isProduction = app.options.mode === "production";
4637
- if (isProduction && !data.virtual) {
4666
+ const isStatic = app.options.mode === "production" || app.options.mode === "testing";
4667
+ if (isStatic && !data.virtual) {
4638
4668
  delete data.content;
4639
4669
  }
4640
4670
  return {
@@ -4643,27 +4673,25 @@ function createPageHandlers({
4643
4673
  state: mappedContext.state,
4644
4674
  page: mappedContext.page,
4645
4675
  path: mappedContext.data.path,
4646
- root: isProduction ? null : mappedContext.elements.root,
4647
- customElements: isProduction ? null : mappedContext.elements.customElements,
4648
- tempElements: isProduction ? null : mappedContext.elements.tempElements,
4649
- skipRenderElements: isProduction ? null : mappedContext.elements.skipRenderElements
4676
+ root: isStatic ? null : mappedContext.elements.root,
4677
+ customElements: isStatic ? null : mappedContext.elements.customElements,
4678
+ tempElements: isStatic ? null : mappedContext.elements.tempElements,
4679
+ skipRenderElements: isStatic ? null : mappedContext.elements.skipRenderElements
4650
4680
  },
4651
4681
  state: mappedContext.state
4652
4682
  };
4653
4683
  };
4654
4684
  const onPageUpdateLocal = async (newValue, oldValue) => {
4655
- if (app.options.mode === "production") {
4685
+ if (app.options.mode === "production" || app.options.mode === "testing") {
4686
+ if (!newValue.result) {
4687
+ await onFileSetLocal(newValue);
4688
+ }
4656
4689
  return newValue.result;
4657
4690
  }
4658
- let newCustomElements;
4659
4691
  if (!newValue.result) {
4660
4692
  const res = await onFileSetLocal(newValue);
4661
4693
  newValue.result = res.value;
4662
- newCustomElements = res.value.customElements;
4663
- } else {
4664
- newCustomElements = newValue.result.customElements;
4665
4694
  }
4666
- const oldElements = (oldValue.result.customElements || []).slice();
4667
4695
  const mappedContext = await triggerHook("onPageUpdate", {
4668
4696
  elements: newValue.result,
4669
4697
  page: newValue.result.page,
@@ -4673,51 +4701,31 @@ function createPageHandlers({
4673
4701
  });
4674
4702
  newValue.result = mappedContext.elements;
4675
4703
  newValue = mappedContext.newValue;
4676
- for (let i = 0; i < newCustomElements.length; i++) {
4677
- const name = newCustomElements[i].name;
4678
- let hasElement = false;
4679
- for (let j = 0; j < oldElements.length; j++) {
4680
- if (name === oldElements[j].name) {
4681
- hasElement = true;
4682
- oldElements.splice(j, 1);
4683
- break;
4684
- }
4685
- }
4686
- if (!hasElement) {
4687
- if (!pageCustomElements[name]) {
4688
- pageCustomElements[name] = /* @__PURE__ */ new Set();
4689
- app._dependencyGraph.pageCustomElements[name] = pageCustomElements[name];
4690
- }
4691
- const customElements = pageCustomElements[name];
4692
- customElements.add(newValue.path.pathname);
4704
+ const elements = parseHTML(newValue.content, app.options.ignoreByAttribute, app.options.skipRenderByAttribute, handleError2);
4705
+ const directPageComponents = app._dependencyGraph.directPageComponents;
4706
+ if (directPageComponents) {
4707
+ if (elements && elements.customElements) {
4708
+ const directComponents = elements.customElements.map((el) => el.name);
4709
+ directPageComponents[newValue.path.pathname] = directComponents;
4710
+ } else {
4711
+ delete directPageComponents[newValue.path.pathname];
4693
4712
  }
4694
4713
  }
4695
- oldElements.forEach((oe) => {
4696
- if (pageCustomElements[oe.name]) {
4697
- const customElements = pageCustomElements[oe.name];
4698
- customElements.delete(newValue.path.pathname);
4699
- }
4700
- });
4714
+ app._refreshDependencyGraph();
4701
4715
  return newValue.result;
4702
4716
  };
4703
4717
  const onPageDeleteLocal = async (value) => {
4704
- if (app.options.mode === "production") {
4705
- return;
4706
- }
4707
4718
  const res = await triggerHook("onPageDelete", {
4708
4719
  data: value,
4709
4720
  app
4710
4721
  });
4711
- value = res.data;
4712
- if (value?.result?.customElements) {
4713
- value.result.customElements.forEach((ce) => {
4714
- const ceName = typeof ce === "string" ? ce : ce.name;
4715
- if (pageCustomElements[ceName]) {
4716
- const customElements = pageCustomElements[ceName];
4717
- customElements.delete(value.path.pathname);
4718
- }
4719
- });
4722
+ const finalData = res?.data || value;
4723
+ const pathname = finalData?.path?.pathname;
4724
+ const directPageComponents = app._dependencyGraph.directPageComponents;
4725
+ if (pathname && directPageComponents) {
4726
+ delete directPageComponents[pathname];
4720
4727
  }
4728
+ app._refreshDependencyGraph();
4721
4729
  };
4722
4730
  const onComponentSetLocal = async (v) => {
4723
4731
  if (v.content === void 0) {
@@ -4915,7 +4923,7 @@ ${css}
4915
4923
  root.children.unshift(styleElement);
4916
4924
  }
4917
4925
  }
4918
- function injectReadinessScript(root, head, hasScripts) {
4926
+ function injectReadinessScript(root, head, hasScripts, mode) {
4919
4927
  const readinessScriptElement = createCoraliteElement({
4920
4928
  type: "tag",
4921
4929
  name: "script",
@@ -4924,6 +4932,9 @@ function injectReadinessScript(root, head, hasScripts) {
4924
4932
  children: []
4925
4933
  });
4926
4934
  let data = "class CoraliteLifecycleManager { constructor() { this.defined = new Promise(r => this._dr = r); this.rendered = new Promise(r => this._rr = r); this.hydrated = new Promise(r => this._hr = r); this._t = 0; this._rc = 0; this._hc = 0; this._ts = 0; this._dt = new Set(); this._ip = new WeakMap(); this._ir = new WeakMap(); this._rs = new WeakSet(); this._hs = new WeakSet(); this._s = false; } _start(t, ts) { this._t = t; this._ts = ts; this._s = true; this._check(); } _check() { if (!this._s) return; if (this._rc >= this._t) this._rr(); if (this._hc >= this._t) this._hr(); if (this._dt.size >= this._ts) this._dr(); } _markDefined(tag) { this._dt.add(tag); this._check(); } _markInstanceRendered(el) { if (el.hasAttribute('data-coralite-initial') && !this._rs.has(el)) { this._rs.add(el); this._rc++; this._check(); } } _markInstanceReady(el) { const r = this._ir.get(el); r && r(); this._ip.set(el, Promise.resolve()); if (el.hasAttribute('data-coralite-initial') && !this._hs.has(el)) { this._hs.add(el); this._hc++; this._check(); } } waitFor(el) { let p = this._ip.get(el); if (!p) { p = new Promise(r => this._ir.set(el, r)); this._ip.set(el, p); } return p; } } window.__coralite__ = { lifecycle: new CoraliteLifecycleManager() };";
4935
+ if (mode === "testing") {
4936
+ data += " window.__coralite__.components = {}; window.__coralite__.events = [];";
4937
+ }
4927
4938
  if (!hasScripts) {
4928
4939
  data += " window.__coralite__.lifecycle._start(0, 0);";
4929
4940
  }
@@ -5030,8 +5041,32 @@ import { getClientContext, createCoraliteClass, globalClientHooks } from '${base
5030
5041
  (async () => {
5031
5042
  const hydrationData = ${hydrationData};
5032
5043
  const declarativeTags = ${JSON.stringify(declarativeTags)};
5033
- const initialElements = Array.from(document.querySelectorAll('[data-coralite-initial]'))
5034
- .filter(el => declarativeTags.includes(el.tagName.toLowerCase()));
5044
+
5045
+ const initialElements = Array.from(document.querySelectorAll('[data-cid]'))
5046
+ .filter(el => {
5047
+ const tagName = el.tagName.toLowerCase();
5048
+ const isDeclarative = declarativeTags.includes(tagName);
5049
+ const isInitial = el.hasAttribute('data-coralite-initial');
5050
+
5051
+ if (window['__coralite__'] && window['__coralite__'].components && isInitial) {
5052
+ const cid = el.getAttribute('data-cid');
5053
+ if (cid && !hydrationData[cid]) {
5054
+ const error = new Error('Coralite Hydration Mismatch: Component with data-cid "' + cid + '" (' + tagName + ') has no matching server hydration data.');
5055
+ if (typeof window['showCoraliteError'] === 'function') {
5056
+ window['showCoraliteError'](error);
5057
+ } else {
5058
+ console.error(error);
5059
+ const overlay = document.createElement('div');
5060
+ overlay.style = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(255,0,0,0.9);color:white;padding:20px;z-index:10000;font-family:monospace;white-space:pre-wrap;overflow:auto;';
5061
+ overlay.innerHTML = '<h1>Coralite Hydration Mismatch</h1><p>' + error.message + '</p>';
5062
+ document.body.appendChild(overlay);
5063
+ }
5064
+ throw error;
5065
+ }
5066
+ }
5067
+
5068
+ return isDeclarative && isInitial;
5069
+ });
5035
5070
  if (window.__coralite__ && window.__coralite__.lifecycle) {
5036
5071
  window.__coralite__.lifecycle._start(initialElements.length, declarativeTags.length);
5037
5072
  }
@@ -5080,16 +5115,6 @@ import { getClientContext, createCoraliteClass, globalClientHooks } from '${base
5080
5115
  customElements.define(id, createCoraliteClass(module.default, getClientContext, globalClientHooks, hydrationData));
5081
5116
  if (window.__coralite__ && window.__coralite__.lifecycle) window.__coralite__.lifecycle._markDefined(id);
5082
5117
  }
5083
-
5084
- // Upgrade any existing elements that might have been created before the definition was loaded
5085
- const elements = document.querySelectorAll(id);
5086
- for (const el of elements) {
5087
- if (el.constructor === HTMLElement) {
5088
- // Re-trigger the lifecycle by replacing the element or manually calling upgrade if supported
5089
- // For most cases, customElements.define handles this automatically if the element is already in DOM,
5090
- // but if it's currently disconnected it might need help.
5091
- }
5092
- }
5093
5118
  }
5094
5119
  })();
5095
5120
  return loadCache[componentId];
@@ -5480,20 +5505,11 @@ function createRenderer({
5480
5505
  componentState = cleanKeys(componentState);
5481
5506
  }
5482
5507
  const module = cloneModuleInstance(moduleComponent.result);
5483
- if (module.values && module.values.refs) {
5484
- for (let i = 0; i < module.values.refs.length; i++) {
5485
- const ref = module.values.refs[i];
5486
- const uniqueRefValue = `${instanceId}__${ref.name}`;
5487
- if (ref.element && ref.element.attribs) {
5488
- ref.element.attribs.ref = uniqueRefValue;
5489
- }
5490
- componentState[`ref_${ref.name}`] = uniqueRefValue;
5491
- }
5492
- }
5493
5508
  const mappedComponentContext = await hooks.trigger("onBeforeComponentRender", {
5494
5509
  state: componentState,
5495
5510
  componentId: module.id,
5496
5511
  instanceId,
5512
+ template: module.template,
5497
5513
  refs: module.values.refs,
5498
5514
  textNodes: module.values.textNodes,
5499
5515
  attributes: module.values.attributes,
@@ -5503,6 +5519,16 @@ function createRenderer({
5503
5519
  app
5504
5520
  });
5505
5521
  componentState = mappedComponentContext.state;
5522
+ if (module.values && module.values.refs) {
5523
+ for (let i = 0; i < module.values.refs.length; i++) {
5524
+ const ref = module.values.refs[i];
5525
+ const uniqueRefValue = `${instanceId}__${ref.name}`;
5526
+ if (ref.element && ref.element.attribs) {
5527
+ ref.element.attribs.ref = uniqueRefValue;
5528
+ }
5529
+ componentState[`ref_${ref.name}`] = uniqueRefValue;
5530
+ }
5531
+ }
5506
5532
  const result = module.template;
5507
5533
  if (module.styles.length) {
5508
5534
  const selector = module.id;
@@ -6080,7 +6106,7 @@ function createRenderer({
6080
6106
  chunkManifest[tag] = scriptResult.manifest[tag];
6081
6107
  }
6082
6108
  }
6083
- injectReadinessScript(mappedComponent.root, headElement, true);
6109
+ injectReadinessScript(mappedComponent.root, headElement, true, normalizedOptions.mode);
6084
6110
  injectImportMap(mappedComponent.root, headElement, scriptResult.importMap);
6085
6111
  const base = normalizedOptions.baseURL.endsWith("/") ? normalizedOptions.baseURL : normalizedOptions.baseURL + "/";
6086
6112
  const hydrationData = {};
@@ -6113,7 +6139,7 @@ function createRenderer({
6113
6139
  }
6114
6140
  removeElements(mappedComponent.skipRenderElements, true);
6115
6141
  if (!mappedSessionObject.scripts.content[mappedComponent.path.pathname]) {
6116
- injectReadinessScript(mappedComponent.root, headElement, false);
6142
+ injectReadinessScript(mappedComponent.root, headElement, false, normalizedOptions.mode);
6117
6143
  }
6118
6144
  const rawHTML = transformNode(mappedComponent.root);
6119
6145
  const result = {
@@ -6214,7 +6240,7 @@ function createRenderer({
6214
6240
  if (!buildOptions) {
6215
6241
  buildOptions = {};
6216
6242
  }
6217
- if (normalizedOptions.mode === "production") {
6243
+ if (normalizedOptions.mode === "production" || normalizedOptions.mode === "testing") {
6218
6244
  const allComponentIds = app.components.list.map((c) => c.result.id);
6219
6245
  globalScriptResult = await scriptManager.compileAllInstances(allComponentIds, normalizedOptions.mode);
6220
6246
  Object.assign(outputFiles, globalScriptResult.outputFiles);
@@ -6286,6 +6312,17 @@ function createRenderer({
6286
6312
  try {
6287
6313
  const content = await readFile3(manifestPath, "utf8");
6288
6314
  manifest = JSON.parse(content);
6315
+ for (const [path2, metadata] of Object.entries(manifest.physical || {})) {
6316
+ if (metadata.dependencies) {
6317
+ app._dependencyGraph.directPageComponents[path2] = metadata.dependencies;
6318
+ }
6319
+ }
6320
+ for (const [path2, metadata] of Object.entries(manifest.virtual || {})) {
6321
+ if (metadata.dependencies) {
6322
+ app._dependencyGraph.directPageComponents[path2] = metadata.dependencies;
6323
+ }
6324
+ }
6325
+ app._refreshDependencyGraph();
6289
6326
  } catch (e) {
6290
6327
  if (e.code !== "ENOENT") {
6291
6328
  handleError2({
@@ -6294,30 +6331,39 @@ function createRenderer({
6294
6331
  });
6295
6332
  }
6296
6333
  }
6334
+ const mocksStr = app.options.testing?.mocks ? serialize2(app.options.testing.mocks) : "";
6335
+ const mocksHash = hash(mocksStr);
6336
+ const mocksChanged = manifest.testingMocksHash !== mocksHash;
6297
6337
  const pagesToRender = [];
6298
6338
  const skippedPages = [];
6299
6339
  const newManifest = {
6300
6340
  physical: {},
6301
6341
  virtual: {},
6302
- dependencies: {}
6342
+ dependencies: {},
6343
+ testingMocksHash: mocksHash
6303
6344
  };
6304
6345
  const componentChanges = /* @__PURE__ */ new Map();
6305
6346
  const allComponents = app.components.list;
6347
+ let anyComponentChanged = false;
6306
6348
  for (const component of allComponents) {
6307
6349
  const { changed, metadata } = await checkFileChange(component.path.pathname, manifest.physical[component.path.pathname]);
6308
6350
  newManifest.physical[component.path.pathname] = metadata;
6309
- if (changed) {
6351
+ if (changed || !manifest.physical[component.path.pathname]) {
6310
6352
  componentChanges.set(component.result.id, true);
6353
+ anyComponentChanged = true;
6311
6354
  await app.components.updateItem(component.path.pathname);
6312
6355
  }
6313
6356
  }
6357
+ if (anyComponentChanged) {
6358
+ app._refreshDependencyGraph();
6359
+ }
6314
6360
  const pageCustomElements = {
6315
6361
  ...manifest.dependencies,
6316
6362
  ...app._dependencyGraph.pageCustomElements
6317
6363
  };
6318
6364
  for (const pageItem of queue) {
6319
6365
  let shouldRebuild = false;
6320
- if (normalizedOptions.output && normalizedOptions.mode === "production") {
6366
+ if (normalizedOptions.output && (normalizedOptions.mode === "production" || normalizedOptions.mode === "testing")) {
6321
6367
  const relativeDir = relative3(normalizedOptions.path.pages, pageItem.path.dirname);
6322
6368
  const outFile = join4(normalizedOptions.output, relativeDir, pageItem.path.filename);
6323
6369
  try {
@@ -6336,6 +6382,9 @@ function createRenderer({
6336
6382
  if (componentIds.some((id) => componentChanges.get(id))) {
6337
6383
  shouldRebuild = true;
6338
6384
  }
6385
+ if (mocksChanged) {
6386
+ shouldRebuild = true;
6387
+ }
6339
6388
  if (pageItem.virtual) {
6340
6389
  if (shouldRebuild || pageItem.volatile || !manifest.virtual || !manifest.virtual[pageItem.path.pathname] || String(manifest.virtual[pageItem.path.pathname].cacheKey) !== String(pageItem.cacheKey) || normalizedOptions.mode === "development") {
6341
6390
  shouldRebuild = true;
@@ -6385,10 +6434,17 @@ function createRenderer({
6385
6434
  }
6386
6435
  }
6387
6436
  }
6388
- const { pageCustomElements: livePageCustomElements } = app._dependencyGraph;
6437
+ const { pageCustomElements: livePageCustomElements, directPageComponents: liveDirectPageComponents } = app._dependencyGraph;
6389
6438
  for (const [id, pages] of Object.entries(livePageCustomElements)) {
6390
6439
  newManifest.dependencies[id] = Array.from(pages);
6391
6440
  }
6441
+ for (const [path2, deps] of Object.entries(liveDirectPageComponents)) {
6442
+ if (newManifest.physical[path2]) {
6443
+ newManifest.physical[path2].dependencies = deps;
6444
+ } else if (newManifest.virtual[path2]) {
6445
+ newManifest.virtual[path2].dependencies = deps;
6446
+ }
6447
+ }
6392
6448
  for (const [id, pages] of Object.entries(manifest.dependencies || {})) {
6393
6449
  if (!newManifest.dependencies[id]) {
6394
6450
  newManifest.dependencies[id] = pages;
@@ -6490,7 +6546,6 @@ function createRenderer({
6490
6546
  });
6491
6547
  renderQueues.delete(buildId);
6492
6548
  sealedQueues.delete(buildId);
6493
- app._clearDependencies();
6494
6549
  pagesToRender.length = 0;
6495
6550
  skippedPages.length = 0;
6496
6551
  }
@@ -6528,7 +6583,8 @@ async function createCoralite({
6528
6583
  skipRenderByAttribute,
6529
6584
  onError,
6530
6585
  mode = "production",
6531
- output
6586
+ output,
6587
+ testing
6532
6588
  }) {
6533
6589
  if (!components || typeof components !== "string") {
6534
6590
  handleError({
@@ -6564,7 +6620,8 @@ async function createCoralite({
6564
6620
  skipRenderByAttribute,
6565
6621
  mode,
6566
6622
  path: path2,
6567
- output: output ? normalize(output) : void 0
6623
+ output: output ? normalize(output) : void 0,
6624
+ testing
6568
6625
  };
6569
6626
  const trackedOutputFiles = /* @__PURE__ */ new Set();
6570
6627
  const app = {
@@ -6595,11 +6652,44 @@ async function createCoralite({
6595
6652
  createComponentElement: null,
6596
6653
  _dependencyGraph: {
6597
6654
  pageCustomElements: {},
6598
- childCustomElements: {}
6655
+ directPageComponents: {}
6656
+ },
6657
+ _refreshDependencyGraph: () => {
6658
+ const { pageCustomElements, directPageComponents } = app._dependencyGraph;
6659
+ for (const tag in pageCustomElements) {
6660
+ delete pageCustomElements[tag];
6661
+ }
6662
+ const resolveDependencies = (pagePath, directComponents) => {
6663
+ const visited = /* @__PURE__ */ new Set();
6664
+ const allDependencies = /* @__PURE__ */ new Set();
6665
+ const walk = (tags) => {
6666
+ for (const tag of tags) {
6667
+ if (visited.has(tag)) {
6668
+ continue;
6669
+ }
6670
+ visited.add(tag);
6671
+ allDependencies.add(tag);
6672
+ const sharedFn = scriptManager.sharedFunctions[tag];
6673
+ if (sharedFn && sharedFn.components?.length) {
6674
+ walk(sharedFn.components);
6675
+ }
6676
+ }
6677
+ };
6678
+ walk(directComponents);
6679
+ for (const tag of allDependencies) {
6680
+ if (!pageCustomElements[tag]) {
6681
+ pageCustomElements[tag] = /* @__PURE__ */ new Set();
6682
+ }
6683
+ pageCustomElements[tag].add(pagePath);
6684
+ }
6685
+ };
6686
+ for (const [pagePath, directComponents] of Object.entries(directPageComponents)) {
6687
+ resolveDependencies(pagePath, directComponents);
6688
+ }
6599
6689
  },
6600
6690
  _clearDependencies: () => {
6601
6691
  app._dependencyGraph.pageCustomElements = {};
6602
- app._dependencyGraph.childCustomElements = {};
6692
+ app._dependencyGraph.directPageComponents = {};
6603
6693
  }
6604
6694
  };
6605
6695
  const plugins = {
@@ -6768,16 +6858,21 @@ async function createCoralite({
6768
6858
  const item = app.components.getItem(targetPath);
6769
6859
  const results = [];
6770
6860
  if (item) {
6771
- const id = app._dependencyGraph.childCustomElements[item.result.id] || item.result.id;
6861
+ const id = item.result.id;
6772
6862
  const pce = app._dependencyGraph.pageCustomElements[id];
6773
6863
  if (pce) {
6774
6864
  pce.forEach((p) => results.push(p));
6775
6865
  }
6866
+ } else {
6867
+ const pce = app._dependencyGraph.pageCustomElements[targetPath];
6868
+ if (pce) {
6869
+ pce.forEach((p) => results.push(p));
6870
+ }
6776
6871
  }
6777
6872
  return results;
6778
6873
  }
6779
6874
  });
6780
- if (app.options.mode === "development") {
6875
+ if (app.options.mode === "testing") {
6781
6876
  app.options.plugins.unshift(testingPlugin);
6782
6877
  }
6783
6878
  app.options.plugins.unshift(metadataPlugin);
@@ -6835,7 +6930,7 @@ async function createCoralite({
6835
6930
  onUpdate: handlers.onPageUpdate,
6836
6931
  onDelete: handlers.onPageDelete
6837
6932
  });
6838
- if (app.options.mode === "production") {
6933
+ if (app.options.mode === "production" || app.options.mode === "testing") {
6839
6934
  for await (const file of discoverHtmlFiles({
6840
6935
  path: app.options.pages,
6841
6936
  recursive: true,
@@ -6876,6 +6971,30 @@ function defineConfig(options) {
6876
6971
  throw new CoraliteError(`Config property "${prop}" cannot be empty`);
6877
6972
  }
6878
6973
  }
6974
+ if (options.mode !== void 0) {
6975
+ const validModes = ["production", "development", "testing"];
6976
+ if (!validModes.includes(options.mode)) {
6977
+ throw new CoraliteError(`Invalid mode: "${options.mode}". Valid modes are: ${validModes.join(", ")}`);
6978
+ }
6979
+ }
6980
+ if (options.testing !== void 0) {
6981
+ if (typeof options.testing !== "object" || options.testing === null) {
6982
+ throw new CoraliteError('Config property "testing" must be an object');
6983
+ }
6984
+ if (options.testing.mocks !== void 0) {
6985
+ if (typeof options.testing.mocks !== "object" || options.testing.mocks === null) {
6986
+ throw new CoraliteError('Config property "testing.mocks" must be an object');
6987
+ }
6988
+ for (const [key, mock] of Object.entries(options.testing.mocks)) {
6989
+ if (typeof mock !== "object" || mock === null) {
6990
+ throw new CoraliteError(`Mock for component "${key}" must be an object`);
6991
+ }
6992
+ if (mock.server !== void 0 && typeof mock.server !== "function") {
6993
+ throw new CoraliteError(`Mock server for component "${key}" must be a function`);
6994
+ }
6995
+ }
6996
+ }
6997
+ }
6879
6998
  if ("plugins" in options && options.plugins !== void 0) {
6880
6999
  if (!Array.isArray(options.plugins)) {
6881
7000
  throw new CoraliteError(