Manecillas de un reloj corriendo sobre fondo desenfocado

Uno de los objetivos que teníamos para este año 2022 era pasar nuestros dos plugins estrella (Nelio Content y Nelio A/B Testing) a TypeScript y React Hooks. Aún no hemos acabado el año pero ya podemos afirmar que este objetivo ha sido un éxito total 🥳 No obstante, el camino ha sido un poco más complicado de lo esperado… especialmente de ver que, después de introducir TypeScript, los tiempos de compilación de nuestros plugins pasaron de ser de unos segundos a algo más de dos minutos; algo estaba fallando y no sabíamos qué.

Pues bien, en la entrada de hoy me gustaría hablarte un poco de esa experiencia y qué hicimos para solucionarlo. Porque, sí, TypeScript siempre hará que el proceso de compilado sea un poco más lento (a fin de cuentas, estamos añadiendo la comprobación de tipos), pero no debería suponer un gran lastre. Y es que, spoiler alert, el problema nunca fue de TypeScript; él simplemente lo hizo obvio. Así que vamos a ello.

Proyecto de ejemplo

Para ayudarte a entender el problema con el que nos encontramos hace unas semanas y cómo lo solucionamos, lo mejor que podemos hacer es crear un ejemplo bien sencillo que sea fácil de reproducir. Así que vamos a crear un pequeño plugin de WordPress en el que la compilación de código JavaScript es lenta por culpa de una mala configuración de webpack. Por supuesto, voy a suponer que tienes un entorno de desarrollo de WordPress con todas las herramientas básicas…

Creación del plugin

Lo primero que debes hacer es crear una nueva carpeta con el nombre de tu plugin (por ejemplo, nelio) en el directorio /wp-content/plugins de tu WordPress y meterle el fichero principal (nelio.php) con el siguiente contenido:

<?php
/**
 * Plugin Name: Nelio
 * Description: This is a test plugin.
 * Version:     0.0.1
 *
 * Author:      Nelio Software
 * Author URI:  https://neliosoftware.com
 * License:     GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 *
 * Text Domain: nelio
 */
if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

Si lo has hecho bien, verás que ya puedes activar el plugin en WordPress:

Captura de pantalla del plugin de pruebas en la lista de plugins
Captura de pantalla del plugin de pruebas en la lista de plugins.

aunque, como puedes imaginar, este no haga nada.

Nelio A/B Testing

Pruebas A/B nativas en WordPress

Usa tu editor de páginas favorito en WordPress para crear variaciones y lanza pruebas A/B con solo un par de clics. No se necesita saber nada de programación para que funcione.

Creación de código TypeScript

A continuación, vamos a centrarnos en la parte JavaScript de nuestro ejemplo, ya que es donde veremos cómo la configuración que usamos puede cambiar radicalmente el tiempo de compilado de nuestro proyecto.

Lo primero que haremos será inicializar npm en la carpeta de nuestro plugin. Ejecuta esto:

npm init

y sigue las instrucciones en pantalla. Luego, instala las dependencias:

npm add -D @wordpress/scripts @wordpress/i18n

y edita el fichero package.json para añadir los scripts de compilación:

{
  ...
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
  },
  ...
}

Después, añadiremos un fichero tsconfig.json con la configuración que queramos usar para el compilador de TypeScript. Also así puede valer para empezar:

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "moduleResolution": "node",
    "outDir": "build",
    "lib": [ "es7", "dom" ]
  },
  "exclude": [ "node_modules" ]
}

Finalmente, vamos a crear los ficheros TypeScript de nuestro proyecto. Para que el ejemplo que estamos haciendo se parezca lo más posible al código que tenenmos en Nelio Content y Nelio A/B Testing, vamos a crear una carpeta src en nuestro proyecto con un par de ficheros TypeScript dentro: index.ts y utils/index.ts.

Vamos a suponer que utils/index.ts es un paquete con utilidades (esto es, funciones que queremos poder usar en los demás ficheros de nuestro proyecto). Por ejemplo, tenemos las funciones min y max que, dados dos números cualesquiera, devuelven el menor y mayor de ellos respectivamente:

export const min = ( a: number, b: number ): number =>
  a < b ? a : b;
export const max = ( a: number, b: number ): number =>
  a > b ? a : b;

Por otro lado, tenemos que index.ts, el fichero principal de nuestra aplicación. No importa mucho qué hace; lo único relevante es que utiliza (alguna) de las funciones definidas en utils/index.ts y, por lo tanto, debería tener a este último como dependencia. Por ejemplo, vamos a suponer que simplemente escribe por consola cuál es menor número de entre dos:

import { __, sprintf } from '@wordpress/i18n';
import { min } from './utils';
const a = 2;
const b = 3;
const m = min( a, b );
console.log(
  sprintf(
    /* translators: 1 -> num, 2 -> num, 3 -> num */
    __( 'Min between %1$s and %2$s is %3$s', 'nelio' ),
    a, b, m
  )
);

Configuración por defecto de @wordpress/scripts

Si compilamos el proyecto tal y como está definido ahora mismo usando npm run build, veremos que todo funciona perfectamente. Y es que @wordpress/scripts está diseñado para funcionar con nuestro ejemplo. Esto es, si tenemos un fichero index.ts en la carpeta src, generará un fichero index.js en la carpeta build junto a un fichero de dependencias index.asset.php:

> ls build
index.asset.php index.js

El fichero de dependencias incluye los nombres de las librerías JavaScript que usamos en nuestro proyecto y que vienen empaquetadas en WordPress. Por ejemplo, dado que nuestro index.ts usaba el paquete @wordpress/i18n para internacionalizar cadenas de texto, verás que en el fichero index.asset.php aparece la dependencia wp-i18n:

build/index.asset.php
<?php
return array(
  'dependencies' => array( 'wp-i18n' ),
  'version'      => 'c6131c7f24df4fa803b7',
);

No obstante, en mi opinión esta configuración por defecto tiene un pequeño problema. Si introducimos un bug cualquiera en el código como, por ejemplo, llamar a la función min con un string en lugar de un number:

const m = min( `${ a }`, b );

esto debería dar un error y, sin embargo, compila sin problemas.

Validando el código TypeScript durante el proceso de compilación

Para solucionar el problema anterior, basta con crear nuestra propia configuración de webpack y que le añadamos el paso de compilar el código TypeScript. Simplemente crea el siguiente fichero webpack.config.json:

const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const config = {
  ...defaultConfig,
  module: {
    ...defaultConfig.module,
    rules: [
      ...defaultConfig.module.rules,
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
};
module.exports = {
  ...config,
  entry: './src/index',
  output: {
    path: __dirname + '/build',
    filename: 'index.js',
  },
};

en el que carga la configuración de webpack por defecto que viene en el paquete @wordpress/scripts y la extendemos añadiendo el ts-loader a los ficheros .ts.

Ahora sí, podemos detectar los errores durante la compilación:

> npm run build
...
ERROR in ...src/index.ts
  TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
webpack 5.74.0 compiled with 1 error in 4470 ms

que, si bien hace que el proceso sea un poco más lento (pasamos de medio segundo a varios segundos de compilación), nos permite enterarnos y corregirlos antes de subir nada a producción.

Encolado de los scripts

Corrije el error y vuelve a compilar el código. Una vez lo tengas, lo único que debes hacer es encolarlo (junto a sus dependencias) en PHP y podrás ver cómo funciona en el navegador.

Abre de nuevo el fichero principal nelio.php y añade el siguiente código al final:

add_action(
  'admin_enqueue_scripts',
  function() {
    $path  = untrailingslashit( plugin_dir_path( __FILE__ ) );
    $url   = untrailingslashit( plugin_dir_url( __FILE__ ) );
    $asset = require( $path . '/build/index.asset.php' );
    wp_enqueue_script(
      'nelio',
      $url . '/build/index.js',
      $asset['dependencies'],
      $asset['version']
    );
  }
);

Luego, acede al escritorio de WordPress y mira en la consola JavaScript de tu navegador. Deberías ver el siguiente texto:

Min between 2 and 3 is 2

¿Y MIS dependencias?

Hablemos un segundo de la gestión de dependencias en JavaScript. @wordpress/scripts está configurado de tal forma que, por defecto, las dependencias a paquetes incluidos en el core de WordPress se añaden al fichero de dependencias. Es por ello que, por ejemplo, hemos visto cómo @wordpress/i18n aparecía listado en las dependencias de nuestro script.

¿Pero qué pasa con las dependencias a «otros» paquetes? ¿Dónde se mete, por ejemplo, la dependencia a nuestro paquete utils? Pues muy sencillo: por defecto webpack compila y combina todas las dependencias dentro del script de salida con lo que, si echas un vistazo al código JavaScript generado (te recomiendo que lo hagas con un npm run start para que no lo minifique), verás que, efectivamente, las funciones de utils están ahí:

...
var __webpack_modules__ = ({
  "./src/utils/index.ts": ((...) => 
    ...
    var min = function (a, b) {
      return a < b ? a : b;
    };
    var max = function (a, b) {
      return a > b ? a : b;
    };
  }),
...

mientras que las dependencias a, por ejemplo, @wordpress/i18n son una simple referencia a la variable global:

...
var __webpack_modules__ = ({
  "./src/utils/index.ts": ...,
  "@wordpress/i18n": ((module)) => {
    module.exports = window["wp"]["i18n"];
  })
...

Como te decía, WordPress viene con un plugin, Dependency Extraction Webpack Plugin, para evitar meter engordar tus scripts metiendo dependencias que ya están en el propio WordPress. En esencia, este plugin compila tu script asumiendo que ciertas dependencias estarán en una variable global (en nuestro caso, por ejemplo, tenemos que @wordpress/i18n está en wp.i18n) e indica, en el fichero .asset.php qué dependencias espera que nosotros encolemos en WordPress.

Configuración personalizada para generar dos scripts

Pues bien, teniendo todo esto en cuenta, supongamos que queremos conseguir lo mismo con nuestro paquete utils. Es decir, no queremos que su contenido se empaquete en el fichero index.js, sino que debería compilarse en su propio fichero .js y aparecer como dependencia en index.asset.php. ¿Cómo hacemos eso?

Lo primero es conseguir que el import de nuestro paquete sea, en general, un poco más cómodo. Es decir, en lugar de importar el script con un path relativo (./utils), sería mejor poder usar un nombre en plan @nelio/utils. Para ello, lo único que debes hacer es editar el fichero package.json del proyecto para añadir una nueva dependencia en dependencies:

{
  ...
  "dependencies": {
    "@nelio/utils": "./src/utils"
  },
  "devDependencies": {
    "@wordpress/i18n": ...,
    "@wordpress/scripts": ...
  },
  ...
}

ejecutar npm install para que meta esta nueva dependencia @nelio/utils en node_modules (como enlace simbólico) y, finalmente, ejecutar el comando npm init en la carpeta src/utils para que, desde el punto de vista de npm, @nelio/utils sea efectivamente un paquete válido.

Luego, para que @nelio/utils se compile en su propio script, necesitamos editar nuestra configuración de webpack.config.js para que en lugar de tener un único export, tenga dos:

  • el que ya teníamos para compilar el fichero ./src/index.ts
  • y otro para compilar el paquete ./src/utils, dejando sus export accesibles en la variable global nelio.utils.

O sea, queremos esto:

module.exports = [
  {
    ...config,
    entry: './src/index',
    output: {
      path: __dirname + '/build',
      filename: 'index.js',
    },
  },
  {
    ...config,
    entry: './src/utils',
    output: {
      path: __dirname + '/build',
      filename: 'utils.js',
      library: [ 'nelio', 'utils' ],
      libraryTarget: 'window',
    },
  },
];

Si compilas el código y echas un vistazo a la carpeta ./build, verás que ahora tenemos todos dos scripts y, en concreto, cómo ./build/utils.js define la variable global nelio.utils:

...
var min = function (a, b) {
  return a < b ? a : b;
};
var max = function (a, b) {
  return a > b ? a : b;
};
(window.nelio = window.nelio || {}).utils = __webpack_exports__;
...

Por desgracia, si volvemos a mirar el contenido de ./build/index.js vemos que sigue embebiendo el contenido de src/utils y que en su lista de dependencias sigue sin aparecer nuestro script…

Configuración personalizada para que nuestro paquete sea una dependencia externa

Si queremos que @nelio/utils se convierta por fin en una dependencia externa, debemos personalizar todavía un poco más nuestro webpack y aprovechar, precisamente, el plugin de extracción de dependencias que hemos mencionado anteriormente. Simplemente, vuelve a abrir el fichero webpack.config.js y modifica la variable config para añadir este nuevo plugin:

const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' );
const config = {
  ...defaultConfig,
  module: {
    ...defaultConfig.module,
    rules: [
      ...defaultConfig.module.rules,
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    ...defaultConfig.plugins.filter( ( p ) =>
      p.constructor.name !== 'DependencyExtractionWebpackPlugin'
    ),
    new DependencyExtractionWebpackPlugin( {
      requestToExternal: ( request ) =>
        '@nelio/utils' === request ? [ 'nelio', 'utils' ] : undefined,
      requestToHandle: ( request ) =>
        '@nelio/utils' === request ? 'nelio-utils' : undefined,
      outputFormat: 'php',
    } ),
  ],
};

¡Y ya está! Ahora sí están las cosas como queríamos. Si echamos un vistazo a las dependencias de ambos scripts, vemos lo siguiente:

build/index.asset.php
<?php
return array( 'dependencies' => array('nelio-utils', 'wp-i18n')... ?>
build/utils.asset.php
<?php
return array( 'dependencies' => array()... ?>

y si miramos en ./build/index.js confirmamos que, efectivamente, la dependencia a @nelio/utils es ahora externa:

...
var __webpack_modules__ = ({
  "@nelio/utils": ((module)) => {
    module.exports = window["nelio"]["utils"];
  }),
  "@wordpress/i18n": ((module)) => {
    module.exports = window["wp"]["i18n"];
  })
...

El único problema que nos queda por resolver ahora es que, si validamos el correcto funcionamiento de nuestro plugin mirando la consola del navegador, veremos que no aparece nada. Y es que nuestro script nelio no se está encolando porque una de sus dependencias (nelio-utils) no está registrada en WordPress. Esto lo podemos arreglar rápidamente editando el fichero nelio.php:

add_action(
  'admin_enqueue_scripts',
  function() {
    $path  = untrailingslashit( plugin_dir_path( __FILE__ ) );
    $url   = untrailingslashit( plugin_dir_url( __FILE__ ) );
    $asset = require( $path . '/build/utils.asset.php' );
    wp_register_script(
      'nelio-utils',
      $url . '/build/utils.js',
      $asset['dependencies'],
      $asset['version']
    );
  }
);

Cómo acelerar el proceso de compilación

Después de toda esta introducción, llega la hora de la verdad. Si ejecutamos varias veces el proceso de compilado y hacemos la media de cuánto tiempo tarda, veremos que el proyecto tarda unos 10 segundos en compilarse:

> yarn run build
...
./src/index.ts + 2 modules ...
webpack 5.74.0 compiled successfully in 5703 ms
...
./src/utils/index.ts ...
webpack 5.74.0 compiled successfully in 5726 m
Done in 10.22s.

lo cual puede no parecer mucho pero, como te decía, en proyectos reales como Nelio Content o Nelio A/B Testing, el tiempo de compilación era de minutos. Entonces, ¿por qué va «lento» y qué podemos hacer para acelerarlo?

Pues, según lo que pude comprobar, el problema estaba en nuestra configuración de webpack. Si usamos múltiples module.exports en webpack, el tiempo de compilación crece de forma exagerada, ya que cada script se compila de forma independiente. Sin embargo, si tenemos un único export, el tiempo es mucho más rápido. Permíteme enseñarte cómo.

En primer lugar, crea un fichero export.ts en src/utils con el siguiente contenido:

export * as utils from './index';

Y luego edita webpack.config.js para que tenga un único export:

module.exports = {
  ...config,
  entry: {
    index: './src/index',
    utils: './src/utils/export',
  },
  output: {
    path: __dirname + '/dist',
    filename: 'js/[name].js',
    library: {
      name: 'nelio',
      type: 'assign-properties',
    },
  },
};

Con todo esto hecho, lanza de nuevo el proceso de compilación y verás que el tiempo medio es de unos 6 segundos:

> yarn run build
...
built modules 522 bytes [built]
  ./src/index.ts + 2 modules ...
  ./src/utils/export.ts + 1 modules ...
webpack 5.74.0 compiled successfully in 4339 ms
Done in 6.02s.

¡Casi, casi la mitad de tiempo! Increíble, ¿verdad?

En resumen

Usar TypeScript en tus proyectos añade un plus de calidad al código, porque permite controlar en tiempo de compilación que los tipos sean correctos y no haya incoherencias. Pero como todo en esta vida, las ventajas de usar TypeScript tienen un precio: compilar el código pasa a ser un poco más lento.

En la entrada de hoy hemos visto que, según cómo tengas configurado webpack, compilar tu proyecto puede ser muchísimo más rápido (o lento). La solución perfecta parece ser tener un único export… y hoy has aprendido cómo hacerlo.

Espero que te haya gustado la entrada. Si es así, compártela. Y si sabes otras formas de optimizar este proceso, dímelo en los comentarios. ¡Nos vemos!

Imagen destacada de Saffu en Unsplash.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

He leído y acepto la política de privacidad de Nelio Software

Tus datos personales se almacenarán en SiteGround y serán usados por Nelio Software con el único objetivo de publicar tu comentario aquí. Con el envío de este comentario, nos das el consentimiento expreso para ello. Escríbenos para acceder, rectificar, limitar o eliminar tus datos personales.