@rettangoli/fe 1.1.3 → 1.2.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/README.md CHANGED
@@ -26,6 +26,7 @@ rtgl fe watch # Start dev server
26
26
  - **[Schema System](./docs/schema.md)** - Component API and metadata
27
27
  - **[Store Management](./docs/store.md)** - State patterns
28
28
  - **[Event Handlers](./docs/handlers.md)** - Event handling
29
+ - **[Internationalization](./docs/i18n.md)** - Locale files, view usage, and locale switching
29
30
 
30
31
  ## Architecture
31
32
 
@@ -123,6 +124,13 @@ fe:
123
124
  - "./src/pages"
124
125
  setup: "setup.js"
125
126
  outfile: "./dist/bundle.js"
127
+ i18n:
128
+ dir: "./src/i18n"
129
+ defaultLocale: "en"
130
+ fallbackLocale: "en"
131
+ locales:
132
+ - "en"
133
+ - "vi"
126
134
  examples:
127
135
  outputDir: "./vt/specs/examples"
128
136
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/fe",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Frontend framework for building reactive web components",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/cli/build.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  RETTANGOLI_FE_VIRTUAL_ENTRY_ID,
7
7
  createRettangoliFeVitePlugin,
8
8
  } from "./vitePlugin.js";
9
+ import { emitI18nAssets, loadI18nBuildContext } from "./i18nBuild.js";
9
10
 
10
11
  const buildRettangoliFrontend = async (options = {}) => {
11
12
  console.log("running build with options", options);
@@ -16,12 +17,18 @@ const buildRettangoliFrontend = async (options = {}) => {
16
17
  outfile = "./vt/static/main.js",
17
18
  setup = "setup.js",
18
19
  development = false,
20
+ i18n = null,
19
21
  } = options;
20
22
 
21
23
  const resolvedOutfile = path.resolve(cwd, outfile);
22
24
  const outDir = path.dirname(resolvedOutfile);
23
25
  const outFileName = path.basename(resolvedOutfile);
24
26
  const relativeOutDir = path.relative(cwd, outDir) || ".";
27
+ const i18nContext = loadI18nBuildContext({
28
+ cwd,
29
+ i18n,
30
+ errorPrefix: "[Build]",
31
+ });
25
32
 
26
33
  await viteBuild({
27
34
  configFile: false,
@@ -31,6 +38,7 @@ const buildRettangoliFrontend = async (options = {}) => {
31
38
  cwd,
32
39
  dirs,
33
40
  setup,
41
+ i18n,
34
42
  errorPrefix: "[Build]",
35
43
  }),
36
44
  ],
@@ -52,6 +60,8 @@ const buildRettangoliFrontend = async (options = {}) => {
52
60
  },
53
61
  });
54
62
 
63
+ emitI18nAssets({ outDir, i18nContext });
64
+
55
65
  console.log(`Build complete. Output file: ${resolvedOutfile}`);
56
66
  };
57
67
 
package/src/cli/check.js CHANGED
@@ -9,11 +9,16 @@ const checkRettangoliFrontend = (options = {}) => {
9
9
  cwd = process.cwd(),
10
10
  dirs = ["./example"],
11
11
  format = "text",
12
+ i18n = null,
12
13
  } = options;
13
14
  const outputFormat = format === "json" ? "json" : "text";
14
15
 
15
16
  const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
16
- const { errors, summary, index } = analyzeComponentDirs({ dirs: resolvedDirs });
17
+ const { errors, summary, index } = analyzeComponentDirs({
18
+ dirs: resolvedDirs,
19
+ cwd,
20
+ i18n,
21
+ });
17
22
 
18
23
  if (errors.length > 0) {
19
24
  if (outputFormat === "json") {
@@ -6,6 +6,7 @@ import {
6
6
  formatContractErrors as formatContractErrorLines,
7
7
  validateComponentContractIndex,
8
8
  } from "../core/contracts/componentFiles.js";
9
+ import { analyzeI18nBuildContext } from "./i18nBuild.js";
9
10
 
10
11
  export const SUPPORTED_COMPONENT_FILE_SUFFIXES = Object.freeze([
11
12
  ".store.js",
@@ -54,9 +55,10 @@ export const collectComponentContractEntriesFromDirs = (dirs = []) => {
54
55
  export const validateComponentEntries = ({
55
56
  entries = [],
56
57
  errorPrefix = "[Check]",
58
+ i18nContext = { enabled: false },
57
59
  }) => {
58
60
  const index = buildComponentContractIndex(entries);
59
- const errors = validateComponentContractIndex(index);
61
+ const errors = validateComponentContractIndex(index, { i18nContext });
60
62
  if (errors.length > 0) {
61
63
  throw new Error(
62
64
  `${errorPrefix} Component contract validation failed:\n${formatContractErrorLines(errors).join("\n")}`,
@@ -71,9 +73,14 @@ export const validateComponentEntries = ({
71
73
  export const validateComponentDirs = ({
72
74
  dirs = [],
73
75
  errorPrefix = "[Check]",
76
+ i18nContext = { enabled: false },
74
77
  }) => {
75
78
  const entries = collectComponentContractEntriesFromDirs(dirs);
76
- const validationResult = validateComponentEntries({ entries, errorPrefix });
79
+ const validationResult = validateComponentEntries({
80
+ entries,
81
+ errorPrefix,
82
+ i18nContext,
83
+ });
77
84
  return {
78
85
  entries,
79
86
  ...validationResult,
@@ -131,9 +138,16 @@ export const formatContractFailureReport = ({
131
138
  ].join("\n");
132
139
  };
133
140
 
134
- export const analyzeComponentEntries = ({ entries = [] }) => {
141
+ export const analyzeComponentEntries = ({
142
+ entries = [],
143
+ i18nContext = { enabled: false },
144
+ i18nErrors = [],
145
+ }) => {
135
146
  const index = buildComponentContractIndex(entries);
136
- const errors = validateComponentContractIndex(index);
147
+ const errors = [
148
+ ...i18nErrors,
149
+ ...validateComponentContractIndex(index, { i18nContext }),
150
+ ];
137
151
  const summary = summarizeContractErrors(errors);
138
152
  return {
139
153
  entries,
@@ -143,7 +157,19 @@ export const analyzeComponentEntries = ({ entries = [] }) => {
143
157
  };
144
158
  };
145
159
 
146
- export const analyzeComponentDirs = ({ dirs = [] }) => {
160
+ export const analyzeComponentDirs = ({
161
+ dirs = [],
162
+ cwd = process.cwd(),
163
+ i18n = null,
164
+ }) => {
147
165
  const entries = collectComponentContractEntriesFromDirs(dirs);
148
- return analyzeComponentEntries({ entries });
166
+ const { context: i18nContext, errors: i18nErrors } = analyzeI18nBuildContext({
167
+ cwd,
168
+ i18n,
169
+ });
170
+ return analyzeComponentEntries({
171
+ entries,
172
+ i18nContext,
173
+ i18nErrors,
174
+ });
149
175
  };
@@ -9,6 +9,10 @@ import {
9
9
  isSupportedComponentFile,
10
10
  validateComponentEntries,
11
11
  } from "./contracts.js";
12
+ import {
13
+ buildI18nAssets,
14
+ loadI18nBuildContext,
15
+ } from "./i18nBuild.js";
12
16
 
13
17
  const MODULE_FILE_TYPES = new Set(["handlers", "store", "methods"]);
14
18
  const YAML_FILE_TYPES = new Set(["view", "constants", "schema"]);
@@ -65,15 +69,52 @@ const createComponentImportLines = ({ componentMatrix, categories }) => {
65
69
  return lines.join("\n");
66
70
  };
67
71
 
72
+ const createI18nRuntimeSource = ({ i18nContext }) => {
73
+ if (!i18nContext.enabled) {
74
+ return "const __rtglFrameworkDeps = {};";
75
+ }
76
+
77
+ const assets = buildI18nAssets({ i18nContext });
78
+ const urlEntries = assets
79
+ .map((asset) => {
80
+ return ` ${JSON.stringify(asset.locale)}: new URL(/* @vite-ignore */ ${JSON.stringify(`./${asset.relativeFileName}`)}, import.meta.url).href,`;
81
+ })
82
+ .join("\n");
83
+
84
+ const initialCatalogs = {};
85
+ initialCatalogs[i18nContext.defaultLocale] =
86
+ i18nContext.catalogs[i18nContext.defaultLocale];
87
+ initialCatalogs[i18nContext.fallbackLocale] =
88
+ i18nContext.catalogs[i18nContext.fallbackLocale];
89
+
90
+ return `
91
+ const __rtglI18nRuntime = createI18nRuntime({
92
+ defaultLocale: ${JSON.stringify(i18nContext.defaultLocale)},
93
+ fallbackLocale: ${JSON.stringify(i18nContext.fallbackLocale)},
94
+ locales: ${JSON.stringify(i18nContext.locales)},
95
+ urls: {
96
+ ${urlEntries}
97
+ },
98
+ initialCatalogs: ${JSON.stringify(initialCatalogs)},
99
+ });
100
+ await __rtglI18nRuntime.ready();
101
+ const __rtglFrameworkDeps = {
102
+ __rtglI18nRuntime,
103
+ locale: __rtglI18nRuntime.locale,
104
+ };`.trim();
105
+ };
106
+
68
107
  export const generateFrontendEntrySource = ({
69
108
  cwd = process.cwd(),
70
109
  dirs = ["./example"],
71
110
  setup = "setup.js",
72
111
  command = "build",
112
+ i18n = null,
73
113
  errorPrefix = "[Build]",
74
114
  } = {}) => {
75
115
  const resolvedDirs = dirs.map((dir) => path.resolve(cwd, dir));
76
116
  const resolvedSetup = path.resolve(cwd, setup);
117
+ const i18nContext = loadI18nBuildContext({ cwd, i18n, errorPrefix });
77
118
  const allFiles = getAllFiles(resolvedDirs)
78
119
  .filter((filePath) => isSupportedComponentFile(filePath))
79
120
  .sort((a, b) => a.localeCompare(b));
@@ -143,6 +184,7 @@ export const generateFrontendEntrySource = ({
143
184
  validateComponentEntries({
144
185
  entries: componentContractEntries,
145
186
  errorPrefix,
187
+ i18nContext,
146
188
  });
147
189
 
148
190
  const setupImportPath = toImportPath({
@@ -153,18 +195,28 @@ export const generateFrontendEntrySource = ({
153
195
  componentMatrix,
154
196
  categories: Object.keys(componentMatrix).sort(),
155
197
  });
198
+ const feImports = i18nContext.enabled
199
+ ? "createComponent, createI18nRuntime"
200
+ : "createComponent";
201
+ const i18nRuntimeSource = createI18nRuntimeSource({ i18nContext });
156
202
 
157
203
  return `
158
204
  ${declarationLines.join("\n")}
159
- import { createComponent } from "@rettangoli/fe";
205
+ import { ${feImports} } from "@rettangoli/fe";
160
206
  import { deps } from ${JSON.stringify(setupImportPath)};
161
207
 
162
208
  ${categoryLines}
163
209
 
210
+ ${i18nRuntimeSource}
211
+
164
212
  Object.keys(imports).forEach((category) => {
165
213
  Object.keys(imports[category]).forEach((component) => {
166
214
  const componentConfig = imports[category][component];
167
- const webComponent = createComponent({ ...componentConfig }, deps[category]);
215
+ const categoryDeps = {
216
+ ...((deps && deps[category]) || {}),
217
+ ...__rtglFrameworkDeps,
218
+ };
219
+ const webComponent = createComponent({ ...componentConfig }, categoryDeps);
168
220
  const elementName = componentConfig.schema?.componentName;
169
221
  if (!elementName) {
170
222
  throw new Error(
@@ -0,0 +1,367 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ } from "node:fs";
7
+ import path from "node:path";
8
+
9
+ import { load as loadYaml } from "js-yaml";
10
+
11
+ const DEFAULT_OUTPUT_DIR = "i18n";
12
+ const LOCALE_PATTERN = /^[A-Za-z0-9_-]+$/;
13
+
14
+ const isPlainObject = (value) =>
15
+ value !== null && typeof value === "object" && !Array.isArray(value);
16
+
17
+ const toPosixPath = (value) => value.split(path.sep).join("/");
18
+
19
+ const createI18nError = ({ code = "RTGL-I18N-004", message, filePath }) => ({
20
+ code,
21
+ message,
22
+ filePath: filePath || "",
23
+ });
24
+
25
+ const assertNoI18nErrors = ({ errors, errorPrefix }) => {
26
+ if (errors.length === 0) {
27
+ return;
28
+ }
29
+
30
+ const details = errors
31
+ .map((error) => `${error.code} ${error.message}${error.filePath ? ` [${error.filePath}]` : ""}`)
32
+ .join("\n");
33
+ throw new Error(`${errorPrefix} i18n validation failed:\n${details}`);
34
+ };
35
+
36
+ const validateLocaleName = ({ locale, errors, filePath }) => {
37
+ if (typeof locale !== "string" || !LOCALE_PATTERN.test(locale)) {
38
+ errors.push(createI18nError({
39
+ message: `invalid locale name ${JSON.stringify(locale)}. Locale names may contain letters, numbers, "_" and "-".`,
40
+ filePath,
41
+ }));
42
+ return false;
43
+ }
44
+ return true;
45
+ };
46
+
47
+ const flattenCatalogKeys = (catalog = {}) => {
48
+ const keys = [];
49
+ Object.entries(catalog).forEach(([namespace, messages]) => {
50
+ Object.keys(messages || {}).forEach((key) => {
51
+ keys.push(`${namespace}.${key}`);
52
+ });
53
+ });
54
+ return keys.sort();
55
+ };
56
+
57
+ const validateCatalog = ({ catalog, locale, filePath }) => {
58
+ const errors = [];
59
+ const normalized = {};
60
+
61
+ if (!isPlainObject(catalog)) {
62
+ errors.push(createI18nError({
63
+ message: `${locale}.yaml must contain a YAML object at the root.`,
64
+ filePath,
65
+ }));
66
+ return { catalog: normalized, errors };
67
+ }
68
+
69
+ Object.entries(catalog).forEach(([namespace, messages]) => {
70
+ if (!isPlainObject(messages)) {
71
+ errors.push(createI18nError({
72
+ message: `${locale}.yaml namespace "${namespace}" must be an object. Use namespace -> key -> content.`,
73
+ filePath,
74
+ }));
75
+ return;
76
+ }
77
+
78
+ normalized[namespace] = {};
79
+ Object.entries(messages).forEach(([key, content]) => {
80
+ if (isPlainObject(content) || Array.isArray(content)) {
81
+ errors.push(createI18nError({
82
+ message: `${locale}.yaml key "${namespace}.${key}" is nested too deeply. Use namespace -> key -> content only.`,
83
+ filePath,
84
+ }));
85
+ return;
86
+ }
87
+
88
+ if (typeof content !== "string") {
89
+ errors.push(createI18nError({
90
+ message: `${locale}.yaml key "${namespace}.${key}" must be a string.`,
91
+ filePath,
92
+ }));
93
+ return;
94
+ }
95
+
96
+ normalized[namespace][key] = content;
97
+ });
98
+ });
99
+
100
+ return { catalog: normalized, errors };
101
+ };
102
+
103
+ const normalizeI18nConfig = ({ cwd, i18n }) => {
104
+ const errors = [];
105
+
106
+ if (!i18n) {
107
+ return {
108
+ config: { enabled: false },
109
+ errors,
110
+ };
111
+ }
112
+
113
+ if (!isPlainObject(i18n)) {
114
+ errors.push(createI18nError({
115
+ message: "fe.i18n must be an object.",
116
+ }));
117
+ return {
118
+ config: { enabled: false },
119
+ errors,
120
+ };
121
+ }
122
+
123
+ const dir = i18n.dir;
124
+ if (typeof dir !== "string" || dir.trim() === "") {
125
+ errors.push(createI18nError({
126
+ message: "fe.i18n.dir must be a non-empty string.",
127
+ }));
128
+ }
129
+
130
+ const defaultLocale = i18n.defaultLocale;
131
+ const fallbackLocale = i18n.fallbackLocale;
132
+ validateLocaleName({ locale: defaultLocale, errors });
133
+ validateLocaleName({ locale: fallbackLocale, errors });
134
+
135
+ if (!Array.isArray(i18n.locales) || i18n.locales.length === 0) {
136
+ errors.push(createI18nError({
137
+ message: "fe.i18n.locales must be a non-empty array.",
138
+ }));
139
+ }
140
+
141
+ const locales = [];
142
+ const seen = new Set();
143
+ if (Array.isArray(i18n.locales)) {
144
+ i18n.locales.forEach((locale) => {
145
+ if (!validateLocaleName({ locale, errors })) {
146
+ return;
147
+ }
148
+ if (seen.has(locale)) {
149
+ errors.push(createI18nError({
150
+ message: `fe.i18n.locales contains duplicate locale "${locale}".`,
151
+ }));
152
+ return;
153
+ }
154
+ seen.add(locale);
155
+ locales.push(locale);
156
+ });
157
+ }
158
+
159
+ if (defaultLocale && locales.length > 0 && !seen.has(defaultLocale)) {
160
+ errors.push(createI18nError({
161
+ message: `fe.i18n.defaultLocale "${defaultLocale}" must be listed in fe.i18n.locales.`,
162
+ }));
163
+ }
164
+
165
+ if (fallbackLocale && locales.length > 0 && !seen.has(fallbackLocale)) {
166
+ errors.push(createI18nError({
167
+ message: `fe.i18n.fallbackLocale "${fallbackLocale}" must be listed in fe.i18n.locales.`,
168
+ }));
169
+ }
170
+
171
+ const outputDir = typeof i18n.outputDir === "string" && i18n.outputDir.trim()
172
+ ? i18n.outputDir
173
+ : DEFAULT_OUTPUT_DIR;
174
+ const normalizedOutputDir = outputDir.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
175
+ if (
176
+ !normalizedOutputDir ||
177
+ normalizedOutputDir.split("/").includes("..") ||
178
+ path.posix.isAbsolute(normalizedOutputDir)
179
+ ) {
180
+ errors.push(createI18nError({
181
+ message: "fe.i18n.outputDir must be a relative path when provided.",
182
+ }));
183
+ }
184
+
185
+ return {
186
+ config: {
187
+ enabled: errors.length === 0,
188
+ dir,
189
+ resolvedDir: dir ? path.resolve(cwd, dir) : null,
190
+ outputDir: normalizedOutputDir || DEFAULT_OUTPUT_DIR,
191
+ defaultLocale,
192
+ fallbackLocale,
193
+ locales,
194
+ },
195
+ errors,
196
+ };
197
+ };
198
+
199
+ export const analyzeI18nBuildContext = ({
200
+ cwd = process.cwd(),
201
+ i18n = null,
202
+ } = {}) => {
203
+ const { config, errors } = normalizeI18nConfig({ cwd, i18n });
204
+
205
+ if (!i18n || errors.length > 0) {
206
+ return {
207
+ context: { enabled: false },
208
+ errors,
209
+ };
210
+ }
211
+
212
+ if (!existsSync(config.resolvedDir)) {
213
+ errors.push(createI18nError({
214
+ message: `fe.i18n.dir does not exist: ${config.resolvedDir}`,
215
+ filePath: config.resolvedDir,
216
+ }));
217
+ return {
218
+ context: { enabled: false },
219
+ errors,
220
+ };
221
+ }
222
+
223
+ const localeFiles = {};
224
+ const catalogs = {};
225
+
226
+ config.locales.forEach((locale) => {
227
+ const filePath = path.join(config.resolvedDir, `${locale}.yaml`);
228
+ localeFiles[locale] = filePath;
229
+
230
+ if (!existsSync(filePath)) {
231
+ errors.push(createI18nError({
232
+ message: `missing i18n file for locale "${locale}". Expected ${filePath}.`,
233
+ filePath,
234
+ }));
235
+ return;
236
+ }
237
+
238
+ let yamlObject;
239
+ try {
240
+ yamlObject = loadYaml(readFileSync(filePath, "utf8")) ?? {};
241
+ } catch (error) {
242
+ errors.push(createI18nError({
243
+ message: `failed to parse ${locale}.yaml: ${error.message}`,
244
+ filePath,
245
+ }));
246
+ return;
247
+ }
248
+
249
+ const result = validateCatalog({ catalog: yamlObject, locale, filePath });
250
+ errors.push(...result.errors);
251
+ catalogs[locale] = result.catalog;
252
+ });
253
+
254
+ const defaultCatalog = catalogs[config.defaultLocale];
255
+ if (defaultCatalog) {
256
+ const defaultKeys = flattenCatalogKeys(defaultCatalog);
257
+ config.locales.forEach((locale) => {
258
+ if (locale === config.defaultLocale || !catalogs[locale]) {
259
+ return;
260
+ }
261
+
262
+ defaultKeys.forEach((fullKey) => {
263
+ const [namespace, key] = fullKey.split(".");
264
+ if (
265
+ !Object.prototype.hasOwnProperty.call(catalogs[locale] || {}, namespace) ||
266
+ !Object.prototype.hasOwnProperty.call(catalogs[locale]?.[namespace] || {}, key)
267
+ ) {
268
+ errors.push(createI18nError({
269
+ message: `${locale}.yaml is missing key "${fullKey}" from default locale "${config.defaultLocale}".`,
270
+ filePath: localeFiles[locale],
271
+ }));
272
+ }
273
+ });
274
+ });
275
+ }
276
+
277
+ const context = {
278
+ ...config,
279
+ enabled: errors.length === 0,
280
+ localeFiles,
281
+ catalogs,
282
+ };
283
+
284
+ return {
285
+ context,
286
+ errors,
287
+ };
288
+ };
289
+
290
+ export const loadI18nBuildContext = ({
291
+ cwd = process.cwd(),
292
+ i18n = null,
293
+ errorPrefix = "[Build]",
294
+ } = {}) => {
295
+ const { context, errors } = analyzeI18nBuildContext({ cwd, i18n });
296
+ assertNoI18nErrors({ errors, errorPrefix });
297
+ return context;
298
+ };
299
+
300
+ export const buildI18nAssets = ({ i18nContext }) => {
301
+ if (!i18nContext?.enabled) {
302
+ return [];
303
+ }
304
+
305
+ return i18nContext.locales.map((locale) => {
306
+ const relativeFileName = path.posix.join(
307
+ i18nContext.outputDir,
308
+ `${locale}.json`,
309
+ );
310
+ return {
311
+ locale,
312
+ relativeFileName,
313
+ sourcePath: i18nContext.localeFiles[locale],
314
+ catalog: i18nContext.catalogs[locale],
315
+ content: `${JSON.stringify(i18nContext.catalogs[locale], null, 2)}\n`,
316
+ };
317
+ });
318
+ };
319
+
320
+ export const emitI18nAssets = ({ outDir, i18nContext }) => {
321
+ const assets = buildI18nAssets({ i18nContext });
322
+
323
+ return assets.map((asset) => {
324
+ const outputPath = path.resolve(outDir, asset.relativeFileName);
325
+ mkdirSync(path.dirname(outputPath), { recursive: true });
326
+ writeFileSync(outputPath, asset.content);
327
+ return {
328
+ ...asset,
329
+ outputPath,
330
+ };
331
+ });
332
+ };
333
+
334
+ export const isI18nSourceFilePath = ({ filePath, i18nContext }) => {
335
+ if (!i18nContext?.enabled && !i18nContext?.resolvedDir) {
336
+ return false;
337
+ }
338
+
339
+ const resolvedFilePath = path.resolve(filePath);
340
+ const extension = path.extname(resolvedFilePath);
341
+ if (extension !== ".yaml") {
342
+ return false;
343
+ }
344
+
345
+ const relativePath = path.relative(i18nContext.resolvedDir, resolvedFilePath);
346
+ return (
347
+ relativePath !== "" &&
348
+ !relativePath.startsWith("..") &&
349
+ !path.isAbsolute(relativePath)
350
+ );
351
+ };
352
+
353
+ export const getI18nPublicAssetPath = ({
354
+ publicEntryPath,
355
+ relativeFileName,
356
+ }) => {
357
+ const normalizedEntry = publicEntryPath
358
+ ? publicEntryPath.replace(/\\/g, "/")
359
+ : "/main.js";
360
+ const entryPath = normalizedEntry.startsWith("/")
361
+ ? normalizedEntry
362
+ : `/${normalizedEntry}`;
363
+ return path.posix.join(
364
+ path.posix.dirname(entryPath),
365
+ toPosixPath(relativeFileName),
366
+ );
367
+ };