apostrophe 4.30.1-beta.1 → 4.31.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.
Files changed (129) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/CHANGELOG.md +22 -2
  3. package/claude-tools/detect-handles.js +46 -0
  4. package/claude-tools/minimal-hang-test.js +28 -0
  5. package/claude-tools/mongo-close-test.js +11 -0
  6. package/claude-tools/stdin-ref-test.js +14 -0
  7. package/eslint.config.js +3 -1
  8. package/modules/@apostrophecms/area/index.js +94 -2
  9. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -40
  10. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +0 -1
  11. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +0 -1
  12. package/modules/@apostrophecms/attachment/index.js +4 -1
  13. package/modules/@apostrophecms/db/index.js +68 -27
  14. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +5 -3
  15. package/modules/@apostrophecms/express/index.js +2 -0
  16. package/modules/@apostrophecms/file/index.js +9 -8
  17. package/modules/@apostrophecms/http/index.js +1 -1
  18. package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
  19. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +2 -2
  20. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +3 -0
  21. package/modules/@apostrophecms/job/index.js +9 -7
  22. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +0 -1
  23. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -1
  24. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +10 -2
  25. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +3 -3
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +52 -23
  27. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +6 -1
  28. package/modules/@apostrophecms/oembed/index.js +2 -1
  29. package/modules/@apostrophecms/piece-type/index.js +2 -1
  30. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +7 -2
  31. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +1 -0
  32. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +21 -4
  33. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +7 -2
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +1 -0
  36. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +1 -1
  37. package/modules/@apostrophecms/schema/ui/apos/components/AposSubform.vue +1 -0
  38. package/modules/@apostrophecms/schema/ui/apos/logic/AposSubform.js +10 -0
  39. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +1 -0
  40. package/modules/@apostrophecms/template/index.js +117 -11
  41. package/modules/@apostrophecms/template/lib/jsxLoader.js +128 -0
  42. package/modules/@apostrophecms/template/lib/jsxRender.js +490 -0
  43. package/modules/@apostrophecms/template/lib/jsxRuntime.js +276 -0
  44. package/modules/@apostrophecms/template/lib/nunjucksLoader.js +11 -36
  45. package/modules/@apostrophecms/template/lib/viewWatcher.js +113 -0
  46. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
  47. package/modules/@apostrophecms/ui/ui/apos/components/AposCellLastEdited.vue +1 -1
  48. package/modules/@apostrophecms/ui/ui/apos/components/AposSelect.vue +1 -0
  49. package/modules/@apostrophecms/ui/ui/apos/components/AposSlat.vue +10 -4
  50. package/modules/@apostrophecms/ui/ui/apos/components/AposSlatList.vue +6 -1
  51. package/modules/@apostrophecms/ui/ui/apos/components/AposSubformPreview.vue +1 -1
  52. package/modules/@apostrophecms/ui/ui/apos/components/AposTreeHeader.vue +1 -1
  53. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +2 -0
  54. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
  55. package/modules/@apostrophecms/uploadfs/index.js +3 -0
  56. package/modules/@apostrophecms/util/index.js +20 -3
  57. package/package.json +14 -10
  58. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
  59. package/test/add-missing-schema-fields-project/test.js +22 -3
  60. package/test/assets.js +110 -67
  61. package/test/db-tools.js +365 -0
  62. package/test/db.js +24 -15
  63. package/test/default-adapter.js +256 -0
  64. package/test/external-front.js +419 -1
  65. package/test/files.js +28 -0
  66. package/test/job.js +1 -1
  67. package/test/modules/jsx-area-test/index.js +23 -0
  68. package/test/modules/jsx-area-test/views/bad-area.jsx +7 -0
  69. package/test/modules/jsx-area-test/views/with-area-ctx.jsx +13 -0
  70. package/test/modules/jsx-area-test/views/with-area.jsx +7 -0
  71. package/test/modules/jsx-area-test/views/with-widget-ctx.jsx +12 -0
  72. package/test/modules/jsx-area-test/views/with-widget.jsx +7 -0
  73. package/test/modules/jsx-async-widget/index.js +6 -0
  74. package/test/modules/jsx-async-widget/views/widget.jsx +11 -0
  75. package/test/modules/jsx-bridge-test/index.js +1 -0
  76. package/test/modules/jsx-bridge-test/views/cross-module.jsx +7 -0
  77. package/test/modules/jsx-bridge-test/views/disambig-name-only.jsx +7 -0
  78. package/test/modules/jsx-bridge-test/views/disambig-target.jsx +8 -0
  79. package/test/modules/jsx-bridge-test/views/disambig-with-template-name.jsx +7 -0
  80. package/test/modules/jsx-bridge-test/views/include-html.jsx +7 -0
  81. package/test/modules/jsx-bridge-test/views/include-target.html +4 -0
  82. package/test/modules/jsx-bridge-test/views/jsx-extends-via-extend.jsx +9 -0
  83. package/test/modules/jsx-bridge-test/views/jsx-extends.jsx +9 -0
  84. package/test/modules/jsx-bridge-test/views/jsx-layout.jsx +14 -0
  85. package/test/modules/jsx-bridge-test/views/njk-extends.jsx +14 -0
  86. package/test/modules/jsx-bridge-test/views/njk-layout.html +9 -0
  87. package/test/modules/jsx-bridge-test/views/short-form.jsx +7 -0
  88. package/test/modules/jsx-bridge-test/views/short-target.jsx +3 -0
  89. package/test/modules/jsx-component-test/index.js +15 -0
  90. package/test/modules/jsx-component-test/views/greet.html +1 -0
  91. package/test/modules/jsx-component-test/views/uses-component.jsx +8 -0
  92. package/test/modules/jsx-ctx-widget/index.js +6 -0
  93. package/test/modules/jsx-ctx-widget/views/widget.jsx +4 -0
  94. package/test/modules/jsx-mixed-test/index.js +9 -0
  95. package/test/modules/jsx-mixed-test/views/apos-full.jsx +21 -0
  96. package/test/modules/jsx-mixed-test/views/async-list.jsx +12 -0
  97. package/test/modules/jsx-mixed-test/views/lib/format.js +3 -0
  98. package/test/modules/jsx-mixed-test/views/localized.jsx +3 -0
  99. package/test/modules/jsx-mixed-test/views/partial.jsx +3 -0
  100. package/test/modules/jsx-mixed-test/views/safe-helper.jsx +3 -0
  101. package/test/modules/jsx-mixed-test/views/syntax-error.jsx +3 -0
  102. package/test/modules/jsx-mixed-test/views/throws.jsx +5 -0
  103. package/test/modules/jsx-mixed-test/views/uses-import.jsx +5 -0
  104. package/test/modules/jsx-mixed-test/views/uses-require.jsx +5 -0
  105. package/test/modules/jsx-watcher-cross-test/index.js +5 -0
  106. package/test/modules/jsx-watcher-cross-test/views/cross-template.jsx +3 -0
  107. package/test/modules/jsx-watcher-test/index.js +5 -0
  108. package/test/modules/jsx-watcher-test/views/watcher-test.jsx +3 -0
  109. package/test/modules/template-jsx-options-test/index.js +12 -0
  110. package/test/modules/template-jsx-options-test/views/options-test.jsx +9 -0
  111. package/test/modules/template-jsx-subclass-test/index.js +3 -0
  112. package/test/modules/template-jsx-subclass-test/views/override-test.jsx +3 -0
  113. package/test/modules/template-jsx-test/index.js +9 -0
  114. package/test/modules/template-jsx-test/views/boolean-attrs.jsx +11 -0
  115. package/test/modules/template-jsx-test/views/class-and-for.jsx +7 -0
  116. package/test/modules/template-jsx-test/views/dangerously-set.jsx +3 -0
  117. package/test/modules/template-jsx-test/views/escape-attr.jsx +3 -0
  118. package/test/modules/template-jsx-test/views/escape-body.jsx +3 -0
  119. package/test/modules/template-jsx-test/views/inherit-test.jsx +3 -0
  120. package/test/modules/template-jsx-test/views/list.jsx +7 -0
  121. package/test/modules/template-jsx-test/views/override-test.jsx +3 -0
  122. package/test/modules/template-jsx-test/views/svg-attrs.jsx +27 -0
  123. package/test/modules/template-jsx-test/views/test.jsx +3 -0
  124. package/test/modules/template-jsx-test/views/void-elements.jsx +9 -0
  125. package/test/templates-jsx-watcher.js +135 -0
  126. package/test/templates-jsx.js +537 -0
  127. package/test/utils.js +103 -0
  128. package/test-lib/util.js +50 -14
  129. package/lib/mongodb-connect.js +0 -62
package/test-lib/util.js CHANGED
@@ -1,5 +1,27 @@
1
1
  const { createId } = require('@paralleldrive/cuid2');
2
- const mongodbConnect = require('../lib/mongodb-connect');
2
+
3
+ const testDbProtocol = process.env.APOS_TEST_DB_PROTOCOL || 'mongodb';
4
+
5
+ // Build a test database URI for postgres based on the shortName.
6
+ // Returns undefined for mongodb, letting the default logic handle it.
7
+ function getTestDbUri(shortName) {
8
+ if (testDbProtocol === 'postgres') {
9
+ // PostgreSQL database names cannot contain hyphens
10
+ const dbName = shortName.replace(/-/g, '_');
11
+ return `postgres://localhost:5432/${dbName}`;
12
+ }
13
+ if (testDbProtocol === 'multipostgres') {
14
+ // Multi-schema mode: shared real database, per-test schema
15
+ const schemaName = shortName.replace(/-/g, '_').replace(/[^a-zA-Z0-9_]/g, '');
16
+ return `multipostgres://localhost:5432/apos_test-${schemaName}`;
17
+ }
18
+ if (testDbProtocol === 'sqlite') {
19
+ const os = require('os');
20
+ const path = require('path');
21
+ const dbName = shortName.replace(/-/g, '_').replace(/[^a-zA-Z0-9_]/g, '');
22
+ return `sqlite://${path.join(os.tmpdir(), `apos_test_${dbName}.db`)}`;
23
+ }
24
+ }
3
25
 
4
26
  // Properly clean up an apostrophe instance and drop its
5
27
  // database collections to create a sane environment for the next test.
@@ -10,23 +32,23 @@ const mongodbConnect = require('../lib/mongodb-connect');
10
32
  // If `apos` is null, no work is done.
11
33
 
12
34
  async function destroy(apos) {
13
- if (!apos) {
35
+ if (!apos || apos._destroyed) {
14
36
  return;
15
37
  }
38
+ apos._destroyed = true;
39
+ const dbModule = apos.modules['@apostrophecms/db'];
40
+ const { uri } = dbModule;
41
+ const dbName = apos.db && (apos.db.databaseName || apos.db._name);
16
42
  await apos.destroy();
17
- const { uri } = apos.modules['@apostrophecms/db'];
18
- const dbName = apos.db && apos.db.databaseName;
19
- // TODO at some point accommodate nonsense like testing remote databases
20
- // that won't let us use dropDatabase, no shell available etc., but the
21
- // important principle here is that we should not have to have an apos
22
- // object to clean up the database, otherwise we have to get hold of one
23
- // when initialization failed and that's really not apostrophe's concern
24
- if (dbName && uri) {
25
- const client = await mongodbConnect(`${uri}${dbName}`);
26
- const db = client.db(dbName);
27
- await db.dropDatabase();
28
- await client.close();
43
+ if (!uri || !dbName) {
44
+ return;
29
45
  }
46
+ // Make a fresh connection (the original was closed by destroy)
47
+ // and use it to drop the test database
48
+ const client = await dbModule.connectToAdapter(uri);
49
+ const db = client.db(dbName);
50
+ await db.dropDatabase();
51
+ await client.close();
30
52
  };
31
53
 
32
54
  async function create(options = {}) {
@@ -55,6 +77,18 @@ async function create(options = {}) {
55
77
  express.options.session.secret = express.options.session.secret || 'test';
56
78
  config.modules['@apostrophecms/express'] = express;
57
79
  }
80
+ // When APOS_TEST_DB_PROTOCOL=postgres, automatically configure the db
81
+ // module to use a postgres URI unless already explicitly configured
82
+ const testUri = getTestDbUri(config.shortName);
83
+ if (testUri) {
84
+ config.modules = config.modules || {};
85
+ const dbModule = config.modules['@apostrophecms/db'] || {};
86
+ dbModule.options = dbModule.options || {};
87
+ if (!dbModule.options.uri && !dbModule.options.client) {
88
+ dbModule.options.uri = testUri;
89
+ }
90
+ config.modules['@apostrophecms/db'] = dbModule;
91
+ }
58
92
  return require('../index.js')(config);
59
93
  }
60
94
 
@@ -151,5 +185,7 @@ module.exports = {
151
185
  loginAs,
152
186
  logout,
153
187
  getUserJar,
188
+ getTestDbUri,
189
+ testDbProtocol,
154
190
  timeout: (process.env.TEST_TIMEOUT && parseInt(process.env.TEST_TIMEOUT)) || 20000
155
191
  };
@@ -1,62 +0,0 @@
1
- const mongo = require('@apostrophecms/emulate-mongo-3-driver');
2
- const dns = require('dns');
3
-
4
- // Connect to MongoDB, using the modern topology and parser, and
5
- // a tolerant policy to successfully connect to "localhost" even if
6
- // the first record returned by the resolver doesn't reach mongodb's
7
- // bind address because localhost resolves first to ::1 (ipv6) and
8
- // mongodb lists only on 127.0.0.1 (ipv4) by default. For broadest
9
- // compatibility we don't assume we know this will happen, we try all the
10
- // addresses that localhost actually resolves to and succeed with the
11
- // first one that works.
12
-
13
- module.exports = async (uri, options) => {
14
- const connectOptions = {
15
- useUnifiedTopology: true,
16
- useNewUrlParser: true,
17
- ...options
18
- };
19
- let parsed;
20
- try {
21
- parsed = new URL(uri);
22
- } catch (e) {
23
- // Parse failed, e.g. old school replica set URI
24
- // with commas, just let the mongo driver handle it
25
- return mongo.MongoClient.connect(uri, connectOptions);
26
- }
27
- if (!parsed || (parsed.protocol !== 'mongodb:') || (parsed.hostname !== 'localhost')) {
28
- return mongo.MongoClient.connect(parsed.toString(), connectOptions);
29
- }
30
- const records = await dns.promises.lookup('localhost', { all: true });
31
- if (!records.length) {
32
- // The computer that reaches this point has bigger problems 😅
33
- throw new Error('Unable to resolve localhost to an IP address.');
34
- }
35
- return new Promise((resolve, reject) => {
36
- let failed = 0;
37
- let succeeded = false;
38
- records.forEach(attempt);
39
- async function attempt(record) {
40
- try {
41
- const parsed = new URL(uri);
42
- parsed.hostname = record.address;
43
- const result = await mongo.MongoClient.connect(parsed.toString(), connectOptions);
44
- if (!succeeded) {
45
- succeeded = true;
46
- resolve(result);
47
- } else {
48
- // We succeeded in reaching localhost at both ip4 and ip6,
49
- // but we only need one of them to succeed
50
- await result.close();
51
- }
52
- } catch (e) {
53
- failed++;
54
- if (failed === records.length) {
55
- // None succeeded, so reject with the last error
56
- // (which one we reject with doesn't really matter)
57
- reject(e);
58
- }
59
- }
60
- }
61
- });
62
- };