apostrophe 4.23.1-alpha.1 → 4.24.1

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 (78) hide show
  1. package/.github/workflows/main.yml +4 -4
  2. package/CHANGELOG.md +23 -2
  3. package/defaults.js +2 -0
  4. package/index.js +33 -24
  5. package/lib/glob.js +4 -2
  6. package/lib/moog.js +1 -1
  7. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbSwitch.vue +1 -1
  8. package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +16 -2
  9. package/modules/@apostrophecms/asset/index.js +4 -3
  10. package/modules/@apostrophecms/asset/lib/build/external-module-api.js +8 -5
  11. package/modules/@apostrophecms/asset/lib/build/internals.js +4 -3
  12. package/modules/@apostrophecms/asset/lib/build/task.js +4 -2
  13. package/modules/@apostrophecms/asset/lib/path.js +24 -0
  14. package/modules/@apostrophecms/asset/lib/webpack/utils.js +1 -1
  15. package/modules/@apostrophecms/box-field/index.js +157 -0
  16. package/modules/@apostrophecms/box-field/ui/apos/components/AposInputBox.vue +323 -0
  17. package/modules/@apostrophecms/box-field/ui/apos/logic/AposInputBox.js +196 -0
  18. package/modules/@apostrophecms/doc/index.js +134 -7
  19. package/modules/@apostrophecms/doc/ui/apos/mixins/AposFieldMetaUtilsMixin.js +1 -1
  20. package/modules/@apostrophecms/doc-type/index.js +1 -1
  21. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +28 -7
  22. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocLocalePicker.vue +45 -17
  23. package/modules/@apostrophecms/file-widget/index.js +18 -0
  24. package/modules/@apostrophecms/file-widget/views/widget.html +16 -0
  25. package/modules/@apostrophecms/i18n/i18n/de.json +29 -2
  26. package/modules/@apostrophecms/i18n/i18n/en.json +28 -1
  27. package/modules/@apostrophecms/i18n/i18n/es.json +28 -2
  28. package/modules/@apostrophecms/i18n/i18n/fr.json +28 -2
  29. package/modules/@apostrophecms/i18n/i18n/it.json +28 -2
  30. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +28 -2
  31. package/modules/@apostrophecms/i18n/i18n/sk.json +28 -2
  32. package/modules/@apostrophecms/i18n/index.js +67 -2
  33. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +75 -50
  34. package/modules/@apostrophecms/job/index.js +61 -35
  35. package/modules/@apostrophecms/layout-column-widget/index.js +2 -1
  36. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +29 -2
  37. package/modules/@apostrophecms/migration/index.js +89 -38
  38. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +6 -4
  39. package/modules/@apostrophecms/notification/index.js +25 -5
  40. package/modules/@apostrophecms/page/index.js +29 -8
  41. package/modules/@apostrophecms/page/ui/apos/components/AposPagesManager.vue +1 -1
  42. package/modules/@apostrophecms/page/ui/apos/logic/AposPagesManager.js +3 -3
  43. package/modules/@apostrophecms/page-type/index.js +2 -4
  44. package/modules/@apostrophecms/piece-type/index.js +11 -3
  45. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +5 -5
  46. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +75 -58
  47. package/modules/@apostrophecms/schema/index.js +1 -1
  48. package/modules/@apostrophecms/schema/lib/newInstance.js +91 -12
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +1 -1
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +138 -37
  51. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +7 -0
  52. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +1 -1
  53. package/modules/@apostrophecms/schema/ui/apos/logic/AposSchema.js +4 -1
  54. package/modules/@apostrophecms/soft-redirect/index.js +6 -1
  55. package/modules/@apostrophecms/task/index.js +1 -1
  56. package/modules/@apostrophecms/template/index.js +8 -4
  57. package/modules/@apostrophecms/ui/ui/apos/scss/global/_inputs.scss +38 -0
  58. package/modules/@apostrophecms/ui/ui/apos/stores/notification.js +5 -2
  59. package/modules/@apostrophecms/util/index.js +13 -2
  60. package/modules/@apostrophecms/widget-type/ui/apos/composables/AposWidget.js +8 -1
  61. package/package.json +3 -3
  62. package/scripts/i18n-ignore.js +9 -0
  63. package/scripts/lint-i18n.js +10 -4
  64. package/test/add-missing-schema-fields-project/app.js +4 -0
  65. package/test/add-missing-schema-fields-project/config.js +13 -0
  66. package/test/add-missing-schema-fields-project/modules/product/index.js +77 -0
  67. package/test/add-missing-schema-fields-project/package.json +16 -0
  68. package/test/add-missing-schema-fields-project/test.js +122 -0
  69. package/test/add-missing-schema-fields.js +6 -4
  70. package/test/areas.js +117 -0
  71. package/test/attachments.js +13 -1
  72. package/test/bundle.js +1 -1
  73. package/test/docs.js +662 -2
  74. package/test/i18n.js +274 -2
  75. package/test/job.js +329 -131
  76. package/test/pages.js +208 -83
  77. package/test/utils.js +85 -6
  78. package/test-lib/test.js +2 -2
@@ -18,21 +18,21 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  strategy:
20
20
  matrix:
21
- node-version: [20, 22]
21
+ node-version: [20, 22, 24]
22
22
  mongodb-version: [6.0, 7.0, 8.0]
23
23
 
24
24
  # Steps represent a sequence of tasks that will be executed as part of the job
25
25
  steps:
26
26
  - name: Git checkout
27
- uses: actions/checkout@v4
27
+ uses: actions/checkout@v5
28
28
 
29
29
  - name: Use Node.js ${{ matrix.node-version }}
30
- uses: actions/setup-node@v4
30
+ uses: actions/setup-node@v6
31
31
  with:
32
32
  node-version: ${{ matrix.node-version }}
33
33
 
34
34
  - name: Start MongoDB
35
- uses: supercharge/mongodb-github-action@1.11.0
35
+ uses: supercharge/mongodb-github-action@1.12.0
36
36
  with:
37
37
  mongodb-version: ${{ matrix.mongodb-version }}
38
38
 
package/CHANGELOG.md CHANGED
@@ -1,19 +1,40 @@
1
1
  # Changelog
2
2
 
3
- ## 4.23.1-alpha.1 (2025-11-10)
3
+ ## 4.24.1 (2025-12-04)
4
+
5
+ ### Fixes
6
+
7
+ * Fixes soft-redirect module to decode the URL path before matching historic URLs, ensuring proper matching of accents, Cyrillic, and other non-ASCII characters in old URLs. While this was not a new issue, the fix is of new importance now that a migration path to eliminate accent marks in existing slugs has been introduced in version 4.24.0.
8
+
9
+ ## 4.24.0 (2025-11-25)
4
10
 
5
11
  ### Adds
6
12
 
13
+ * Adds `stripUrlAccents` option in `@apostrophecms/i18n` module to globally control whether accents are stripped from URLs. When set to `true`, all URLs (slugs) will have accents from Latin characters removed on document creation and updates. No existing documents are modified automatically; this only affects new or updated documents. A new task `node app @apostrophecms/i18n:strip-slug-accents` is provided to update existing document slugs in the database when needed.
14
+ * Add `@apostrophecms/migration:add-missing-schema-fields` task. This task does not run database migrations.
7
15
  * Translation strings added for the layout- and layout-column-widgets.
16
+ * Adds `@apostrophecms/doc:get-apos-doc-id` and `@apostrophecms/doc:set-apos-doc-id` tasks.
17
+ * New `box` schema field type
18
+ * When switching locale from the doc editor, ask if the user wants to localize the current document in the target locale or want to start a blank document.
19
+ * Adds batch failure notifications.
20
+ * Introduced a new `longPolling: false` option for the `@apostrophecms/notification` module. This eliminates long-pending requests when logged in, but also slows down the delivery of notifications. The behavior can be tuned further via the `pollingInterval` option, which defaults to `5000` milliseconds.
21
+ * Add support for `def` in area fields - array of widget names to use as defaults when the area is created.
8
22
 
9
23
  ### Changes
10
24
 
25
+ * `@apostrophecms/migration:requirements` handler now runs the migration requirements like `insertIfMissing`, `implementParkAllInDefaultLocale`, `replicate` and `implementParkAllInOtherLocales`.
11
26
  * Bump nodemailer to v7.x.
27
+ * Improves client error log when unable to render a widget.
28
+ * Rich text `styles` are once again available to insert menu items, such as our optional `@apostrophecms/ai-helper` module.
12
29
 
13
30
  ### Fixes
14
31
 
15
32
  * Specify the content type when calling back to Astro with JSON to render an area. This is required starting in Astro 4.9.0 and up, otherwise the request is blocked by CSRF protection.
16
- * Improved support for Node.js on "plain vanilla" Windows, without WSL.
33
+ * Improved support for Node.js on "plain vanilla" Windows, e.g. without WSL. We suggest working with NVM for Windows and Git Bash.
34
+ * Fixes `AposBreadcrumbSwitch` tooltip prop that is supposed to be an object, not a string. Object returned from the shared method `getOperationTooltip`.
35
+ * Empty text nodes are output properly without a warning.
36
+ * Uses `modalData.locale` in `AposI18nLocalize` component. Fixes watcher on `relatedDocTypes` not being properly triggered (uses data and methods for more control instead).
37
+ * Layout area fix when no columns are present.
17
38
 
18
39
  ## 4.23.0 (2025-10-30)
19
40
 
package/defaults.js CHANGED
@@ -46,8 +46,10 @@ module.exports = {
46
46
  '@apostrophecms/rich-text-widget': {},
47
47
  '@apostrophecms/html-widget': {},
48
48
  '@apostrophecms/color-field': {},
49
+ '@apostrophecms/box-field': {},
49
50
  '@apostrophecms/oembed-field': {},
50
51
  '@apostrophecms/video-widget': {},
52
+ '@apostrophecms/file-widget': {},
51
53
  '@apostrophecms/ui': {},
52
54
  '@apostrophecms/user': {},
53
55
  '@apostrophecms/settings': {},
package/index.js CHANGED
@@ -13,6 +13,7 @@ const npmResolve = require('resolve');
13
13
  const glob = require('./lib/glob.js');
14
14
  const moogRequire = require('./lib/moog-require');
15
15
  const importFresh = require('./lib/import-fresh');
16
+ const { pathToFileURL } = require('node:url');
16
17
  let defaults = require('./defaults.js');
17
18
 
18
19
  // ## Top-level options
@@ -322,21 +323,7 @@ async function apostrophe(options, telemetry, rootSpan) {
322
323
  await self.emit('modulesRegistered'); // formerly modulesReady
323
324
  self.apos.schema.validateAllSchemas();
324
325
  self.apos.schema.registerAllSchemas();
325
- await self.apos.lock.withLock('@apostrophecms/migration:migrate', async () => {
326
- await self.apos.migration.migrate(self.argv);
327
- // Inserts the global doc in the default locale if it does not exist;
328
- // same for other singleton piece types registered by other modules
329
- for (const apostropheModule of Object.values(self.modules)) {
330
- if (self.instanceOf(apostropheModule, '@apostrophecms/piece-type') && apostropheModule.options.singletonAuto) {
331
- await apostropheModule.insertIfMissing();
332
- }
333
- }
334
- await self.apos.page.implementParkAllInDefaultLocale();
335
- await self.apos.doc.replicate(); // emits beforeReplicate and afterReplicate events
336
- // Replicate will have created the parked pages across locales if needed,
337
- // but we may still need to reset parked properties
338
- await self.apos.page.implementParkAllInOtherLocales();
339
- });
326
+ await self.apos.migration.migrate(self.argv);
340
327
  await self.emit('ready'); // formerly afterInit
341
328
 
342
329
  if (self.taskRan) {
@@ -376,7 +363,7 @@ async function apostrophe(options, telemetry, rootSpan) {
376
363
  const reallyLocalPath = self.rootDir + localPath;
377
364
 
378
365
  if (fs.existsSync(reallyLocalPath)) {
379
- local = await self.root.import(reallyLocalPath);
366
+ local = await self.root.import(pathToFileURL(reallyLocalPath));
380
367
  }
381
368
 
382
369
  // Otherwise making a second apos instance
@@ -408,7 +395,7 @@ async function apostrophe(options, telemetry, rootSpan) {
408
395
  const configs = glob(self.localModules + '/**/modules.js', { follow: true });
409
396
  for (const config of configs) {
410
397
  try {
411
- _.merge(self.options.modules, await self.root.import(config));
398
+ _.merge(self.options.modules, await self.root.import(pathToFileURL(config)));
412
399
  } catch (e) {
413
400
  console.error(stripIndent`
414
401
  When nestedModuleSubdirs is active, any modules.js file beneath:
@@ -526,7 +513,7 @@ async function apostrophe(options, telemetry, rootSpan) {
526
513
  checkTestModule();
527
514
  // Allow tests to be in test/ or in tests/
528
515
  const testDir = path.dirname(m.filename);
529
- const moduleDir = testDir.replace(/\/tests?$/, '');
516
+ const moduleDir = testDir.replace(/\/tests?$/, '').replace(/\\tests?$/, '');
530
517
  if (testDir === moduleDir) {
531
518
  throw new Error('Test file must be in test/ or tests/ subdirectory of module');
532
519
  }
@@ -540,7 +527,7 @@ async function apostrophe(options, telemetry, rootSpan) {
540
527
 
541
528
  if (!fs.existsSync(testDir + '/node_modules')) {
542
529
  fs.mkdirSync(testDir + '/node_modules' + pkgNamespace, { recursive: true });
543
- fs.symlinkSync(moduleDir, testDir + '/node_modules/' + pkgName, 'dir');
530
+ fs.symlinkSync(moduleDir, testDir + '/node_modules/' + pkgName, 'junction');
544
531
  }
545
532
  // Makes sure we encounter mocha along the way
546
533
  // and throws an exception if we don't
@@ -734,7 +721,7 @@ async function apostrophe(options, telemetry, rootSpan) {
734
721
  if (fs.existsSync(path.resolve(self.localModules, name, 'modules.js'))) {
735
722
  return;
736
723
  }
737
- const submodule = await self.root.import(path.resolve(self.localModules, name, 'index.js'));
724
+ const submodule = await self.root.import(pathToFileURL(path.resolve(self.localModules, name, 'index.js')));
738
725
  if (
739
726
  submodule &&
740
727
  submodule.options &&
@@ -914,8 +901,19 @@ function getRoot(options) {
914
901
  if (root?.filename && root?.require) {
915
902
  return {
916
903
  filename: root.filename,
917
- import: async (id) => root.require(id),
918
- require: (id) => root.require(id)
904
+ async import(id) {
905
+ // Must accept URL objects
906
+ id = id.toString();
907
+ // To accurately simulate ES import, we need to
908
+ // accept file:// URLs like it can
909
+ if (id.startsWith('file:')) {
910
+ id = url.fileURLToPath(id);
911
+ }
912
+ return root.require(id);
913
+ },
914
+ require(id) {
915
+ return root.require(id);
916
+ }
919
917
  };
920
918
  }
921
919
 
@@ -957,8 +955,19 @@ function getRoot(options) {
957
955
  const legacyRoot = getLegacyRoot();
958
956
  return {
959
957
  filename: legacyRoot.filename,
960
- import: async (id) => legacyRoot.require(id),
961
- require: (id) => legacyRoot.require(id)
958
+ async import(id) {
959
+ // Must accept URL objects
960
+ id = id.toString();
961
+ // To accuratesly simulate ES import, we need to
962
+ // accept file:// URLs like it can
963
+ if (id.startsWith('file:')) {
964
+ id = url.fileURLToPath(id);
965
+ }
966
+ return legacyRoot.require(id);
967
+ },
968
+ require(id) {
969
+ return legacyRoot.require(id);
970
+ }
962
971
  };
963
972
  };
964
973
 
package/lib/glob.js CHANGED
@@ -1,10 +1,12 @@
1
1
  const { globSync } = require('glob');
2
2
 
3
3
  // synchronous glob 10 but with the sorting semantics of glob 8,
4
- // to ease backwards compatibility in Apostrophe startup logic
4
+ // to ease backwards compatibility in Apostrophe startup logic.
5
+ // Also replaces \ with / for consistency across platforms
5
6
 
6
7
  module.exports = (pattern, options) => {
7
- const result = globSync(pattern, options);
8
+ pattern = pattern.replaceAll('\\', '//');
9
+ const result = globSync(pattern, options).map(path => path.replaceAll('\\', '/'));
8
10
  if (!options.nosort) {
9
11
  result.sort((a, b) => a.localeCompare(b, 'en'));
10
12
  }
package/lib/moog.js CHANGED
@@ -189,7 +189,7 @@ module.exports = function(options) {
189
189
  }
190
190
  // You can have access to options within a function, if you choose to
191
191
  // provide one
192
- const properties = ((typeof step[cascade]) === 'function') ? step[cascade](that, options) : step[cascade];
192
+ const properties = ((typeof step[cascade]) === 'function') ? await step[cascade](that, options) : step[cascade];
193
193
  if (properties) {
194
194
  const valid = [ 'add', 'remove', 'order', 'group' ];
195
195
  if (properties.add) {
@@ -51,7 +51,7 @@ export default {
51
51
  required: true
52
52
  },
53
53
  tooltip: {
54
- type: String,
54
+ type: Object,
55
55
  default: null
56
56
  },
57
57
  disabled: {
@@ -626,10 +626,24 @@ export default {
626
626
  },
627
627
  // Return a new widget object in which defaults are fully populated,
628
628
  // especially valid sub-area objects, so that nested edits work on the page
629
- newWidget(type) {
629
+ // Optional schemaOverride array can be provided to override any fields from
630
+ // the base schema. Useful for e.g. per-instance defaults.
631
+ newWidget(type, schemaOverride) {
630
632
  const schema = apos.modules[apos.area.widgetManagers[type]].schema;
633
+ const finalSchema = Array.isArray(schemaOverride)
634
+ ? schema.map(field => {
635
+ const _field = schemaOverride.find(f => f.name === field.name);
636
+ if (_field) {
637
+ return {
638
+ ...field,
639
+ ..._field
640
+ };
641
+ }
642
+ return field;
643
+ })
644
+ : schema;
631
645
  const widget = {
632
- ...newInstance(schema),
646
+ ...newInstance(finalSchema),
633
647
  type
634
648
  };
635
649
  return widget;
@@ -6,7 +6,7 @@ const { stripIndent } = require('common-tags');
6
6
  const { createId } = require('@paralleldrive/cuid2');
7
7
  const chokidar = require('chokidar');
8
8
  const _ = require('lodash');
9
- const { glob } = require('glob');
9
+ const { glob } = require('./lib/path');
10
10
  const globalIcons = require('./lib/globalIcons');
11
11
  const {
12
12
  checkModulesWebpackConfig,
@@ -31,7 +31,7 @@ module.exports = {
31
31
  // If false no UI assets sources will be watched in development.
32
32
  // This option has no effect in production (watch disabled).
33
33
  watch: true,
34
- // Miliseconds to wait between asset sources changes before
34
+ // Milliseconds to wait between asset sources changes before
35
35
  // performing a build.
36
36
  watchDebounceMs: 1000,
37
37
  // Object containing instructions for remapping existing bundles.
@@ -939,7 +939,8 @@ module.exports = {
939
939
  const pulledChanges = [];
940
940
  let change = changes.pop();
941
941
  while (change) {
942
- pulledChanges.push(change);
942
+ // Fix windows paths
943
+ pulledChanges.push(change.replace(/\\/g, '/'));
943
944
  change = changes.pop();
944
945
  }
945
946
  // No changes - should never happen.
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('node:path');
3
- const { glob } = require('glob');
3
+ const { glob } = require('../../lib/path');
4
4
  const { stripIndent } = require('common-tags');
5
5
 
6
6
  // High and Low level public API for external modules.
@@ -196,7 +196,7 @@ function invoke() {
196
196
  }` + '\n'
197
197
  : '';
198
198
 
199
- // Remove the identation per line.
199
+ // Remove the indentation per line.
200
200
  // It may look weird, but the result is nice and formatted import file.
201
201
  output += (js && js.invokeCode.trim().split('\n').map(l => l.trim()).join('\n') + '\n') || '';
202
202
 
@@ -653,7 +653,9 @@ function invoke() {
653
653
  }
654
654
  }
655
655
  }
656
- const jsFilename = JSON.stringify(component);
656
+ // You would think we should run pathToFileURL over realPath,
657
+ // but that actually breaks both Windows and Linux with Vite. -Tom
658
+ const importPath = JSON.stringify(realPath);
657
659
  const name = self.getComponentNameByPath(
658
660
  component,
659
661
  { enumerate: options.enumerateImports === true ? i : false }
@@ -661,8 +663,8 @@ function invoke() {
661
663
  const jsName = JSON.stringify(name);
662
664
  const importName = `${name}${options.importSuffix || ''}`;
663
665
  const importCode = options.importName === false
664
- ? `import ${jsFilename};\n`
665
- : `import ${importName} from ${jsFilename};\n`;
666
+ ? `import ${importPath};\n`
667
+ : `import ${importName} from ${importPath};\n`;
666
668
 
667
669
  output.importCode += `${importCode}`;
668
670
 
@@ -720,6 +722,7 @@ function invoke() {
720
722
  let importName = importFrom;
721
723
  if (!importIndex.includes(importFrom)) {
722
724
  if (importFrom.substring(0, 1) === '~') {
725
+ // An npm module name
723
726
  importName = self.apos.util.slugify(importFrom).replaceAll('-', '');
724
727
  output.importCode += `import ${importName}Icon from '${importFrom.substring(1)}';\n`;
725
728
  } else {
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('node:path');
3
3
  const util = require('node:util');
4
- const { glob } = require('glob');
4
+ const { glob } = require('../../lib/path');
5
5
  const { getBuildExtensions, fillExtraBundles } = require('./utils');
6
6
 
7
7
  // Internal build interface.
@@ -127,11 +127,12 @@ module.exports = (self) => {
127
127
  // Get the component name from a file path. The `enumerate` option allows
128
128
  // to append a number to the component name.
129
129
  getComponentNameByPath(componentPath, { enumerate } = {}) {
130
- return path
130
+ const result = path
131
131
  .basename(componentPath)
132
132
  .replace(/-/g, '_')
133
133
  .replace(/\s+/g, '')
134
134
  .replace(/\.\w+/, '') + (typeof enumerate === 'number' ? `_${enumerate}` : '');
135
+ return result;
135
136
  },
136
137
 
137
138
  // Return the reported by the external module during build dev server URL.
@@ -201,7 +202,7 @@ module.exports = (self) => {
201
202
  // return a list of all bundle files.
202
203
  //
203
204
  // The `bundles` (Set) property added to the entrypoints configuration
204
- // contans the bundle files used later when injecting the scripts and
205
+ // contains the bundle files used later when injecting the scripts and
205
206
  // stylesheets in the browser. The `metadata` is the return value of the
206
207
  // external build module build method (see `self.build()` and
207
208
  // `configureBuildModule()`).
@@ -5,6 +5,7 @@ const fs = require('fs-extra');
5
5
  const { stripIndent } = require('common-tags');
6
6
  const webpackModule = require('webpack');
7
7
  const { mergeWithCustomize: webpackMerge } = require('webpack-merge');
8
+ const { pathToFileURL } = require('node:url');
8
9
  const {
9
10
  getBundlesNames,
10
11
  writeBundlesImportFiles,
@@ -675,12 +676,13 @@ module.exports = (self) => ({
675
676
  `);
676
677
  }
677
678
  }
678
- const jsFilename = JSON.stringify(component);
679
+ // We know component is a file path at this point
680
+ const importUrl = JSON.stringify(pathToFileURL(component));
679
681
  const name = getComponentName(component, options, i);
680
682
  const jsName = JSON.stringify(name);
681
683
  const importName = `${name}${options.importSuffix || ''}`;
682
684
  const importCode = `
683
- import ${importName} from ${jsFilename};
685
+ import ${importName} from ${importUrl};
684
686
  `;
685
687
 
686
688
  output.paths.push(component);
@@ -0,0 +1,24 @@
1
+ const { glob: globOriginal } = require('glob');
2
+ const {
3
+ resolve: resolveOriginal,
4
+ dirname: dirnameOriginal
5
+ } = require('path');
6
+
7
+ module.exports = {
8
+ glob,
9
+ resolve,
10
+ dirname
11
+ };
12
+
13
+ async function glob(path, options) {
14
+ const results = await globOriginal(path, options);
15
+ return results.map(result => result.replaceAll('\\', '/'));
16
+ }
17
+
18
+ function resolve(...args) {
19
+ return resolveOriginal(...args).replaceAll('\\', '/');
20
+ }
21
+
22
+ function dirname(path) {
23
+ return dirnameOriginal(path).replaceAll('\\', '/');
24
+ }
@@ -284,7 +284,7 @@ async function verifyBundlesEntryPoints (bundles) {
284
284
  // already verified it's unique for the given module
285
285
  if (!remapping.source) {
286
286
  // Bundle name for "main" "doesn't matter - it will be ignored and
287
- // never built, we want to achieve a free from colision name. What
287
+ // never built, we want to achieve a free from collision name. What
288
288
  // matters is main = true. Target is 'main' by convention:
289
289
  // `main.bundleName`
290
290
  bundleName = `${remapping.target}.${bundleName}`;
@@ -0,0 +1,157 @@
1
+ module.exports = {
2
+ options: {
3
+ name: 'box',
4
+ alias: 'boxField'
5
+ },
6
+ init(self) {
7
+ self.name = self.options.name;
8
+ self.addFieldType();
9
+ self.enableBrowserData();
10
+ },
11
+ methods(self) {
12
+ return {
13
+ addFieldType() {
14
+ self.apos.schema.addFieldType({
15
+ name: 'box',
16
+ convert(req, field, data, destination) {
17
+ const defProps = [ 'top', 'right', 'bottom', 'left' ];
18
+ const temp = {};
19
+ const min = self.apos.launder.integer(field.min);
20
+ let max = null;
21
+
22
+ if ('max' in field) {
23
+ max = self.apos.launder.integer(field.max);
24
+ }
25
+
26
+ // All values to numbers or null
27
+ defProps.forEach(side => {
28
+ const int = parseInt(data[field.name][side]);
29
+ if (int || int === 0) {
30
+ temp[side] = int;
31
+ } else {
32
+ temp[side] = null;
33
+ }
34
+ });
35
+
36
+ // One non-null value if required
37
+ if (field.required) {
38
+ const unique = [ ...new Set(Object.values(temp)) ];
39
+ if (unique.length === 1 && unique[0] === null) {
40
+ throw self.apos.error('required');
41
+ }
42
+ }
43
+
44
+ // Minimum values in range
45
+ for (const key of defProps) {
46
+ if (temp[key] && temp < min) {
47
+ throw self.apos.error(`${key} is below the min ${field.min}, is ${temp[key]}`);
48
+ }
49
+ }
50
+
51
+ // Maximum values in range
52
+ if (max) {
53
+ for (const key of defProps) {
54
+ if (temp[key] && temp[key] > field.max) {
55
+ throw self.apos.error(`${key} is greater than the max ${field.max}, is ${temp[key]}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ // Copy values to destination
61
+ destination[field.name] = temp;
62
+ },
63
+ validate(field, options, warn, fail) {
64
+ const defProps = [ 'top', 'right', 'bottom', 'left' ];
65
+ let defMin = 0;
66
+
67
+ if (field.max && typeof field.max !== 'number') {
68
+ fail('Property "max" must be a number');
69
+ }
70
+
71
+ if (field.step && typeof field.step !== 'number') {
72
+ fail('Property "step" must be a number');
73
+ }
74
+
75
+ if (field.def) {
76
+ const fieldDefProps = Object.keys(field.def);
77
+ if (
78
+ !(fieldDefProps.length === defProps.length &&
79
+ fieldDefProps.every(k => defProps.includes(k)))
80
+ ) {
81
+ fail('Def must be an object with only "top", "right", "bottom", and "left" keys');
82
+ }
83
+
84
+ for (const key in field.def) {
85
+ if (!Number.isFinite(field.def[key])) {
86
+ fail(`Default property "${key}" must be a number`);
87
+ }
88
+ }
89
+ }
90
+
91
+ if (field.min) {
92
+ if (typeof field.min !== 'number') {
93
+ fail('Property "min" must be a number');
94
+ }
95
+ } else {
96
+ if (field.def) {
97
+ // if def has a negative value, it needs to be the min
98
+ const fieldDefMin = Math.min(...Object.values(field.def));
99
+ if (defMin > fieldDefMin) {
100
+ defMin = fieldDefMin;
101
+ }
102
+ }
103
+ field.min = defMin;
104
+ }
105
+
106
+ },
107
+ def: {
108
+ top: null,
109
+ right: null,
110
+ bottom: null,
111
+ left: null
112
+ }
113
+ });
114
+ },
115
+ getBrowserData(req) {
116
+ return {
117
+ name: self.name,
118
+ action: self.action
119
+ };
120
+ }
121
+ };
122
+ },
123
+ helpers() {
124
+ return {
125
+ toCss(value, property, unit = 'px') {
126
+ const {
127
+ top, right, bottom, left
128
+ } = value;
129
+ const vals = [ top, right, bottom, left ];
130
+
131
+ if (vals.every(v => v == null)) {
132
+ return '';
133
+ };
134
+
135
+ if (vals.every(v => v === top && v != null)) {
136
+ return `${property}: ${top}${unit};`;
137
+ }
138
+
139
+ const sides = {
140
+ top,
141
+ right,
142
+ bottom,
143
+ left
144
+ };
145
+ const parts = [];
146
+
147
+ for (const [ side, val ] of Object.entries(sides)) {
148
+ if (val != null) {
149
+ parts.push(`${property}-${side}: ${val}${unit};`);
150
+ }
151
+ }
152
+
153
+ return parts.join(' ');
154
+ }
155
+ };
156
+ }
157
+ };