@dylandepass/helix-product-pipeline 2.2.0 → 2.4.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [2.4.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.3.0...v2.4.0) (2026-03-09)
2
+
3
+
4
+ ### Features
5
+
6
+ * append optional image filenames in HTML and JSON pipeline output ([#72](https://github.com/adobe-rnd/helix-product-pipeline/issues/72)) ([c87e79c](https://github.com/adobe-rnd/helix-product-pipeline/commit/c87e79c9d74cf045135fd3b8049e7c31e1c9e175))
7
+
8
+ # [2.3.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.2.0...v2.3.0) (2026-02-12)
9
+
10
+
11
+ ### Features
12
+
13
+ * add limit/offset pagination and deterministic sort to product index ([#71](https://github.com/adobe-rnd/helix-product-pipeline/issues/71)) ([28499dd](https://github.com/adobe-rnd/helix-product-pipeline/commit/28499dd2140c830ed46ecec03bad4cee1fdd1ba5))
14
+
1
15
  # [2.2.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.1.0...v2.2.0) (2026-02-04)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dylandepass/helix-product-pipeline",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Helix Product Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@adobe/helix-html-pipeline": "6.27.9",
49
49
  "@adobe/helix-shared-utils": "3.0.2",
50
- "@dylandepass/helix-product-shared": "1.5.2",
50
+ "@dylandepass/helix-product-shared": "1.6.0",
51
51
  "dayjs": "1.11.19",
52
52
  "github-slugger": "2.0.0",
53
53
  "hast-util-from-html": "^2.0.3",
@@ -16,6 +16,7 @@ import addHeadingIds from './steps/add-heading-ids.js';
16
16
  import { getPathInfo, validatePathInfo } from './utils/path.js';
17
17
  import initConfig from './steps/init-config.js';
18
18
  import fetchProductBusContent from './steps/fetch-productbus.js';
19
+ import transformImages from './steps/transform-images.js';
19
20
  import fetchEdgeContent from './steps/fetch-edge-product.js';
20
21
  import { setLastModified } from './utils/last-modified.js';
21
22
  import html from './steps/make-html.js';
@@ -71,6 +72,7 @@ export async function productHTMLPipe(state, req) {
71
72
  return res;
72
73
  }
73
74
 
75
+ transformImages(state);
74
76
  state.timer?.update('render');
75
77
  await html(state);
76
78
 
@@ -17,22 +17,33 @@ import initConfig from './steps/init-config.js';
17
17
  import fetchProductBusContent from './steps/fetch-productbus.js';
18
18
  import { setLastModified } from './utils/last-modified.js';
19
19
  import { set404CacheHeaders } from './steps/set-cache-headers.js';
20
- import { getIncludes } from './steps/utils.js';
20
+ import { getIncludes, getPaginationParams } from './steps/utils.js';
21
21
 
22
22
  /**
23
- * Returns index as spreadsheet
23
+ * Returns index as spreadsheet with pagination support.
24
24
  * @param {SharedTypes.StoredIndex} index
25
25
  * @param {Record<string, boolean>} includes
26
+ * @param {{ hasParams?: boolean, limit?: number, offset?: number }} [pagination={}]
27
+ * - Pagination options
26
28
  * @returns {{
27
29
  * ':type': 'sheet',
30
+ * total: number,
31
+ * offset: number,
32
+ * limit: number,
28
33
  * columns: string[],
29
34
  * data: Record<string, string>[]
30
35
  * }}
31
36
  */
32
- export function toSpreadsheet(index, includes) {
37
+ export function toSpreadsheet(index, includes, pagination = {}) {
38
+ const {
39
+ limit = pagination.hasParams ? 1000 : Infinity,
40
+ offset = 0,
41
+ } = pagination;
42
+
33
43
  const columns = new Set(['sku']);
34
44
 
35
45
  const products = Object.entries(index)
46
+ .sort(([a], [b]) => a.localeCompare(b))
36
47
  .filter(([_, entry]) => includes.all || !entry.filters?.noindex || includes.noindex)
37
48
  .reduce((acc, [sluggedSku, entry]) => {
38
49
  const product = entry.data;
@@ -80,10 +91,17 @@ export function toSpreadsheet(index, includes) {
80
91
  return acc;
81
92
  }, []);
82
93
 
94
+ const total = products.length;
95
+ const len = Math.min(limit, total - offset);
96
+ const paginatedData = products.slice(offset, offset + len);
97
+
83
98
  return {
84
99
  ':type': 'sheet',
100
+ total,
101
+ offset,
102
+ limit: paginatedData.length,
85
103
  columns: Array.from(columns),
86
- data: products,
104
+ data: paginatedData,
87
105
  };
88
106
  }
89
107
 
@@ -135,7 +153,9 @@ export async function productIndexPipe(state, req) {
135
153
 
136
154
  setLastModified(state, res);
137
155
 
138
- res.body = JSON.stringify(toSpreadsheet(state.content.data, getIncludes(req)), null, 2);
156
+ const pagination = getPaginationParams(req);
157
+ const spreadsheet = toSpreadsheet(state.content.data, getIncludes(req), pagination);
158
+ res.body = JSON.stringify(spreadsheet, null, 2);
139
159
  } catch (e) {
140
160
  const errorRes = new PipelineResponse('', {
141
161
  /* c8 ignore next 6 */
@@ -15,6 +15,7 @@ import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
15
15
  import { validatePathInfo } from './utils/path.js';
16
16
  import initConfig from './steps/init-config.js';
17
17
  import fetchProductBusContent from './steps/fetch-productbus.js';
18
+ import transformImages from './steps/transform-images.js';
18
19
  import { setLastModified } from './utils/last-modified.js';
19
20
  import { set404CacheHeaders, setProductCacheHeaders } from './steps/set-cache-headers.js';
20
21
 
@@ -60,6 +61,8 @@ export async function productJSONPipe(state, req) {
60
61
  throw new PipelineStatusError(res.status, res.error);
61
62
  }
62
63
 
64
+ transformImages(state);
65
+
63
66
  // set surrogate keys
64
67
  await setProductCacheHeaders(state, req, res);
65
68
 
@@ -55,9 +55,8 @@ export function createOptimizedPicture(src, alt = '', title = undefined) {
55
55
  height = props.get('height');
56
56
  }
57
57
 
58
- // Extract filename and check if it starts with 'media_'
59
- const filename = pathname.substring(pathname.lastIndexOf('/') + 1);
60
- const isMediaBusFile = filename.startsWith('media_');
58
+ // Matches media bus URLs: /media_{sha1hash}.ext or /media_{sha1hash}/filename.ext
59
+ const isMediaBusFile = /^\/media_[a-f0-9]+(?:\/[^/]+)?\.[a-z0-9]+$/i.test(pathname);
61
60
 
62
61
  // If not a media file, return a simple picture with just an img tag (no optimization)
63
62
  if (!isMediaBusFile) {
@@ -0,0 +1,34 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { appendFilenameToMediaUrl } from '@dylandepass/helix-product-shared';
14
+
15
+ function transformImage(image) {
16
+ if (!image || typeof image !== 'object') {
17
+ return;
18
+ }
19
+ image.url = appendFilenameToMediaUrl(image.url, image.filename);
20
+ }
21
+
22
+ export default function transformImages(state) {
23
+ const data = state?.content?.data;
24
+ if (!data || typeof data !== 'object') {
25
+ return;
26
+ }
27
+
28
+ if (Array.isArray(data.images)) {
29
+ data.images.forEach(transformImage);
30
+ }
31
+ if (Array.isArray(data.variants)) {
32
+ data.variants.forEach((variant) => (variant?.images || []).forEach(transformImage));
33
+ }
34
+ }
@@ -125,3 +125,36 @@ export function getIncludes(req) {
125
125
  }
126
126
  return includes;
127
127
  }
128
+
129
+ /**
130
+ * Get pagination params from request URL.
131
+ * @param {PipelineRequest} req
132
+ * @returns {{ hasParams?: boolean, limit?: number, offset?: number }}
133
+ */
134
+ export function getPaginationParams(req) {
135
+ const { searchParams } = req.url;
136
+ const hasParams = searchParams.has('limit') || searchParams.has('offset');
137
+ if (!hasParams) {
138
+ return {};
139
+ }
140
+
141
+ const result = { hasParams: true };
142
+
143
+ const limitStr = searchParams.get('limit');
144
+ if (limitStr !== null) {
145
+ const limit = parseInt(limitStr, 10);
146
+ if (!Number.isNaN(limit) && limit >= 0) {
147
+ result.limit = limit;
148
+ }
149
+ }
150
+
151
+ const offsetStr = searchParams.get('offset');
152
+ if (offsetStr !== null) {
153
+ const offset = parseInt(offsetStr, 10);
154
+ if (!Number.isNaN(offset) && offset >= 0) {
155
+ result.offset = offset;
156
+ }
157
+ }
158
+
159
+ return result;
160
+ }