Usando un módulo de nodo compartido para clases comunes

15

Objetivo

Entonces estoy teniendo un proyecto con esta estructura:

  • aplicación iónica
  • funciones de firebase
  • compartido

El objetivo es definir interfaces y clases comunes en el sharedmódulo.

Restricciones

No quiero cargar mi código a npm para usarlo localmente y no planeo cargar el código en absoluto. Debería funcionar al 100% sin conexión.

Si bien el proceso de desarrollo debería funcionar sin conexión, los módulos ionic-appy firebase-functionsse implementarán en firebase (hosting y funciones). Por lo tanto, el código del sharedmódulo debe estar disponible allí.

Lo que he intentado hasta ahora

  • He intentado usar referencias de proyecto en mecanografiado, pero no lo he logrado.
  • Lo probé instalándolo como un módulo npm como en la segunda respuesta de esta pregunta
    • Al principio parece estar funcionando bien, pero durante la compilación, aparece un error como este cuando se ejecuta firebase deploy:
Function failed on loading user code. Error message: Code in file lib/index.js can't be loaded.
Did you list all required modules in the package.json dependencies?
Detailed stack trace: Error: Cannot find module 'shared'
    at Function.Module._resolveFilename (module.js:548:15)
    at Function.Module._load (module.js:475:25)
    at Module.require (module.js:597:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/srv/lib/index.js:5:18)

Pregunta

¿Tiene una solución para hacer un módulo compartido usando ya sea config de script o NPM?

No marque esto como un duplicado → He intentado cualquier solución que he encontrado en StackOverflow.

Información adicional

Configuración para compartido:

// package.json
{
  "name": "shared",
  "version": "1.0.0",
  "description": "",
  "main": "dist/src/index.js",
  "types": "dist/src/index.d.ts",
  "files": [
    "dist/src/**/*"
  ],
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "publishConfig": {
    "access": "private"
  }
}

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "rootDir": ".",
    "sourceRoot": "src",
    "outDir": "dist",
    "sourceMap": true,
    "declaration": true,
    "target": "es2017"
  }
}

Configuración para funciones:

// package.json
{
  "name": "functions",
  "scripts": {
    "lint": "tslint --project tsconfig.json",
    "build": "tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "8"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^8.0.0",
    "firebase-functions": "^3.1.0",
    "shared": "file:../../shared"
  },
  "devDependencies": {
    "@types/braintree": "^2.20.0",
    "tslint": "^5.12.0",
    "typescript": "^3.2.2"
  },
  "private": true
}


// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./",
    "module": "commonjs",
    "noImplicitReturns": true,
    "noUnusedLocals": false,
    "rootDir": "src",
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "es2017"
  }
}

Sopa actual

He agregado un script npm al módulo compartido, que copia todos los archivos (sin index.js) a los otros módulos. Esto tiene el problema, que verifico el código duplicado en SCM y que necesito ejecutar ese comando en cada cambio. Además, el IDE solo lo trata como archivos diferentes.

MauriceNino
fuente

Respuestas:

4

Prefacio: no estoy muy familiarizado con el funcionamiento de la compilación de Typecript y cómopackage.json se debe definir dicho módulo. Esta solución, aunque funciona, podría considerarse una forma poco hábil de lograr la tarea en cuestión.

Asumiendo la siguiente estructura de directorios:

project/
  ionic-app/
    package.json
  functions/
    src/
      index.ts
    lib/
      index.js
    package.json
  shared/
    src/
      shared.ts
    lib/
      shared.js
    package.json

Al implementar un servicio Firebase, puede adjuntar comandos a los ganchos anteriores y posteriores a la implementación . Esto se realiza a firebase.jsontravés de las propiedades predeployy postdeployen el servicio deseado. Estas propiedades contienen una serie de comandos secuenciales para ejecutar antes y después de implementar su código, respectivamente. Además, estos comandos se llaman con las variables de entorno RESOURCE_DIR(la ruta del directorio de ./functionso ./ionic-app, según corresponda) y PROJECT_DIR(la ruta del directorio que contiene firebase.json).

Usando la predeploymatriz para functionsadentro firebase.json, podemos copiar el código de la biblioteca compartida en la carpeta que se implementa en la instancia de Cloud Functions. Al hacer esto, simplemente puede incluir el código compartido como si fuera una biblioteca ubicada en una subcarpeta o puede asignar su nombre usando la asignación de ruta de TypeScript en tsconfig.jsonun módulo con nombre (para que pueda usarimport { hiThere } from 'shared'; ).

La predeploydefinición de enlace (utiliza la instalación global de shxcompatibilidad con Windows):

// firebase.json
{
  "functions": {
    "predeploy": [
      "shx rm -rf \"$RESOURCE_DIR/src/shared\"", // delete existing files
      "shx cp -R \"$PROJECT_DIR/shared/.\" \"$RESOURCE_DIR/src/shared\"", // copy latest version
      "npm --prefix \"$RESOURCE_DIR\" run lint", // lint & compile
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  },
  "hosting": {
    "public": "ionic-app",
    ...
  }
}

Vinculación de la fuente de mecanografiado de la biblioteca copiada a la configuración del compilador de mecanografiado de funciones:

// functions/tsconfig.json
{
  "compilerOptions": {
    ...,
    "baseUrl": "./src",
    "paths": {
      "shared": ["shared/src"]
    }
  },
  "include": [
    "src"
  ],
  ...
}

Asociar el nombre del módulo, "compartido", a la carpeta del paquete de la biblioteca copiada.

// functions/package.json
{
  "name": "functions",
  "scripts": {
    ...
  },
  "engines": {
    "node": "8"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^8.6.0",
    "firebase-functions": "^3.3.0",
    "shared": "file:./src/shared",
    ...
  },
  "devDependencies": {
    "tslint": "^5.12.0",
    "typescript": "^3.2.2",
    "firebase-functions-test": "^0.1.6"
  },
  "private": true
}

Se puede usar el mismo enfoque con la carpeta de alojamiento.


Esperemos que esto inspire a alguien que esté más familiarizado con la compilación de Typecript a encontrar una solución más limpia que haga uso de estos ganchos.

samthecodingman
fuente
3

Es posible que desee probar Lerna , una herramienta para administrar proyectos JavaScript (y TypeScript) con múltiples paquetes.

Preparar

Asumiendo que su proyecto tiene la siguiente estructura de directorios:

packages
  ionic-app
    package.json
  firebase-functions
    package.json
  shared
    package.json

Asegúrese de especificar el nivel de acceso correcto ( privatey las config/accessclaves) en todos los módulos que no desea publicar, así como la typingsentrada en su sharedmódulo:

Compartido:

{
  "name": "shared",
  "version": "1.0.0",
  "private": true,
  "config": {
    "access": "private"
  },
  "main": "lib/index.js",
  "typings": "lib/index.d.ts",
  "scripts": {
    "compile": "tsc --project tsconfig.json"
  }
}

Aplicación iónica:

{
  "name": "ionic-app",
  "version": "1.0.0",
  "private": true,
  "config": {
    "access": "private"
  },
  "main": "lib/index.js",
  "scripts": {
    "compile": "tsc --project tsconfig.json"
  },
  "dependencies": {
    "shared": "1.0.0"
  }
}

Con los cambios anteriores en su lugar, puede crear un nivel raíz package.jsondonde puede especificar cualquiera devDependenciesque desee que tengan acceso a todos los módulos de su proyecto, como su marco de prueba de unidad, tslint, etc.

packages
  ionic-app
    package.json
  firebase-functions
    package.json
  shared
    package.json
package.json         // root-level, same as the `packages` dir

También puede usar este nivel raíz package.jsonpara definir scripts npm que invocarán los scripts correspondientes en los módulos de su proyecto (a través de lerna):

{
  "name": "my-project",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "compile": "lerna run compile --stream",
    "postinstall": "lerna bootstrap",
  },
  "devDependencies": {
    "lerna": "^3.18.4",
    "tslint": "^5.20.1",
    "typescript": "^3.7.2"
  },
}

Con eso en su lugar, agregue el archivo de configuración de lerna en su directorio raíz:

packages
  ionic-app
    package.json
  firebase-functions
    package.json
  shared
    package.json
package.json
lerna.json

con los siguientes contenidos:

{
  "lerna": "3.18.4",
  "loglevel": "info",
  "packages": [
    "packages/*"
  ],
  "version": "1.0.0"
}

Ahora, cuando se ejecuta npm installen el directorio raíz, se invocará el postinstallscript definido en su nivel raíz .package.jsonlerna bootstrap

Lo que lerna bootstraphace es que vinculará su sharedmódulo ionic-app/node_modules/sharedy firebase-functions/node_modules/shared, desde el punto de vista de esos dos módulos, se sharedparece a cualquier otro módulo npm.

Compilacion

Por supuesto, simular los módulos no es suficiente, ya que aún necesita compilarlos de TypeScript a JavaScript.

Ahí es donde package.json compileentra en juego el script de nivel raíz .

Cuando se ejecuta npm run compileen la raíz del proyecto, npm invocará lerna run compile --streame lerna run compile --streaminvocará el script llamado compileen cada uno de los package.jsonarchivos de sus módulos .

Dado que cada uno de sus módulos ahora tiene su propio compilescript, debe tener un tsonfig.jsonarchivo por módulo. Si no le gusta la duplicación, puede salirse con una tsconfig de nivel raíz, o una combinación de una tsconfig de nivel raíz y archivos tsconfig de nivel de módulo que heredan de la raíz.

Si desea ver cómo funciona esta configuración en un proyecto del mundo real, eche un vistazo a Serenity / JS, donde la he estado usando ampliamente.

Despliegue

Lo bueno de tener el sharedmódulo de enlace simbólico bajo node_modulesbajo firebase-functionsy ionic-app, y su devDepedenciesbajo node_modulesen la raíz del proyecto es que si tiene que desplegar el cualquier parte del módulo de consumo (por lo que el ionic-apppor ejemplo), usted podría comprimir todo para arriba junto con su node_modulesy no preocuparse tener que eliminar las dependencias de desarrollo antes de la implementación.

¡Espero que esto ayude!

ene

Jan Molak
fuente
Intresting! Definitivamente lo comprobaré y veré si es el adecuado.
MauriceNino
2

Otra posible solución, si está usando git para administrar su código, está usando git submodule . Utilizando git submodulepuedes incluir otro repositorio git en tu proyecto.

Aplicado a su caso de uso:

  1. Empuje la versión actual de su repositorio de git compartido
  2. Use git submodule add <shared-git-repository-link>dentro de su (s) proyecto (s) principal (es) para vincular el repositorio compartido.

Aquí hay un enlace a la documentación: https://git-scm.com/docs/git-submodule

Friedow
fuente
En realidad no es una mala idea, pero el desarrollo y las pruebas locales básicamente se han ido con este enfoque.
MauriceNino
0

Si entiendo su problema correctamente, la solución es más compleja que una sola respuesta y depende en parte de su preferencia.

Enfoque 1: copias locales

Puede usar Gulp para automatizar la solución de trabajo que ya describió, pero IMO no es muy fácil de mantener y aumenta drásticamente la complejidad si en algún momento entra otro desarrollador.

Enfoque 2: Monorepo

Puede crear un único repositorio que contenga las tres carpetas y conectarlas para que se comporten como un solo proyecto. Como ya se respondió anteriormente, puede usar Lerna . Requiere un poco de configuración, pero una vez hecho, esas carpetas se comportarán como un solo proyecto.

Enfoque 3: componentes

Trate cada una de estas carpetas como un componente independiente. Echa un vistazo a Bit . Le permitirá configurar las carpetas como partes más pequeñas de un proyecto más grande y puede crear una cuenta privada que abarcará esos componentes solo para usted. Una vez configurado inicialmente, le permitirá incluso aplicar actualizaciones a las carpetas separadas y el padre que las usa obtendrá automáticamente las actualizaciones.

Enfoque 4: Paquetes

Dijiste específicamente que no quieres usar npm, pero quiero compartirlo, porque actualmente estoy trabajando con una configuración como se describe a continuación y estoy haciendo un trabajo perfecto para mí:

  1. Use npmo yarnpara crear un paquete para cada carpeta (puede crear paquetes de ámbito para ambos, de modo que el código solo estará disponible para usted, si esto le preocupa).
  2. En la carpeta principal (que usa todas estas carpetas), los paquetes creados están conectados como dependencias.
  3. Uso webpack para agrupar todo el código, usando alias de ruta webpack en combinación con rutas de mecanografía.

Funciona a las mil maravillas y cuando los paquetes están enlazados para el desarrollo local, funciona completamente fuera de línea y, en mi experiencia, cada carpeta es escalable por separado y muy fácil de mantener.

Nota

Los paquetes 'secundarios' ya están precompilados en mi caso, ya que son bastante grandes y he creado tsconfigs por separado para cada paquete, pero lo hermoso es que puedes cambiarlo fácilmente. En el pasado, utilicé el mecanografiado en el módulo y los archivos compilados, y también archivos js sin formato, por lo que todo es muy, muy versátil.

Espero que esto ayude

***** ACTUALIZACIÓN **** Para continuar en el punto 4: Pido disculpas, mi mal. Tal vez me equivoqué porque, hasta donde yo sé, no puedes vincular un módulo si no está cargado. Sin embargo, aquí está:

  1. Tienes un módulo npm separado, usemos firebase-functions para eso. Lo compilas o usas ts sin procesar, según tu preferencia.
  2. En su proyecto principal agregue firebase-functions como dependencia.
  3. En tsconfig.json , agregue"paths": {"firebase-functions: ['node_modules/firebase-functions']"}
  4. En webpack - resolve: {extensions: ['ts', 'js'], alias: 'firebase-functions': }

De esta manera, hace referencia a todas sus funciones exportadas desde el firebase-functionsmódulo simplemente mediante el uso import { Something } from 'firebase-functions'. Webpack y TypeScript lo vincularán a la carpeta de módulos de nodo. Con esta configuración, al proyecto principal no le importará si elfirebase-functions módulo está escrito en TypeScript o javascript vainilla.

Una vez configurado, funcionará perfectamente para la producción. Luego, para vincular y trabajar sin conexión:

  1. Navega al firebase-functionsproyecto y escribe npm link. Creará un enlace simbólico, local a su máquina y asignará al enlace el nombre que configuró en package.json.
  2. Navegue al proyecto principal y escriba npm link firebase-functions, lo que creará el enlace simbólico y asignará la dependencia de las funciones de firebase a la carpeta en la que lo ha creado.
Ivan Dzhurov
fuente
Creo que malinterpretaste algo. Nunca dije que no quiero usar npm. De hecho, los tres módulos son módulos de nodo. Acabo de decir que no quiero subir mis módulos a npm. ¿Puedes explicar un poco más sobre esa cuarta parte, que suena interesante? tal vez proporcionar una muestra de código?
MauriceNino
Agregaré otra respuesta, ya que será larga e ilegible como comentario
Ivan Dzhurov el
Actualicé mi respuesta inicial, espero que sea más clara
Ivan Dzhurov
0

No quiero cargar mi código a npm para usarlo localmente y no planeo cargar el código en absoluto. Debería funcionar al 100% sin conexión.

Todos los módulos npm se instalan localmente y siempre funcionan sin conexión, pero si no desea publicar sus paquetes públicamente para que la gente pueda verlos, puede instalar un registro privado npm.

ProGet es un servidor de repositorio privado NuGet / Npm disponible para Windows que puede usar en su entorno privado de desarrollo / producción para alojar, acceder y publicar sus paquetes privados. Aunque está en Windows, pero estoy seguro de que hay varias alternativas disponibles en Linux.

  1. Git Submodules es una mala idea, es realmente una forma antigua de compartir código que no está versionado como los paquetes, cambiar y comprometer submódulos es un verdadero dolor.
  2. La carpeta de importación de origen también es una mala idea, de nuevo el problema del control de versiones es porque si alguien modifica la carpeta dependiente en el repositorio dependiente, volver a rastrearlo es una pesadilla.
  3. Cualquier herramienta de script de terceros para emular la separación de paquetes es una pérdida de tiempo, ya que npm ya ofrece una gama de herramientas para administrar paquetes tan bien.

Aquí está nuestro escenario de construcción / implementación.

  1. Cada paquete privado tiene .npmrccuál contiene registry=https://private-npm-repository.
  2. Publicamos todos nuestros paquetes privados en nuestro repositorio ProGet alojado de forma privada.
  3. Cada paquete privado contiene paquetes privados dependientes en ProGet.
  4. Nuestro servidor de compilación accede a ProGet a través de la autenticación npm establecida por nosotros. Nadie fuera de nuestra red tiene acceso a este repositorio.
  5. Nuestro servidor de compilación crea un paquete npm con el bundled dependenciesque contiene todos los paquetes dentro node_modulesy el servidor de producción nunca necesita acceder a NPM o paquetes privados de NPM ya que todos los paquetes necesarios ya están agrupados.

El uso del repositorio privado npm tiene varias ventajas,

  1. No necesita script personalizado
  2. Se adapta a la tubería de publicación / publicación de nodos
  3. Cada paquete npm privado contendrá un enlace directo a su control de fuente git privado, fácil de depurar e investigar errores en el futuro
  4. Cada paquete es una instantánea de solo lectura, por lo que una vez publicado no se puede modificar, y mientras realiza nuevas funciones, la base de código existente con la versión anterior de paquetes dependientes no se verá afectada.
  5. Puede hacer públicos algunos paquetes fácilmente y pasar a otro repositorio en el futuro
  6. Si el software de su proveedor npm privado cambia, por ejemplo, decide mover su código a la nube de registro del paquete npm privado del nodo, no necesitará realizar ningún cambio en su código.
Akash Kava
fuente
Esto podría ser una solución, pero desafortunadamente no es para mí. ¡Gracias por tu tiempo!
MauriceNino
También hay un repositorio npm local que se instala como un servidor de nodo pequeño, verdaccio.org
Akash Kava
-1

La herramienta que estás buscando es npm link. npm linkproporciona enlaces simbólicos a un paquete npm local. De esa manera, puede vincular un paquete y usarlo en su proyecto principal sin publicarlo en la biblioteca de paquetes npm.

Aplicado a su caso de uso:

  1. Úselo npm linkdentro de su sharedpaquete. Esto establecerá el destino del enlace simbólico para futuras instalaciones.
  2. Navegue a su (s) proyecto (s) principal (es). Dentro de su functionspaquete y úselo npm link sharedpara vincular el paquete compartido y agregarlo al node_modulesdirectorio.

Aquí hay un enlace a la documentación: https://docs.npmjs.com/cli/link.html

Friedow
fuente
Hasta donde sé, el enlace npm es solo para pruebas y no funciona si desea implementar el código resultante (por ejemplo, mis funciones).
MauriceNino
Ya veo, probablemente deberías agregar este requisito a tu pregunta.
Friedow
Ya se menciona en la pregunta, pero lo aclararé.
MauriceNino