coralite 0.40.2 → 0.42.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
@@ -2248,6 +2248,22 @@ function findAndExtractScript(code) {
2248
2248
  let startLine = value.loc.start.line - 1;
2249
2249
  let prefix = "";
2250
2250
  let content = "";
2251
+ let instanceIdVar;
2252
+ if (value.params && value.params[0]) {
2253
+ const param = value.params[0];
2254
+ if (param.type === "Identifier") {
2255
+ instanceIdVar = param.name + ".instanceId";
2256
+ } else if (param.type === "ObjectPattern") {
2257
+ const idProp = param.properties.find((p) => p.key?.type === "Identifier" && p.key?.name === "instanceId");
2258
+ if (idProp) {
2259
+ if (idProp.value.type === "Identifier") {
2260
+ instanceIdVar = idProp.value.name;
2261
+ }
2262
+ }
2263
+ }
2264
+ } else if (method) {
2265
+ instanceIdVar = "this._instanceId";
2266
+ }
2251
2267
  let source = code.slice(value.start, value.end);
2252
2268
  const replacements = [];
2253
2269
  walkJS(value, {
@@ -2262,7 +2278,7 @@ function findAndExtractScript(code) {
2262
2278
  replacements.push({
2263
2279
  start: node2.right.end - value.start,
2264
2280
  end: node2.right.end - value.start,
2265
- replacement: ")"
2281
+ replacement: `, ${instanceIdVar})`
2266
2282
  });
2267
2283
  }
2268
2284
  },
@@ -2292,7 +2308,7 @@ function findAndExtractScript(code) {
2292
2308
  replacements.push({
2293
2309
  start: arg.end - value.start,
2294
2310
  end: arg.end - value.start,
2295
- replacement: ")"
2311
+ replacement: `, ${instanceIdVar})`
2296
2312
  });
2297
2313
  }
2298
2314
  }
@@ -3460,62 +3476,131 @@ var staticAssetPlugin = (assets = []) => {
3460
3476
  };
3461
3477
 
3462
3478
  // plugins/testing.js
3463
- function traverseAndAddTestId(children) {
3479
+ function traverseAndAddTestId(children, instanceId, { autoTestId = false, counters = {}, mode = "production" } = {}) {
3464
3480
  if (!Array.isArray(children)) {
3465
3481
  return;
3466
3482
  }
3483
+ const isProduction = mode === "production";
3484
+ const isDevOrTest = mode === "development" || mode === "testing";
3485
+ let prefix = "";
3486
+ if (instanceId === "page") {
3487
+ prefix = "page__";
3488
+ } else if (instanceId) {
3489
+ prefix = `${instanceId}__`;
3490
+ }
3467
3491
  for (let i = 0; i < children.length; i++) {
3468
3492
  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;
3493
+ if (node.type === "tag") {
3494
+ if (node.attribs) {
3495
+ if (node.attribs.test !== void 0) {
3496
+ delete node.attribs.test;
3497
+ }
3498
+ if (node.attribs["data-testid"] !== void 0) {
3499
+ if (isProduction) {
3500
+ delete node.attribs["data-testid"];
3501
+ } else if (isDevOrTest) {
3502
+ const val = node.attribs["data-testid"];
3503
+ if (prefix && !val.startsWith(prefix)) {
3504
+ node.attribs["data-testid"] = `${prefix}${val}`;
3505
+ }
3506
+ }
3507
+ }
3508
+ }
3509
+ if (autoTestId && isDevOrTest) {
3510
+ const tagName = node.name.toLowerCase();
3511
+ const isInteractive = [
3512
+ "button",
3513
+ "a",
3514
+ "input",
3515
+ "form",
3516
+ "select",
3517
+ "textarea"
3518
+ ].includes(tagName) || node.attribs && (node.attribs.tabindex !== void 0 || node.attribs.role && ["button", "link", "checkbox"].includes(node.attribs.role)) || node.slots;
3519
+ if (isInteractive) {
3520
+ if (!counters[tagName]) {
3521
+ counters[tagName] = 0;
3522
+ }
3523
+ const index = counters[tagName]++;
3524
+ if (!node.attribs) {
3525
+ node.attribs = {};
3526
+ }
3527
+ if (!node.attribs["data-testid"]) {
3528
+ node.attribs["data-testid"] = `${prefix}${tagName}-${index}`;
3529
+ }
3530
+ }
3472
3531
  }
3473
3532
  }
3474
3533
  if (node.children?.length > 0) {
3475
- traverseAndAddTestId(node.children);
3534
+ traverseAndAddTestId(node.children, instanceId, {
3535
+ autoTestId,
3536
+ counters,
3537
+ mode
3538
+ });
3476
3539
  }
3477
3540
  }
3478
3541
  }
3479
3542
  var testingPlugin = definePlugin({
3480
3543
  name: "testing",
3481
3544
  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
- }
3545
+ onBeforeBuild: ({ app }) => {
3546
+ if (app.options.mode !== "testing") {
3547
+ return;
3492
3548
  }
3549
+ app.options.externalStyles = app.options.externalStyles || [];
3550
+ const velocityStyle = `
3551
+ *, *::before, *::after {
3552
+ transition: none !important;
3553
+ animation: none !important;
3554
+ scroll-behavior: auto !important;
3555
+ }
3556
+ `.trim();
3557
+ app.options.externalStyles.push(`data:text/css;base64,${Buffer.from(velocityStyle).toString("base64")}`);
3493
3558
  },
3494
- onComponentSet: ({ component }) => {
3495
- const children = component?.template?.children;
3496
- if (children) {
3497
- traverseAndAddTestId(children);
3559
+ onBeforeComponentRender: ({ instanceId, template, app }) => {
3560
+ const mode = app.options.mode;
3561
+ const isDevOrTest = mode === "development" || mode === "testing";
3562
+ const counters = {};
3563
+ const templateNode = template;
3564
+ if (templateNode && templateNode.children) {
3565
+ traverseAndAddTestId(templateNode.children, instanceId, {
3566
+ autoTestId: isDevOrTest,
3567
+ counters,
3568
+ mode
3569
+ });
3498
3570
  }
3499
3571
  },
3500
- onPageSet: ({ elements }) => {
3501
- const children = elements?.root?.children;
3502
- if (children) {
3503
- traverseAndAddTestId(children);
3572
+ onAfterComponentRender: ({ result, app }) => {
3573
+ const mode = app.options.mode;
3574
+ if (mode !== "production") {
3575
+ return;
3504
3576
  }
3505
- }
3506
- },
3507
- client: {
3508
- onBeforeComponentRender: ({ instanceId, refs }) => {
3509
- for (let i = 0; i < refs.length; i++) {
3510
- const ref = refs[i];
3511
- const uniqueRefValue = `${instanceId}__${ref.name}`;
3512
- if (ref.element && ref.element.setAttribute) {
3513
- const currentTestId = ref.element.getAttribute("data-testid");
3514
- if (!currentTestId || currentTestId === ref.name) {
3515
- ref.element.setAttribute("data-testid", uniqueRefValue);
3577
+ const traverse = (children) => {
3578
+ if (!Array.isArray(children)) {
3579
+ return;
3580
+ }
3581
+ for (const node of children) {
3582
+ if (node.type === "tag" && node.attribs) {
3583
+ delete node.attribs["data-testid"];
3584
+ delete node.attribs.test;
3585
+ }
3586
+ if (node.children) {
3587
+ traverse(node.children);
3516
3588
  }
3517
3589
  }
3590
+ };
3591
+ if (result && result.children) {
3592
+ traverse(result.children);
3518
3593
  }
3594
+ },
3595
+ onPageSet: ({ elements, app }) => {
3596
+ const mode = app.options.mode;
3597
+ const isDevOrTest = mode === "development" || mode === "testing";
3598
+ const counters = {};
3599
+ traverseAndAddTestId(elements?.root?.children, "page", {
3600
+ autoTestId: isDevOrTest,
3601
+ counters,
3602
+ mode
3603
+ });
3519
3604
  }
3520
3605
  }
3521
3606
  });
@@ -4190,8 +4275,15 @@ function createComponentDefinition({ app }) {
4190
4275
  }
4191
4276
  }
4192
4277
  }
4193
- if (typeof server === "function") {
4194
- const serverResult = await server({
4278
+ let serverToExecute = server;
4279
+ if (app.options.mode === "testing" && app.options.testing?.mocks) {
4280
+ const mock = app.options.testing.mocks[module.id];
4281
+ if (mock && typeof mock.server === "function") {
4282
+ serverToExecute = mock.server;
4283
+ }
4284
+ }
4285
+ if (typeof serverToExecute === "function") {
4286
+ const serverResult = await serverToExecute({
4195
4287
  ...context2,
4196
4288
  ...initialState
4197
4289
  });
@@ -4609,8 +4701,8 @@ function createPageHandlers({
4609
4701
  data,
4610
4702
  app
4611
4703
  });
4612
- const isProduction = app.options.mode === "production";
4613
- if (isProduction && !data.virtual) {
4704
+ const isStatic = app.options.mode === "production" || app.options.mode === "testing";
4705
+ if (isStatic && !data.virtual) {
4614
4706
  delete data.content;
4615
4707
  }
4616
4708
  return {
@@ -4619,16 +4711,16 @@ function createPageHandlers({
4619
4711
  state: mappedContext.state,
4620
4712
  page: mappedContext.page,
4621
4713
  path: mappedContext.data.path,
4622
- root: isProduction ? null : mappedContext.elements.root,
4623
- customElements: isProduction ? null : mappedContext.elements.customElements,
4624
- tempElements: isProduction ? null : mappedContext.elements.tempElements,
4625
- skipRenderElements: isProduction ? null : mappedContext.elements.skipRenderElements
4714
+ root: isStatic ? null : mappedContext.elements.root,
4715
+ customElements: isStatic ? null : mappedContext.elements.customElements,
4716
+ tempElements: isStatic ? null : mappedContext.elements.tempElements,
4717
+ skipRenderElements: isStatic ? null : mappedContext.elements.skipRenderElements
4626
4718
  },
4627
4719
  state: mappedContext.state
4628
4720
  };
4629
4721
  };
4630
4722
  const onPageUpdateLocal = async (newValue, oldValue) => {
4631
- if (app.options.mode === "production") {
4723
+ if (app.options.mode === "production" || app.options.mode === "testing") {
4632
4724
  if (!newValue.result) {
4633
4725
  await onFileSetLocal(newValue);
4634
4726
  }
@@ -4869,7 +4961,7 @@ ${css}
4869
4961
  root.children.unshift(styleElement);
4870
4962
  }
4871
4963
  }
4872
- function injectReadinessScript(root, head, hasScripts) {
4964
+ function injectReadinessScript(root, head, hasScripts, mode) {
4873
4965
  const readinessScriptElement = createCoraliteElement({
4874
4966
  type: "tag",
4875
4967
  name: "script",
@@ -4878,6 +4970,12 @@ function injectReadinessScript(root, head, hasScripts) {
4878
4970
  children: []
4879
4971
  });
4880
4972
  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() };";
4973
+ if (mode) {
4974
+ data += ` window.__coralite__.mode = '${mode}';`;
4975
+ }
4976
+ if (mode === "testing") {
4977
+ data += " window.__coralite__.components = {}; window.__coralite__.events = [];";
4978
+ }
4881
4979
  if (!hasScripts) {
4882
4980
  data += " window.__coralite__.lifecycle._start(0, 0);";
4883
4981
  }
@@ -4892,8 +4990,12 @@ function injectReadinessScript(root, head, hasScripts) {
4892
4990
  root.children.unshift(readinessScriptElement);
4893
4991
  }
4894
4992
  }
4895
- function injectImportMap(root, head, importMap) {
4896
- if (!importMap || Object.keys(importMap).length === 0) {
4993
+ function injectImportMap(root, head, importMap, base) {
4994
+ const finalImportMap = { ...importMap };
4995
+ if (base) {
4996
+ finalImportMap["assets/js/manifest.js"] = `${base}assets/js/manifest.js`;
4997
+ }
4998
+ if (Object.keys(finalImportMap).length === 0) {
4897
4999
  return;
4898
5000
  }
4899
5001
  const importMapElement = createCoraliteElement({
@@ -4907,7 +5009,7 @@ function injectImportMap(root, head, importMap) {
4907
5009
  });
4908
5010
  importMapElement.children.push(createCoraliteTextNode({
4909
5011
  type: "text",
4910
- data: JSON.stringify({ imports: importMap }),
5012
+ data: JSON.stringify({ imports: finalImportMap }),
4911
5013
  parent: importMapElement
4912
5014
  }));
4913
5015
  if (head) {
@@ -4974,25 +5076,50 @@ function resolvePageQueue(pagesCollection, path2) {
4974
5076
  function generateClientRuntime({
4975
5077
  base,
4976
5078
  sharedChunkPath,
4977
- chunkManifest,
4978
5079
  declarativeTags = [],
4979
- hydrationData = "{}"
5080
+ hydrationData = "{}",
5081
+ mode = "production"
4980
5082
  }) {
4981
5083
  return `
4982
5084
  import { getClientContext, createCoraliteClass, globalClientHooks } from '${base}assets/js/${sharedChunkPath}';
5085
+ import componentManifest from '${base}assets/js/manifest.js';
4983
5086
 
4984
5087
  (async () => {
4985
5088
  const hydrationData = ${hydrationData};
4986
5089
  const declarativeTags = ${JSON.stringify(declarativeTags)};
4987
- const initialElements = Array.from(document.querySelectorAll('[data-coralite-initial]'))
4988
- .filter(el => declarativeTags.includes(el.tagName.toLowerCase()));
5090
+ window.__coralite_mode__ = '${mode}';
5091
+
5092
+ const initialElements = Array.from(document.querySelectorAll('[data-cid]'))
5093
+ .filter(el => {
5094
+ const tagName = el.tagName.toLowerCase();
5095
+ const isDeclarative = declarativeTags.includes(tagName);
5096
+ const isInitial = el.hasAttribute('data-coralite-initial');
5097
+
5098
+ if (window['__coralite__'] && window['__coralite__'].components && isInitial) {
5099
+ const cid = el.getAttribute('data-cid');
5100
+ if (cid && !hydrationData[cid]) {
5101
+ const error = new Error('Coralite Hydration Mismatch: Component with data-cid "' + cid + '" (' + tagName + ') has no matching server hydration data.');
5102
+ if (typeof window['showCoraliteError'] === 'function') {
5103
+ window['showCoraliteError'](error);
5104
+ } else {
5105
+ console.error(error);
5106
+ const overlay = document.createElement('div');
5107
+ 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;';
5108
+ overlay.innerHTML = '<h1>Coralite Hydration Mismatch</h1><p>' + error.message + '</p>';
5109
+ document.body.appendChild(overlay);
5110
+ }
5111
+ throw error;
5112
+ }
5113
+ }
5114
+
5115
+ return isDeclarative && isInitial;
5116
+ });
4989
5117
  if (window.__coralite__ && window.__coralite__.lifecycle) {
4990
5118
  window.__coralite__.lifecycle._start(initialElements.length, declarativeTags.length);
4991
5119
  }
4992
5120
  globalThis.executableScripts = [];
4993
5121
  globalThis.globalAbortController = new AbortController();
4994
5122
 
4995
- const componentManifest = ${JSON.stringify(chunkManifest)};
4996
5123
  const loadCache = {};
4997
5124
 
4998
5125
  const loadComponent = (componentId) => {
@@ -5007,7 +5134,10 @@ import { getClientContext, createCoraliteClass, globalClientHooks } from '${base
5007
5134
 
5008
5135
  if (cssPath) {
5009
5136
  const fullCssPath = '${base}assets/css/' + cssPath;
5010
- if (!document.querySelector('link[href="' + fullCssPath + '"]')) {
5137
+ const inlineStyles = document.getElementById('coralite-inline-styles');
5138
+ const hasStyleInInline = inlineStyles && inlineStyles.textContent.includes('[data-style-selector="' + componentId + '"]');
5139
+
5140
+ if (!document.querySelector('link[href="' + fullCssPath + '"]') && !hasStyleInInline) {
5011
5141
  const link = document.createElement('link');
5012
5142
  link.rel = 'stylesheet';
5013
5143
  link.href = fullCssPath;
@@ -5072,8 +5202,38 @@ import { getClientContext, createCoraliteClass, globalClientHooks } from '${base
5072
5202
  return element;
5073
5203
  };
5074
5204
 
5075
- window.processHTML = (html) => {
5205
+ window.processHTML = (html, instanceId) => {
5076
5206
  if (typeof html !== 'string') return html;
5207
+
5208
+ const mode = window.__coralite_mode__;
5209
+ const isDevOrTest = mode === 'development' || mode === 'testing';
5210
+ const isProduction = mode === 'production';
5211
+
5212
+ if (isDevOrTest || isProduction) {
5213
+ html = html.replace(/<([a-zA-Z0-9-]+)([^>]*)>/g, (match, tagName, attrs) => {
5214
+ let newAttrs = attrs;
5215
+
5216
+ // Strip deprecated 'test' attribute
5217
+ newAttrs = newAttrs.replace(/\\s+test\\s*=\\s*(['"]).*?\\1/g, '');
5218
+
5219
+ // Handle data-testid
5220
+ const testIdRegex = /\\s+data-testid\\s*=\\s*(['"])(.*?)\\1/g;
5221
+ if (isProduction) {
5222
+ newAttrs = newAttrs.replace(testIdRegex, '');
5223
+ } else if (isDevOrTest) {
5224
+ const prefix = instanceId ? instanceId + '__' : '';
5225
+ if (prefix) {
5226
+ newAttrs = newAttrs.replace(testIdRegex, (attrMatch, quote, testValue) => {
5227
+ if (testValue.startsWith(prefix)) return attrMatch;
5228
+ return ' data-testid="' + prefix + testValue + '"';
5229
+ });
5230
+ }
5231
+ }
5232
+
5233
+ return '<' + tagName + newAttrs + '>';
5234
+ });
5235
+ }
5236
+
5077
5237
  const matches = html.matchAll(/<([a-zA-Z0-9-]+)/g);
5078
5238
  for (const match of matches) {
5079
5239
  const tag = match[1].toLowerCase();
@@ -5424,20 +5584,11 @@ function createRenderer({
5424
5584
  componentState = cleanKeys(componentState);
5425
5585
  }
5426
5586
  const module = cloneModuleInstance(moduleComponent.result);
5427
- if (module.values && module.values.refs) {
5428
- for (let i = 0; i < module.values.refs.length; i++) {
5429
- const ref = module.values.refs[i];
5430
- const uniqueRefValue = `${instanceId}__${ref.name}`;
5431
- if (ref.element && ref.element.attribs) {
5432
- ref.element.attribs.ref = uniqueRefValue;
5433
- }
5434
- componentState[`ref_${ref.name}`] = uniqueRefValue;
5435
- }
5436
- }
5437
5587
  const mappedComponentContext = await hooks.trigger("onBeforeComponentRender", {
5438
5588
  state: componentState,
5439
5589
  componentId: module.id,
5440
5590
  instanceId,
5591
+ template: module.template,
5441
5592
  refs: module.values.refs,
5442
5593
  textNodes: module.values.textNodes,
5443
5594
  attributes: module.values.attributes,
@@ -5447,6 +5598,16 @@ function createRenderer({
5447
5598
  app
5448
5599
  });
5449
5600
  componentState = mappedComponentContext.state;
5601
+ if (module.values && module.values.refs) {
5602
+ for (let i = 0; i < module.values.refs.length; i++) {
5603
+ const ref = module.values.refs[i];
5604
+ const uniqueRefValue = `${instanceId}__${ref.name}`;
5605
+ if (ref.element && ref.element.attribs) {
5606
+ ref.element.attribs.ref = uniqueRefValue;
5607
+ }
5608
+ componentState[`ref_${ref.name}`] = uniqueRefValue;
5609
+ }
5610
+ }
5450
5611
  const result = module.template;
5451
5612
  if (module.styles.length) {
5452
5613
  const selector = module.id;
@@ -5947,27 +6108,7 @@ function createRenderer({
5947
6108
  injectExternalStyles(mappedComponent.root, headElement, normalizedOptions.externalStyles);
5948
6109
  }
5949
6110
  if (mappedSessionObject.styles.size > 0) {
5950
- if (isProduction && globalScriptResult && globalScriptResult.manifest) {
5951
- const cssPaths = [];
5952
- const remainingStyles = /* @__PURE__ */ new Map();
5953
- for (const [selector, css] of mappedSessionObject.styles) {
5954
- const entry = globalScriptResult.manifest[selector];
5955
- if (entry && entry.css) {
5956
- cssPaths.push(entry.css);
5957
- } else {
5958
- remainingStyles.set(selector, css);
5959
- }
5960
- }
5961
- if (cssPaths.length > 0) {
5962
- const base = normalizedOptions.baseURL.endsWith("/") ? normalizedOptions.baseURL : normalizedOptions.baseURL + "/";
5963
- injectExternalStyleLinks(mappedComponent.root, headElement, cssPaths, base);
5964
- }
5965
- if (remainingStyles.size > 0) {
5966
- injectStyles(mappedComponent.root, headElement, remainingStyles);
5967
- }
5968
- } else {
5969
- injectStyles(mappedComponent.root, headElement, mappedSessionObject.styles);
5970
- }
6111
+ injectStyles(mappedComponent.root, headElement, mappedSessionObject.styles);
5971
6112
  }
5972
6113
  if (componentsToInclude.size > 0) {
5973
6114
  const targetElement = headElement || bodyElement || mappedComponent.root;
@@ -6013,20 +6154,9 @@ function createRenderer({
6013
6154
  error: new Error(JSON.stringify(scriptResult.manifest))
6014
6155
  });
6015
6156
  }
6016
- const chunkManifest = {};
6017
- for (const tag of componentsToInclude) {
6018
- if (scriptResult.manifest[tag]) {
6019
- chunkManifest[tag] = scriptResult.manifest[tag];
6020
- }
6021
- }
6022
- for (const tag in scriptResult.manifest) {
6023
- if (tag !== "coralite-runtime" && !chunkManifest[tag]) {
6024
- chunkManifest[tag] = scriptResult.manifest[tag];
6025
- }
6026
- }
6027
- injectReadinessScript(mappedComponent.root, headElement, true);
6028
- injectImportMap(mappedComponent.root, headElement, scriptResult.importMap);
6157
+ injectReadinessScript(mappedComponent.root, headElement, true, normalizedOptions.mode);
6029
6158
  const base = normalizedOptions.baseURL.endsWith("/") ? normalizedOptions.baseURL : normalizedOptions.baseURL + "/";
6159
+ injectImportMap(mappedComponent.root, headElement, scriptResult.importMap, base);
6030
6160
  const hydrationData = {};
6031
6161
  for (const [id, instance] of Object.entries(instances)) {
6032
6162
  if (instance.state && Object.keys(instance.state).length > 0) {
@@ -6037,9 +6167,9 @@ function createRenderer({
6037
6167
  const scriptContent = generateClientRuntime({
6038
6168
  base,
6039
6169
  sharedChunkPath: scriptResult.manifest["coralite-runtime"],
6040
- chunkManifest,
6041
6170
  declarativeTags: Array.from(declarativeTags),
6042
- hydrationData: serialize2(hydrationData)
6171
+ hydrationData: serialize2(hydrationData),
6172
+ mode: normalizedOptions.mode
6043
6173
  });
6044
6174
  const scriptElement = createCoraliteElement({
6045
6175
  type: "tag",
@@ -6057,7 +6187,7 @@ function createRenderer({
6057
6187
  }
6058
6188
  removeElements(mappedComponent.skipRenderElements, true);
6059
6189
  if (!mappedSessionObject.scripts.content[mappedComponent.path.pathname]) {
6060
- injectReadinessScript(mappedComponent.root, headElement, false);
6190
+ injectReadinessScript(mappedComponent.root, headElement, false, normalizedOptions.mode);
6061
6191
  }
6062
6192
  const rawHTML = transformNode(mappedComponent.root);
6063
6193
  const result = {
@@ -6158,10 +6288,74 @@ function createRenderer({
6158
6288
  if (!buildOptions) {
6159
6289
  buildOptions = {};
6160
6290
  }
6161
- if (normalizedOptions.mode === "production") {
6291
+ const projectRoot = app.options.projectRoot || process.cwd();
6292
+ const cacheDir = join4(projectRoot, ".coralite");
6293
+ const manifestPath = join4(cacheDir, "manifest.json");
6294
+ let manifest = {
6295
+ physical: {},
6296
+ virtual: {},
6297
+ dependencies: {},
6298
+ components: {}
6299
+ };
6300
+ try {
6301
+ const content = await readFile3(manifestPath, "utf8");
6302
+ manifest = JSON.parse(content);
6303
+ if (!manifest.components) {
6304
+ manifest.components = {};
6305
+ }
6306
+ } catch (e) {
6307
+ if (e.code !== "ENOENT") {
6308
+ handleError2({
6309
+ level: "WARN",
6310
+ message: `Could not parse manifest at ${manifestPath}: ${e.message}. Starting with fresh manifest.`
6311
+ });
6312
+ }
6313
+ }
6314
+ let componentBuildInfo = {
6315
+ completed: 0,
6316
+ skipped: 0,
6317
+ details: []
6318
+ };
6319
+ if (normalizedOptions.mode === "production" || normalizedOptions.mode === "testing") {
6162
6320
  const allComponentIds = app.components.list.map((c) => c.result.id);
6163
6321
  globalScriptResult = await scriptManager.compileAllInstances(allComponentIds, normalizedOptions.mode);
6164
6322
  Object.assign(outputFiles, globalScriptResult.outputFiles);
6323
+ if (globalScriptResult.manifest) {
6324
+ const manifestJS = `export default ${JSON.stringify(globalScriptResult.manifest)};`;
6325
+ outputFiles["manifest.js"] = {
6326
+ path: "assets/js/manifest.js",
6327
+ hashedPath: "manifest.js",
6328
+ text: manifestJS
6329
+ };
6330
+ const newComponentManifest = globalScriptResult.manifest;
6331
+ const oldComponentManifest = manifest.components;
6332
+ for (const [id, value] of Object.entries(newComponentManifest)) {
6333
+ if (id === "coralite-runtime") {
6334
+ continue;
6335
+ }
6336
+ const isNew = !oldComponentManifest[id];
6337
+ const hasChanged = !isNew && (value.js !== oldComponentManifest[id].js || value.css !== oldComponentManifest[id].css);
6338
+ if (isNew || hasChanged) {
6339
+ componentBuildInfo.completed++;
6340
+ componentBuildInfo.details.push({
6341
+ id,
6342
+ status: "built",
6343
+ reason: isNew ? "New component" : "Source changed"
6344
+ });
6345
+ } else {
6346
+ componentBuildInfo.skipped++;
6347
+ componentBuildInfo.details.push({
6348
+ id,
6349
+ status: "skipped"
6350
+ });
6351
+ }
6352
+ }
6353
+ if (typeof buildOptions.onComponentBuild === "function") {
6354
+ await buildOptions.onComponentBuild(componentBuildInfo);
6355
+ } else if (typeof normalizedOptions.onComponentBuild === "function") {
6356
+ await normalizedOptions.onComponentBuild(componentBuildInfo);
6357
+ }
6358
+ }
6165
6359
  } else if (normalizedOptions.mode === "development") {
6166
6360
  if (!siteWideBundlePromise) {
6167
6361
  const bundlePromise = (async () => {
@@ -6173,6 +6367,14 @@ function createRenderer({
6173
6367
  delete outputFiles[key];
6174
6368
  }
6175
6369
  Object.assign(outputFiles, result.outputFiles);
6370
+ if (result.manifest) {
6371
+ const manifestJS = `export default ${JSON.stringify(result.manifest)};`;
6372
+ outputFiles["manifest.js"] = {
6373
+ path: "assets/js/manifest.js",
6374
+ hashedPath: "manifest.js",
6375
+ text: manifestJS
6376
+ };
6377
+ }
6176
6378
  }
6177
6379
  return result;
6178
6380
  })();
@@ -6219,42 +6421,28 @@ function createRenderer({
6219
6421
  }
6220
6422
  }
6221
6423
  sealedQueues.add(buildId);
6222
- const projectRoot = app.options.projectRoot || process.cwd();
6223
- const cacheDir = join4(projectRoot, ".coralite");
6224
- const manifestPath = join4(cacheDir, "manifest.json");
6225
- let manifest = {
6226
- physical: {},
6227
- virtual: {},
6228
- dependencies: {}
6229
- };
6230
- try {
6231
- const content = await readFile3(manifestPath, "utf8");
6232
- manifest = JSON.parse(content);
6233
- for (const [path2, metadata] of Object.entries(manifest.physical || {})) {
6234
- if (metadata.dependencies) {
6235
- app._dependencyGraph.directPageComponents[path2] = metadata.dependencies;
6236
- }
6424
+ for (const [path2, metadata] of Object.entries(manifest.physical || {})) {
6425
+ if (metadata.dependencies) {
6426
+ app._dependencyGraph.directPageComponents[path2] = metadata.dependencies;
6237
6427
  }
6238
- for (const [path2, metadata] of Object.entries(manifest.virtual || {})) {
6239
- if (metadata.dependencies) {
6240
- app._dependencyGraph.directPageComponents[path2] = metadata.dependencies;
6241
- }
6242
- }
6243
- app._refreshDependencyGraph();
6244
- } catch (e) {
6245
- if (e.code !== "ENOENT") {
6246
- handleError2({
6247
- level: "WARN",
6248
- message: `Could not parse manifest at ${manifestPath}: ${e.message}. Starting with fresh manifest.`
6249
- });
6428
+ }
6429
+ for (const [path2, metadata] of Object.entries(manifest.virtual || {})) {
6430
+ if (metadata.dependencies) {
6431
+ app._dependencyGraph.directPageComponents[path2] = metadata.dependencies;
6250
6432
  }
6251
6433
  }
6434
+ app._refreshDependencyGraph();
6435
+ const mocksStr = app.options.testing?.mocks ? serialize2(app.options.testing.mocks) : "";
6436
+ const mocksHash = hash(mocksStr);
6437
+ const mocksChanged = manifest.testingMocksHash !== mocksHash;
6252
6438
  const pagesToRender = [];
6253
6439
  const skippedPages = [];
6254
6440
  const newManifest = {
6255
6441
  physical: {},
6256
6442
  virtual: {},
6257
- dependencies: {}
6443
+ dependencies: {},
6444
+ components: {},
6445
+ testingMocksHash: mocksHash
6258
6446
  };
6259
6447
  const componentChanges = /* @__PURE__ */ new Map();
6260
6448
  const allComponents = app.components.list;
@@ -6277,7 +6465,7 @@ function createRenderer({
6277
6465
  };
6278
6466
  for (const pageItem of queue) {
6279
6467
  let shouldRebuild = false;
6280
- if (normalizedOptions.output && normalizedOptions.mode === "production") {
6468
+ if (normalizedOptions.output && (normalizedOptions.mode === "production" || normalizedOptions.mode === "testing")) {
6281
6469
  const relativeDir = relative3(normalizedOptions.path.pages, pageItem.path.dirname);
6282
6470
  const outFile = join4(normalizedOptions.output, relativeDir, pageItem.path.filename);
6283
6471
  try {
@@ -6296,6 +6484,9 @@ function createRenderer({
6296
6484
  if (componentIds.some((id) => componentChanges.get(id))) {
6297
6485
  shouldRebuild = true;
6298
6486
  }
6487
+ if (mocksChanged) {
6488
+ shouldRebuild = true;
6489
+ }
6299
6490
  if (pageItem.virtual) {
6300
6491
  if (shouldRebuild || pageItem.volatile || !manifest.virtual || !manifest.virtual[pageItem.path.pathname] || String(manifest.virtual[pageItem.path.pathname].cacheKey) !== String(pageItem.cacheKey) || normalizedOptions.mode === "development") {
6301
6492
  shouldRebuild = true;
@@ -6427,6 +6618,7 @@ function createRenderer({
6427
6618
  }
6428
6619
  try {
6429
6620
  await mkdir2(cacheDir, { recursive: true });
6621
+ newManifest.components = globalScriptResult?.manifest || {};
6430
6622
  const tempManifestPath = `${manifestPath}.tmp`;
6431
6623
  await writeFile(tempManifestPath, JSON.stringify(newManifest, null, 2));
6432
6624
  await rename(tempManifestPath, manifestPath);
@@ -6493,8 +6685,10 @@ async function createCoralite({
6493
6685
  ignoreByAttribute,
6494
6686
  skipRenderByAttribute,
6495
6687
  onError,
6688
+ onComponentBuild,
6496
6689
  mode = "production",
6497
- output
6690
+ output,
6691
+ testing
6498
6692
  }) {
6499
6693
  if (!components || typeof components !== "string") {
6500
6694
  handleError({
@@ -6530,7 +6724,9 @@ async function createCoralite({
6530
6724
  skipRenderByAttribute,
6531
6725
  mode,
6532
6726
  path: path2,
6533
- output: output ? normalize(output) : void 0
6727
+ output: output ? normalize(output) : void 0,
6728
+ onComponentBuild,
6729
+ testing
6534
6730
  };
6535
6731
  const trackedOutputFiles = /* @__PURE__ */ new Set();
6536
6732
  const app = {
@@ -6781,9 +6977,7 @@ async function createCoralite({
6781
6977
  return results;
6782
6978
  }
6783
6979
  });
6784
- if (app.options.mode === "development") {
6785
- app.options.plugins.unshift(testingPlugin);
6786
- }
6980
+ app.options.plugins.unshift(testingPlugin);
6787
6981
  app.options.plugins.unshift(metadataPlugin);
6788
6982
  if (assets) {
6789
6983
  app.options.plugins.unshift(staticAssetPlugin(assets));
@@ -6839,7 +7033,7 @@ async function createCoralite({
6839
7033
  onUpdate: handlers.onPageUpdate,
6840
7034
  onDelete: handlers.onPageDelete
6841
7035
  });
6842
- if (app.options.mode === "production") {
7036
+ if (app.options.mode === "production" || app.options.mode === "testing") {
6843
7037
  for await (const file of discoverHtmlFiles({
6844
7038
  path: app.options.pages,
6845
7039
  recursive: true,
@@ -6880,6 +7074,30 @@ function defineConfig(options) {
6880
7074
  throw new CoraliteError(`Config property "${prop}" cannot be empty`);
6881
7075
  }
6882
7076
  }
7077
+ if (options.mode !== void 0) {
7078
+ const validModes = ["production", "development", "testing"];
7079
+ if (!validModes.includes(options.mode)) {
7080
+ throw new CoraliteError(`Invalid mode: "${options.mode}". Valid modes are: ${validModes.join(", ")}`);
7081
+ }
7082
+ }
7083
+ if (options.testing !== void 0) {
7084
+ if (typeof options.testing !== "object" || options.testing === null) {
7085
+ throw new CoraliteError('Config property "testing" must be an object');
7086
+ }
7087
+ if (options.testing.mocks !== void 0) {
7088
+ if (typeof options.testing.mocks !== "object" || options.testing.mocks === null) {
7089
+ throw new CoraliteError('Config property "testing.mocks" must be an object');
7090
+ }
7091
+ for (const [key, mock] of Object.entries(options.testing.mocks)) {
7092
+ if (typeof mock !== "object" || mock === null) {
7093
+ throw new CoraliteError(`Mock for component "${key}" must be an object`);
7094
+ }
7095
+ if (mock.server !== void 0 && typeof mock.server !== "function") {
7096
+ throw new CoraliteError(`Mock server for component "${key}" must be a function`);
7097
+ }
7098
+ }
7099
+ }
7100
+ }
6883
7101
  if ("plugins" in options && options.plugins !== void 0) {
6884
7102
  if (!Array.isArray(options.plugins)) {
6885
7103
  throw new CoraliteError(