apostrophe 3.18.0 → 3.19.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 (26) hide show
  1. package/.stylelintrc +2 -1
  2. package/CHANGELOG.md +23 -1
  3. package/lib/moog-require.js +7 -1
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +1 -1
  5. package/modules/@apostrophecms/asset/index.js +281 -24
  6. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +7 -0
  7. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.config.js +7 -0
  8. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +10 -1
  9. package/modules/@apostrophecms/i18n/i18n/en.json +3 -0
  10. package/modules/@apostrophecms/i18n/i18n/es.json +1 -0
  11. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +1 -0
  12. package/modules/@apostrophecms/i18n/i18n/sk.json +1 -0
  13. package/modules/@apostrophecms/image/ui/apos/components/AposImageCropper.vue +2 -2
  14. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +5 -5
  15. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +3 -2
  16. package/modules/@apostrophecms/schema/index.js +2 -1123
  17. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +1139 -0
  18. package/modules/@apostrophecms/schema/ui/apos/components/AposInputDateAndTime.vue +115 -0
  19. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +21 -1
  20. package/modules/@apostrophecms/ui/ui/apos/components/AposSpinner.vue +7 -7
  21. package/modules/@apostrophecms/ui/ui/apos/components/AposToggle.vue +68 -0
  22. package/package.json +2 -3
  23. package/test/assets.js +462 -4
  24. package/test/schemas.js +25 -0
  25. package/test-lib/test.js +12 -0
  26. package/test-lib/util.js +4 -3
package/test/assets.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const t = require('../test-lib/test.js');
2
- const assert = require('assert');
2
+ const assert = require('assert').strict;
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
+ const Promise = require('bluebird');
5
6
 
6
7
  const {
7
8
  checkModulesWebpackConfig,
@@ -13,11 +14,17 @@ let apos;
13
14
 
14
15
  const badModules = {
15
16
  badModuleConfig: {
17
+ options: {
18
+ ignoreNoCodeWarning: true
19
+ },
16
20
  webpack: {
17
21
  badprop: {}
18
22
  }
19
23
  },
20
24
  badModuleConfig2: {
25
+ options: {
26
+ ignoreNoCodeWarning: true
27
+ },
21
28
  webpack: []
22
29
  }
23
30
  };
@@ -78,19 +85,24 @@ const modules = {
78
85
  describe('Assets', function() {
79
86
  const {
80
87
  publicFolderPath,
88
+ cacheFolderPath,
81
89
  getScriptMarkup,
82
90
  getStylesheetMarkup,
83
91
  expectedBundlesNames,
84
92
  deleteBuiltFolders,
85
- allBundlesAreIncluded
93
+ allBundlesAreIncluded,
94
+ removeCache,
95
+ getCacheMeta,
96
+ retryAssertTrue
86
97
  } = loadUtils();
87
98
 
88
99
  after(async function() {
89
100
  await deleteBuiltFolders(publicFolderPath, true);
101
+ await removeCache();
90
102
  return t.destroy(apos);
91
103
  });
92
104
 
93
- this.timeout(90000);
105
+ this.timeout(5 * 60 * 1000);
94
106
 
95
107
  it('should exist on the apos object', async function() {
96
108
  apos = await t.create({
@@ -284,10 +296,407 @@ describe('Assets', function() {
284
296
  assert(bundlePage.includes(getScriptMarkup('extra2-module-bundle', 'module')));
285
297
  assert(bundlePage.includes(getScriptMarkup('extra2-nomodule-bundle', 'nomodule')));
286
298
  });
299
+
300
+ it('should build with cache and gain performance', async function() {
301
+ await t.destroy(apos);
302
+ await removeCache();
303
+ await removeCache(cacheFolderPath.replace('/webpack-cache', '/changed'));
304
+
305
+ const es5Modules = {
306
+ ...modules,
307
+ '@apostrophecms/asset': {
308
+ options: {
309
+ es5: true
310
+ }
311
+ }
312
+ };
313
+
314
+ apos = await t.create({
315
+ root: module,
316
+ modules: es5Modules
317
+ });
318
+ assert.throws(() => fs.readdirSync(cacheFolderPath), {
319
+ code: 'ENOENT'
320
+ });
321
+
322
+ let startTime;
323
+
324
+ // Cold run
325
+ startTime = Date.now();
326
+ await apos.asset.tasks.build.task({
327
+ 'check-apos-build': false
328
+ });
329
+ const execTime = Date.now() - startTime;
330
+ const { meta, folders } = getCacheMeta();
331
+ assert.equal(folders.length, 3);
332
+ assert.equal(Object.keys(meta).length, 3);
333
+ assert(meta['default:apos']);
334
+ assert(meta['default:src']);
335
+ assert(meta['default:src-es5']);
336
+
337
+ // Cache
338
+ startTime = Date.now();
339
+ await apos.asset.tasks.build.task({
340
+ 'check-apos-build': false
341
+ });
342
+ const execTimeCached = Date.now() - startTime;
343
+ const { meta: meta2, folders: folders2 } = getCacheMeta();
344
+ assert.equal(folders2.length, 3);
345
+ assert.equal(Object.keys(meta2).length, 3);
346
+ assert(meta2['default:apos']);
347
+ assert(meta2['default:src']);
348
+ assert(meta2['default:src-es5']);
349
+
350
+ // Expect at least 40% gain, in reallity it should be 50+
351
+ const gain = (execTime - execTimeCached) / execTime * 100;
352
+ assert(gain >= 20, `Expected gain >=20%, got ${gain}%`);
353
+
354
+ // Modification times
355
+ assert(meta['default:apos'].mdate);
356
+ assert(meta2['default:apos'].mdate);
357
+ assert(meta['default:src'].mdate);
358
+ assert(meta2['default:src'].mdate);
359
+ assert(meta['default:src-es5'].mdate);
360
+ assert(meta2['default:src-es5'].mdate);
361
+ assert(
362
+ new Date(meta['default:apos'].mdate) < new Date(meta2['default:apos'].mdate)
363
+ );
364
+ assert.equal(
365
+ new Date(meta2['default:apos'].mdate).toISOString(),
366
+ fs.statSync(meta2['default:apos'].location).mtime.toISOString()
367
+ );
368
+ assert(
369
+ new Date(meta['default:src'].mdate) < new Date(meta2['default:src'].mdate)
370
+ );
371
+ assert.equal(
372
+ new Date(meta2['default:src'].mdate).toISOString(),
373
+ fs.statSync(meta2['default:src'].location).mtime.toISOString()
374
+ );
375
+ assert(
376
+ new Date(meta['default:src-es5'].mdate) < new Date(meta2['default:src-es5'].mdate)
377
+ );
378
+ assert.equal(
379
+ new Date(meta2['default:src-es5'].mdate).toISOString(),
380
+ fs.statSync(meta2['default:src-es5'].location).mtime.toISOString()
381
+ );
382
+ });
383
+
384
+ it('should invalidate build cache when namespace changes', async function() {
385
+ process.env.APOS_DEBUG_NAMESPACE = 'test';
386
+ await apos.asset.tasks.build.task({
387
+ 'check-apos-build': false
388
+ });
389
+ const { meta, folders } = getCacheMeta();
390
+ assert.equal(folders.length, 6);
391
+ assert.equal(Object.keys(meta).length, 6);
392
+ assert(meta['test:apos']);
393
+ assert(meta['test:src']);
394
+ assert(meta['test:src-es5']);
395
+ assert(meta['default:apos']);
396
+ assert(meta['default:src']);
397
+ assert(meta['default:src-es5']);
398
+ delete process.env.APOS_DEBUG_NAMESPACE;
399
+ });
400
+
401
+ it('should invalidate build cache when packages change', async function() {
402
+ await t.destroy(apos);
403
+ const lock = require('./package-lock.json');
404
+ assert.equal(lock.version, 'current');
405
+ lock.version = 'new';
406
+ fs.writeFileSync(
407
+ path.join(process.cwd(), 'test/package-lock.json'),
408
+ JSON.stringify(lock, null, ' '),
409
+ 'utf8'
410
+ );
411
+ const es5Modules = {
412
+ ...modules,
413
+ '@apostrophecms/asset': {
414
+ options: {
415
+ es5: true
416
+ }
417
+ }
418
+ };
419
+
420
+ apos = await t.create({
421
+ root: module,
422
+ modules: es5Modules
423
+ });
424
+ await apos.asset.tasks.build.task({
425
+ 'check-apos-build': false
426
+ });
427
+
428
+ const { meta, folders } = getCacheMeta();
429
+ assert.equal(folders.length, 9);
430
+ assert.equal(Object.keys(meta).length, 9);
431
+ assert(meta['default:apos_2']);
432
+ assert(meta['default:src_2']);
433
+ assert(meta['default:src-es5_2']);
434
+ });
435
+
436
+ it('should invalidate build cache when configuration changes', async function() {
437
+ await t.destroy(apos);
438
+ const es5Modules = {
439
+ ...modules,
440
+ 'bundle-page': {
441
+ webpack: {
442
+ extensions: {
443
+ ext1: {
444
+ resolve: {
445
+ alias: {
446
+ ext1: 'changed'
447
+ }
448
+ }
449
+ }
450
+ }
451
+ }
452
+ },
453
+ '@apostrophecms/asset': {
454
+ options: {
455
+ es5: true
456
+ }
457
+ }
458
+ };
459
+ apos = await t.create({
460
+ root: module,
461
+ modules: es5Modules
462
+ });
463
+ await apos.asset.tasks.build.task({
464
+ 'check-apos-build': false
465
+ });
466
+
467
+ const { meta, folders } = getCacheMeta();
468
+ assert.equal(folders.length, 11);
469
+ assert.equal(Object.keys(meta).length, 11);
470
+ assert(!meta['default:apos_3']);
471
+ assert(meta['default:src_3']);
472
+ assert(meta['default:src-es5_3']);
473
+ });
474
+
475
+ it('should clear build cache', async function() {
476
+ const cacheFolders = fs.readdirSync(cacheFolderPath, 'utf8');
477
+ assert(cacheFolders.length > 0);
478
+ await apos.asset.tasks['clear-cache'].task();
479
+
480
+ assert.equal(fs.readdirSync(cacheFolderPath, 'utf8').length, 0);
481
+ });
482
+
483
+ it('should be able to override the build cache location via APOS_ASSET_CACHE', async function() {
484
+ await t.destroy(apos);
485
+ await removeCache();
486
+ const altCacheLoc = cacheFolderPath.replace('/webpack-cache', '/changed');
487
+ await removeCache(altCacheLoc);
488
+ process.env.APOS_ASSET_CACHE = altCacheLoc;
489
+
490
+ apos = await t.create({
491
+ root: module,
492
+ modules
493
+ });
494
+ assert.throws(() => fs.readdirSync(altCacheLoc), {
495
+ code: 'ENOENT'
496
+ });
497
+ await apos.asset.tasks.build.task({
498
+ 'check-apos-build': false
499
+ });
500
+ const { meta, folders } = getCacheMeta(altCacheLoc);
501
+ assert.equal(folders.length, 2);
502
+ assert.equal(Object.keys(meta).length, 2);
503
+ assert(meta['default:apos']);
504
+ assert(meta['default:src']);
505
+
506
+ delete process.env.APOS_ASSET_CACHE;
507
+ await removeCache(altCacheLoc);
508
+ });
509
+
510
+ it('should watch and rebuild assets and reload page in development', async function() {
511
+ await t.destroy(apos);
512
+
513
+ apos = await t.create({
514
+ root: module,
515
+ autoBuild: true,
516
+ modules
517
+ });
518
+ const restartId = apos.asset.restartId;
519
+ assert(apos.asset.buildWatcher);
520
+ assert(apos.asset.restartId);
521
+
522
+ // Modify asset and rebuild
523
+ const assetPath = path.join(process.cwd(), 'test/modules/bundle-page/ui/src/extra.js');
524
+ const assetPathPublic = path.join(process.cwd(), 'test/public/apos-frontend/default/extra-module-bundle.js');
525
+ const assetContent = 'export default () => {};\n';
526
+ fs.writeFileSync(
527
+ assetPath,
528
+ 'export default () => { \'bundle-page-watcher-test\'; };\n',
529
+ 'utf8'
530
+ );
531
+
532
+ await retryAssertTrue(
533
+ async () => (await fs.readFile(assetPathPublic, 'utf8')).match(/bundle-page-watcher-test/),
534
+ 'Unable to verify public asset was rebuilt by the watcher',
535
+ 500,
536
+ 10000
537
+ );
538
+
539
+ await retryAssertTrue(
540
+ () => apos.asset.restartId !== restartId,
541
+ 'Unable to verify restartId has been changed',
542
+ 500,
543
+ 10000
544
+ );
545
+
546
+ await t.destroy(apos);
547
+ assert.equal(apos.asset.buildWatcher, null);
548
+ apos = null;
549
+ fs.writeFileSync(assetPath, assetContent, 'utf8');
550
+ });
551
+
552
+ it('should watch and rebuild assets in a debounced queue', async function() {
553
+ await t.destroy(apos);
554
+ let timesRebuilt = 0;
555
+ const inc = () => {
556
+ timesRebuilt += 1;
557
+ };
558
+
559
+ apos = await t.create({
560
+ root: module,
561
+ autoBuild: true,
562
+ modules: {
563
+ ...modules,
564
+ '@apostrophecms/asset': {
565
+ extendMethods() {
566
+ return {
567
+ async watchUiAndRebuild(_super) {
568
+ return _super(inc);
569
+ }
570
+ };
571
+ }
572
+ }
573
+ }
574
+ });
575
+ assert(apos.asset.buildWatcher);
576
+
577
+ const assetPath = path.join(process.cwd(), 'test/modules/bundle-page/ui/src/extra.js');
578
+ const assetPathPublic = path.join(process.cwd(), 'test/public/apos-frontend/default/extra-module-bundle.js');
579
+ const assetContent = 'export default () => {};\n';
580
+
581
+ // Modify below the debounce rate
582
+ for (const i of [ 1, 2, 3 ]) {
583
+ await fs.writeFile(
584
+ assetPath,
585
+ `export default () => { 'bundle-page-watcher-test-${i}'; };\n`,
586
+ 'utf8'
587
+ );
588
+ await Promise.delay(300);
589
+ }
590
+ await retryAssertTrue(
591
+ async () => (await fs.readFile(assetPathPublic, 'utf8')).match(/bundle-page-watcher-test-3/),
592
+ 'Unable to verify public asset rebuilding by the watcher',
593
+ 500,
594
+ 10000
595
+ );
596
+ await retryAssertTrue(
597
+ () => timesRebuilt === 1,
598
+ `Expected to rebuild 1 time, got ${timesRebuilt}`,
599
+ 100,
600
+ 5000
601
+ );
602
+
603
+ // Modify above the debounce rate, test the queue cap
604
+ timesRebuilt = 0;
605
+ for (const i of [ 1, 2, 3 ]) {
606
+ await fs.writeFile(
607
+ assetPath,
608
+ `export default () => { 'bundle-page-watcher-test-${i}0'; };\n`,
609
+ 'utf8'
610
+ );
611
+ await Promise.delay(1050);
612
+ }
613
+ await retryAssertTrue(
614
+ async () => (await fs.readFile(assetPathPublic, 'utf8')).match(/bundle-page-watcher-test-30/),
615
+ 'Unable to verify public asset rebuilding by the watcher',
616
+ 500,
617
+ 10000
618
+ );
619
+ await retryAssertTrue(
620
+ () => timesRebuilt === 2,
621
+ `Expected to rebuild 2 times, got ${timesRebuilt}`,
622
+ 100,
623
+ 5000
624
+ );
625
+
626
+ await t.destroy(apos);
627
+ apos = null;
628
+ fs.writeFileSync(assetPath, assetContent, 'utf8');
629
+ });
630
+
631
+ it('should be able to setup the debounce time', async function() {
632
+ await t.destroy(apos);
633
+
634
+ apos = await t.create({
635
+ root: module,
636
+ modules: {
637
+ '@apostrophecms/asset': {
638
+ options: {
639
+ watchDebounceMs: 500
640
+ }
641
+ }
642
+ }
643
+ });
644
+ assert.equal(apos.asset.buildWatcherDebounceMs, 500);
645
+ });
646
+
647
+ it('should not watch if explicitly disabled by option or env in development', async function() {
648
+ await t.destroy(apos);
649
+ process.env.APOS_ASSET_WATCH = '0';
650
+
651
+ apos = await t.create({
652
+ root: module,
653
+ autoBuild: true
654
+ });
655
+ assert(!apos.asset.buildWatcher);
656
+ delete process.env.APOS_ASSET_WATCH;
657
+ await t.destroy(apos);
658
+
659
+ apos = await t.create({
660
+ root: module,
661
+ autoBuild: true,
662
+ modules: {
663
+ '@apostrophecms/asset': {
664
+ options: {
665
+ watch: false
666
+ }
667
+ }
668
+ }
669
+ });
670
+ assert(!apos.asset.buildWatcher);
671
+ });
672
+
673
+ it('should not watch if autoBuild is disabled', async function() {
674
+ await t.destroy(apos);
675
+
676
+ apos = await t.create({
677
+ root: module
678
+ });
679
+ assert(!apos.asset.buildWatcher);
680
+ });
681
+
682
+ it('should not watch in production', async function() {
683
+ await t.destroy(apos);
684
+ process.env.NODE_ENV = 'production';
685
+
686
+ apos = await t.create({
687
+ root: module,
688
+ autoBuild: true,
689
+ modules
690
+ });
691
+ assert(!apos.asset.buildWatcher);
692
+ process.env.NODE_ENV = 'development';
693
+ });
287
694
  });
288
695
 
289
696
  function loadUtils () {
290
697
  const publicFolderPath = path.join(process.cwd(), 'test/public');
698
+ const cacheFolderPath = process.env.APOS_ASSET_CACHE ||
699
+ path.join(process.cwd(), 'test/data/temp/webpack-cache');
291
700
 
292
701
  const getScriptMarkup = (file, mod) => {
293
702
  const moduleStr = mod === 'module' ? ' type="module"' : ' nomodule';
@@ -318,12 +727,61 @@ function loadUtils () {
318
727
  assert(page.includes(getScriptMarkup('extra2-module-bundle')));
319
728
  };
320
729
 
730
+ const removeCache = async (loc) => {
731
+ await fs.remove(loc || cacheFolderPath);
732
+ };
733
+
734
+ const getCacheMeta = (loc) => {
735
+ const cacheFolders = fs.readdirSync(loc || cacheFolderPath, 'utf8');
736
+ const i = {};
737
+ const meta = cacheFolders
738
+ .reduce((prev, folder) => {
739
+ const location = `${loc || cacheFolderPath}/${folder}/.apos`;
740
+ const m = fs.readFileSync(location, 'utf8');
741
+ let [ mdate, id ] = m.split(' ');
742
+ // e.g. default:apos_2, default:apos_3, etc
743
+ if (prev[id]) {
744
+ i[id] = (i[id] || 1) + 1;
745
+ id = `${id}_${i[id]}`;
746
+ }
747
+ return {
748
+ ...prev,
749
+ [id]: {
750
+ mdate: new Date(mdate),
751
+ folder,
752
+ location
753
+ }
754
+ };
755
+ }, {});
756
+ return {
757
+ folders: cacheFolders,
758
+ meta
759
+ };
760
+ };
761
+
762
+ // Retry `max` ms with `delay` ms between the retries
763
+ // until `assertFn` returns true or fail with `failMsg`
764
+ const retryAssertTrue = async (assertFn, failMsg, delay, max) => {
765
+ let current = 0;
766
+ while (!(await assertFn())) {
767
+ await Promise.delay(delay);
768
+ current += delay;
769
+ if (current >= max) {
770
+ assert.fail(`${failMsg}`);
771
+ }
772
+ }
773
+ };
774
+
321
775
  return {
322
776
  publicFolderPath,
777
+ cacheFolderPath,
323
778
  getScriptMarkup,
324
779
  getStylesheetMarkup,
325
780
  expectedBundlesNames,
326
781
  deleteBuiltFolders,
327
- allBundlesAreIncluded
782
+ allBundlesAreIncluded,
783
+ removeCache,
784
+ getCacheMeta,
785
+ retryAssertTrue
328
786
  };
329
787
  }
package/test/schemas.js CHANGED
@@ -1652,6 +1652,31 @@ describe('Schemas', function() {
1652
1652
  age: null
1653
1653
  }, 'age', 'required');
1654
1654
  });
1655
+
1656
+ it('should save date and time with the right format', async function () {
1657
+ const req = apos.task.getReq();
1658
+ const schema = apos.schema.compose({
1659
+ addFields: [
1660
+ {
1661
+ name: 'emptyValue',
1662
+ type: 'dateAndTime'
1663
+ },
1664
+ {
1665
+ name: 'goodValue',
1666
+ type: 'dateAndTime'
1667
+ }
1668
+ ]
1669
+ });
1670
+
1671
+ const output = {};
1672
+ await apos.schema.convert(req, schema, {
1673
+ emptyValue: null,
1674
+ goodValue: '2022-05-09T22:36:00.000Z'
1675
+ }, output);
1676
+
1677
+ assert(output.emptyValue === null);
1678
+ assert(output.goodValue === '2022-05-09T22:36:00.000Z');
1679
+ });
1655
1680
  });
1656
1681
 
1657
1682
  async function testSchemaError(schema, input, path, name) {
package/test-lib/test.js CHANGED
@@ -34,4 +34,16 @@ for (const dir of dirs) {
34
34
 
35
35
  fs.writeFileSync(packageJson, JSON.stringify(packageJsonInfo, null, ' '));
36
36
 
37
+ // A "project level" package-lock.json for checking webpack build cache
38
+
39
+ const packageLockJson = path.join(__dirname, '/../test/package-lock.json');
40
+ const packageLockJsonInfo = {
41
+ _: 'Do not change, fake lock used for testing',
42
+ name: 'apostrophe',
43
+ version: 'current',
44
+ packages: {}
45
+ };
46
+ fs.removeSync(packageLockJson);
47
+ fs.writeFileSync(packageLockJson, JSON.stringify(packageLockJsonInfo, null, ' '));
48
+
37
49
  module.exports = require('./util.js');
package/test-lib/util.js CHANGED
@@ -10,10 +10,11 @@ const mongodbConnect = require('../lib/mongodb-connect');
10
10
  // If `apos` is null, no work is done.
11
11
 
12
12
  async function destroy(apos) {
13
- const dbName = apos.db && apos.db.databaseName;
14
- if (apos) {
15
- await apos.destroy();
13
+ if (!apos) {
14
+ return;
16
15
  }
16
+ await apos.destroy();
17
+ const dbName = apos.db && apos.db.databaseName;
17
18
  // TODO at some point accommodate nonsense like testing remote databases
18
19
  // that won't let us use dropDatabase, no shell available etc., but the
19
20
  // important principle here is that we should not have to have an apos