Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_modules
*.launch
mxproject
coverage
test/results

# Packed packages
mendix-*.tgz
14 changes: 14 additions & 0 deletions .pnpmfile.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function beforePacking(pkg) {
// Remove bundled dependency from published package
delete pkg.dependencies['@mendix/widget-typings-generator'];

console.log('✓ Removed bundled dependency from package.json');

return pkg;
}

module.exports = {
hooks: {
beforePacking
}
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"repository": {
"type": "git",
"url": "https://github.com/mendix/widgets-tools.git"
}
},
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
}
2 changes: 1 addition & 1 deletion packages/command-tests/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async function main() {
console.log("Preparing...");

const pluggableWidgetsToolsPath = "../pluggable-widgets-tools";
const { stdout: packOutput } = await execAsync("npm pack", join(__dirname, pluggableWidgetsToolsPath));
const { stdout: packOutput } = await execAsync("pnpm pack", join(__dirname, pluggableWidgetsToolsPath));
const toolsPackagePath = join(__dirname, pluggableWidgetsToolsPath, packOutput.trim().split(/\n/g).pop());

const workDirs = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { promises as fs } from "fs";
import { extname, join } from "path";
import { listDir } from "./shared.mjs";

const { transformPackage } = await import(new URL("../dist/typings-generator/index.js", import.meta.url));
import { transformPackage } from "../dist/widget-typings-generator.js";

export function widgetTyping({ sourceDir }) {
let firstRun = true;
Expand Down
1 change: 0 additions & 1 deletion packages/pluggable-widgets-tools/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const webConfig = {
rootDir: ".",
testMatch: [
"<rootDir>/src/web/**/*.spec.{ts,tsx}",
"<rootDir>/src/typings-generator/**/*.spec.{ts,tsx}",
"<rootDir>/src/utils/**/*.spec.{ts,tsx}"
]
};
Expand Down
4 changes: 3 additions & 1 deletion packages/pluggable-widgets-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"pluggable-widgets-tools": "bin/mx-scripts.js"
},
"scripts": {
"prepack": "shx rm -rf dist && tsc",
"build:typings": "pnpm --filter=@mendix/widget-typings-generator run build",
"prepack": "shx rm -rf dist && pnpm build:typings && tsc && rollup -c prepack.rollup.mjs",
"test": "jest"
},
"files": [
Expand All @@ -25,6 +26,7 @@
"utils"
],
"dependencies": {
"@mendix/widget-typings-generator": "workspace:*",
"@babel/core": "^7.26.0",
"@babel/plugin-transform-class-properties": "^7.25.9",
"@babel/plugin-transform-private-methods": "^7.25.9",
Expand Down
21 changes: 21 additions & 0 deletions packages/pluggable-widgets-tools/prepack.rollup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default [
{
treeshake: false,
input: 'node_modules/@mendix/widget-typings-generator/dist/index.js',
output: {
file: 'dist/widget-typings-generator.js',
format: 'cjs',
exports: 'named',
sourcemap: false
},
plugins: [
commonjs(),
nodeResolve({
preferBuiltins: true
}),
]
}
];
5 changes: 3 additions & 2 deletions packages/pluggable-widgets-tools/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./common";
export * from "./native/common";
export * from "./web/common";
export * from "./utils/typings";
export * from "./utils";
export * from "./utils/typings";
export * from "./web/common";

3 changes: 3 additions & 0 deletions packages/vite-config-widgets-web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
node_modules
test/results
142 changes: 142 additions & 0 deletions packages/vite-config-widgets-web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# @mendix/vite-config-widgets-web

This package provides Vite configuration for building Mendix pluggable web widgets.


## Installation

```bash
pnpm add -D @mendix/vite-config-widgets-web vite
```

## Contents

- `config.web.ts` – thin orchestrator and public entrypoint.
- `types.ts` – shared types used by the config/build modules.
- `config/` – config derivation and mode handling.
- `build/` – editor artifact and MPK build steps.
- `helpers/` – package metadata/path helpers.
- `test/` – integration tests for end-to-end verification.
- `benchmark.js` – helper script to compare build time and output size between the
existing Rollup build and the new Vite build for a given widget.

## Usage

Create a local `vite.config.ts` in your widget package:

```ts
import { createWidgetViteConfig } from "@mendix/vite-config-widgets-web/config.web";

export default createWidgetViteConfig();
```

You can optionally override inferred values:

```ts
import { createWidgetViteConfig } from "@mendix/vite-config-widgets-web/config.web";

export default createWidgetViteConfig({
widgetName: "MyWidget",
runtimeDirectoryName: "mywidget"
});
```

## Build Modes

This config supports two build modes via the `--mode` flag:

### Development Mode (`--mode dev`)

Development builds prioritize debugging and quick iteration:

- **Minification:** Disabled
- **Source Maps:** Inline (for debugging)
- **Optimization:** Off (preserves code structure)
- **NODE_ENV:** `"development"`
- **Output Size:** Larger MPK (suitable for local dev and CI)

```json
"scripts": {
"build": "vite build --mode dev"
}
```

### Production Mode (`--mode prod` or default)

Production builds prioritize size and performance:

- **Minification:** Full (esbuild)
- **Source Maps:** None
- **Optimization:** On (tree-shaking, inlining, etc.)
- **NODE_ENV:** `"production"`
- **Output Size:** Smaller MPK (suitable for releases and marketplace)

```json
"scripts": {
"release": "vite build --mode prod"
}
```

If no mode is specified, production mode is used by default.

## Internal Module Map

- `vite.config.ts`: public exports and Vite `defineConfig` wiring
- `config/create.ts`: top-level Vite config object creation
- `config/resolve.ts`: resolves widget/runtime config and build mode
- `config/infer.ts`: infers file paths/artifacts/editor entries
- `build/editor-artifacts.ts`: builds editor preview/config outputs
- `build/mpk.ts`: stages files and creates the `.mpk`
- `helpers/package-json.ts`: package.json loading and widget name resolution
- `types.ts`: cross-module type definitions

## Development & Testing

### Build Output Structure

The build process creates artifacts in `dist/tmp/widgets/`:

```
dist/
├── tmp/
│ └── widgets/ # Staging directory for MPK
│ ├── {WidgetName}.xml # Widget definition
│ ├── package.xml # Package metadata
│ └── {packagePath}/ # Runtime files
│ └── {runtimeDir}/
│ ├── {WidgetName}.js # CommonJS bundle
│ └── {WidgetName}.mjs # ES Module bundle
└── {version}/
└── {WidgetName}.mpk # Final distributable package
```

### Integration Tests

Integration tests verify the package works end-to-end by building a real widget in an isolated environment:

```bash
# Run integration tests
pnpm test:integration

# Clean test artifacts
pnpm test:integration:clean
```

**How it works:**
1. Creates a temporary directory (using Node.js `tmpdir()`)
2. Packs the vite-config package as a tarball
3. Copies test widget to temp directory
4. Installs dependencies and the packed tarball
5. Builds the test widget
6. Verifies all artifacts (MPK, runtime files, metadata)
7. Copies results to `test/results/` for inspection
8. Cleans up temporary directory

This ensures the package works correctly when installed from npm without interfering with the monorepo.

### Package Scripts

- `pnpm build` - Build the vite config package
- `pnpm test:integration` - Run end-to-end integration tests
- `pnpm test:integration:clean` - Remove test artifacts

12 changes: 12 additions & 0 deletions packages/vite-config-widgets-web/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as esbuild from "esbuild";

await esbuild.build({
entryPoints: ["config.web.ts"],
bundle: true,
platform: "node",
format: "esm",
outfile: "dist/config.web.mjs",
external: ["vite", "archiver"],
});

console.log("✓ Built dist/config.web.mjs");
39 changes: 39 additions & 0 deletions packages/vite-config-widgets-web/build/editor-artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { build as viteBuild } from "vite";
import type { EditorBuild } from "../types";
import { getResolveAlias } from "../config/resolve";

export async function buildEditorArtifacts(editorBuilds: EditorBuild[], isDev: boolean = false): Promise<void> {
const editorOutDir = "dist/tmp/widgets";
const alias = getResolveAlias();
const minifyMode = isDev ? false : "esbuild";
const sourcemapMode = isDev ? "inline" : false;

for (const editorBuild of editorBuilds) {
await viteBuild({
configFile: false,
resolve: {
alias
},
build: {
target: "es2019",
minify: minifyMode,
sourcemap: sourcemapMode,
emptyOutDir: false,
outDir: editorOutDir,
lib: {
entry: editorBuild.entry,
formats: [editorBuild.format ?? "cjs"],
fileName: () => editorBuild.outputFile
},
rollupOptions: {
external: editorBuild.externals,
output: {
format: editorBuild.format ?? "cjs",
entryFileNames: editorBuild.outputFile,
inlineDynamicImports: true
}
}
}
});
}
}
84 changes: 84 additions & 0 deletions packages/vite-config-widgets-web/build/mpk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Archiver from "archiver";
import { copyFileSync, createWriteStream, existsSync, mkdirSync, rmSync } from "fs";
import { cp } from "fs/promises";
import { join, resolve } from "path";
import type { ResolvedConfig } from "../types";

async function copyDir(src: string, dest: string): Promise<void> {
mkdirSync(dest, { recursive: true });
if (existsSync(src)) {
await cp(src, dest, { recursive: true, force: true });
}
}

export async function createMPK(options: ResolvedConfig): Promise<string> {
const distPath = resolve(process.cwd(), "dist");
const stagingDir = join(distPath, "tmp", "widgets");
const outputDir = join(process.cwd(), "dist", options.widgetVersion);
const mpkPath = join(outputDir, options.mpkName);

mkdirSync(stagingDir, { recursive: true });
mkdirSync(outputDir, { recursive: true });

for (const file of options.metadataFiles) {
const srcPath = resolve(process.cwd(), file.src);
const destPath = join(stagingDir, file.dest);
mkdirSync(join(stagingDir, file.dest.split("/").slice(0, -1).join("/")), { recursive: true });
if (existsSync(srcPath)) {
copyFileSync(srcPath, destPath);
}
}

for (const requiredArtifact of options.requiredArtifacts ?? []) {
const requiredPath = join(stagingDir, requiredArtifact);
if (!existsSync(requiredPath)) {
throw new Error(`Missing compiled artifact: ${requiredPath}`);
}
}

for (const removePath of options.removeBeforeCopy ?? []) {
const absolutePath = join(stagingDir, removePath);
if (existsSync(absolutePath)) {
rmSync(absolutePath);
}
}

await new Promise<void>((resolvePromise, reject) => {
const output = createWriteStream(mpkPath);
const archive = (Archiver as unknown as (format: string, options: { zlib: { level: number } }) => any)("zip", {
zlib: { level: 9 }
});

output.on("close", () => {
console.log(`Created ${mpkPath} (${archive.pointer()} bytes)`);
resolvePromise();
});

archive.on("error", reject);
archive.pipe(output);
archive.directory(stagingDir, false);
archive.finalize();
});

return mpkPath;
}

export async function deployMPKToMxProject(mpkPath: string): Promise<void> {
const mxProjectPath = process.env.MX_PROJECT_PATH;

if (!mxProjectPath) {
return;
}

const widgetsDir = resolve(mxProjectPath, "widgets");
const fileName = mpkPath.split("/").pop();

if (!fileName) {
throw new Error(`Invalid MPK path: ${mpkPath}`);
}

mkdirSync(widgetsDir, { recursive: true });
const targetPath = join(widgetsDir, fileName);
copyFileSync(mpkPath, targetPath);
console.log(`Deployed ${fileName} to ${widgetsDir}`);
}
Loading
Loading