@shopware/cms-base-layer 3.0.1 → 4.0.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 (52) hide show
  1. package/README.md +78 -15
  2. package/app/app.config.ts +3 -3
  3. package/app/assets/css/rich-text.css +80 -0
  4. package/app/components/SwContactForm.vue +2 -1
  5. package/app/components/SwFilterChips.vue +29 -7
  6. package/app/components/SwNewsletterForm.vue +2 -1
  7. package/app/components/SwProductCard.vue +19 -3
  8. package/app/components/SwProductCardImage.vue +5 -1
  9. package/app/components/SwProductListingFilter.vue +5 -0
  10. package/app/components/SwProductListingFilters.vue +76 -74
  11. package/app/components/SwProductListingFiltersHorizontal.vue +73 -72
  12. package/app/components/SwStockInfo.vue +5 -2
  13. package/app/components/SwVariantConfigurator.vue +8 -3
  14. package/app/components/listing-filters/SwFilterCategories.vue +158 -0
  15. package/app/components/public/cms/CmsGenericBlock.vue +1 -1
  16. package/app/components/public/cms/CmsGenericElement.vue +1 -1
  17. package/app/components/public/cms/CmsNoComponent.vue +1 -1
  18. package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +1 -1
  19. package/app/components/public/cms/block/CmsBlockCenterText.vue +2 -1
  20. package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +5 -1
  21. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +6 -2
  22. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +9 -5
  23. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +7 -2
  24. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +9 -3
  25. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +7 -2
  26. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +7 -3
  27. package/app/components/public/cms/block/CmsBlockProductHeading.vue +2 -1
  28. package/app/components/public/cms/element/CmsElementBuyBox.vue +4 -1
  29. package/app/components/public/cms/element/CmsElementCrossSelling.vue +2 -1
  30. package/app/components/public/cms/element/CmsElementHtml.vue +1 -1
  31. package/app/components/public/cms/element/CmsElementImage.vue +41 -2
  32. package/app/components/public/cms/element/CmsElementImageGallery.vue +2 -1
  33. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +18 -9
  34. package/app/components/public/cms/element/CmsElementText.md +77 -0
  35. package/app/components/public/cms/element/CmsElementText.vue +5 -32
  36. package/app/helpers/html-to-vue/renderToHtml.test.ts +55 -0
  37. package/app/helpers/html-to-vue/renderToHtml.ts +5 -1
  38. package/app/helpers/html-to-vue/renderer.ts +10 -1
  39. package/app/utils/listingFilters.test.ts +39 -0
  40. package/app/utils/listingFilters.ts +19 -0
  41. package/app/utils/routeQuery.test.ts +65 -0
  42. package/app/utils/routeQuery.ts +26 -0
  43. package/app/utils/useSelectedListingFilters.test.ts +92 -0
  44. package/app/utils/useSelectedListingFilters.ts +111 -0
  45. package/component-dirs.json +20 -0
  46. package/index.d.ts +2 -2
  47. package/nuxt.config.ts +35 -29
  48. package/package.json +27 -31
  49. package/dist/index.d.mts +0 -5
  50. package/dist/index.d.ts +0 -5
  51. package/dist/index.mjs +0 -31
  52. package/index.cjs +0 -7
@@ -32,6 +32,10 @@ const CmsTextRender = defineComponent({
32
32
  const { resolveUrl } = useUrlResolver();
33
33
 
34
34
  const config = {
35
+ container: {
36
+ type: "div",
37
+ class: "cms-element-text",
38
+ },
35
39
  textTransformer: (text: string) => decodeHTML(text),
36
40
  extraComponentsMap: {
37
41
  link: {
@@ -148,7 +152,7 @@ const CmsTextRender = defineComponent({
148
152
  const rawHtml =
149
153
  mappedContent.value?.length > 0
150
154
  ? mappedContent.value
151
- : "<div class='cms-element-text missing-content-element'></div>";
155
+ : "<div class='missing-content-element'></div>";
152
156
 
153
157
  return () => renderHtml(rawHtml, config, h, context, resolveUrl);
154
158
  },
@@ -160,34 +164,3 @@ const CmsTextRender = defineComponent({
160
164
  </div>
161
165
  <CmsTextRender v-else />
162
166
  </template>
163
- <style scoped>
164
- /** Global CSS styles for text elements */
165
- h1,
166
- h2,
167
- h3,
168
- h4,
169
- h5 {
170
- margin-bottom: 10px;
171
- font-weight: 600;
172
- }
173
- h1 {
174
- line-height: 2.5rem;
175
- font-size: 2.25rem;
176
- }
177
- h2 {
178
- line-height: 2rem;
179
- font-size: 1.75rem;
180
- }
181
- h3 {
182
- line-height: 1.5rem;
183
- font-size: 1.25rem;
184
- }
185
- ol,
186
- ul,
187
- dl {
188
- list-style-type: disc;
189
- padding-left: 40px;
190
- margin-top: 0;
191
- margin-bottom: 1rem;
192
- }
193
- </style>
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { h } from "vue";
3
+
4
+ import { renderHtml } from "./renderToHtml";
5
+
6
+ describe("renderHtml", () => {
7
+ const resolveUrl = (url: string) => url;
8
+
9
+ const render = (
10
+ html: string,
11
+ config: Parameters<typeof renderHtml>[1] = {},
12
+ ) => renderHtml(html, config, h, null, resolveUrl);
13
+
14
+ it("wraps the content in a plain div by default", () => {
15
+ const vnode = render("<p>content</p>");
16
+
17
+ expect(vnode.type).toBe("div");
18
+ expect(vnode.props?.class).toBeUndefined();
19
+ });
20
+
21
+ it("applies container.class to the wrapper element", () => {
22
+ const vnode = render("<h1>headline</h1>", {
23
+ container: { type: "div", class: "cms-element-text" },
24
+ });
25
+
26
+ expect(vnode.props?.class).toBe("cms-element-text");
27
+ });
28
+
29
+ it("honours a custom container type", () => {
30
+ const vnode = render("<p>content</p>", {
31
+ container: { type: "section", class: "cms-element-text" },
32
+ });
33
+
34
+ expect(vnode.type).toBe("section");
35
+ });
36
+
37
+ it("does not leak container.class into later renders", () => {
38
+ render("<p>scoped</p>", {
39
+ container: { type: "div", class: "cms-element-text" },
40
+ });
41
+
42
+ const vnode = render("<p>unscoped</p>");
43
+
44
+ expect(vnode.props?.class).toBeUndefined();
45
+ });
46
+
47
+ it("keeps rendering the parsed children next to the container class", () => {
48
+ const vnode = render("<h2>headline</h2>", {
49
+ container: { type: "div", class: "cms-element-text" },
50
+ });
51
+
52
+ expect(Array.isArray(vnode.children)).toBe(true);
53
+ expect((vnode.children as { type: string }[])[0]?.type).toBe("h2");
54
+ });
55
+ });
@@ -25,7 +25,11 @@ export function renderHtml(
25
25
  context: ComponentInternalInstance | null,
26
26
  resolveUrl: (url: string) => string,
27
27
  ) {
28
- const mergedConfig = Object.assign(defaultConfig, config);
28
+ const mergedConfig: DefaultConfig = {
29
+ ...defaultConfig,
30
+ ...config,
31
+ container: { ...defaultConfig.container, ...config.container },
32
+ };
29
33
  const _ast = generateAST(html);
30
34
  const rectifyConfig: RectifyConfig = {
31
35
  extraComponentsMap: config.extraComponentsMap,
@@ -27,6 +27,7 @@ export type ExtraComponentConfig = {
27
27
  export type RendererConfig = {
28
28
  container: {
29
29
  type: string;
30
+ class?: string;
30
31
  };
31
32
  extraComponentsMap: Record<string, ExtraComponentConfig>;
32
33
  renderAnyway: boolean;
@@ -113,5 +114,13 @@ export function renderer(
113
114
  const rendered = _render(createElement, ast);
114
115
  const children = flattenChildren(rendered);
115
116
 
116
- return createElement(config.container.type, context?.data || {}, children);
117
+ const containerProps: Record<string, unknown> = { ...(context?.data || {}) };
118
+
119
+ if (config.container.class) {
120
+ containerProps.class = containerProps.class
121
+ ? [containerProps.class, config.container.class]
122
+ : config.container.class;
123
+ }
124
+
125
+ return createElement(config.container.type, containerProps, children);
117
126
  }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getVisibleListingFilters } from "./listingFilters";
4
+
5
+ describe("getVisibleListingFilters", () => {
6
+ const filters = [
7
+ { code: "manufacturer" },
8
+ { code: "categories" },
9
+ { code: "price" },
10
+ ];
11
+
12
+ it("should keep the categories filter on a search listing", () => {
13
+ expect(
14
+ getVisibleListingFilters(filters, { isProductSearch: true }),
15
+ ).toStrictEqual(filters);
16
+ });
17
+
18
+ it("should drop the categories filter on a non-search listing", () => {
19
+ expect(
20
+ getVisibleListingFilters(filters, { isProductSearch: false }),
21
+ ).toStrictEqual([{ code: "manufacturer" }, { code: "price" }]);
22
+ });
23
+
24
+ it("should leave listings without a categories filter untouched", () => {
25
+ const withoutCategories = [{ code: "manufacturer" }, { code: "rating" }];
26
+ expect(
27
+ getVisibleListingFilters(withoutCategories, { isProductSearch: false }),
28
+ ).toStrictEqual(withoutCategories);
29
+ });
30
+
31
+ it("should return an empty array for missing filters", () => {
32
+ expect(
33
+ getVisibleListingFilters(undefined, { isProductSearch: true }),
34
+ ).toStrictEqual([]);
35
+ expect(
36
+ getVisibleListingFilters(null, { isProductSearch: false }),
37
+ ).toStrictEqual([]);
38
+ });
39
+ });
@@ -0,0 +1,19 @@
1
+ import { CATEGORY_AGGREGATION_NAME } from "@shopware/helpers";
2
+
3
+ /**
4
+ * Narrows the listing filters to the ones the current listing can act on.
5
+ *
6
+ * The category selection is only written to the `categories` URL param and to
7
+ * the criteria `post-filter` on search listings. A category listing that
8
+ * happens to request the category aggregations would otherwise render a
9
+ * checkbox that the next query sync resets and that never changes the result
10
+ * set, so the filter is dropped there.
11
+ */
12
+ export const getVisibleListingFilters = <T extends { code: string }>(
13
+ filters: T[] | undefined | null,
14
+ { isProductSearch }: { isProductSearch: boolean },
15
+ ): T[] => {
16
+ if (!filters) return [];
17
+ if (isProductSearch) return filters;
18
+ return filters.filter((filter) => filter.code !== CATEGORY_AGGREGATION_NAME);
19
+ };
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { firstQueryValue, toNumber } from "./routeQuery";
4
+
5
+ describe("firstQueryValue", () => {
6
+ it("returns a plain string value", () => {
7
+ expect(firstQueryValue("name-asc")).toBe("name-asc");
8
+ });
9
+
10
+ it("returns the first element of a repeated (array) param", () => {
11
+ expect(firstQueryValue(["30", "45"])).toBe("30");
12
+ });
13
+
14
+ it("returns undefined for undefined", () => {
15
+ expect(firstQueryValue(undefined)).toBeUndefined();
16
+ });
17
+
18
+ it("returns undefined for null", () => {
19
+ expect(firstQueryValue(null)).toBeUndefined();
20
+ });
21
+
22
+ it("returns undefined for an empty array", () => {
23
+ expect(firstQueryValue([])).toBeUndefined();
24
+ });
25
+
26
+ it("returns undefined when the first array element is null", () => {
27
+ expect(firstQueryValue([null])).toBeUndefined();
28
+ });
29
+ });
30
+
31
+ describe("toNumber", () => {
32
+ it("parses an integer string", () => {
33
+ expect(toNumber("15")).toBe(15);
34
+ });
35
+
36
+ it("parses a decimal string", () => {
37
+ expect(toNumber("19.99")).toBe(19.99);
38
+ });
39
+
40
+ it("parses zero and negative values", () => {
41
+ expect(toNumber("0")).toBe(0);
42
+ expect(toNumber("-5")).toBe(-5);
43
+ });
44
+
45
+ it("returns undefined for undefined", () => {
46
+ expect(toNumber(undefined)).toBeUndefined();
47
+ });
48
+
49
+ it("returns undefined for an empty string", () => {
50
+ expect(toNumber("")).toBeUndefined();
51
+ });
52
+
53
+ it("returns undefined for a non-numeric string", () => {
54
+ expect(toNumber("abc")).toBeUndefined();
55
+ expect(toNumber("12px")).toBeUndefined();
56
+ });
57
+
58
+ it("returns undefined for non-finite values", () => {
59
+ expect(toNumber("Infinity")).toBeUndefined();
60
+ });
61
+
62
+ it("composes with firstQueryValue for repeated numeric params", () => {
63
+ expect(toNumber(firstQueryValue(["30", "45"]))).toBe(30);
64
+ });
65
+ });
@@ -0,0 +1,26 @@
1
+ import type { LocationQuery } from "vue-router";
2
+
3
+ /**
4
+ * Collapses a route-query param to a single string.
5
+ *
6
+ * Router query values are `string | null`, an array when a param is repeated
7
+ * (e.g. `?p=1&p=2`), or `undefined` when absent. Returns the first usable
8
+ * string, or `undefined` for missing/empty/null values.
9
+ */
10
+ export const firstQueryValue = (
11
+ value: LocationQuery[string] | undefined,
12
+ ): string | undefined => {
13
+ const raw = Array.isArray(value) ? value[0] : value;
14
+ return raw ?? undefined;
15
+ };
16
+
17
+ /**
18
+ * Parses a query string (typically from {@link firstQueryValue}) into a finite
19
+ * number, or `undefined` when missing/empty/non-numeric. Lets callers fall back
20
+ * with `??` instead of forwarding `NaN` into a request.
21
+ */
22
+ export const toNumber = (value: string | undefined): number | undefined => {
23
+ if (value === undefined || value === "") return undefined;
24
+ const parsed = Number(value);
25
+ return Number.isFinite(parsed) ? parsed : undefined;
26
+ };
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ applyQueryToFilters,
5
+ createEmptyFilterState,
6
+ } from "./useSelectedListingFilters";
7
+
8
+ describe("applyQueryToFilters", () => {
9
+ it("populates sets from pipe-joined values", () => {
10
+ const state = createEmptyFilterState();
11
+ applyQueryToFilters(state, {
12
+ manufacturer: "a|b",
13
+ properties: "x",
14
+ categories: "c1|c2",
15
+ });
16
+ expect([...state.manufacturer]).toEqual(["a", "b"]);
17
+ expect([...state.properties]).toEqual(["x"]);
18
+ expect([...state.categories]).toEqual(["c1", "c2"]);
19
+ });
20
+
21
+ it("clears removed filters when the query no longer has them (Back/Forward)", () => {
22
+ const state = createEmptyFilterState();
23
+ applyQueryToFilters(state, {
24
+ manufacturer: "a|b",
25
+ categories: "c1",
26
+ search: "shirt",
27
+ });
28
+ applyQueryToFilters(state, { search: "shirt" }); // manufacturer + categories removed
29
+ expect(state.manufacturer.size).toBe(0);
30
+ expect(state.properties.size).toBe(0);
31
+ expect(state.categories.size).toBe(0);
32
+ });
33
+
34
+ it("clears removed scalar filters on resync", () => {
35
+ const state = createEmptyFilterState();
36
+ applyQueryToFilters(state, {
37
+ "min-price": "10",
38
+ "max-price": "20",
39
+ rating: "4",
40
+ "shipping-free": "true",
41
+ });
42
+ applyQueryToFilters(state, {});
43
+ expect(state["min-price"]).toBeUndefined();
44
+ expect(state["max-price"]).toBeUndefined();
45
+ expect(state.rating).toBeUndefined();
46
+ expect(state["shipping-free"]).toBeUndefined();
47
+ });
48
+
49
+ it("keeps the Set identity stable across re-applies (reactive binding)", () => {
50
+ const state = createEmptyFilterState();
51
+ const manufacturerRef = state.manufacturer;
52
+ const propertiesRef = state.properties;
53
+ applyQueryToFilters(state, { manufacturer: "a" });
54
+ expect(state.manufacturer).toBe(manufacturerRef);
55
+ expect(state.properties).toBe(propertiesRef);
56
+ });
57
+
58
+ it("drops empty members from pipe-joined values", () => {
59
+ const state = createEmptyFilterState();
60
+ applyQueryToFilters(state, { manufacturer: "a||b|" });
61
+ expect([...state.manufacturer]).toEqual(["a", "b"]);
62
+ });
63
+
64
+ it("parses scalars array/NaN-safe and ignores junk", () => {
65
+ const state = createEmptyFilterState();
66
+ applyQueryToFilters(state, {
67
+ "min-price": ["10", "20"],
68
+ "max-price": "abc",
69
+ rating: "4",
70
+ "shipping-free": "true",
71
+ });
72
+ expect(state["min-price"]).toBe(10); // first of repeated param
73
+ expect(state["max-price"]).toBeUndefined(); // NaN -> undefined
74
+ expect(state.rating).toBe(4);
75
+ expect(state["shipping-free"]).toBe(true);
76
+ });
77
+
78
+ it("treats shipping-free other than 'true' as false", () => {
79
+ const state = createEmptyFilterState();
80
+ applyQueryToFilters(state, { "shipping-free": "false" });
81
+ expect(state["shipping-free"]).toBe(false);
82
+ });
83
+
84
+ it("is idempotent (same query twice -> equal state)", () => {
85
+ const state = createEmptyFilterState();
86
+ const query = { manufacturer: "a|b", rating: "4" };
87
+ applyQueryToFilters(state, query);
88
+ applyQueryToFilters(state, query);
89
+ expect([...state.manufacturer]).toEqual(["a", "b"]);
90
+ expect(state.rating).toBe(4);
91
+ });
92
+ });
@@ -0,0 +1,111 @@
1
+ import { reactive, watch } from "vue";
2
+ import type { UnwrapNestedRefs } from "vue";
3
+ import { useRoute } from "vue-router";
4
+ import type { LocationQuery } from "vue-router";
5
+
6
+ import { firstQueryValue, toNumber } from "./routeQuery";
7
+
8
+ export type FilterState = {
9
+ manufacturer: Set<string>;
10
+ properties: Set<string>;
11
+ categories: Set<string>;
12
+ "min-price": number | undefined;
13
+ "max-price": number | undefined;
14
+ rating: number | undefined;
15
+ "shipping-free": boolean | undefined;
16
+ };
17
+
18
+ /** Fresh, fully-empty filter state. The single source of "no selection". */
19
+ export const createEmptyFilterState = (): FilterState => ({
20
+ manufacturer: new Set<string>(),
21
+ properties: new Set<string>(),
22
+ categories: new Set<string>(),
23
+ "min-price": undefined,
24
+ "max-price": undefined,
25
+ rating: undefined,
26
+ "shipping-free": undefined,
27
+ });
28
+
29
+ /**
30
+ * Resets `state` to empty, then repopulates it from `query`. Pure with respect
31
+ * to the router: it only mutates `state`, never navigates or fetches.
32
+ *
33
+ * Reset-first is what clears filters whose query param vanished (e.g. browser
34
+ * Back/Forward), which the old add-only loop left lingering. Idempotent:
35
+ * applying the same query twice yields identical contents, so it is safe to
36
+ * call from a route.query watcher that also fires on the user's own
37
+ * executeSearch() push.
38
+ *
39
+ * Array-valued params (e.g. ?manufacturer=a&manufacturer=b) collapse to the
40
+ * first value via firstQueryValue; numeric params are NaN-safe via toNumber -
41
+ * consistent with the rest of the listing parsing.
42
+ */
43
+ export const applyQueryToFilters = (
44
+ state: FilterState,
45
+ query: LocationQuery,
46
+ ): void => {
47
+ // 1) Reset every field. Clear the Sets in place so their identity is kept
48
+ // (props bound to them stay reactive); replace scalars with undefined.
49
+ state.manufacturer.clear();
50
+ state.properties.clear();
51
+ state.categories.clear();
52
+ state["min-price"] = undefined;
53
+ state["max-price"] = undefined;
54
+ state.rating = undefined;
55
+ state["shipping-free"] = undefined;
56
+
57
+ // 2) Repopulate from the current query.
58
+ for (const code of ["manufacturer", "properties", "categories"] as const) {
59
+ const value = firstQueryValue(query[code]);
60
+ if (!value) continue;
61
+ for (const element of value.split("|")) {
62
+ if (element) state[code].add(element);
63
+ }
64
+ }
65
+
66
+ const minPrice = toNumber(firstQueryValue(query["min-price"]));
67
+ if (minPrice !== undefined) state["min-price"] = minPrice;
68
+
69
+ const maxPrice = toNumber(firstQueryValue(query["max-price"]));
70
+ if (maxPrice !== undefined) state["max-price"] = maxPrice;
71
+
72
+ const rating = toNumber(firstQueryValue(query.rating));
73
+ if (rating !== undefined) state.rating = rating;
74
+
75
+ const shippingFree = firstQueryValue(query["shipping-free"]);
76
+ if (shippingFree !== undefined) {
77
+ state["shipping-free"] = shippingFree === "true";
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Returns the reactive filter state, populated from the current route.query at
83
+ * setup (runs on server AND client -> identical first render, no hydration
84
+ * mismatch) and kept in sync on every subsequent query-only navigation
85
+ * (Back/Forward, manual URL edits, the component's own executeSearch push).
86
+ *
87
+ * The watcher is intentionally NOT immediate: the setup-time apply already
88
+ * covers the first render, and an immediate watcher would re-run during
89
+ * hydration. It only resyncs UI state - it never navigates or fetches - so it
90
+ * cannot loop with executeSearch() nor cause an extra listing request.
91
+ */
92
+ export const useSelectedListingFilters = (): UnwrapNestedRefs<FilterState> => {
93
+ const route = useRoute();
94
+ // Fresh state per call, deliberately: the sidebar and the horizontal filter
95
+ // bar each hold their own selection object, so a mutation in one is invisible
96
+ // to the other until it reaches the route. The URL is what synchronises them.
97
+ const state = reactive<FilterState>(createEmptyFilterState());
98
+
99
+ // Initial parse (SSR + first client render).
100
+ applyQueryToFilters(state as FilterState, route.query);
101
+
102
+ // Resync on query-only route changes while the component stays mounted.
103
+ watch(
104
+ () => route.query,
105
+ (query) => {
106
+ applyQueryToFilters(state as FilterState, query);
107
+ },
108
+ );
109
+
110
+ return state;
111
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "dirs": [
3
+ {
4
+ "path": "./app/components",
5
+ "pattern": "Sw*",
6
+ "global": true
7
+ },
8
+ {
9
+ "path": "./app/components/public",
10
+ "pathPrefix": false,
11
+ "global": true
12
+ },
13
+ {
14
+ "path": "./app/components/ui",
15
+ "prefix": "Sw",
16
+ "global": true
17
+ }
18
+ ],
19
+ "excluded": ["SwMedia3D"]
20
+ }
package/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  /// <reference types="@nuxt/schema" />
2
2
 
3
- export * from "@shopware/composables";
4
- export * from "./.nuxt/imports";
5
3
 
6
4
  type CmsBaseLayerAppConfig = {
7
5
  /** Placeholder shown while CMS images are loading */
@@ -42,3 +40,5 @@ declare module "@nuxt/schema" {
42
40
  interface AppConfig extends CmsBaseLayerAppConfig {}
43
41
  interface AppConfigInput extends CmsBaseLayerAppConfig {}
44
42
  }
43
+
44
+ export {};
package/nuxt.config.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { createResolver } from "@nuxt/kit";
2
2
  import type { NuxtConfig } from "@nuxt/schema";
3
3
  import { defineNuxtConfig } from "nuxt/config";
4
+ import type { LoggingFunction, RollupLog } from "rollup";
5
+
6
+ import componentDirs from "./component-dirs.json";
4
7
 
5
8
  const { resolve: resolveLayer } = createResolver(import.meta.url);
6
9
 
@@ -9,20 +12,20 @@ export default defineNuxtConfig({
9
12
  // to prevent bundling heavy 3D libraries in the initial bundle.
10
13
  // If you need 3D support, add "@tresjs/nuxt" to your app's nuxt.config.ts modules array
11
14
  // and dynamically import SwMedia3D using defineAsyncComponent.
12
- modules: ["@unocss/nuxt", "@nuxt/image"],
15
+ //
16
+ // @unocss/nuxt is not included here either. This layer only ships CMS components,
17
+ // the UnoCSS setup lives in @shopware/unocss-design-tokens-layer (or your own config),
18
+ // so apps that don't use UnoCSS can still extend this layer.
19
+ modules: ["@nuxt/image"],
20
+ css: [resolveLayer("./app/assets/css/rich-text.css")],
13
21
 
14
22
  hooks: {
15
23
  "components:extend"(components) {
16
24
  // Exclude SwMedia3D from auto-import to prevent bundling heavy 3D libraries
17
25
  // It should be dynamically imported when needed using defineAsyncComponent.
18
- const index = components.findIndex(
19
- (c) =>
20
- c.pascalName === "SwMedia3D" ||
21
- c.kebabName === "sw-media3-d" ||
22
- c.filePath?.includes("SwMedia3D.vue"),
23
- );
24
- if (index > -1) {
25
- components.splice(index, 1);
26
+ for (const name of componentDirs.excluded) {
27
+ const index = components.findIndex((c) => c.pascalName === name);
28
+ if (index > -1) components.splice(index, 1);
26
29
  }
27
30
  },
28
31
  },
@@ -85,26 +88,11 @@ export default defineNuxtConfig({
85
88
  },
86
89
  },
87
90
 
88
- components: [
89
- {
90
- path: resolveLayer("./app/components"),
91
- pattern: "Sw*",
92
- extensions: [".vue"],
93
- global: true,
94
- },
95
- {
96
- path: resolveLayer("./app/components/ui"),
97
- extensions: [".vue"],
98
- prefix: "Sw",
99
- global: true,
100
- },
101
- {
102
- path: resolveLayer("./app/components/public"),
103
- pathPrefix: false,
104
- global: true,
105
- extensions: [".vue"],
106
- },
107
- ],
91
+ components: componentDirs.dirs.map(({ path, ...dir }) => ({
92
+ ...dir,
93
+ path: resolveLayer(path),
94
+ extensions: [".vue"],
95
+ })),
108
96
  alias: {
109
97
  "@cms-assets": resolveLayer("./app/assets"),
110
98
  },
@@ -115,6 +103,24 @@ export default defineNuxtConfig({
115
103
  optimizeDeps: {
116
104
  include: ["xss"],
117
105
  },
106
+ build: {
107
+ // Async SwMedia3D/three chunk is ~977 kB after minify; that split is intentional.
108
+ chunkSizeWarningLimit: 1000,
109
+ },
110
+ },
111
+ nitro: {
112
+ rollupConfig: {
113
+ // @vueuse/shared still embeds @__NO_SIDE_EFFECTS__ inside JSDoc (injectLocal).
114
+ // Nitro/Rollup treats that as a misplaced annotation. Track upstream vueuse.
115
+ onwarn(warning: RollupLog, warn: LoggingFunction) {
116
+ if (
117
+ warning.message?.includes("annotation that Rollup cannot interpret")
118
+ ) {
119
+ return;
120
+ }
121
+ warn(warning);
122
+ },
123
+ },
118
124
  },
119
125
  telemetry: {
120
126
  enabled: false,