Cuando pensamos en crear aplicaciones, normalmente hablamos de una aplicación, un repositorio de git y un resultado de la compilación.

Sin embargo, esta configuración de una aplicación y un repositorio no siempre refleja la experiencia del mundo real de los desarrolladores.

A menudo, las organizaciones harán uso de un único repositorio con todas las aplicaciones, componentes y bibliotecas que podrían usarse en un desarrollo común.

Eso es un monorepositorio o repositorio único y están comenzando a ser muy populares.

Entonces, ¿Qué hace que un monorepositorio sea interesante para las organizaciones? ¿Por qué poner todo el código en un solo lugar?

¿Por qué no tener un repositorio git único en el que tener muchos repositorios pequeños y separados?

Si te gusta más el inglés, puedes encontrar el contenido original en el Blog de Ionic. Es posible que existan pequeños fallos de traducción a continuación. 🤞

¿Qué hace que un repositorio sea un monorepositorio?

Realmente, un monorepositorio es solo una palabra para un repositorio de git con varios proyectos. Estos proyectos podrían ser múltiples aplicaciones, bibliotecas/servicios compartidos, componentes compartidos, etc.

De esta manera, hay un lugar para que las personas contribuyan con código y lo consuman en sus aplicaciones. Un gran ejemplo de dónde esto podría ser útil es en las aplicaciones de servicios de entrega.

Por ejemplo, si hay dos aplicaciones públicas que satisfacen necesidades diferentes pero utilizan las mismas utilidades.

Podría haber una biblioteca de componentes compartidos que proporcione los elementos en un diseño coherente, también podría haber diferentes servicios que se utilicen para comunicarse con el backend que se necesita en ambas aplicaciones.

En lugar de tener múltiples implementaciones, tiene más sentido proporcionar esas utilidades en un solo monorepositorio, donde todas las aplicaciones pueden hacer uso de los mismos paquetes.

El término clave aquí son los “paquetes” puesto que lo que se crea en un monorepositorio son esencialmente varios paquetes que se pueden consumir en una aplicación.

¿Por qué es esto mejor?

Si mantenemos todo nuestro código en un proyecto, ¿Por qué es mejor dividir todo el código y mantener repositorios separados?

Al mantener todo el código en un repositorio, se mantienen todas las dependencias actualizadas en toda una organización.

Este es probablemente el mayor beneficio de un monorepositorio. Así dejaremos de tener que perder el tiempo en actualizar todas las dependencias de varios proyectos diferentes.

En su lugar, actualizamos todo una vez desde la raíz y ya habremos terminamos, además de realizar los cambios de código necesarios.

Si necesitamos hacer cambios en el código, tenemos acceso a toda la base del código y podemos hacer esos cambios directamente.

Si necesitamos realizar cambios en diferentes partes de las aplicaciones con las que quizás no estemos familiarizados, no existe ningún problema porque podemos coordinarnos con los equipos que son propietarios del código y realizar esos cambios.

Los monorepositorios en este caso pueden:

  • Reducir la carga de mantener dependencias.
  • Simplificar la refactorización del código en una organización u equipo.
  • Mejorar la colaboración entre los diferentes equipos.

Lo mires por donde lo mires, sin lugar a dudas, los monorepositorios son una victoria para el trabajo en equipo.

¿Cómo implementar un monorepositorio en la práctica?

Ok, los monorepositorios son geniales, pero ¿Cómo los implementamos?

Existen muchas formas diferentes de implementar un monorepositorio y cada ecosistema de un framework tiene su propio enfoque recomendado que daría para mucho más que una simple publicación de blog.

Entre compartir código, administrar dependencias entre proyectos y otras tareas, los monorepositorios pueden requerir una gran inversión de tiempo para hacerse bien.

A continuación vamos a ver algunas formas recomendadas de implementar monorepositorios en frameworks como Angular, React y Vue.

Vamos a ver algunas herramientas diferentes que pueden agilizar el proceso y llevar a los desarrolladores por el camino correcto.

Por ejemplo, la herramienta de Nx, los espacios de trabajo de Yarn, los espacios de trabajo de npm.

Monorepositorios para Angular

Veamos la configuración de un espacio de trabajo Nx y algunos paquetes que pueden ayudar a los desarrolladores de Ionic Angular a administrar configuraciones de aplicaciones complejas y enviar sus aplicaciones más rápido.

¿Qué es Nx?

Nx se describe como “herramientas de desarrollo extensibles para monorepositorios” y es creado por Nrwl, una empresa fundada por ex miembros del equipo de Angular.

La herramienta tiene soporte de primera clase para Angular, React, Next.js y más, y todo dentro de un solo espacio de trabajo. Eso incluye la generación de aplicaciones y bibliotecas para todos esos frameworks.

La magia proviene de la CLI de Nx. La experiencia es muy similar a utilizar la CLI de Angular pero con soporte para muchas más tecnologías además de Angular.

La mayoría de las herramientas de monorepositorios están diseñadas para actualizar las aplicaciones existentes, pero Nx tiene un enfoque diferente para ofrecer la mejor experiencia de *+monorepositorios** posible.

Si bien es posible portar una aplicación existente o monorepositorio a un espacio de trabajo de Nx, es mejor dejar que la CLI de Nx genere el espacio de trabajo y la aplicación base debido a la forma en que los arquitectos de herramientas diseñan varios proyectos en un mismo repositorio.

Una única fuente

Nx refuerza enérgicamente la “filosofía de paquete único”. Esto es cuando todos los proyectos en un espacio de trabajo de monorepositorio comparten la misma versión de todas las dependencias. Cuando las dependencias no coinciden en los proyectos, pueden encontrarse con errores e incoherencias.

Las herramientas como los espacios de trabajo de Lerna o Yarn ayudan a eso, pero aún puede resultar confuso para los desarrolladores porque cada proyecto normalmente tiene su propio package.json.

Nx anima a los usuarios a agregar todas las dependencias a la raíz del espacio de trabajo package.json para realizar un seguimiento fácil de las versiones y actualizaciones de las dependencias.

Si bien esto puede parecer una desviación de lo que está acostumbrado, la arquitectura del complemento Nx va a hacer que el proyecto sea más fácil de administrar.

Arquitectura extensible

Si bien los frameworks ayudan a proporcionar una estructura para la construcción de aplicaciones, Nx se dio cuenta de que no existe una solución única para todos.

Para abordar eso, Nx tiene su propia arquitectura de complementos para automatizar la configuración y adaptar su experiencia a las herramientas que utiliza.

Los complementos de Nx te permiten concentrarte en el código, no en la configuración.

Los complementos predeterminados te ayudan a generar aplicaciones/bibliotecas de Angular o React en un solo espacio de trabajo, pero también existen complementos para Storybook, Cypress, ESLint y más.

Casos de uso común

De acuerdo, Nx suena genial, pero es posible que te preguntes: “¿En qué me beneficia esto a mí y a mis aplicaciones de Ionic?”

Algunas situaciones comunes en las que Nx puede ayudar son:

  • Si tu organización desarrolla varias aplicaciones Ionic, es posible que estés implementando componentes muy similares en varias aplicaciones. Ya sea la interfaz de autenticación o un diseño similar para las páginas de configuración, puedes beneficiarte de compartir esos componentes entre aplicaciones. Nx facilita el desarrollo de bibliotecas compartidas para React y Angular y te permite evitar copiar y pegar código. Esto también unifica la experiencia de usuario.
  • Si tus aplicaciones no comparten muchos componentes de la interfaz de usuario, aún te puedes beneficiar de compartir tipos, interfaces y otras funciones. Compartir el código no solo puede mejorar la experiencia del desarrollador, también puede mejorar la confiabilidad de las aplicaciones y evitar errores.
  • Nx también admite configuraciones extendidas para TypeScript, ESLint, Webpack y más. Al crear archivos de configuración base desde los que se extienden todas las aplicaciones y bibliotecas, puedes aplicar las mejores prácticas acordadas en toda tu organización. Esto, combinado con las herramientas y los complementos avanzados de la CLI, hace que la creación de aplicaciones a escala sea más fácil de mantener y coherente.

Complementos de Nxtend

Sobre la base de la arquitectura de complementos que proporciona Nx, Nxtend es una serie de complementos que se centran principalmente en permitir el desarrollo de Ionic y Capacitor en un espacio de trabajo de Nx.

Estos complementos son @nxtend/ionic-react, @nxtend/ionic-angular, @nxtend/capacitor y más.

La principal funcionalidad que brindan estos complementos es la generación de aplicaciones Ionic en un espacio de trabajo Nx que se ajusta a los estándares de otras aplicaciones Nx.

Si bien la documentación oficial de Nx es el mejor lugar para aprender a usar los complementos de Nxtend, puedes comenzar a desarrollar con Nx de inmediato. Primero, debes comenzar por crear un espacio de trabajo Nx.

Para ello, simplemente debes ejecutar desde la terminal o línea de comando:

npx create-nx-workspace my-org --preset=empty

La CLI de Nx permite generar un nuevo espacio de trabajo con una aplicación React o Angular preconfigurada, pero como vamos a agregar los complementos de Nxtend manualmente, queremos generar nuestro espacio de trabajo con la opción --preset=empty.

Generación de aplicaciones

Para este ejemplo, generaremos una aplicación Ionic Angular que usa Capacitor (La generación de una aplicación Ionic Angular es muy similar, y se puede encontrar más información al respecto en la documentación oficial de Nxtend).

Primero, instala la dependencia Nxtend Ionic Angular en tu espacio de trabajo:

npm install --save-dev @nxtend/ionic-angular

Desde aquí, inicializa el complemento:

nx generate @nxtend/ionic-angular:init

Finalmente, genera una nueva aplicación:

nx generate @nxtend/ionic-angular:app my-app

La CLI te pedirá la plantilla de inicio de Ionic que te gustaría usar y una vez que el comando esté completo, puedes comenzar a servir tu aplicación Ionic:

nx serve my-app

Usando Condensador

Las aplicaciones Nxtend Ionic se genera con soporte Capacitor de forma predeterminada. Sin embargo, antes de que podamos usar Capacitor, debemos construir la aplicación:

nx build my-app

A continuación, puedes agregar una plataforma de condensadores como iOS:

nx run my-app:add:ios

Y finalmente, puedes abrir la aplicación en Xcode:

nx run my-app:open:ios

El capacitor también se puede agregar a cualquier aplicación web existente en un espacio de trabajo de Nx. Obtén más información sobre esto en la documentación oficial de Nxtend.

Bibliotecas compartidas

Los complementos de Nxtend Ionic Angular son compatibles con las bibliotecas de Nx Angular, lo que significa que una vez que generes una biblioteca, puedes comenzar a crear componentes en la biblioteca y exportarlos a index.ts para usarlos en tus aplicaciones.

Para generar una nueva biblioteca Angular, ejecuta:

nx generate @nrwl/angular:library my-lib

A continuación, podemos generar un componente en la nueva biblioteca:

nx generate @nrwl/angular:component MyComponent --project my-lib

Antes de que podamos usar componentes Ionic en la nueva biblioteca, debemos importar IonicModule y exportar el nuevo componente en libs/my-lib/src/lib/my-lib.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyComponentComponent } from './my-component/my-component.component';
import { IonicModule } from '@ionic/angular';

@NgModule({
  imports: [CommonModule, IonicModule],
  declarations: [MyComponentComponent],
  exports: [MyComponentComponent],
})
export class MyLibModule {}

Luego, vas a poder actualizar el componente que generamos con Ionic:

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title> Blank </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Blank</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container">
    <h1>Welcome to my-app!</h1>
    <strong>Ready to create an app?</strong>
    <p>
      Start with Ionic
      <a
        target="_blank"
        rel="noopener noreferrer"
        href="https://ionicframework.com/docs/components"
        >UI Components</a
      >
    </p>
  </div>
</ion-content>

Finalmente, importa el módulo en tu aplicación:

import { MyLibModule } from '@my-org/my-lib';

Con eso vas a poder compartir componentes, interfaces, utilidades y más.

Terminando con Nx

Nx es una excelente herramienta para crear espacios de trabajo y la arquitectura del plugin abre la puerta para que las aplicaciones y bibliotecas se sientan como en casa dentro de un monorepositorio.

El desarrollo de aplicaciones Ionic en un espacio de trabajo de Nx puede ofrecer muchos beneficios y el conjunto de complementos de Nxtend está diseñado para que sea un proceso sencillo.

Entre la generación de aplicaciones, bibliotecas, la gestión de dependencias y la capacidad de compartir código entre proyectos, Nx beneficia a proyectos tanto grandes como pequeños.

Monorepositorios para React

La creación y el mantenimiento de varios proyectos de desarrollo conlleva un conjunto único de problemas que los equipos deben resolver.

¿Cómo se comparten fragmentos de código comunes entre proyectos? ¿Cómo se sincronizan las dependencias entre proyectos? ¿Cómo optimizar la colaboración entre proyectos?

En este caso vamos a crear un monorepositorio usando Lerna, una herramienta muy ligera y que funciona bien con proyectos del framework Ionic, React e Ionic Appflow.

El monorepositorio que vamos a completar a continuación constará de tres paquetes; dos aplicaciones de Ionic Framework React y una biblioteca de React compartida que proporcionará un contexto de React diferente para cada aplicación.

Empezando un repositorio con Lerna

Antes de comenzar a generar aplicaciones Ionic Framework o construir la biblioteca de código compartido, necesitamos inicializar un repositorio de Lerna. Este espacio va a contener todos los paquetes.

$ npm install -g lerna
$ git init my-organization && cd my-organization
$ lerna init

Abre el archivo lerna.json. De forma predeterminada, Lerna declara que todos sus paquetes de monorepositorios se alojarán en la carpeta packages/ aunque si lo deseas puedes modificar la carpeta en la que se albergan los paquetes.

En este caso, vamos a crear una carpeta en la que guardar las aplicaciones Ionic React y otra para albergar las bibliotecas de React compartidas.

$ rmdir packages
$ mkdir apps
$ mkdir shared

Actualiza el fichero lerna.json para poder transmitir la estructura del monorepositorio a Lerna:

{
  "packages": ["apps/*", "shared/*"],
  "version": "0.0.0"
}

Antes de agregar nuevos paquetes al monorepositorio, asegurate de usar la CLI de Ionic para establecer una configuración de múltiples aplicaciones:

$ ionic init --multi-app

Generación de múltiples aplicaciones Ionic React

La estructura inicial del monorepositorio está configurada, ahora es el momento de crear las aplicaciones Ionic React. Para asegurarnos de que las apps se crean en la carpeta de apps/ debes acceder a la ruta cd y luego generar las aplicaciones.

$ cd apps/
$ ionic start customers blank --type=react
$ ionic start employees blank --type=react --no-deps

¿Recuerda que un desafío que resuelve monorepos es la capacidad de administrar dependencias entre proyectos? Podemos usar una técnica conocida como “elevación de dependencias” para que ambos paquetes apunten a las mismas carpetas que contienen las dependencias.

$ cd ../
$ lerna bootstrap --hoist

Este proceso movió las dependencias compartidas entre paquetes a una node_modulescarpeta en la raíz del repositorio. Lerna crea enlaces simbólicos para que los paquetes hagan referencia cuando se requiera una dependencia compartida.

Lerna no inicializa a .gitignoreen la raíz del repositorio. No es una buena idea confirmar todas las dependencias elevadas, así que creemos una para excluir nuestras dependencias de estar comprometidas con el control de código fuente.

$ echo "node_modules" > .gitignore

Nota: Para ejecutar los comandos npm de un paquete mediante la CLI de Lerna, el comando es lerna run --scope=<package>. Como ejemplo, para ejecutar la aplicación Empleados lerna run start --scope=employees.

Crear una biblioteca de códigos compartida

Existen herramientas como Storybook y Bit que proporcionan CLI que generan bibliotecas React destinadas a ser compartidas. Pueden ser excelentes herramientas para que las agregues a tu caja de herramientas de desarrollo, pero pueden tener otros defectos.

Para el propósito de esta publicación de blog, usaremos Rollup para crear el nuestro.

Estructurando el paquete

Lerna nos permite crear proyectos genéricos de JavaScript a través de su CLI. Agreguemos un paquete y estructurémoslo de manera que se pueda usar como una biblioteca React reutilizable.

$ lerna create @myorg/core shared --description="Core shared library" --es-module --access=restricted --yes

Agreguemos Rollup al paquete y hagamos algunas modificaciones en la estructura del paquete.

$ cd shared/core
$ npm install --save-dev rollup rollup-plugin-typescript2
$ echo "dist" >> .gitignore
$ rm -rf __tests__ README.md
$ mv src/core.js src/index.ts
$ touch rollup.config.js tsconfig.json
$ cd ../../

A continuación, completa shared/code/rollup.config.js con el siguiente código:

import typescript from 'rollup-plugin-typescript2';
import pkg from './package.json';

const input = "src/index.ts";

const external = [
  ...Object.keys(pkg.dependencies || {}),
  ...Object.keys(pkg.peerDependencies || {}),
];

const plugins = [ typescript({ typescript: require("typescript") }) ];

export default [
  {
    input,
    output: { file: pkg.module, format: "esm", sourcemap: true },
    plugins,
    external
  },
  {
    input,
    output: { file: pkg.main, format: "cjs", sourcemap: true },
    plugins,
    external
  },
];

Luego, complete shared/core/tsconfig.jsoncon el siguiente código:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "allowUnreachableCode": false,
    "declaration": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "importHelpers": true,
    "lib": ["es2015", "dom"],
    "module": "es2015",
    "moduleResolution": "node",
    "noEmitHelpers": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": false,
    "noUnusedParameters": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "es2017",
    "sourceMap": true,
    "inlineSources": true,
    "jsx": "react"
  },
  "include": ["src/**/*"],
  "exclude": ["src/**/**.test.*"]
}

Finalmente, necesitamos hacer algunas modificaciones a shared/core/package.json.

  1. Agrega una nueva sección llamada peerDependencies. Copia la matriz dependencies de una de las aplicaciones y pegala en esta sección.
  2. Agrega la matriz de peerDependenciesa la matriz de devDependencies.
  3. Quita la sección files.
  4. En la sección directories cambia el libvalor src y elimina la entrada test.
  5. Actualiza la propiedad main de dist/index.js y module a dist/index.esm.js.
  6. Reemplaza el contenido de la sección scripts con los siguientes scripts:
"build": "npx rollup -c",
"watch": "npx rollup -c -w"

Nota: No todos peerDependencies o devDependencies son necesarios. Puedes eliminar cualquiera que no se estés utilizando en el código fuente del paquete.

Ahora puedes agregar el paquete compartido a las aplicaciones.

$ lerna run build [email protected]/core
$ lerna bootstrap --hoist
$ lerna add @myorg/core

Cuando agregues un paquete compartido a los paquetes de aplicaciones en un monorepositorio, usa el enfoque de nomenclatura de paquetes npm con alcance (como @myorg/core).

Construyendo un contexto temático

Técnicamente, podemos hacer una demostración de la biblioteca compartida en nuestros paquetes de aplicaciones importando la función core() definida en, @myorg/core pero eso es bastante poco convincente.

En su lugar, creamos un contexto de React que proporciona las tuberías necesarias para permitir a los usuarios alternar entre el modo claro/oscuro en las aplicaciones.

$ echo "export * from './theme/ThemeContext';" > shared/core/src/index.ts
$ mkdir shared/core/src/theme
$ touch shared/core/src/theme/ThemeContext.tsx

Completa shared/core/src/theme/ThemeContext.tsx con el siguiente código:

import React, { createContext, useContext, useEffect, useState } from "react";

const initialContext = {
  isDarkMode: false,
  toggleDarkMode: (_: boolean) => {},
};

const ThemeContext = createContext(initialContext);
export const useTheme = () => useContext(ThemeContext);

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkMode, setDarkMode] = useState<boolean>(false);

  useEffect(() => {
    const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
    prefersDark.addEventListener("change", (e) => setDarkMode(e.matches));
    toggleDarkMode(prefersDark.matches);
  }, []);

  const toggleDarkMode = (useDarkMode: boolean) => {
    document.body.classList.toggle("dark", useDarkMode);
    setDarkMode(useDarkMode);
  };

  return (
    <ThemeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
      {children}
    </ThemeContext.Provider>
  );
};

Reconstruye el paquete @myorg/core para que las aplicaciones puedan tener acceso a “y useTheme().

$ lerna run build [email protected]/core

Nota: Al desarrollar bibliotecas compartidas, es beneficioso ejecutar lerna run watch --scope= para reconstruir la biblioteca en tiempo real.

Poniendo todo en conjunto

Actualiza los paquetes de aplicaciones para ver el contexto del tema en acción. Las siguientes acciones deben realizarse en los paquetes customers y employees:

  1. Elimina la @media (prefers-color-scheme: dark)consulta de medios en variables.css.
  2. En variables.css, anexar .darka cualquier bodyselector; bodyse vuelve body.dark, se .ios bodyvuelve .ios body.darky se .md bodyvuelve .md body.dark.
  3. Además, App.tsxagregue la siguiente importación:import { ThemeProvider } from '@myorg/core';
  4. En App.tsx, envuelva el componente y sus hijos con, por lo que “es el componente más externo de la Appplantilla.

¡Sirve una de las aplicaciones ( lerna run start --scope=), cambia tu preferencia de modo oscuro y actualiza el navegador para probarlo!

Integración con Appflow

Appflow es la plataforma CI / CD móvil de Ionic que facilita la creación, publicación y actualización de sus aplicaciones a lo largo del tiempo. ¡Ah, y también es compatible con monorepos!

Para admitir una estructura de monorepo, Appflow necesita un appflow.config.jsonarchivo singular en la raíz del repositorio de monorepo.

$ touch appflow.config.json

Complete el archivo con lo siguiente:

{
  "apps": [
    {
      "appId": "XXXXXXXX",
      "root": "apps/customers",
      "dependencyInstallCommand": "cd ../../ && npx lerna bootstrap && npx lerna run build [email protected]/core"

    },
    {
      "appId": "XXXXXXX",
      "root": "apps/employees",
      "dependencyInstallCommand": "cd ../../ && npx lerna bootstrap && npx lerna run build [email protected]/core"

    }
  ]
}

Reemplace los appIdvalores con los que le proporcionó Appflow, por supuesto. Lo importante aquí es que, como parte del comando de instalación de dependencias, construimos los paquetes compartidos después de arrancar Lerna. En un escenario del mundo real, probablemente desee crear un script de shell personalizado para encapsular los comandos utilizados anteriormente, especialmente como la cantidad de bibliotecas compartidas en su escala monorepo.

Terminando

¡Ahora tenemos un monorepo construido con Lerna que contiene dos aplicaciones Ionic Framework React y una biblioteca React compartida, conectada a Appflow para una integración y una implementación continuas!

Nuestro monorepo resuelve varios problemas encontrados al mantener múltiples proyectos: comparte código común entre proyectos, administra dependencias entre proyectos y facilita el esfuerzo de colaboración entre proyectos.

Con una base sólida en su lugar, puede continuar escalando su monorepo para que se ajuste a sus necesidades y a las de su equipo de desarrollo.

Monorepositorios para Vue

Npm ha sido durante mucho tiempo la solución de facto para administrar dependencias, y tiene sentido que con el lanzamiento de npm 7.0, finalmente tengamos una solución incorporada para crear un monorepositorio sin depender de herramientas externas.

Sin embargo, en comparación con otras soluciones, los espacios de trabajo de npm carecen de algunas características y aún tienen algunos aspectos ásperos.

Si bien es posible construir algo con él, por simplicidad, sugeriría mirar a Lerna como una alternativa. Dicho esto, veamos cómo podemos configurar un espacio de trabajo npm para trabajar con Ionic y Vue.

Construcción

Para preparar la escena, lo que vamos a construir es una aplicación Ionic Vue y un segundo proyecto que contiene un gancho Vue. El gancho se tomó prestado del proyecto vue-composable .

Comienza creando primero los directorio base e inicializando tanto package.json como ionic.config.json. Para ejecutar el package.json:

mkdir vue-monorepo
cd vue-monorepo
npm init -y

Desde aquí, también puedes crear un proyecto Ionic básico con el comando ionic init.

ionic init --multi-app

También puedes crear un directorio que contenga todos los paquetes. Para eso, te servirá un directorio llamado packages, pero el nombre puede ser el que desees. packages es solo una convención común en la que la gente se ha familiarizado.

mkdir packages
cd packages

Una vez hecho esto, crearás un solo proyecto de Ionic Vue y un paquete de utilidades mínimo.

mkdir utils
ionic start client-app tabs --type vue --no-deps --no-git

Actualmente, incluso si pasas la bandera --no-deps las dependencias se instalarán cuando se configure Capacitor. Simplemente haz cd client-app y elimina la carpeta node_modules del proyecto.

Configuración de las utilidades

Parael paquete utils habrá que realizar un poco más de trabajo manual para configurar un paquete mínimo de ganchos para un proyecto Vue.

cd packages/utils
npm init -y
mkdir src
touch tsconfig.json

Abre package.json y pega lo siguiente:

{
  "name": "@client/hooks",
  "version": "0.1.0",
  "private": true,
  "main": "dist/index.js",
  "module": "dist/index.js",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "watch": "tsc -p tsconfig.json --watch"
  },
  "dependencies": {
    "vue": "^3.0.0"
  },
  "files": ["dist/"],
  "devDependencies": {
    "typescript": "~4.1.5"
  }
}

Luego, abre tu tsconfig.json y pega lo siguiente:

{
  "compilerOptions": {
    "target": "ES5",
    "outDir": "dist",
    "module": "CommonJS",
    "strict": true,
    "importHelpers": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": false,
    "declaration": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "lib": ["esnext", "dom", "dom.iterable"]
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

Desde aquí, pouedes hacer un archivo src/index.ts y pegar el siguiente código.

/* eslint-disable */
import { ref, Ref } from 'vue';

// useOnline composable hook.
// Adapted from https://github.com/pikax/vue-composable

const PASSIVE_EV: AddEventListenerOptions = { passive: true };
let online: Ref<boolean> | undefined = undefined;
export function useOnline() {
  const supported = 'onLine' in navigator;
  if (!supported) {
    online = ref(false);
  }

  if (!online) {
    online = ref(navigator.onLine);
    window.addEventListener(
      'offline',
      () => (online!.value = false),
      PASSIVE_EV
    );
    window.addEventListener('online', () => (online!.value = true), PASSIVE_EV);
  }

  return { supported, online };
}

Ahora puedes dejar el directorio `utils y volver al proyecto raíz.

Configurar el espacio de trabajo

Con el código inicial creado, ahora puedes configurar el espacio de trabajo. Para npm los espacios de trabajo son solo una entrada en la raíz package.json. Dado que todos los paquetes están en el directorio packages puedes agregar lo siguiente a la raíz package.json.

{
  "name": "ionic-vue-npm-workspaces",
  "version": "1.0.0",
  "description": "",
  "scripts": {...},
  "license": "MIT",

  "workspaces": [
    "packages/*"
  ]

}

La entrada workspaces te permite declarar qué paquetes están disponibles en el nivel superior. Como queremos exponer todos los paquetes en el directorio puedes usarpackages/*`para obtener todos los paquetes.

Con esto completado, corre npm install desde el nivel superior. Con el espacio de trabajo configurado para incluir todos los subpaquetes, la instalación instalará *todas las dependencias utilizadas en ambos proyectos** en un directorio de nivel superior node_modules.

Eso significa que vas a poder tener un mejor control sobre las dependencias que estamos usando en cada proyecto y vas a tener unificadas todas las dependencias en una sola versión.

Con las dependencias instaladas, ¿cómo construimos nuestros subpaquetes? Esto se puede hacer llamando al script que queremos ejecutar, seguido de --workspace=. Si queremos construir el utilsdirectorio, usamos la entrada de nombre de package.json ( @client/hooks) como valor para el espacio de trabajo. Entonces, nuestro comando final se ve así:

npm run build [email protected]/hooks

Se aplicaría la misma lógica si queremos construir/servir la aplicación: Elegimos el script que queremos ejecutar y pasamos el nombre al espacio de trabajo.

Incluyendo un paquete

Hasta ahora, tienes los paquetes configurados y construidos, pero no se están utilizando, lo que anula el punto de tener un monorepositorio. Entonces, ¿Cómo podemos incluir los paquetes de utilidades en nuestra aplicación principal? Para hacer esto, debes referenciar el paquete en la aplicación.

En el proyecto client-app abre package.jsony agrega una nueva línea a las dependencias para @client/hooks:

{
  "dependencies": {
    "@capacitor/core": "3.0.0-rc.1",
    "@client/hooks": "0.1.0",
    "@ionic/vue": "^5.4.0",
    "@ionic/vue-router": "^5.4.0",
    "core-js": "^3.6.5",
    "vue": "^3.0.0-0",
    "vue-router": "^4.0.0-0"
  }
}

Luego puedes agregar una referencia a @client/hooks en el proyecto en el componente client-app/src/views/Tab1.vue.

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Tab 1</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">Tab 1</ion-title>
        </ion-toolbar>
      </ion-header>
        <h1>Is the App online?</h1>
        <p>{{ online }}</p>
      <ExploreContainer name="Tab 1 page" />
    </ion-content>
  </ion-page>
</template>

<script lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import ExploreContainer from '@/components/ExploreContainer.vue';

import { useOnline } from '@client/hooks';

export default  {
  name: 'Tab1',
  components: { ExploreContainer, IonHeader, IonToolbar, IonTitle, IonContent, IonPage },
    setup() {
    const { online } = useOnline();
    return { online };
  },

}
</script>

Puedes guardar y volver a la terminal, para desde la raíz ejecutar:

npm install
npm run serve --workspace=client-app

Cuando abrimos el navegador en localhost:8080 la aplicación debería incluir el código de nuestro segundo paquete.

Reflexión Final

De todas las opciones disponibles, los espacios de trabajo npm incluyen la menor cantidad de funciones en comparación con yarn/Lerna o nx. Pero eso podría ser beneficioso para ti y tu equipo si deseas tener más control sobre cómo funcionan tus monorepositorios.

De cualquier manera, es genial ver a npm ingresar al juego de los monorepositorios y habrá que ir ojeando como evoluciona en este campo con el paso del tiempo.