apostrophe 3.6.0 → 3.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 (71) hide show
  1. package/.eslintrc +4 -0
  2. package/.github/workflows/main.yml +45 -0
  3. package/CHANGELOG.md +92 -3
  4. package/README.md +2 -3
  5. package/index.js +104 -3
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +5 -1
  7. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +15 -10
  8. package/modules/@apostrophecms/asset/index.js +105 -15
  9. package/modules/@apostrophecms/attachment/index.js +1 -4
  10. package/modules/@apostrophecms/db/index.js +5 -6
  11. package/modules/@apostrophecms/doc/index.js +2 -0
  12. package/modules/@apostrophecms/doc-type/index.js +39 -16
  13. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +13 -1
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +0 -1
  15. package/modules/@apostrophecms/i18n/i18n/en.json +23 -4
  16. package/modules/@apostrophecms/i18n/i18n/es.json +1 -2
  17. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
  18. package/modules/@apostrophecms/i18n/i18n/sk.json +3 -4
  19. package/modules/@apostrophecms/i18n/index.js +36 -6
  20. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManager.vue +6 -3
  21. package/modules/@apostrophecms/image-widget/index.js +2 -1
  22. package/modules/@apostrophecms/image-widget/views/widget.html +12 -2
  23. package/modules/@apostrophecms/job/index.js +165 -220
  24. package/modules/@apostrophecms/login/index.js +0 -15
  25. package/modules/@apostrophecms/migration/index.js +1 -1
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +151 -61
  27. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +8 -6
  28. package/modules/@apostrophecms/modal/ui/apos/mixins/AposDocsManagerMixin.js +12 -15
  29. package/modules/@apostrophecms/module/index.js +1 -4
  30. package/modules/@apostrophecms/notification/index.js +116 -8
  31. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +89 -11
  32. package/modules/@apostrophecms/notification/ui/apos/components/TheAposNotifications.vue +1 -1
  33. package/modules/@apostrophecms/page/index.js +84 -52
  34. package/modules/@apostrophecms/page-type/index.js +5 -1
  35. package/modules/@apostrophecms/piece-type/index.js +183 -61
  36. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +180 -50
  37. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -3
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerSelectBox.vue +141 -0
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +35 -6
  40. package/modules/@apostrophecms/schema/index.js +81 -25
  41. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +9 -3
  42. package/modules/@apostrophecms/schema/ui/apos/components/AposInputAttachment.vue +11 -160
  43. package/modules/@apostrophecms/schema/ui/apos/components/AposInputPassword.vue +11 -4
  44. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRange.vue +2 -2
  45. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +0 -2
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposLogo.vue +1 -1
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoIcon.vue +1 -1
  51. package/modules/@apostrophecms/schema/ui/apos/components/AposLogoPadless.vue +1 -1
  52. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +0 -1
  53. package/modules/@apostrophecms/search/index.js +53 -33
  54. package/modules/@apostrophecms/task/index.js +7 -3
  55. package/modules/@apostrophecms/template/index.js +7 -11
  56. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +5 -0
  57. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  58. package/modules/@apostrophecms/ui/ui/apos/components/AposFile.vue +205 -0
  59. package/modules/@apostrophecms/ui/ui/apos/components/AposMinMaxCount.vue +9 -3
  60. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  61. package/modules/@apostrophecms/ui/ui/apos/mixins/AposPublishMixin.js +3 -2
  62. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  63. package/modules/@apostrophecms/util/ui/src/util.js +15 -0
  64. package/modules/@apostrophecms/widget-type/index.js +1 -1
  65. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +5 -19
  66. package/package.json +2 -2
  67. package/test/job.js +224 -0
  68. package/test/pieces.js +34 -0
  69. package/test-lib/util.js +32 -0
  70. package/.circleci/config.yml +0 -94
  71. package/.scratch.md +0 -2
package/.eslintrc CHANGED
@@ -10,6 +10,10 @@
10
10
  "no-var": "error",
11
11
  "no-console": 0,
12
12
  "multiline-ternary": "off",
13
+ "no-unused-vars": [
14
+ "error",
15
+ { "varsIgnorePattern": "^_.", "args": "none" }
16
+ ],
13
17
  "vue/no-deprecated-destroyed-lifecycle": 0,
14
18
  "vue/v-on-event-hyphenation": 1,
15
19
  "vue/custom-event-name-casing": ["warn", "kebab-case"],
@@ -0,0 +1,45 @@
1
+ # This is a basic workflow to help you get started with Actions
2
+
3
+ name: Tests
4
+
5
+ # Controls when the action will run.
6
+ on:
7
+ push:
8
+ branches: [ '*' ]
9
+ pull_request:
10
+ branches: [ '*' ]
11
+
12
+ # Allows you to run this workflow manually from the Actions tab
13
+ workflow_dispatch:
14
+
15
+ # A workflow run is made up of one or more jobs that can run sequentially or in parallel
16
+ jobs:
17
+ # This workflow contains a single job called "build"
18
+ build:
19
+ # The type of runner that the job will run on
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ matrix:
23
+ node-version: [12, 14, 16]
24
+ mongodb-version: [4.2, 4.4, 5.0]
25
+
26
+ # Steps represent a sequence of tasks that will be executed as part of the job
27
+ steps:
28
+ - name: Git checkout
29
+ uses: actions/checkout@v2
30
+
31
+ - name: Use Node.js ${{ matrix.node-version }}
32
+ uses: actions/setup-node@v1
33
+ with:
34
+ node-version: ${{ matrix.node-version }}
35
+
36
+ - name: Start MongoDB
37
+ uses: supercharge/mongodb-github-action@1.3.0
38
+ with:
39
+ mongodb-version: ${{ matrix.mongodb-version }}
40
+
41
+ - run: npm install
42
+
43
+ - run: npm test
44
+ env:
45
+ CI: true
package/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.9.0 - 2021-12-08
4
+
5
+ ### Adds
6
+
7
+ * Developers can now override any Vue component of the ApostropheCMS admin UI by providing a component of the same name in the `ui/apos/components` folder of their own module. This is not always the best approach, see the documentation for details.
8
+ * When running a job, we now trigger the notification before to run the job, this way the progress notification ID is available from the job and the notification can be dismissed if needed.
9
+ * Adds `maxUi`, `maxLabel`, `minUi`, and `minLabel` localization strings for array input and other UI.
10
+
11
+ ### Fixes
12
+
13
+ * Fully removes references to the A2 `self.partial` module method. It appeared only once outside of comments, but was not actually used by the UI. The `self.render` method should be used for simple template rendering.
14
+ * Fixes string interpolation for the confirmation modal when publishing a page that has an unpublished parent page.
15
+ * No more "cannot set headers after they are sent to the client" and "req.res.redirect not defined" messages when handling URLs with extra trailing slashes.
16
+ * The `apos.util.runPlayers` method is not called until all of the widgets in a particular tree of areas and sub-areas have been added to the DOM. This means a parent area widget player will see the expected markup for any sub-widgets when the "Edit" button is clicked.
17
+ * Properly activates the `apostropheI18nDebugPlugin` i18next debugging plugin when using the `APOS_SHOW_I18N` environment variable. The full set of l10n emoji indicators previously available for the UI is now available for template and server-side strings.
18
+ * Actually registers piece types for site search unless the `searchable` option is `false`.
19
+ * Fixes the methods required for the search `index` task.
20
+
21
+ ### Changes
22
+
23
+ * Adds localization keys for the password field component's min and max error messages.
24
+
25
+ ## 3.8.1 - 2021-11-23
26
+
27
+ ### Fixes
28
+
29
+ * The search field of the pieces manager modal works properly. Thanks to [Miro Yovchev](https://github.com/myovchev) for pointing out the issue and providing a solution.
30
+ * Fixes a bug in `AposRichTextWidgetEditor.vue` when a rich text widget was specifically configured with an empty array as the `styles` option. In that case a new empty rich text widget will initiate with an empty paragraph tag.
31
+ * The`fieldsPresent` method that is used with the `presentFieldsOnly` option in doc-type was broken, looking for properties in strings and wasn't returning anything.
32
+
33
+ ## 3.8.0 - 2021-11-15
34
+
35
+ ### Adds
36
+
37
+ * Checkboxes for pieces are back, a main checkbox allows to select all page items. When all pieces on a page are checked, a banner where the user can select all pieces appears. A launder for mongo projections has been added.
38
+ * Registered `batchOperations` on a piece-type will now become buttons in the manager batch operations "more menu" (styled as a kebab icon). Batch operations should include a label, `messages` object, and `modalOptions` for the confirmation modal.
39
+ * `batchOperations` can be grouped into a single button with a menu using the `group` cascade subproperty.
40
+ * `batchOperations` can be conditional with an `if` conditional object. This allows developers to pass a single value or an array of values.
41
+ * Piece types can have `utilityOperations` configured as a top-level cascade property. These operations are made available in the piece manager as new buttons.
42
+ * Notifications may now include an `event` property, which the AposNotification component will emit on mount. The `event` property should be set to an object with `name` (the event name) and optionally `data` (data included with the event emission).
43
+ * Adds support for using the attachments query builder in REST API calls via the query string.
44
+ * Adds contextual menu for pieces, any module extending the piece-type one can add actions in this contextual menu.
45
+ * When clicking on a batch operation, it opens a confirmation modal using modal options from the batch operation, it also works for operations in grouped ones. operations name property has been renamed in action to work with AposContextMenu component.
46
+ * Beginning with this release, a module-specific static asset in your project such as `modules/mymodulename/public/images/bg.png` can always be referenced in your `.scss` and `.css` files as `/modules/mymodulename/images/bg.png`, even if assets are actually being deployed to S3, CDNs, etc. Note that `public` and `ui/public` module subdirectories have separate functions. See the documentation for more information.
47
+ * Adds AposFile.vue component to abstract file dropzone UI, uses it in AposInputAttachment, and uses it in the confirmation modal for pieces import.
48
+ * Optionally add `dimensionAttrs` option to image widget, which sets width & height attributes to optimize for Cumulative Layout Shift. Thank you to [Qiao Lin](https://github.com/qclin) for the contribution.
49
+
50
+ ### Fixes
51
+
52
+ * The `apos.util.attachmentUrl` method now works correctly. To facilitate that, `apos.uploadsUrl` is now populated browser-side at all times as the frontend logic originally expected. For backwards compatibility `apos.attachment.uploadsUrl` is still populated when logged in.
53
+ * Widget players are now prevented from being played twice by the implementing vue component.
54
+
55
+ ### Changes
56
+ * Removes Apostrophe 2 documentation and UI configuration from the `@apostrophecms/job` module. These options were not yet in use for A3.
57
+ * Renames methods and removes unsupported routes in the `@apostrophecms/job` module that were not yet in use. This was not done lightly, but specifically because of the minimal likelihood that they were in use in project code given the lack of UI support.
58
+ * The deprecated `cancel` route was removed and will likely be replaced at a later date.
59
+ * `run` was renamed `runBatch` as its purpose is specifically to run processes on a "batch selected" array of pieces or pages.
60
+ * `runNonBatch` was renamed to `run` as it is the more generic job-running method. It is likely that `runBatch` will eventually be refactored to use this method.
61
+ * The `good` and `bad` methods are renamed `success` and `failure`, respectively. The expected methods used in the `run` method were similarly renamed. They still increment job document properties called `good` and `bad`.
62
+ * Comments out the unused `batchSimpleRoute` methods in the page and piece-type modules to avoid usage before they are fully implemented.
63
+ * Optionally add `dimensionAttrs` option to image widget, which sets width & height attributes to optimize for Cumulative Layout Shift.
64
+ * Temporarily removes `npm audit` from our automated tests because of a sub-dependency of uploadfs that doesn't actually cause a security vulnerability for apostrophe.
65
+
66
+ ## 3.7.0 - 2021-10-28
67
+
68
+ ### Adds
69
+
70
+ * Schema select field choices can now be populated by a server side function, like an API call. Set the `choices` property to a method name of the calling module. That function should take a single argument of `req`, and return an array of objects with `label` and `value` properties. The function can be async and will be awaited.
71
+ * Apostrophe now has built-in support for the Node.js cluster module. If the `APOS_CLUSTER_PROCESSES` environment variable is set to a number, that number of child processes are forked, sharing the same listening port. If the variable is set to `0`, one process is forked for each CPU core, with a minimum of `2` to provide availability during restarts. If the variable is set to a negative number, that number is added to the number of CPU cores, e.g. `-1` is a good way to reserve one core for MongoDB if it is running on the same server. This is for production use only (`NODE_ENV=production`). If a child process fails it is restarted automatically.
72
+
73
+ ### Fixes
74
+
75
+ * Prevents double-escaping interpolated localization strings in the UI.
76
+ * Rich text editor style labels are now run through a localization method to get the translated strings from their l10n keys.
77
+ * Fixes README Node version requirement (Node 12+).
78
+ * The text alignment buttons now work immediately in a new rich text widget. Previously they worked only after manually setting a style or refreshing the page. Thanks to Michelin for their support of this fix.
79
+ * Users can now activate the built-in date and time editing popups of modern browsers when using the `date` and `time` schema field types.
80
+ * Developers can now `require` their project `app.js` file in the Node.js REPL for debugging and inspection. Thanks to [Matthew Francis Brunetti](https://github.com/zenflow).
81
+ * If a static text phrase is unavailable in both the current locale and the default locale, Apostrophe will always fall back to the `en` locale as a last resort, which ensures the admin UI works if it has not been translated.
82
+ * Developers can now `require` their project `app.js` in the Node.js REPL for debugging and inspection
83
+ * Ensure array field items have valid _id prop before storing. Thanks to Thanks to [Matthew Francis Brunetti](https://github.com/zenflow).
84
+
85
+ ### Changes
86
+
87
+ * In 3.x, `relationship` fields have an optional `builders` property, which replaces `filters` from 2.x, and within that an optional `project` property, which replaces `projection` from 2.x (to match MongoDB's `cursor.project`). Prior to this release leaving the old syntax in place could lead to severe performance problems due to a lack of projections. Starting with this release the 2.x syntax results in an error at startup to help the developer correct their code.
88
+ * The `className` option from the widget options in a rich text area field is now also applied to the rich text editor itself, for a consistently WYSIWYG appearance when editing and when viewing. Thanks to [Max Mulatz](https://github.com/klappradla) for this contribution.
89
+ * Adds deprecation notes to doc module `afterLoad` events, which are deprecated.
90
+ * Removes unused `afterLogin` method in the login module.
91
+
3
92
  ## 3.6.0 - 2021-10-13
4
93
 
5
94
  ### Adds
@@ -8,7 +97,8 @@
8
97
  * Adds 'no-search' modifier to relationship fields as a UI simplification option.
9
98
  * Fields can now have their own `modifiers` array. This is combined with the schema modifiers, allowing for finer grained control of field rendering.
10
99
  * Adds a Slovak localization file. Activate the `sk` locale to use this. Many thanks to [Michael Huna](https://github.com/Miselrkba) for the contribution.
11
- * Adds a Spanish localization file. Activate the `es` locale to use this. Many thanks to [egonzalezg9](https://github.com/egonzalezg9) for the contribution.
100
+ * Adds a Spanish localization file. Activate the `es` locale to use this. Many thanks to [Eugenio Gonzalez](https://github.com/egonzalezg9) for the contribution.
101
+ * Adds a Brazilian Portuguese localization file. Activate the `pt-BR` locale to use this. Many thanks to [Pietro Rutzen](https://github.com/pietro-rutzen) for the contribution.
12
102
 
13
103
  ### Fixes
14
104
 
@@ -159,8 +249,7 @@ No changes. Publishing to correctly mark the latest 3.x release as "latest" in n
159
249
  can include the widget on the clipboard, a special Clipboard widget will appear in area's Add UI. This works across pages as well.
160
250
 
161
251
  ### Changes
162
- * Apostrophe's Global's UI (the @apostrophecms/global singleton has moved from the admin bar's content controls to
163
- the admin utility tray under a cog icon.
252
+ * Apostrophe's Global's UI (the @apostrophecms/global singleton has moved from the admin bar's content controls to the admin utility tray under a cog icon.
164
253
  * The context bar's document Edit button, which was a cog icon, has been rolled into the doc's context menu.
165
254
 
166
255
  ## 3.1.3 - 2021-07-16
package/README.md CHANGED
@@ -1,5 +1,4 @@
1
-
2
- [![CircleCI](https://circleci.com/gh/apostrophecms/apostrophe/tree/main.svg?style=svg)](https://circleci.com/gh/apostrophecms/apostrophe/tree/main)
1
+ ![Unit Tests](https://github.com/apostrophecms/apostrophe/actions/workflows/main.yml/badge.svg)
3
2
  [![Chat on Discord](https://img.shields.io/discord/517772094482677790.svg)](https://chat.apostrophecms.org)
4
3
 
5
4
  <p align="center">
@@ -43,7 +42,7 @@ We recommend installing the following with [Homebrew](https://brew.sh/) on macOS
43
42
 
44
43
  | Software | Minimum Version | Notes
45
44
  | ------------- | ------------- | -----
46
- | Node.js | 10.x | Or better
45
+ | Node.js | 12.x | Or better
47
46
  | npm | 6.x | Or better
48
47
  | MongoDB | 3.6 | Or better
49
48
  | Imagemagick | Any | Faster image uploads, GIF support (optional)
package/index.js CHANGED
@@ -2,11 +2,43 @@ const path = require('path');
2
2
  const _ = require('lodash');
3
3
  const argv = require('boring')({ end: true });
4
4
  const fs = require('fs');
5
+ const { stripIndent } = require('common-tags');
6
+ const cluster = require('cluster');
7
+ const { cpus } = require('os');
8
+ const process = require('process');
5
9
  const npmResolve = require('resolve');
10
+
6
11
  let defaults = require('./defaults.js');
7
- const { stripIndent } = require('common-tags');
8
12
 
9
- // **Awaiting the Apostrophe function is optional**
13
+ // ## Top-level options
14
+ //
15
+ // `cluster`
16
+ //
17
+ // If set to `true`, Apostrophe will spawn as many processes as
18
+ // there are CPU cores on the server, or a minimum of 2, and balance
19
+ // incoming connections among them. This ensures availability while one
20
+ // process is restarting due to a crash and also increases scalability if
21
+ // the server has multiple CPU cores.
22
+ //
23
+ // If set to an object with a `processes` property, that many
24
+ // processes are started. If `processes` is 0 or a negative number,
25
+ // it is added to the number of CPU cores reported by the server.
26
+ // Notably, `-1` can be a good way to reserve one CPU core for MongoDB
27
+ // in a single-server deployment.
28
+ //
29
+ // However when in cluster mode no fewer than 2 processes will be
30
+ // started as there is no availability benefit without at least 2.
31
+ //
32
+ // If a child process exits with a failure status code it will be
33
+ // restarted. However, if it exits in less than 20 seconds after
34
+ // startup there will be a 20 second delay to avoid flooding logs
35
+ // and pinning the CPU.
36
+ //
37
+ // Alternatively the `APOS_CLUSTER_PROCESSES` environment variable
38
+ // can be set to a number, which will effectively set the cluster
39
+ // option to `cluster: { processes: n }`.
40
+ //
41
+ // ## Awaiting the Apostrophe function
10
42
  //
11
43
  // The apos function is async, but in typical cases you do not
12
44
  // need to await it. If you simply call it, Apostrophe will
@@ -21,8 +53,63 @@ const { stripIndent } = require('common-tags');
21
53
  // To avoid exiting on errors, pass the `exit: false` option.
22
54
  // This can option also can be used to allow awaiting a command line
23
55
  // task, as they also normally exit on completion.
56
+ //
57
+ // If `options.cluster` is truthy, the function quickly resolves to
58
+ // `null` in the primary process. In the child process it resolves as
59
+ // documented above.
24
60
 
25
61
  module.exports = async function(options) {
62
+ const guardTime = 20000;
63
+ if (process.env.APOS_CLUSTER_PROCESSES) {
64
+ options.cluster = {
65
+ processes: parseInt(process.env.APOS_CLUSTER_PROCESSES)
66
+ };
67
+ }
68
+ if (options.cluster && (process.env.NODE_ENV !== 'production')) {
69
+ console.log('NODE_ENV is not set to production, disabling cluster mode');
70
+ options.cluster = false;
71
+ }
72
+ if (options.cluster && !argv._.length) {
73
+ // For bc with node 14 and below we need to check both
74
+ if (cluster.isPrimary || cluster.isMaster) {
75
+ let processes = options.cluster.processes || cpus().length;
76
+ if (processes <= 0) {
77
+ processes = cpus().length + processes;
78
+ }
79
+ let capped = '';
80
+ if (processes > cpus().length) {
81
+ processes = cpus().length;
82
+ capped = ' (capped to number of CPU cores)';
83
+ }
84
+ if (processes < 2) {
85
+ processes = 2;
86
+ if (capped) {
87
+ capped = ' (less than 2 cores, capped to minimum of 2)';
88
+ } else {
89
+ capped = ' (using minimum of 2)';
90
+ }
91
+ }
92
+ console.log(`Starting ${processes} cluster child processes${capped}`);
93
+ for (let i = 0; i < processes; i++) {
94
+ clusterFork();
95
+ }
96
+ cluster.on('exit', (worker, code, signal) => {
97
+ if (code !== 0) {
98
+ if ((Date.now() - worker.bornAt) < guardTime) {
99
+ console.error(`Worker process ${worker.process.pid} failed in ${seconds(Date.now() - worker.bornAt)}, waiting ${seconds(guardTime)} before restart`);
100
+ setTimeout(() => {
101
+ respawn(worker);
102
+ }, guardTime);
103
+ } else {
104
+ respawn(worker);
105
+ }
106
+ }
107
+ });
108
+ return null;
109
+ } else {
110
+ console.log(`Cluster worker ${process.pid} started`);
111
+ }
112
+ }
26
113
 
27
114
  // The core is not a true moog object but it must look enough like one
28
115
  // to participate as an async event emitter
@@ -174,7 +261,7 @@ module.exports = async function(options) {
174
261
  function getRoot() {
175
262
  let _module = module;
176
263
  let m = _module;
177
- while (m.parent) {
264
+ while (m.parent && m.parent.filename) {
178
265
  // The test file is the root as far as we are concerned,
179
266
  // not mocha itself
180
267
  if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
@@ -518,3 +605,17 @@ module.exports.bundle = {
518
605
  modules: abstractClasses.concat(_.keys(defaults.modules)),
519
606
  directory: 'modules'
520
607
  };
608
+
609
+ function seconds(msec) {
610
+ return (Math.round(msec / 100) / 10) + ' seconds';
611
+ }
612
+
613
+ function clusterFork() {
614
+ const worker = cluster.fork();
615
+ worker.bornAt = Date.now();
616
+ }
617
+
618
+ function respawn(worker) {
619
+ console.error(`Respawning worker process ${worker.process.pid}`);
620
+ clusterFork();
621
+ }
@@ -463,7 +463,11 @@ export default {
463
463
  }
464
464
  },
465
465
  async onContentChanged(e) {
466
- if (e.doc && (e.doc._id === this.context._id)) {
466
+
467
+ if (
468
+ (e.doc && (e.doc._id === this.context._id)) ||
469
+ (e.docIds && e.docIds.includes(this.context._id))
470
+ ) {
467
471
  if (e.action === 'delete') {
468
472
  if (!this.contextStack.length) {
469
473
  // With the current page gone, we need to move to safe ground
@@ -3,27 +3,32 @@ import { klona } from 'klona';
3
3
 
4
4
  export default function() {
5
5
 
6
+ let widgetsRendering = 0;
7
+
6
8
  createWidgetClipboardApp();
7
9
 
8
- prepareAreas();
10
+ createAreaApps();
9
11
 
10
12
  document.documentElement.style.setProperty('--a-widget-margin', apos.ui.widgetMargin);
11
13
 
14
+ apos.bus.$on('widget-rendering', function() {
15
+ widgetsRendering++;
16
+ });
17
+
12
18
  apos.bus.$on('widget-rendered', function() {
13
- prepareAreas();
19
+ widgetsRendering--;
20
+ createAreaAppsAndRunPlayersIfDone();
14
21
  });
22
+
15
23
  apos.bus.$on('refreshed', function() {
16
- prepareAreas();
24
+ createAreaAppsAndRunPlayersIfDone();
17
25
  });
18
26
 
19
- function prepareAreas() {
20
- // Doing this first allows markup to be captured for the editor
21
- // before players alter it
27
+ function createAreaAppsAndRunPlayersIfDone() {
22
28
  createAreaApps();
23
- // Even though we invoke the player directly from
24
- // the widget mixin used for editable widgets, we still have to
25
- // call runPlayers eventually to account for any foreign area widgets
26
- apos.util.runPlayers();
29
+ if (widgetsRendering === 0) {
30
+ apos.util.runPlayers();
31
+ }
27
32
  }
28
33
 
29
34
  function createAreaApps() {
@@ -73,6 +73,12 @@ module.exports = {
73
73
  await fs.remove(buildDir);
74
74
  await fs.mkdirp(buildDir);
75
75
 
76
+ // Static asset files in `public` subdirs of each module are copied
77
+ // to the same relative path `/public/apos-frontend/namespace/modules/modulename`.
78
+ // Inherited files are also copied, with the deepest subclass overriding in the
79
+ // event of a conflict
80
+ await moduleOverrides(`${bundleDir}/modules`, 'public');
81
+
76
82
  for (const [ name, options ] of Object.entries(self.builds)) {
77
83
  // If the option is not present always rebuild everything
78
84
  let rebuild = argv && !argv['check-apos-build'];
@@ -122,12 +128,17 @@ module.exports = {
122
128
  // just `public` and `apos`) by examining those specified as
123
129
  // targets for the various builds
124
130
  const scenes = [ ...new Set(Object.values(self.builds).map(options => options.scenes).flat()) ];
125
- let bundles = [];
131
+ let deployFiles = [];
126
132
  for (const scene of scenes) {
127
- bundles = [ ...bundles, ...merge(scene) ];
133
+ deployFiles = [ ...deployFiles, ...merge(scene) ];
128
134
  }
129
-
130
- await deploy(bundles);
135
+ // enumerate public assets and include them in deployment if appropriate
136
+ const publicAssets = glob.sync('modules/**/*', {
137
+ cwd: bundleDir,
138
+ mark: true
139
+ }).filter(match => !match.endsWith('/'));
140
+ deployFiles = [ ...deployFiles, ...publicAssets ];
141
+ await deploy(deployFiles);
131
142
 
132
143
  if (process.env.APOS_BUNDLE_ANALYZER) {
133
144
  return new Promise((resolve, reject) => {
@@ -162,7 +173,7 @@ module.exports = {
162
173
  for (const name of names) {
163
174
  const moduleDir = `${modulesDir}/${name}`;
164
175
  for (const dir of directories[name]) {
165
- const srcDir = `${dir}/ui/${source}`;
176
+ const srcDir = `${dir}/${source}`;
166
177
  if (fs.existsSync(srcDir)) {
167
178
  await fs.copy(srcDir, moduleDir);
168
179
  }
@@ -176,12 +187,15 @@ module.exports = {
176
187
  }));
177
188
  const modulesDir = `${buildDir}/${name}/modules`;
178
189
  const source = options.source || name;
179
- await moduleOverrides(modulesDir, source);
190
+ await moduleOverrides(modulesDir, `ui/${source}`);
180
191
 
181
192
  let iconImports, componentImports, tiptapExtensionImports, appImports, indexJsImports, indexSassImports;
182
193
  if (options.apos) {
183
194
  iconImports = getIcons();
184
- componentImports = getImports(`${source}/components`, '*.vue', { registerComponents: true });
195
+ componentImports = getImports(`${source}/components`, '*.vue', {
196
+ registerComponents: true,
197
+ importLastVersion: true
198
+ });
185
199
  tiptapExtensionImports = getImports(`${source}/tiptap-extensions`, '*.js', { registerTiptapExtensions: true });
186
200
  appImports = getImports(`${source}/apps`, '*.js', {
187
201
  invokeApps: true,
@@ -233,7 +247,8 @@ module.exports = {
233
247
  // Remove previous build artifacts, as some pipelines won't build all artifacts
234
248
  // if there is no input, and we don't want stale output in the bundle
235
249
  fs.removeSync(`${bundleDir}/${outputFilename}`);
236
- fs.removeSync(`${bundleDir}/${outputFilename}`.replace(/\.js$/, '.css'));
250
+ const cssPath = `${bundleDir}/${outputFilename}`.replace(/\.js$/, '.css');
251
+ fs.removeSync(cssPath);
237
252
  await Promise.promisify(webpackModule)(require(`./lib/webpack/${name}/webpack.config`)(
238
253
  {
239
254
  importFile,
@@ -243,6 +258,11 @@ module.exports = {
243
258
  },
244
259
  self.apos
245
260
  ));
261
+ if (fs.existsSync(cssPath)) {
262
+ fs.writeFileSync(cssPath, self.filterCss(fs.readFileSync(cssPath, 'utf8'), {
263
+ modulesPrefix: `${self.getAssetBaseUrl()}/modules`
264
+ }));
265
+ }
246
266
  if (options.apos) {
247
267
  const now = Date.now().toString();
248
268
  fs.writeFileSync(`${bundleDir}/${name}-build-timestamp.txt`, now);
@@ -269,7 +289,9 @@ module.exports = {
269
289
  const publicImports = getImports(name, '*.css', { });
270
290
  fs.writeFileSync(`${bundleDir}/${name}-build.css`,
271
291
  publicImports.paths.map(path => {
272
- return fs.readFileSync(path, 'utf8');
292
+ return self.filterCss(fs.readFileSync(path, 'utf8'), {
293
+ modulesPrefix: `${self.getAssetBaseUrl()}/modules`
294
+ });
273
295
  }).join('\n')
274
296
  );
275
297
  }
@@ -358,7 +380,19 @@ module.exports = {
358
380
  return [ jsModules, jsNoModules, css ];
359
381
  }
360
382
 
361
- async function deploy(bundles) {
383
+ // If NODE_ENV is production, this function will copy the given
384
+ // array of asset files from `${bundleDir}/${file}` to
385
+ // the same relative location in the appropriate release subdirectory in
386
+ // `/public/apos-frontend/releases`, or in `/apos-frontend/releases` in
387
+ // uploadfs if `APOS_UPLOADFS_ASSETS` is present.
388
+ //
389
+ // If NODE_ENV is not production this function does nothing and
390
+ // the assets are served directly from `/public/apos-frontend/${file}`.
391
+ //
392
+ // The namespace (e.g. default) should be part of each filename given.
393
+ // A leading slash should NOT be passed.
394
+
395
+ async function deploy(files) {
362
396
  if (process.env.NODE_ENV !== 'production') {
363
397
  return;
364
398
  }
@@ -373,17 +407,23 @@ module.exports = {
373
407
  } else {
374
408
  // The right choice with Docker if uploadfs is just the local filesystem
375
409
  // mapped to a volume (a Docker build step can't access that)
376
- copyIn = fs.copyFile;
410
+ copyIn = fsCopyIn;
377
411
  releaseDir = `${self.apos.rootDir}/public/apos-frontend/releases/${releaseId}/${namespace}`;
378
412
  await fs.mkdirp(releaseDir);
379
413
  }
380
- for (const bundle of bundles) {
381
- const src = `${bundleDir}/${bundle}`;
382
- await copyIn(src, `${releaseDir}/${bundle}`);
414
+ for (const file of files) {
415
+ const src = `${bundleDir}/${file}`;
416
+ await copyIn(src, `${releaseDir}/${file}`);
383
417
  await fs.remove(src);
384
418
  }
385
419
  }
386
420
 
421
+ async function fsCopyIn(from, to) {
422
+ const base = path.dirname(to);
423
+ await fs.mkdirp(base);
424
+ return fs.copyFile(from, to);
425
+ }
426
+
387
427
  function getImports(folder, pattern, options) {
388
428
  let components = [];
389
429
  const seen = {};
@@ -404,6 +444,25 @@ module.exports = {
404
444
  paths: []
405
445
  };
406
446
 
447
+ if (options.importLastVersion) {
448
+ // Reverse the list so we can easily find the last configured import
449
+ // of a given component, allowing "improve" modules to win over
450
+ // the originals when shipping an override of a Vue component
451
+ // with the same name, and filter out earlier versions
452
+ components.reverse();
453
+ const seen = new Set();
454
+ components = components.filter(component => {
455
+ const name = getComponentName(component, options);
456
+ if (seen.has(name)) {
457
+ return false;
458
+ }
459
+ seen.add(name);
460
+ return true;
461
+ });
462
+ // Put the components back in their original order
463
+ components.reverse();
464
+ }
465
+
407
466
  components.forEach((component, i) => {
408
467
  if (options.requireDefaultExport) {
409
468
  if (!fs.readFileSync(component, 'utf8').match(/export[\s\n]+default/)) {
@@ -416,7 +475,7 @@ module.exports = {
416
475
  }
417
476
  }
418
477
  const jsFilename = JSON.stringify(component);
419
- const name = require('path').basename(component).replace(/\.\w+/, '') + (options.enumerateImports ? `_${i}` : '');
478
+ const name = getComponentName(component, options, i);
420
479
  const jsName = JSON.stringify(name);
421
480
  output.paths.push(component);
422
481
  const importCode = `
@@ -455,6 +514,10 @@ module.exports = {
455
514
 
456
515
  return pkgTimestamp > parseInt(timestamp);
457
516
  }
517
+
518
+ function getComponentName(component, options, i) {
519
+ return require('path').basename(component).replace(/\.\w+/, '') + (options.enumerateImports ? `_${i}` : '');
520
+ }
458
521
  }
459
522
  }
460
523
  };
@@ -650,6 +713,30 @@ module.exports = {
650
713
  if (!self.options.es5) {
651
714
  delete self.builds['src-es5'];
652
715
  }
716
+ },
717
+ // Filter the given css performing any necessary transformations,
718
+ // such as support for the /modules path regardless of where
719
+ // static assets are actually deployed
720
+ filterCss(css, { modulesPrefix }) {
721
+ return self.filterCssUrls(css, url => {
722
+ if (url.startsWith('/modules')) {
723
+ return url.replace('/modules', modulesPrefix);
724
+ }
725
+ return url;
726
+ });
727
+ },
728
+ // Run all URLs in CSS through a filter function
729
+ filterCssUrls(css, filter) {
730
+ css = css.replace(/url\(([^'"].*?)\)/g, function(s, url) {
731
+ return 'url(' + filter(url) + ')';
732
+ });
733
+ css = css.replace(/url\("([^"]+?)"\)/g, function(s, url) {
734
+ return 'url("' + filter(url) + '")';
735
+ });
736
+ css = css.replace(/url\('([^']+?)'\)/g, function(s, url) {
737
+ return 'url(\'' + filter(url) + '\')';
738
+ });
739
+ return css;
653
740
  }
654
741
  };
655
742
  },
@@ -667,6 +754,9 @@ module.exports = {
667
754
  }
668
755
  const script = fs.readFileSync(path.join(__dirname, '/lib/refresh-on-restart.js'), 'utf8');
669
756
  return self.apos.template.safe(`<script data-apos-refresh-on-restart="${self.action}/restart-id">\n${script}</script>`);
757
+ },
758
+ url(path) {
759
+ return `${self.getAssetBaseUrl()}${path}`;
670
760
  }
671
761
  };
672
762
  },
@@ -230,7 +230,6 @@ module.exports = {
230
230
  addFieldType() {
231
231
  self.apos.schema.addFieldType({
232
232
  name: self.name,
233
- partial: self.fieldTypePartial,
234
233
  convert: self.convert,
235
234
  index: self.index,
236
235
  register: self.register
@@ -274,9 +273,6 @@ module.exports = {
274
273
  await self.db.replaceOne({ _id: info._id }, info);
275
274
  object[field.name] = info;
276
275
  },
277
- fieldTypePartial(data) {
278
- return self.partial('attachment', data);
279
- },
280
276
  index(value, field, texts) {
281
277
  const silent = field.silent === undefined ? true : field.silent;
282
278
  texts.push({
@@ -1038,6 +1034,7 @@ module.exports = {
1038
1034
  action: self.action,
1039
1035
  fileGroups: self.fileGroups,
1040
1036
  name: self.name,
1037
+ // for bc
1041
1038
  uploadsUrl: self.uploadfs.getUrl(),
1042
1039
  croppable: self.croppable,
1043
1040
  sized: self.sized