Using Environment Variables in Angular Node Package - node.js

I am creating an angular 4 node package for a logging service that we have created. Is it possible to use the angular cli environment variables to set the default path for the service? If so, how should the path be defined for the environment file?
I am following these instructions for creating the node package:
https://medium.com/#cyrilletuzi/how-to-build-and-publish-an-angular-module-7ad19c0b4464
I followed these examples for setting up environment variables:
http://tattoocoder.com/angular-cli-using-the-environment-option/
https://medium.com/beautiful-angular/angular-2-and-environment-variables-59c57ba643be

You can do that using the forRoot methodology explained in the Angular docs here.
Inside of your external Angular library modify the main exported module to have this structure:
export class MyLibraryModule {
static forRoot(config: configData): ModuleWithProviders {
return {
ngModule: MyLibraryModule,
providers: [
{provide: AppConfigModel, useValue: config }
]
};
}
constructor(#Optional() config: AppConfigModel) {
if (config) { /* Save config data */ }
}
}
export class AppConfigModel {
defaultPath: string;
}
When you import this module into your application, use the forRoot static method and pass in the config data.
imports: [
MyLibraryModule.forRoot({ defaultPath: environment.defaultPath }),
]
On a side note, I use this library for creating my Angular npm modules. It simplifies the process quite a bit.

Related

Nestjs common repo dependency in conflicts with Project repo dependency

I'm working with a home grown mono repo structure in with NestJS and legacy code. The NestJS parts of the monorepo depend on a common folder in the root that is imported into each Nest Project via "commonPackage":"file:../common" in the package.json file.
The issue I'm experiencing is that the common folders install of #nestjs/config is conflicting with the consuming project's install of the same package. I've been using a workaround to import the necessary code from commonPackage/node_modules/#nestjs/config however that is using the common folder's .env file instead of the consuming project's .env
I have no runtime dependencies in the common package, and I've set #nestjs/config as a peer dependency with a version flag of ^1 however, when attempting to import the consuming project's config
(i.e. import {ConfigService} from '#nestjs/config'; and not the above) service I get an error about an internal property not matching in the spec like below.
src/app.module.ts:16:26 - error TS2345: Argument of type '(config: ConfigService) => ConnectionOptions' is not assignable to parameter of type '(config: ConfigService<Record<string, unknown>>) => ConnectionOptions'.
Types of parameters 'config' and 'config' are incompatible.
Type 'ConfigService<Record<string, unknown>>' is not assignable to type 'ConfigService<Record<string, unknown>, false>'.
Types have separate declarations of a private property 'internalConfig'.
16 MysqlModule.register(sqlConfig),
~~~~~~~~~
[3:47:23 PM] Found 1 error. Watching for file changes.
The workaround solution I worked out was to simply export the config service I was using for my internal module, however I now think it should be possible to pass in the config service when registering the module.
so My current solution is to export the config service from the module that uses it in the common repo:
export declare type SpecConfig = ConfigService;
Use this when defining factories that get put into that module.
*** One caveat is that you will not be able to specify a custom config file with this method as the import is handled in your register logic.***
another option is to add the config service as a dependency to the module registration, but I have yet to work that out.
for example this is my Dynamic Module that was causing the issue.
import { DynamicModule, Module, Provider } from '#nestjs/common';
import { ConfigModule, ConfigService } from '#nestjs/config';
import * as mysql from 'mysql2';
import { MysqlConnectionService } from './mysql-connection/mysql-connection.service';
#Module({})
export class MysqlModule {
static register(...options: MysqlModuleOptions[]): DynamicModule {
const providers: Provider<any>[] = options.map(({config: connectionConfig, name}) => {
const config = {
provide: `${name}-mysql-config`,
useFactory: connectionConfig,
inject: [ConfigService]
}
const provider = {
provide: `${name}-mysql`,
useFactory: (config: mysql.ConnectionOptions) => new MysqlConnectionService(config),
inject: [`${name}-mysql-config`],
}
return [config, provider];
}).reduce((acc, val) => acc.concat(val), [])
return {
module: MysqlModule,
imports: [ConfigModule.forRoot({isGlobal: true})],
providers: [...providers],
exports: [...providers]
}
}
}
export type MysqlConfigFunc = (config: ConfigService) => mysql.ConnectionOptions
export type MsqlConfigService = ConfigService;
export class MysqlModuleOptions {
name: string;
config: MysqlConfigFunc;
}

The injectable 'PlatformLocation' needs to be compiled using the JIT compiler, but '#angular/compiler' is not available

My Angular application is served via Node 16.13.0. After updating to Angular 13, I'm receiving the following error:
JIT compilation failed for injectable [class PlatformLocation]
file:///Users/btaylor/work/angular-apps/dz-outages-ui/node_modules/#angular/core/fesm2015/core.mjs:4058
throw new Error(message);
^
Error: The injectable 'PlatformLocation' needs to be compiled using the JIT compiler, but '#angular/compiler' is not available.
The injectable is part of a library that has been partially compiled.
However, the Angular Linker has not processed the library such that JIT compilation is used as fallback.
Ideally, the library is processed using the Angular Linker to become fully AOT compiled.
Alternatively, the JIT compiler should be loaded by bootstrapping using '#angular/platform-browser-dynamic' or '#angular/platform-server',
or manually provide the compiler with 'import "#angular/compiler";' before bootstrapping.
at getCompilerFacade (file:///Users/btaylor/work/angular-apps/dz-outages-ui/node_modules/#angular/core/fesm2015/core.mjs:4058:15)
at Module.ɵɵngDeclareFactory (file:///Users/btaylor/work/angular-apps/dz-outages-ui/node_modules/#angular/core/fesm2015/core.mjs:32999:22)
at file:///Users/btaylor/work/angular-apps/dz-outages-ui/node_modules/#angular/common/fesm2015/common.mjs:90:28
at ModuleJob.run (node:internal/modules/esm/module_job:185:25)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:281:24)
at async loadESM (node:internal/process/esm_loader:88:5)
at async handleMainPromise (node:internal/modules/run_main:65:12)
I have tried numerous solutions, such as: Angular JIT compilation failed: '#angular/compiler' not loaded
Currently, I have "type": "module" in my package.json
I have updated my postinstall command to: ngcc --properties es2020 browser module main --first-only --create-ivy-entry-points
I also added import '#angular/compiler'; to my main.ts file.
The project will compile, but won't run via Node.
I believe I have found the solution (presuming you are using jest as your test runner). In the test-setup.ts file my project still was using the outdated import for jest-preset-angular. Instead of import 'jest-preset-angular'; try using import 'jest-preset-angular/setup-jest';.
This addressed the issue for me.
It seems angular 13 made babel-loader a mandatory requirement to link Ivy-native packages. https://github.com/angular/angular/issues/44026#issuecomment-974137408 - and the issue is happening as core Angular libraries are already compiled with the Ivy package format.
I haven't yet made the change in my own projects, but processing .mjs files with babel and a special linker should be enough.
import { dynamicImport } from 'tsimportlib';
/**
* Webpack configuration
*
* See: http://webpack.github.io/docs/configuration.html#cli
*/
export default async (options: IWebpackOptions) => {
const linkerPlugin = await dynamicImport('#angular/compiler-cli/linker/babel', module);
const config: any = {
module: {
rules: [{
test: /\.mjs$/,
loader: 'babel-loader',
options: {
compact: false,
plugins: [linkerPlugin.default],
},
resolve: {
fullySpecified: false
}
}
}
}
}
I'll make more updated to this post once I am able to test it myself.
If I correctly understand, you use Angular Universal also.
I want to share my experience with the same problem which came to my project after updating Angular from v8 to v13.
First of all, I want to say that the main problem was with the incorrect started file for SSR. In my project, it was server.js and now it's main.js.
So, maybe your project also tries to start from the incorrect file which doesn't have the necessary code to start it.
More details:
I updated the project step by step, increasing the version as recommended by the Angular team and according to https://update.angular.io/?l=3&v=8.2-13.0
Unfortunately, at every step, I only checked the version of SPA without SSR and only compiled the project to check that all is OK, but didn't start it with SSR. Now I can't say when the problem with the JIT compiler started.
And when I finished updating and fixing bugs with SPA, compiled the project with SSR, and tried to start it I saw this problem:
Error: The injectable 'PlatformLocation' needs to be compiled using the JIT compiler, but '#angular/compiler' is not available.
I tried different solutions like you, but nothing helped. After that, I created a new clean project with ng13 and added Angular Universal. And compare my project with new, generated by Angular-CLI.
What I changed in my project(sorry, I can't show the project - NDA):
angular.json
"projects": {
"projectName": {
...
"architect": {
...
"server": {
...
"options": {
"outputPath": "dist/server",
//"main": "src/main.server.ts", //removed
"main": "server.ts", //added
...
}
}
}
}
}
package.json
"scripts": {
...
//"serve:production:ssr": "node dist/server --production" // removed. It was incorrect file server.js which gave the error from this case
"serve:production:ssr": "node dist/server/main --production", // added
...
"postinstall": "ngcc --properties es5 browser module main --first-only" // added. In my case, actual version is es5
}
3.server.ts
import 'zone.js/node';
import { ngExpressEngine } from '#nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
...
// after all imports I inserted previous code into function run()
...
export function run() {
// previous code from project with some small changes
}
//and added next code from clean project:
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from '../src/main.server';
src/main.server.ts
// was only:
export { AppServerModule } from './app/app.server.module';
// replaced by code from clean project:
/***************************************************************************************************
* Initialize the server environment - for example, adding DOM built-in types to the global scope.
*
* NOTE:
* This import must come before any imports (direct or transitive) that rely on DOM built-ins being
* available, such as `#angular/elements`.
*/
import '#angular/platform-server/init';
import { enableProdMode } from '#angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
export { renderModule, renderModuleFactory } from '#angular/platform-server';
src/main.ts
...
//this part of code
document.addEventListener("DOMContentLoaded", () => {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.log(err));
});
// replaced by
function bootstrap() {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
};
if (document.readyState === 'complete') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}
//but I don't think it plays a role in solving the JIT problem. Wrote just in case
tsconfig.server.json
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/app-server",
"module": "commonjs",// it's only in my case, I don't have time for rewrote server.ts now
"types": ["node"],
},
"files": [
"src/main.server.ts",
"server.ts"
],
"include":["server/**/*.ts","node/*.ts"],
"angularCompilerOptions": {
"entryModule": "./src/app/app.server.module#AppServerModule"
}
}
Ok, I think that's all. After all these edits I don't have file /dist/server.js and start the project from /dist/server/main.js without error from this case.
N.B.: When I was updating the project step-by-step I noticed that the process of updating nguniversal by Angular-CLI didn't change anything in the project. Only the version of packages. That's why I recommend comparing your project with an actual clean project manually

Failed to extend node.js global with typescript

I am trying to extend the global in node.js, using typescript.
For this, I've created the following structure:
File content of index.d.ts:
import socketio from 'socket.io';
declare namespace NodeJS {
interface Global {
sockets: socketio.Socket[];
}
}
In my tsconfig.json file I provided: "typeRoots": [ "./node_modules/#types", "./#types"]
But I still can't use global.sockets. How to fix it?
Please note that if I would write index.d.ts as:
declare namespace NodeJS {
interface Global {
sockets: any[];
}
}
Then I would be able to access global.sockets
From this stackoverflow question: Ques
Turns out that adding export {} and declare global would do the work.
So the final file would be:
export {};
import socketio from 'socket.io';
declare global {
declare namespace NodeJS {
interface Global {
sockets: socketio.Socket[];
}
}
}

How to use TypeScript in a Custom Test Environment file in Jest?

I need to enable some global variables to be reachable for my test so I am setting up a Custom Environment to be used in the testEnvironment option in my jest.config.json to achieve that.
For our project we have a TypeScript file that we use for setupFilesAfterEnv option and that works just fine, however the testEnvironment seems to support only ES5. Is there any way to use TypeScript in such option?
I successfully created a Custom Jest Environment using ES5 syntax, however since we are injecting global variables I need TypeScript to also declare a global namespace see: https://stackoverflow.com/a/42304473/4655076.
{
///...
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'], // This works with ts
testEnvironment: '<rootDir>/test/customJestEnvironment.ts', // This doesn't work with ts
}
You might find this helpful: Configure Jest global tests setup with .ts file (TypeScript)
But basically you can only pass in compiled JS files as environments.
You can do what that article suggests. But it didn't work for me out of the box. So I manually compile my env.
i.e.
in package.json
"test": "tsc --lib es6 --target es6 --skipLibCheck -m commonjs --esModuleInterop true path/to/env.ts &&
jest --config=jest.config.js",
And in jest.config.js
{
testEnvironment: '<rootDir>/path/to/env.js', // Note JS extension.
}
I solved this by using ts-node and the following command:
node -r ts-node/register ./node_modules/jest/bin/jest.js
This essentially compiles the typescript on-the-fly, so that jest receives the emitted javascript, without the need of actually compiling your typescript sources to js.
You will need to enable esModuleInterop TS compiler option for this to work properly.
TestEnvironment.ts
import NodeEnvironment from 'jest-environment-node';
import type {Config} from '#jest/types';
class TestEnvironment extends NodeEnvironment {
constructor(config: Config.ProjectConfig) {
super(config);
// this.testPath = context.testPath;
// this.docblockPragmas = context.docblockPragmas;
}
public async setup(): Promise<void> {
await super.setup();
console.log('SETTING UP...');
// await someSetupTasks(this.testPath);
// this.global.someGlobalObject = createGlobalObject();
// // Will trigger if docblock contains #my-custom-pragma my-pragma-value
// if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') {
// // ...
// }
}
public async teardown(): Promise<void> {
await super.teardown();
console.log('TEARING DOWN!');
// this.global.someGlobalObject = destroyGlobalObject();
// await someTeardownTasks();
}
}
export default TestEnvironment;
This solution however, will break globalSetup -- if you use jest-ts.
As you might know, typescript files are just superset to javascript to provide strong type checking. Jest's engine/runtime however expects your files in CommonJS format javascript files.
You can have a separate tsconfig.env.json just for this env.ts. Compile this before running jest test and use the compiled env.js in your jest.config.js.
tsc -p tsconfig.env.json && jest
Also i have never seen people writing configuration files in TS.
why CommonJS ? because jest is essentially running on top of node. node supports Javascript files in CommonJS format. Node has started supporting es modules as well recently! This is a big thing!
You can create a global.d.ts file at the root of your project.
Then you can define global variables as seen below. In my case, it was a NestJS application, but you can define anything.
declare global {
namespace NodeJS {
interface Global {
app: INestApplication;
}
}
}
This is another example for client project where we define window properties like innerWidth;
declare namespace NodeJS {
interface Global {
innerWidth: number;
dispatchEvent: Function;
}
}
Inside your .d.ts definition file:
type MyGlobalFunctionType = (name: string) => void
Add members to the browser's window context:
interface Window {
myGlobalFunction: MyGlobalFunctionType
}
Same for NodeJS:
declare module NodeJS {
interface Global {
myGlobalFunction: MyGlobalFunctionType
}
}
Now you declare the root variable
declare const myGlobalFunction: MyGlobalFunctionType;
Then in a regular .ts file, but imported as side-effect, you actually implement it:
global/* or window */.myGlobalFunction = function (name: string) {
console.log("Hey !", name);
};
And finally use it elsewhere :
global/* or window */.myGlobalFunction("Ayush");
myGlobalFunction("Ayush");

How to bundle an environment dependant package?

problem
Currently my package is developed in a way where to import it you need to decide on your target. So:
import * as myLib from 'mylib/node' // if i want to use the node implementation
import * as myLib from 'mylib/web' // if i want to use the web implementation
The functionality is identical, the implementation differs tho because they use different APIs. I want to move to a single import that will work for node and web. To do that i changed my code to detect whether its running in node or the web. This alows me to import it like this:
import * as myLib from 'mylib'
Which works. However when i go to bundle some code using mylib with webpack (as web target) it goes bonkers as it tries to bundle the nodejs implementation (fails on bundling packages like worker_threads)
webpack.config.ts:
import * as path from 'path'
import { Configuration } from 'webpack'
const config: Configuration = {
entry: './dist/index.js',
target: 'web',
output: {
filename: 'mylib.web.js',
path: path.resolve(__dirname, 'dist'),
library: 'mylib',
libraryTarget: 'umd'
},
mode: 'production'
}
export default config
question
How can i bundle such a package or write my package in a way to support both node and web and bundle correctly.
edits
To specify i merged web and node implementation in such manner:
Before:
// mylib/node
export const func = () => {
// using worker_threads here
}
// mylib/web
export const func = () => {
// using web worker here
}
After:
// mylib
export const func = () => {
if(/* am i in node test */) {
// execute the worker_threads implementation
} else {
// execute the web workers implementation
}
}

Resources