support css as part of the rspack integration

This commit is contained in:
Nacho Codoñer
2025-08-18 12:35:35 +02:00
parent 586c69515f
commit 488b80a33a
6 changed files with 139 additions and 7 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@meteorjs/rspack",
"version": "0.0.17",
"version": "0.0.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@meteorjs/rspack",
"version": "0.0.17",
"version": "0.0.18",
"license": "ISC",
"dependencies": {
"ignore-loader": "^0.1.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@meteorjs/rspack",
"version": "0.0.17",
"version": "0.0.18",
"description": "Configuration logic for using Rspack in Meteor projects",
"main": "index.js",
"type": "module",

View File

@@ -0,0 +1,53 @@
import fs from 'fs/promises';
import path from 'path';
/**
* Rspack plugin to clean and recreate build directories
* before each compilation.
*
* Options:
* - targets {string[]} : Directories to clean.
* Defaults:
* - public/_build-assets
* - public/_build-bundles
* - private/_build-assets
* - verbose {boolean} : If true, logs cleaning operations. Default: false
*/
export default class CleanBuildAssetsPlugin {
constructor(options = {}) {
const defaults = [
'public/_build-assets',
'public/_build-bundles',
'private/_build-assets',
];
this.targets = Array.isArray(options.targets)
? options.targets
: defaults;
this.verbose = options.verbose || false;
}
apply(compiler) {
compiler.hooks.beforeCompile.tapPromise(
'CleanBuildAssetsPlugin',
async () => {
for (const target of this.targets) {
const dir = path.resolve(compiler.context, target);
try {
await fs.rm(dir, { recursive: true, force: true });
await fs.mkdir(dir, { recursive: true });
if (this.verbose) {
console.log(`[CleanBuildAssetsPlugin] Cleaned: ${dir}`);
}
} catch (err) {
console.warn(
`[CleanBuildAssetsPlugin] Failed to clean ${dir}:`,
err.message
);
}
}
}
)
}
}

View File

@@ -0,0 +1,47 @@
import path from 'node:path';
import { createRequire } from 'node:module';
function loadHtmlRspackPluginFromHost(compiler) {
// Prefer the compiler's context; fall back to process.cwd()
const ctx = compiler.options?.context || compiler.context || process.cwd();
const requireFromHost = createRequire(path.join(ctx, 'package.json'));
const core = requireFromHost('@rspack/core'); // host's instance
// Rspack exports can be shaped a couple ways; be defensive
return core.HtmlRspackPlugin || core.rspack?.HtmlRspackPlugin || core.default?.HtmlRspackPlugin;
}
/**
* Rspack plugin to:
* 1. Remove the injected `client-rspack.js` script tag
* 2. Strip <!doctype> and <html>…</html> wrappers from the final HTML
*/
export default class RspackMeteorHtmlPlugin {
apply(compiler) {
const HtmlRspackPlugin = loadHtmlRspackPluginFromHost(compiler);
if (!HtmlRspackPlugin?.getCompilationHooks) {
throw new Error('Could not load HtmlRspackPlugin from host project.');
}
compiler.hooks.compilation.tap('RspackMeteorHtmlPlugin', compilation => {
const hooks = HtmlRspackPlugin.getCompilationHooks(compilation);
// remove <script src=".../client-rspack.js">
hooks.alterAssetTags.tap('RspackMeteorHtmlPlugin', data => {
data.assetTags.scripts = data.assetTags.scripts.filter(t => {
const src = t.attributes?.src || t.asset || '';
return !(t.tagName === 'script' && /(?:^|\/)client-rspack\.js$/i.test(src));
});
});
// unwrap <!doctype> and <html>…</html>
hooks.beforeEmit.tap('RspackMeteorHtmlPlugin', data => {
data.html = data.html
.replace(/<!doctype[^>]*>\s*/i, '')
.replace(/<html[^>]*>\s*/i, '')
.replace(/\s*<\/html>\s*$/i, '')
.trim();
});
});
}
}

View File

@@ -1,4 +1,4 @@
import { DefinePlugin, BannerPlugin } from '@rspack/core';
import { DefinePlugin, BannerPlugin, HtmlRspackPlugin } from '@rspack/core';
import fs from 'fs';
import { createRequire } from 'module';
import path from 'path';
@@ -8,6 +8,8 @@ import { inspect } from "node:util";
import { RequireExternalsPlugin } from './plugins/RequireExtenalsPlugin.js';
import { getMeteorAppSwcConfig } from "./lib/swc.js";
import { mergeSplitOverlap } from './lib/mergeRulesSplitOverlap.js';
import CleanBuildAssetsPlugin from "./plugins/CleanBuildAssetsPlugin.js";
import RspackMeteorHtmlPlugin from "./plugins/RspackMeteorHtmlPlugin.js";
const require = createRequire(import.meta.url);
@@ -136,6 +138,7 @@ export default function (inMeteor = {}, argv = {}) {
// Determine output points
const outputPath = Meteor.outputPath;
const outputDir = path.dirname(Meteor.outputPath || '');
const outputFilename = Meteor.outputFilename;
// Determine run point
@@ -232,8 +235,14 @@ export default function (inMeteor = {}, argv = {}) {
isDevEnvironment ? outputFilename : `../${buildContext}/${outputPath}`,
libraryTarget: 'commonjs',
publicPath: '/',
chunkFilename: `${bundlesContext}/[id].[chunkhash].js`,
chunkFilename: `${bundlesContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
assetModuleFilename: `${assetsContext}/[hash][ext][query]`,
cssFilename: `${assetsContext}/[name]${
isProd ? '.[contenthash]' : ''
}.css`,
cssChunkFilename: `${assetsContext}/[id]${
isProd ? '.[contenthash]' : ''
}.css`,
},
optimization: {
usedExports: true,
@@ -256,6 +265,9 @@ export default function (inMeteor = {}, argv = {}) {
resolve: { extensions, alias },
externals,
plugins: [
...(isProd ? [
new CleanBuildAssetsPlugin({ verbose: Meteor.isDebug || Meteor.isVerbose }),
] : []),
...[
...(isReactEnabled && reactRefreshModule && isDevEnvironment
? [new reactRefreshModule()]
@@ -273,6 +285,24 @@ export default function (inMeteor = {}, argv = {}) {
banner: bannerOutput,
entryOnly: true,
}),
new HtmlRspackPlugin({
inject: false,
cache: true,
filename: `../${buildContext}/${outputDir}/index.html`,
templateContent: `
<head>
<% for tag in htmlRspackPlugin.tags.headTags { %>
<%= toHtml(tag) %>
<% } %>
</head>
<body>
<% for tag in htmlRspackPlugin.tags.bodyTags { %>
<%= toHtml(tag) %>
<% } %>
</body>
`,
}),
new RspackMeteorHtmlPlugin({}),
],
watchOptions,
devtool: isDevEnvironment || isTest ? 'source-map' : 'hidden-source-map',
@@ -284,10 +314,12 @@ export default function (inMeteor = {}, argv = {}) {
...(Meteor.isBlazeEnabled && { hot: false }),
port: Meteor.devServerPort || 8080,
devMiddleware: {
writeToDisk: false,
writeToDisk: (filePath) =>
/\.(html)$/.test(filePath) && !filePath.includes('.hot-update.'),
},
},
}),
experiments: { css: true },
};
const serverNameConfig = `[${isTest && 'test-' || ''}${isTestModule && 'module' || 'server'}-rspack]`;

View File

@@ -5,7 +5,7 @@
export const DEFAULT_RSPACK_VERSION = '1.4.8';
export const DEFAULT_METEOR_RSPACK_VERSION = '0.0.17';
export const DEFAULT_METEOR_RSPACK_VERSION = '0.0.18';
export const DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION = '1.4.3';