Usando um módulo de nó compartilhado para classes comuns

15

Objetivo

Então, eu estou tendo um projeto com esta estrutura:

  • ionic-app
  • firebase-functions
  • compartilhado

O objetivo é definir interfaces e classes comuns no sharedmódulo.

Restrições

Não quero fazer upload do meu código para o npm para usá-lo localmente e não pretendo fazer o upload do código. Deve 100% funcionar offline.

Enquanto o processo de desenvolvimento deve funcionar offline, os módulos ionic-appe firebase-functionsserão implantados na firebase (hospedagem e funções). Portanto, o código do sharedmódulo deve estar disponível lá.

O que eu tentei até agora

  • Eu tentei usar as referências do projeto em texto datilografado, mas não cheguei nem perto de trabalhar
  • Eu tentei instalá-lo como um módulo npm, como na segunda resposta desta pergunta
    • Parece estar funcionando bem no começo, mas durante a compilação, recebo um erro como este ao executar 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)

Questão

Você tem uma solução para criar um módulo compartilhado usando a configuração de scripts de texto ou o NPM?

Por favor, não marque isso como duplicado → Tentei qualquer solução encontrada no StackOverflow.

Informação adicional

Configuração para compartilhada:

// 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"
  }
}

Configuração para funções:

// 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"
  }
}

Alma atual

Eu adicionei um script npm ao módulo compartilhado, que copia todos os arquivos (sem o index.js) para os outros módulos. Isso tem o problema: eu verifico o código duplicado no SCM e preciso executar esse comando em todas as alterações. Além disso, o IDE apenas o trata como arquivos diferentes.

MauriceNino
fonte

Respostas:

4

Prefácio: Não estou muito familiarizado com o funcionamento da compilação Typescript e como package.jsondeve ser definido nesse módulo. Essa solução, embora funcione, pode ser considerada uma maneira hacky de realizar a tarefa em questão.

Supondo a seguinte estrutura de diretórios:

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

Ao implantar um serviço Firebase, você pode anexar comandos aos ganchos pré - implantar e pós-implantar . Isso é feito firebase.jsonatravés das propriedades predeploye postdeployno serviço desejado. Essas propriedades contêm uma matriz de comandos seqüenciais para executar antes e depois da implantação do código, respectivamente. Além disso, esses comandos são chamados com as variáveis ​​de ambiente RESOURCE_DIR(o caminho do diretório ./functionsou ./ionic-app, o que for aplicável) e PROJECT_DIR(o caminho do diretório que contém firebase.json).

Usando a predeploymatriz for functionsinside firebase.json, podemos copiar o código da biblioteca compartilhada na pasta implantada na instância do Cloud Functions. Ao fazer isso, você pode simplesmente incluir o código compartilhado como se fosse uma biblioteca localizada em uma subpasta ou você pode mapeá-lo de nome usando mapeamento de caminho de Typescript em tsconfig.jsonum módulo chamado (assim você pode usar import { hiThere } from 'shared';).

A predeploydefinição de gancho (usa a instalação global de shxpara compatibilidade com o 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",
    ...
  }
}

Vinculando a fonte datilografada da biblioteca copiada à configuração do compilador datilografado das funções:

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

Associando o nome do módulo, "compartilhado", à pasta do pacote da 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
}

A mesma abordagem pode ser usada com a pasta de hospedagem.


Espero que isso inspire alguém que esteja mais familiarizado com a compilação Typescript a criar uma solução mais limpa que faça uso desses ganchos.

samthecodingman
fonte
3

Você pode experimentar o Lerna , uma ferramenta para gerenciar projetos JavaScript (e TypeScript) com vários pacotes.

Configuração

Supondo que seu projeto tenha a seguinte estrutura de diretórios:

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

Certifique-se de especificar o nível de acesso correto ( privatee config/accesschaves) em todos os módulos que você não deseja publicar, bem como a typingsentrada em seu sharedmódulo:

Compartilhado:

{
  "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"
  }
}

Ionic-app:

{
  "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"
  }
}

Com as alterações acima, você pode criar um nível de raiz no package.jsonqual pode especificar qualquer um devDependenciesao qual deseja que todos os seus módulos de projeto tenham acesso, como sua estrutura de teste de unidade, tslint etc.

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

Você também pode usar esse nível raiz package.jsonpara definir scripts npm que invocam os scripts correspondentes nos módulos do seu projeto (via 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"
  },
}

Com isso, adicione o arquivo de configuração lerna no diretório raiz:

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

com o seguinte conteúdo:

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

Agora, quando você executar npm installno diretório raiz, o postinstallscript definido em seu nível raiz package.jsonserá chamado lerna bootstrap.

O que lerna bootstrapfaz é que ele irá vincular seu sharedmódulo ao link ionic-app/node_modules/sharede firebase-functions/node_modules/shared, portanto, do ponto desses dois módulos, se sharedparece com qualquer outro módulo npm.

Compilação

Obviamente, não é suficiente desvincular os módulos, pois você ainda precisa compilá-los do TypeScript para o JavaScript.

É aí que o package.json compilescript no nível raiz entra em cena.

Quando você executa npm run compilena raiz do projeto, o npm chama lerna run compile --streame lerna run compile --streamchama o script chamado compileno package.jsonarquivo de cada módulo .

Como agora cada um de seus módulos possui seu próprio compilescript, você deve ter um tsonfig.jsonarquivo por módulo. Se você não gostar da duplicação, poderá se safar de um tsconfig no nível raiz ou uma combinação de arquivos tsconfig no nível raiz e tsconfig no nível do módulo herdados do raiz.

Se você gostaria de ver como essa configuração funciona em um projeto do mundo real, dê uma olhada no Serenity / JS, onde eu o uso bastante.

Desdobramento, desenvolvimento

A coisa agradável sobre ter o sharedmódulo simbolicamente sob node_modulessob firebase-functionse ionic-app, e sua devDepedenciessob node_modulessob raiz do projeto é que, se você precisa para implantar a qualquer módulo do consumidor (por isso o ionic-apppor exemplo), você só podia fechá-lo todos juntos com a sua node_modulese não se preocupar tendo que remover as impedâncias de desenvolvimento antes da implantação.

Espero que isto ajude!

Jan

Jan Molak
fonte
Interessante! Definitivamente vou dar uma olhada e ver se este é o ajuste certo.
MauriceNino
2

Outra solução possível, se você estiver usando o git para gerenciar seu código, está usando git submodule. Usando git submodulevocê é possível incluir outro repositório git em seu projeto.

Aplicado ao seu caso de uso:

  1. Envie a versão atual do seu repositório git compartilhado
  2. Use git submodule add <shared-git-repository-link>dentro dos seus principais projetos para vincular o repositório compartilhado.

Aqui está um link para a documentação: https://git-scm.com/docs/git-submodule

friedow
fonte
Na verdade, não é uma má idéia, mas o desenvolvimento e o teste local estão basicamente relacionados a essa abordagem.
MauriceNino
0

Se entendi seu problema corretamente, a solução é mais complexa do que uma única resposta e depende em parte da sua preferência.

Abordagem 1: cópias locais

Você pode usar o Gulp para automatizar a solução de trabalho que você já descreveu, mas o IMO não é muito fácil de manter e aumenta drasticamente a complexidade se em algum momento outro desenvolvedor entrar.

Abordagem 2: Monorepo

Você pode criar um único repositório que contém todas as três pastas e conectá-las para que elas se comportem como um único projeto. Como já foi respondido acima, você pode usar o Lerna . Isso requer um pouco de configuração, mas, uma vez concluídas, essas pastas se comportarão como um único projeto.

Abordagem 3: Componentes

Trate cada uma dessas pastas como um componente autônomo. Dê uma olhada no Bit . Isso permitirá que você configure as pastas como partes menores de um projeto maior e crie uma conta privada que terá como escopo esses componentes apenas para você. Uma vez configurado inicialmente, ele permitirá que você aplique atualizações nas pastas separadas e a pasta pai que as usa obterá as atualizações automaticamente.

Abordagem 4: Pacotes

Você disse especificamente que não deseja usar o npm, mas quero compartilhá-lo, porque atualmente estou trabalhando com uma configuração conforme descrito abaixo e está fazendo um trabalho perfeito para mim:

  1. Use npmou yarnpara criar um pacote para cada pasta (você pode criar pacotes com escopo definido para ambos, para que o código esteja disponível apenas para você, se essa for sua preocupação).
  2. Na pasta pai (que usa todas essas pastas), os pacotes criados são conectados como dependências.
  3. Eu uso o webpack para agrupar todo o código, usando aliases de caminho do webpack em combinação com caminhos de texto datilografado.

Funciona como um encanto e, quando os pacotes são vinculados ao desenvolvimento local, ele funciona totalmente offline e, na minha experiência - cada pasta é escalável separadamente e muito fácil de manter.

Nota

Os pacotes 'child' já estão pré-compilados no meu caso, pois são muito grandes e eu criei tsconfigs separados para cada pacote, mas o mais bonito é que você pode alterá-lo facilmente. No passado, eu usei texto datilografado no módulo e arquivos compilados, e também arquivos js brutos, então a coisa toda é muito, muito versátil.

Espero que isto ajude

***** ATUALIZAÇÃO **** Para continuar no ponto 4: peço desculpas, meu mal. Talvez eu tenha entendido errado, porque, até onde eu sei, você não pode vincular um módulo se ele não for carregado. No entanto, aqui está:

  1. Você tem um módulo npm separado, vamos usar firebase-functionspara isso. Você o compila ou usa ts brutos, dependendo da sua preferência.
  2. No seu projeto pai, adicione firebase-functionscomo uma dependência.
  3. Em tsconfig.json, adicione"paths": {"firebase-functions: ['node_modules/firebase-functions']"}
  4. No webpack - resolve: {extensions: ['ts', 'js'], alias: 'firebase-functions': }

Dessa forma, você faz referência a todas as suas funções exportadas do firebase-functionsmódulo simplesmente usando import { Something } from 'firebase-functions'. O Webpack e o TypeScript o vincularão à pasta de módulos do nó. Com essa configuração, o projeto pai não se importará se o firebase-functionsmódulo estiver escrito em TypeScript ou javascript vanilla.

Uma vez configurado, ele funcionará perfeitamente para a produção. Em seguida, para vincular e trabalhar offline:

  1. Navegue para firebase-functionsprojetar e escrever npm link. Ele criará um link simbólico local para sua máquina e mapeará o link com o nome que você definiu em package.json.
  2. Navegue até o projeto pai e escreva npm link firebase-functions, o que criará o link simbólico e mapeie a dependência das funções do firebase para a pasta em que você o criou.
Ivan Dzhurov
fonte
Eu acho que você entendeu algo errado. Eu nunca disse que não quero usar o npm. De fato, todos os três desses módulos são módulos de nós. Acabei de dizer que não quero enviar meus módulos para o npm. Você pode elaborar um pouco mais essa quarta parte - isso parece interessante? talvez fornecer uma amostra de código?
MauriceNino
Vou acrescentar outra resposta, pois será longo e ilegível como comentário
Ivan Dzhurov 15/11/19
Atualizei a minha resposta inicial, espero que seja mais clara
Ivan Dzhurov
0

Não quero fazer upload do meu código para o npm para usá-lo localmente e não pretendo fazer o upload do código. Deve 100% funcionar offline.

Todos os módulos npm são instalados localmente e sempre funcionam offline, mas se você não deseja publicar seus pacotes publicamente para que as pessoas possam vê-lo, é possível instalar o registro privado do npm.

O ProGet é um servidor de repositório privado NuGet / Npm disponível para janelas que você pode usar em seu ambiente privado de desenvolvimento / produção para hospedar, acessar e publicar seus pacotes privados. Embora seja no Windows, mas tenho certeza que existem várias alternativas disponíveis no Linux.

  1. Git Submodules é uma péssima idéia, é realmente uma maneira antiga de compartilhar código que não é versionado como os pacotes, alterar e confirmar submodules são uma verdadeira dor de cabeça.
  2. A pasta de importação de origem também é uma má idéia, novamente o problema de controle de versão, porque se alguém modificar a pasta dependente no repositório dependente, rastrear novamente é um pesadelo.
  3. Quaisquer ferramentas de script de terceiros para simular a separação de pacotes são perda de tempo, já que o npm já oferece diversas ferramentas para gerenciar pacotes tão bem.

Aqui está o nosso cenário de construção / implantação.

  1. Todo pacote privado possui o .npmrcque contém registry=https://private-npm-repository.
  2. Publicamos todos os nossos pacotes particulares em nosso repositório ProGet hospedado em particular.
  3. Todo pacote privado contém pacotes particulares dependentes no ProGet.
  4. Nosso servidor de build acessa o ProGet por meio da autenticação npm definida por nós. Ninguém fora da nossa rede tem acesso a este repositório.
  5. Nosso servidor de criação cria um pacote npm com o bundled dependenciesqual contém todos os pacotes internos node_modulese o servidor de produção nunca precisa acessar os pacotes NPM ou NPM privados, pois todos os pacotes necessários já estão empacotados.

O uso do repositório npm privado tem várias vantagens,

  1. Não há necessidade de script personalizado
  2. Encaixa no pipid buid / publicar nó
  3. Cada pacote npm privado conterá um link direto para seu controle de fonte git privado, fácil de depurar e investigar erros no futuro
  4. Cada pacote é um instantâneo somente leitura, portanto, uma vez publicado, não pode ser modificado e, enquanto você cria novos recursos, a base de código existente com a versão mais antiga dos pacotes dependentes não será afetada.
  5. Você pode facilmente tornar alguns pacotes públicos e mudar para outro repositório no futuro
  6. Se o software do provedor npm privado mudar, por exemplo, você decide mover seu código para a nuvem de registro do pacote npm privado do nó, não será necessário fazer alterações no código.
Akash Kava
fonte
Isso pode ser uma solução, mas infelizmente não é para mim. Obrigado pelo seu tempo!
MauriceNino 18/11/19
Há também um repositório npm local, que é instalado como um pequeno servidor de nó, verdaccio.org
Akash Kava
-1

A ferramenta que você está procurando é npm link. npm linkfornece links simbólicos para um pacote npm local. Dessa forma, você pode vincular um pacote e usá-lo em seu projeto principal sem publicá-lo na biblioteca de pacotes npm.

Aplicado ao seu caso de uso:

  1. Use npm linkdentro da sua sharedembalagem. Isso definirá o destino do link simbólico para instalações futuras.
  2. Navegue para o (s) seu (s) projeto (s) principal (s). Dentro do seu functionspacote e use npm link sharedpara vincular o pacote compartilhado e adicioná-lo ao node_modulesdiretório

Aqui está um link para a documentação: https://docs.npmjs.com/cli/link.html

friedow
fonte
Até onde eu sei, o link npm é apenas para teste e não funciona se você deseja implantar o código resultante (por exemplo, minhas funções).
MauriceNino 13/11/19
Entendo, você provavelmente deve adicionar esse requisito à sua pergunta.
Friedow 13/11/19
Já foi mencionado na pergunta, mas vou esclarecer.
MauriceNino 13/11/19