En el desarrollo frontend moderno, las herramientas de compilación simplifican muchas tareas. La construcción multiplataforma de aplicaciones de página única es una de las aplicaciones más extendidas. Webpack, con sus loaders y plugins, ofrece a los desarrolladores un gran espacio de maniobra para manipular el proceso de compilación. Al operar sobre artefactos intermedios, podemos implementar fácilmente la compilación multiplataforma. Esta es una filosofía, no un método intrínsecamente ligado a Webpack; herramientas como Rollup, Vite y Rspack también pueden lograrlo.
Descripción
Primero, hablemos de la compilación multiplataforma. La idea es simple: dentro del mismo proyecto, podemos usar un conjunto de código para generar código para múltiples plataformas. Escenarios como la compatibilidad multiplataforma de mini-programas, la compatibilidad multiplataforma de extensiones de navegador, o la gestión de recursos para aplicaciones en diferentes regiones geográficas, comparten la característica de que el código central es coherente. Las diferencias radican principalmente en la invocación de interfaces o en la configuración de implementaciones, y esta porción de código divergente suele ser mínima. En tales casos, utilizar una herramienta de compilación para la compilación multiplataforma es muy apropiado.
Es crucial notar que manejamos la redundancia de código causada por la adaptabilidad multiplataforma durante el proceso de compilación. El código de compatibilidad para diferentes versiones de navegadores, por ejemplo, requiere una evaluación dinámica en tiempo de ejecución y no puede tratarse como redundancia, ya que no es factible distribuir un paquete de código separado para cada versión. Por lo tanto, esta situación no se incluye en nuestro ámbito de discusión sobre la construcción multiplataforma. De hecho, podemos entenderlo como que, dado que podemos determinar la plataforma del código de manera absoluta y distribuir paquetes de aplicaciones de forma independiente, podemos separar el código durante la compilación. El código de compatibilidad de plataforma no desaparece, sino que se traslada, esencialmente moviendo la lógica de determinación de plataforma de tiempo de ejecución a tiempo de compilación, logrando así un mejor rendimiento y un tamaño de paquete más pequeño.
Para implementar la construcción multiplataforma, necesitamos aprovechar las capacidades de las herramientas de compilación. Típicamente, las herramientas de compilación tienen la capacidad de eliminar el DEAD CODE durante la compresión de recursos de código. Incluso si una herramienta no tiene esta funcionalidad predeterminada, a menudo existen plugins que la combinan. Podemos aprovechar este método para lograr la construcción multiplataforma. Específicamente, podemos usar declaraciones if junto con expresiones de código para asegurar que las condiciones booleanas sean absolutas durante la compilación, permitiendo que la herramienta de compilación elimine el código que no cumple las condiciones como DEAD CODE. Además, dado que estamos eliminando DEAD CODE, en algunos escenarios, como lógicas diferentes para SDKs internos y externos o dependencias de paquetes, podemos optimizar el tamaño del paquete mediante TreeShaking de la herramienta de compilación.
if ("chromium" === "chromium") {
// xxx
}
if ("gecko" === "chromium") {
// xxx
}
process.env
Durante el desarrollo, especialmente al introducir paquetes de terceros de npm, es posible que nos encontremos con el error ReferenceError: process is not defined después de la compilación. Este es un error clásico. Por lo general, ocurre al aplicar código de Node.js en un entorno de navegador. Más allá de esto, process.env también es necesario en escenarios de compilación frontend. Por ejemplo, en el archivo de entrada react/index.js de React, podemos ver el siguiente código:
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
Esto ocurre en tiempo de compilación, ya que todavía se ejecuta en un entorno Node. Al diferenciar entre diferentes variables de entorno, se empaquetan diferentes artefactos, lo que permite distinguir entre el código de producción y el de desarrollo, proporcionando así funcionalidades y advertencias relacionadas con el entorno de desarrollo. De manera similar, podemos usar este enfoque como condición para la construcción multiplataforma, utilizando process.env para determinar la plataforma actual y eliminar el código que no cumple las condiciones durante la compilación. Compilar multiplataforma de esta manera, similar a React, es factible. Sin embargo, parece ser una forma de gestión de módulos commonjs. Los módulos ES son declaraciones estáticas; es decir, las declaraciones de importación y exportación deben usarse en el ámbito superior del módulo y no en bloques de código como sentencias condicionales o bucles. Por lo tanto, este código a menudo requiere mantenimiento manual o generación automática por herramientas.
En el contexto de las declaraciones estáticas de ES Module, necesitamos herramientas de compilación para la compilación multiplataforma. Volviendo al problema de process is not defined, además de las dos situaciones mencionadas, hay otra circunstancia común: la variable process existe en el código, pero al ejecutarse en el navegador en tiempo de ejecución, se detecta que no existe y se lanza una excepción. Al principio, me sorprendía por qué esta variable de Node aparecía en el navegador. Para resolver esto, podría haber declarado la variable globalmente. Ahora veo que esa podría haber sido una aplicación errónea. De hecho, deberíamos usar las herramientas de compilación del navegador para gestionar la configuración del entorno actual. Por ejemplo, supongamos que nuestra variable de entorno process.env.NODE_ENV es development y nuestro código fuente es el siguiente. Después de procesarlo con una herramienta de compilación, la condición de esta comparación se convierte en "development" === "development", que siempre es verdadera. La parte else se convertiría en DEAD CODE y se eliminaría. Por lo tanto, la url que obtenemos al final es xxx. De manera similar, cuando es production, la url se convertiría en xxxxxx.
let url = "xxx";
if (process.env.NODE_ENV === "development") {
console.log("Development Env");
} else {
url = "xxxxxx";
}
export const URL = url;
// Después del procesamiento
let url = "xxx";
if ("development" === "development") {
console.log("Development Env");
}// else {
// url = "xxxxxx";
// }
export const URL = url;
Este es un método de procesamiento muy general. Se utiliza la especificación de variables de entorno para diferenciar entornos, permitiendo la eliminación de código innecesario durante la compilación. Por ejemplo, el framework Create React App tiene configuraciones relacionadas con custom-environment-variables, donde las variables de entorno deben comenzar con REACT_APP_ y la variable de entorno NODE_ENV se inyecta automáticamente. Sin embargo, es importante tener en cuenta que no debemos usar nombres de variables de entorno como claves privadas que comiencen con REACT_APP_, ya que esto podría provocar fugas de claves si se utilizen en el código fuente compilado frontend. Esta es la razón por la que Create React App exige que las variables de entorno que comienzan con REACT_APP_ sean las que se inyecten.
Esta funcionalidad se parece mucho al reemplazo de cadenas. Webpack proporciona webpack.DefinePlugin, que está listo para usar, para lograr esto (https://webpack.js.org/plugins/define-plugin/). Este plugin puede reemplazar variables especificadas con valores especificados durante la compilación, permitiendo el comportamiento diferente que necesitamos para la compilación multiplataforma. Podemos configurarlo directamente en el archivo de configuración de Webpack. Además, usar ps -e o systemctl status para ver el pid del proceso y combinarlo con cat /proc/${pid}/environ | tr '\0' '\n' para leer las variables de entorno de un programa en ejecución es una buena manera.
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.PLATFORM": JSON.stringify(process.env.PLATFORM),
});
Hablando de esto, no podemos dejar de mencionar la opción de configuración sideEffects en package.json. sideEffects se traduce comúnmente como "efectos secundarios". En ES Module, los módulos declarados en el nivel superior son completamente estáticos, lo que significa que la estructura de dependencias de todo el módulo se puede determinar de manera inequívoca en tiempo de compilación. Por lo tanto, lograr TreeShaking mediante dependencias determinadas es relativamente sencillo. Sin embargo, los métodos de importación dinámica como require e import() no permiten determinar la estructura de dependencias de forma estática, por lo que los módulos con referencias dinámicas generalmente no son fáciles de someter a TreeShaking. Supongamos que implementamos el módulo ES A y este hace referencia al módulo B. Si en el módulo B solo se utiliza una parte de una función implementada y la otra parte no se utiliza en todo el proyecto, entonces esta parte del código será eliminada después del análisis estático.
Lo descrito anteriormente es una situación bastante convencional. De hecho, combinado con nuestro process.env, podemos aprovechar aún más esta capacidad. Diferentes plataformas pueden encapsular diferentes módulos a través de variables de entorno. Durante la compilación, dado que en realidad solo se hace referencia pero no se llama, todo el módulo puede someterse a TreeShaking. Supongamos que tenemos tres módulos A -> B -> C. Si se puede determinar en A que B no se utiliza, lo que implica que B es un módulo sin efectos secundarios, entonces al interrumpir la referencia a B, se puede ahorrar el tamaño de los módulos B y C en el paquete. De hecho, la profundidad de las referencias a nuestros módulos puede ser considerable, formando una estructura jerárquica de árbol N-aria. Si se puede interrumpir en el medio, el tamaño se puede optimizar en gran medida.
Volviendo a la configuración de sideEffects, supongamos que nuestro módulo A hace referencia al módulo B, pero en realidad no hay ninguna llamada a funciones del módulo B en A, solo una referencia simple. El módulo B implementa código con efectos secundarios de inicialización, por ejemplo, secuestrando directamente una función de Node.prototype en el módulo B. Tenga en cuenta que este secuestro no está encapsulado en una función, sino que se ejecuta directamente en el módulo. En este caso, por defecto, es decir, cuando package.json no está configurado, sideEffects es true, lo que implica que todos los módulos tienen efectos secundarios. El código del módulo B se ejecutará efectivamente. Si se marca sideEffects como false, este código no se ejecutará. Hay otra situación: al escribir TS, generalmente no usamos la sintaxis import type xxx from "xxx";. En este caso, cuando solo hacemos referencia a tipos, los efectos secundarios innecesarios aún se ejecutarán. Por lo tanto, sideEffects es muy necesario aquí. Por supuesto, también podemos usar Lint para procesar automáticamente las declaraciones import type cuando solo se hace referencia a tipos.
De hecho, configurar sideEffects directamente como false generalmente no satisface nuestras necesidades. A veces, hacemos referencia directamente a css, similar a import "./index.css". Dado que no hay llamadas a funciones reales, este CSS también será eliminado por TreeShaking. Además, en el entorno de desarrollo, Webpack generalmente no habilita TreeShaking por defecto, por lo que es necesario configurarlo. Por lo tanto, en muchos paquetes de npm, podemos ver la siguiente configuración, que generalmente indica explícitamente módulos con efectos secundarios para evitar eliminaciones accidentales de módulos.
"sideEffects": [
"dist/**/*",
"*.scss",
"*.less",
"*.css",
"**/styles/**"
],
__DEV__
Al leer el código fuente de React y Vue, a menudo podemos ver la variable __DEV__. Si observamos con atención, podemos descubrir que, aunque es una variable, no está declarada en el archivo actual ni importada de otro módulo. Las declaraciones en global.d.ts no cuentan, ya que no se inyectan en tiempo de ejecución. En realidad, esta variable, al igual que process.env.NODE_ENV, se inyecta en tiempo de compilación y cumple una función similar. Sin embargo, como su nombre indica, esta varible se centra más en las diferencias de comportamiento entre la compilación de desarrollo y la de producción.
En realidad, esta forma es una forma de abordar otro escenario. process.env es un escenario relativamente general y una forma de definición de compilación que la mayoría de la gente puede entender. __DEV__ se parece más a una variable personalizada interna, por lo que este método es más adecuado para uso interno. Es decir, si el comportamiento asociado con esta variable se crea internamente durante nuestro proceso de desarrollo y compilación, normalmente durante el desarrollo de paquetes de npm, entonces se recomienda usar variables de entorno similares a __DEV__, ya que normalmente predefinimos los valores relevantes durante la compilación y no es necesario leerlos de las variables de entorno. Además, después de la compilación, el código relevante se eliminará y no provocará comportamientos adicionales. Si durante el proceso de compilación se requieren variables de entorno que el usuario pueda personalizar, entonces se recomienda usar process.env. Esta es una forma de definición más universalmente aceptada, y dado que se puede leer el contenido a través de variables de entorno, es más conveniente para el uso del usuario.
Anteriormente, explicamos su uso en Webpack. Dado que utiliza el mismo método, solo simplifica la configuración. Aquí, tenemos una configuración similar. ¿Han notado un detalle? Usamos JSON.stringify para procesar el valor de la variable de entorno. Esto es bastante interesante. Durante mis prácticas, me preguntaba cuál era el propósito de JSON.stringify. Si ya es una cadena, ¿por qué volver a aplicarle stringify? En realidad, es simple. Por ejemplo, la cadena "production", después de aplicarle stringify, se convierte en '"production"' o se representa como "\"production\"", similar a envolver la cadena una vez más. Supongamos que nuestro código es el siguiente:
if (process.env.NODE_ENV === "development") {
// xxx
}
Aquí está el punto clave. Mencionamos antes que esta forma de definir variables de entorno es similar al modo de reemplazo de cadenas. Y en la sintaxis básica de JS, si pasamos una variable como cadena, se convertirá en un literal de cadena en la salida. Por ejemplo, si ejecutamos console.log("production"), la salida es production, mientras que si ejecutamos console.log("\"production\""), la salida es "production". La respuesta es obvia. Si no aplicamos JSON.stringify, la salida en el código fuente impreso será directamente production en lugar de "production", lo que provocará un error durante la compilación porque no hemos definido la variable production.
console.log("production"); // production
console.log('"production"'); // "production"
console.log("\"production\""); // "production"
// "production" después de la compilación
if (production === "development") {
// xxx
}
// "\"production\"" después de la compilación
if ("production" === "development") {
// xxx
}
Las herramientas de compilación modernas suelen tener soluciones relevantes. Los frameworks de aplicaciones basados en Webpack también pueden definir directamente la configuración subyacente de Webpack para inyectar variables de entorno. Algunas formas comunes de configurar herramientas de compilación son las siguientes:
// webpack
new webpack.DefinePlugin({
"__DEV__": JSON.stringify(process.env.NODE_ENV === "development"),
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.PLATFORM": JSON.stringify(process.env.PLATFORM),
});
// vite
export default defineConfig({
define: {
"__DEV__": JSON.stringify(process.env.NODE_ENV === "development"),
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.PLATFORM": JSON.stringify(process.env.PLATFORM),
},
});
// rollup
import replace from "@rollup/plugin-replace";
export default {
plugins: [
replace({
values: {
"__DEV__": JSON.stringify(process.env.NODE_ENV === "development"),
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.PLATFORM": JSON.stringify(process.env.PLATFORM),
},
preventAssignment: true
}),
],
};
// rspack
module.exports = {
builtins: {
define: {
"__DEV__": JSON.stringify(process.env.NODE_ENV === "development"),
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.PLATFORM": JSON.stringify(process.env.PLATFORM),
},
}
}
if-def
Al manejar problemas de compilación multiplataforma, los métodos que más uso son process.env y __DEV__. Sin embargo, después de usarlos mucho, me di cuenta de que en este tipo de compilación condicional, el uso extensivo de process.env.PLATFORM === xxx puede llevar fácilmente a anidaciones profundas, lo que empeora la legibilidad. Después de todo, nuestras Promise se crearon para resolver el infierno de anidación de callbacks asíncronos. Si introducimos anidación nuevamente debido a la necesidad de compilación multiplataforma, no se siente como una buena solución.
En C/C++, existe un preprocesador muy interesante, el C Preprocessor. No es parte del compilador, sino un paso separado en el proceso de compilación. En pocas palabras, el C Preprocessor actúa como una herramienta de reemplazo de texto. Por ejemplo, las macros sin parámetros son reemplazos de texto sin procesar, lo que permite al compilador realizar el preprocesamiento necesario antes de la compilación real. Directivas como #include, #define, #ifdef, etc., pertenecen al preprocesador del C Preprocessor. Aquí, nos centraremos principalmente en la parte de compilación condicional, es decir, las directivas de compilación condicional como #if, #endif, #ifdef, #ifndef, etc.
#if VERBOSE >= 2
print("trace message");
#endif
#ifdef __unix__ /* __unix__ usually defined by compilers targeting Unix systems */
# include <unistd.h>
#elif defined _WIN32 /* _WIN32 usually defined by compilers targeting 32 or 64 bit Windows systems */
# include <windows.h>
#endif
Podemos implementar un enfoque similar utilizando herramientas de compilación. Primero, el C Preprocessor es una herramienta de preprocesamiento y no participa en el comportamiento de compilación real. ¿No se parece esto a un loader en Webpack? El reemplazo directo de texto sin procesar también es completamente factible dentro de un loader. Y para #ifdef, #endif, podemos lograrlo a través de comentarios. Esto puede evitar problemas de anidamiento profundo. La lógica de reemplazo de cadenas se puede modificar directamente para procesarla. Por ejemplo, el código que no cumple las condiciones de plataforma se puede eliminar, y el código que cumple las condiciones se puede conservar, logrando así un efecto similar a #ifdef, #endif. Además, el uso de comentarios puede ser útil para escenarios complejos. Por ejemplo, me encontré con un escenario de empaquetado de SDK bastante complejo, donde los comportamientos para uso interno, externo y para el proyecto principal eran inconsistentes. Si no se compilan múltiples paquetes, la multiplataforma requeriría que el usuario configure sus propias herramientas de compilación. El uso de comentarios permite un empaquetado completo sin necesidad de configurar loaders, lo que en algunos casos puede evitar que los usuarios modifiquen su configuración. Sin embargo, esta situación está bastante acoplada a los escenarios de negocio y solo se proporciona como referencia.
// #IFDEF CHROMIUM
console.log("IS IN CHROMIUM");
// #ENDIF
// #IFDEF GECKO
console.log("IS IN GECKO");
// #ENDIF
Además, al implementar requisitos multiplataforma en el pasado, descubrí que usar demasiada lógica con directivas de preprocesador no era bueno, especialmente cuando involucraba lógica else. Es difícil garantizar si necesitaremos compatibilidad con nuevas plataformas en el futuro. Si usamos lógica else, deberíamos revisar todas las ramas de lógica multiplataforma al agregar o eliminar plataformas de compilación. Es fácil pasar por alto algunas ramas, lo que lleva a errores. Por lo tanto, aquí solo necesitamos usar #IFDEF, #ENDIF. Es decir, especificamos explícitamente la plataforma para la cual se debe compilar este código, evitando así problemas innecesarios y manteniendo la extensibilidad de la plataforma.
A continuación, necesitamos implementar la funcionalidad a través de un loader. Aquí, me baso en rspack, que también es compatible con la interfaz básica de webpack5. Sin embargo, dado que principalmente procesamos código fuente, solo usamos las capacidades básicas de la API. De hecho, son universales en la mayoría de los casos. Escribir el loader no requiere mucha descripción. Un loader es una función que toma el código fuente como parámetro y devuelve el código procesado. La información necesaria se puede obtener directamente de this. Aquí he añadido anotaciones de tipo con jsdoc.
const path = require("path");
const fs = require("fs");
/**
* @this {import('@rspack/core').LoaderContext}
* @param {string} source
* @returns {string}
*/
function IfDefineLoader(source) {
return source;
}
A continuación, para mantener la generalidad, procesamos algunos parámetros, incluyendo el nombre de la variable de entorno a leer, include, exclude y el modo debug, y realizamos una coincidencia. Si el archivo coincide y necesita ser procesado, continuamos; de lo contrario, devolvemos directamente el código fuente. El modo debug puede ayudarnos a mostrar información de depuración.
// Verificar la configuración de parámetros
/** @type {boolean} */
const debug = this.query.debug || false;
/** @type {(string|RegExp)[]} */
const include = this.query.include || [path.resolve("src")];
/** @type {(string|RegExp)[]} */
const exclude = this.query.exclude || [/node_modules/];
/** @type {string} */
const envKey = this.query.platform || "PLATFORM";
// Filtrar la ruta del recurso
let hit = false;
const resourcePath = this.resourcePath;
for (const includeConfig of include) {
const verified =
includeConfig instanceof RegExp
? includeConfig.test(resourcePath)
: resourcePath.startsWith(includeConfig);
if (verified) {
hit = true;
break;
}
}
for (const excludeConfig of exclude) {
const verified =
excludeConfig instanceof RegExp
? excludeConfig.test(resourcePath)
: resourcePath.startsWith(excludeConfig);
if (verified) {
hit = false;
break;
}
}
if (debug && hit) {
console.log("if-def-loader hit path", resourcePath);
}
if (!hit) return source;
A continuación, viene la lógica específica de procesamiento de código. Al principio, quería usar expresiones regulares para procesarlo directamente, pero descubrí que era bastante complicado, especialmente con anidaciones. Luego pensé que, de todos modos, el código se procesa línea por línea, y el procesamiento línea por línea es la forma más conveniente. Especialmente porque el código está comentado y eventualmente se eliminará. Incluso si hay sangría, podemos eliminar los espacios en blanco al principio y al final para coincidir directamente con las marcas y procesarlas. Esto simplifica enormemente el pensamiento. La directiva de preprocesamiento de inicio #IFDEF solo establecerá true, y la directiva de preprocesamiento de fin #ENDIF solo establecerá false. Nuestro objetivo final es eliminar código, por lo que podemos devolver líneas de código en blanco que no cumplen las condiciones. Sin embargo, aún debemos prestar atención al procesar anidaciones. Necesitamos una pila para registrar el índice de inicio del preprocesamiento actual #IFDEF, es decir, hacer push. Cuando nos encontramos con #ENDIF, hacemos pop. También necesitamos registrar el estado de procesamiento actual. Si el estado de procesamiento actual es true, entonces al hacer pop, debemos determinar si es necesario marcar el estado actual como false para finalizar el procesamiento del bloque actual. También podemos usar debug para generar archivos de los módulos que coinciden después del procesamiento.
// PLATAFORMA ACTUAL: GECKO
// #IFDEF CHROMIUM
// algunas expresiones... // eliminar
// #ENDIF
// #IFDEF GECKO
// algunas expresiones... // conservar
// #ENDIF
// #IFDEF CHROMIUM
// algunas expresiones... // eliminar
// #IFDEF GECKO
// algunas expresiones... // eliminar
// #ENDIF
// #ENDIF
// #IFDEF GECKO
// algunas expresiones... // conservar
// #IFDEF CHROMIUM
// algunas expresiones... // eliminar
// #ENDIF
// #ENDIF
// #IFDEF CHROMIUM|GECKO
// algunas expresiones... // conservar
// #IFDEF GECKO
// algunas expresiones... // conservar
// #ENDIF
// #ENDIF
// Controla si la línea actual cumple la condición de preprocesamiento durante la iteración
const platform = (process.env[envKey] || "").toLowerCase();
let terser = false;
let revised = false;
let terserIndex = -1;
/** @type {number[]} */
const stack = [];
const lines = source.split("\n");
const target = lines.map((line, index) => {
// Eliminar espacios en blanco al principio y al final, eliminar el prefijo de comentario y los espacios en blanco (opcional)
const code = line.trim().replace(/^\/\/\s*/, "");
// Comprobar la directiva de preprocesamiento de inicio #IFDEF, solo establecer en true
if (/^#IFDEF/.test(code)) {
stack.push(index);
// Si es 'true', continuar
if (terser) return "";
const match = code.replace("#IFDEF", "").trim();
const group = match.split("|").map(item => item.trim().toLowerCase());
if (group.indexOf(platform) === -1) {
terser = true;
revised = true;
terserIndex = index;
}
return "";
}
// Comprobar la directiva de preprocesamiento de fin #ENDIF, solo establecer en false
if (/^#ENDIF$/.test(code)) {
const index = stack.pop();
// Ignorar #ENDIF adicionales
if (index === undefined) return "";
if (index === terserIndex) {
terser = false;
terserIndex = -1;
}
return "";
}
// Si la condición de preprocesamiento coincide, borrar
if (terser) return "";
return line;
});
// Sobrescribir archivo de prueba
if (debug && revised) {
// rm -rf ./**/*.log
console.log("if-def-loader revise path", resourcePath);
fs.writeFile(resourcePath + ".log", target.join("\n"), () => null);
}
// Devolver el resultado del procesamiento
return target.join("\n");
El código completo se puede consultar en https://github.com/WindrunnerMax/TKScript/blob/master/packages/force-copy/script/if-def/index.js. También hay implementaciones para desarrollar extensiones de navegador v2/v3 y compatibilidad con Gecko/Chromeium para referencia. El desarrollo de plugins de Tampermonkey también se puede encontrar en el repositorio. Si desea usar un loader ya desarrollado, puede instalar directamente if-def-processor y consultar la configuración en https://github.com/WindrunnerMax/TKScript/blob/master/packages/force-copy/rspack.config.js.
Pregunta Diaria
https://github.com/WindrunnerMax/EveryDay
Referencias
https://juejin.cn/post/6945789317218304014
https://www.rspack.dev/config/builtins.html
https://en.wikipedia.org/wiki/C_preprocessor
https://webpack.js.org/plugins/define-plugin
https://vitejs.dev/config/shared-options.html
https://github.com/rollup/plugins/tree/master/packages/replace