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
@@ -0,0 +1,15 @@
1
+ const Promise = require('bluebird');
2
+
3
+ module.exports = {
4
+ components(self) {
5
+ return {
6
+ async greet(req, input) {
7
+ await Promise.delay(20);
8
+ return {
9
+ who: input.who,
10
+ afterDelay: true
11
+ };
12
+ }
13
+ };
14
+ }
15
+ };
@@ -0,0 +1 @@
1
+ <span class="greet">Hello {{ data.who }}{% if data.afterDelay %} (after delay){% endif %}</span>
@@ -0,0 +1,8 @@
1
+ export default function (data, { Component }) {
2
+ return (
3
+ <section>
4
+ <h1>Welcome</h1>
5
+ <Component module='jsx-component-test' name='greet' who={data.name} />
6
+ </section>
7
+ );
8
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ extend: '@apostrophecms/widget-type',
3
+ options: {
4
+ label: 'JSX Ctx Widget'
5
+ }
6
+ };
@@ -0,0 +1,4 @@
1
+ export default function (data) {
2
+ const tag = (data.contextOptions && data.contextOptions.tag) || 'no-tag';
3
+ return <span class='ctx-widget' data-tag={tag}>{tag}</span>;
4
+ }
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ init(self) {
3
+ self.addHelpers({
4
+ safeBold(text) {
5
+ return self.apos.template.safe(`<b>${self.apos.util.escapeHtml(text)}</b>`);
6
+ }
7
+ });
8
+ }
9
+ };
@@ -0,0 +1,21 @@
1
+ export default async function (data, { apos, helpers }) {
2
+ const id = apos.util.generateId();
3
+ // apos.doc.find returns a real cursor we can await. apos.doc here is the
4
+ // doc-module instance from self.apos, not a helper bag.
5
+ const docs = await apos.doc.find(data.req, { type: '@apostrophecms/global' }).toArray();
6
+ // The doc module instance has methods (e.g. find) that the helper bag
7
+ // does not — and the two `modules` collections are different objects.
8
+ const aposDocIsModule = typeof apos.doc.find === 'function';
9
+ const helpersDocIsHelperBag = (apos.doc !== helpers.modules['@apostrophecms/doc']);
10
+ const aposModulesIsNotHelpersModules = (apos.modules !== helpers.modules);
11
+ return (
12
+ <div
13
+ data-id-length={String(id.length)}
14
+ data-global-count={String(docs.length)}
15
+ data-distinct={String(apos !== helpers)}
16
+ data-apos-doc-is-module={String(aposDocIsModule)}
17
+ data-helpers-doc-is-helper-bag={String(helpersDocIsHelperBag)}
18
+ data-modules-distinct={String(aposModulesIsNotHelpersModules)}
19
+ />
20
+ );
21
+ }
@@ -0,0 +1,12 @@
1
+ async function bullet(value) {
2
+ await new Promise((resolve) => setTimeout(resolve, 5));
3
+ return <li>{value}</li>;
4
+ }
5
+
6
+ export default async function (data) {
7
+ return (
8
+ <ul>
9
+ {data.items.map((item) => bullet(item))}
10
+ </ul>
11
+ );
12
+ }
@@ -0,0 +1,3 @@
1
+ module.exports = function format(value) {
2
+ return `[${value}]`;
3
+ };
@@ -0,0 +1,3 @@
1
+ export default function (data, { __t }) {
2
+ return <p>{__t('apostrophe:notFoundPageTitle')}</p>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function (data) {
2
+ return <p class='partial'>partial:{data.item}</p>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function (data, { helpers }) {
2
+ return <div>{helpers.modules['jsx-mixed-test'].safeBold('hello & <world>')}</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function () {
2
+ return <div>missing closing tag<;
3
+ }
@@ -0,0 +1,5 @@
1
+ export default function () {
2
+ // intentionally accesses a property of undefined to trigger an error
3
+ const data = undefined;
4
+ return <p>{data.boom}</p>;
5
+ }
@@ -0,0 +1,5 @@
1
+ import format from './lib/format.js';
2
+
3
+ export default function (data) {
4
+ return <span>{format(data.value)}</span>;
5
+ }
@@ -0,0 +1,5 @@
1
+ const format = require('./lib/format.js');
2
+
3
+ export default function (data) {
4
+ return <span>{format(data.value)}</span>;
5
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ options: {
3
+ ignoreNoCodeWarning: true
4
+ }
5
+ };
@@ -0,0 +1,3 @@
1
+ export default function () {
2
+ return <h1>cross-original</h1>;
3
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ options: {
3
+ ignoreNoCodeWarning: true
4
+ }
5
+ };
@@ -0,0 +1,3 @@
1
+ export default function () {
2
+ return <h1>own-original</h1>;
3
+ }
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ options: {
3
+ spiffiness: 'nifty'
4
+ },
5
+ init(self) {
6
+ self.addHelpers({
7
+ test(a) {
8
+ return a * 2;
9
+ }
10
+ });
11
+ }
12
+ };
@@ -0,0 +1,9 @@
1
+ export default function (data, { helpers }) {
2
+ return (
3
+ <>
4
+ {helpers.modules['template-jsx-options-test'].options.spiffiness}
5
+ {' '}
6
+ {helpers.modules['template-jsx-options-test'].test(2)}
7
+ </>
8
+ );
9
+ }
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ extend: 'template-jsx-test'
3
+ };
@@ -0,0 +1,3 @@
1
+ export default function () {
2
+ return <h1>I am overridden</h1>;
3
+ }
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ options: {
3
+ templateData: {
4
+ age: 30,
5
+ multiline: 'first line\nsecond line\n<a href="javascript:alert(\'oh no\')">CSRF attempt</a>',
6
+ multilineSafe: 'first line\nsecond line\n<a href="http://niceurl.com">This is okay</a>'
7
+ }
8
+ }
9
+ };
@@ -0,0 +1,11 @@
1
+ export default function () {
2
+ return (
3
+ <input
4
+ type='checkbox'
5
+ checked
6
+ disabled={false}
7
+ data-extra={null}
8
+ data-undefined={undefined}
9
+ />
10
+ );
11
+ }
@@ -0,0 +1,7 @@
1
+ export default function () {
2
+ return (
3
+ <label className='form-label' htmlFor='username'>
4
+ Username
5
+ </label>
6
+ );
7
+ }
@@ -0,0 +1,3 @@
1
+ export default function (data) {
2
+ return <div dangerouslySetInnerHTML={{ __html: data.html }} />;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function (data) {
2
+ return <a href={data.url} title={data.title}>link</a>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function (data) {
2
+ return <p>{data.html}</p>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function () {
2
+ return <h1>I am inherited</h1>;
3
+ }
@@ -0,0 +1,7 @@
1
+ export default function (data) {
2
+ return (
3
+ <ul>
4
+ {data.items.map((item) => <li key={item}>{item}</li>)}
5
+ </ul>
6
+ );
7
+ }
@@ -0,0 +1,3 @@
1
+ export default function () {
2
+ return <h1>I am a bug</h1>;
3
+ }
@@ -0,0 +1,27 @@
1
+ // Verifies that camelCase SVG presentation attributes are emitted in
2
+ // the kebab-case form expected by browsers parsing text/html, and that
3
+ // natively camelCase SVG attributes (viewBox, preserveAspectRatio) are
4
+ // preserved.
5
+
6
+ export default function () {
7
+ return (
8
+ <svg
9
+ xmlns='http://www.w3.org/2000/svg'
10
+ viewBox='0 0 24 24'
11
+ preserveAspectRatio='xMidYMid meet'
12
+ fill='none'
13
+ stroke='currentColor'
14
+ strokeWidth='2'
15
+ strokeLinecap='round'
16
+ strokeLinejoin='round'
17
+ strokeDasharray='4 2'
18
+ strokeOpacity='0.5'
19
+ fillRule='evenodd'
20
+ clipRule='evenodd'
21
+ pointerEvents='none'
22
+ textAnchor='middle'
23
+ >
24
+ <path d='M0 0L24 24' />
25
+ </svg>
26
+ );
27
+ }
@@ -0,0 +1,3 @@
1
+ export default function (data) {
2
+ return <h1>{data.age}</h1>;
3
+ }
@@ -0,0 +1,9 @@
1
+ export default function () {
2
+ return (
3
+ <>
4
+ <hr />
5
+ <input type='text' name='email' />
6
+ <br />
7
+ </>
8
+ );
9
+ }
@@ -0,0 +1,135 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert/strict');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const isWsl = require('is-wsl');
6
+
7
+ describe('Templates: JSX watcher', function () {
8
+
9
+ let apos;
10
+ this.timeout(t.timeout);
11
+
12
+ // Two fixture modules. `jsx-watcher-test` is the simple case: render its
13
+ // own template directly. `jsx-watcher-cross-test` is the realistic-page
14
+ // case: another module renders this one's template via the qualified
15
+ // `module:template` form (mirroring how `@apostrophecms/page` renders
16
+ // `@apostrophecms/home-page:page` in the wild). The first surfaces a
17
+ // bug where renderBody never arms a watcher for JSX renders; the second
18
+ // surfaces a bug where the watcher was armed against the caller's views
19
+ // instead of the resolved module's views.
20
+ const ownModule = 'jsx-watcher-test';
21
+ const ownTemplate = 'watcher-test';
22
+ const ownPath = path.join(
23
+ __dirname, 'modules', ownModule, 'views', `${ownTemplate}.jsx`
24
+ );
25
+ const ownOriginal = 'export default function () {\n return <h1>own-original</h1>;\n}\n';
26
+ const ownChanged = 'export default function () {\n return <h1>own-changed</h1>;\n}\n';
27
+
28
+ const crossModule = 'jsx-watcher-cross-test';
29
+ const crossTemplate = 'cross-template';
30
+ const crossPath = path.join(
31
+ __dirname, 'modules', crossModule, 'views', `${crossTemplate}.jsx`
32
+ );
33
+ const crossOriginal = 'export default function () {\n return <h1>cross-original</h1>;\n}\n';
34
+ const crossChanged = 'export default function () {\n return <h1>cross-changed</h1>;\n}\n';
35
+
36
+ before(async function () {
37
+ // Defend against poisoned fixtures from a prior failed run.
38
+ fs.writeFileSync(ownPath, ownOriginal);
39
+ fs.writeFileSync(crossPath, crossOriginal);
40
+ apos = await t.create({
41
+ root: module,
42
+ modules: {
43
+ [ownModule]: {},
44
+ [crossModule]: {}
45
+ }
46
+ });
47
+ });
48
+
49
+ after(async function () {
50
+ try {
51
+ fs.writeFileSync(ownPath, ownOriginal);
52
+ } catch (e) {
53
+ // best-effort restore
54
+ }
55
+ try {
56
+ fs.writeFileSync(crossPath, crossOriginal);
57
+ } catch (e) {
58
+ // best-effort restore
59
+ }
60
+ return t.destroy(apos);
61
+ });
62
+
63
+ // Wait for chokidar's initial scan to complete on every watcher the
64
+ // template module has registered. If the JSX render path failed to arm
65
+ // any watcher for our views directory, no `change` event would ever
66
+ // fire and the caller's `waitForChange` would time out.
67
+ async function waitForWatchersReady() {
68
+ await Promise.all(
69
+ (apos.template._viewWatchers || []).map(watcher => {
70
+ if (watcher._readyEmitted) {
71
+ return Promise.resolve();
72
+ }
73
+ return new Promise(resolve => watcher.on('ready', resolve));
74
+ })
75
+ );
76
+ }
77
+
78
+ function waitForChange(absolutePath) {
79
+ return new Promise((resolve, reject) => {
80
+ const timer = setTimeout(
81
+ () => reject(new Error(`No view-change event was observed for ${absolutePath}.`)),
82
+ 5000
83
+ );
84
+ apos.template.onViewChange(filePath => {
85
+ if (filePath && path.resolve(filePath) === path.resolve(absolutePath)) {
86
+ clearTimeout(timer);
87
+ resolve();
88
+ }
89
+ });
90
+ });
91
+ }
92
+
93
+ it('should pick up edits to a .jsx template rendered through its own module', async function () {
94
+ if (isWsl) {
95
+ this.skip();
96
+ return;
97
+ }
98
+ const req = apos.task.getAnonReq();
99
+ const initial = await apos.modules[ownModule].render(req, ownTemplate);
100
+ assert.match(initial, /own-original/);
101
+
102
+ await waitForWatchersReady();
103
+ const sawChange = waitForChange(ownPath);
104
+ fs.writeFileSync(ownPath, ownChanged);
105
+ await sawChange;
106
+
107
+ const after = await apos.modules[ownModule].render(req, ownTemplate);
108
+ assert.match(after, /own-changed/);
109
+ assert.doesNotMatch(after, /own-original/);
110
+ });
111
+
112
+ it('should pick up edits when one module renders another module\'s .jsx via `module:template`', async function () {
113
+ if (isWsl) {
114
+ this.skip();
115
+ return;
116
+ }
117
+ const req = apos.task.getAnonReq();
118
+ // The caller is `ownModule`, but the template lives in `crossModule`.
119
+ // This is the shape that breaks if the watcher is armed against the
120
+ // caller's view chain instead of the resolved file's module.
121
+ const qualified = `${crossModule}:${crossTemplate}`;
122
+ const initial = await apos.modules[ownModule].render(req, qualified);
123
+ assert.match(initial, /cross-original/);
124
+
125
+ await waitForWatchersReady();
126
+ const sawChange = waitForChange(crossPath);
127
+ fs.writeFileSync(crossPath, crossChanged);
128
+ await sawChange;
129
+
130
+ const after = await apos.modules[ownModule].render(req, qualified);
131
+ assert.match(after, /cross-changed/);
132
+ assert.doesNotMatch(after, /cross-original/);
133
+ });
134
+
135
+ });