apostrophe 4.30.0 → 4.31.0-alpha.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/claude-tools/detect-handles.js +46 -0
  3. package/claude-tools/minimal-hang-test.js +28 -0
  4. package/claude-tools/mongo-close-test.js +11 -0
  5. package/claude-tools/stdin-ref-test.js +14 -0
  6. package/eslint.config.js +3 -1
  7. package/modules/@apostrophecms/area/index.js +94 -2
  8. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -40
  9. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +0 -1
  10. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +0 -1
  11. package/modules/@apostrophecms/attachment/index.js +4 -1
  12. package/modules/@apostrophecms/db/index.js +68 -27
  13. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +5 -3
  14. package/modules/@apostrophecms/express/index.js +2 -0
  15. package/modules/@apostrophecms/http/index.js +1 -1
  16. package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
  17. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +2 -2
  18. package/modules/@apostrophecms/job/index.js +9 -7
  19. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +0 -1
  20. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -1
  21. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +10 -2
  22. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +3 -3
  23. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +52 -23
  24. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +6 -1
  25. package/modules/@apostrophecms/oembed/index.js +2 -1
  26. package/modules/@apostrophecms/piece-page-type/index.js +7 -0
  27. package/modules/@apostrophecms/piece-type/index.js +2 -1
  28. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +7 -2
  29. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +1 -0
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +21 -4
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  32. package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +7 -2
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +1 -0
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +1 -1
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +1 -0
  36. package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +10 -0
  37. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +1 -0
  38. package/modules/@apostrophecms/template/index.js +117 -11
  39. package/modules/@apostrophecms/template/lib/jsxLoader.js +128 -0
  40. package/modules/@apostrophecms/template/lib/jsxRender.js +490 -0
  41. package/modules/@apostrophecms/template/lib/jsxRuntime.js +276 -0
  42. package/modules/@apostrophecms/template/lib/nunjucksLoader.js +11 -36
  43. package/modules/@apostrophecms/template/lib/viewWatcher.js +113 -0
  44. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
  45. package/modules/@apostrophecms/ui/ui/apos/components/AposCellLastEdited.vue +1 -1
  46. package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +1 -0
  47. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +10 -4
  48. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +6 -1
  49. package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +1 -1
  50. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +1 -1
  51. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
  52. package/modules/@apostrophecms/uploadfs/index.js +3 -0
  53. package/modules/@apostrophecms/util/index.js +3 -3
  54. package/package.json +14 -10
  55. package/test/add-missing-schema-fields-project/test.js +22 -3
  56. package/test/assets.js +110 -67
  57. package/test/db-tools.js +365 -0
  58. package/test/db.js +24 -15
  59. package/test/default-adapter.js +256 -0
  60. package/test/external-front.js +419 -1
  61. package/test/job.js +1 -1
  62. package/test/modules/jsx-area-test/index.js +23 -0
  63. package/test/modules/jsx-area-test/views/bad-area.jsx +7 -0
  64. package/test/modules/jsx-area-test/views/with-area-ctx.jsx +13 -0
  65. package/test/modules/jsx-area-test/views/with-area.jsx +7 -0
  66. package/test/modules/jsx-area-test/views/with-widget-ctx.jsx +12 -0
  67. package/test/modules/jsx-area-test/views/with-widget.jsx +7 -0
  68. package/test/modules/jsx-async-widget/index.js +6 -0
  69. package/test/modules/jsx-async-widget/views/widget.jsx +11 -0
  70. package/test/modules/jsx-bridge-test/index.js +1 -0
  71. package/test/modules/jsx-bridge-test/views/cross-module.jsx +7 -0
  72. package/test/modules/jsx-bridge-test/views/disambig-name-only.jsx +7 -0
  73. package/test/modules/jsx-bridge-test/views/disambig-target.jsx +8 -0
  74. package/test/modules/jsx-bridge-test/views/disambig-with-template-name.jsx +7 -0
  75. package/test/modules/jsx-bridge-test/views/include-html.jsx +7 -0
  76. package/test/modules/jsx-bridge-test/views/include-target.html +4 -0
  77. package/test/modules/jsx-bridge-test/views/jsx-extends-via-extend.jsx +9 -0
  78. package/test/modules/jsx-bridge-test/views/jsx-extends.jsx +9 -0
  79. package/test/modules/jsx-bridge-test/views/jsx-layout.jsx +14 -0
  80. package/test/modules/jsx-bridge-test/views/njk-extends.jsx +14 -0
  81. package/test/modules/jsx-bridge-test/views/njk-layout.html +9 -0
  82. package/test/modules/jsx-bridge-test/views/short-form.jsx +7 -0
  83. package/test/modules/jsx-bridge-test/views/short-target.jsx +3 -0
  84. package/test/modules/jsx-component-test/index.js +15 -0
  85. package/test/modules/jsx-component-test/views/greet.html +1 -0
  86. package/test/modules/jsx-component-test/views/uses-component.jsx +8 -0
  87. package/test/modules/jsx-ctx-widget/index.js +6 -0
  88. package/test/modules/jsx-ctx-widget/views/widget.jsx +4 -0
  89. package/test/modules/jsx-mixed-test/index.js +9 -0
  90. package/test/modules/jsx-mixed-test/views/apos-full.jsx +21 -0
  91. package/test/modules/jsx-mixed-test/views/async-list.jsx +12 -0
  92. package/test/modules/jsx-mixed-test/views/lib/format.js +3 -0
  93. package/test/modules/jsx-mixed-test/views/localized.jsx +3 -0
  94. package/test/modules/jsx-mixed-test/views/partial.jsx +3 -0
  95. package/test/modules/jsx-mixed-test/views/safe-helper.jsx +3 -0
  96. package/test/modules/jsx-mixed-test/views/syntax-error.jsx +3 -0
  97. package/test/modules/jsx-mixed-test/views/throws.jsx +5 -0
  98. package/test/modules/jsx-mixed-test/views/uses-import.jsx +5 -0
  99. package/test/modules/jsx-mixed-test/views/uses-require.jsx +5 -0
  100. package/test/modules/jsx-watcher-cross-test/index.js +5 -0
  101. package/test/modules/jsx-watcher-cross-test/views/cross-template.jsx +3 -0
  102. package/test/modules/jsx-watcher-test/index.js +5 -0
  103. package/test/modules/jsx-watcher-test/views/watcher-test.jsx +3 -0
  104. package/test/modules/template-jsx-options-test/index.js +12 -0
  105. package/test/modules/template-jsx-options-test/views/options-test.jsx +9 -0
  106. package/test/modules/template-jsx-subclass-test/index.js +3 -0
  107. package/test/modules/template-jsx-subclass-test/views/override-test.jsx +3 -0
  108. package/test/modules/template-jsx-test/index.js +9 -0
  109. package/test/modules/template-jsx-test/views/boolean-attrs.jsx +11 -0
  110. package/test/modules/template-jsx-test/views/class-and-for.jsx +7 -0
  111. package/test/modules/template-jsx-test/views/dangerously-set.jsx +3 -0
  112. package/test/modules/template-jsx-test/views/escape-attr.jsx +3 -0
  113. package/test/modules/template-jsx-test/views/escape-body.jsx +3 -0
  114. package/test/modules/template-jsx-test/views/inherit-test.jsx +3 -0
  115. package/test/modules/template-jsx-test/views/list.jsx +7 -0
  116. package/test/modules/template-jsx-test/views/override-test.jsx +3 -0
  117. package/test/modules/template-jsx-test/views/svg-attrs.jsx +27 -0
  118. package/test/modules/template-jsx-test/views/test.jsx +3 -0
  119. package/test/modules/template-jsx-test/views/void-elements.jsx +9 -0
  120. package/test/templates-jsx-watcher.js +135 -0
  121. package/test/templates-jsx.js +537 -0
  122. package/test-lib/util.js +50 -14
  123. package/.claude/settings.local.json +0 -15
  124. package/lib/mongodb-connect.js +0 -62
  125. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +0 -131
@@ -76,6 +76,32 @@ module.exports = {
76
76
  self.insertions = {};
77
77
  self.runtimeNodes = {};
78
78
 
79
+ // Install the .jsx require hook and teach the JSX runtime about
80
+ // Nunjucks' SafeString class so its instances pass through unescaped.
81
+ self.initJsx();
82
+
83
+ // Wire up the view-folder watcher with the two default invalidation
84
+ // handlers — Nunjucks loader caches and compiled .jsx modules. Both
85
+ // engines share a single set of chokidar watchers so we don't pay
86
+ // twice for watching the same directories.
87
+ const jsxLoader = require('./lib/jsxLoader.js');
88
+ self.onViewChange(function clearNunjucksLoaderCaches() {
89
+ // Setting `cache = {}` mirrors the historical in-loader behavior
90
+ // and is exactly what Nunjucks itself reads when looking up a
91
+ // previously-loaded template.
92
+ for (const loader of Object.values(self.loaders || {})) {
93
+ loader.cache = {};
94
+ }
95
+ });
96
+ self.onViewChange(function invalidateJsxModules(filePath) {
97
+ if (filePath && filePath.endsWith('.jsx')) {
98
+ jsxLoader.invalidate(path.resolve(filePath));
99
+ } else {
100
+ // Anything else (e.g. a Nunjucks file) might be a template imported
101
+ // by a `.jsx` file via require()/import — be safe and drop them all.
102
+ jsxLoader.invalidateAll();
103
+ }
104
+ });
79
105
  },
80
106
  handlers(self) {
81
107
  return {
@@ -117,9 +143,15 @@ module.exports = {
117
143
  },
118
144
  'apostrophe:destroy': {
119
145
  async nunjucksLoaderCleanup() {
146
+ // Older code paths used to manage chokidar watchers per loader;
147
+ // a no-op `destroy()` is still defined for backwards compat.
120
148
  for (const loader of Object.values(self.loaders || {})) {
121
149
  await loader.destroy();
122
150
  }
151
+ },
152
+ async closeViewWatchers() {
153
+ // Tear down chokidar watchers (Nunjucks + JSX share these).
154
+ await self.closeViewWatchers();
123
155
  }
124
156
  }
125
157
  };
@@ -127,6 +159,19 @@ module.exports = {
127
159
  methods(self) {
128
160
  return {
129
161
  ...require('./lib/bundlesLoader')(self),
162
+ ...require('./lib/jsxRender')(self),
163
+ ...require('./lib/viewWatcher')(self),
164
+
165
+ // Arm chokidar for the view-folder chain of the module whose views
166
+ // actually contain the resolved JSX file. For a same-module render
167
+ // that's the caller; for a cross-module render like
168
+ // `@apostrophecms/page` rendering `@apostrophecms/home-page:page`
169
+ // it's the target module. Idempotent per absolute directory.
170
+ watchJsxRenderTargets(callerModule, resolved) {
171
+ const owner = (resolved && self.apos.modules[resolved.moduleName]) ||
172
+ callerModule;
173
+ self.watchViewFolders(self.getViewFolders(owner));
174
+ },
130
175
 
131
176
  // Add helpers in the namespace for a particular module.
132
177
  // They will be visible in nunjucks at
@@ -261,6 +306,24 @@ module.exports = {
261
306
 
262
307
  let result;
263
308
 
309
+ // For named files, resolve through the module's view-folder
310
+ // chain. Chain position wins: a closer directory's .html/.njk
311
+ // beats a more distant directory's .jsx. JSX only takes
312
+ // precedence over Nunjucks within the same directory. See
313
+ // resolveTemplate. Falling back to Nunjucks happens automatically
314
+ // below when the resolved file is not JSX.
315
+ if (type === 'file') {
316
+ const resolved = self.resolveTemplate(module, s);
317
+ if (resolved && resolved.kind === 'jsx') {
318
+ const renderData = self.getRenderDataArgs(req, data, module);
319
+ result = await self.renderJsxTemplate(req, resolved, renderData, module);
320
+ if (process.platform === 'win32') {
321
+ result = result.replaceAll('\r', '');
322
+ }
323
+ return result;
324
+ }
325
+ }
326
+
264
327
  const args = self.getRenderArgs(req, data, module);
265
328
 
266
329
  const env = self.getEnv(req, module);
@@ -495,6 +558,9 @@ module.exports = {
495
558
  }
496
559
  if (!self.loaders[key]) {
497
560
  self.loaders[key] = self.newLoader(moduleName, dirs);
561
+ // Register these dirs with the shared view watcher (idempotent
562
+ // per absolute path, so calling it for every loader is fine).
563
+ self.watchViewFolders(dirs);
498
564
  }
499
565
  return self.loaders[key];
500
566
  },
@@ -1231,7 +1297,7 @@ module.exports = {
1231
1297
  async annotateDataForExternalFront(req, template, data, moduleName) {
1232
1298
  const docs = self.getDocsForExternalFront(req, template, data, moduleName);
1233
1299
  for (const doc of docs) {
1234
- self.annotateDocForExternalFront(doc, { scene: req.scene });
1300
+ await self.annotateDocForExternalFront(doc, { scene: req.scene });
1235
1301
  }
1236
1302
  data.aposBodyData = await self.getBodyData(req);
1237
1303
  // Already contains module name too
@@ -1283,16 +1349,31 @@ module.exports = {
1283
1349
  ].filter(doc => !!doc);
1284
1350
  },
1285
1351
 
1286
- annotateDocForExternalFront(doc, { scene } = {}) {
1352
+ async annotateDocForExternalFront(doc, { scene } = {}) {
1353
+ const handled = new WeakSet();
1354
+ const missingAreas = [];
1287
1355
  self.apos.doc.walk(doc, (o, k, v) => {
1356
+ if (o._edit === true && !handled.has(o)) {
1357
+ handled.add(o);
1358
+ for (const field of self.missingSchemaAreas(o)) {
1359
+ missingAreas.push([ o, field ]);
1360
+ }
1361
+ }
1288
1362
  if (v && v.metaType === 'area') {
1289
- const manager = self.apos.util.getManagerOf(o);
1363
+ // A missing manager here is expected (e.g. an area reached on a
1364
+ // container without a manager) and handled below, so suppress the
1365
+ // low-level per-call log and rely on the once-per-process warning.
1366
+ const manager = self.apos.util.getManagerOf(o, { log: false });
1290
1367
  if (!manager) {
1291
- self.apos.util.warnDevOnce('noManagerForDocInExternalFront', `No manager for: ${o.metaType} ${o.type || ''}`);
1368
+ self.apos.util.warnDevOnce(
1369
+ 'noManagerForDocInExternalFront',
1370
+ `No manager for: ${o.metaType} ${o.type || ''}`
1371
+ );
1292
1372
  return;
1293
1373
  }
1294
1374
  const field = manager.schema.find(f => f.name === k);
1295
1375
  if (!field) {
1376
+ v._isOrphan = true;
1296
1377
  self.apos.util.warnDevOnce(
1297
1378
  'noSchemaFieldForAreaInExternalFront',
1298
1379
  `Area ${k} has no matching schema field in ${o.metaType} ${o.type || ''}`
@@ -1302,6 +1383,14 @@ module.exports = {
1302
1383
  return self.annotateAreaForExternalFront(field, v, { scene });
1303
1384
  }
1304
1385
  });
1386
+ // Materialize every missing area, after the walk so we never add keys
1387
+ // to an object while it is being traversed.
1388
+ for (const [ o, field ] of missingAreas) {
1389
+ const area = await self.apos.area.addMissingArea(o, field.name);
1390
+ area._edit = true;
1391
+ area._docId = o._docId ?? (o.metaType === 'doc' ? o._id : null);
1392
+ self.annotateAreaForExternalFront(field, area, { scene });
1393
+ }
1305
1394
  },
1306
1395
 
1307
1396
  // Annotate an area for easy rendering by an external front end
@@ -1310,6 +1399,7 @@ module.exports = {
1310
1399
  // at least as an empty array.
1311
1400
 
1312
1401
  annotateAreaForExternalFront(field, area, { scene } = {}) {
1402
+ area._aposAnnotated = true;
1313
1403
  area.field = field;
1314
1404
  area.options = field.options;
1315
1405
  // Really widget configurations, but the method name is already set in
@@ -1324,25 +1414,41 @@ module.exports = {
1324
1414
  };
1325
1415
  }).filter(choice => !!choice);
1326
1416
 
1327
- area.items ||= [];
1417
+ // Drop corrupt items (null, or not a widget).
1418
+ area.items = (area.items || []).filter((item) => {
1419
+ const valid = item && item.metaType === 'widget' && item.type;
1420
+ if (!valid) {
1421
+ self.apos.util.warnDevOnce(
1422
+ 'corruptAreaItemInExternalFront',
1423
+ `Dropping malformed item in area ${area._id || ''}`
1424
+ );
1425
+ }
1426
+ return valid;
1427
+ });
1428
+
1328
1429
  for (const item of area.items) {
1329
1430
  // Add _docId if area has one
1330
1431
  if (area._docId) {
1331
1432
  item._docId = area._docId;
1332
1433
  }
1333
1434
 
1334
- // Annotate each individual widget with its options
1335
- // Each widget must elect into this by creating an
1336
- // `annotateWidgetForExternalFront() method.
1435
+ // Annotate each individual widget with its options. Each widget must
1436
+ // elect into this by creating an `annotateWidgetForExternalFront()`
1437
+ // method.
1337
1438
  const manager = self.apos.area.getWidgetManager(item.type);
1338
1439
  if (manager) {
1339
- const widgetOptions = manager.annotateWidgetForExternalFront(item, { scene });
1340
- item._options = widgetOptions;
1440
+ item._options = manager.annotateWidgetForExternalFront(item, { scene });
1341
1441
  } else {
1342
1442
  self.apos.area.warnMissingWidgetType(item.type);
1343
- throw self.apos.error('invalid', 'Missing widget type');
1344
1443
  }
1345
1444
  }
1445
+ },
1446
+
1447
+ // The schema area fields of `object` that have no value yet. Returns an
1448
+ // empty array for anything without a schema manager.
1449
+ missingSchemaAreas(object) {
1450
+ const schema = self.apos.util.getManagerOf(object, { log: false })?.schema ?? [];
1451
+ return schema.filter(field => field.type === 'area' && !object[field.name]);
1346
1452
  }
1347
1453
  };
1348
1454
  }
@@ -0,0 +1,128 @@
1
+ // Compiles Apostrophe `.jsx` template files via Babel and registers a
2
+ // `require.extensions['.jsx']` hook so they can be loaded with `require()`
3
+ // (and `import` after CommonJS transformation) just like normal modules.
4
+ //
5
+ // Each compiled module is automatically prefixed with a `require()` of our
6
+ // JSX runtime so the `h` and `Fragment` identifiers produced by the Babel
7
+ // transform resolve without the user importing them. Both `import` and
8
+ // `require` work inside `.jsx` files because the CommonJS transform also
9
+ // runs.
10
+ //
11
+ // Source maps are kept in memory and wired through `source-map-support`,
12
+ // which means stack traces from a JSX template point at the original
13
+ // `views/page.jsx` line/column rather than the compiled output.
14
+
15
+ const fs = require('fs');
16
+ const Module = require('module');
17
+ const babel = require('@babel/core');
18
+ const sourceMapSupport = require('source-map-support');
19
+
20
+ const runtimePath = require.resolve('./jsxRuntime.js');
21
+
22
+ const sourceMaps = new Map();
23
+ let installed = false;
24
+
25
+ // Idempotent: register the require hook + source-map handler once per
26
+ // process even if multiple Apostrophe instances boot in the same Node
27
+ // process (e.g. tests, multisite).
28
+ function install() {
29
+ if (installed) {
30
+ return;
31
+ }
32
+ installed = true;
33
+
34
+ sourceMapSupport.install({
35
+ environment: 'node',
36
+ hookRequire: false,
37
+ handleUncaughtExceptions: false,
38
+ retrieveSourceMap(filename) {
39
+ const map = sourceMaps.get(filename);
40
+ if (!map) {
41
+ return null;
42
+ }
43
+ return {
44
+ url: filename,
45
+ map
46
+ };
47
+ }
48
+ });
49
+
50
+ Module._extensions['.jsx'] = function(module, filename) {
51
+ const src = fs.readFileSync(filename, 'utf-8');
52
+ const compiled = compile(src, filename);
53
+ sourceMaps.set(filename, compiled.map);
54
+ module._compile(compiled.code, filename);
55
+ };
56
+ }
57
+
58
+ // Compile a JSX source string for `filename`. Returns `{ code, map }`.
59
+ // `code` is CommonJS-compatible JS with our runtime injected at the top.
60
+ function compile(src, filename) {
61
+ let result;
62
+ try {
63
+ result = babel.transformSync(src, {
64
+ filename,
65
+ sourceMaps: true,
66
+ sourceFileName: filename,
67
+ babelrc: false,
68
+ configFile: false,
69
+ compact: false,
70
+ plugins: [
71
+ [
72
+ require.resolve('@babel/plugin-transform-react-jsx'),
73
+ {
74
+ pragma: '__aposJsx.h',
75
+ pragmaFrag: '__aposJsx.Fragment',
76
+ useBuiltIns: false,
77
+ throwIfNamespace: false
78
+ }
79
+ ],
80
+ require.resolve('@babel/plugin-transform-modules-commonjs')
81
+ ]
82
+ });
83
+ } catch (e) {
84
+ // Babel errors already include code frames pointing at the offending
85
+ // line/column. Preserve that detail and add the file path for clarity.
86
+ const err = new Error(`JSX compile error in ${filename}: ${e.message}`);
87
+ err.cause = e;
88
+ err.code = 'APOS_JSX_COMPILE_ERROR';
89
+ err.filename = filename;
90
+ throw err;
91
+ }
92
+
93
+ // Inject runtime references. Using a single `__aposJsx` namespace avoids
94
+ // colliding with user variables named `h` or `Fragment` while still
95
+ // matching the pragma we passed to Babel above. Source maps remain valid
96
+ // because we only prepend a single line and rely on a leading `\n` to
97
+ // keep line numbers stable.
98
+ const prefix = `var __aposJsx = require(${JSON.stringify(runtimePath)});\n`;
99
+ return {
100
+ code: prefix + result.code,
101
+ map: result.map
102
+ };
103
+ }
104
+
105
+ // Drop a single .jsx file from the require cache and our source-map cache.
106
+ // Called by the template module's chokidar watcher when a JSX file changes,
107
+ // so the next render picks up the new code without restarting the process.
108
+ function invalidate(filename) {
109
+ sourceMaps.delete(filename);
110
+ delete Module._cache[filename];
111
+ }
112
+
113
+ // Drop every cached .jsx module and source map. Used when watcher events
114
+ // don't carry a specific path or when an unknown view file was modified.
115
+ function invalidateAll() {
116
+ for (const filename of sourceMaps.keys()) {
117
+ delete Module._cache[filename];
118
+ }
119
+ sourceMaps.clear();
120
+ }
121
+
122
+ module.exports = {
123
+ install,
124
+ compile,
125
+ invalidate,
126
+ invalidateAll,
127
+ runtimePath
128
+ };