Testing TypeScript modules with package dependencies - javascript

I have a class written in TypeScript:
import * as uuid from "uuid";
export class IdGenerator {
getId() {
return uuid.v4();
}
}
This has a dependency on the uuid package that I have installed with npm.
I deploy this with other code written in TypeScript to the browser using webpack. Dependencies are resolved, all is well.
I want to test the class, so I write the following test:
import { IdGenerator } from "../src/IdGenerator";
describe("An id generator", () => {
const idGenerator = new IdGenerator();
it("generates an id", () => {
expect(idGenerator.getId()).not.toBeNull();
});
});
I use Chutzpah with the following configuration:
{
"Framework": "jasmine",
"TestHarnessReferenceMode": "AMD",
"TestHarnessLocationMode": "SettingsFileAdjacent",
"EnableTestFileBatching": true,
"References": [
{
"Path": "node_modules/requirejs/require.js",
"IsTestFrameworkFile": true
}
],
"Compile": {
"Mode": "External",
"Extensions": [ ".ts" ],
"ExtensionsWithNoOutput": [ ".d.ts" ]
},
"Tests": [
{ "Path": "Components/test" }
]
}
When trying to test with Chutzpah, I get the following error:
Unhandled exception [...] in [...] /node_modules/requirejs/require.js [...] Script error for "uuid", needed by: Components/src/IdGenerator
So I add a reference to the path in chutzpah.json, which gets rid of this error but raises an error for one of uuid's dependencies. I add a reference for the dependency, then get a further dependency error, and so on.
Ideally I would like all dependencies to be resolved in the same way as they are with the bundle deployed to the browser.
Should I abandon the idea of trying to test my TypeScript files with package dependencies in this way and instead look into using dependency injection and mock the package dependencies in the tested TypeScript files? Perhaps also create separate JavaScript tests for the bundle? Or is there another approach that will allow the testing of my TypeScript code with the package dependencies?

This looks like it won't work since requireJS and Node are not compatible in this way. See here. From that answer:
It is not possible to use RequireJS on the client (browser) to access files from node_modules. Files under node_modules must first be copied to a location that is accessible (under the public folder) before the client can access them. The documentation says RequireJS can access Node modules, but this only works for server-side JavaScript (when you want to use RequireJS-style module syntax in Node).
To use your config module in the client app, you must first transform it into a RequireJS-compatible module and copy it under public. This article explains how to automate this with package managers and build tools, and contains the quote that finally fixed my broken understanding of RequireJS + Node:
In order to use node modules on the browser you will need a build
tool. This might bother you. If this is a deal breaker then by all
means continue wasting your life copying and pasting code and
arranging your script tags.

Related

How can I change in Rollup.js an import module of a dependency package to a local file replacing that module?

I have a JavaScript project that must be bundled using Rollup.js which depends on a package A which in turn depends on a package B:
"mypackage" ---import--> "A" ----import----> "B"
Let's say that my package import a function "connect" from package A, which in turn import a "connect" function exported by the module B. Something like:
//mypackage index.js
import { connect } from 'A'
//A index.js
import { connect } from 'B'
//B index.js
export function connect() {}
Since my package requires a bundled version of the package B (let's say "B.bundle.js"), how can i configure Rollup.js in order to replace for each dependency of my project requiring B (A in this case) to use my local bundled version (i.e. B.bundle.js, which of course exports the "connect" function too)?
When Rollup.js creates the bundled version of my project i would like to achieve something like the following:
//A index.js after being processed by Rollup
import { connect } from './src/B.bundle.js'
Is something like this possible with Rollup or with a plugin? Sorry for the question, but I'm new to rollup and bundling in general.
I solved this issue using some configuration of my package package.json and the rollup plugin #rollup/plugin-node-resolve.
In the package.json of my package I inserted the browser option that specifies how modules should be resolved when my package is used in the browser context. From the npm doc on the browser option of the package.json:
If your module is meant to be used client-side the browser field should be used instead of the main field. This is helpful to hint users that it might rely on primitives that aren't available in Node.js modules. (e.g. window)
So considering the example provided in the original question the npm package contains something like this:
{
"name": "mypackage",
"version": "1.5.1",
"description": "A brand new package",
"main": "index.js",
"browser": {
"B": "./B.bundle.js"
},
}
This means that when mypackage is used in the context of the browser the module B import will load from the file located in "./B.bundle.js".
Now, with rollup i need to specify that the bundle i am creating is intended for browser context. The plugin that handle imports of node modules is #rollup/plugin-node-resolve. There is an option is this plugin that specify that the context is browser. From the plugin documentation about the option browser:
If true, instructs the plugin to use the browser module resolutions in package.json and adds 'browser' to exportConditions if it is not present so browser conditionals in exports are applied. If false, any browser properties in package files will be ignored. Alternatively, a value of 'browser' can be added to both the mainFields and exportConditions options, however this option takes precedence over mainFields.
So that in my rollup config file i have something like:
// rollup.config.js
import commonjs from "#rollup/plugin-commonjs";
import resolve from "#rollup/plugin-node-resolve";
import nodePolyfills from "rollup-plugin-node-polyfills";
export default {
input: "index.js",
output: {
file: "dist/mypackage.bundle.js",
format: "es",
},
plugins: [
nodePolyfills(),
resolve({
browser: true, //<- tells to rollup to use browser module resolution
}),
commonjs(),
],
},
The answer of #vrajpaljhala seems feasable for the purpose but imho #rollup/plugin-replace its kind of too tricky and raw approach to the problem because it involves direct replacmeent of strings surrounded by "". We may face some very "hard to discover" errors if the package name we are going to replace is a common word that can be present as a string in the code but not in the context of an import statement.
We had the exact same requirement and we resolved it using #rollup/plugin-replace package.
Basically, our project uses a monorepo structure but we are not using any tools like learna or workspace for managing it. So we have two packages which are interdependent. Our ui-kit uses icons package for different icons and icons package uses the Icon component from ui-kit package. We just replaced the imports for ui-kit with local path with the following:
import replace from '#rollup/plugin-replace';
...
...
export default {
...
...
plugins: [
replace({
'ui-kit': JSON.stringify('../../../../src/components/Icon'),
delimiters: ['"', '"'],
}),
]
...
...
}

TypeScript module system config vs Webpack library type

I'm working on my first TypeScript library (it's actually even my first JavaScript library) which is used in the front-end. In essence, it should expose a function which receives a DOM element and adds another DOM element to it as a child.
I would like to use Webpack to bundle the library and during configuration of it and TypeScript I stumbled across module systems. And they are confusing me.
In tsconfig.json I can define which module system should be used in the compiled code, if I understand correctly:
{
"compilerOptions": {
"module": "es6"
}
}
And in the webpack.config.js I am able to set a desired target for my library using output.library.type, where I can again specify a module system:
module.exports = {
output: {
library: {
name: 'my-lib',
type: 'umd',
}
},
I only need my library to be installable via npm/yarn:
$ yarn add my-lib
And consumable via an an import statement like that:
import { myFunc } from 'my-lib';
So far so good, with these settings it seems to do what I want. But I don't understand what I am doing here. Hence the questions: What is the difference between the two module system configuration options (the one in the TypeScript config and the one in the Webpack config)? And what settings are appropriate for my use case?

How to import a different bundle for server and client using Rollup in Sapper?

I'm creating a tool which launches a server and fetches content from the server and displays it in the browser. I'm trying to integrate it with frontend frameworks. One of those frameworks is Sapper/Svelte. The problem is that my bundle contains imports to built-in modules which are not needed by the browser, and also not resolved by the browser, which in turn throws an error.
I think what I need to do is make my tool isomorphic and split my tool it into two bundles. One for the server (server.js), and one for the browser (client.js) which doesn't contain the imports to built-in modules. I have a good idea of how I can split the code, using code splitting in Rollup, but what I don't know is how I tell Sapper to use server.js for the server and client.js for the client.
How can I bundle my module so when it's consumed by other applications it knows which one to use for the server and which one to use for the browser? Is this something I can do in my module or do I have to also configure this in the framework it's being used in?
I discovered that #rollup/plugin-node-resolve has a flag to instruct Rollup to use an alternative bundle specified in the browser property in the module's package.json.
As Sapper is configured to create a bundle for both the client and server it has this flag already in it's rollup.config.js.
Sapper
// rollup.config.js
export default {
client: {
// ...
plugins: [
// ...
resolve({
browser: true, // <-- flag
dedupe: [ 'svelte' ],
exclude: [ 'node_modules/**' ]
})
// ...
No changes needed here.
Your NPM Module
You need to create two bundles. One for the server and one for the browser. I felt it was easier to create two different entry points in Rollup for this. It might be possible to use the same entry point and use conditional logic to output a different bundle (something I'm not familiar with).
// rollup.config.js
export default [
{
input: 'src/server.js',
output: {
file: 'dist/server.js',
format: 'cjs'
}
},
{
input: 'src/browser.js',
output: {
file: 'dist/browser.js',
format: 'cjs'
}
}
];
Now we add the path to the browser specific bundle in package.json.
// package.json
{
"main": "dist/server.js"
// ...
"browser": "dist/browser.js"
}
Setting this up means that when Sapper starts it will use a different bundle for the server and the client. When you create the separate bundles you need to structure them so that they work independently of each other. In my case, I isolated all server functionality to the server-specific bundle and excluded all dependencies like http, fs and path from the browser-specific bundle.

Does ts-node support '#' style import? If so, how to set it up?

I'm creating a command-line script, using classes from the main express app.
Script resides in the folder:
bin/utils/
├── sync-buyers.ts
└── tsconfig.json
The main express app is in /app use uses import '#/foo/bar/thing.
This is set up in the tsconfig.json of the main app, as follows:
"paths": {
"#/*": ["*"],
"*": [
"node_modules/*",
"app/typings/*"
]
}
},
"include": ["app/**/*", "test/**/*"],
"exclude": ["app/**/*.test.ts", "/__tests__/", "/__mocks__/", "/__snapshots__/", "app/**/__mocks__/"],
"files": ["typings/global.d.ts"]
Script Execution
I'm testing to see if I can import from the main app, so I created a sayHello() function.
#!/usr/bin/env ts-node
/* tslint:disable */
import { sayHello } from '../../app/services/v2/oapp';
sayHello();
When I run it:
TSError: ⨯ Unable to compile TypeScript:
../../app/services/v2/oapp.ts(9,19): error TS2307: Cannot find module
'#/helpers/fetch'.
../../app/services/v2/oapp.ts(10,31): error TS2307: Cannot find module
'#/services/v2/buyer'.
../../app/services/v2/oapp.ts(11,51): error TS2307: Cannot find module
'#/http/HttpHeader'.
Summary:
Does ts-node support '#' style of import? If so, how do I set it up?
So the TypeScript paths configuration only applies to TypeScript's type resolution and checking, meaning that it will allow TypeScript to understand those imports for the purposes of type-checking only, but the code it generates won't automatically rewrite those imports to the correct locations.
There's two common approaches for solving this:
Update the Node resolver to understand the TypeScript paths config. The generated files will still refer to those paths by their #-name.
Most commonly, the tsconfig-paths module is used for this. You can require that module from the node command directly:
node -r tsconfig-paths/register main.js
Rewrite the generated files so that the #-names get replaced with the "real" local relative path locations.
There's a standalone module for this, tspath - you simply run tspath after compiling your TypeScript, and it updates the generated files with the correct paths.
If you're using Webpack, you can also use tsconfig-paths-webpack-plugin, which will take care of configuring Webpack's resolver to correctly locate those #-name paths.
And finally if you're using Babel, you might be interested in babel-plugin-module-resolver which does a similar thing for the Babel toolchain, however the downside here is it doesn't read the paths config from tsconfig.json, so you essentially have to duplicate your paths config in the alias config of this plugin.
Personally I'd recommend tsconfig-paths if this is a Node script or server that's compiled with tsc directly and tsconfig-paths-webpack-plugin if this is a frontend Webpack build.

How VSCode injects "vscode" engine into the extensions?

When developing extensions for VSCode. We see this import:
import * as vscode from 'vscode';
and in package.json, we have
"engines": {
"vscode": "*"
}
Nowhere in the dependencies we have 'vscode'. But, looks like it is available for extension. Any explanation would be appreciated.
Imports are resolved by the host environment, in this case VSCode's possibly-modified version of Electron. So when it sees a request for the vscode module, it provides it (internally) rather than looking for an external dependency.
FWIW, a defacto standard is emerging that "raw" module names, like 'vscode', tend to be provided directly by the host environment whereas ones with paths ('./foo') are external. (That's why the src on script type="module" tags is required to have a path, at least for now.)
The "engines" section in package.json is not related with module import system.
It's for some native module to know howto compile when npm install.
And can check engine version. eg: you can set engines: {node: >=8}, then node v7 will deny to run your code, but that's not enforce.
VS Code is using vscode-loader as module loader, it's very like require.js, but has many other function for vscode.
The "global" function "require" you called, is override by vscode-loader, not node's native "require".
Same with any other module loader system, vscode-loader allows you to modify the "require" function.
vscode is changing so fast, you can do a simple search with nodeRequire('module').
Currentlly, related code is in src/vs/workbench/api/node/extHost.api.impl.ts file:
const node_module = <any>require.__$__nodeRequire('module');
const original = node_module._load;
node_module._load = function load(request: string, parent: any, isMain: any) {
if (request !== 'vscode') {
return original.apply(this, arguments);
}
.....
.....
// and finally, return apiImpl, the "vscode" object
}
require() will call module._load(), but this module._load is already overrided by vscode-loader.
You can also override it again like this.
That is called "Monkey Patch".

Categories

Resources