Goal
So I am having a project with this structure:
The goal is to define common interfaces and classes in the shared
module.
Restrictions
I don't want to upload my code to npm to use it locally and am not planning on uploading the code at all. It should 100% work offline.
While the development process should work offline, the ionic-app
and firebase-functions
modules are going to be deployed to firebase (hosting & functions). Therefore, the code from the shared
module should be available there.
What I have tried so far
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)
Question
Do you have a solution for making a shared module using either typescripts config, or NPM?
Please do not mark this as a duplicate → I have tried any solution I have found on StackOverflow.
Additional Info
Config for shared:
// 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"
}
}
Config for functions:
// 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"
}
}
Current soution
I have added a npm script to the shared module, which copies all files (without the index.js) to the other modules. This has the problem, that I check in duplicate code into SCM, and that I need to run that command on every change. Also, the IDE just treats it as different files.
Option 1: Link to a Local Project Folder Once you've moved your shared code into a separate project, link the project as a dependency using npm link. Note: The shared library can be maintained in a separate repository or the same repository as your other projects (a.k.a, monorepo).
Yes you can copy whole node_modules (have done it multiple times) from one project to another and use same package.
We do not need to install a module every time when installed globally. It takes less memory as only one copy is installed. We can make . js scripts and run them anywhere without having a node_modules folder in the same directory when packages are installed globally.
Preface: I'm not too familiar with how Typescript compilation works and how package.json
in such a module should be defined. This solution, although it works, could be considered a hacky way of achieving the task at hand.
Assuming the following directory structure:
project/
ionic-app/
package.json
functions/
src/
index.ts
lib/
index.js
package.json
shared/
src/
shared.ts
lib/
shared.js
package.json
When deploying a Firebase service, you can attach commands to the predeploy and postdeploy hooks. This is done in firebase.json
via the properties predeploy
and postdeploy
on the desired service. These properties contain an array of sequential commands to run before and after deploying your code respectively. Furthermore, these commands are called with the environment variables RESOURCE_DIR
(the directory path of ./functions
or ./ionic-app
, whichever is applicable) and PROJECT_DIR
(the directory path containing firebase.json
).
Using the predeploy
array for functions
inside firebase.json
, we can copy the shared library's code into the folder that is deployed to the Cloud Functions instance. By doing this, you can simply include the shared code as if it were a library located in a subfolder or you can map it's name using Typescript's path mapping in tsconfig.json
to a named module (so you can use import { hiThere } from 'shared';
).
The predeploy
hook definition (uses global install of shx
for Windows compatibility):
// 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",
...
}
}
Linking the copied library's typescript source to the functions typescript compiler config:
// functions/tsconfig.json
{
"compilerOptions": {
...,
"baseUrl": "./src",
"paths": {
"shared": ["shared/src"]
}
},
"include": [
"src"
],
...
}
Associating the module name, "shared", to the copied library's package folder.
// 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
}
The same approach can be used with the hosting folder.
You might want to try Lerna, a tool for managing JavaScript (and TypeScript) projects with multiple packages.
Assuming that your project has the following directory structure:
packages
ionic-app
package.json
firebase-functions
package.json
shared
package.json
Make sure to specify the correct access level (private
and config/access
keys) in all the modules you don't wish to have published, as well as the typings
entry in your shared
module:
Shared:
{
"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"
}
}
With the above changes in place, you can create a root-level package.json
where you can specify any devDependencies
that you wish all your project modules to have access to, such as your unit testing framework, tslint, etc.
packages
ionic-app
package.json
firebase-functions
package.json
shared
package.json
package.json // root-level, same as the `packages` dir
You can also use this root-level package.json
to define npm scripts that will invoke the corresponding scripts in your project's modules (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"
},
}
With that in place, add the lerna config file in your root directory:
packages
ionic-app
package.json
firebase-functions
package.json
shared
package.json
package.json
lerna.json
with following contents:
{
"lerna": "3.18.4",
"loglevel": "info",
"packages": [
"packages/*"
],
"version": "1.0.0"
}
Now when you run npm install
in the root directory, the postinstall
script defined in your root-level package.json
will invoke lerna bootstrap
.
What lerna bootstrap
does is that it will symlink your shared
module to ionic-app/node_modules/shared
and firebase-functions/node_modules/shared
, so from the point of those two modules, shared
looks just like any other npm module.
Of course, symlinking the modules is not enough as you still need to compile them from TypeScript to JavaScript.
That's where the root-level package.json
compile
script comes into play.
When you run npm run compile
in your project root, npm will invoke lerna run compile --stream
, and lerna run compile --stream
invokes the script called compile
in each of your modules' package.json
file.
Since each of your modules now has its own compile
script, you should can have a tsonfig.json
file per module. If you don't like the duplication you could get away with a root-level tsconfig, or a combination of a root-level tsconfig and module-level tsconfig files inheriting from the root one.
If you'd like to see how this setup works on a real-world project, have a look at Serenity/JS where I've been using it quite extensively.
The nice thing about having the shared
module symlinked under node_modules
under firebase-functions
and ionic-app
, and your devDepedencies
under node_modules
under project root is that if you need to deploy the consumer module anywhere (so the ionic-app
for example), you could just zip it all up together with its node_modules
and not worry about having to remove the dev depedencies prior to deployment.
Hope this helps!
Jan
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With