apostrophe 4.27.1 → 4.28.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 (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/index.js +3 -0
  3. package/lib/stream-proxy.js +49 -0
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
  5. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
  6. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
  8. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
  9. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
  10. package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
  11. package/modules/@apostrophecms/asset/index.js +3 -2
  12. package/modules/@apostrophecms/attachment/index.js +270 -0
  13. package/modules/@apostrophecms/doc/index.js +8 -2
  14. package/modules/@apostrophecms/doc-type/index.js +81 -1
  15. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
  16. package/modules/@apostrophecms/express/index.js +30 -1
  17. package/modules/@apostrophecms/file/index.js +71 -6
  18. package/modules/@apostrophecms/i18n/index.js +20 -1
  19. package/modules/@apostrophecms/image/index.js +11 -0
  20. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
  21. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
  22. package/modules/@apostrophecms/login/index.js +43 -11
  23. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
  24. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
  25. package/modules/@apostrophecms/page/index.js +9 -11
  26. package/modules/@apostrophecms/page-type/index.js +6 -1
  27. package/modules/@apostrophecms/piece-page-type/index.js +100 -13
  28. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  32. package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
  33. package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
  34. package/modules/@apostrophecms/styles/lib/methods.js +35 -12
  35. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
  36. package/modules/@apostrophecms/task/index.js +9 -1
  37. package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
  38. package/modules/@apostrophecms/ui/index.js +2 -0
  39. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
  40. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
  42. package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
  43. package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
  44. package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
  45. package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
  46. package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
  47. package/modules/@apostrophecms/uploadfs/index.js +15 -1
  48. package/modules/@apostrophecms/url/index.js +419 -1
  49. package/package.json +6 -6
  50. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
  51. package/test/external-front.js +1 -0
  52. package/test/files.js +135 -0
  53. package/test/login-requirements.js +145 -3
  54. package/test/static-build.js +2701 -0
  55. package/test/universal-graph.js +1135 -0
@@ -7,10 +7,145 @@ const _ = require('lodash');
7
7
  const qs = require('qs');
8
8
 
9
9
  module.exports = {
10
- options: { alias: 'url' },
10
+
11
+ options: {
12
+ alias: 'url',
13
+ static: false
14
+ },
15
+
16
+ restApiRoutes(self) {
17
+ return {
18
+ // GET /api/v1/@apostrophecms/url
19
+ //
20
+ // Returns the result of `getAllUrlMetadata` — an object
21
+ // with `pages` and `attachments` properties.
22
+ // See the `getAllUrlMetadata` method for full documentation.
23
+ async getAll(req) {
24
+ if (!self.isExternalFront(req)) {
25
+ throw self.apos.error('forbidden');
26
+ }
27
+ if (!self.options.static) {
28
+ throw self.apos.error('invalid',
29
+ 'The @apostrophecms/url module must be configured with the "static: true" option to use this API. ' +
30
+ 'Without it, URL metadata for filters and pagination cannot be fully enumerated for a static build.'
31
+ );
32
+ }
33
+
34
+ // Parse and sanitize attachment options from the query string.
35
+ const launder = self.apos.launder;
36
+ const wantAttachments = launder.boolean(req.query.attachments);
37
+ const splitSizes = (val) => {
38
+ const list = launder.string(val)
39
+ .split(',').map(s => s.trim()).filter(Boolean);
40
+ return list.length ? list : undefined;
41
+ };
42
+ const attachments = wantAttachments
43
+ ? {
44
+ sizes: splitSizes(req.query.attachmentSizes),
45
+ skipSizes: splitSizes(req.query.attachmentSkipSizes),
46
+ scope: launder.select(
47
+ req.query.attachmentScope,
48
+ [ 'used', 'all' ],
49
+ 'used'
50
+ )
51
+ }
52
+ : false;
53
+
54
+ return self.getAllUrlMetadata(req, {
55
+ attachments
56
+ });
57
+ }
58
+ };
59
+ },
60
+
61
+ handlers(self) {
62
+ return {
63
+ '@apostrophecms/page:beforeSend': {
64
+ addStaticUrlsFlag(req) {
65
+ req.data.staticUrls = !!self.options.static;
66
+ }
67
+ }
68
+ };
69
+ },
70
+
11
71
  methods(self) {
12
72
  return {
13
73
 
74
+ // Returns `true` if the given `req` represents a static build
75
+ // request. This is the single source of truth — modules should
76
+ // use this method rather than inspecting `req` properties
77
+ // directly.
78
+ //
79
+ // Static build requests are those made by an external frontend
80
+ // (e.g. Astro) that opted in to static-build URL handling via
81
+ // the `x-apos-static-base-url: 1` header. The Express
82
+ // middleware sets `req.aposStaticBuild` and (when configured)
83
+ // `req.staticBaseUrl` in response.
84
+ isStaticBuild(req) {
85
+ return !!req.aposStaticBuild;
86
+ },
87
+
88
+ // Returns `true` if the given `req` originates from an
89
+ // external frontend integration (e.g. Astro, Next.js).
90
+ // This is the single source of truth — modules should use
91
+ // this method rather than inspecting `req.aposExternalFront`
92
+ // directly.
93
+ isExternalFront(req) {
94
+ return !!req.aposExternalFront;
95
+ },
96
+
97
+ // Returns the effective base URL for the given request.
98
+ //
99
+ // Resolution order:
100
+ // 1. If a hostname is configured for the active locale,
101
+ // `<protocol>://<hostname>` is returned (locale-specific
102
+ // host always wins, prefix is never appended).
103
+ // 2. If the request is a static build (`isStaticBuild(req)`),
104
+ // `req.staticBaseUrl` + prefix is returned (or the empty
105
+ // string when none is configured).
106
+ // 3. Otherwise, `apos.baseUrl` + prefix is returned (or the
107
+ // empty string).
108
+ //
109
+ // ### `options.strict`
110
+ //
111
+ // When `true`, guarantees a non-empty return value:
112
+ // - In a static build where `staticBaseUrl` is empty,
113
+ // falls back to `apos.baseUrl`.
114
+ // - In a non-static context where `apos.baseUrl` is empty,
115
+ // still returns the empty string (nothing more to fall
116
+ // back to).
117
+ //
118
+ // Use `strict: true` when an absolute URL is required (e.g.
119
+ // sitemap `<loc>` values).
120
+ //
121
+ // ### `options.prefix`
122
+ //
123
+ // When `true` (the default), the global `apos.prefix` is
124
+ // appended to the returned URL (e.g. `/blog`). The prefix
125
+ // is **not** appended when a locale-specific hostname is
126
+ // used — that hostname already represents the full origin.
127
+ //
128
+ // Pass `prefix: false` to obtain only the origin / base URL
129
+ // without the prefix. This is the legacy behavior of
130
+ // `apos.page.getBaseUrl(req)` before the delegation to this
131
+ // method.
132
+ getBaseUrl(req, { strict = false, prefix = true } = {}) {
133
+ const hostname = self.apos.i18n.locales?.[req.locale]?.hostname;
134
+ if (hostname) {
135
+ // Locale hostnames are fully qualified origins;
136
+ // the global prefix does not apply.
137
+ return `${req.protocol}://${hostname}`;
138
+ }
139
+ const aposPrefix = prefix ? (self.apos.prefix || '') : '';
140
+ if (self.isStaticBuild(req)) {
141
+ const staticUrl = req.staticBaseUrl || '';
142
+ if (staticUrl || !strict) {
143
+ return staticUrl + aposPrefix;
144
+ }
145
+ }
146
+ return (self.apos.baseUrl || '') + aposPrefix;
147
+ },
148
+
14
149
  // Build filter URLs. `data` is an object whose properties
15
150
  // become new query parameters. These parameters override any
16
151
  // existing parameters of the same name in the URL. If you
@@ -210,6 +345,289 @@ module.exports = {
210
345
  } else {
211
346
  return restoreHash(base);
212
347
  }
348
+ },
349
+
350
+ // Generate a list of all URLs reachable with the given
351
+ // req object. Used internally to implement static site
352
+ // generation and sitemaps. Usually called in a loop,
353
+ // once for each locale.
354
+ //
355
+ // ## Returned shape
356
+ //
357
+ // The return value is always:
358
+ //
359
+ // ```js
360
+ // {
361
+ // pages: [ ...page metadata entries ],
362
+ // attachments: { // null when not requested
363
+ // uploadsUrl: '/uploads',
364
+ // results: [
365
+ // { _id: 'abc', urls: [{ size?, path }] },
366
+ // ...
367
+ // ]
368
+ // }
369
+ // }
370
+ // ```
371
+ //
372
+ // ## Page metadata entries (`pages`)
373
+ //
374
+ // Each entry in the `pages` array may contain the
375
+ // following properties:
376
+ //
377
+ // ### `url` (string, always present)
378
+ // The URL path for this entry — a purely relative path
379
+ // without origin or prefix (e.g. `/articles` or
380
+ // `/articles/category/tech`, never
381
+ // `https://example.com/my-repo/articles`).
382
+ //
383
+ // For document entries, the framework strips the base
384
+ // URL (origin + prefix) automatically after collection.
385
+ // For **literal content** entries added by event
386
+ // handlers, the URL **must** be provided as a relative,
387
+ // prefix-free path (e.g. `/robots.txt`, not
388
+ // `/my-repo/robots.txt`). The consumer (e.g. Astro
389
+ // integration) is responsible for prepending the prefix
390
+ // when fetching from the backend.
391
+ //
392
+ // ### `i18nId` (string, always present)
393
+ // A stable identifier that is consistent across localized
394
+ // versions of the same logical URL. Used by external
395
+ // frontends (e.g. Astro) to correlate URLs across locales.
396
+ // For the primary view of a document this equals `aposDocId`.
397
+ // For derived URLs (pagination, filter combinations) it is
398
+ // built by appending suffixes to the base doc's `aposDocId`,
399
+ // e.g. `myDocId.category.tech.1` or `myDocId.2`.
400
+ //
401
+ // ### `type` (string, present for document entries)
402
+ // The Apostrophe doc `type` name (e.g. `'article'`,
403
+ // `'@apostrophecms/home-page'`). Absent on non-document
404
+ // entries such as literal content URLs.
405
+ //
406
+ // ### `aposDocId` (string, present for document entries)
407
+ // The locale-independent document ID. Absent on
408
+ // non-document entries.
409
+ //
410
+ // ### `_id` (string, present for document entries)
411
+ // The full locale-qualified MongoDB `_id` of the document
412
+ // (e.g. `'xyz:en:published'`). Absent on non-document
413
+ // entries.
414
+ //
415
+ // ### `contentType` (string, present for literal content entries only)
416
+ // A MIME type such as `'text/css'` or `'text/plain'`.
417
+ // When present, this signals that the URL returns non-HTML
418
+ // content that should be proxied literally by the consumer
419
+ // (e.g. an Astro static build). The consumer should fetch
420
+ // the `url` and write the response body to disk with the
421
+ // given content type instead of rendering it as a page.
422
+ // When absent, the URL is an ordinary HTML page.
423
+ //
424
+ // Document entries (pages, pieces, etc.) should NEVER set
425
+ // `contentType`. Consumers such as the sitemap module and
426
+ // Astro use its absence to identify renderable HTML pages
427
+ // vs literal assets.
428
+ //
429
+ // Literal content entries should NOT include `changefreq`
430
+ // or `priority` — those are only meaningful for document
431
+ // entries in sitemaps.
432
+ //
433
+ // ### `sitemap` (boolean, optional, default `true`)
434
+ // When explicitly set to `false`, the entry is excluded
435
+ // from sitemap generation but still included in static
436
+ // builds. Useful for URLs that must exist in the build
437
+ // (e.g. paginated filter pages, CSS files) but should
438
+ // not appear in `sitemap.xml`. If omitted, the entry is
439
+ // included in sitemaps.
440
+ //
441
+ // ### `changefreq` (string, optional, document entries only)
442
+ // Sitemap hint (e.g. `'daily'`). Included for legacy
443
+ // sitemap compatibility. Google explicitly ignores this.
444
+ // Must NOT be set on literal content entries.
445
+ //
446
+ // ### `priority` (number, optional, document entries only)
447
+ // Sitemap priority hint (e.g. `1.0`). Included for legacy
448
+ // sitemap compatibility. Google explicitly ignores this.
449
+ // Must NOT be set on literal content entries.
450
+ //
451
+ // ## Literal content entries
452
+ //
453
+ // Some entries represent non-HTML content that should be
454
+ // served literally with a specific MIME type, such as
455
+ // CSS stylesheets, `robots.txt`, `llms.txt`, etc. These
456
+ // entries include a `contentType` property (e.g.
457
+ // `text/css`, `text/plain`). Consumers of this API
458
+ // (e.g. an Astro static build) should fetch the `url`
459
+ // and serve the response body with the specified content
460
+ // type rather than rendering it as an HTML page.
461
+ //
462
+ // ## Extension points
463
+ //
464
+ // This method emits the
465
+ // `@apostrophecms/url:getAllUrlMetadata` event, so
466
+ // that handlers in any module can add URLs to the
467
+ // results. The default implementation already calls
468
+ // `getAllUrlMetadata` on every doc type manager that
469
+ // has at least one doc in the database, so listening
470
+ // for the event is only for edge cases that can't be
471
+ // covered by extending `getAllUrlMetadata` or
472
+ // `getUrlMetadata` on such a manager.
473
+ //
474
+ // Handlers should respect `excludeTypes`.
475
+ //
476
+ // **Important:** handlers that push literal content
477
+ // entries must provide a relative, prefix-free `url`
478
+ // path (e.g. `/robots.txt`). The base URL stripping
479
+ // that runs after collection only applies to document
480
+ // entries whose `url` starts with the effective base
481
+ // URL — it will not strip a prefix that was manually
482
+ // added by a handler. Providing a relative path
483
+ // ensures correct behaviour regardless of whether a
484
+ // prefix is configured.
485
+ //
486
+ // ## Attachment metadata (`attachments`)
487
+ //
488
+ // When `options.attachments` is a truthy object, attachment
489
+ // metadata is collected after URL enumeration and returned
490
+ // alongside the pages. The option accepts:
491
+ //
492
+ // - `scope` (`'used'` | `'all'`): `'used'` (default) limits
493
+ // to attachments referenced by documents present in the
494
+ // results. `'all'` returns every non-archived attachment.
495
+ // - `sizes` (string[]): explicit image sizes to include.
496
+ // - `skipSizes` (string[]): image sizes to exclude.
497
+ //
498
+ // `attachments.uploadsUrl` is the uploadfs base URL prefix
499
+ // (e.g. `/uploads` or `https://cdn.example.com`).
500
+ //
501
+ // Each entry in `attachments.results` contains:
502
+ // - `_id` (string): the attachment record ID.
503
+ // - `urls` (array): `{ size, path }` objects where `path`
504
+ // is the uploadfs-relative file path.
505
+ //
506
+ // After attachment metadata is collected, the
507
+ // `@apostrophecms/url:getAllAttachmentMetadata` event is
508
+ // emitted. Handlers receive `(req, results, options)` where
509
+ // `results` is the attachments results array and `options`
510
+ // includes `{ sizes, skipSizes, scope, uploadsUrl }`. This
511
+ // is an escape hatch for edge cases where a module needs to
512
+ // contribute additional attachment entries or modify the
513
+ // results programmatically.
514
+ //
515
+ async getAllUrlMetadata(req, { excludeTypes = [], attachments = false } = {}) {
516
+ // Ensure global doc is available for event handlers
517
+ // that may need it (e.g. @apostrophecms/styles)
518
+ await self.apos.global.addGlobalToData(req);
519
+ const results = [];
520
+ const allAttachmentDocIds = new Set();
521
+ const collectDocIds = !!attachments && attachments.scope !== 'all';
522
+ const types = await self.apos.doc.db.distinct('type');
523
+ for (const type of types) {
524
+ if (!excludeTypes.includes(type)) {
525
+ const manager = self.apos.doc.getManager(type);
526
+ if (!manager?.getAllUrlMetadata) {
527
+ continue;
528
+ }
529
+ const {
530
+ metadata,
531
+ attachmentDocIds
532
+ } = await manager
533
+ .getAllUrlMetadata(req, { attachments: collectDocIds });
534
+ for (const entry of metadata) {
535
+ results.push(entry);
536
+ }
537
+ for (const id of attachmentDocIds) {
538
+ allAttachmentDocIds.add(id);
539
+ }
540
+ }
541
+ }
542
+ await self.emit('getAllUrlMetadata', req, results, { excludeTypes });
543
+
544
+ const response = {
545
+ pages: results,
546
+ attachments: null
547
+ };
548
+
549
+ if (attachments) {
550
+ const {
551
+ sizes, skipSizes, scope
552
+ } = attachments;
553
+
554
+ const docIds = collectDocIds
555
+ ? [ ...allAttachmentDocIds ]
556
+ : undefined;
557
+
558
+ response.attachments = {
559
+ uploadsUrl: self.apos.attachment.uploadfs.getUrl(),
560
+ results: await self.apos.attachment.getStaticMetadata({
561
+ docIds,
562
+ sizes,
563
+ skipSizes
564
+ })
565
+ };
566
+ await self.emit(
567
+ 'getAllAttachmentMetadata',
568
+ req,
569
+ response.attachments.results,
570
+ {
571
+ sizes,
572
+ skipSizes,
573
+ scope,
574
+ uploadsUrl: response.attachments.uploadsUrl
575
+ }
576
+ );
577
+ }
578
+
579
+ // Strip the base URL (origin + prefix) that `_url` values
580
+ // were built with, producing purely relative, prefix-free
581
+ // paths (e.g. `/about`, `/fr/articles/page/2`).
582
+ const effectiveBaseUrl = self.getBaseUrl(req);
583
+ if (effectiveBaseUrl) {
584
+ for (const entry of response.pages) {
585
+ if (entry.url?.startsWith(effectiveBaseUrl)) {
586
+ entry.url = entry.url.slice(effectiveBaseUrl.length) || '/';
587
+ }
588
+ }
589
+ }
590
+
591
+ // Strip the backend origin from uploadsUrl so that the
592
+ // consumer receives a relative, prefix-qualified path
593
+ // (e.g. `/uploads` or `/cms/uploads`).
594
+ // CDN URLs (different origin) and already-relative URLs
595
+ // are left untouched.
596
+ if (response.attachments) {
597
+ const baseUrl = self.apos.baseUrl || '';
598
+ if (baseUrl && response.attachments.uploadsUrl.startsWith(baseUrl)) {
599
+ response.attachments.uploadsUrl =
600
+ response.attachments.uploadsUrl.slice(baseUrl.length);
601
+ }
602
+ }
603
+
604
+ return response;
605
+ },
606
+ // Returns a string suitable to append to the original page URL when we're
607
+ // specifying a particular filter and a page number. Pages start with 1
608
+ getChoiceFilter(name, value, page) {
609
+ if (value === null) {
610
+ return '';
611
+ }
612
+ name = encodeURIComponent(name);
613
+ value = encodeURIComponent(value);
614
+ if (self.options.static) {
615
+ return `/${name}/${value}${page > 1 ? `/page/${page}` : ''}`;
616
+ } else {
617
+ return `?${name}=${value}${page > 1 ? `&page=${page}` : ''}`;
618
+ }
619
+ },
620
+ // Returns a string suitable to append to the original page URL when all we're
621
+ // adding is a page number. Pages start with 1
622
+ getPageFilter(page) {
623
+ if (page <= 1) {
624
+ return '';
625
+ }
626
+ if (self.options.static) {
627
+ return `/page/${page}`;
628
+ } else {
629
+ return `?page=${page}`;
630
+ }
213
631
  }
214
632
  };
215
633
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "4.27.1",
3
+ "version": "4.28.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -110,7 +110,7 @@
110
110
  "tiny-emitter": "^2.1.0",
111
111
  "tough-cookie": "^4.0.0",
112
112
  "underscore.string": "^3.3.4",
113
- "uploadfs": "^1.25.1",
113
+ "uploadfs": "^1.26.1",
114
114
  "void-elements": "^3.1.0",
115
115
  "vue": "^3.5.20",
116
116
  "vue-advanced-cropper": "^2.8.8",
@@ -119,12 +119,12 @@
119
119
  "webpack": "^5.72.0",
120
120
  "webpack-merge": "^5.7.3",
121
121
  "xregexp": "^2.0.0",
122
- "@apostrophecms/emulate-mongo-3-driver": "^1.0.6",
123
- "express-cache-on-demand": "^1.0.4",
124
- "postcss-viewport-to-container-toggle": "^2.2.0",
125
122
  "boring": "^1.1.1",
123
+ "postcss-viewport-to-container-toggle": "^2.3.0",
124
+ "sanitize-html": "^2.17.2",
125
+ "@apostrophecms/emulate-mongo-3-driver": "^1.0.6",
126
126
  "broadband": "^1.1.0",
127
- "sanitize-html": "^2.17.0"
127
+ "express-cache-on-demand": "^1.0.4"
128
128
  },
129
129
  "devDependencies": {
130
130
  "eslint": "^9.39.1",
@@ -0,0 +1,131 @@
1
+ {
2
+ "name": "add-missing-schema-fields-project",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "../..": {
8
+ "version": "4.27.0",
9
+ "license": "MIT",
10
+ "dependencies": {
11
+ "@apostrophecms/emulate-mongo-3-driver": "workspace:^",
12
+ "@apostrophecms/vue-material-design-icons": "^1.0.0",
13
+ "@ctrl/tinycolor": "^4.1.0",
14
+ "@floating-ui/dom": "^1.5.3",
15
+ "@opentelemetry/api": "^1.9.0",
16
+ "@opentelemetry/semantic-conventions": "^1.0.1",
17
+ "@paralleldrive/cuid2": "^2.2.2",
18
+ "@tiptap/extension-color": "^2.4.0",
19
+ "@tiptap/extension-floating-menu": "^2.0.3",
20
+ "@tiptap/extension-highlight": "^2.0.3",
21
+ "@tiptap/extension-link": "^2.0.3",
22
+ "@tiptap/extension-placeholder": "^2.0.3",
23
+ "@tiptap/extension-subscript": "^2.0.3",
24
+ "@tiptap/extension-superscript": "^2.0.3",
25
+ "@tiptap/extension-table": "^2.0.3",
26
+ "@tiptap/extension-table-cell": "^2.0.3",
27
+ "@tiptap/extension-table-header": "^2.0.3",
28
+ "@tiptap/extension-table-row": "^2.0.3",
29
+ "@tiptap/extension-text-align": "^2.0.3",
30
+ "@tiptap/extension-text-style": "^2.0.3",
31
+ "@tiptap/extension-underline": "^2.0.3",
32
+ "@tiptap/starter-kit": "^2.0.3",
33
+ "@tiptap/vue-3": "^2.0.3",
34
+ "@vue/compiler-sfc": "^3.3.8",
35
+ "autoprefixer": "^10.4.1",
36
+ "bluebird": "^3.7.2",
37
+ "body-parser": "^1.18.2",
38
+ "boring": "workspace:^",
39
+ "broadband": "workspace:^",
40
+ "cheerio": "^1.0.0-rc.10",
41
+ "chokidar": "^3.5.2",
42
+ "common-tags": "^1.8.0",
43
+ "concat-with-sourcemaps": "^1.1.0",
44
+ "connect-mongo": "^5.1.0",
45
+ "cookie-parser": "^1.4.5",
46
+ "cors": "^2.8.5",
47
+ "css-loader": "^5.2.4",
48
+ "cssnano": "^7.1.1",
49
+ "csv-parse": "^5.6.0",
50
+ "dayjs": "^1.9.8",
51
+ "dompurify": "^3.2.5",
52
+ "encodeurl": "^2.0.0",
53
+ "express": "^4.16.4",
54
+ "express-bearer-token": "^3.0.0",
55
+ "express-cache-on-demand": "workspace:^",
56
+ "express-session": "^1.18.2",
57
+ "fs-extra": "^7.0.1",
58
+ "glob": "^10.4.5",
59
+ "he": "^1.2.0",
60
+ "html-to-text": "^9.0.5",
61
+ "i18next": "^20.3.2",
62
+ "i18next-http-middleware": "^3.1.5",
63
+ "import-fresh": "^3.3.0",
64
+ "is-wsl": "^2.2.0",
65
+ "jsdom": "^24.1.0",
66
+ "klona": "^2.0.4",
67
+ "launder": "^1.4.0",
68
+ "lodash": "^4.17.21",
69
+ "mini-css-extract-plugin": "^1.6.0",
70
+ "minimatch": "^3.0.4",
71
+ "mkdirp": "^0.5.5",
72
+ "multer": "^2.0.2",
73
+ "node-fetch": "^2.6.1",
74
+ "nodemailer": "^7.0.10",
75
+ "nunjucks": "^3.2.1",
76
+ "oembetter": "^1.1.3",
77
+ "parseurl": "^1.3.3",
78
+ "passport": "^0.6.0",
79
+ "passport-local": "^1.0.0",
80
+ "path-to-regexp": "^1.8.0",
81
+ "performance-now": "^2.1.0",
82
+ "pinia": "^2.1.7",
83
+ "postcss": "^8.4.47",
84
+ "postcss-html": "^1.3.0",
85
+ "postcss-loader": "^8.1.1",
86
+ "postcss-scss": "^4.0.3",
87
+ "postcss-viewport-to-container-toggle": "workspace:^",
88
+ "prompts": "^2.4.1",
89
+ "qs": "^6.10.1",
90
+ "regexp-quote": "0.0.0",
91
+ "resolve": "^1.19.0",
92
+ "resolve-from": "^5.0.0",
93
+ "sanitize-html": "workspace:^",
94
+ "sass": "^1.80.3",
95
+ "sass-loader": "^16.0.0",
96
+ "server-destroy": "^1.0.1",
97
+ "sluggo": "^1.0.0",
98
+ "sortablejs": "^1.15.0",
99
+ "sortablejs-vue3": "^1.2.11",
100
+ "tiny-emitter": "^2.1.0",
101
+ "tough-cookie": "^4.0.0",
102
+ "underscore.string": "^3.3.4",
103
+ "uploadfs": "^1.25.1",
104
+ "void-elements": "^3.1.0",
105
+ "vue": "^3.5.20",
106
+ "vue-advanced-cropper": "^2.8.8",
107
+ "vue-loader": "^17.1.0",
108
+ "vue-style-loader": "^4.1.3",
109
+ "webpack": "^5.72.0",
110
+ "webpack-merge": "^5.7.3",
111
+ "xregexp": "^2.0.0"
112
+ },
113
+ "devDependencies": {
114
+ "eslint": "^9.39.1",
115
+ "eslint-config-apostrophe": "workspace:^",
116
+ "form-data": "^4.0.4",
117
+ "mocha": "^11.7.1",
118
+ "nyc": "^17.1.0",
119
+ "stylelint": "^16.5.0",
120
+ "stylelint-config-apostrophe": "workspace:^"
121
+ },
122
+ "engines": {
123
+ "node": ">=16.0.0"
124
+ }
125
+ },
126
+ "node_modules/apostrophe": {
127
+ "resolved": "../..",
128
+ "link": true
129
+ }
130
+ }
131
+ }
@@ -11,6 +11,7 @@ describe('External Front', function() {
11
11
  this.timeout(t.timeout);
12
12
 
13
13
  after(function() {
14
+ delete process.env.APOS_EXTERNAL_FRONT_KEY;
14
15
  return t.destroy(apos);
15
16
  });
16
17