apostrophe 3.9.0 → 3.12.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 (42) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/index.js +37 -1
  3. package/lib/moog.js +28 -3
  4. package/modules/@apostrophecms/admin-bar/ui/apos/apps/AposAdminBar.js +7 -1
  5. package/modules/@apostrophecms/any-page-type/index.js +2 -0
  6. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +13 -1
  7. package/modules/@apostrophecms/asset/index.js +7 -2
  8. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +2 -2
  9. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +1 -1
  10. package/modules/@apostrophecms/busy/ui/apos/apps/AposBusy.js +3 -1
  11. package/modules/@apostrophecms/doc/index.js +2 -2
  12. package/modules/@apostrophecms/doc-type/index.js +12 -8
  13. package/modules/@apostrophecms/i18n/index.js +59 -12
  14. package/modules/@apostrophecms/login/index.js +23 -10
  15. package/modules/@apostrophecms/login/ui/apos/apps/AposLogin.js +3 -2
  16. package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +8 -5
  17. package/modules/@apostrophecms/module/index.js +12 -2
  18. package/modules/@apostrophecms/notification/ui/apos/apps/AposNotification.js +3 -1
  19. package/modules/@apostrophecms/page/index.js +4 -5
  20. package/modules/@apostrophecms/permission/index.js +1 -1
  21. package/modules/@apostrophecms/piece-type/index.js +1 -1
  22. package/modules/@apostrophecms/rich-text-widget/index.js +2 -1
  23. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +3 -1
  24. package/modules/@apostrophecms/schema/index.js +25 -3
  25. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +12 -5
  26. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +7 -3
  27. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +17 -0
  28. package/modules/@apostrophecms/util/index.js +3 -9
  29. package/modules/@apostrophecms/widget-type/index.js +9 -0
  30. package/package.json +10 -10
  31. package/test/modules/base-type/i18n/custom/en.json +4 -0
  32. package/test/modules/base-type/i18n/en.json +3 -0
  33. package/test/modules/nested-module-subdirs/example1/index.js +5 -0
  34. package/test/modules/nested-module-subdirs/modules.js +7 -0
  35. package/test/modules/subtype/i18n/custom/en.json +4 -0
  36. package/test/modules/subtype/index.js +7 -0
  37. package/test/moog.js +48 -0
  38. package/test/static-i18n.js +28 -0
  39. package/test/with-nested-module-subdirs.js +32 -0
  40. package/test/without-nested-module-subdirs.js +31 -0
  41. package/test-lib/util.js +4 -2
  42. package/.github/pull_request_template.md +0 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.12.0 - 2022-01-21
4
+
5
+ ### Adds
6
+
7
+ * It is now best practice to deliver namespaced i18n strings as JSON files in module-level subdirectories of `i18n/` named to match the namespace, e.g. `i18n/ourTeam` if the namespace is `ourTeam`. This allows base class modules to deliver phrases to any namespace without conflicting with those introduced at project level. The `i18n` option is now deprecated in favor of the new `i18n` module format section, which is only needed if `browser: true` must be specified for a namespace.
8
+ * Brought back the `nestedModuleSubdirs` feature from A2, which allows modules to be nested in subdirectories if `nestedModuleSubdirs: true` is set in `app.js`. As in A2, module configuration (including activation) can also be grouped in a `modules.js` file in such subdirectories.
9
+
10
+ ### Fixes
11
+
12
+ * Fixes minor inline documentation comments.
13
+ * UI strings that are not registered localization keys will now display properly when they contain a colon (`:`). These were previously interpreted as i18next namespace/key pairs and the "namespace" portion was left out.
14
+ * Fixes a bug where changing the page type immediately after clicking "New Page" would produce a console error. In general, areas and checkboxes now correctly handle their value being changed to `null` by the parent schema after initial startup of the `AposInputArea` or `AposInputCheckboxes` component.
15
+ * It is now best practice to deliver namespaced i18n strings as JSON files in module-level subdirectories of `i18n/` named to match the namespace, e.g. `i18n/ourTeam` if the namespace is `ourTeam`. This allows base class modules to deliver phrases to any namespace without conflicting with those introduced at project level. The `i18n` option is now deprecated in favor of the new `i18n` module format section, which is only needed if `browser: true` must be specified for a namespace.
16
+ * Removes the `@apostrophecms/util` module template helper `indexBy`, which was using a lodash method not included in lodash v4.
17
+ * Removes an unimplemented `csrfExceptions` module section cascade. Use the `csrfExceptions` *option* of any module to set an array of URLs excluded from CSRF protection. More information is forthcoming in the documentation.
18
+ * Fix `[Object Object]` in the console when warning `A permission.can() call was made with a type that has no manager` is printed.
19
+
20
+ ### Changes
21
+
22
+ * Temporarily removes `npm audit` from our automated tests because of a sub-dependency of vue-loader that doesn't actually cause a security vulnerability for apostrophe.
23
+
24
+ ## 3.11.0 - 2022-01-06
25
+
26
+ ### Adds
27
+
28
+ * Apostrophe now extends Passport's `req.login` to emit an `afterSessionLogin` event from the `@apostrophecms:login` module, with `req` as an argument. Note that this does not occur at all for login API calls that return a bearer token rather than establishing an Express session.
29
+
30
+ ### Fixes
31
+
32
+ * Apostrophe's extension of `req.login` now accounts for the `req.logIn` alias and the skippable `options` parameter, which is relied upon in some `passport` strategies.
33
+ * Apostrophe now warns if a nonexistent widget type is configured for an area field, with special attention to when `-widget` has been erroneously included in the name. For backwards compatibility this is a startup warning rather than a fatal error, as sites generally did operate successfully otherwise with this type of bug present.
34
+
35
+ ### Changes
36
+
37
+ * Unpins `vue-click-outside-element` the packaging of which has been fixed upstream.
38
+ * Adds deprecation note to `__testDefaults` option. It is not in use, but removing would be a minor BC break we don't need to make.
39
+ * Allows test modules to use a custom port as an option on the `@apostrophecms/express` module.
40
+ * Removes the code base pull request template to instead inherit the organization-level template.
41
+ * Adds `npm audit` back to the test scripts.
42
+
43
+ ## 3.10.0 - 2021-12-22
44
+
45
+ ### Fixes
46
+
47
+ * `slug` type fields can now have an empty string or `null` as their `def` value without the string `'none'` populating automatically.
48
+ * The `underline` feature works properly in tiptap toolbar configuration.
49
+ * Required checkbox fields now properly prevent editor submission when empty.
50
+ * Pins `vue-click-outside-element` to a version that does not attempt to use `eval` in its distribution build, which is incompatible with a strict Content Security Policy.
51
+
52
+ ### Adds
53
+
54
+ * Adds a `last` option to fields. Setting `last: true` on a field puts that field at the end of the field's widget order. If more than one field has that option active the true last item will depend on general field registration order. If the field is ordered with the `fields.order` array or field group ordering, those specified orders will take precedence.
55
+
56
+ ### Changes
57
+
58
+ * Adds deprecation notes to the widget class methods `getWidgetWrapperClasses` and `getWidgetClasses` from A2.
59
+ * Adds a deprecation note to the `reorganize` query builder for the next major version.
60
+ * Uses the runtime build of Vue. This has major performance and bundle size benefits, however it does require changes to Apostrophe admin UI apps that use a `template` property (components should require no changes, just apps require an update). These apps must now use a `render` function instead. Since custom admin UI apps are not yet a documented feature we do not regard this as a bc break.
61
+ * Compatible with the `@apostrophecms/security-headers` module, which supports a strict `Content-Security-Policy`.
62
+ * Adds a deprecation note to the `addLateCriteria` query builder.
63
+ * Updates the `toCount` doc type query method to use Math.ceil rather than Math.floor plus an additional step.
64
+
3
65
  ## 3.9.0 - 2021-12-08
4
66
 
5
67
  ### Adds
package/index.js CHANGED
@@ -7,6 +7,7 @@ const cluster = require('cluster');
7
7
  const { cpus } = require('os');
8
8
  const process = require('process');
9
9
  const npmResolve = require('resolve');
10
+ const glob = require('glob');
10
11
 
11
12
  let defaults = require('./defaults.js');
12
13
 
@@ -273,6 +274,33 @@ module.exports = async function(options) {
273
274
  return _module;
274
275
  }
275
276
 
277
+ function nestedModuleSubdirs() {
278
+ if (!options.nestedModuleSubdirs) {
279
+ return;
280
+ }
281
+ const configs = glob.sync(self.localModules + '/**/modules.js');
282
+ _.each(configs, function(config) {
283
+ try {
284
+ _.merge(self.options.modules, require(config));
285
+ } catch (e) {
286
+ console.error(stripIndent`
287
+ When nestedModuleSubdirs is active, any modules.js file beneath:
288
+
289
+ ${self.localModules}
290
+
291
+ must export an object containing configuration for Apostrophe modules.
292
+
293
+ The file:
294
+
295
+ ${config}
296
+
297
+ did not parse.
298
+ `);
299
+ throw e;
300
+ }
301
+ });
302
+ }
303
+
276
304
  function autodetectBundles() {
277
305
  const modules = _.keys(self.options.modules);
278
306
  _.each(modules, function(name) {
@@ -406,7 +434,13 @@ module.exports = async function(options) {
406
434
  localModules: self.localModules,
407
435
  defaultBaseClass: '@apostrophecms/module',
408
436
  sections: [ 'helpers', 'handlers', 'routes', 'apiRoutes', 'restApiRoutes', 'renderRoutes', 'middleware', 'customTags', 'components', 'tasks' ],
409
- unparsedSections: [ 'queries', 'extendQueries', 'icons' ]
437
+ nestedModuleSubdirs: self.options.nestedModuleSubdirs,
438
+ unparsedSections: [
439
+ 'queries',
440
+ 'extendQueries',
441
+ 'icons',
442
+ 'i18n'
443
+ ]
410
444
  });
411
445
 
412
446
  self.synth = synth;
@@ -417,6 +451,8 @@ module.exports = async function(options) {
417
451
  self.redefine = self.synth.redefine;
418
452
  self.create = self.synth.create;
419
453
 
454
+ nestedModuleSubdirs();
455
+
420
456
  _.each(self.options.modules, function(options, name) {
421
457
  synth.define(name, options);
422
458
  });
package/lib/moog.js CHANGED
@@ -179,17 +179,42 @@ module.exports = function(options) {
179
179
  ...properties.add
180
180
  };
181
181
  }
182
+
183
+ const lastFields = Object.entries(that[cascade])
184
+ .filter(([ field, val ]) => val.last === true)
185
+ .map(([ field, val ]) => field);
186
+
182
187
  if (properties.remove) {
183
188
  for (const field of properties.remove) {
184
189
  delete that[cascade][field];
185
190
  }
186
191
  }
187
192
  if (properties.order) {
193
+ // 1. Ordered fields
194
+ // 2. Other fields not marked as last
195
+ // 3. Fields marked as last
188
196
  that[cascade] = Object.fromEntries([
189
197
  ...properties.order.map(field => [ field, that[cascade][field] ]),
190
- ...Object.keys(that[cascade]).filter(field => !properties.order.includes[field]).map(field => [ field, that[cascade][field] ])
198
+ ...Object.keys(that[cascade])
199
+ .filter(field => {
200
+ return !properties.order.includes(field) &&
201
+ !lastFields.includes(field);
202
+ })
203
+ .map(field => [ field, that[cascade][field] ]),
204
+ ...lastFields.filter(field => !properties.order.includes(field))
205
+ .map(field => [ field, that[cascade][field] ])
206
+ ]);
207
+ } else if (lastFields.length > 0) {
208
+ // 1. Fields not marked as last
209
+ // 2. Fields marked as last
210
+ that[cascade] = Object.fromEntries([
211
+ ...Object.keys(that[cascade])
212
+ .filter(field => !lastFields.includes(field))
213
+ .map(field => [ field, that[cascade][field] ]),
214
+ ...lastFields.map(field => [ field, that[cascade][field] ])
191
215
  ]);
192
216
  }
217
+
193
218
  if (properties.group) {
194
219
  const groups = klona(that[`${cascade}Groups`]);
195
220
  for (const value of Object.values(properties.group)) {
@@ -344,9 +369,9 @@ module.exports = function(options) {
344
369
  }
345
370
 
346
371
  function capture(section) {
347
- that[section] = {};
372
+ that.__meta[section] = {};
348
373
  for (const step of steps) {
349
- that[section][step.__meta.name] = step[section];
374
+ that.__meta[section][step.__meta.name] = step[section];
350
375
  }
351
376
  }
352
377
 
@@ -10,7 +10,13 @@ export default function() {
10
10
  return window.apos;
11
11
  }
12
12
  },
13
- template: '<component :is="`TheAposAdminBar`" :items="apos.adminBar.items" />'
13
+ render: function (h) {
14
+ return h('TheAposAdminBar', {
15
+ props: {
16
+ items: apos.adminBar.items
17
+ }
18
+ });
19
+ }
14
20
  });
15
21
  }
16
22
  };
@@ -287,6 +287,8 @@ module.exports = {
287
287
  // are suitable for display in the reorganize view.
288
288
  // The only pages excluded are those with a `reorganize`
289
289
  // property explicitly set to `false`.
290
+ // NOTE: This query builder is deprecated and will be removed in the
291
+ // next major version.
290
292
  reorganize: {
291
293
  def: null,
292
294
  finalize() {
@@ -111,7 +111,19 @@ export default function() {
111
111
  renderings
112
112
  };
113
113
  },
114
- template: `<${component} :options="options" :items="items" :choices="choices" :id="$data.id" :docId="$data.docId" :fieldId="fieldId" :renderings="renderings" />`
114
+ render(h) {
115
+ return h(component, {
116
+ props: {
117
+ options: this.options,
118
+ items: this.items,
119
+ choices: this.choices,
120
+ id: this.id,
121
+ docId: this.docId,
122
+ fieldId: this.fieldId,
123
+ renderings: this.renderings
124
+ }
125
+ });
126
+ }
115
127
  });
116
128
  }
117
129
  }
@@ -752,8 +752,7 @@ module.exports = {
752
752
  if (!self.shouldRefreshOnRestart()) {
753
753
  return '';
754
754
  }
755
- const script = fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
756
- return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id">\n${script}</script>`);
755
+ return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id" src="${self.action}/refresh-on-restart"></script>`);
757
756
  },
758
757
  url(path) {
759
758
  return `${self.getAssetBaseUrl()}${path}`;
@@ -765,6 +764,12 @@ module.exports = {
765
764
  return;
766
765
  }
767
766
  return {
767
+ get: {
768
+ refreshOnRestart(req) {
769
+ req.res.setHeader('content-type', 'text/javascript');
770
+ return fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
771
+ }
772
+ },
768
773
  // Use a POST route so IE11 doesn't cache it
769
774
  post: {
770
775
  async restartId(req) {
@@ -28,7 +28,7 @@ module.exports = ({
28
28
  optimization: {
29
29
  minimize: process.env.NODE_ENV === 'production'
30
30
  },
31
- devtool: 'eval-source-map',
31
+ devtool: 'source-map',
32
32
  output: {
33
33
  path: outputPath,
34
34
  filename: outputFilename
@@ -42,7 +42,7 @@ module.exports = ({
42
42
  resolve: {
43
43
  extensions: [ '*', '.js', '.vue', '.json' ],
44
44
  alias: {
45
- vue$: 'vue/dist/vue.esm.js',
45
+ vue$: 'vue/dist/vue.runtime.esm.js',
46
46
  // resolve apostrophe modules
47
47
  Modules: path.resolve(modulesDir)
48
48
  },
@@ -34,7 +34,7 @@ module.exports = ({
34
34
  optimization: {
35
35
  minimize: process.env.NODE_ENV === 'production'
36
36
  },
37
- devtool: 'eval-source-map',
37
+ devtool: 'source-map',
38
38
  output: {
39
39
  path: outputPath,
40
40
  filename: outputFilename
@@ -3,6 +3,8 @@ import Vue from 'Modules/@apostrophecms/ui/lib/vue';
3
3
  export default function() {
4
4
  return new Vue({
5
5
  el: '#apos-busy',
6
- template: '<component :is="`TheAposBusy`" />'
6
+ render: function (h) {
7
+ return h('TheAposBusy');
8
+ }
7
9
  });
8
10
  };
@@ -675,10 +675,10 @@ module.exports = {
675
675
  },
676
676
 
677
677
  // Insert the given document. Called by `.insert()`. You will usually want to
678
- // call the update method of the appropriate doc type manager instead:
678
+ // call the insert method of the appropriate doc type manager instead:
679
679
  //
680
680
  // ```javascript
681
- // self.apos.doc.getManager(doc.type).update(...)
681
+ // self.apos.doc.getManager(doc.type).insert(...)
682
682
  // ```
683
683
  //
684
684
  // However you can override this method to alter the
@@ -1152,8 +1152,10 @@ module.exports = {
1152
1152
 
1153
1153
  // `.addLateCriteria({...})` provides an object to be merged directly into the final
1154
1154
  // criteria object that will go to MongoDB. This is to be used only
1155
- // in cases where MongoDB forbids the use of an operator inside
1156
- // `$and`, such as the `$near` operator.
1155
+ // in cases where MongoDB forbids the use of an operator inside `$and`.
1156
+ //
1157
+ // TODO: Since `$near` can now be used in `$and` operators, this query
1158
+ // builder is deprecated and should be removed in the 4.x major version.
1157
1159
  addLateCriteria: {
1158
1160
  set(c) {
1159
1161
  let lateCriteria = query.get('lateCriteria');
@@ -1420,7 +1422,7 @@ module.exports = {
1420
1422
  }
1421
1423
  },
1422
1424
 
1423
- // `.permission('admin')` would limit the returned docs to those for which the
1425
+ // `.permission('edit')` would limit the returned docs to those for which the
1424
1426
  // user associated with the query's `req` has the named permission.
1425
1427
  // By default, `view` is checked for. You might want to specify
1426
1428
  // `edit`.
@@ -1658,7 +1660,11 @@ module.exports = {
1658
1660
  const _req = query.req.clone({
1659
1661
  mode: 'published'
1660
1662
  });
1661
- const publishedDocs = await self.find(_req)._ids(results.map(result => result._id.replace(':draft', ':published'))).project(query.get('project')).toArray();
1663
+ const publishedDocs = await self.find(_req)
1664
+ ._ids(results.map(result => {
1665
+ return result._id.replace(':draft', ':published');
1666
+ })).project(query.get('project')).toArray();
1667
+
1662
1668
  for (const doc of results) {
1663
1669
  const publishedDoc = publishedDocs.find(publishedDoc => doc.aposDocId === publishedDoc.aposDocId);
1664
1670
  doc._publishedDoc = publishedDoc;
@@ -2148,10 +2154,8 @@ module.exports = {
2148
2154
  const count = await mongo.count();
2149
2155
  if (query.get('perPage')) {
2150
2156
  const perPage = query.get('perPage');
2151
- let totalPages = Math.floor(count / perPage);
2152
- if (count % perPage) {
2153
- totalPages++;
2154
- }
2157
+ const totalPages = Math.ceil(count / perPage);
2158
+
2155
2159
  query.set('totalPages', totalPages);
2156
2160
  }
2157
2161
  return count;
@@ -10,6 +10,7 @@ const fs = require('fs');
10
10
  const _ = require('lodash');
11
11
  const { stripIndent } = require('common-tags');
12
12
  const ExpressSessionCookie = require('express-session/session/cookie');
13
+ const path = require('path');
13
14
 
14
15
  const apostropheI18nDebugPlugin = {
15
16
  type: 'postProcessor',
@@ -380,21 +381,65 @@ module.exports = {
380
381
  // Add the i18next resources provided by the specified module,
381
382
  // merging with any existing phrases for the same locales and namespaces
382
383
  addResourcesForModule(module) {
383
- if (!module.options.i18n) {
384
- return;
385
- }
386
- const ns = module.options.i18n.ns || 'default';
384
+ self.addDefaultResourcesForModule(module);
385
+ self.addNamespacedResourcesForModule(module);
386
+ },
387
+ // Automatically adds any localizations found in .json files in the main `i18n` subdirectory
388
+ // of a module.
389
+ //
390
+ // These are added to the `default` namespace, unless the legacy `i18n.ns` option is set
391
+ // for the module (not the preferred way, use namespace subdirectories in new projects).
392
+ addDefaultResourcesForModule(module) {
393
+ const ns = (module.options.i18n && module.options.i18n.ns) || 'default';
387
394
  self.namespaces[ns] = self.namespaces[ns] || {};
388
- self.namespaces[ns].browser = self.namespaces[ns].browser || !!module.options.i18n.browser;
395
+ self.namespaces[ns].browser = self.namespaces[ns].browser || (module.options.i18n && module.options.i18n.browser);
389
396
  for (const entry of module.__meta.chain) {
390
- const localizationsDir = `${entry.dirname}/i18n`;
391
- if (!fs.existsSync(localizationsDir)) {
392
- continue;
397
+ const localizationsDir = path.join(entry.dirname, 'i18n');
398
+ if (!self.defaultLocalizationsDirsAdded.has(localizationsDir)) {
399
+ self.defaultLocalizationsDirsAdded.add(localizationsDir);
400
+ if (!fs.existsSync(localizationsDir)) {
401
+ continue;
402
+ }
403
+ for (const localizationFile of fs.readdirSync(localizationsDir)) {
404
+ if (!localizationFile.endsWith('.json')) {
405
+ // Likely a namespace subdirectory
406
+ continue;
407
+ }
408
+ const data = JSON.parse(fs.readFileSync(path.join(localizationsDir, localizationFile)));
409
+ const locale = localizationFile.replace('.json', '');
410
+ self.i18next.addResourceBundle(locale, ns, data, true, true);
411
+ }
393
412
  }
394
- for (const localizationFile of fs.readdirSync(localizationsDir)) {
395
- const data = JSON.parse(fs.readFileSync(`${localizationsDir}/${localizationFile}`));
396
- const locale = localizationFile.replace('.json', '');
397
- self.i18next.addResourceBundle(locale, ns, data, true, true);
413
+ }
414
+ },
415
+ // Automatically adds any localizations found in subdirectories of the main `i18n`
416
+ // subdirectory of a module. The subdirectory's name is treated as an i18n namespace
417
+ // name.
418
+ addNamespacedResourcesForModule(module) {
419
+ for (const entry of module.__meta.chain) {
420
+ const metadata = module.__meta.i18n[entry.name] || {};
421
+ const localizationsDir = `${entry.dirname}/i18n`;
422
+ if (!self.namespacedLocalizationsDirsAdded.has(localizationsDir)) {
423
+ self.namespacedLocalizationsDirsAdded.add(localizationsDir);
424
+ if (!fs.existsSync(localizationsDir)) {
425
+ continue;
426
+ }
427
+ for (const ns of fs.readdirSync(localizationsDir)) {
428
+ if (ns.endsWith('.json')) {
429
+ // A JSON file for the default namespace, already handled
430
+ continue;
431
+ }
432
+ self.namespaces[ns] = self.namespaces[ns] || {};
433
+ self.namespaces[ns].browser = self.namespaces[ns].browser ||
434
+ (metadata[ns] && metadata[ns].browser);
435
+ const namespaceDir = path.join(localizationsDir, ns);
436
+ for (const localizationFile of fs.readdirSync(namespaceDir)) {
437
+ const fullLocalizationFile = path.join(namespaceDir, localizationFile);
438
+ const data = JSON.parse(fs.readFileSync(fullLocalizationFile));
439
+ const locale = localizationFile.replace('.json', '');
440
+ self.i18next.addResourceBundle(locale, ns, data, true, true);
441
+ }
442
+ }
398
443
  }
399
444
  }
400
445
  },
@@ -402,6 +447,8 @@ module.exports = {
402
447
  // itself, called by init. Later modules call addResourcesForModule(self),
403
448
  // making phrases available gradually as Apostrophe starts up
404
449
  addInitialResources() {
450
+ self.defaultLocalizationsDirsAdded = new Set();
451
+ self.namespacedLocalizationsDirsAdded = new Set();
405
452
  for (const module of Object.values(self.apos.modules)) {
406
453
  self.addResourcesForModule(module);
407
454
  }
@@ -37,13 +37,6 @@
37
37
  //
38
38
  // Apostrophe's instance of the [passport](https://npmjs.org/package/passport) npm module.
39
39
  // You may access this object if you need to implement additional passport "strategies."
40
- //
41
- // ## callAll method: loginAfterLogin
42
- //
43
- // The method `loginAfterLogin` is invoked on **all modules that have one**. This method
44
- // is a good place to set `req.redirect` to the URL of your choice. If no module sets
45
- // `req.redirect`, the newly logged-in user is redirected to the home page. `loginAfterLogin`
46
- // is invoked with `req` and may be an async function.
47
40
 
48
41
  const Passport = require('passport').Passport;
49
42
  const LocalStrategy = require('passport-local');
@@ -405,15 +398,35 @@ module.exports = {
405
398
  before: '@apostrophecms/i18n',
406
399
  middleware(req, res, next) {
407
400
  const superLogin = req.login.bind(req);
408
- req.login = (user, callback) => {
409
- return superLogin(user, (err) => {
401
+ req.login = (user, ...args) => {
402
+ let options, callback;
403
+ // Support inconsistent calling conventions inside passport core
404
+ if (typeof args[0] === 'function') {
405
+ options = {};
406
+ callback = args[0];
407
+ } else {
408
+ options = args[0];
409
+ callback = args[1];
410
+ }
411
+ return superLogin(user, options, async (err) => {
410
412
  if (err) {
411
413
  return callback(err);
412
414
  }
413
- req.session.loginAt = Date.now();
415
+ await self.emit('afterSessionLogin', req);
416
+ // Make sure no handler removed req.user
417
+ if (req.user) {
418
+ // Mark the login timestamp. Middleware takes care of ensuring
419
+ // that logins cannot be used to carry out actions prior
420
+ // to this property being added
421
+ req.session.loginAt = Date.now();
422
+ }
414
423
  return callback(null);
415
424
  });
416
425
  };
426
+ // Passport itself maintains this bc alias, while refusing
427
+ // to actually decide which one is best in its own dev docs.
428
+ // Both have to exist to avoid bugs when passport calls itself
429
+ req.logIn = req.login;
417
430
  return next();
418
431
  }
419
432
  },
@@ -5,8 +5,9 @@ export default function() {
5
5
  if (el) {
6
6
  return new Vue({
7
7
  el,
8
- // TODO check apos.login.browser.components.theAposLogin for alternate name
9
- template: '<component :is="`TheAposLogin`" />'
8
+ render: function (h) {
9
+ return h('TheAposLogin');
10
+ }
10
11
  });
11
12
  }
12
13
  apos.bus.$on('admin-menu-click', async (item) => {
@@ -27,11 +27,14 @@ export default function() {
27
27
  return this.$refs.modals.execute(componentName, props);
28
28
  }
29
29
  },
30
- template: `<component
31
- ref="modals"
32
- :is="apos.modal.components.the"
33
- :modals="apos.modal.modals"
34
- />`
30
+ render(h) {
31
+ return h(apos.modal.components.the, {
32
+ ref: 'modals',
33
+ props: {
34
+ modals: apos.modal.modals
35
+ }
36
+ });
37
+ }
35
38
  });
36
39
  apos.modal.execute = theAposModals.execute;
37
40
  apos.confirm = theAposModals.confirm;
@@ -29,10 +29,20 @@ const _ = require('lodash');
29
29
 
30
30
  module.exports = {
31
31
 
32
- cascades: [ 'csrfExceptions' ],
33
-
34
32
  init(self) {
35
33
  self.apos = self.options.apos;
34
+ const capturedSections = [
35
+ 'queries',
36
+ 'extendQueries',
37
+ 'icons'
38
+ ];
39
+ for (const section of capturedSections) {
40
+ // Unparsed sections are now captured in __meta, promote
41
+ // these to the top level to maintain bc. For new unparsed
42
+ // sections we'll leave them in `__meta` to avoid bc breaks
43
+ // with project-level properties of the module
44
+ self[section] = self.__meta[section];
45
+ }
36
46
  // all apostrophe modules are properties of self.apos.modules.
37
47
  // Those with an alias are also properties of self.apos
38
48
  self.apos.modules[self.__meta.name] = self;
@@ -9,6 +9,8 @@ export default function() {
9
9
 
10
10
  return new Vue({
11
11
  el: '#apos-notification',
12
- template: '<TheAposNotifications />'
12
+ render: function (h) {
13
+ return h('TheAposNotifications');
14
+ }
13
15
  });
14
16
  };
@@ -1977,14 +1977,13 @@ database.`);
1977
1977
  );
1978
1978
  },
1979
1979
  // Returns the effective base URL for the given request.
1980
- // If Apostrophe's top-level `baseUrl` option is set, it is returned,
1981
- // otherwise the empty string. This makes it easier to build absolute
1980
+ // If Apostrophe's top-level `baseUrl` option is set, or a hostname is
1981
+ // defined for the active locale, then that is consulted, otherwise the base URL
1982
+ // is the empty string. This makes it easier to build absolute
1982
1983
  // URLs (when `baseUrl` is configured), or to harmlessly prepend
1983
1984
  // the empty string (when it is not configured). The
1984
1985
  // Apostrophe queries used to fetch Apostrophe pages
1985
- // consult this method, and it is extended by the optional
1986
- // `@apostrophecms/workflow` module to create correct absolute URLs
1987
- // for specific locales.
1986
+ // consult this method.
1988
1987
  getBaseUrl(req) {
1989
1988
  const hostname = self.apos.i18n.locales[req.locale].hostname;
1990
1989
  if (hostname) {
@@ -63,7 +63,7 @@ module.exports = {
63
63
  const doc = (docOrType && docOrType._id) ? docOrType : null;
64
64
  const manager = type && self.apos.doc.getManager(type);
65
65
  if (type && !manager) {
66
- self.apos.util.warn(`A permission.can() call was made with a type that has no manager: ${type}`);
66
+ self.apos.util.warn('A permission.can() call was made with a type that has no manager:', type);
67
67
  return false;
68
68
  }
69
69
  if (action === 'view') {
@@ -566,7 +566,7 @@ module.exports = {
566
566
  },
567
567
  //
568
568
  // Update a piece. Convenience wrapper for `apos.doc.insert`.
569
- // Returns the piece. `beforeInsert`, `beforeSave`, `afterInsert`
569
+ // Returns the piece. `beforeUpdate`, `beforeSave`, `afterUpdate`
570
570
  // and `afterSave` async events are emitted by this module.
571
571
  async update(req, piece, options) {
572
572
  return self.apos.doc.update(req, piece, options);
@@ -250,7 +250,8 @@ module.exports = {
250
250
  codeBlock: [
251
251
  'pre',
252
252
  'code'
253
- ]
253
+ ],
254
+ underline: [ 'u' ]
254
255
  };
255
256
  for (const item of options.toolbar || []) {
256
257
  if (simple[item]) {
@@ -44,6 +44,7 @@ import StarterKit from '@tiptap/starter-kit';
44
44
  import TextAlign from '@tiptap/extension-text-align';
45
45
  import Highlight from '@tiptap/extension-highlight';
46
46
  import TextStyle from '@tiptap/extension-text-style';
47
+ import Underline from '@tiptap/extension-underline';
47
48
  export default {
48
49
  name: 'AposRichTextWidgetEditor',
49
50
  components: {
@@ -181,7 +182,8 @@ export default {
181
182
  types: [ 'heading', 'paragraph' ]
182
183
  }),
183
184
  Highlight,
184
- TextStyle
185
+ TextStyle,
186
+ Underline
185
187
  ].concat(this.aposTiptapExtensions)
186
188
  });
187
189
  },
@@ -18,7 +18,7 @@ const _ = require('lodash');
18
18
  const dayjs = require('dayjs');
19
19
  const tinycolor = require('tinycolor2');
20
20
  const { klona } = require('klona');
21
- const { stripIndent } = require('common-tags');
21
+ const { stripIndents } = require('common-tags');
22
22
 
23
23
  module.exports = {
24
24
  options: {
@@ -74,6 +74,22 @@ module.exports = {
74
74
  return true;
75
75
  }
76
76
  return _.isEqual(one[field.name], two[field.name]);
77
+ },
78
+ validate: function (field, options, warn, fail) {
79
+ if (field.options && field.options.widgets) {
80
+ for (const name of Object.keys(field.options.widgets)) {
81
+ if (!self.apos.modules[`${name}-widget`]) {
82
+ if (name.match(/-widget$/)) {
83
+ warn(stripIndents`
84
+ Do not include "-widget" in the name when configuring a widget in an area field.
85
+ Apostrophe will automatically add "-widget" when looking for the right module.
86
+ `);
87
+ } else {
88
+ warn(`Nonexistent widget type name ${name} in area field.`);
89
+ }
90
+ }
91
+ }
92
+ }
77
93
  }
78
94
  });
79
95
 
@@ -142,11 +158,14 @@ module.exports = {
142
158
  // leading slash required). Otherwise, expect a object-style slug
143
159
  // (no slashes at all)
144
160
  convert: function (req, field, data, destination) {
145
- const options = {};
161
+ const options = {
162
+ def: field.def
163
+ };
146
164
  if (field.page) {
147
165
  options.allow = '/';
148
166
  }
149
167
  destination[field.name] = self.apos.util.slugify(self.apos.launder.string(data[field.name], field.def), options);
168
+
150
169
  if (field.page) {
151
170
  if (!(destination[field.name].charAt(0) === '/')) {
152
171
  destination[field.name] = '/' + destination[field.name];
@@ -1217,12 +1236,14 @@ module.exports = {
1217
1236
 
1218
1237
  // all fields in the schema will end up in this variable
1219
1238
  let newSchema = [];
1239
+
1220
1240
  // loop over any groups and orders we want to respect
1221
1241
  _.each(groups, function (group) {
1222
1242
 
1223
1243
  _.each(group.fields, function (field) {
1224
1244
  // find the field we are ordering
1225
1245
  let f = _.find(schema, { name: field });
1246
+
1226
1247
  if (!f) {
1227
1248
  // May have already been migrated due to subclasses re-grouping fields
1228
1249
  f = _.find(newSchema, { name: field });
@@ -1242,6 +1263,7 @@ module.exports = {
1242
1263
  if (fIndex !== -1) {
1243
1264
  newSchema.splice(fIndex, 1);
1244
1265
  }
1266
+
1245
1267
  newSchema.push(f);
1246
1268
 
1247
1269
  // remove the field from the old schema, if that is where we got it from
@@ -2241,7 +2263,7 @@ module.exports = {
2241
2263
  self.apos.util.error(format(s));
2242
2264
  }
2243
2265
  function format(s) {
2244
- return stripIndent`
2266
+ return stripIndents`
2245
2267
  ${options.type} ${options.subtype}, ${field.type} field "${field.name}":
2246
2268
 
2247
2269
  ${s}
@@ -39,11 +39,7 @@ export default {
39
39
  mixins: [ AposInputMixin ],
40
40
  data () {
41
41
  return {
42
- next: this.value.data || {
43
- metaType: 'area',
44
- _id: cuid(),
45
- items: []
46
- },
42
+ next: this.value.data || this.getEmptyValue(),
47
43
  error: false,
48
44
  // This is just meant to be sufficient to prevent unintended collisions
49
45
  // in the UI between id attributes
@@ -66,6 +62,17 @@ export default {
66
62
  }
67
63
  },
68
64
  methods: {
65
+ getEmptyValue() {
66
+ return {
67
+ metaType: 'area',
68
+ _id: cuid(),
69
+ items: []
70
+ };
71
+ },
72
+ watchValue () {
73
+ this.error = this.value.error;
74
+ this.next = this.value.data || this.getEmptyValue();
75
+ },
69
76
  validate(value) {
70
77
  if (this.field.required) {
71
78
  if (!value.items.length) {
@@ -32,13 +32,17 @@ export default {
32
32
  getChoiceId(uid, value) {
33
33
  return uid + value.replace(/\s/g, '');
34
34
  },
35
+ watchValue () {
36
+ this.error = this.value.error;
37
+ this.next = this.value.data || [];
38
+ },
35
39
  validate(values) {
36
- if (!Array.isArray(this.field.choices)) {
40
+ // The choices and values should always be arrays.
41
+ if (!Array.isArray(this.field.choices) || !Array.isArray(values)) {
37
42
  return 'malformed';
38
43
  }
39
44
 
40
- if (this.field.required &&
41
- !Array.isArray(values) && (!values || !values.length)) {
45
+ if (this.field.required && !values.length) {
42
46
  return 'required';
43
47
  }
44
48
 
@@ -23,6 +23,23 @@ export default {
23
23
  debug: i18n.debug,
24
24
  interpolation: {
25
25
  escapeValue: false
26
+ },
27
+ appendNamespaceToMissingKey: true,
28
+ parseMissingKeyHandler (key) {
29
+ // We include namespaces with unrecognized l10n keys using
30
+ // `appendNamespaceToMissingKey: true`. This passes strings containing
31
+ // colons that were never meant to be localized through to the UI.
32
+ //
33
+ // Strings that do not include colons ("Content area") are given the
34
+ // default namespace by i18next ("translation," by default). Here we
35
+ // check if the key starts with that default namespace, meaning it
36
+ // belongs to no other registered namespace, then remove that default
37
+ // namespace before passing this through to be processed and displayed.
38
+ if (key.startsWith(`${this.defaultNS[0]}:`)) {
39
+ return key.slice(this.defaultNS[0].length + 1);
40
+ } else {
41
+ return key;
42
+ }
26
43
  }
27
44
  });
28
45
 
@@ -936,15 +936,6 @@ module.exports = {
936
936
  });
937
937
  },
938
938
 
939
- // If propertyName is _id, then the keys in the returned
940
- // object will be the ids of each object in arr,
941
- // and the values will be the corresponding objects.
942
- // You may index by any property name.
943
-
944
- indexBy: function(arr, propertyName) {
945
- return _.indexBy(arr, propertyName);
946
- },
947
-
948
939
  // Find all the array elements, if any, that have the specified value for
949
940
  // the specified property.
950
941
 
@@ -1061,11 +1052,14 @@ module.exports = {
1061
1052
 
1062
1053
  function groupByArray(items, arrayName) {
1063
1054
  const results = {};
1055
+ // looping over each item in the original array
1064
1056
  _.each(items, function(item) {
1057
+ // looping over each item in the array within the top level item
1065
1058
  _.each(item[arrayName] || [], function(inner) {
1066
1059
  if (!results[inner]) {
1067
1060
  results[inner] = [];
1068
1061
  }
1062
+ // grouping top level items on the sub properties
1069
1063
  results[inner].push(item);
1070
1064
  });
1071
1065
  });
@@ -112,6 +112,7 @@
112
112
  // you are debugging a change and need to test all of the different ways a widget has
113
113
  // been used, or are wondering if you can safely remove one.
114
114
 
115
+ const { stripIndent } = require('common-tags');
115
116
  const _ = require('lodash');
116
117
 
117
118
  module.exports = {
@@ -310,12 +311,20 @@ module.exports = {
310
311
  },
311
312
 
312
313
  // override to add CSS classes to the outer wrapper div of the widget.
314
+ // TODO: Remove in the 4.x major version.
313
315
  getWidgetWrapperClasses(widget) {
316
+ self.apos.util.warnDev(stripIndent`
317
+ The getWidgetWrapperClasses method is deprecated and will be removed in the next
318
+ major version. The method in 3.x simply returns an empty array.`);
314
319
  return [];
315
320
  },
316
321
 
317
322
  // Override to add CSS classes to the div of the widget itself.
323
+ // TODO: Remove in the 4.x major version.
318
324
  getWidgetClasses(widget) {
325
+ self.apos.util.warnDev(stripIndent`
326
+ The getWidgetClasses method is deprecated and will be removed in the next major
327
+ version. The method in 3.x simply returns an empty array.`);
319
328
  return [];
320
329
  }
321
330
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.9.0",
3
+ "version": "3.12.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -28,16 +28,16 @@
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "@apostrophecms/vue-color": "^2.8.2",
31
- "@babel/core": "^7.14.3",
32
- "@babel/preset-env": "^7.14.4",
31
+ "@babel/core": "^7.16.7",
32
+ "@babel/preset-env": "^7.16.7",
33
33
  "@tiptap/extension-highlight": "^2.0.0-beta.13",
34
34
  "@tiptap/extension-link": "^2.0.0-beta.17",
35
35
  "@tiptap/extension-text-align": "^2.0.0-beta.17",
36
36
  "@tiptap/extension-text-style": "^2.0.0-beta.13",
37
- "@tiptap/extension-underline": "^2.0.0-beta.14",
38
- "@tiptap/starter-kit": "^2.0.0-beta.75",
39
- "@tiptap/vue-2": "^2.0.0-beta.34",
40
- "autoprefixer": "^10.2.4",
37
+ "@tiptap/extension-underline": "^2.0.0-beta.22",
38
+ "@tiptap/starter-kit": "^2.0.0-beta.164",
39
+ "@tiptap/vue-2": "^2.0.0-beta.73",
40
+ "autoprefixer": "^10.4.1",
41
41
  "babel-loader": "^8.2.2",
42
42
  "bluebird": "^3.7.2",
43
43
  "body-parser": "^1.18.2",
@@ -68,7 +68,7 @@
68
68
  "he": "^0.5.0",
69
69
  "html-to-text": "^5.1.1",
70
70
  "i18next": "^20.3.2",
71
- "i18next-http-middleware": "^3.1.4",
71
+ "i18next-http-middleware": "^3.1.5",
72
72
  "import-fresh": "^3.3.0",
73
73
  "is-wsl": "^2.2.0",
74
74
  "jsdom": "^17.0.0",
@@ -95,7 +95,7 @@
95
95
  "resolve": "^1.19.0",
96
96
  "resolve-from": "^5.0.0",
97
97
  "sanitize-html": "^2.0.0",
98
- "sass": "^1.34.1",
98
+ "sass": "^1.45.2",
99
99
  "sass-loader": "^10.1.1",
100
100
  "server-destroy": "^1.0.1",
101
101
  "sluggo": "^0.3.0",
@@ -110,7 +110,7 @@
110
110
  "uploadfs": "^1.17.1",
111
111
  "v-tooltip": "^2.0.3",
112
112
  "vue": "^2.6.14",
113
- "vue-click-outside-element": "^1.0.13",
113
+ "vue-click-outside-element": "^1.0.15",
114
114
  "vue-loader": "^15.9.6",
115
115
  "vue-material-design-icons": "~4.12.1",
116
116
  "vue-style-loader": "^4.1.2",
@@ -0,0 +1,4 @@
1
+ {
2
+ "customTestOne": "Custom Test One From Base Type",
3
+ "customTestTwo": "Custom Test Two From Base Type"
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "defaultTestOne": "Default Test One"
3
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ init(self) {
3
+ self.initialized = true;
4
+ }
5
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ example1: {
3
+ options: {
4
+ folderLevelOption: true
5
+ }
6
+ }
7
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "customTestOne": "Custom Test One From Subtype",
3
+ "customTestThree": "Custom Test Three From Subtype"
4
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ i18n: {
3
+ custom: {
4
+ browser: true
5
+ }
6
+ }
7
+ };
package/test/moog.js CHANGED
@@ -284,6 +284,54 @@ describe('moog', function() {
284
284
  assert(myObject.fieldsGroups.basics.fields.includes('five'));
285
285
  });
286
286
 
287
+ it('should order fields with the last option unless the order array overrides', async function() {
288
+ const moog = require('../lib/moog.js')({});
289
+
290
+ moog.define('unorderedObject', {
291
+ cascades: [ 'fields' ],
292
+ fields: {
293
+ add: {
294
+ first: { type: 'string' },
295
+ last: {
296
+ type: 'string',
297
+ last: true
298
+ },
299
+ second: { type: 'string' },
300
+ third: { type: 'string' }
301
+ }
302
+ }
303
+ });
304
+
305
+ moog.define('orderedObject', {
306
+ cascades: [ 'fields' ],
307
+ fields: {
308
+ add: {
309
+ first: { type: 'string' },
310
+ last: {
311
+ type: 'string',
312
+ last: true
313
+ },
314
+ second: { type: 'string' },
315
+ third: { type: 'string' }
316
+ },
317
+ order: [ 'last', 'third', 'second', 'first' ]
318
+ }
319
+ });
320
+ const unordered = await moog.create('unorderedObject', {});
321
+ assert(unordered);
322
+ assert(Object.keys(unordered.fields)[0] === 'first');
323
+ assert(Object.keys(unordered.fields)[1] === 'second');
324
+ assert(Object.keys(unordered.fields)[2] === 'third');
325
+ assert(Object.keys(unordered.fields)[3] === 'last');
326
+
327
+ const ordered = await moog.create('orderedObject', {});
328
+ assert(ordered);
329
+ assert(Object.keys(ordered.fields)[0] === 'last');
330
+ assert(Object.keys(ordered.fields)[1] === 'third');
331
+ assert(Object.keys(ordered.fields)[2] === 'second');
332
+ assert(Object.keys(ordered.fields)[3] === 'first');
333
+ });
334
+
287
335
  // ==================================================
288
336
  // `redefine` AND `isDefined`
289
337
  // ==================================================
@@ -33,9 +33,19 @@ describe('static i18n', function() {
33
33
  'apos-fr': {
34
34
  options: {
35
35
  i18n: {
36
+ // Legacy technique must work
36
37
  ns: 'apostrophe'
37
38
  }
38
39
  }
40
+ },
41
+ // A base class that contributes some namespaced phrases in the new style way (subdirs)
42
+ 'base-type': {
43
+ instantiate: false
44
+ },
45
+ // Also contributes namespaced phrases in the new style way (subdirs)
46
+ // plus default locale phrases in the root i18n folder
47
+ subtype: {
48
+ extend: 'base-type'
39
49
  }
40
50
  }
41
51
  });
@@ -60,4 +70,22 @@ describe('static i18n', function() {
60
70
  assert.strictEqual(apos.task.getReq({ locale: 'fr' }).t('apostrophe:richTextAlignCenter'), 'Aligner Le Centre');
61
71
  });
62
72
 
73
+ it('should fetch default locale phrases from main i18n dir with no i18n option necessary', function() {
74
+ assert.strictEqual(apos.task.getReq().t('defaultTestOne'), 'Default Test One');
75
+ });
76
+
77
+ it('should fetch custom locale phrases from corresponding subdir', function() {
78
+ assert.strictEqual(apos.task.getReq().t('custom:customTestTwo'), 'Custom Test Two From Base Type');
79
+ assert.strictEqual(apos.task.getReq().t('custom:customTestThree'), 'Custom Test Three From Subtype');
80
+ });
81
+
82
+ it('last appearance in inheritance + configuration order wins', function() {
83
+ assert.strictEqual(apos.task.getReq().t('custom:customTestOne'), 'Custom Test One From Subtype');
84
+ });
85
+
86
+ it('should honor the browser: true flag in the i18n section of an index.js file', function() {
87
+ const browserData = apos.i18n.getBrowserData(apos.task.getReq());
88
+ assert.strictEqual(browserData.i18n.en.custom.customTestOne, 'Custom Test One From Subtype');
89
+ });
90
+
63
91
  });
@@ -0,0 +1,32 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('With Nested Module Subdirs', function() {
5
+ this.timeout(t.timeout);
6
+
7
+ let apos;
8
+
9
+ after(function () {
10
+ return t.destroy(apos);
11
+ });
12
+
13
+ /// ///
14
+ // EXISTENCE
15
+ /// ///
16
+
17
+ it('should initialize', async function() {
18
+ apos = await t.create({
19
+ root: module,
20
+ nestedModuleSubdirs: true,
21
+ modules: {
22
+ example1: {}
23
+ }
24
+ });
25
+ assert(apos.modules.example1);
26
+ // With nestedModuleSubdirs switched on, the index.js should be found,
27
+ // and modules.js should be loaded
28
+ assert(apos.modules.example1.options.folderLevelOption);
29
+ assert(apos.modules.example1.initialized);
30
+ });
31
+
32
+ });
@@ -0,0 +1,31 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('Without Nested Module Subdirs', function() {
5
+ this.timeout(t.timeout);
6
+
7
+ let apos;
8
+
9
+ after(function () {
10
+ return t.destroy(apos);
11
+ });
12
+
13
+ /// ///
14
+ // EXISTENCE
15
+ /// ///
16
+
17
+ it('should initialize', async function() {
18
+ apos = await t.create({
19
+ root: module,
20
+ modules: {
21
+ example1: {}
22
+ }
23
+ });
24
+ assert(apos.modules.example1);
25
+ // Should fail because we didn't turn on nestedModuleSubdirs,
26
+ // so the index.js was not found and modules.js was not loaded
27
+ assert(!apos.modules.example1.options.folderLevelOption);
28
+ assert(!apos.modules.example1.initialized);
29
+ });
30
+
31
+ });
package/test-lib/util.js CHANGED
@@ -43,12 +43,14 @@ async function create(options) {
43
43
  };
44
44
  // Automatically configure Express, but not if we're in a special
45
45
  // environment where the default apostrophe modules don't initialize
46
+ // TODO: Remove __testDefaults references in 4.x major version or formalize
47
+ // intended usage with documentation.
46
48
  if (!config.__testDefaults) {
47
49
  config.modules = config.modules || {};
48
50
  const express = config.modules['@apostrophecms/express'] || {};
49
51
  express.options = express.options || {};
50
- // Allow OS to choose open port
51
- express.options.port = null;
52
+ // Allow OS to choose open port if not explicitly set.
53
+ express.options.port = express.options.port || null;
52
54
  express.options.address = express.options.address || 'localhost';
53
55
  express.options.session = express.options.session || {};
54
56
  express.options.session.secret = express.options.session.secret || 'test';
@@ -1,8 +0,0 @@
1
- Please ensure your pull request follows these guidelines:
2
-
3
- - [ ] Link to the related issue with requirements and/or clearly describe 1) what problem this solves and 2) the requirements to review it against.
4
- - [ ] Update the `CHANGELOG.md` file with the added feature or fix. If there is not yet a heading for an upcoming release, add one following the established pattern.
5
- - [ ] Keep the pull request minimal! Break large updates into separate pull requests for individual features/fixes whenever possible. Small requests get reviewed more quickly.
6
- - [ ] All tests must pass, including the linters. Run `npm run lint` to test code linting independently.
7
-
8
- Thanks for contributing!