UNPKG

repacked

Version:

To initialize a new project with `repacked`, you can use the following command:

814 lines (779 loc) 22.9 kB
#!/usr/bin/env node // src/cli.ts import yargs from "yargs"; import { hideBin } from "yargs/helpers"; // src/features/client/build.ts import { rspack as rspack2 } from "@rspack/core"; // src/features/client/getClientRspackConfig.ts import "@rspack/dev-server"; import { HotModuleReplacementPlugin, HtmlRspackPlugin as HtmlRspackPlugin2, rspack } from "@rspack/core"; import ReactRefreshRspackPlugin from "@rspack/plugin-react-refresh"; import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack"; // src/utils/cwd.ts import path from "path"; var cwd = (...paths) => { return path.join(process.cwd(), ...paths); }; var cwd_default = cwd; // src/features/client/plugins/envVariables.ts import { DefinePlugin } from "@rspack/core"; var getEnvValues = (filterCallback) => { const envs = process.env || {}; const filteredEnvs = {}; Object.entries(envs).forEach(([key, value]) => { if (filterCallback(key, value)) { filteredEnvs[key] = value; } }); return filteredEnvs; }; var EnvVariablesPlugin = (filterCallback) => { return new DefinePlugin({ "process.env": JSON.stringify({ ...getEnvValues(filterCallback) }) }); }; // src/features/client/plugins/htmlMFWebpackPlugin.ts import { HtmlRspackPlugin } from "@rspack/core"; var HtmlMFRspackPlugin = class { constructor(entryFile = "remoteEntry.js") { this.entryFile = entryFile; } apply(compiler) { compiler.hooks.compilation.tap( "HtmlMFRspackPlugin", (compilation) => { HtmlRspackPlugin.getCompilationHooks( compilation ).alterAssetTags.tapAsync("HtmlMFRspackPlugin", (data, cb) => { const entryFile = `${data.publicPath.toLowerCase() === "auto" ? "" : data.publicPath}${this.entryFile}`; data.assetTags.scripts = data.assetTags.scripts.filter( (script) => script.attributes.src !== entryFile ); cb(null, data); }); } ); } }; var htmlMFWebpackPlugin_default = HtmlMFRspackPlugin; // src/features/rspack/getRspackConfig.ts import "@rspack/dev-server"; // src/features/swc/swcOptions.ts var getSwcOptions = (options) => { return { jsc: { parser: { syntax: "typescript", tsx: options.tsx }, externalHelpers: false, transform: { react: { runtime: "automatic", development: !options.isProduction, refresh: !options.isProduction && !options.isServer } } }, env: { targets: "Chrome >= 48" } }; }; // src/features/rspack/getRspackConfig.ts var getRspackConfig = async (mode, appConfig, options) => { const isDevelopment = mode === "development"; const isServer = options?.target === "server"; const configOverride = options?.override ?? ((config) => config); const outputDirectory = cwd_default(appConfig.output.dir); const rspackConfig = { mode, watch: options?.watch, watchOptions: options?.watch ? { ignored: /node_modules/, poll: 1e3, aggregateTimeout: 300 } : void 0, cache: false, entry: cwd_default(appConfig.client.entry), devtool: "source-map", output: { uniqueName: appConfig.appName, publicPath: appConfig.client.publicPath, path: outputDirectory, filename: "js/[name].[fullhash].js", clean: typeof options?.clean === "boolean" ? options.clean : true }, plugins: [], module: { rules: [ { test: /\.(j|t)s?$/, use: { loader: "builtin:swc-loader", options: getSwcOptions({ tsx: false, isProduction: !isDevelopment, isServer }) }, exclude: /node_modules/ }, { test: /\.(js|ts)x?$/, use: { loader: "builtin:swc-loader", options: getSwcOptions({ tsx: true, isProduction: !isDevelopment, isServer }) }, exclude: /node_modules/ }, { test: /\.css$/, exclude: /node_modules/, use: ["style-loader", "css-loader"] }, { test: /\.(png|svg|jpg|gif)$/, exclude: /node_modules/, use: [ isServer ? { loader: "file-loader", options: { emitFile: false, publicPath: appConfig.client.publicPath } } : "file-loader" ] } ] }, resolve: { extensions: ["*", ".jsx", ".tsx", ".ts", ".js"] }, performance: { hints: false } }; appConfig.plugins.forEach((plugin) => { const customPlugin = plugin({ target: options.target, appConfig }); rspackConfig.plugins?.push(customPlugin); customPlugin.updateConfig?.(rspackConfig); }); return appConfig.rspack( configOverride(rspackConfig), options?.target ?? "client" ); }; var getRspackConfig_default = getRspackConfig; // src/features/client/plugins/manifestPlugin.ts import { sources } from "@rspack/core"; var defaultOptions = { fileName: "client-assets.json" }; var ManifestPlugin = class _ManifestPlugin { constructor(options) { this.options = { ...defaultOptions, ...options }; } apply(compiler) { compiler.hooks.emit.tapAsync( _ManifestPlugin.name, (compilation, callback) => { const manifest = {}; for (const [entryName, entrypoint] of compilation.entrypoints) { const chunks = entrypoint.chunks.filter( (chunk) => chunk.canBeInitial?.() ); const jsFiles = /* @__PURE__ */ new Set(); const cssFiles = /* @__PURE__ */ new Set(); for (const chunk of chunks) { for (const file of chunk.files) { if (file.endsWith(".hot-update.js")) { continue; } if (file.endsWith(".js")) { jsFiles.add(file); } else if (file.endsWith(".css")) { cssFiles.add(file); } } } manifest[entryName] = { js: Array.from(jsFiles), css: Array.from(cssFiles) }; } const json = JSON.stringify(manifest, null, 2); compilation.emitAsset( this.options.fileName, new sources.RawSource(json) ); callback(); } ); } }; // src/constants.ts var CLIENT_MANIFEST_FILENAME = "client-assets.json"; // src/features/client/getClientRspackConfig.ts var getClientRspackConfig = async (mode, appConfig, options) => { const isDevelopment = mode === "development"; const outputDirectory = cwd_default(appConfig.output.dir); let entry = []; if (appConfig.server.enabled && isDevelopment) { entry = [ "webpack-hot-middleware/client?reload=true", cwd_default(appConfig.client.entry) ]; } else { entry = cwd_default(appConfig.client.entry); } const plugins = []; plugins.push( new HtmlRspackPlugin2({ template: cwd_default(appConfig.client.template) }) ); plugins.push(EnvVariablesPlugin(appConfig.client.envFilter)); isDevelopment && plugins.push(new HotModuleReplacementPlugin()); isDevelopment && plugins.push(new ReactRefreshRspackPlugin({ library: appConfig.appName })); plugins.push( new rspack.CopyRspackPlugin({ patterns: [ { from: cwd_default(appConfig.client.assetsDir), to: outputDirectory } ] }) ); plugins.push(new ManifestPlugin({ fileName: CLIENT_MANIFEST_FILENAME })); if (appConfig.moduleFederation) { plugins.push(new ModuleFederationPlugin(appConfig.moduleFederation)); plugins.push(new htmlMFWebpackPlugin_default(appConfig.moduleFederation.filename)); } return await getRspackConfig_default(mode, appConfig, { ...options ?? {}, target: "client", override: (config) => { config.entry = entry; config.plugins = [...config.plugins, ...plugins]; config.devServer = { hot: true, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization" }, ...appConfig.development }; return config; } }); }; var getClientRspackConfig_default = getClientRspackConfig; // src/features/rspack/utils.ts var filterKnownWarnings = (warnings) => { const knownPackages = ["yargs", "express", "repacked"].map( (pkg) => `node_modules/${pkg}` ); const containsAny = (str, values) => { return values.some((value) => str.includes(value)); }; return warnings.filter((warning) => { if (warning.message.includes("Critical dependency") && (!warning.moduleName || containsAny(warning.moduleName ?? "", knownPackages))) { return false; } return true; }); }; var logRspackErrors = (err, stats) => { if (err) { console.error(err.stack || err); if (err.details) { console.error(err.details); } process.exit(1); } const statsData = stats?.toJson(); if (stats?.hasErrors()) { console.error(statsData?.errors); process.exit(1); } if (stats?.hasWarnings()) { const filteredWarnings = filterKnownWarnings(statsData?.warnings ?? []); if (filteredWarnings.length > 0) { console.warn(filteredWarnings); } } }; // src/features/client/build.ts var buildClient = async (mode, appConfig) => { const rspackConfig = await getClientRspackConfig_default(mode, appConfig); rspack2(rspackConfig, logRspackErrors); }; // src/features/server/build.ts import { rspack as rspack3 } from "@rspack/core"; // src/features/server/getServerRspackConfig.ts import { LicenseWebpackPlugin } from "license-webpack-plugin"; // src/features/server/plugins/hotReloadServer.ts import cluster from "node:cluster"; import path2 from "path"; var HotReloadServer = class _HotReloadServer { constructor(appConfig) { this.appConfig = appConfig; this.callback = () => { }; this.workers = []; cluster.setupPrimary({ exec: path2.resolve(cwd_default(appConfig.output.dir, "index.cjs")), args: [`--port=${appConfig.development.port + 1}`] }); cluster.on("online", (worker) => { this.workers.push(worker); this.callback(); }); } apply(compiler) { const pluginName = _HotReloadServer.name; compiler.hooks.afterEmit.tapAsync(pluginName, (compilation, callback) => { this.callback = callback; this.workers.forEach((worker) => { try { worker.process.kill("SIGTERM"); } catch (e) { console.warn(`Unable to kill worker ${worker.process.pid}`); } }); this.workers = []; cluster.fork(); }); } }; // src/features/server/getServerRspackConfig.ts import path3 from "path"; import { DefinePlugin as DefinePlugin2 } from "@rspack/core"; // src/utils/dirname.ts import { fileURLToPath } from "url"; import { dirname } from "path"; var __filename = fileURLToPath(import.meta.url); var __dirname = dirname(__filename); // src/features/server/getServerRspackConfig.ts var getServerRspackConfig = async (mode, appConfig, options) => { const runtimeEnv = mode === "production" ? "prod" : "dev"; return await getRspackConfig_default(mode, appConfig, { ...options ?? {}, target: "server", override: (config) => { config.module?.rules?.push({ test: path3.join( __dirname, "/features/server/runtimes/", `runtime.${runtimeEnv}.js` ), use: [ { loader: path3.resolve( __dirname, "./features/server/loaders/loadServer.js" ), options: { entry: cwd_default(appConfig.server.entry) } } ] }); config.plugins?.push( new DefinePlugin2({ "process.env.__INTERNAL_REPACKED_SERVER_CONFIG": { client: { enabled: appConfig.client.enabled }, development: { port: appConfig.development.port } } }) ); if (mode === "production") { config.plugins?.push( new LicenseWebpackPlugin({ outputFilename: "LICENSE", perChunkOutput: false, addBanner: true, stats: { warnings: false } }) ); } if (mode === "development") { config.plugins?.push(new HotReloadServer(appConfig)); } config.target = "node"; config.entry = { index: path3.join( __dirname, "/features/server/runtimes/", `runtime.${runtimeEnv}.js` ) }; config.output.libraryTarget = "commonjs2"; config.output.filename = "[name].cjs"; config.output.publicPath = appConfig.client.publicPath; config.optimization = { sideEffects: true }; delete config.devServer; return config; } }); }; var getServerRspackConfig_default = getServerRspackConfig; // src/features/server/build.ts var buildServer = async (mode, appConfig) => { const rspackConfig = await getServerRspackConfig_default(mode, appConfig, { clean: false }); return new Promise((resolve, reject) => { rspack3(rspackConfig, (err, stats) => { logRspackErrors(err, stats); if (err) { reject(err); return; } if (stats?.hasErrors()) { const info = stats.toJson(); reject( new Error(`Build failed with errors: ${info.errors?.join("\n")}`) ); return; } resolve(true); }); }); }; // src/features/build/index.ts import path4 from "path"; // src/features/build/utils/removeFolder.ts import { promises as fs } from "fs"; var removeFolder = async (folder) => { try { await fs.rm(folder, { recursive: true, force: true }); console.log(`Folder "${folder}" removed successfully.`); } catch (error) { console.error(`Error removing folder: ${error.message}`); throw error; } }; var removeFolder_default = removeFolder; // src/features/build/index.ts var build = async (mode, appConfig) => { const serverEnabled = appConfig.server.enabled; const clientEnabled = appConfig.client.enabled; const clientOutputPath = serverEnabled ? path4.join(appConfig.output.dir, "client") : appConfig.output.dir; await removeFolder_default(cwd_default(appConfig.output.dir)); clientEnabled && await buildClient(mode, { ...appConfig, output: { ...appConfig.output, dir: clientOutputPath } }); if (serverEnabled) { await buildServer(mode, appConfig); } }; var build_default = build; // src/cli.ts import "dotenv/config"; // src/features/test/jestConfig.ts import path5 from "path"; var getJestConfig = () => { return { rootDir: cwd_default(""), roots: ["<rootDir>/src"], collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"], setupFilesAfterEnv: ["@testing-library/jest-dom"], testMatch: [ "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}" ], testEnvironment: "jsdom", moduleFileExtensions: ["ts", "tsx", "js", "jsx"], transform: { "^.+\\.(js|ts|mjs|cjs)$": "@swc/jest", "^.+\\.(jsx|tsx)$": [ "@swc/jest", getSwcOptions({ isProduction: true, tsx: true }) ], "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": path5.resolve( __dirname, "./features/test/transformers/fileTransformer.js" ), "^.+\\.css$": path5.resolve( __dirname, "./features/test/transformers/cssTransformer.js" ) } }; }; var getJestConfigAsJSON = (override) => { return JSON.stringify(override(getJestConfig())); }; // src/features/test/test.ts import jest from "jest"; var runTest = async (argv, appConfig) => { const jestConfig = getJestConfigAsJSON(appConfig.jest); argv.push("--config", jestConfig); jest.run(argv); }; // src/features/serve/index.ts import { rspack as rspack4 } from "@rspack/core"; import { RspackDevServer } from "@rspack/dev-server"; // src/features/server/server.ts import express from "express"; var expressServer = () => { const app = express(); return app; }; // src/features/serve/index.ts import history from "connect-history-api-fallback"; import rspackHotMiddleware from "webpack-hot-middleware"; import rspackDevMiddleware from "webpack-dev-middleware"; import { createProxyMiddleware } from "http-proxy-middleware"; // src/features/serve/utils/copyHeaders.ts function copyHeaders(proxyRes, res) { if (proxyRes.statusCode) { res.statusCode = proxyRes.statusCode; } if (proxyRes.statusMessage) { res.statusMessage = proxyRes.statusMessage; } if (typeof res.setHeader === "function") { let keys = Object.keys(proxyRes.headers); keys = keys.filter( (key) => !["content-encoding", "transfer-encoding"].includes(key) ); keys.forEach((key) => { let value = proxyRes.headers[key]; if (value === void 0) { return; } if (key === "set-cookie") { value = Array.isArray(value) ? value : [value]; value = value.map((x) => x.replace(/Domain=[^;]+?/i, "")); } res.setHeader(key, value); }); } else { res.headers = proxyRes.headers; } } // src/features/serve/index.ts var serveClientOnly = async (mode, appConfig) => { const rspackConfig = await getClientRspackConfig_default(mode, appConfig); rspackConfig.devServer.historyApiFallback = true; const compiler = rspack4(rspackConfig); const server = new RspackDevServer(rspackConfig.devServer ?? {}, compiler); const runServer = async () => { console.log("Starting server..."); await server.start(); }; runServer(); }; var serveServer = async (mode, appConfig) => { const serverRspackConfig = await getServerRspackConfig_default(mode, appConfig, { watch: true }); rspack4(serverRspackConfig, logRspackErrors); const clientRspackConfig = await getClientRspackConfig_default(mode, appConfig, {}); clientRspackConfig.output.publicPath = appConfig.client.publicPath; const clientCompiler = rspack4(clientRspackConfig); const port = appConfig.development.port || 3e3; const clientEnabled = appConfig.client.enabled; const app = expressServer(); const nextWeakMap = /* @__PURE__ */ new WeakMap(); const proxy = createProxyMiddleware({ target: `http://localhost:${port + 1}`, changeOrigin: true, selfHandleResponse: true, on: { proxyRes: (proxyRes, req, res) => { if (!clientEnabled) { proxyRes.pipe(res); return; } if (proxyRes.headers?.["x-dev-repacked-route-status"] === "unhandled") { const next = nextWeakMap.get(req); return next(); } else { copyHeaders(proxyRes, res); proxyRes.pipe(res); } } } }); app.use((req, res, next) => { if (req.headers["x-dev-repacked-client-only"]) { return next(); } nextWeakMap.set(req, next); proxy(req, res, next); }); if (clientEnabled) { app.use(history()); const devMiddleware = rspackDevMiddleware( //@todo: fix once rspack dev middleware released clientCompiler, { publicPath: clientRspackConfig.output?.publicPath } ); app.use(devMiddleware); app.use(rspackHotMiddleware(clientCompiler)); } app.listen(port, () => { console.log(`Server is running at http://localhost:${port}`); }); }; var serve = async (mode, appConfig) => { if (appConfig.server.enabled) { serveServer(mode, appConfig); } else { serveClientOnly(mode, appConfig); } }; var serve_default = serve; // src/features/app-config/getAppConfig.ts import path6 from "path"; var defaultAppConfig = { appName: "app_name", output: { dir: "./dist" }, client: { enabled: true, template: "./src/index.html", entry: "./src/index.tsx", publicPath: "/", assetsDir: "./public", envFilter: (key) => key.startsWith("PUBLIC_") }, server: { enabled: false, entry: "./src/server.ts" }, development: { port: 3e3, open: true }, rspack: (config) => config, jest: (config) => config, plugins: [] }; var resolveConfig = async (extension) => { try { const configPath = path6.resolve(cwd_default(`config.repacked.${extension}`)); const configModule = await import(configPath); return configModule.default ?? configModule; } catch (e) { return null; } }; var resolveConfigByExtensions = async () => { const mjs = await resolveConfig("mjs"); if (mjs) { return mjs; } const cjs = await resolveConfig("js"); if (cjs) { return cjs; } throw new Error("unable to resolve config file"); }; var getAppConfig = async () => { try { const config = await resolveConfigByExtensions(); const finalConfig = { ...defaultAppConfig, ...config, output: { ...defaultAppConfig.output, ...config.output ?? {} }, client: { ...defaultAppConfig.client, ...config.client || {} }, server: { ...defaultAppConfig.server, ...config.server || {} }, development: { ...defaultAppConfig.development, ...config.development || {} }, rspack: config.rspack ?? config.webpack ?? defaultAppConfig.rspack, plugins: config.plugins ?? defaultAppConfig.plugins }; return finalConfig; } catch (e) { console.warn("loading default config", e); return defaultAppConfig; } }; var getAppConfig_default = getAppConfig; // src/cli.ts var exec = () => yargs(hideBin(process.argv)).command( "serve", "start the server", (yargs2) => { return yargs2.option("mode", { describe: "serve mode", default: "development", choices: ["production", "development"] }); }, async (argv) => { const appConfig = await getAppConfig_default(); serve_default(argv.mode, appConfig); } ).command( "build", "build the app", (yargs2) => { return yargs2.option("mode", { describe: "build mode", default: "production", choices: ["production", "development"] }); }, async (argv) => { const appConfig = await getAppConfig_default(); build_default(argv.mode, appConfig); } ).command( "test", "test the app", (yargs2) => { return yargs2; }, async (argv) => { const jestArgv = process.argv.slice(2); const appConfig = await getAppConfig_default(); runTest(jestArgv, appConfig); } ).strictCommands().demandCommand(1).parse(); exec();