@mendix/pluggable-widgets-tools
Version:
Mendix Pluggable Widgets Tools
269 lines (250 loc) • 10.3 kB
JavaScript
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { existsSync } from "fs";
import { join, relative } from "path";
import { getBabelInputPlugin, getBabelOutputPlugin } from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import image from "@rollup/plugin-image";
import json from "@rollup/plugin-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import replace from "rollup-plugin-re";
import typescript from "@rollup/plugin-typescript";
import url from "@rollup/plugin-url";
import colors from "ansi-colors";
import loadConfigFile from "rollup/dist/loadConfigFile.js";
import clear from "rollup-plugin-clear";
import command from "rollup-plugin-command";
import terser from "@rollup/plugin-terser";
import shelljs from "shelljs";
import { widgetTyping } from "./rollup-plugin-widget-typing.mjs";
import { collectDependencies } from "./rollup-plugin-collect-dependencies.mjs";
import {
editorConfigEntry,
isTypescript,
projectPath,
sourcePath,
widgetEntry,
widgetName,
widgetPackage,
widgetVersion,
onwarn
} from "./shared.mjs";
import { copyLicenseFile, createMpkFile, licenseCustomTemplate } from "./helpers/rollup-helper.mjs";
const { cp } = shelljs;
const { blue } = colors;
const outDir = join(sourcePath, "/dist/tmp/widgets/");
const outWidgetFile = join(widgetPackage.replace(/\./g, "/"), widgetName.toLowerCase(), `${widgetName}`);
const mpkDir = join(sourcePath, "dist", widgetVersion);
const mpkFile = join(mpkDir, process.env.MPKOUTPUT ? process.env.MPKOUTPUT : `${widgetPackage}.${widgetName}.mpk`);
const extensions = [".js", ".jsx", ".tsx", ".ts"];
const editorConfigExternal = [
// "mendix" and internals under "mendix/"
/^mendix($|\/)/,
// "react"
/^react$/,
// "react/jsx-runtime"
/^react\/jsx-runtime$/,
// "react-dom"
/^react-dom$/
];
const nativeExternal = [
/^mendix($|\/)/,
/^react-native($|\/)/,
/^big.js$/,
/^react($|\/)/,
/^react-native-gesture-handler($|\/)/,
/^react-native-reanimated($|\/)/,
/^react-native-fast-image($|\/)/,
/^react-native-svg($|\/)/,
/^react-native-vector-icons($|\/)/,
/^@?react-navigation($|\/)/,
/^react-native-safe-area-context($|\/)/
];
export default async args => {
const production = Boolean(args.configProduction);
if (!production && projectPath) {
console.info(blue(`Project Path: ${projectPath}`));
}
const result = [];
["ios", "android"].forEach((os, i) => {
result.push({
input: widgetEntry,
output: {
format: "es",
file: join(outDir, `${outWidgetFile}.${os}.js`),
sourcemap: false
},
external: nativeExternal,
plugins: [
replace({
patterns: [
{
test: /\b(?<!\.)Platform.OS\b(?!\s*=[^=])/g,
replace: `"${os}"`
}
]
}),
...(i === 0 ? getClientComponentPlugins() : []),
json(),
collectDependencies({
outputDir: outDir,
onlyNative: true,
widgetName,
...(production && i === 0
? {
licenseOptions: {
thirdParty: {
output: [
{
file: join(outDir, "dependencies.txt")
},
{
file: join(outDir, "dependencies.json"),
template: licenseCustomTemplate
}
]
}
}
}
: null)
}),
...getCommonPlugins({
sourceMaps: false,
extensions: [`.${os}.js`, ".native.js", ".js", ".jsx", ".ts", ".tsx"],
transpile: false,
external: nativeExternal,
licenses: production && i === 0
})
],
onwarn: (warning, warn) => {
if (warning.code === "UNUSED_EXTERNAL_IMPORT" && /('|")Platform('|")/.test(warning.message)) {
return;
}
onwarn(args)(warning, warn);
}
});
});
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: editorConfigExternal,
treeshake: { moduleSideEffects: false },
plugins: [
url({ include: ["**/*.svg"], limit: 204800 }), // SVG file size limit of 200 kB
...getCommonPlugins({
sourceMaps: false,
extensions,
transpile: true,
babelConfig: { presets: [["@babel/preset-env", { targets: { ie: "11" } }]] },
external: editorConfigExternal
})
],
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({ mangle: false }) : 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/native/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);
}
}
])
];
}
};