apostrophe 3.21.0 → 3.21.1-alpha.2022060701

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## UNRELEASED
4
+
5
+ ### Adds
6
+
7
+ * Possibility to pass options to webpack extensions from any module.
8
+
9
+ ### Fixes
10
+
11
+ * Fix a Webpack cache issue leading to modules symlinked in `node_modules` not being rebuilt.
12
+ * Fixes login maximum attempts error message that wasn't showing the plural when lockoutMinutes is more than 1.
13
+ * Fixes the text color of the current array item's slat label in the array editor modal
14
+ * Fixes the maximum width of an array item's slat label so as to not obscure the Remove button in narrow viewports
15
+
16
+ ### Changes
17
+
18
+ * If an array field's titleField option is set to a select field, use the selected option's label as the slat label rather it's value.
19
+ * Disable the slat controls of the attachment component while uploading.
20
+ * Fixes bug when re-attaching the same file won't trigger an upload.
21
+ * AposSlat now fully respects the disabled state.
22
+
23
+ ## 3.21.1 (2022-06-04)
24
+
25
+ ### Fixes
26
+
27
+ * Work around backwards compatibility break in `sass` module by pinning to `sass` `1.50.x` while we investigate. If you saw the error `RangeError: Invalid value: Not in inclusive range 0..145: -1` you can now fix that by upgrading with `npm update`. If it does not immediately clear up the issue in development, try `node app @apostrophecms/asset:clear-cache`.
28
+
3
29
  ## 3.21.0 (2022-05-25)
4
30
 
5
31
  ### Adds
@@ -49,13 +49,16 @@ module.exports = {
49
49
  self.configureBuilds();
50
50
  self.initUploadfs();
51
51
 
52
- const { extensions, verifiedBundles } = await getWebpackExtensions({
52
+ const {
53
+ extensions = {}, extensionOptions = {}, verifiedBundles = {}
54
+ } = await getWebpackExtensions({
53
55
  getMetadata: self.apos.synth.getMetadata,
54
56
  modulesToInstantiate: self.apos.modulesToBeInstantiated()
55
57
  });
56
58
 
57
59
  self.extraBundles = fillExtraBundles(verifiedBundles);
58
60
  self.webpackExtensions = extensions;
61
+ self.webpackExtensionOptions = extensionOptions;
59
62
  self.verifiedBundles = verifiedBundles;
60
63
  self.buildWatcherEnable = process.env.APOS_ASSET_WATCH !== '0' && self.options.watch !== false;
61
64
  self.buildWatcherDebounceMs = parseInt(self.options.watchDebounceMs || 1000, 10);
@@ -125,6 +128,7 @@ module.exports = {
125
128
  const buildDir = `${self.apos.rootDir}/apos-build/${namespace}`;
126
129
  const bundleDir = `${self.apos.rootDir}/public/apos-frontend/${namespace}`;
127
130
  const modulesToInstantiate = self.apos.modulesToBeInstantiated();
131
+ const symLinkModules = await findNodeModulesSymlinks(self.apos.npmRootDir);
128
132
  // Make it clear if builds should detect changes
129
133
  const detectChanges = typeof argv.changes === 'string';
130
134
  // Remove invalid changes. `argv.changes` is a comma separated list of relative
@@ -353,9 +357,16 @@ module.exports = {
353
357
  ? webpackMerge(webpackInstanceConfig, ...Object.values(self.webpackExtensions))
354
358
  : webpackInstanceConfig;
355
359
 
356
- // Inject the cache location at the end - we need the merged
357
- const cacheMeta = await computeCacheMeta(name, webpackInstanceConfigMerged);
360
+ // Inject the cache location at the end - we need the merged config
361
+ const cacheMeta = await computeCacheMeta(name, webpackInstanceConfigMerged, symLinkModules);
358
362
  webpackInstanceConfigMerged.cache.cacheLocation = cacheMeta.location;
363
+ // Exclude symlinked modules from the cache managedPaths, no other way for now
364
+ // https://github.com/webpack/webpack/issues/12112
365
+ if (cacheMeta.managedPathsRegex) {
366
+ webpackInstanceConfigMerged.snapshot = {
367
+ managedPaths: [ cacheMeta.managedPathsRegex ]
368
+ };
369
+ }
359
370
 
360
371
  const result = await webpack(webpackInstanceConfigMerged);
361
372
  await writeCacheMeta(name, cacheMeta);
@@ -716,7 +727,7 @@ module.exports = {
716
727
  // but it can be overridden by an APOS_ASSET_CACHE environment.
717
728
  // In order to compute an accurate hash, this helper needs
718
729
  // the final, merged webpack configuration.
719
- async function computeCacheMeta(name, webpackConfig) {
730
+ async function computeCacheMeta(name, webpackConfig, symLinkModules) {
720
731
  const cacheBase = self.getCacheBasePath();
721
732
 
722
733
  if (!packageLockContentCached) {
@@ -759,10 +770,22 @@ module.exports = {
759
770
  );
760
771
  const location = path.resolve(cacheBase, hash);
761
772
 
773
+ // Retrieve symlinkModules and convert them to managedPaths regex rule
774
+ let managedPathsRegex;
775
+ if (symLinkModules.length > 0) {
776
+ const regex = symLinkModules
777
+ .map(m => self.apos.util.regExpQuote(m))
778
+ .join('|');
779
+ managedPathsRegex = new RegExp(
780
+ '^(.+?[\\/]node_modules)[\\/]((?!' + regex + ')).*[\\/]*'
781
+ );
782
+ }
783
+
762
784
  return {
763
785
  base: cacheBase,
764
786
  hash,
765
- location
787
+ location,
788
+ managedPathsRegex
766
789
  };
767
790
  }
768
791
 
@@ -3,7 +3,8 @@ const path = require('path');
3
3
 
4
4
  module.exports = {
5
5
  checkModulesWebpackConfig(modules, t) {
6
- const allowedProperties = [ 'extensions', 'bundles' ];
6
+ const allowedProperties = [ 'extensions', 'extensionOptions', 'bundles' ];
7
+
7
8
  for (const mod of Object.values(modules)) {
8
9
  const webpackConfig = mod.__meta.webpack[mod.__meta.name];
9
10
 
@@ -18,7 +19,8 @@ module.exports = {
18
19
  Object.keys(webpackConfig).some((prop) => !allowedProperties.includes(prop))
19
20
  ) {
20
21
  const error = t('apostrophe:assetWebpackConfigWarning', {
21
- module: mod.__meta.name
22
+ module: mod.__meta.name,
23
+ properties: allowedProperties.join(', ')
22
24
  });
23
25
 
24
26
  throw new Error(error);
@@ -51,7 +53,9 @@ module.exports = {
51
53
  const modulesMeta = modulesToInstantiate
52
54
  .map((name) => getMetadata(name));
53
55
 
54
- const { extensions, foundBundles } = getModulesWebpackConfigs(
56
+ const {
57
+ extensions, extensionOptions, foundBundles
58
+ } = getModulesWebpackConfigs(
55
59
  modulesMeta
56
60
  );
57
61
 
@@ -59,6 +63,7 @@ module.exports = {
59
63
 
60
64
  return {
61
65
  extensions,
66
+ extensionOptions,
62
67
  verifiedBundles
63
68
  };
64
69
  },
@@ -173,42 +178,58 @@ async function findSymlinks(where, sub = '') {
173
178
  }
174
179
 
175
180
  function getModulesWebpackConfigs (modulesMeta) {
176
- const { extensions, bundles } = modulesMeta.reduce((acc, meta) => {
181
+ const {
182
+ extensions, extensionOptions, bundles
183
+ } = modulesMeta.reduce((modulesAcc, meta) => {
177
184
  const { webpack, __meta } = meta;
178
185
 
179
186
  const configs = formatConfigs(__meta.chain, webpack);
180
187
 
181
188
  if (!configs.length) {
182
- return acc;
189
+ return modulesAcc;
183
190
  }
184
191
 
185
- const moduleBundles = configs.reduce((acc, conf) => {
186
- return {
192
+ const reduce = (list, prop) => {
193
+ return list.reduce((acc, cur) => ({
187
194
  ...acc,
188
- ...conf.bundles
189
- };
190
- }, {});
195
+ ...cur[prop] || {}
196
+ }), {});
197
+ };
198
+
199
+ const extensionOptions = configs.reduce((acc, { extensionOptions = {} }) => {
200
+ return [
201
+ ...acc,
202
+ extensionOptions
203
+ ];
204
+ }, []);
191
205
 
192
206
  return {
193
207
  extensions: {
194
- ...acc.extensions,
195
- ...configs.reduce((acc, config) => ({
196
- ...acc,
197
- ...config.extensions
198
- }), {})
208
+ ...modulesAcc.extensions,
209
+ ...reduce(configs, 'extensions')
199
210
  },
211
+ extensionOptions: [
212
+ ...modulesAcc.extensionOptions,
213
+ ...extensionOptions
214
+ ],
200
215
  bundles: {
201
- ...acc.bundles,
202
- ...moduleBundles
216
+ ...modulesAcc.bundles,
217
+ ...reduce(configs, 'bundles')
203
218
  }
204
219
  };
205
220
  }, {
206
221
  extensions: {},
222
+ extensionOptions: [],
207
223
  bundles: {}
208
224
  });
209
225
 
226
+ const formattedOptions = formatExtensionsOptions(extensionOptions);
227
+
228
+ const { exts, options } = fillExtensionsOptions(extensions, formattedOptions);
229
+
210
230
  return {
211
- extensions,
231
+ extensions: exts,
232
+ extensionOptions: options,
212
233
  foundBundles: flattenBundles(bundles)
213
234
  };
214
235
  };
@@ -223,8 +244,8 @@ async function verifyBundlesEntryPoints (bundles) {
223
244
 
224
245
  return {
225
246
  bundleName,
226
- ...jsFileExists && { jsPath: jsPath },
227
- ...scssFileExists && { scssPath: scssPath }
247
+ ...jsFileExists && { jsPath },
248
+ ...scssFileExists && { scssPath }
228
249
  };
229
250
  });
230
251
 
@@ -263,10 +284,13 @@ function formatConfigs (chain, webpackConfigs) {
263
284
  return null;
264
285
  }
265
286
 
266
- const { bundles = {}, extensions = {} } = config;
287
+ const {
288
+ bundles = {}, extensions = {}, extensionOptions = {}
289
+ } = config;
267
290
 
268
291
  return {
269
292
  extensions,
293
+ extensionOptions,
270
294
  bundles: {
271
295
  [name]: {
272
296
  bundleNames: Object.keys(bundles),
@@ -289,3 +313,66 @@ function flattenBundles (bundles) {
289
313
  ];
290
314
  }, []);
291
315
  }
316
+
317
+ function fillExtensionsOptions (extensions, options) {
318
+ const isObject = (val) => val &&
319
+ typeof val === 'object' && !Array.isArray(val);
320
+
321
+ return Object.entries(extensions).reduce((acc, [ name, config ]) => {
322
+ if (isObject(config)) {
323
+ return {
324
+ ...acc,
325
+ exts: {
326
+ ...acc.exts,
327
+ [name]: config
328
+ }
329
+ };
330
+ }
331
+
332
+ if (typeof config !== 'function') {
333
+ return acc;
334
+ }
335
+
336
+ const computedOptions = computeOptions(options[name] || [], isObject);
337
+
338
+ return {
339
+ exts: {
340
+ ...acc.exts,
341
+ [name]: config(computedOptions)
342
+ },
343
+ options: {
344
+ ...acc.options,
345
+ [name]: computedOptions
346
+ }
347
+ };
348
+ }, {
349
+ exts: {},
350
+ options: {}
351
+ });
352
+
353
+ function computeOptions (options, isObject) {
354
+ return options.reduce((acc, option) => {
355
+ if (!isObject(option) && typeof option !== 'function') {
356
+ return acc;
357
+ }
358
+
359
+ return {
360
+ ...acc,
361
+ ...isObject(option) ? option : option(acc)
362
+ };
363
+ }, {});
364
+ }
365
+ }
366
+
367
+ function formatExtensionsOptions (options) {
368
+ return options.reduce(
369
+ (acc, current) => {
370
+ return {
371
+ ...acc,
372
+ ...Object.fromEntries(Object.entries(current)
373
+ .map(([ ext, option ]) => [ ext, [ option, ...(acc[ext] || []) ] ]))
374
+ };
375
+ },
376
+ {}
377
+ );
378
+ }
@@ -33,7 +33,7 @@
33
33
  "aspectRatioWarning": "The aspect ratio cannot be changed for this widget",
34
34
  "assetTypeBuildComplete": "👍 {{ label }} is complete!",
35
35
  "assetTypeBuilding": "🧑‍💻 Building the {{ label }}...",
36
- "assetWebpackConfigWarning": "⚠️ In the module {{ module }}, your webpack config is incorrect. It must be an object and should contain only two properties extensions and bundles.",
36
+ "assetWebpackConfigWarning": "⚠️ In the module {{ module }}, your webpack config is incorrect. It must be an object and should contain only the properties: {{ properties }}.",
37
37
  "assetWebpackBundlesWarning": "⚠️ In the module {{ module }} your webpack config is incorrect. Each bundle can only have one property 'templates' that must be an array of strings.",
38
38
  "assetWebpackCacheCleared": "Build cache cleared.",
39
39
  "assetBuildWatchStarted": "Watching for UI changes...",
@@ -121,6 +121,7 @@
121
121
  "errorCount": "{{ count }} error remaining",
122
122
  "errorCount_plural": "{{ count }} errors remaining",
123
123
  "errorCreatingNewContent": "Error while creating new, empty content.",
124
+ "errorFetchingTitleFieldChoicesByMethod": "An error occurred while fetching the choices for the titleField {{ name }}",
124
125
  "errorWhileRestoring": "An error occurred while restoring the previously published version.",
125
126
  "errorWhileUnpublishing": "An error occurred while unpublishing the document.",
126
127
  "errorWhileArchiving": "An error occurred while moving the document to the archive.",
@@ -202,7 +202,9 @@ module.exports = {
202
202
  .checkLoginAttempts(user.username, loginNamespace);
203
203
 
204
204
  if (reached) {
205
- throw self.apos.error('invalid', req.t('apostrophe:loginMaxAttemptsReached'));
205
+ throw self.apos.error('invalid', req.t('apostrophe:loginMaxAttemptsReached', {
206
+ count: self.options.throttle.lockoutMinutes
207
+ }));
206
208
  }
207
209
 
208
210
  try {
@@ -121,6 +121,7 @@ export default {
121
121
  type: 'overlay',
122
122
  showModal: false
123
123
  },
124
+ titleFieldChoices: null,
124
125
  // If we don't clone, then we're making
125
126
  // permanent modifications whether the user
126
127
  // clicks save or not
@@ -210,6 +211,8 @@ export default {
210
211
  await this.nextTick();
211
212
  aposSchema.scrollFieldIntoView(name);
212
213
  }
214
+ this.titleFieldChoices = await this.getTitleFieldChoices();
215
+
213
216
  },
214
217
  methods: {
215
218
  async select(_id) {
@@ -354,7 +357,19 @@ export default {
354
357
  label(item) {
355
358
  let candidate;
356
359
  if (this.field.titleField) {
360
+
361
+ // Initial field value
357
362
  candidate = get(item, this.field.titleField);
363
+
364
+ // If the titleField references a select input, use the
365
+ // select label as the slat label, rather than the value.
366
+ if (this.titleFieldChoices) {
367
+ const choice = this.titleFieldChoices.find(choice => choice.value === candidate);
368
+ if (choice && choice.label) {
369
+ candidate = choice.label;
370
+ }
371
+ }
372
+
358
373
  } else if (this.schema.find(field => field.name === 'title') && (item.title !== undefined)) {
359
374
  candidate = item.title;
360
375
  }
@@ -374,6 +389,42 @@ export default {
374
389
  title: this.label(item)
375
390
  }));
376
391
  return result;
392
+ },
393
+ async getTitleFieldChoices() {
394
+ // If the titleField references a select input, get it's choices
395
+ // to use as labels for the slat UI
396
+
397
+ let choices = null;
398
+ const titleField = this.schema.find(field => field.name === this.field.titleField);
399
+
400
+ // The titleField is a select
401
+ if (titleField?.choices) {
402
+
403
+ // Choices are provided by a method
404
+ if (typeof titleField.choices === 'string') {
405
+ const action = `${this.moduleOptions.action}/choices`;
406
+ try {
407
+ const result = await apos.http.get(
408
+ action,
409
+ {
410
+ qs: {
411
+ fieldId: titleField._id
412
+ }
413
+ }
414
+ );
415
+ if (result && result.choices) {
416
+ choices = result.choices;
417
+ }
418
+ } catch (e) {
419
+ console.error(this.$t('apostrophe:errorFetchingTitleFieldChoicesByMethod', { name: titleField.name }));
420
+ }
421
+
422
+ // Choices are a normal, hardcoded array
423
+ } else if (Array.isArray(titleField.choices)) {
424
+ choices = titleField.choices;
425
+ }
426
+ }
427
+ return choices;
377
428
  }
378
429
  }
379
430
  };
@@ -24,6 +24,7 @@
24
24
  </template>
25
25
  </p>
26
26
  <input
27
+ ref="uploadField"
27
28
  type="file"
28
29
  class="apos-sr-only"
29
30
  :disabled="disabled || fileOrAttachment"
@@ -35,7 +36,7 @@
35
36
  <AposSlatList
36
37
  :value="[fileOrAttachment]"
37
38
  @input="update"
38
- :disabled="readOnly"
39
+ :disabled="attachmentDisabled"
39
40
  />
40
41
  </div>
41
42
  </div>
@@ -77,10 +78,13 @@ export default {
77
78
  };
78
79
  },
79
80
  computed: {
80
- fileOrAttachment () {
81
+ fileOrAttachment() {
81
82
  return this.selectedFile || this.attachment;
82
83
  },
83
- messages () {
84
+ attachmentDisabled() {
85
+ return this.uploading || this.readOnly || this.disabled;
86
+ },
87
+ messages() {
84
88
  const msgs = {
85
89
  primary: 'Drop a file here or',
86
90
  highlighted: 'click to open the file explorer'
@@ -100,6 +104,7 @@ export default {
100
104
  async uploadFile ({ target, dataTransfer }) {
101
105
  this.dragging = false;
102
106
  const [ file ] = target.files ? target.files : (dataTransfer.files || []);
107
+ this.resetField();
103
108
 
104
109
  const extension = file.name.split('.').pop();
105
110
  const allowedFile = await this.checkFileGroup(`.${extension}`);
@@ -117,6 +122,9 @@ export default {
117
122
 
118
123
  this.$emit('upload-file', file);
119
124
  },
125
+ resetField() {
126
+ this.$refs.uploadField.value = null;
127
+ },
120
128
  dragHandler (event) {
121
129
  event.preventDefault();
122
130
 
@@ -8,7 +8,8 @@
8
8
  :class="{
9
9
  'apos-is-engaged': engaged,
10
10
  'apos-is-only-child': slatCount === 1,
11
- 'apos-is-selected': selected
11
+ 'apos-is-selected': selected,
12
+ 'apos-is-disabled': disabled,
12
13
  }"
13
14
  @keydown.prevent.space="toggleEngage"
14
15
  @keydown.prevent.enter="toggleEngage"
@@ -33,6 +34,7 @@
33
34
  @item-clicked="$emit('item-clicked', item)"
34
35
  menu-placement="bottom-start"
35
36
  menu-offset="40, 10"
37
+ disabled="disabled"
36
38
  />
37
39
  <AposButton
38
40
  class="apos-slat__editor-btn"
@@ -46,6 +48,7 @@
46
48
  :icon-only="true"
47
49
  :modifiers="['inline']"
48
50
  @click="$emit('item-clicked', item)"
51
+ disabled="disabled"
49
52
  />
50
53
  <a
51
54
  class="apos-slat__control apos-slat__control--view"
@@ -222,6 +225,12 @@ export default {
222
225
  color: var(--a-text-primary);
223
226
  @include apos-transition();
224
227
 
228
+ &.apos-is-disabled {
229
+ .apos-slat__control--view {
230
+ pointer-events: none;
231
+ }
232
+ }
233
+
225
234
  &:hover {
226
235
  cursor: grab;
227
236
  background-color: var(--a-base-7);
@@ -262,6 +271,9 @@ export default {
262
271
  &:hover {
263
272
  background-color: var(--a-primary-dark-10);
264
273
  }
274
+ .apos-slat__label {
275
+ color: var(--a-white);
276
+ }
265
277
  }
266
278
 
267
279
  .apos-slat-list__item--disabled {
@@ -274,6 +286,7 @@ export default {
274
286
  .apos-slat__main {
275
287
  display: flex;
276
288
  align-items: center;
289
+ max-width: 75%;
277
290
  & ::v-deep .trigger {
278
291
  /* This gets inline positioned and has doesn't provide an extra class to beef up, sorry */
279
292
  /* stylelint-disable-next-line declaration-no-important */
@@ -285,7 +298,6 @@ export default {
285
298
  @include type-small;
286
299
  overflow: hidden;
287
300
  margin-left: 5px;
288
- max-width: 220px;
289
301
  white-space: nowrap;
290
302
  text-overflow: ellipsis;
291
303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.21.0",
3
+ "version": "3.21.1-alpha.2022060701",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -103,7 +103,7 @@
103
103
  "resolve": "^1.19.0",
104
104
  "resolve-from": "^5.0.0",
105
105
  "sanitize-html": "^2.0.0",
106
- "sass": "^1.50.1",
106
+ "sass": "1.52.1",
107
107
  "sass-loader": "^10.1.1",
108
108
  "server-destroy": "^1.0.1",
109
109
  "sluggo": "^0.3.0",
@@ -132,6 +132,7 @@
132
132
  "@babel/eslint-parser": "^7.17.0",
133
133
  "eslint": "^7.25.0",
134
134
  "eslint-config-apostrophe": "^3.4.0",
135
+ "eslint-plugin-n": "^15.2.1",
135
136
  "eslint-plugin-node": "^11.1.0",
136
137
  "eslint-plugin-vue": "^7.9.0",
137
138
  "mocha": "^9.1.2",
package/test/assets.js CHANGED
@@ -145,7 +145,9 @@ describe('Assets', function() {
145
145
  modules
146
146
  });
147
147
 
148
- const { extensions, verifiedBundles } = await getWebpackExtensions({
148
+ const {
149
+ extensions, verifiedBundles
150
+ } = await getWebpackExtensions({
149
151
  name: 'src',
150
152
  getMetadata: apos.synth.getMetadata,
151
153
  modulesToInstantiate: apos.modulesToBeInstantiated()
@@ -465,6 +467,7 @@ describe('Assets', function() {
465
467
  });
466
468
 
467
469
  const { meta, folders } = getCacheMeta();
470
+
468
471
  assert.equal(folders.length, 11);
469
472
  assert.equal(Object.keys(meta).length, 11);
470
473
  assert(!meta['default:apos_3']);
@@ -594,7 +597,7 @@ describe('Assets', function() {
594
597
  const assetPathAposJs = path.join(rootPath, 'test/public/apos-frontend/default/apos-module-bundle.js');
595
598
  const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css');
596
599
  const assetContentJs = 'export default () => {};\n';
597
- const assetContentScss = '.default-page {color:red};\n';
600
+ const assetContentScss = '.default-page {color:red;}\n';
598
601
  // Resurrect the default assets content if test has failed
599
602
  fs.writeFileSync(assetPathJs, assetContentJs, 'utf8');
600
603
  fs.writeFileSync(assetPathScss, assetContentScss, 'utf8');
@@ -633,7 +636,7 @@ describe('Assets', function() {
633
636
  );
634
637
  fs.writeFileSync(
635
638
  assetPathScss,
636
- '.default-page-watcher-test-src{color:red};\n',
639
+ '.default-page-watcher-test-src{color:red;}\n',
637
640
  'utf8'
638
641
  );
639
642
 
@@ -719,7 +722,7 @@ describe('Assets', function() {
719
722
  const assetPathAposJs = path.join(rootPath, 'test/public/apos-frontend/default/apos-module-bundle.js');
720
723
  const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css');
721
724
  const assetContentJs = 'export default () => {};\n';
722
- const assetContentScss = '.default-page {color:red};\n';
725
+ const assetContentScss = '.default-page {color:red;}\n';
723
726
  // Resurrect the default assets content if test has failed
724
727
  fs.writeFileSync(assetPathJs, assetContentJs, 'utf8');
725
728
  fs.writeFileSync(assetPathCss, assetContentScss, 'utf8');
@@ -758,7 +761,7 @@ describe('Assets', function() {
758
761
  );
759
762
  fs.writeFileSync(
760
763
  assetPathCss,
761
- '.default-page-watcher-test-public{color:red};\n',
764
+ '.default-page-watcher-test-public{color:red;}\n',
762
765
  'utf8'
763
766
  );
764
767
 
@@ -934,7 +937,7 @@ describe('Assets', function() {
934
937
  const assetPathScss = path.join(rootPath, 'test/modules/default-page/ui/src/index.scss');
935
938
  const assetPathPublicCss = path.join(rootPath, 'test/public/apos-frontend/default/public-bundle.css');
936
939
  const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css');
937
- const assetContentScss = '.default-page {color:red};\n';
940
+ const assetContentScss = '.default-page {color:red;}\n';
938
941
  // Resurrect the default assets content if test has failed
939
942
  fs.writeFileSync(assetPathScss, assetContentScss, 'utf8');
940
943
 
@@ -987,7 +990,7 @@ describe('Assets', function() {
987
990
  // * modify assets and recover
988
991
  fs.writeFileSync(
989
992
  assetPathScss,
990
- '.default-page-watcher-test-recover{color:red};\n',
993
+ '.default-page-watcher-test-recover{color:red;}\n',
991
994
  'utf8'
992
995
  );
993
996
 
@@ -1060,7 +1063,7 @@ describe('Assets', function() {
1060
1063
  const assetPathAposJs = path.join(rootPath, 'test/public/apos-frontend/default/apos-module-bundle.js');
1061
1064
  const assetPathAposCss = path.join(rootPath, 'test/public/apos-frontend/default/apos-bundle.css');
1062
1065
  const assetContentJs = 'export default () => {};\n';
1063
- const assetContentScss = '.default-page {color:red};\n';
1066
+ const assetContentScss = '.default-page {color:red;}\n';
1064
1067
  // Resurrect the default assets content if test has failed
1065
1068
  fs.writeFileSync(assetPathJs, assetContentJs, 'utf8');
1066
1069
  fs.writeFileSync(assetPathScss, assetContentScss, 'utf8');
@@ -1099,7 +1102,7 @@ describe('Assets', function() {
1099
1102
  );
1100
1103
  fs.writeFileSync(
1101
1104
  assetPathScss,
1102
- '.default-page-watcher-test-src{color:red};\n',
1105
+ '.default-page-watcher-test-src{color:red;}\n',
1103
1106
  'utf8'
1104
1107
  );
1105
1108
 
@@ -1215,6 +1218,7 @@ describe('Assets', function() {
1215
1218
  );
1216
1219
  await Promise.delay(300);
1217
1220
  }
1221
+
1218
1222
  await retryAssertTrue(
1219
1223
  async () => (await fs.readFile(assetPathPublic, 'utf8')).match(/bundle-page-watcher-test-3/),
1220
1224
  'Unable to verify public asset rebuilding by the watcher',
@@ -1245,8 +1249,8 @@ describe('Assets', function() {
1245
1249
  10000
1246
1250
  );
1247
1251
  await retryAssertTrue(
1248
- () => timesRebuilt === 2,
1249
- `Expected to rebuild 2 times, got ${timesRebuilt}`,
1252
+ () => timesRebuilt === 3,
1253
+ `Expected to rebuild 3 times, got ${timesRebuilt}`,
1250
1254
  100,
1251
1255
  5000
1252
1256
  );
@@ -1257,7 +1261,6 @@ describe('Assets', function() {
1257
1261
  });
1258
1262
 
1259
1263
  it('should be able to setup the debounce time', async function() {
1260
- await t.destroy(apos);
1261
1264
 
1262
1265
  apos = await t.create({
1263
1266
  root: module,
@@ -1346,6 +1349,69 @@ describe('Assets', function() {
1346
1349
  });
1347
1350
  assert(!apos.asset.buildWatcher);
1348
1351
  process.env.NODE_ENV = 'development';
1352
+
1353
+ await t.destroy(apos);
1354
+ });
1355
+
1356
+ it('should pass the right options to webpack extensions from all modules', async function() {
1357
+ const { extConfig1, extConfig2 } = getWebpackConfigsForExtensionOptions();
1358
+
1359
+ apos = await t.create({
1360
+ root: module,
1361
+ modules: {
1362
+ 'test-widget': {
1363
+ extend: '@apostrophecms/widget-type',
1364
+ webpack: extConfig1
1365
+ },
1366
+ test: {
1367
+ extend: '@apostrophecms/piece-type',
1368
+ webpack: extConfig2
1369
+ }
1370
+ }
1371
+ });
1372
+
1373
+ const {
1374
+ extensions, extensionOptions
1375
+ } = await getWebpackExtensions({
1376
+ name: 'src',
1377
+ getMetadata: apos.synth.getMetadata,
1378
+ modulesToInstantiate: apos.modulesToBeInstantiated()
1379
+ });
1380
+
1381
+ assertWebpackExtensionOptions(extensions, extensionOptions);
1382
+
1383
+ await t.destroy(apos);
1384
+ });
1385
+
1386
+ it('should allow two modules extending each others to pass options to the same webpack extension', async function() {
1387
+ const { extConfig1, extConfig2 } = getWebpackConfigsForExtensionOptions();
1388
+
1389
+ apos = await t.create({
1390
+ root: module,
1391
+ modules: {
1392
+ 'test-widget': {
1393
+ extend: '@apostrophecms/widget-type',
1394
+ instantiate: false,
1395
+ webpack: extConfig1
1396
+ },
1397
+ 'test-widget-special': {
1398
+ extend: 'test-widget',
1399
+ webpack: extConfig2
1400
+ }
1401
+ }
1402
+ });
1403
+
1404
+ assert(!apos.modules['test-widget']);
1405
+
1406
+ const {
1407
+ extensions, extensionOptions
1408
+ } = await getWebpackExtensions({
1409
+ name: 'src',
1410
+ getMetadata: apos.synth.getMetadata,
1411
+ modulesToInstantiate: apos.modulesToBeInstantiated()
1412
+ });
1413
+
1414
+ assertWebpackExtensionOptions(extensions, extensionOptions);
1349
1415
  });
1350
1416
  });
1351
1417
 
@@ -1441,3 +1507,82 @@ function loadUtils () {
1441
1507
  retryAssertTrue
1442
1508
  };
1443
1509
  }
1510
+
1511
+ function assertWebpackExtensionOptions(extensions, extensionOptions) {
1512
+ assert(extensions.ext1.mode === 'production');
1513
+ assert(extensions.ext1.resolve.alias.testAlias === 'test-path');
1514
+ assert(extensions.ext1.resolve.alias.ext1 === 'ext1-path');
1515
+
1516
+ assert(extensions.ext2.resolve.alias.ext2 === 'ext2-path');
1517
+ assert(extensions.ext2.resolve.alias.newAlias1 === 'new-path1');
1518
+ assert(extensions.ext2.resolve.alias.newAlias2 === 'new-path2');
1519
+
1520
+ assert(extensionOptions.ext1.mode === 'production');
1521
+ assert(extensionOptions.ext1.alias.testAlias === 'test-path');
1522
+
1523
+ assert(extensionOptions.ext2.alias.newAlias1 === 'new-path1');
1524
+ assert(extensionOptions.ext2.alias.newAlias2 === 'new-path2');
1525
+ }
1526
+
1527
+ function getWebpackConfigsForExtensionOptions () {
1528
+ return {
1529
+ extConfig1: {
1530
+ extensions: {
1531
+ ext1 ({ mode, alias = {} }) {
1532
+ return {
1533
+ mode,
1534
+ resolve: {
1535
+ alias: {
1536
+ ext1: 'ext1-path',
1537
+ ...alias
1538
+ }
1539
+ }
1540
+ };
1541
+ },
1542
+ ext2 ({ alias = {} }) {
1543
+ return {
1544
+ resolve: {
1545
+ alias: {
1546
+ ext2: 'ext2-path',
1547
+ ...alias
1548
+ }
1549
+ }
1550
+ };
1551
+ }
1552
+ },
1553
+ extensionOptions: {
1554
+ ext1: {
1555
+ mode: 'production'
1556
+ },
1557
+ ext2 (options) {
1558
+ return {
1559
+ alias: {
1560
+ newAlias1: 'new-path1',
1561
+ ...options.alias || {}
1562
+ }
1563
+ };
1564
+ }
1565
+ }
1566
+ },
1567
+ extConfig2: {
1568
+ extensionOptions: {
1569
+ ext1(options) {
1570
+ return {
1571
+ alias: {
1572
+ ...options.alias || {},
1573
+ testAlias: 'test-path'
1574
+ }
1575
+ };
1576
+ },
1577
+ ext2 (options) {
1578
+ return {
1579
+ alias: {
1580
+ newAlias2: 'new-path2',
1581
+ ...options.alias || {}
1582
+ }
1583
+ };
1584
+ }
1585
+ }
1586
+ }
1587
+ };
1588
+ }
@@ -1 +1 @@
1
- .default-page {color:red};
1
+ .default-page {color:red;}
@@ -1 +1 @@
1
- .default-page {color:red};
1
+ .default-page {color:red;}
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "test",
3
+ "dependencies": {
4
+ "apostrophe": "^3.0.0",
5
+ "improve-global": "1.0.0",
6
+ "improve-piece-type": "1.0.0"
7
+ },
8
+ "devDependencies": {
9
+ "test-bundle": "1.0.0"
10
+ }
11
+ }