@hitachivantara/app-shell-vite-plugin 2.2.1 → 2.3.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.
@@ -15,16 +15,20 @@ export declare function readSupportedLocales(filePath: string): string[];
15
15
  */
16
16
  export declare function discoverLanguageDirs(localesDir: string): string[];
17
17
  /**
18
- * Computes the merged supported-locales list respecting ordering:
18
+ * Computes the effective supported-locales list.
19
19
  *
20
- * 1. Entries from the **app's** `supported-locales.json` (in their original order).
21
- * 2. Entries from the **shell's** `supported-locales.json` not already listed
22
- * (in their original order).
23
- * 3. Any language directories discovered on disk that are not in either
24
- * manifest (alphabetical order).
20
+ * Discovers all language directories from both sources (upstream shell and
21
+ * local app). If the app provides a `supported-locales.json`, it acts as a
22
+ * **filter**: only locales listed there are kept. Otherwise all discovered
23
+ * locales are included.
25
24
  *
26
- * @param shellLocalesDir - The app-shell-ui locales directory (lower priority).
27
- * @param appLocalesDir - The app's locales directory (higher priority).
25
+ * The result is always sorted alphabetically.
26
+ *
27
+ * Locales listed in the local manifest but without a matching language
28
+ * directory are warned about and dropped.
29
+ *
30
+ * @param shellLocalesDir - The app-shell-ui locales directory (upstream).
31
+ * @param appLocalesDir - The app's locales directory (downstream / local).
28
32
  */
29
33
  export declare function computeSupportedLocales(shellLocalesDir: string | undefined, appLocalesDir: string | undefined): string[];
30
34
  /**
@@ -38,7 +42,11 @@ export declare function readJsonFile(filePath: string): unknown;
38
42
  * Non-JSON files are skipped (locale bundles are always JSON).
39
43
  *
40
44
  * `supported-locales.json` is skipped — it is handled separately after the
41
- * merge so it can reflect the union of both sources plus any discovered
42
- * language directories.
45
+ * merge so it can reflect the computed list.
46
+ *
47
+ * When `allowedLocales` is provided, only top-level directories whose name
48
+ * is in the set are merged; all others are skipped. This allows the app's
49
+ * `supported-locales.json` to act as a filter over upstream locale dirs.
50
+ * Sub-directories (namespace folders) are always merged recursively.
43
51
  */
44
- export declare function mergeDirs(src: string, dest: string): void;
52
+ export declare function mergeDirs(src: string, dest: string, allowedLocales?: Set<string>): void;
@@ -54,53 +54,23 @@ export function discoverLanguageDirs(localesDir) {
54
54
  .toSorted();
55
55
  }
56
56
  /**
57
- * Computes the merged supported-locales list respecting ordering:
57
+ * Computes the effective supported-locales list.
58
58
  *
59
- * 1. Entries from the **app's** `supported-locales.json` (in their original order).
60
- * 2. Entries from the **shell's** `supported-locales.json` not already listed
61
- * (in their original order).
62
- * 3. Any language directories discovered on disk that are not in either
63
- * manifest (alphabetical order).
59
+ * Discovers all language directories from both sources (upstream shell and
60
+ * local app). If the app provides a `supported-locales.json`, it acts as a
61
+ * **filter**: only locales listed there are kept. Otherwise all discovered
62
+ * locales are included.
64
63
  *
65
- * @param shellLocalesDir - The app-shell-ui locales directory (lower priority).
66
- * @param appLocalesDir - The app's locales directory (higher priority).
64
+ * The result is always sorted alphabetically.
65
+ *
66
+ * Locales listed in the local manifest but without a matching language
67
+ * directory are warned about and dropped.
68
+ *
69
+ * @param shellLocalesDir - The app-shell-ui locales directory (upstream).
70
+ * @param appLocalesDir - The app's locales directory (downstream / local).
67
71
  */
68
72
  export function computeSupportedLocales(shellLocalesDir, appLocalesDir) {
69
- const seen = new Set();
70
- const result = [];
71
- const addUnique = (lng) => {
72
- if (!seen.has(lng)) {
73
- seen.add(lng);
74
- result.push(lng);
75
- }
76
- };
77
- // 1. App manifest entries first (highest priority order)
78
- if (appLocalesDir) {
79
- for (const lng of readSupportedLocales(path.join(appLocalesDir, SUPPORTED_LOCALES_FILE))) {
80
- addUnique(lng);
81
- }
82
- }
83
- // 2. Shell manifest entries that weren't already listed
84
- if (shellLocalesDir) {
85
- for (const lng of readSupportedLocales(path.join(shellLocalesDir, SUPPORTED_LOCALES_FILE))) {
86
- addUnique(lng);
87
- }
88
- }
89
- // 3. Discovered language directories not in either manifest (alphabetical)
90
- const discoveredDirs = [];
91
- for (const dir of [shellLocalesDir, appLocalesDir]) {
92
- if (dir) {
93
- for (const lng of discoverLanguageDirs(dir)) {
94
- if (!seen.has(lng))
95
- discoveredDirs.push(lng);
96
- }
97
- }
98
- }
99
- // deduplicate and sort before appending
100
- for (const lng of [...new Set(discoveredDirs)].toSorted()) {
101
- addUnique(lng);
102
- }
103
- // Validate: warn and drop manifest entries without a matching language directory
73
+ // Collect all language directories that actually exist on disk
104
74
  const allDirs = new Set();
105
75
  for (const dir of [shellLocalesDir, appLocalesDir]) {
106
76
  if (dir) {
@@ -109,12 +79,33 @@ export function computeSupportedLocales(shellLocalesDir, appLocalesDir) {
109
79
  }
110
80
  }
111
81
  }
112
- return result.filter((lng) => {
113
- if (allDirs.has(lng))
114
- return true;
115
- console.warn(`[app-shell-locales] Locale "${lng}" is listed in ${SUPPORTED_LOCALES_FILE} but has no corresponding language directory — ignoring.`);
116
- return false;
117
- });
82
+ // Determine if the app provides a supported-locales.json file.
83
+ // Physical existence of the file is what matters — even if it's empty,
84
+ // malformed, or contains no valid entries, it still acts as a filter
85
+ // (resulting in an empty effective locale list).
86
+ const manifestPath = appLocalesDir
87
+ ? path.join(appLocalesDir, SUPPORTED_LOCALES_FILE)
88
+ : undefined;
89
+ const hasLocalManifest = manifestPath ? fs.existsSync(manifestPath) : false;
90
+ // Read the manifest entries (empty if file is missing/invalid)
91
+ const localManifest = manifestPath ? readSupportedLocales(manifestPath) : [];
92
+ // Determine the effective set of locales
93
+ let result;
94
+ if (hasLocalManifest) {
95
+ // Filter: only keep unique locales from the manifest that have a directory.
96
+ // If the manifest is empty/malformed, this correctly yields an empty list.
97
+ result = [...new Set(localManifest)].filter((lng) => {
98
+ if (allDirs.has(lng))
99
+ return true;
100
+ console.warn(`[app-shell-locales] Locale "${lng}" is listed in ${SUPPORTED_LOCALES_FILE} but has no corresponding language directory — ignoring.`);
101
+ return false;
102
+ });
103
+ }
104
+ else {
105
+ // No local manifest → include everything discovered
106
+ result = [...allDirs];
107
+ }
108
+ return result.toSorted();
118
109
  }
119
110
  /**
120
111
  * Parses a JSON file, wrapping parse errors with the file path for
@@ -135,15 +126,23 @@ export function readJsonFile(filePath) {
135
126
  * Non-JSON files are skipped (locale bundles are always JSON).
136
127
  *
137
128
  * `supported-locales.json` is skipped — it is handled separately after the
138
- * merge so it can reflect the union of both sources plus any discovered
139
- * language directories.
129
+ * merge so it can reflect the computed list.
130
+ *
131
+ * When `allowedLocales` is provided, only top-level directories whose name
132
+ * is in the set are merged; all others are skipped. This allows the app's
133
+ * `supported-locales.json` to act as a filter over upstream locale dirs.
134
+ * Sub-directories (namespace folders) are always merged recursively.
140
135
  */
141
- export function mergeDirs(src, dest) {
136
+ export function mergeDirs(src, dest, allowedLocales) {
142
137
  fs.mkdirSync(dest, { recursive: true });
143
138
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
144
139
  const srcPath = path.join(src, entry.name);
145
140
  const destPath = path.join(dest, entry.name);
146
141
  if (entry.isDirectory()) {
142
+ // At the top level, skip locale dirs not in the allowed set
143
+ if (allowedLocales && !allowedLocales.has(entry.name))
144
+ continue;
145
+ // Sub-directories are always merged (no further filtering)
147
146
  mergeDirs(srcPath, destPath);
148
147
  }
149
148
  else if (entry.name === SUPPORTED_LOCALES_FILE) {
@@ -12,7 +12,7 @@ export function addUseCredentials(scriptSrc, html) {
12
12
  }
13
13
  function processScript(bundle, scriptSrc, html, seen = new Set()) {
14
14
  seen.add(scriptSrc);
15
- const script = bundle[scriptSrc];
15
+ const script = bundle?.[scriptSrc];
16
16
  if (!script || script.type !== "chunk") {
17
17
  return html;
18
18
  }
@@ -6,7 +6,17 @@ import type { PluginOption } from "vite";
6
6
  *
7
7
  * Local locale files (from the app's public/locales/) always take priority.
8
8
  *
9
- * `supported-locales.json` is merged by taking the union of both arrays and
10
- * adding any language directories discovered during the merge.
9
+ * If the app provides a `supported-locales.json`, it acts as a filter:
10
+ * only listed locales are included from upstream. If no local file is
11
+ * provided, all upstream locales are merged in.
12
+ *
13
+ * When `mergeUpstream` is false (e.g. `type: "bundle"`), the build skips
14
+ * upstream merging entirely but still generates `supported-locales.json`
15
+ * from the local locale directories, filtering by the local manifest when
16
+ * present. In dev mode, upstream locales are always served regardless of
17
+ * this flag, since the full app runs locally and needs the shell translations.
18
+ *
19
+ * @param mergeUpstream - Whether to merge upstream app-shell-ui locales.
20
+ * Defaults to `true` (for `type: "app"`).
11
21
  */
12
- export default function copyAppShellLocales(): PluginOption;
22
+ export default function copyAppShellLocales(mergeUpstream?: boolean): PluginOption;
@@ -21,6 +21,24 @@ function resolveAppShellUiLocales() {
21
21
  }
22
22
  return undefined;
23
23
  }
24
+ /**
25
+ * Computes the effective set of allowed locales for a given locales directory.
26
+ *
27
+ * When a local `supported-locales.json` exists, it acts as a filter — even if
28
+ * it resolves to an empty list (all entries invalid), so that an invalid
29
+ * manifest doesn't accidentally allow everything.
30
+ *
31
+ * @returns The effective locale list (sorted, deduplicated) and the
32
+ * corresponding `Set` to use as a filter (or `undefined` when no local
33
+ * manifest exists — meaning "allow all").
34
+ */
35
+ function resolveEffectiveLocales(shellLocalesDir, appLocalesDir) {
36
+ const hasLocalManifest = fs.existsSync(path.join(appLocalesDir, SUPPORTED_LOCALES_FILE));
37
+ const locales = computeSupportedLocales(shellLocalesDir, appLocalesDir);
38
+ // When a local manifest exists, always filter — even with an empty set.
39
+ const allowedSet = hasLocalManifest ? new Set(locales) : undefined;
40
+ return { locales, allowedSet };
41
+ }
24
42
  /**
25
43
  * Vite plugin that handles app-shell locale files:
26
44
  * - In dev mode: serves merged locale files via middleware
@@ -28,10 +46,20 @@ function resolveAppShellUiLocales() {
28
46
  *
29
47
  * Local locale files (from the app's public/locales/) always take priority.
30
48
  *
31
- * `supported-locales.json` is merged by taking the union of both arrays and
32
- * adding any language directories discovered during the merge.
49
+ * If the app provides a `supported-locales.json`, it acts as a filter:
50
+ * only listed locales are included from upstream. If no local file is
51
+ * provided, all upstream locales are merged in.
52
+ *
53
+ * When `mergeUpstream` is false (e.g. `type: "bundle"`), the build skips
54
+ * upstream merging entirely but still generates `supported-locales.json`
55
+ * from the local locale directories, filtering by the local manifest when
56
+ * present. In dev mode, upstream locales are always served regardless of
57
+ * this flag, since the full app runs locally and needs the shell translations.
58
+ *
59
+ * @param mergeUpstream - Whether to merge upstream app-shell-ui locales.
60
+ * Defaults to `true` (for `type: "app"`).
33
61
  */
34
- export default function copyAppShellLocales() {
62
+ export default function copyAppShellLocales(mergeUpstream = true) {
35
63
  let resolvedOutDir;
36
64
  let isBuild = false;
37
65
  return {
@@ -42,8 +70,16 @@ export default function copyAppShellLocales() {
42
70
  isBuild = config.command === "build";
43
71
  },
44
72
  // --- DEV MODE: serve merged locales via middleware ---
73
+ // In dev, always resolve upstream locales regardless of `mergeUpstream`,
74
+ // because the full app runs locally and needs the shell translations.
75
+ // The `mergeUpstream` flag only affects the build output.
45
76
  configureServer(server) {
46
77
  const appShellUiLocalesDir = resolveAppShellUiLocales();
78
+ const localLocalesDir = path.resolve(server.config.root, "public/locales");
79
+ // Compute the effective locale set once at server start, using the
80
+ // same logic as the build path. A server restart is needed when the
81
+ // locale directory structure or supported-locales.json changes.
82
+ const { locales: effectiveLocales, allowedSet } = resolveEffectiveLocales(appShellUiLocalesDir, localLocalesDir);
47
83
  server.middlewares.use((req, res, next) => {
48
84
  // Parse the pathname, stripping the Vite base and any query string
49
85
  // so locale requests work under non-root base paths (e.g. /myapp/).
@@ -58,19 +94,15 @@ export default function copyAppShellLocales() {
58
94
  if (!pathname.startsWith(base))
59
95
  return next();
60
96
  const relativePath = pathname.slice(base.length);
61
- // Handle supported-locales.json separately — it's an array, not a
62
- // key/value bundle, and must reflect the union of both sources plus
63
- // any language directories that exist on disk.
97
+ // Serve the computed supported-locales.json
64
98
  if (relativePath === `locales/${SUPPORTED_LOCALES_FILE}`) {
65
- const localLocalesDir = path.resolve(server.config.root, "public/locales");
66
- const merged = computeSupportedLocales(appShellUiLocalesDir, localLocalesDir);
67
- if (merged.length === 0) {
99
+ if (effectiveLocales.length === 0) {
68
100
  res.statusCode = 404;
69
101
  res.end();
70
102
  return;
71
103
  }
72
104
  res.setHeader("Content-Type", "application/json");
73
- res.end(JSON.stringify(merged));
105
+ res.end(JSON.stringify(effectiveLocales));
74
106
  return;
75
107
  }
76
108
  const match = relativePath.match(/^locales\/([^/]+)\/([^/]+\.json)$/);
@@ -80,9 +112,11 @@ export default function copyAppShellLocales() {
80
112
  // Guard against path traversal (e.g. `..` segments)
81
113
  if (lng.includes("..") || nsFile.includes(".."))
82
114
  return next();
83
- const localLocalesBase = path.resolve(server.config.root, "public/locales");
84
- const localPath = path.join(localLocalesBase, lng, nsFile);
85
- if (!localPath.startsWith(localLocalesBase + path.sep))
115
+ // Filter by the effective locale set (same logic as build)
116
+ if (allowedSet && !allowedSet.has(lng))
117
+ return next();
118
+ const localPath = path.join(localLocalesDir, lng, nsFile);
119
+ if (!localPath.startsWith(localLocalesDir + path.sep))
86
120
  return next();
87
121
  const shellPath = appShellUiLocalesDir
88
122
  ? path.join(appShellUiLocalesDir, lng, nsFile)
@@ -118,19 +152,31 @@ export default function copyAppShellLocales() {
118
152
  // guard, locale files would be written to that folder on disk.
119
153
  if (!isBuild || !resolvedOutDir)
120
154
  return;
121
- const appShellUiLocales = resolveAppShellUiLocales();
122
- if (!appShellUiLocales)
123
- return;
124
155
  const targetLocales = path.resolve(resolvedOutDir, "locales");
125
- // Recursive merge: app-shell-ui files are merged into target,
126
- // with existing (local) keys taking priority in JSON files.
127
- // supported-locales.json is skipped during this step.
128
- mergeDirs(appShellUiLocales, targetLocales);
129
- // Compute the union of supported locales from both sources and from
130
- // the language directories that now exist in the merged output.
131
- const merged = computeSupportedLocales(appShellUiLocales, targetLocales);
132
- if (merged.length > 0) {
133
- fs.writeFileSync(path.join(targetLocales, SUPPORTED_LOCALES_FILE), `${JSON.stringify(merged)}\n`);
156
+ const appShellUiLocales = mergeUpstream
157
+ ? resolveAppShellUiLocales()
158
+ : undefined;
159
+ if (appShellUiLocales) {
160
+ const { allowedSet } = resolveEffectiveLocales(appShellUiLocales, targetLocales);
161
+ // Recursive merge: app-shell-ui files are merged into target,
162
+ // with existing (local) keys taking priority in JSON files.
163
+ // supported-locales.json is skipped during this step.
164
+ // Only locale dirs in the allowed set are merged (if filtering).
165
+ mergeDirs(appShellUiLocales, targetLocales, allowedSet);
166
+ }
167
+ // Generate supported-locales.json from the (possibly merged) output.
168
+ // When no upstream is involved, this is purely based on local dirs.
169
+ const { locales: finalLocales } = resolveEffectiveLocales(appShellUiLocales, targetLocales);
170
+ // Always write/normalize the output supported-locales.json so that any
171
+ // stale copy from public/ (already placed into dist/ by Vite) is
172
+ // overwritten with the computed result. If the effective list is empty,
173
+ // remove the file entirely so the output doesn't ship invalid entries.
174
+ const manifestPath = path.join(targetLocales, SUPPORTED_LOCALES_FILE);
175
+ if (finalLocales.length > 0) {
176
+ fs.writeFileSync(manifestPath, `${JSON.stringify(finalLocales)}\n`);
177
+ }
178
+ else if (fs.existsSync(manifestPath)) {
179
+ fs.unlinkSync(manifestPath);
134
180
  }
135
181
  },
136
182
  };
@@ -56,6 +56,7 @@ export async function HvAppShellVitePlugin(opts = {}, env = {}) {
56
56
  {
57
57
  src: resolveModule("es-module-shims"),
58
58
  dest: "bundles",
59
+ rename: { stripBase: true },
59
60
  },
60
61
  ...(!devMode && buildEntryPoint
61
62
  ? [
@@ -70,6 +71,7 @@ export async function HvAppShellVitePlugin(opts = {}, env = {}) {
70
71
  }
71
72
  }),
72
73
  dest: "bundles",
74
+ rename: { stripBase: true },
73
75
  },
74
76
  ]
75
77
  : []),
@@ -118,6 +120,6 @@ export async function HvAppShellVitePlugin(opts = {}, env = {}) {
118
120
  // generate the shell script to replace the placeholders in the index.html
119
121
  generateEmptyShell && generateBashScript(externalImportMap, inlineConfig),
120
122
  // copy/merge app-shell-ui locales into dist (build) or serve via middleware (dev)
121
- (buildEntryPoint || devMode) && copyAppShellLocales(),
123
+ copyAppShellLocales(buildEntryPoint),
122
124
  ];
123
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hitachivantara/app-shell-vite-plugin",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "author": "Hitachi Vantara UI Kit Team",
@@ -21,8 +21,8 @@
21
21
  "@emotion/cache": "^11.11.0",
22
22
  "@emotion/react": "^11.11.1",
23
23
  "@hitachivantara/app-shell-services": "^2.0.3",
24
- "@hitachivantara/app-shell-shared": "^2.3.0",
25
- "@hitachivantara/app-shell-ui": "^2.3.1",
24
+ "@hitachivantara/app-shell-shared": "^2.3.1",
25
+ "@hitachivantara/app-shell-ui": "^2.3.2",
26
26
  "@hitachivantara/uikit-react-icons": "^6.0.4",
27
27
  "@hitachivantara/uikit-react-shared": "^6.0.4",
28
28
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -37,7 +37,7 @@
37
37
  "react-dom": "^18.2.0",
38
38
  "react-router-dom": "^6.9.0",
39
39
  "rollup": "^4.57.1",
40
- "vite-plugin-static-copy": "^3.1.0"
40
+ "vite-plugin-static-copy": "^4.1.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "vite": "^4.1.4 || ^5.0.4 || ^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -59,5 +59,5 @@
59
59
  },
60
60
  "./package.json": "./package.json"
61
61
  },
62
- "gitHead": "96cf5b00b7c82a85b7ebae56e00d48c0b543bebc"
62
+ "gitHead": "333e132a9823018d508ebbe71c81d0b467f9008d"
63
63
  }