@lwrjs/shared-utils 0.21.7 → 0.22.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.
package/build/cjs/fs.cjs CHANGED
@@ -33,6 +33,7 @@ __export(exports, {
33
33
  getViewSourceFromFile: () => getViewSourceFromFile,
34
34
  hashContent: () => hashContent,
35
35
  isLocalPath: () => isLocalPath,
36
+ joinUrlPath: () => joinUrlPath,
36
37
  mimeLookup: () => import_mime_types.lookup,
37
38
  normalizeAssetSpecifier: () => normalizeAssetSpecifier,
38
39
  normalizeDirectory: () => normalizeDirectory,
@@ -42,7 +43,8 @@ __export(exports, {
42
43
  readFile: () => readFile,
43
44
  resolveFileExtension: () => resolveFileExtension,
44
45
  streamToString: () => streamToString,
45
- stringToStream: () => stringToStream
46
+ stringToStream: () => stringToStream,
47
+ toForwardSlash: () => toForwardSlash
46
48
  });
47
49
  var import_fs_extra = __toModule(require("fs-extra"));
48
50
  var import_path = __toModule(require("path"));
@@ -173,6 +175,20 @@ function normalizeResourcePath(rawPath, {rootDir, assets, contentDir, layoutsDir
173
175
  return alias;
174
176
  });
175
177
  }
178
+ function toForwardSlash(p) {
179
+ return p.replace(/\\/g, "/").replace(/%5C/gi, "/");
180
+ }
181
+ function joinUrlPath(...segments) {
182
+ const parts = segments.filter((s) => s !== "");
183
+ return parts.map((seg, i) => {
184
+ let s = toForwardSlash(seg);
185
+ if (i > 0 && !s.startsWith("/"))
186
+ s = `/${s}`;
187
+ if (i < parts.length - 1)
188
+ s = s.replace(/\/$/, "");
189
+ return s;
190
+ }).join("");
191
+ }
176
192
  function logMetrics(filePath) {
177
193
  if (import_diagnostics2.logger.isDebugEnabled()) {
178
194
  let count = files.get(filePath) || 0;
@@ -191,11 +207,11 @@ function normalizeAssetSpecifier(assetId, assetPathMap, resourcePaths, basePath)
191
207
  if (!importer) {
192
208
  throw Error(`Unable to resolve relative import "${specifier}" without an importer.`);
193
209
  }
194
- return import_path.default.join(import_path.default.dirname(importer), specifier);
210
+ return toForwardSlash(import_path.default.join(import_path.default.dirname(importer), specifier));
195
211
  }
196
212
  if (type === "content-asset") {
197
213
  const originSpecifier = !basePath ? specifier : specifier.split(basePath)[1];
198
- return import_path.default.join(resourcePaths.contentDir, originSpecifier);
214
+ return toForwardSlash(import_path.default.join(resourcePaths.contentDir, originSpecifier));
199
215
  }
200
216
  if (specifier[0] === "$") {
201
217
  return normalizeResourcePath(specifier, resourcePaths);
@@ -29,13 +29,14 @@ __export(exports, {
29
29
  getSsgAppRelativePath: () => getSsgAppRelativePath
30
30
  });
31
31
  var import_path = __toModule(require("path"));
32
+ var import_fs = __toModule(require("./fs.cjs"));
32
33
  var MRT_BASE_URL = `/mobify/bundle/${process.env.BUNDLE_ID || "development"}/`;
33
34
  var DEFAULT_SSG_ROOT = "site";
34
35
  function getSsgAppRelativePath(staticSiteGenerator) {
35
36
  return staticSiteGenerator?.outputDir || DEFAULT_SSG_ROOT;
36
37
  }
37
38
  function getMrtArtifactUrl(basePath, ssgRoot, artifactPath = "") {
38
- return import_path.default.join(basePath, MRT_BASE_URL, ssgRoot, artifactPath);
39
+ return (0, import_fs.joinUrlPath)(basePath, MRT_BASE_URL, ssgRoot, artifactPath);
39
40
  }
40
41
  function getMrtSsgRoot(staticSiteGenerator) {
41
42
  return import_path.default.join(getSsgAppRelativePath(staticSiteGenerator));
@@ -37,9 +37,9 @@ __export(exports, {
37
37
  toHostname: () => toHostname
38
38
  });
39
39
  var import_path_to_regexp = __toModule(require("path-to-regexp"));
40
- var import_path = __toModule(require("path"));
41
40
  var import_os = __toModule(require("os"));
42
41
  var import_url = __toModule(require("url"));
42
+ var import_fs = __toModule(require("./fs.cjs"));
43
43
  var CONFIG_SUFFIX = "/config.js";
44
44
  var SIGNATURE_SIGIL = "s";
45
45
  function getClientBootstrapConfigurationUri(routeInfo, runtimeEnvironment, runtimeParams, signature) {
@@ -114,9 +114,9 @@ function getViewUri(routePath, basePath, locale, i18n) {
114
114
  }
115
115
  let url = basePath;
116
116
  if (i18n.uriPattern === "path-prefix" && locale !== i18n.defaultLocale) {
117
- url = import_path.default.join(url, `/${locale}`);
117
+ url = (0, import_fs.joinUrlPath)(url, `/${locale}`);
118
118
  }
119
- url = import_path.default.join(url, routePath);
119
+ url = (0, import_fs.joinUrlPath)(url, routePath);
120
120
  return url;
121
121
  }
122
122
  function isURL(uri) {
@@ -134,6 +134,9 @@ function sortedQueryParamString(query) {
134
134
  return "?" + sortedKeys.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`).join("&");
135
135
  }
136
136
  function crossEnvFileURL(url) {
137
+ if (typeof __filename !== "undefined") {
138
+ return url;
139
+ }
137
140
  if (import_os.default.platform() === "win32" && !url.startsWith("@")) {
138
141
  return (0, import_url.pathToFileURL)(url).href;
139
142
  }
package/build/es/fs.d.ts CHANGED
@@ -49,6 +49,44 @@ export declare function normalizeToFileUrl(filePath: string, rootDir: string): s
49
49
  export declare function normalizeFromFileURL(fileURL: string | undefined, basePath: string): string | null;
50
50
  export declare function isLocalPath(filePath: string): boolean;
51
51
  export declare function normalizeResourcePath(rawPath: string, { rootDir, assets, contentDir, layoutsDir }: ResourcePaths, allowUnresolvedAlias?: boolean): string;
52
+ /**
53
+ * Normalize a filesystem path for use in URIs, specifiers, and cache keys
54
+ * by converting backslashes (`\`) and URL-encoded backslashes (`%5C`) to
55
+ * forward slashes.
56
+ *
57
+ * On macOS/Linux this is a no-op. Node's `path` APIs never produce backslashes.
58
+ * On Windows, `path.join()` and `path.normalize()` return backslashes, which
59
+ * break URI routing, `startsWith()` comparisons, and cache-key matching.
60
+ * Additionally, when a backslash path passes through `encodeURI()` or similar
61
+ * encoding (e.g. in asset transform plugins), `\` becomes `%5C`. This function
62
+ * normalizes both forms.
63
+ *
64
+ * Wrap any `path.join()` / `path.normalize()` result with this function
65
+ * whenever the output is used as a URI, specifier, or map key.
66
+ *
67
+ * @example
68
+ * toForwardSlash(path.join(basePath, uri)); // '/base/path/asset.png'
69
+ * toForwardSlash(path.normalize(assetPath)); // '/app/src/assets/img.png'
70
+ * toForwardSlash('/public%5Cassets%5Cimg.png'); // '/public/assets/img.png'
71
+ */
72
+ export declare function toForwardSlash(p: string): string;
73
+ /**
74
+ * Join URL path segments using string concatenation instead of
75
+ * path.join(), which is a filesystem API and produces backslashes
76
+ * on Windows.
77
+ *
78
+ * Empty segments are filtered out. Each remaining segment is passed
79
+ * through toForwardSlash() to normalize backslashes. Trailing slashes
80
+ * are stripped from all segments except the last, and a leading slash
81
+ * is ensured between each pair.
82
+ *
83
+ * @example
84
+ * joinUrlPath('/base', '/assets/img.png') // '/base/assets/img.png'
85
+ * joinUrlPath('/base/', 'assets', 'img.png') // '/base/assets/img.png'
86
+ * joinUrlPath(basePath, MRT_BASE_URL, ssgRoot, '') // '/bp/mrt/root'
87
+ * joinUrlPath(basePath, MRT_BASE_URL, ssgRoot, path) // '/bp/mrt/root/path'
88
+ */
89
+ export declare function joinUrlPath(...segments: string[]): string;
52
90
  export { lookup as mimeLookup };
53
91
  /**
54
92
  * Tries to convert any URL or $aliased path into a canonical fs path
package/build/es/fs.js CHANGED
@@ -190,6 +190,58 @@ export function normalizeResourcePath(rawPath, { rootDir, assets, contentDir, la
190
190
  return alias;
191
191
  });
192
192
  }
193
+ /**
194
+ * Normalize a filesystem path for use in URIs, specifiers, and cache keys
195
+ * by converting backslashes (`\`) and URL-encoded backslashes (`%5C`) to
196
+ * forward slashes.
197
+ *
198
+ * On macOS/Linux this is a no-op. Node's `path` APIs never produce backslashes.
199
+ * On Windows, `path.join()` and `path.normalize()` return backslashes, which
200
+ * break URI routing, `startsWith()` comparisons, and cache-key matching.
201
+ * Additionally, when a backslash path passes through `encodeURI()` or similar
202
+ * encoding (e.g. in asset transform plugins), `\` becomes `%5C`. This function
203
+ * normalizes both forms.
204
+ *
205
+ * Wrap any `path.join()` / `path.normalize()` result with this function
206
+ * whenever the output is used as a URI, specifier, or map key.
207
+ *
208
+ * @example
209
+ * toForwardSlash(path.join(basePath, uri)); // '/base/path/asset.png'
210
+ * toForwardSlash(path.normalize(assetPath)); // '/app/src/assets/img.png'
211
+ * toForwardSlash('/public%5Cassets%5Cimg.png'); // '/public/assets/img.png'
212
+ */
213
+ export function toForwardSlash(p) {
214
+ return p.replace(/\\/g, '/').replace(/%5C/gi, '/');
215
+ }
216
+ /**
217
+ * Join URL path segments using string concatenation instead of
218
+ * path.join(), which is a filesystem API and produces backslashes
219
+ * on Windows.
220
+ *
221
+ * Empty segments are filtered out. Each remaining segment is passed
222
+ * through toForwardSlash() to normalize backslashes. Trailing slashes
223
+ * are stripped from all segments except the last, and a leading slash
224
+ * is ensured between each pair.
225
+ *
226
+ * @example
227
+ * joinUrlPath('/base', '/assets/img.png') // '/base/assets/img.png'
228
+ * joinUrlPath('/base/', 'assets', 'img.png') // '/base/assets/img.png'
229
+ * joinUrlPath(basePath, MRT_BASE_URL, ssgRoot, '') // '/bp/mrt/root'
230
+ * joinUrlPath(basePath, MRT_BASE_URL, ssgRoot, path) // '/bp/mrt/root/path'
231
+ */
232
+ export function joinUrlPath(...segments) {
233
+ const parts = segments.filter((s) => s !== '');
234
+ return parts
235
+ .map((seg, i) => {
236
+ let s = toForwardSlash(seg);
237
+ if (i > 0 && !s.startsWith('/'))
238
+ s = `/${s}`;
239
+ if (i < parts.length - 1)
240
+ s = s.replace(/\/$/, '');
241
+ return s;
242
+ })
243
+ .join('');
244
+ }
193
245
  export { lookup as mimeLookup };
194
246
  function logMetrics(filePath) {
195
247
  if (logger.isDebugEnabled()) {
@@ -212,11 +264,11 @@ export function normalizeAssetSpecifier(assetId, assetPathMap, resourcePaths, ba
212
264
  if (!importer) {
213
265
  throw Error(`Unable to resolve relative import "${specifier}" without an importer.`);
214
266
  }
215
- return pathLib.join(pathLib.dirname(importer), specifier);
267
+ return toForwardSlash(pathLib.join(pathLib.dirname(importer), specifier));
216
268
  }
217
269
  if (type === 'content-asset') {
218
270
  const originSpecifier = !basePath ? specifier : specifier.split(basePath)[1];
219
- return pathLib.join(resourcePaths.contentDir, originSpecifier);
271
+ return toForwardSlash(pathLib.join(resourcePaths.contentDir, originSpecifier));
220
272
  }
221
273
  if (specifier[0] === '$') {
222
274
  // This is a fs path containing an asset alias
@@ -1,4 +1,5 @@
1
1
  import path from 'path';
2
+ import { joinUrlPath } from './fs.js';
2
3
  // MRT has a specific URL to fetch associated artifacts from the deployed MRT bundle
3
4
  const MRT_BASE_URL = `/mobify/bundle/${process.env.BUNDLE_ID || 'development'}/`;
4
5
  // Default generated site root if not set in the config
@@ -10,7 +11,7 @@ export function getSsgAppRelativePath(staticSiteGenerator) {
10
11
  * Get the URL for a artifact given the configured SSG root and the path to the artifact
11
12
  */
12
13
  export function getMrtArtifactUrl(basePath, ssgRoot, artifactPath = '') {
13
- return path.join(basePath, MRT_BASE_URL, ssgRoot, artifactPath);
14
+ return joinUrlPath(basePath, MRT_BASE_URL, ssgRoot, artifactPath);
14
15
  }
15
16
  /**
16
17
  * Get the root directory of the static config in the mrt lambda
@@ -37,6 +37,36 @@ export declare function getViewUri(routePath: string, basePath: string, locale:
37
37
  * Returns true is the URI starts with http:// or https://
38
38
  */
39
39
  export declare function isURL(uri: string): boolean;
40
+ /**
41
+ * Convert a filesystem path to a URL suitable for dynamic module loading.
42
+ *
43
+ * On Windows, Node.js ESM `import()` requires `file://` URLs for absolute paths
44
+ * (raw `C:\...` paths are rejected by the ESM loader). This function wraps the
45
+ * path with `pathToFileURL()` on Windows so that `import(crossEnvFileURL(path))`
46
+ * works cross-platform.
47
+ *
48
+ * **CJS compatibility:** The CJS build (`build/cjs/`) is generated by esbuild
49
+ * with `format: 'cjs'` (see `scripts/build-cjs.mjs`), which transpiles
50
+ * `import()` to `require()`. Since `require()` does NOT accept `file://` URLs,
51
+ * we detect the CJS context at runtime and return the raw path unchanged.
52
+ *
53
+ * Detection mechanism: In Node.js, CJS modules always have `__filename` defined
54
+ * as a module-scoped variable. ESM modules do not. We use `typeof __filename`
55
+ * to distinguish the two contexts.
56
+ *
57
+ * **Edge cases that could break this:**
58
+ * - If esbuild's ESM output is generated with `platform: 'node'`, esbuild
59
+ * injects a `__filename` shim (`const __filename = fileURLToPath(import.meta.url)`),
60
+ * which would make this check return `true` in ESM and skip the `file://`
61
+ * wrapping. The current LWR build uses `tsc -b` for ESM output, so this
62
+ * does not apply. If the ESM build pipeline changes to esbuild with
63
+ * `platform: 'node'`, this check must be revisited.
64
+ * - If a bundler polyfills `__filename` in ESM context for compatibility,
65
+ * the same false-positive would occur.
66
+ *
67
+ * @param url - An absolute filesystem path or bare specifier (e.g. `@lwrjs/...`)
68
+ * @returns The path as-is (CJS or non-Windows), or a `file://` URL (ESM on Windows)
69
+ */
40
70
  export declare function crossEnvFileURL(url: string): string;
41
71
  export declare function toHostname(url: string): string;
42
72
  //# sourceMappingURL=urls.d.ts.map
package/build/es/urls.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { pathToRegexp } from 'path-to-regexp';
2
- import path from 'path';
3
2
  import os from 'os';
4
3
  import { pathToFileURL } from 'url';
4
+ import { joinUrlPath } from './fs.js';
5
5
  const CONFIG_SUFFIX = '/config.js';
6
6
  const SIGNATURE_SIGIL = 's';
7
7
  export function getClientBootstrapConfigurationUri(routeInfo, runtimeEnvironment, runtimeParams, signature) {
@@ -105,9 +105,9 @@ export function getViewUri(routePath, basePath, locale, i18n) {
105
105
  }
106
106
  let url = basePath;
107
107
  if (i18n.uriPattern === 'path-prefix' && locale !== i18n.defaultLocale) {
108
- url = path.join(url, `/${locale}`);
108
+ url = joinUrlPath(url, `/${locale}`);
109
109
  }
110
- url = path.join(url, routePath);
110
+ url = joinUrlPath(url, routePath);
111
111
  return url;
112
112
  }
113
113
  /**
@@ -130,10 +130,45 @@ function sortedQueryParamString(query) {
130
130
  .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`)
131
131
  .join('&'));
132
132
  }
133
- // Only URLs with a scheme in: file, data, and node are supported by the default ESM loader
134
- // On Windows, absolute paths must be valid file:// URLs.
135
- // Without this code for windows, dynamic imports will fail
133
+ /**
134
+ * Convert a filesystem path to a URL suitable for dynamic module loading.
135
+ *
136
+ * On Windows, Node.js ESM `import()` requires `file://` URLs for absolute paths
137
+ * (raw `C:\...` paths are rejected by the ESM loader). This function wraps the
138
+ * path with `pathToFileURL()` on Windows so that `import(crossEnvFileURL(path))`
139
+ * works cross-platform.
140
+ *
141
+ * **CJS compatibility:** The CJS build (`build/cjs/`) is generated by esbuild
142
+ * with `format: 'cjs'` (see `scripts/build-cjs.mjs`), which transpiles
143
+ * `import()` to `require()`. Since `require()` does NOT accept `file://` URLs,
144
+ * we detect the CJS context at runtime and return the raw path unchanged.
145
+ *
146
+ * Detection mechanism: In Node.js, CJS modules always have `__filename` defined
147
+ * as a module-scoped variable. ESM modules do not. We use `typeof __filename`
148
+ * to distinguish the two contexts.
149
+ *
150
+ * **Edge cases that could break this:**
151
+ * - If esbuild's ESM output is generated with `platform: 'node'`, esbuild
152
+ * injects a `__filename` shim (`const __filename = fileURLToPath(import.meta.url)`),
153
+ * which would make this check return `true` in ESM and skip the `file://`
154
+ * wrapping. The current LWR build uses `tsc -b` for ESM output, so this
155
+ * does not apply. If the ESM build pipeline changes to esbuild with
156
+ * `platform: 'node'`, this check must be revisited.
157
+ * - If a bundler polyfills `__filename` in ESM context for compatibility,
158
+ * the same false-positive would occur.
159
+ *
160
+ * @param url - An absolute filesystem path or bare specifier (e.g. `@lwrjs/...`)
161
+ * @returns The path as-is (CJS or non-Windows), or a `file://` URL (ESM on Windows)
162
+ */
136
163
  export function crossEnvFileURL(url) {
164
+ // In CJS context, esbuild transpiled import() to require().
165
+ // require() needs raw filesystem paths, not file:// URLs.
166
+ if (typeof __filename !== 'undefined') {
167
+ return url;
168
+ }
169
+ // ESM import() on Windows requires file:// URLs for absolute paths.
170
+ // Raw paths like C:\app\module.js are not valid URL specifiers.
171
+ // Bare specifiers (e.g. @lwrjs/...) are excluded. They resolve via node_modules.
137
172
  if (os.platform() === 'win32' && !url.startsWith('@')) {
138
173
  return pathToFileURL(url).href;
139
174
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.21.7",
7
+ "version": "0.22.1",
8
8
  "homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
9
9
  "repository": {
10
10
  "type": "git",
@@ -37,7 +37,7 @@
37
37
  "build/**/*.d.ts"
38
38
  ],
39
39
  "dependencies": {
40
- "@lwrjs/diagnostics": "0.21.7",
40
+ "@lwrjs/diagnostics": "0.22.1",
41
41
  "es-module-lexer": "^1.5.4",
42
42
  "fast-json-stable-stringify": "^2.1.0",
43
43
  "magic-string": "^0.30.9",
@@ -50,13 +50,13 @@
50
50
  "slugify": "^1.4.5"
51
51
  },
52
52
  "devDependencies": {
53
- "@lwrjs/types": "0.21.7",
53
+ "@lwrjs/types": "0.22.1",
54
54
  "@types/mime-types": "2.1.4",
55
55
  "@types/path-to-regexp": "^1.7.0",
56
56
  "memfs": "^4.13.0"
57
57
  },
58
58
  "engines": {
59
- "node": ">=20.0.0"
59
+ "node": ">=22.0.0"
60
60
  },
61
- "gitHead": "685ec1660fd33420c61437bb3fcd37021036db12"
61
+ "gitHead": "29d44fb69ae394a4191c6a81024a8a81f2125556"
62
62
  }