@hitachivantara/app-shell-vite-plugin 2.3.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.
@@ -62,7 +62,7 @@ export function mapFolderIndexFilesToRoutes(root, viewsFolder) {
62
62
  .replaceAll(/\$/g, ":")
63
63
  .toLowerCase();
64
64
  const viewConfig = {
65
- bundle: `@self/${getFinalModuleName(bundle)}.js`,
65
+ bundle: `$app/${getFinalModuleName(bundle)}.js`,
66
66
  route,
67
67
  };
68
68
  routes.push({ viewConfig, module: bundle });
@@ -102,11 +102,15 @@ export function applyAutomaticViewsAndRoutes(config, selfAppName, root, viewsFol
102
102
  const flattenedViews = flattenViews(appShellConfiguration.mainPanel.views);
103
103
  const existingRoutes = flattenedViews.map((view) => view.route);
104
104
  const existingBundles = flattenedViews.reduce((bundles, view) => {
105
- if (view.bundle.startsWith("@self/")) {
105
+ if (view.bundle.startsWith("$app/")) {
106
106
  bundles.push(view.bundle);
107
107
  }
108
+ else if (view.bundle.startsWith("@self/")) {
109
+ // TODO(major): remove @self/ support in favour of $app/
110
+ bundles.push(view.bundle.replace("@self/", "$app/"));
111
+ }
108
112
  else if (view.bundle.startsWith(selfAppName)) {
109
- bundles.push(view.bundle.replace(selfAppName, "@self"));
113
+ bundles.push(view.bundle.replace(selfAppName, "$app"));
110
114
  }
111
115
  return bundles;
112
116
  }, []);
@@ -6,4 +6,4 @@ export declare const require: NodeJS.Require;
6
6
  * @param suffix to be added after the module path
7
7
  * @returns The module path normalized
8
8
  */
9
- export declare function resolveModule(moduleName: string, suffix?: string): string;
9
+ export declare function resolveModule(moduleName: string, suffix?: string): any;
@@ -1,16 +1,30 @@
1
1
  import type { PluginOption } from "vite";
2
2
  import type { HvAppShellConfig } from "@hitachivantara/app-shell-shared";
3
+ /**
4
+ * Options for the configuration processor plugin.
5
+ */
6
+ export interface ProcessConfigurationOptions {
7
+ /** Project root directory. */
8
+ root: string;
9
+ /** The original App Shell configuration json. */
10
+ appShellConfig: HvAppShellConfig;
11
+ /** The name of the application bundle being built. */
12
+ selfAppName: string;
13
+ /** The set of modules to be created by the rollup. */
14
+ modules: string[];
15
+ /** If true, the index.html entry point will be added to the bundle. */
16
+ buildEntryPoint: boolean;
17
+ /** Flag to control if config is included at index.html. */
18
+ inlineConfig: boolean;
19
+ /** Flag to control if we are creating an empty AppShell instance. */
20
+ generateEmptyShell: boolean;
21
+ /** If true, always writes app-shell.config.json to dist for dual-use packages. */
22
+ experimentalNewPackageLayout: boolean;
23
+ }
3
24
  /**
4
25
  * Process configuration, executing several tasks:
5
26
  * - Create rollup configuration to support module creation
6
27
  * - Generates final transformed configuration json
7
28
  * - "base" value is always "./" for build, and main app baseUrl for preview or dev
8
- * @param root Project root directory.
9
- * @param appShellConfig The original App Shell configuration json.
10
- * @param selfAppName The name of the application bundle being built.
11
- * @param buildEntryPoint If true, the index.html entry point will be added to the bundle.
12
- * @param inlineConfig flag to control if config is included at index.html
13
- * @param generateEmptyShell flag to control if we are creating an empty AppShell instance
14
- * @param modules the set of modules to be created by the rollup
15
29
  */
16
- export default function processConfiguration(root: string, appShellConfig: HvAppShellConfig, selfAppName: string, buildEntryPoint: boolean, inlineConfig: boolean, generateEmptyShell: boolean, modules?: string[]): PluginOption;
30
+ export default function processConfiguration(options: ProcessConfigurationOptions): PluginOption;
@@ -7,15 +7,9 @@ import sharedDependencies from "./shared-dependencies.js";
7
7
  * - Create rollup configuration to support module creation
8
8
  * - Generates final transformed configuration json
9
9
  * - "base" value is always "./" for build, and main app baseUrl for preview or dev
10
- * @param root Project root directory.
11
- * @param appShellConfig The original App Shell configuration json.
12
- * @param selfAppName The name of the application bundle being built.
13
- * @param buildEntryPoint If true, the index.html entry point will be added to the bundle.
14
- * @param inlineConfig flag to control if config is included at index.html
15
- * @param generateEmptyShell flag to control if we are creating an empty AppShell instance
16
- * @param modules the set of modules to be created by the rollup
17
10
  */
18
- export default function processConfiguration(root, appShellConfig, selfAppName, buildEntryPoint, inlineConfig, generateEmptyShell, modules = []) {
11
+ export default function processConfiguration(options) {
12
+ const { root, appShellConfig, selfAppName, modules, buildEntryPoint, inlineConfig, generateEmptyShell, experimentalNewPackageLayout, } = options;
19
13
  let finalAppShellConfig;
20
14
  let basePath;
21
15
  return {
@@ -58,7 +52,7 @@ export default function processConfiguration(root, appShellConfig, selfAppName,
58
52
  * @param options build options
59
53
  */
60
54
  async generateBundle(options) {
61
- if (generateEmptyShell || !buildEntryPoint) {
55
+ if (generateEmptyShell) {
62
56
  return;
63
57
  }
64
58
  // obtain the directory (dist) where the new config file will be placed
@@ -82,11 +76,16 @@ export default function processConfiguration(root, appShellConfig, selfAppName,
82
76
  finalAppShellConfig.baseUrl = basePath;
83
77
  }
84
78
  finalAppShellConfig.apps = undefined;
85
- // Replace all @self references using simple string replacement
79
+ // Replace all $app and @self (deprecated) references using simple string replacement.
86
80
  let configString = JSON.stringify(finalAppShellConfig);
81
+ configString = configString.replaceAll(`"$app/`, `"${selfAppName}/`);
82
+ // TODO(major): remove @self/ support in favour of $app/
87
83
  configString = configString.replaceAll(`"@self/`, `"${selfAppName}/`);
88
84
  finalAppShellConfig = JSON.parse(configString);
89
- if (!inlineConfig) {
85
+ // Write app-shell.config.json to dist when:
86
+ // - inlineConfig is false (standard flow), OR
87
+ // - experimentalNewPackageLayout is true (ensures dual-use: app shell or app bundle)
88
+ if (!inlineConfig || experimentalNewPackageLayout) {
90
89
  fs.writeFileSync(path.resolve(targetDir, "app-shell.config.json"), JSON.stringify(finalAppShellConfig));
91
90
  }
92
91
  },
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from "vite";
2
+ /**
3
+ * Generates a cleaned dist/package.json during Vite builds.
4
+ */
5
+ export default function distPackageJsonPlugin(root?: string, sourceCondition?: string): Plugin;
@@ -0,0 +1,211 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ // ─── Constants ───────────────────────────────────────────────────────────────
4
+ /**
5
+ * Default custom export condition used internally by workspace packages
6
+ * to resolve TypeScript source during development.
7
+ *
8
+ * This condition must never leak into published artifacts because external
9
+ * consumers would not be able to resolve it.
10
+ */
11
+ const DEFAULT_SOURCE_CONDITION = "@pentaho-apps:source";
12
+ /**
13
+ * Runtime-relevant package.json fields copied into dist output.
14
+ *
15
+ * All other fields are intentionally excluded.
16
+ */
17
+ const INCLUDED_FIELDS = [
18
+ "name",
19
+ "version",
20
+ "type",
21
+ "license",
22
+ "description",
23
+ "dependencies",
24
+ "peerDependencies",
25
+ "peerDependenciesMeta",
26
+ "optionalDependencies",
27
+ ];
28
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
29
+ /**
30
+ * Reads and parses a JSON file.
31
+ *
32
+ * Returns `undefined` if the file does not exist or cannot be parsed.
33
+ */
34
+ function readJsonFile(filePath) {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /**
43
+ * Removes the build output prefix from an export path.
44
+ *
45
+ * Example:
46
+ * ./dist/index.js → ./index.js
47
+ */
48
+ function stripDistPrefix(prefix, value) {
49
+ return value.startsWith(prefix) ? `./${value.slice(prefix.length)}` : value;
50
+ }
51
+ /**
52
+ * Transforms a package export target for dist/package.json.
53
+ *
54
+ * Processing rules:
55
+ *
56
+ * Strings:
57
+ * - Must resolve inside build.outDir
58
+ * - build.outDir prefix is stripped
59
+ *
60
+ * Objects:
61
+ * - Removes dev-only source condition
62
+ * - Recursively transforms nested targets
63
+ * - Removed if empty after transformation
64
+ *
65
+ * Arrays:
66
+ * - Node fallback arrays are eagerly collapsed to the first
67
+ * valid non-null target because import maps do not support
68
+ * Node conditional fallback resolution semantics.
69
+ *
70
+ * null:
71
+ * - Preserved as explicit export exclusion
72
+ *
73
+ * undefined:
74
+ * - Internal sentinel meaning "omit this entry"
75
+ */
76
+ function transformExportTargetForDist(target, distPrefix, sourceCondition) {
77
+ // Explicit export exclusion must be preserved
78
+ if (target === null) {
79
+ return null;
80
+ }
81
+ // Direct export path
82
+ if (typeof target === "string") {
83
+ const stripped = stripDistPrefix(distPrefix, target);
84
+ // Ignore paths outside dist output
85
+ return stripped === target ? undefined : stripped;
86
+ }
87
+ // Fallback array
88
+ if (Array.isArray(target)) {
89
+ for (const entry of target) {
90
+ const resolved = transformExportTargetForDist(entry, distPrefix, sourceCondition);
91
+ if (resolved !== undefined && resolved !== null) {
92
+ return resolved;
93
+ }
94
+ }
95
+ return undefined;
96
+ }
97
+ // Conditional exports object
98
+ const result = {};
99
+ for (const [condition, value] of Object.entries(target)) {
100
+ // Remove workspace-only source condition
101
+ if (condition === sourceCondition) {
102
+ continue;
103
+ }
104
+ const transformed = transformExportTargetForDist(value, distPrefix, sourceCondition);
105
+ // Omitted targets disappear entirely
106
+ if (transformed === undefined) {
107
+ continue;
108
+ }
109
+ result[condition] = transformed;
110
+ }
111
+ return Object.keys(result).length === 0 ? undefined : result;
112
+ }
113
+ /**
114
+ * Normalizes package exports into canonical subpath map form.
115
+ *
116
+ * Converts:
117
+ *
118
+ * "exports": "./index.js"
119
+ * → { ".": "./index.js" }
120
+ *
121
+ * "exports": { "import": "./index.js" }
122
+ * → { ".": { "import": "./index.js" } }
123
+ *
124
+ * Leaves already-normalized subpath maps untouched.
125
+ */
126
+ function normalizeExportsToSubpathMap(exportsField) {
127
+ if (exportsField == null) {
128
+ throw new Error(`[App Shell]: package.json is missing "exports".`);
129
+ }
130
+ // Sugar form:
131
+ // "exports": "./index.js"
132
+ // "exports": ["./a.js", "./b.js"]
133
+ if (typeof exportsField === "string" || Array.isArray(exportsField)) {
134
+ return { ".": exportsField };
135
+ }
136
+ const keys = Object.keys(exportsField);
137
+ // Already a subpath map
138
+ if (keys.every((key) => key.startsWith("."))) {
139
+ return exportsField;
140
+ }
141
+ // Root conditional exports sugar
142
+ return { ".": exportsField };
143
+ }
144
+ /**
145
+ * Appends standard metadata exports required at runtime.
146
+ */
147
+ function appendStandardExports(exportsMap, outDir) {
148
+ exportsMap["./package.json"] = "./package.json";
149
+ exportsMap["./app-shell.config.json"] = "./app-shell.config.json";
150
+ if (fs.existsSync(path.join(outDir, "locales"))) {
151
+ exportsMap["./locales/*"] = "./locales/*";
152
+ }
153
+ }
154
+ // ─── Dist package.json generation ────────────────────────────────────────────
155
+ /**
156
+ * Generates dist/package.json from the source package.
157
+ *
158
+ * The generated package:
159
+ * - strips dev-only export conditions
160
+ * - rewrites export paths relative to dist/
161
+ * - preserves explicit null exclusions
162
+ * - supports nested conditional exports
163
+ */
164
+ function generateDistPackageJson(root, outDir, buildOutDir, sourceCondition) {
165
+ const pkgPath = path.join(root, "package.json");
166
+ const pkg = readJsonFile(pkgPath);
167
+ if (!pkg || !fs.existsSync(outDir)) {
168
+ return false;
169
+ }
170
+ const rawExports = pkg.exports;
171
+ const distPrefix = `./${buildOutDir}/`;
172
+ const transformedExports = transformExportTargetForDist(normalizeExportsToSubpathMap(rawExports), distPrefix, sourceCondition);
173
+ /**
174
+ * The entire exports tree may collapse after removing dev-only
175
+ * conditions and invalid dist targets.
176
+ */
177
+ if (transformedExports === undefined) {
178
+ throw new Error(`[App Shell] ${pkg.name}: exports resolved to empty after transformation.`);
179
+ }
180
+ appendStandardExports(transformedExports, outDir);
181
+ const distPkg = {};
182
+ // Copy runtime-relevant fields
183
+ for (const field of INCLUDED_FIELDS) {
184
+ if (pkg[field] != null) {
185
+ distPkg[field] = pkg[field];
186
+ }
187
+ }
188
+ distPkg.exports = transformedExports;
189
+ fs.writeFileSync(path.join(outDir, "package.json"), JSON.stringify(distPkg, null, 2) + "\n");
190
+ console.info(`[App Shell] Generated ${buildOutDir}/package.json for ${pkg.name}`);
191
+ return true;
192
+ }
193
+ // ─── Plugin ──────────────────────────────────────────────────────────────────
194
+ /**
195
+ * Generates a cleaned dist/package.json during Vite builds.
196
+ */
197
+ export default function distPackageJsonPlugin(root, sourceCondition = DEFAULT_SOURCE_CONDITION) {
198
+ let config;
199
+ return {
200
+ name: "app-shell:vite-dist-package-json-plugin",
201
+ apply: "build",
202
+ configResolved(resolved) {
203
+ config = resolved;
204
+ },
205
+ closeBundle() {
206
+ const packageRoot = root ?? config.root;
207
+ const outDir = path.resolve(packageRoot, config.build.outDir);
208
+ generateDistPackageJson(packageRoot, outDir, config.build.outDir, sourceCondition);
209
+ },
210
+ };
211
+ }
@@ -97,6 +97,42 @@ export interface AppShellVitePluginOptions {
97
97
  * @default false
98
98
  */
99
99
  disableAppsKeyNormalization?: boolean;
100
+ /**
101
+ * Enables the experimental new package layout feature set.
102
+ *
103
+ * When `true`, the plugin activates behaviors that are part of the
104
+ * new app bundle distribution/publishing package layout. Currently, this gates:
105
+ *
106
+ * - **`dist/package.json` generation** — a cleaned-up `package.json` is
107
+ * written to the Vite output directory after each build. The source
108
+ * `package.json` contains dev-time fields and conditions that do not apply
109
+ * to published/distributed packages; the generated manifest only includes
110
+ * the fields relevant to consumers.
111
+ * - **`dist/app-shell.config.json`** — the resolved App Shell configuration
112
+ * is written to the output directory so that app bundles are self-contained
113
+ * and can be consumed both as standalone App Shells or as bundles for
114
+ * another App Shell.
115
+ *
116
+ * Additional behaviors may be added under this flag in future PRs before
117
+ * the feature set is stabilized and the flag is removed.
118
+ *
119
+ * @default false
120
+ * @experimental
121
+ */
122
+ experimentalNewPackageLayout?: boolean;
123
+ /**
124
+ * The custom exports condition used to resolve TypeScript source files during
125
+ * development. Only applies when `experimentalNewPackageLayout` is `true`.
126
+ *
127
+ * Workspace packages declare a scoped condition (e.g. `"@pentaho-apps:source"`)
128
+ * in their `exports` map that points directly at the TypeScript source. This
129
+ * lets consumers import the live source during development without a prior
130
+ * build step. The condition is stripped from `dist/package.json` so it never
131
+ * leaks to consumers that don't have the TypeScript sources available.
132
+ *
133
+ * @default "@pentaho-apps:source"
134
+ */
135
+ sourceCondition?: string;
100
136
  }
101
137
  /**
102
138
  * Vite plugin to support App Shell apps setup
@@ -10,6 +10,7 @@ import SHARED_DEPENDENCIES from "./shared-dependencies.js";
10
10
  import getVirtualEntrypoints from "./virtual-entrypoints.js";
11
11
  import processConfiguration from "./vite-configuration-processor-plugin.js";
12
12
  import fixCrossOrigin from "./vite-crossorigin-fix-plugin.js";
13
+ import distPackageJsonPlugin from "./vite-dist-package-json-plugin.js";
13
14
  import generateBaseTag from "./vite-generate-base-plugin.js";
14
15
  import generateBashScript from "./vite-generate-bash-script-plugin.js";
15
16
  import generateImportmap, { extraDependencies, } from "./vite-importmap-plugin.js";
@@ -26,7 +27,7 @@ const ViteBuildMode = {
26
27
  * @param env Environment variable
27
28
  */
28
29
  export async function HvAppShellVitePlugin(opts = {}, env = {}) {
29
- const { root = process.cwd(), mode = ViteBuildMode.PRODUCTION, externalImportMap = false, viewsFolder = "src/pages", autoViewsAndRoutes = false, autoMenu = false, inlineConfig = opts.generateEmptyShell ?? false, generateEmptyShell = false, modules = [], disableAppsKeyNormalization = false, } = opts;
30
+ const { root = process.cwd(), mode = ViteBuildMode.PRODUCTION, externalImportMap = false, viewsFolder = "src/pages", autoViewsAndRoutes = false, autoMenu = false, inlineConfig = opts.generateEmptyShell ?? false, generateEmptyShell = false, modules = [], disableAppsKeyNormalization = false, experimentalNewPackageLayout = false, sourceCondition, } = opts;
30
31
  const globalEnv = loadEnv(mode, process.cwd(), "");
31
32
  const { type = globalEnv.CI ? "bundle" : "app" } = opts;
32
33
  console.info(`Vite running in mode: ${mode}`);
@@ -112,7 +113,16 @@ export async function HvAppShellVitePlugin(opts = {}, env = {}) {
112
113
  buildEntryPoint &&
113
114
  generateBaseTag(appShellConfiguration, generateEmptyShell),
114
115
  // configure the build process based on the config file
115
- processConfiguration(root, appShellConfiguration, packageJson.name, buildEntryPoint, inlineConfig, generateEmptyShell, modules.concat(autoViewsBundles)),
116
+ processConfiguration({
117
+ root,
118
+ appShellConfig: appShellConfiguration,
119
+ selfAppName: packageJson.name,
120
+ modules: modules.concat(autoViewsBundles),
121
+ buildEntryPoint,
122
+ inlineConfig,
123
+ generateEmptyShell,
124
+ experimentalNewPackageLayout,
125
+ }),
116
126
  // allow crossorigin="use-credentials" in the index.html
117
127
  fixCrossOrigin(),
118
128
  // serve the app shell config file as json and watch for changes
@@ -121,5 +131,8 @@ export async function HvAppShellVitePlugin(opts = {}, env = {}) {
121
131
  generateEmptyShell && generateBashScript(externalImportMap, inlineConfig),
122
132
  // copy/merge app-shell-ui locales into dist (build) or serve via middleware (dev)
123
133
  copyAppShellLocales(buildEntryPoint),
134
+ // generate dist/package.json for the build output directory
135
+ experimentalNewPackageLayout &&
136
+ distPackageJsonPlugin(root, sourceCondition),
124
137
  ];
125
138
  }
@@ -1,6 +1,8 @@
1
1
  import path from "node:path";
2
2
  const prepareConfigForDevMode = (config, selfAppName) => {
3
3
  let configString = JSON.stringify(config);
4
+ configString = configString.replaceAll(`"$app/`, `"${selfAppName}/`);
5
+ // TODO(major): remove @self/ support in favour of $app/
4
6
  configString = configString.replaceAll(`"@self/`, `"${selfAppName}/`);
5
7
  return JSON.parse(configString);
6
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hitachivantara/app-shell-vite-plugin",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "author": "Hitachi Vantara UI Kit Team",
@@ -20,11 +20,10 @@
20
20
  "dependencies": {
21
21
  "@emotion/cache": "^11.11.0",
22
22
  "@emotion/react": "^11.11.1",
23
- "@hitachivantara/app-shell-services": "^2.0.3",
24
- "@hitachivantara/app-shell-shared": "^2.3.1",
25
- "@hitachivantara/app-shell-ui": "^2.3.2",
26
- "@hitachivantara/uikit-react-icons": "^6.0.4",
27
- "@hitachivantara/uikit-react-shared": "^6.0.4",
23
+ "@hitachivantara/app-shell-services": "^2.0.4",
24
+ "@hitachivantara/app-shell-shared": "^2.3.2",
25
+ "@hitachivantara/app-shell-ui": "^2.3.3",
26
+ "@hitachivantara/uikit-react-shared": "^6.0.5",
28
27
  "@rollup/plugin-commonjs": "^29.0.0",
29
28
  "@rollup/plugin-json": "^6.0.0",
30
29
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -59,5 +58,5 @@
59
58
  },
60
59
  "./package.json": "./package.json"
61
60
  },
62
- "gitHead": "333e132a9823018d508ebbe71c81d0b467f9008d"
61
+ "gitHead": "65c4f4394e8f8c7cccb58203e1c08c6832434638"
63
62
  }