@mendix/pluggable-widgets-tools
Version:
Mendix Pluggable Widgets Tools
361 lines (339 loc) • 14 kB
JavaScript
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { existsSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import alias from "@rollup/plugin-alias";
import { getBabelInputPlugin, getBabelOutputPlugin } from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import image from "@rollup/plugin-image";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import replace from "rollup-plugin-re";
import typescript from "@rollup/plugin-typescript";
import colors from "ansi-colors";
import postcssImport from "postcss-import";
import postcssUrl from "postcss-url";
import rollupLoadConfigFile from "rollup/dist/loadConfigFile.js";
import clear from "rollup-plugin-clear";
import command from "rollup-plugin-command";
import license from "rollup-plugin-license";
import livereload from "rollup-plugin-livereload";
import postcss from "rollup-plugin-postcss";
import terser from "@rollup/plugin-terser";
import shelljs from "shelljs";
import { widgetTyping } from "./rollup-plugin-widget-typing.mjs";
import {
editorConfigEntry,
isTypescript,
previewEntry,
projectPath,
sourcePath,
widgetEntry,
widgetName,
widgetPackage,
widgetVersion,
onwarn
} from "./shared.mjs";
import { copyLicenseFile, createMpkFile, licenseCustomTemplate } from "./helpers/rollup-helper.mjs";
import url from "./rollup-plugin-assets.mjs";
const { loadConfigFile } = rollupLoadConfigFile;
const { cp } = shelljs;
const outDir = join(sourcePath, "/dist/tmp/widgets/");
const outWidgetDir = join(widgetPackage.replace(/\./g, "/"), widgetName.toLowerCase());
const outWidgetFile = join(outWidgetDir, `${widgetName}`);
const absoluteOutPackageDir = join(outDir, outWidgetDir);
const mpkDir = join(sourcePath, "dist", widgetVersion);
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetPackage}.${widgetName}.mpk`);
const assetsDirName = "assets";
const absoluteOutAssetsDir = join(absoluteOutPackageDir, assetsDirName);
const outAssetsDir = join(outWidgetDir, assetsDirName);
const imagesAndFonts = [
"**/*.svg",
"**/*.png",
"**/*.jp(e)?g",
"**/*.gif",
"**/*.webp",
"**/*.ttf",
"**/*.woff(2)?",
"**/*.eot"
];
const extensions = [".js", ".jsx", ".tsx", ".ts"];
const commonExternalLibs = [
// "mendix" and internals under "mendix/"
/^mendix($|\/)/,
// "react"
/^react$/,
// "react/jsx-runtime"
/^react\/jsx-runtime$/,
// "react-dom"
/^react-dom$/
];
const webExternal = [...commonExternalLibs, /^big.js$/];
/**
* This function is used by postcss-url.
* Its main purpose to "adjust" asset path so that
* after bundling css by studio assets paths stay correct.
* Adjustment is required because of assets copying -- postcss-url can copy
* files, but final url will be relative to *destination* file and though
* will be broken after bundling by studio (pro).
*
* Example
* before: assets/icon.png
* after: com/mendix/widget/web/accordion/assets/icon.png
*/
const cssUrlTransform = asset =>
asset.url.startsWith(`${assetsDirName}/`) ? `${outWidgetDir.replace(/\\/g, "/")}/${asset.url}` : asset.url;
export default async args => {
const production = Boolean(args.configProduction);
if (!production && projectPath) {
console.info(colors.blue(`Project Path: ${projectPath}`));
}
const result = [];
["amd", "es"].forEach(outputFormat => {
result.push({
input: widgetEntry,
output: {
format: outputFormat,
file: join(outDir, `${outWidgetFile}.${outputFormat === "es" ? "mjs" : "js"}`),
sourcemap: !production ? "inline" : false
},
external: webExternal,
plugins: [
...getClientComponentPlugins(),
url({
include: imagesAndFonts,
limit: 0,
publicPath: `${join("widgets", outAssetsDir)}/`, // Prefix for the actual import, relative to Mendix web server root
destDir: absoluteOutAssetsDir
}),
postCssPlugin(outputFormat, production),
alias({
entries: {
"react-hot-loader/root": fileURLToPath(new URL("hot", import.meta.url)),
}
}),
...getCommonPlugins({
sourceMaps: !production,
extensions,
transpile: production,
babelConfig: {
presets: [["@babel/preset-env", { targets: { safari: "12" } }]],
allowAllFormats: true
},
external: webExternal,
licenses: production && outputFormat === "amd"
})
],
onwarn: onwarn(args)
});
});
if (previewEntry) {
result.push({
input: previewEntry,
output: {
format: "commonjs",
file: join(outDir, `${widgetName}.editorPreview.js`),
sourcemap: !production ? "inline" : false
},
external: commonExternalLibs,
plugins: [
postcss({
extensions: [".css", ".sass", ".scss"],
extract: false,
inject: true,
minimize: production,
plugins: [postcssImport(), postcssUrl({ url: "inline" })],
sourceMap: !production ? "inline" : false,
use: ["sass"]
}),
...getCommonPlugins({
sourceMaps: !production,
extensions,
transpile: production,
babelConfig: { presets: [["@babel/preset-env", { targets: { safari: "12" } }]] },
external: commonExternalLibs
})
],
onwarn: onwarn(args)
});
}
if (editorConfigEntry) {
// We target Studio Pro's JS engine that supports only es5 and no source maps
result.push({
input: editorConfigEntry,
output: {
format: "commonjs",
file: join(outDir, `${widgetName}.editorConfig.js`),
sourcemap: false
},
external: commonExternalLibs,
strictDeprecations: true,
treeshake: { moduleSideEffects: false },
plugins: [
url({ include: ["**/*.svg"], limit: 143360 }), // SVG file size limit of 140 kB
...getCommonPlugins({
sourceMaps: false,
extensions,
transpile: true,
babelConfig: { presets: [["@babel/preset-env", { targets: { ie: "11" } }]] },
external: commonExternalLibs
}),
{
closeBundle() {
if (!process.env.ROLLUP_WATCH) {
setTimeout(() => process.exit(0));
}
},
name: 'force-close'
}
],
onwarn: onwarn(args)
});
}
const customConfigPathJS = join(sourcePath, "rollup.config.js");
const customConfigPathESM = join(sourcePath, "rollup.config.mjs");
const existingConfigPath =
existsSync(customConfigPathJS) ? customConfigPathJS
: existsSync(customConfigPathESM) ? customConfigPathESM
: null;
if (existingConfigPath != null) {
const customConfig = await loadConfigFile(existingConfigPath, { ...args, configDefaultConfig: result });
customConfig.warnings.flush();
return customConfig.options;
}
return result;
function getCommonPlugins(config) {
return [
nodeResolve({ preferBuiltins: false, mainFields: ["module", "browser", "main"] }),
isTypescript
? typescript({
noEmitOnError: !args.watch,
sourceMap: config.sourceMaps,
inlineSources: config.sourceMaps,
target: "es2022", // we transpile the result with babel anyway, see below
exclude: ["**/__tests__/**/*"]
})
: null,
// Babel can transpile source JS and resulting JS, hence are input/output plugins. The good
// practice is to do the most of conversions on resulting code, since then we ensure that
// babel doesn't interfere with `import`s and `require`s used by rollup/commonjs plugin;
// also resulting code includes generated code that deserve transpilation as well.
getBabelInputPlugin({
sourceMaps: config.sourceMaps,
babelrc: false,
babelHelpers: "bundled",
overrides: [
{
test: /node_modules/,
plugins: ["@babel/plugin-transform-flow-strip-types", "@babel/plugin-transform-react-jsx"]
},
{
exclude: /node_modules/,
plugins: [["@babel/plugin-transform-react-jsx", { pragma: "createElement" }]]
}
]
}),
commonjs({
extensions: config.extensions,
transformMixedEsModules: true,
requireReturnsDefault: "auto",
ignore: id => (config.external || []).some(value => new RegExp(value).test(id))
}),
replace({
patterns: [
{
test: "process.env.NODE_ENV",
replace: production ? "'production'" : "'development'"
}
]
}),
config.transpile
? getBabelOutputPlugin({
sourceMaps: config.sourceMaps,
babelrc: false,
compact: false,
...(config.babelConfig || {})
})
: null,
image(),
production ? terser() : null,
config.licenses
? license({
thirdParty: {
includePrivate: true,
output: [
{
file: join(outDir, "dependencies.txt")
},
{
file: join(outDir, "dependencies.json"),
template: licenseCustomTemplate
}
]
}
})
: null,
// We need to create .mpk and copy results to test project after bundling is finished.
// In case of a regular build is it is on `writeBundle` of the last config we define
// (since rollup processes configs sequentially). But in watch mode rollup re-bundles only
// configs affected by a change => we cannot know in advance which one will be "the last".
// So we run the same logic for all configs, letting the last one win.
command([
async () => config.licenses && copyLicenseFile(sourcePath, outDir),
async () =>
createMpkFile({
mpkDir,
mpkFile,
widgetTmpDir: outDir,
isProduction: production,
mxProjectPath: projectPath,
deploymentPath: "deployment/web/widgets"
})
])
];
}
function getClientComponentPlugins() {
return [
isTypescript ? widgetTyping({ sourceDir: join(sourcePath, "src") }) : null,
clear({ targets: [outDir, mpkDir] }),
command([
() => {
cp(join(sourcePath, "src/**/*.xml"), outDir);
if (existsSync(`src/${widgetName}.icon.png`) || existsSync(`src/${widgetName}.tile.png`)) {
cp(join(sourcePath, `src/${widgetName}.@(tile|icon)?(.dark).png`), outDir);
}
}
]),
args.watch && !production && projectPath ? livereload() : null
];
}
};
export function postCssPlugin(outputFormat, production, postcssPlugins = []) {
return postcss({
extensions: [".css", ".sass", ".scss"],
extract: outputFormat === "amd",
inject: false,
minimize: production,
plugins: [
postcssImport(),
/**
* We need two copies of postcss-url because of final styles bundling in studio (pro).
* On line below, we just copying assets to widget bundle directory (com.mendix.widgets...)
* To make it work, this plugin have few requirements:
* 1. You should put your assets in src/assets/
* 2. You should use relative path in your .scss files (e.g. url(../assets/icon.png)
* 3. This plugin relies on `to` property of postcss plugin and it should be present, when
* copying files to destination.
*/
postcssUrl({ url: "copy", assetsPath: "assets" }),
/**
* This instance of postcss-url is just for adjusting asset path.
* Check doc comment for *createCssUrlTransform* for explanation.
*/
postcssUrl({ url: cssUrlTransform }),
...postcssPlugins
],
sourceMap: !production ? "inline" : false,
use: ["sass"],
to: join(outDir, `${outWidgetFile}.css`)
});
}