apostrophe 4.8.1 → 4.9.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 (126) hide show
  1. package/.eslintrc +7 -6
  2. package/.github/workflows/main.yml +5 -5
  3. package/CHANGELOG.md +54 -2
  4. package/index.js +252 -102
  5. package/lib/moog-require.js +19 -15
  6. package/lib/moog.js +10 -9
  7. package/modules/@apostrophecms/admin-bar/index.js +3 -0
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +2 -6
  9. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +5 -5
  10. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +58 -14
  11. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +201 -21
  12. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +4 -25
  13. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextUndoRedo.vue +5 -12
  14. package/modules/@apostrophecms/area/ui/apos/components/AposAreaExpandedMenu.vue +8 -0
  15. package/modules/@apostrophecms/area/ui/apos/components/AposAreaMenu.vue +9 -2
  16. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +1 -1
  17. package/modules/@apostrophecms/asset/index.js +569 -878
  18. package/modules/@apostrophecms/asset/lib/build/external-module-api.js +745 -0
  19. package/modules/@apostrophecms/asset/lib/build/internals.js +553 -0
  20. package/modules/@apostrophecms/asset/lib/build/manager-apos.js +75 -0
  21. package/modules/@apostrophecms/asset/lib/build/manager-bundled.js +31 -0
  22. package/modules/@apostrophecms/asset/lib/build/manager-custom.js +62 -0
  23. package/modules/@apostrophecms/asset/lib/build/manager-index.js +72 -0
  24. package/modules/@apostrophecms/asset/lib/build/managers.js +20 -0
  25. package/modules/@apostrophecms/asset/lib/build/task.js +837 -0
  26. package/modules/@apostrophecms/asset/lib/build/utils.js +199 -0
  27. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.scss.js +21 -10
  28. package/modules/@apostrophecms/asset/lib/webpack/postcss-replace-viewport-units-plugin.js +40 -0
  29. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +23 -11
  30. package/modules/@apostrophecms/asset/lib/webpack/utils.js +13 -5
  31. package/modules/@apostrophecms/command-menu/ui/apos/components/AposCommandMenuShortcut.vue +1 -0
  32. package/modules/@apostrophecms/doc/index.js +11 -7
  33. package/modules/@apostrophecms/doc-type/index.js +41 -3
  34. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +20 -1
  35. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +1 -0
  36. package/modules/@apostrophecms/express/index.js +6 -4
  37. package/modules/@apostrophecms/i18n/i18n/de.json +119 -3
  38. package/modules/@apostrophecms/i18n/i18n/en.json +3 -1
  39. package/modules/@apostrophecms/i18n/i18n/es.json +157 -1
  40. package/modules/@apostrophecms/i18n/i18n/fr.json +143 -1
  41. package/modules/@apostrophecms/i18n/i18n/it.json +77 -4
  42. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +160 -1
  43. package/modules/@apostrophecms/i18n/i18n/sk.json +139 -1
  44. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +2 -0
  45. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +1 -1
  46. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +4 -1
  47. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerDisplay.vue +8 -1
  48. package/modules/@apostrophecms/image/ui/apos/components/AposMediaUploader.vue +6 -1
  49. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +65 -29
  50. package/modules/@apostrophecms/modal/ui/apos/components/AposModalBody.vue +0 -1
  51. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +2 -0
  52. package/modules/@apostrophecms/modal/ui/apos/components/AposModalShareDraft.vue +1 -0
  53. package/modules/@apostrophecms/modal/ui/apos/composables/AposFocus.js +7 -2
  54. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +2 -0
  55. package/modules/@apostrophecms/permission/index.js +10 -10
  56. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -0
  57. package/modules/@apostrophecms/piece-type/ui/apos/components/AposRelationshipEditor.vue +1 -0
  58. package/modules/@apostrophecms/schema/index.js +173 -36
  59. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +36 -13
  60. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  61. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +13 -11
  62. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +9 -0
  63. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +45 -5
  64. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +41 -4
  65. package/modules/@apostrophecms/schema/ui/apos/logic/AposSearchList.js +25 -6
  66. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputChoicesMixin.js +47 -20
  67. package/modules/@apostrophecms/template/index.js +157 -11
  68. package/modules/@apostrophecms/template/lib/bundlesLoader.js +45 -8
  69. package/modules/@apostrophecms/template/views/outerLayoutBase.html +4 -0
  70. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +21 -1
  71. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +8 -1
  72. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +11 -1
  73. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
  74. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +32 -4
  75. package/modules/@apostrophecms/ui/ui/apos/components/AposLoading.vue +1 -1
  76. package/modules/@apostrophecms/ui/ui/apos/components/AposSpinner.vue +1 -1
  77. package/modules/@apostrophecms/ui/ui/apos/package.json +4 -0
  78. package/modules/@apostrophecms/ui/ui/apos/scss/global/_breakpoint_preview.scss +5 -2
  79. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_responsive.scss +2 -2
  80. package/modules/@apostrophecms/ui/ui/apos/scss/mixins/_theme_mixins.scss +10 -12
  81. package/modules/@apostrophecms/ui/ui/apos/utils/index.js +2 -2
  82. package/modules/@apostrophecms/util/index.js +10 -3
  83. package/modules/@apostrophecms/widget-type/index.js +5 -0
  84. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
  85. package/package.json +12 -11
  86. package/test/asset-external.js +263 -0
  87. package/test/assets.js +22 -17
  88. package/test/common-js.js +42 -0
  89. package/test/docs.js +32 -0
  90. package/test/esm-project/app.js +16 -0
  91. package/test/esm-project/esm.js +50 -0
  92. package/test/esm-project/package.json +16 -0
  93. package/test/extra_node_modules/before-global/index.js +4 -0
  94. package/test/log.js +6 -9
  95. package/test/modules/@apostrophecms/home-page/ui/src/main.js +6 -0
  96. package/test/modules/@apostrophecms/home-page/ui/src/topic.js +6 -0
  97. package/test/modules/article-page/index.js +4 -0
  98. package/test/modules/article-page/ui/src/index.js +6 -0
  99. package/test/modules/article-page/ui/src/main.js +6 -0
  100. package/test/modules/article-page/ui/src/main.scss +3 -0
  101. package/test/modules/article-widget/index.js +4 -0
  102. package/test/modules/article-widget/ui/src/carousel.js +6 -0
  103. package/test/modules/article-widget/ui/src/carousel.scss +4 -0
  104. package/test/modules/article-widget/ui/src/topic.js +6 -0
  105. package/test/modules/inject-test/index.js +81 -0
  106. package/test/modules/inject-test/views/appendDevTest.html +1 -0
  107. package/test/modules/inject-test/views/appendDevViteTest.html +1 -0
  108. package/test/modules/inject-test/views/appendDevWebpackTest.html +1 -0
  109. package/test/modules/inject-test/views/appendProdWebpackTest.html +1 -0
  110. package/test/modules/inject-test/views/prependDevTest.html +1 -0
  111. package/test/modules/inject-test/views/prependDevViteTest.html +1 -0
  112. package/test/modules/inject-test/views/prependDevWebpackTest.html +1 -0
  113. package/test/modules/inject-test/views/prependProdTest.html +1 -0
  114. package/test/modules/inject-test/views/prependViteTest.html +1 -0
  115. package/test/modules/inject-test/views/prependWebpackTest.html +1 -0
  116. package/test/modules/selected-article-widget/index.js +4 -0
  117. package/test/modules/selected-article-widget/ui/src/tabs.js +6 -0
  118. package/test/modules/test-before/index.js +4 -0
  119. package/test/modules/with-layout-page/views/page.html +6 -0
  120. package/test/modules-order.js +167 -0
  121. package/test/moog.js +45 -45
  122. package/test/postcss.js +64 -0
  123. package/test/relationships.js +224 -0
  124. package/test/templates.js +46 -2
  125. package/test/utils.js +11 -2
  126. package/test-lib/test.js +49 -45
package/.eslintrc CHANGED
@@ -10,6 +10,7 @@
10
10
  "apos": true
11
11
  },
12
12
  "rules": {
13
+ "max-len": "off",
13
14
  "no-var": "error",
14
15
  "no-console": 0,
15
16
  "vue/no-deprecated-v-on-native-modifier": 0,
@@ -56,12 +57,12 @@
56
57
  {
57
58
  "files": "*.vue",
58
59
  "globals": {
59
- "defineProps": "readable",
60
- "defineEmits": "readable",
61
- "defineExpose": "readable",
62
- "defineOptions": "readable",
63
- "defineModel": "readable",
64
- "defineSlots": "readable"
60
+ "defineProps": "readonly",
61
+ "defineEmits": "readonly",
62
+ "defineExpose": "readonly",
63
+ "defineOptions": "readonly",
64
+ "defineModel": "readonly",
65
+ "defineSlots": "readonly"
65
66
  }
66
67
  },
67
68
  {
@@ -18,21 +18,21 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  strategy:
20
20
  matrix:
21
- node-version: [18, 20]
22
- mongodb-version: [4.4, 5.0, 6.0, 7.0]
21
+ node-version: [18, 20, 22]
22
+ mongodb-version: [6.0, 7.0, 8.0]
23
23
 
24
24
  # Steps represent a sequence of tasks that will be executed as part of the job
25
25
  steps:
26
26
  - name: Git checkout
27
- uses: actions/checkout@v2
27
+ uses: actions/checkout@v4
28
28
 
29
29
  - name: Use Node.js ${{ matrix.node-version }}
30
- uses: actions/setup-node@v1
30
+ uses: actions/setup-node@v4
31
31
  with:
32
32
  node-version: ${{ matrix.node-version }}
33
33
 
34
34
  - name: Start MongoDB
35
- uses: supercharge/mongodb-github-action@1.3.0
35
+ uses: supercharge/mongodb-github-action@1.11.0
36
36
  with:
37
37
  mongodb-version: ${{ matrix.mongodb-version }}
38
38
 
package/CHANGELOG.md CHANGED
@@ -1,8 +1,60 @@
1
1
  # Changelog
2
2
 
3
- ## 4.8.1 (2024-10-09)
3
+ ## 4.9.0 (2024-10-31)
4
+
5
+ ### Adds
6
+
7
+ * Relationship inputs have aria accessibility tags and autocomplete suggestions can be controlled by keyboard.
8
+ * Elements inside modals can have a `data-apos-focus-priority` attribute that prioritizes them inside the focusable elements list.
9
+ * Modals will continute trying to find focusable elements until an element marked `data-apos-focus-priority` appears or the max retry threshold is reached.
10
+ * Takes care of an edge case where Media Manager would duplicate search results.
11
+ * Add support for ESM projects.
12
+ * Modules can now have a `before: "module-name"` property in their configuration to initialize them before another module, bypassing the normal
13
+ order implied by `defaults.js` and `app.js`.
14
+ * `select` and `checkboxes` fields that implement dynamic choices can now take into account the value of other fields on the fly, by specifying
15
+ a `following` property with an array of other field names. Array and object subfields can access properties of the parent document
16
+ by adding a `<` prefix (or more than one) to field names in `following` to look upwards a level. Your custom method on the server side will
17
+ now receive a `following` object as an additional argument. One limitation: for now, a field with dynamic choices cannot depend on another field
18
+ with dynamic choices in this way.
19
+ * Adds AI-generated missing translations
20
+ * Adds the mobile preview dropdown for non visibles breakpoints. Uses the new `shortcut` property to display breakpoints out of the dropdown.
21
+ * Adds possibility to have two icons in a button.
22
+ * Breakpoint preview only targets `[data-apos-refreshable]`.
23
+ * Adds a `isActive` state to context menu items. Also adds possibility to add icons to context menu items.
24
+ * Add a postcss plugin to handle `vh` and `vw` values on breakpoint preview mode.
25
+ * Adds inject component `when` condition with possible values `hmr`, `prod`, and `dev`. Modules should explicitely register their components with the same `when` value and the condition should be met to inject the component.
26
+ * Adds inject `bundler` registration condition. It's in use only when registering a component and will be evaluated on runtime. The value should match the current build module (`webpack` or the external build module alias).
27
+ * Adds new development task `@apostrophecms/asset:reset` to reset the asset build cache and all build artifacts.
28
+ * Revamps the `@apostrophecms/asset` module to enable bundling via build modules.
29
+ * Adds `apos.asset.devServerUrl()` nunjucks helper to get the (bundle) dev server URL when available.
30
+ * The asset module has a new option, `options.hmr` that accepts `public` (default), `apos` or `false` to enable HMR for the public bundle or the admin UI bundle or disable it respectively. This configuration works only with external build modules that support HMR.
31
+ * The asset module has a new option, `options.hmrPort` that accepts an integer (default `null`) to specify the HMR WS port. If not specified, the default express port is used. This configuration works only with external build modules that support HMR WS.
32
+ * The asset module has a new option, `options.productionSourceMaps` that accepts a boolean (default `false`) to enable source maps in production. This configuration works only with external build modules that support source maps.
4
33
 
5
- * Correct a race condition that can cause a crash at startup when custom `uploadfs` options are present in some environments.
34
+ ### Changes
35
+
36
+ * Silence deprecation warnings from Sass 1.80+ regarding the use of `@import`. The Sass team [has stated there will be a two-year transition period](https://sass-lang.com/documentation/breaking-changes/import/#transition-period) before the feature is actually removed. The use of `@import` is common practice in the Apostrophe codebase and in many project codebases. We will arrange for an orderly migration to the new `@use` directive before Sass 3.x appears.
37
+ * Move saving indicator after breakpoint preview.
38
+ * Internal methods `mergeConfiguration`, `autodetectBundles`, `lintModules`, `nestedModuleSubdirs` and `testDir` are now async.
39
+ * `express.getSessionOptions` is now async.
40
+
41
+ ### Fixes
42
+
43
+ * Modifies the `AposAreaMenu.vue` component to set the `disabled` attribute to `true` if the max number of widgets have been added in an area with `expanded: true`.
44
+ * `pnpm: true` option in `app.js` is no longer breaking the application.
45
+ * Remove unused `vue-template-compiler` dependency.
46
+ * Prevent un-publishing the `@apostrophecms/global` doc and more generally all singletons.
47
+ * When opening a context menu while another is already opened, prevent from focusing the button of the first one instead of the newly opened menu.
48
+ * Updates `isEqual` method of `area` field type to avoid comparing an area having temporary properties with one having none.
49
+ * In a relationship field, when asking for sub relationships using `withRelationships` an dot notion.
50
+ If this is done in combination with a projection, this projection is updated to add the id storage fields of the needed relationships for the whole `withRelationships` path.
51
+ * The admin UI no longer fails to function when the HTML page is rendered with a direct `sendPage` call and there is no current "in context" page or piece.
52
+
53
+ ## 4.7.2 and 4.8.1 (2024-10-09)
54
+
55
+ ### Fixes
56
+
57
+ * Correct a race condition that can cause a crash at startup when custom `uploadfs` options are present in some specific cloud environments e.g. when using Azure Blob Storage.
6
58
 
7
59
  ## 4.8.0 (2024-10-03)
8
60
 
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // this should be loaded first
2
2
  const opentelemetry = require('./lib/opentelemetry');
3
3
  const path = require('path');
4
+ const url = require('url');
4
5
  const _ = require('lodash');
5
6
  const argv = require('boring')({ end: true });
6
7
  const fs = require('fs');
@@ -10,6 +11,7 @@ const { cpus } = require('os');
10
11
  const process = require('process');
11
12
  const npmResolve = require('resolve');
12
13
  const glob = require('./lib/glob.js');
14
+ const moogRequire = require('./lib/moog-require');
13
15
  let defaults = require('./defaults.js');
14
16
 
15
17
  // ## Top-level options
@@ -212,8 +214,8 @@ async function apostrophe(options, telemetry, rootSpan) {
212
214
  try {
213
215
  const matches = process.version.match(/^v(\d+)/);
214
216
  const version = parseInt(matches[1]);
215
- if (version < 12) {
216
- throw new Error('Apostrophe 3.x requires at least Node.js 12.x.');
217
+ if (version < 18) {
218
+ throw new Error('Apostrophe requires at least Node.js 18.x.');
217
219
  }
218
220
  // The core must have a reference to itself in order to use the
219
221
  // promise event emitter code
@@ -225,10 +227,27 @@ async function apostrophe(options, telemetry, rootSpan) {
225
227
  Object.assign(self, require('./modules/@apostrophecms/module/lib/events.js')(self));
226
228
 
227
229
  // Determine root module and root directory
228
- self.root = options.root || getRoot();
229
- self.rootDir = options.rootDir || path.dirname(self.root.filename);
230
- self.npmRootDir = options.npmRootDir || self.rootDir;
231
- self.selfDir = __dirname;
230
+
231
+ const {
232
+ root,
233
+ rootDir,
234
+ npmRootDir,
235
+ selfDir
236
+ } = buildRoot(options);
237
+ self.root = root;
238
+ self.rootDir = rootDir;
239
+ self.npmRootDir = npmRootDir;
240
+ self.selfDir = selfDir;
241
+ self.getNpmPath = (name) => {
242
+ try {
243
+ return getNpmPath(name, self.npmRootDir);
244
+ } catch (e) {
245
+ // Not found via npm. This does not mean it doesn't
246
+ // exist as a project-level thing
247
+ return null;
248
+ }
249
+ };
250
+
232
251
  // Signals to various (build related) places that we are running a pnpm installation.
233
252
  // The relevant option, if set, has a higher precedence over the automated check.
234
253
  self.isPnpm = options.pnpm ??
@@ -236,8 +255,8 @@ async function apostrophe(options, telemetry, rootSpan) {
236
255
 
237
256
  testModule();
238
257
 
239
- self.options = mergeConfiguration(options, defaults);
240
- autodetectBundles();
258
+ self.options = await mergeConfiguration(options, defaults);
259
+ await autodetectBundles();
241
260
  acceptGlobalOptions();
242
261
 
243
262
  // Module-based async events (self.on and self.emit of each module,
@@ -271,8 +290,8 @@ async function apostrophe(options, telemetry, rootSpan) {
271
290
  // your own piece types
272
291
 
273
292
  self.instancesOf = function(name) {
274
- return _.filter(self.modules, function(module) {
275
- return self.synth.instanceOf(module, name);
293
+ return _.filter(self.modules, function(apostropheModule) {
294
+ return self.synth.instanceOf(apostropheModule, name);
276
295
  });
277
296
  };
278
297
 
@@ -292,10 +311,10 @@ async function apostrophe(options, telemetry, rootSpan) {
292
311
  self.aliasEvent('modulesReady', 'modulesRegistered');
293
312
  self.aliasEvent('afterInit', 'ready');
294
313
 
295
- defineModules();
314
+ await defineModules();
296
315
 
297
316
  await instantiateModules();
298
- lintModules();
317
+ await lintModules();
299
318
  await self.emit('modulesRegistered'); // formerly modulesReady
300
319
  self.apos.schema.validateAllSchemas();
301
320
  self.apos.schema.registerAllSchemas();
@@ -303,9 +322,9 @@ async function apostrophe(options, telemetry, rootSpan) {
303
322
  await self.apos.migration.migrate(self.argv);
304
323
  // Inserts the global doc in the default locale if it does not exist; same for other
305
324
  // singleton piece types registered by other modules
306
- for (const module of Object.values(self.modules)) {
307
- if (self.instanceOf(module, '@apostrophecms/piece-type') && module.options.singletonAuto) {
308
- await module.insertIfMissing();
325
+ for (const apostropheModule of Object.values(self.modules)) {
326
+ if (self.instanceOf(apostropheModule, '@apostrophecms/piece-type') && apostropheModule.options.singletonAuto) {
327
+ await apostropheModule.insertIfMissing();
309
328
  }
310
329
  }
311
330
  await self.apos.page.implementParkAllInDefaultLocale();
@@ -337,14 +356,14 @@ async function apostrophe(options, telemetry, rootSpan) {
337
356
  // SUPPORTING FUNCTIONS BEGIN HERE
338
357
 
339
358
  // Merge configuration from defaults, data/local.js and app.js
340
- function mergeConfiguration(options, defaults) {
359
+ async function mergeConfiguration(options, defaults) {
341
360
  let config = {};
342
361
  let local = {};
343
362
  const localPath = options.__localPath || '/data/local.js';
344
363
  const reallyLocalPath = self.rootDir + localPath;
345
364
 
346
365
  if (fs.existsSync(reallyLocalPath)) {
347
- local = require(reallyLocalPath);
366
+ local = await self.root.import(reallyLocalPath);
348
367
  }
349
368
 
350
369
  // Otherwise making a second apos instance
@@ -369,29 +388,14 @@ async function apostrophe(options, telemetry, rootSpan) {
369
388
  return config;
370
389
  }
371
390
 
372
- function getRoot() {
373
- let _module = module;
374
- let m = _module;
375
- while (m.parent && m.parent.filename) {
376
- // The test file is the root as far as we are concerned,
377
- // not mocha itself
378
- if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
379
- return m;
380
- }
381
- m = m.parent;
382
- _module = m;
383
- }
384
- return _module;
385
- }
386
-
387
- function nestedModuleSubdirs() {
391
+ async function nestedModuleSubdirs() {
388
392
  if (!options.nestedModuleSubdirs) {
389
393
  return;
390
394
  }
391
395
  const configs = glob(self.localModules + '/**/modules.js', { follow: true });
392
- _.each(configs, function(config) {
396
+ for (const config of configs) {
393
397
  try {
394
- _.merge(self.options.modules, require(config));
398
+ _.merge(self.options.modules, await self.root.import(config));
395
399
  } catch (e) {
396
400
  console.error(stripIndent`
397
401
  When nestedModuleSubdirs is active, any modules.js file beneath:
@@ -408,39 +412,37 @@ async function apostrophe(options, telemetry, rootSpan) {
408
412
  `);
409
413
  throw e;
410
414
  }
411
- });
415
+ }
412
416
  }
413
417
 
414
- function autodetectBundles() {
415
- const modules = _.keys(self.options.modules);
416
- _.each(modules, function(name) {
417
- const path = getNpmPath(name);
418
- if (!path) {
419
- return;
418
+ async function autodetectBundles() {
419
+ const apostropheModules = Object.keys(self.options.modules);
420
+ for (const apostropheModuleName of apostropheModules) {
421
+ const npmPath = self.getNpmPath(apostropheModuleName);
422
+ if (!npmPath) {
423
+ continue;
420
424
  }
421
- const module = require(path);
422
- if (module.bundle) {
423
- self.options.bundles = (self.options.bundles || []).concat(name);
424
- _.each(module.bundle.modules, function(name) {
425
- if (!_.has(self.options.modules, name)) {
426
- const bundledModule = require(require('path').dirname(path) + '/' + module.bundle.directory + '/' + name);
425
+
426
+ const apostropheModule = await self.root.import(npmPath);
427
+ if (apostropheModule.bundle) {
428
+ self.options.bundles = (self.options.bundles || []).concat(apostropheModuleName);
429
+ const bundleModules = apostropheModule.bundle.modules;
430
+ for (const bundleModuleName of bundleModules) {
431
+ if (!apostropheModules.includes(bundleModuleName)) {
432
+ const bundledModule = await self.root.import(
433
+ path.resolve(
434
+ path.dirname(npmPath),
435
+ apostropheModule.bundle.directory,
436
+ bundleModuleName,
437
+ 'index.js'
438
+ )
439
+ );
427
440
  if (bundledModule.improve) {
428
- self.options.modules[name] = {};
441
+ self.options.modules[bundleModuleName] = {};
429
442
  }
430
443
  }
431
- });
444
+ }
432
445
  }
433
- });
434
- }
435
-
436
- function getNpmPath(name) {
437
- const parentPath = path.resolve(self.npmRootDir);
438
- try {
439
- return npmResolve.sync(name, { basedir: parentPath });
440
- } catch (e) {
441
- // Not found via npm. This does not mean it doesn't
442
- // exist as a project-level thing
443
- return null;
444
446
  }
445
447
  }
446
448
 
@@ -499,9 +501,10 @@ async function apostrophe(options, telemetry, rootSpan) {
499
501
  port: 7900,
500
502
  secret: 'irrelevant'
501
503
  });
502
- const m = findTestModule();
504
+ const m = self.root;
505
+ checkTestModule();
503
506
  // Allow tests to be in test/ or in tests/
504
- const testDir = require('path').dirname(m.filename);
507
+ const testDir = path.dirname(m.filename);
505
508
  const moduleDir = testDir.replace(/\/tests?$/, '');
506
509
  if (testDir === moduleDir) {
507
510
  throw new Error('Test file must be in test/ or tests/ subdirectory of module');
@@ -518,33 +521,21 @@ async function apostrophe(options, telemetry, rootSpan) {
518
521
  fs.mkdirSync(testDir + '/node_modules' + pkgNamespace, { recursive: true });
519
522
  fs.symlinkSync(moduleDir, testDir + '/node_modules/' + pkgName, 'dir');
520
523
  }
521
-
522
- // Not quite superfluous: it'll return self.root, but
523
- // it also makes sure we encounter mocha along the way
524
+ // Makes sure we encounter mocha along the way
524
525
  // and throws an exception if we don't
525
- function findTestModule() {
526
- let m = module;
526
+ function checkTestModule() {
527
527
  const testFor = `node_modules${path.sep}mocha`;
528
528
  if (!require.main.filename.includes(testFor)) {
529
529
  throw new Error('mocha does not seem to be running, is this really a test?');
530
530
  }
531
- while (m) {
532
- if (m.parent && m.parent.filename.includes(testFor)) {
533
- return m;
534
- } else if (!m.parent) {
535
- // Mocha v10 doesn't inject mocha paths inside `module`, therefore, we only detect the parent until the last parent. But we can get Mocha running using `require.main` - Amin
536
- return m;
537
- }
538
- m = m.parent;
539
- }
540
531
  }
541
532
  }
542
533
 
543
- function defineModules() {
534
+ async function defineModules() {
544
535
  // Set moog-require up to create our module manager objects
545
536
 
546
537
  self.localModules = self.options.modulesSubdir || self.options.__testLocalModules || (self.rootDir + '/modules');
547
- const synth = require('./lib/moog-require')({
538
+ const synth = await moogRequire({
548
539
  root: self.root,
549
540
  bundles: [ 'apostrophe' ].concat(self.options.bundles || []),
550
541
  localModules: self.localModules,
@@ -557,7 +548,9 @@ async function apostrophe(options, telemetry, rootSpan) {
557
548
  'icons',
558
549
  'i18n',
559
550
  'webpack',
560
- 'commands'
551
+ 'build',
552
+ 'commands',
553
+ 'before'
561
554
  ]
562
555
  });
563
556
 
@@ -569,11 +562,11 @@ async function apostrophe(options, telemetry, rootSpan) {
569
562
  self.redefine = self.synth.redefine;
570
563
  self.create = self.synth.create;
571
564
 
572
- nestedModuleSubdirs();
565
+ await nestedModuleSubdirs();
573
566
 
574
- _.each(self.options.modules, function(options, name) {
575
- synth.define(name, options);
576
- });
567
+ for (const [ name, options ] of Object.entries(self.options.modules)) {
568
+ await synth.define(name, options);
569
+ }
577
570
 
578
571
  // Apostrophe prefers that any improvements to @apostrophecms/global
579
572
  // be applied before any project level version of @apostrophecms/global
@@ -582,12 +575,95 @@ async function apostrophe(options, telemetry, rootSpan) {
582
575
  return synth;
583
576
  }
584
577
 
578
+ // Reorder modules based on their `before` property.
579
+ async function sortModules(moduleNames) {
580
+ // The module names that have a `before` property
581
+ const beforeModules = [];
582
+ // The metadata quick access of all modules
583
+ const modules = {};
584
+ // Recursion guard
585
+ const recursionGuard = {};
586
+ // The sorted modules result
587
+ const sorted = [];
588
+
589
+ // The base module sort metadata
590
+ for (const name of moduleNames) {
591
+ const metadata = await self.synth.getMetadata(name);
592
+ const before = Object.values(metadata.before).reverse().find(name => typeof name === 'string');
593
+ if (before) {
594
+ beforeModules.push(name);
595
+ }
596
+ modules[name] = {
597
+ before,
598
+ beforeSelf: []
599
+ };
600
+ }
601
+
602
+ // Loop through the modules that have a `before` property,
603
+ // validate and fill the initial `beforeSelf` metadata (first pass).
604
+ for (const name of beforeModules) {
605
+ const m = modules[name];
606
+ const before = m.before;
607
+ if (m.before === name) {
608
+ throw new Error(`Module "${name}" has a 'before' property that references itself.`);
609
+ }
610
+ if (!modules[before]) {
611
+ throw new Error(`Module "${name}" has a 'before' property that references a non-existent module: "${before}".`);
612
+ }
613
+ // Add the current module name to the target's beforeSelf.
614
+ modules[before].beforeSelf.push(name);
615
+ }
616
+
617
+ // Loop through the modules that have a `before` properties
618
+ // now that we have the initial metadata (second pass).
619
+ // This takes care of edge cases like `before` that points to another module
620
+ // that has a `before` property itself, circular `before` references, etc.
621
+ // in a very predictable way.
622
+ for (const name of beforeModules) {
623
+ const m = modules[name];
624
+ const target = modules[m.before];
625
+ if (!target) {
626
+ continue;
627
+ }
628
+ // Add all the modules that want to be before this one to the target's beforeSelf.
629
+ // Do this recursively for every module from the beforeSelf array that has own `beforeSelf` members.
630
+ addBeforeSelfRecursive(name, m.beforeSelf, target.beforeSelf);
631
+ }
632
+
633
+ // Fill in the sorted array, first wins when uniquefy-ing.
634
+ for (const name of moduleNames) {
635
+ sorted.push(...modules[name].beforeSelf, name);
636
+ }
637
+
638
+ // A unique array of sorted module names.
639
+ return [ ...new Set(sorted) ];
640
+
641
+ function addBeforeSelfRecursive(moduleName, beforeSelf, target) {
642
+ if (beforeSelf.length === 0) {
643
+ return;
644
+ }
645
+ if (recursionGuard[moduleName]) {
646
+ return;
647
+ }
648
+ recursionGuard[moduleName] = true;
649
+
650
+ beforeSelf.forEach((name) => {
651
+ if (recursionGuard[name]) {
652
+ return;
653
+ }
654
+ target.unshift(name);
655
+ addBeforeSelfRecursive(name, modules[name].beforeSelf, target);
656
+ });
657
+ }
658
+ }
659
+
585
660
  async function instantiateModules() {
586
661
  self.modules = {};
587
- for (const item of modulesToBeInstantiated()) {
662
+ const sorted = await sortModules(modulesToBeInstantiated());
663
+ for (const item of sorted) {
588
664
  // module registers itself in self.modules
589
- const module = await self.synth.create(item, { apos: self });
590
- await module.emit('moduleReady');
665
+ const apostropheModule = await self.synth.create(item, { apos: self });
666
+ await apostropheModule.emit('moduleReady');
591
667
  }
592
668
  }
593
669
 
@@ -598,10 +674,10 @@ async function apostrophe(options, telemetry, rootSpan) {
598
674
  });
599
675
  }
600
676
 
601
- function lintModules() {
677
+ async function lintModules() {
602
678
  const validSteps = [];
603
- for (const module of Object.values(self.modules)) {
604
- for (const step of module.__meta.chain) {
679
+ for (const apostropheModule of Object.values(self.modules)) {
680
+ for (const step of apostropheModule.__meta.chain) {
605
681
  validSteps.push(step.name);
606
682
  }
607
683
  }
@@ -616,19 +692,19 @@ async function apostrophe(options, telemetry, rootSpan) {
616
692
  const nsDirs = fs.readdirSync(`${self.localModules}/${dir}`);
617
693
  for (let nsDir of nsDirs) {
618
694
  nsDir = `${dir}/${nsDir}`;
619
- testDir(nsDir);
695
+ await testDir(nsDir);
620
696
  }
621
697
  } else {
622
698
  testDir(dir);
623
699
  }
624
700
  }
625
- function testDir(name) {
701
+ async function testDir(name) {
626
702
  // Projects that have different theme modules activated at different times
627
703
  // are a frequent source of false positives for this warning, so ignore
628
704
  // seemingly unused modules with "theme" in the name
629
705
  if (!validSteps.includes(name)) {
630
706
  try {
631
- const submodule = require(require('path').resolve(`${self.localModules}/${name}`));
707
+ const submodule = await self.root.import(path.resolve(self.localModules, name, 'index.js'));
632
708
  if (submodule && submodule.options && submodule.options.ignoreUnusedFolderWarning) {
633
709
  return;
634
710
  }
@@ -666,7 +742,7 @@ async function apostrophe(options, telemetry, rootSpan) {
666
742
  }
667
743
  }
668
744
 
669
- for (const [ name, module ] of Object.entries(self.modules)) {
745
+ for (const [ name, apostropheModule ] of Object.entries(self.modules)) {
670
746
  if (name.match(/^apostrophe-/)) {
671
747
  self.util.warnDevOnce(
672
748
  'namespace-apostrophe-modules',
@@ -691,17 +767,17 @@ async function apostrophe(options, telemetry, rootSpan) {
691
767
  );
692
768
  }
693
769
 
694
- if (module.options.extends && ((typeof module.options.extends) === 'string')) {
770
+ if (apostropheModule.options.extends && ((typeof apostropheModule.options.extends) === 'string')) {
695
771
  lint(`The module ${name} contains an "extends" option. This is probably a\nmistake. In Apostrophe "extend" is used to extend other modules.`);
696
772
  }
697
- if (module.options.singletonWarningIfNot && (name !== module.options.singletonWarningIfNot)) {
698
- lint(`The module ${name} extends ${module.options.singletonWarningIfNot}, which is normally\na singleton (Apostrophe creates only one instance of it). Two competing\ninstances will lead to problems. If you are adding project-level code to it,\njust use modules/${module.options.singletonWarningIfNot}/index.js and do not use "extend".\nIf you are improving it via an npm module, use "improve" rather than "extend".\nIf neither situation applies you should probably just make a new module that does\nnot extend anything.\n\nIf you are sure you know what you are doing, you can set the\nsingletonWarningIfNot: false option for this module.`);
773
+ if (apostropheModule.options.singletonWarningIfNot && (name !== apostropheModule.options.singletonWarningIfNot)) {
774
+ lint(`The module ${name} extends ${apostropheModule.options.singletonWarningIfNot}, which is normally\na singleton (Apostrophe creates only one instance of it). Two competing\ninstances will lead to problems. If you are adding project-level code to it,\njust use modules/${apostropheModule.options.singletonWarningIfNot}/index.js and do not use "extend".\nIf you are improving it via an npm module, use "improve" rather than "extend".\nIf neither situation applies you should probably just make a new module that does\nnot extend anything.\n\nIf you are sure you know what you are doing, you can set the\nsingletonWarningIfNot: false option for this module.`);
699
775
  }
700
- if (name.match(/-widget$/) && (!extending(module)) && (!module.options.ignoreNoExtendWarning)) {
776
+ if (name.match(/-widget$/) && (!extending(apostropheModule)) && (!apostropheModule.options.ignoreNoExtendWarning)) {
701
777
  lint(`The module ${name} does not extend anything.\n\nA -widget module usually extends @apostrophecms/widget-type or another widget type.\nOr possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\nignoreNoExtendWarning option to true for this module.`);
702
- } else if (name.match(/-page$/) && (name !== '@apostrophecms/page') && (!extending(module)) && (!module.options.ignoreNoExtendWarning)) {
778
+ } else if (name.match(/-page$/) && (name !== '@apostrophecms/page') && (!extending(apostropheModule)) && (!apostropheModule.options.ignoreNoExtendWarning)) {
703
779
  lint(`The module ${name} does not extend anything.\n\nA -page module usually extends @apostrophecms/page-type or\n@apostrophecms/piece-page-type or another page type.\nOr possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\nignoreNoExtendWarning option to true for this module.`);
704
- } else if ((!extending(module)) && (!hasCode(name)) && (!isBundle(name)) && (!module.options.ignoreNoCodeWarning)) {
780
+ } else if ((!extending(apostropheModule)) && (!hasCode(name)) && (!isBundle(name)) && (!apostropheModule.options.ignoreNoCodeWarning)) {
705
781
  lint(`The module ${name} does not extend anything and does not have any code.\n\nThis usually means that you:\n\n1. Forgot to "extend" another module\n2. Configured a module that comes from npm without npm installing it\n3. Simply haven't written your "index.js" yet\n\nIf you really want a module with no code, set the ignoreNoCodeWarning option\nto true for this module.`);
706
782
  }
707
783
  }
@@ -738,12 +814,12 @@ async function apostrophe(options, telemetry, rootSpan) {
738
814
  const d = self.synth.definitions[name];
739
815
  return d.bundle || (d.extend && d.extend.bundle);
740
816
  }
741
- function extending(module) {
817
+ function extending(apostropheModule) {
742
818
  // If the module extends no other module, then it will
743
819
  // have up to four entries in its inheritance chain:
744
820
  // project level self, npm level self, `apostrophe-modules`
745
821
  // project-level and `apostrophe-modules` npm level.
746
- return module.__meta.chain.length > 4;
822
+ return apostropheModule.__meta.chain.length > 4;
747
823
  }
748
824
 
749
825
  function lint(s) {
@@ -775,3 +851,77 @@ function respawn(worker) {
775
851
  console.error(`Respawning worker process ${worker.process.pid}`);
776
852
  clusterFork();
777
853
  }
854
+
855
+ module.exports.buildRoot = buildRoot;
856
+
857
+ function buildRoot(options) {
858
+ const root = getRoot(options);
859
+ const rootDir = options.rootDir || path.dirname(root.filename);
860
+ const npmRootDir = options.npmRootDir || rootDir;
861
+ const selfDir = __dirname;
862
+
863
+ return {
864
+ root,
865
+ rootDir,
866
+ npmRootDir,
867
+ selfDir
868
+ };
869
+ }
870
+ function getRoot(options) {
871
+ const root = options.root;
872
+ if (root?.filename && root?.require) {
873
+ return {
874
+ filename: root.filename,
875
+ import: async (id) => root.require(id),
876
+ require: (id) => root.require(id)
877
+ };
878
+ }
879
+
880
+ if (root?.url) {
881
+ // Apostrophe was started from an ESM project
882
+ const filename = url.fileURLToPath(root.url);
883
+ const dynamicImport = async (id) => {
884
+ const { default: defaultExport, ...rest } = await import(id);
885
+
886
+ return defaultExport || rest;
887
+ };
888
+
889
+ return {
890
+ filename,
891
+ import: dynamicImport,
892
+ require: (id) => {
893
+ console.warn(`self.apos.root.require is now async, please verify that you await the promise (${id})`);
894
+
895
+ return dynamicImport(id);
896
+ }
897
+ };
898
+ }
899
+
900
+ // Legacy commonjs logic
901
+ function getLegacyRoot() {
902
+ let _module = module;
903
+ let m = _module;
904
+ while (m.parent && m.parent.filename) {
905
+ // The test file is the root as far as we are concerned,
906
+ // not mocha itself
907
+ if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
908
+ return m;
909
+ }
910
+ m = m.parent;
911
+ _module = m;
912
+ }
913
+ return _module;
914
+ }
915
+ const legacyRoot = getLegacyRoot();
916
+ return {
917
+ filename: legacyRoot.filename,
918
+ import: async (id) => legacyRoot.require(id),
919
+ require: (id) => legacyRoot.require(id)
920
+ };
921
+ };
922
+
923
+ module.exports.getNpmPath = getNpmPath;
924
+
925
+ function getNpmPath(name, baseDir) {
926
+ return npmResolve.sync(name, { basedir: path.resolve(baseDir) });
927
+ }