Skip to content

Commit 1873b3e

Browse files
committed
[DX] Rework building process, and add build/watch scripts to packages package.json
1 parent 5517388 commit 1873b3e

File tree

27 files changed

+311
-338
lines changed

27 files changed

+311
-338
lines changed

bin/build_javascript.js

Lines changed: 0 additions & 43 deletions
This file was deleted.

bin/build_package.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* This file is used to compile the assets from an UX package.
3+
*/
4+
5+
const { parseArgs } = require('node:util');
6+
const path = require('node:path');
7+
const fs = require('node:fs');
8+
const glob = require('glob');
9+
const rollup = require('rollup');
10+
const CleanCSS = require('clean-css');
11+
const { getRollupConfiguration } = require('./rollup');
12+
13+
const args = parseArgs({
14+
allowPositionals: true,
15+
options: {
16+
watch: {
17+
type: 'boolean',
18+
description: 'Watch the source files for changes and rebuild when necessary.',
19+
},
20+
},
21+
});
22+
23+
async function main() {
24+
const packageRoot = path.resolve(process.cwd(), args.positionals[0]);
25+
26+
if (!fs.existsSync(packageRoot)) {
27+
console.error(`The package directory "${packageRoot}" does not exist.`);
28+
process.exit(1);
29+
}
30+
31+
if (!fs.existsSync(path.join(packageRoot, 'package.json'))) {
32+
console.error(`The package directory "${packageRoot}" does not contain a package.json file.`);
33+
process.exit(1);
34+
}
35+
36+
const packageData = require(path.join(packageRoot, 'package.json'));
37+
const packageName = packageData.name;
38+
const srcDir = path.join(packageRoot, 'src');
39+
const distDir = path.join(packageRoot, 'dist');
40+
41+
42+
if (!fs.existsSync(srcDir)) {
43+
console.error(`The package directory "${packageRoot}" does not contain a "src" directory.`);
44+
process.exit(1);
45+
}
46+
47+
if (fs.existsSync(distDir)) {
48+
console.log(`Cleaning up the "${distDir}" directory...`);
49+
await fs.promises.rm(distDir, { recursive: true });
50+
await fs.promises.mkdir(distDir);
51+
}
52+
53+
const inputScriptFiles = [
54+
...glob.sync(path.join(srcDir, '*controller.ts')),
55+
...(['@symfony/ux-react', '@symfony/ux-vue', '@symfony/ux-svelte'].includes(packageName)
56+
? [
57+
path.join(srcDir, 'loader.ts'),
58+
path.join(srcDir, 'components.ts'),
59+
]
60+
: []),
61+
...(packageName === '@symfony/stimulus-bundle'
62+
? [
63+
path.join(srcDir, 'loader.ts'),
64+
path.join(srcDir, 'controllers.ts'),
65+
] : []),
66+
];
67+
68+
const inputStyleFile = packageData.config && packageData.config.css_source;
69+
const buildCss = async () => {
70+
const inputStyleFileDist = inputStyleFile ? path.resolve(distDir, `${path.basename(inputStyleFile, '.css')}.min.css`) : undefined;
71+
if (!inputStyleFile) {
72+
return;
73+
}
74+
75+
console.log('Minifying CSS...');
76+
const css = await fs.promises.readFile(inputStyleFile, 'utf-8');
77+
const minified = new CleanCSS().minify(css).styles;
78+
await fs.promises.writeFile(inputStyleFileDist, minified);
79+
}
80+
81+
if (inputScriptFiles.length === 0) {
82+
console.error(`No input files found for package "${packageName}" (directory "${packageRoot}").\nEnsure you have at least a file matching the pattern "src/*_controller.ts", or manually specify input files in "${__filename}" file.`);
83+
process.exit(1);
84+
}
85+
86+
const rollupConfig = getRollupConfiguration({ packageRoot, inputFiles: inputScriptFiles });
87+
88+
if (args.values.watch) {
89+
console.log(`Watching for JavaScript${inputStyleFile ? ' and CSS' : ''} files modifications in "${srcDir}" directory...`);
90+
91+
if (inputStyleFile) {
92+
rollupConfig.plugins = (rollupConfig.plugins || []).concat({
93+
name: 'watcher',
94+
buildStart() {
95+
this.addWatchFile(inputStyleFile);
96+
},
97+
});
98+
}
99+
100+
const watcher = rollup.watch(rollupConfig);
101+
watcher.on('event', ({ result }) => {
102+
if (result) {
103+
result.close();
104+
}
105+
});
106+
watcher.on('change', async (id, { event }) => {
107+
if (event === 'update') {
108+
console.log(`Files were modified, rebuilding...`);
109+
}
110+
111+
if (inputStyleFile && id === inputStyleFile) {
112+
await buildCss();
113+
}
114+
});
115+
} else {
116+
console.log(`Building JavaScript files from ${packageName} package...`);
117+
const start = Date.now();
118+
119+
const bundle = await rollup.rollup(rollupConfig);
120+
await bundle.write(rollupConfig.output);
121+
122+
await buildCss();
123+
124+
console.log(`Done in ${((Date.now() - start) / 1000).toFixed(3)} seconds.`);
125+
}
126+
}
127+
128+
main();

bin/build_styles.js

Lines changed: 0 additions & 46 deletions
This file was deleted.

bin/rollup.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
const resolve = require('@rollup/plugin-node-resolve');
2+
const commonjs = require('@rollup/plugin-commonjs');
3+
const typescript = require('@rollup/plugin-typescript');
4+
const fs = require('fs');
5+
const glob = require('glob');
6+
const path = require('path');
7+
8+
/**
9+
* Guarantees that any files imported from a peer dependency are treated as an external.
10+
*
11+
* For example, if we import `chart.js/auto`, that would not normally
12+
* match the "chart.js" we pass to the "externals" config. This plugin
13+
* catches that case and adds it as an external.
14+
*
15+
* Inspired by https://github.com/oat-sa/rollup-plugin-wildcard-external
16+
*/
17+
const wildcardExternalsPlugin = (peerDependencies) => ({
18+
name: 'wildcard-externals',
19+
resolveId(source, importer) {
20+
if (importer) {
21+
let matchesExternal = false;
22+
peerDependencies.forEach((peerDependency) => {
23+
if (source.includes(`/${peerDependency}/`)) {
24+
matchesExternal = true;
25+
}
26+
});
27+
28+
if (matchesExternal) {
29+
return {
30+
id: source,
31+
external: true,
32+
moduleSideEffects: true,
33+
};
34+
}
35+
}
36+
37+
return null; // other ids should be handled as usually
38+
},
39+
});
40+
41+
/**
42+
* Moves the generated TypeScript declaration files to the correct location.
43+
*
44+
* This could probably be configured in the TypeScript plugin.
45+
*/
46+
const moveTypescriptDeclarationsPlugin = (packageRoot) => ({
47+
name: 'move-ts-declarations',
48+
writeBundle: async () => {
49+
const isBridge = packageRoot.includes('src/Bridge');
50+
const globPattern = path.join('dist', '**', 'assets', 'src', '**/*.d.ts');
51+
const files = glob.sync(globPattern);
52+
53+
files.forEach((file) => {
54+
const relativePath = file;
55+
// a bit odd, but remove first 7 or 4 directories, which will leave only the relative path to the file
56+
// ex: dist/Chartjs/assets/src/controller.d.ts' => 'dist/controller.d.ts'
57+
const targetFile = relativePath.replace(relativePath.split('/').slice(1, isBridge ? 7 : 4).join('/') + '/', '');
58+
59+
if (!fs.existsSync(path.dirname(targetFile))) {
60+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
61+
}
62+
fs.renameSync(file, targetFile);
63+
});
64+
},
65+
});
66+
67+
/**
68+
* @param {String} packageRoot
69+
* @param {Array<String>} inputFiles
70+
*/
71+
function getRollupConfiguration({ packageRoot, inputFiles }) {
72+
const packagePath = path.join(packageRoot, 'package.json');
73+
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
74+
const peerDependencies = [
75+
'@hotwired/stimulus',
76+
...(packageData.peerDependencies ? Object.keys(packageData.peerDependencies) : []),
77+
];
78+
79+
inputFiles.forEach((file) => {
80+
// custom handling for StimulusBundle
81+
if (file.includes('StimulusBundle/assets/src/loader.ts')) {
82+
peerDependencies.push('./controllers.js');
83+
}
84+
85+
// React, Vue
86+
if (file.includes('assets/src/loader.ts')) {
87+
peerDependencies.push('./components.js');
88+
}
89+
});
90+
91+
const outDir = path.join(packageRoot, 'dist');
92+
93+
return {
94+
input: inputFiles,
95+
output: {
96+
dir: outDir,
97+
entryFileNames: '[name].js',
98+
format: 'esm',
99+
},
100+
external: peerDependencies,
101+
plugins: [
102+
resolve(),
103+
typescript({
104+
filterRoot: '.',
105+
tsconfig: path.join(__dirname, '..', 'tsconfig.json'),
106+
include: [
107+
'src/**/*.ts',
108+
// TODO: Remove for the next major release
109+
// "@rollup/plugin-typescript" v11.0.0 fixed an issue (https://github.com/rollup/plugins/pull/1310) that
110+
// cause a breaking change for UX React users, the dist file requires "react-dom/client" instead of "react-dom"
111+
// and it will break for users using the Symfony AssetMapper without Symfony Flex (for automatic "importmap.php" upgrade).
112+
'**/node_modules/react-dom/client.js',
113+
],
114+
compilerOptions: {
115+
outDir: outDir,
116+
declaration: true,
117+
emitDeclarationOnly: true,
118+
},
119+
}),
120+
commonjs(),
121+
wildcardExternalsPlugin(peerDependencies),
122+
moveTypescriptDeclarationsPlugin(packageRoot),
123+
],
124+
};
125+
}
126+
127+
module.exports = {
128+
getRollupConfiguration,
129+
};

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"src/*/src/Bridge/*/assets"
77
],
88
"scripts": {
9-
"build": "node bin/build_javascript.js && node bin/build_styles.js",
9+
"build": "yarn workspaces foreach -Apt run build",
1010
"test": "bin/run-vitest-all.sh",
11-
"lint": "yarn workspaces foreach --all -pt run lint",
12-
"format": "yarn workspaces foreach --all -pt run format",
13-
"check-lint": "yarn workspaces foreach --all -pt run check-lint",
14-
"check-format": "yarn workspaces foreach --all -pt run check-format"
11+
"lint": "yarn workspaces foreach -Apt run lint",
12+
"format": "yarn workspaces foreach -Apt run format",
13+
"check-lint": "yarn workspaces foreach -Apt run check-lint",
14+
"check-format": "yarn workspaces foreach -Apt run check-format"
1515
},
1616
"devDependencies": {
1717
"@babel/core": "^7.25.2",
@@ -24,7 +24,7 @@
2424
"@rollup/plugin-typescript": "^11.1.6",
2525
"@symfony/stimulus-testing": "^2.0.1",
2626
"@vitest/browser": "^2.1.1",
27-
"clean-css-cli": "^5.6.2",
27+
"clean-css": "^5.3.3",
2828
"playwright": "^1.47.0",
2929
"rollup": "^4.22.5",
3030
"tslib": "^2.6.3",

0 commit comments

Comments
 (0)