I am trying to make a library that works both in the browser as well as in node.
I have three json config files where the latter two extend tsconfig.json
tsconfig.json (just contains files for the build)
tsconfig.browser.json
tsconfig.node.json
tsconfig.browser.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "system",
"outFile": "../dist/browser/libjs.js",
"removeComments": true,
"declaration": true
}
}
tsconfig.node.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"outDir": "../dist/node",
"removeComments": true,
"declaration": true,
"declarationDir": "../dist/node/typings"
},
"files": [
"./index"
]
}
I have this index.ts file (only included on the node build):
export { collect } from './components/collections'
export { query } from './components/query'
Then I have this in the collections.ts file:
export namespace libjs {
export function collect<T>(data: T[]) {
// Do some stuff
}
export class collection(){}
}
And this query.ts file:
export namespace libjs {
export class query<T> {
private _results: collection<T>
}
}
The issue I am having, is when I try to build to node, the index file cannot find the collect function, and when I build to the browser the query class cannot find the collection class. What is the best way to code this so I can build to both node and the browser? If I remove the export on the namespace I can build to the browser fine, but I cannot build to node.
The way I would like to use these are as follows:
Nodejs
const libjs = require('libjs')
let c = libjs.collect([1, 123, 123, 1231, 32, 4])
Browser
<script src="/js/libjs.js"></script>
<script>
let c = libjs.collect([1, 123, 123, 1231, 32, 4])
</script>
When you compile files that have export ... at the top level, each file is treated as a module with its own scope, and namespace libjs in each file is distinct and separate from libjs in every other file.
If you want to generate a single script that can be used in a browser without module loader (defining libjs as global), you have to remove all toplevel exports, and don't set module at all in tsconfig:
components/collections.ts
namespace libjs {
export function collect<T>(data: T[]) {
// Do some stuff
}
export class collection<T>{}
}
components/query.ts
namespace libjs {
export class query<T> {
private _results: collection<T>
}
}
Now, you can use the same generated script in node too, if you add code that detects node environment at runtime and assigns libjs to module.exports:
index.ts
namespace libjs {
declare var module: any;
if (typeof module !== "undefined" && module.exports) {
module.exports = libjs;
}
}
single tsconfig.json for browser and node (note that I changed output to dist from ../dist)
{
"compilerOptions": {
"outFile": "./dist/libjs.js",
"removeComments": true,
"declaration": true
},
"files": [
"components/collections.ts",
"components/query.ts",
"index.ts"
]
}
You can use generated script right away in node in javascript:
test-js.js
const lib = require('./dist/libjs')
console.log(typeof lib.collect);
let c = lib.collect([1, 123, 123, 1231, 32, 4])
Unfortunately, you can't use it in node in typescript with generated libjs.d.ts because it declares libjs as global. For node, you need separate libjs.d.ts that contains one additional export = libjs statement that you have to add manually or as part of build process:
complete dist/libjs.d.ts for node
declare namespace libjs {
function collect<T>(data: T[]): void;
class collection<T> {
}
}
declare namespace libjs {
class query<T> {
private _results;
}
}
declare namespace libjs {
}
// this line needs to be added manually after compilation
export = libjs;
test-ts.ts
import libjs = require('./dist/libjs');
console.log(typeof libjs.collect);
let c = libjs.collect([1, 123, 123, 1231, 32, 4])
tsconfig.test.json
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"removeComments": true
},
"files": [
"./test-ts.ts",
"./dist/libjs.d.ts"
]
}
You can choose to go a completely different route and build a library composed of modules, but for that you have to use module loader in the browser (or build with webpack), and you probably need to read this answer explaining why namespace libjs is totally unnecessary with modules.
Related
I have a node application that compiles typescript files to a dist folder and then serves these files as lambda resolvers via aws cdk. Here is an example of my setup:
The code
register.ts
import ValidateUserFields from '../utils/forms';
exports.main = async function (event: any, context: any) {
return {
statusCode: 200,
};
}
register-lambda-config.ts
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
export class FrontendService extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const api = new apigateway.RestApi(this, 'frontend-api', {
restApiName: 'Frontend Service',
description: 'This service serves the frontend.',
});
const functionName = 'register';
const handler = new lambda.Function(this, functionName, {
functionName,
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('dist/src/lambda'),
handler: 'register.main',
});
const registerIntegration = new apigateway.LambdaIntegration(handler, {
requestTemplates: { 'application/json': '{ "statusCode": "200" }' },
});
const registerResource = api.root.addResource('register');
registerResource.addMethod('POST', registerIntegration);
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["es2018"],
"declaration": true,
"strict": true,
"noImplicitAny": false,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"esModuleInterop": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"outDir": "dist",
"typeRoots": ["./node_modules/#types"]
},
"exclude": ["node_modules", "cdk.out", "./dist/**/*"]
}
And finally here is the script part of my package.json file:
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"cdk": "cdk",
"bootstrap": "cdk bootstrap",
"deploy": "cdk deploy && rimraf cdk.out",
"destroy": "cdk destroy",
"run-same-local-fe-api": "sam local start-api -p 4000 -t ./template.yaml",
"dev": "npm run build && npm run synth && concurrently --kill-others \"npm run watch\" \"npm run run-same-local-fe-api\"",
"synth": "cdk synth --no-staging > template.yaml"
},
The problem
When I run npm run dev it compiles my typescript files to the dist folder in the same structure as what I have in my src folder (where all my typescript files live). I however run into the following error if I have any imports in my register.ts file:
{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot
find module '../utils/forms'\nRequire stack:\n-
/var/task/register.js\n- /var/runtime/UserFunction.js\n-
/var/runtime/index.js","stack":["Runtime.ImportModuleError: Error:
Cannot find module '../utils/forms'","Require stack:","-
/var/task/register.js","- /var/runtime/UserFunction.js","-
/var/runtime/index.js"," at _loadUserApp
(/var/runtime/UserFunction.js:202:13)"," at
Object.module.exports.load (/var/runtime/UserFunction.js:242:17)","
at Object. (/var/runtime/index.js:43:30)"," at
Module._compile (internal/modules/cjs/loader.js:1085:14)"," at
Object.Module._extensions..js
(internal/modules/cjs/loader.js:1114:10)"," at Module.load
(internal/modules/cjs/loader.js:950:32)"," at Function.Module._load
(internal/modules/cjs/loader.js:790:12)"," at
Function.executeUserEntryPoint [as runMain]
(internal/modules/run_main.js:75:12)"," at
internal/main/run_main_module.js:17:47"]}
This happens for imports from relative local files (like '../utils/forms' as shown in the code above) but also for imports from node_modules. When I look into the compiled register.js file in the dist folder I see that it has made an attempt to parse the import:
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const forms_1 = __importDefault(require("../utils/forms"));
const bucketName = process.env.BUCKET;
exports.main = async function (event, context) { ...
however it shows the error message above. I have tried using require instead of import but it was the same result...
Any help would be greatly appreciate! Thanks
Stated that this is really hard to answer without a minimal reproducible example; I would at least suggest to avoid any require and exports, and to use only import / export statements and following in tsconfig.json.
{
"compilerOptions": {
"module": "esnext"
}
}
Well.. I do understand that you want your main function to look something like this:
// final result written in javascript
exports.main = async function (event, context) {
return {
statusCode: 200,
};
}
But... using module.exports in Typescript is not the way to achieve that. Instead, Typescript using export directive (no s at the end of it) to define which parts of your code should be export. It's then up to your tsconfig.json file to determine which syntax will be used in order to represent this export (this is actually a part of Typescript engine)
So... a script written like this in Typescript
export async function main(event: any, context: any) {
return {
statusCode: 200,
};
}
Will be parse in Typescript as follow (I've used module: commonjs to achieve below result)
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.main = void 0;
async function main(event, context) {
return {
statusCode: 200,
};
}
exports.main = main;
//# sourceMappingURL=test.js.map
Please note how the resulted js file correctly use modile.exports and main as you intended
In short: when using Typescript, please use the language directives and let the engine to do the rest for you. This way - a single source of code can be deployed for different environment without requireing changing your app logic. Neat!
we have a TS app that uses a JS library to which we built a .d.ts file to use it with TypeScript.
When we had that file in a "typings" folder inside the project, everything was working fine. However, we decided to move those typings to a separate repo in order to share it with other projects.
Issue: After moving the typings to an external repo, the IDE (both Idea and VSCode) no longer recognize the properties and methods from the super type. The app compiles, runs and all features work. But the IDEs keep saying the files have errors.
Any ideas?
Typing repo structure is like this:
-custom-typings
- index.d.ts
- tsconfig.json
- package.json
package.json:
{
"name": "custom-typings",
"author": "Author Name",
"version": "0.1.3",
"types": "./index.d.ts",
"dependencies": {
"typescript": "next"
}
}
tsconfig:
{
"compilerOptions": {
"target": "ES2015",
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"esModuleInterop": true,
"alwaysStrict": true,
"lib": [
"ES2015",
"ES2016",
"dom"
],
"pretty": true,
"moduleResolution": "node",
"module": "ES2020"
},
"exclude": [
"dist",
"node_modules",
],
"typeAcquisition": {
"enable": true
}
}
index.d.ts
declare module 'custom-typings' {
export class A {
aMethod();
}
export class B extends A{
bProperty: any = {foo: 'Foo'};
}
const CustomTypes: {
B: typeof B;
};
export default CustomTypes
}
Using the types:
import CustomTypes from 'custom-typings';
class MyComponent extends CustomTypes.B {
init() {
console.log(this.bProperty.foo); // property 'bProperty' does not exist in type MyComponent
this.aMethod(); // property 'aMethod' does not exist in type MyComponent
}
}
So, it happened that the structure of the "custom-typings" repo was preventing the project to resolve the types.
We were writing types definitions for an existing library that had its own module declared. Our repo was declaring the module with the same name as the lib's one, let's call it "lib-module".
The types repo needs to contain a directory called "lib-module" and declare the module with that same name:
declare module "lib-module" {
// properties and functions here
}
and the new structure is:
-custom-typings
- lib-module
- index.d.ts
- tsconfig.json
- package.json
This way, any project can import the types like:
import CustomTypes from 'lib-module';
Something is a bit cloudy in my mind which is the following.
I have a module written in typescript which will be imported later in some html pages.
sdk.ts
export class PM {
header: any;
headerLink: string;
headerDiv: any;
/**
* #todo remove constructor.
*/
constructor(mode: string) {
if (mode == null || mode == undefined) {
this.buildGUI();
}
}
/**
* Build GUI.
* It builds the GUI by wrapping the body in a container, adding the header and sidebar.
*/
buildGUI(): void {
this.initAndCreateComponents();
this.insertScript(this.headerLink);
}
/**
* Insert script.
* It inserts the script's import tag in the head of the document.
* #param {string} scriptLink - script's link to be loaded.
*/
insertScript(scriptLink: string): void {
const script = document.createElement('script');
script.src = scriptLink;
document.body.appendChild(script);
};
/**
* Init and Create Components.
* It initialises the variables values and it creates the components.
*/
initAndCreateComponents(): void {
this.headerLink = '/header/pm-header.js';
this.header = document.createElement("pm-header");
this.headerDiv = document.createElement("div");
this.headerDiv.classList.add('pm-header-wrapper');
this.headerDiv.appendChild(this.header);
document.body.insertBefore(this.headerDiv, document.body.firstChild);
}
}
new PM(null);
and this is my tsconfig.json
{
"compileOnSave": false,
"include": [
"src",
"test"
],
"exclude": [
"dist",
"node_modules"
],
"compilerOptions": {
"sourceMap": false,
"outDir": "./dist",
"declaration": true,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"node_modules/#types"
],
"types": [
"#types/jasmine",
"#types/node"
],
"lib": [
"es2017",
"dom",
"es2015.generator",
"es2015.iterable",
"es2015.promise",
"es2015.symbol",
"es2015.symbol.wellknown",
"esnext.asynciterable"
]
}
}
now when I run tsc I get and sdk.js that looks like this:
define(["require", "exports"], function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var PM = /** #class */ (function () {
/**
* #todo remove constructor.
*/
function PM(mode) {
if (mode == null || mode == undefined) {
this.buildGUI();
}
}
/**
* Build GUI.
* It builds the GUI by wrapping the body in a container, adding the header and sidebar.
*/
PM.prototype.buildGUI = function () {
this.initAndCreateComponents();
this.insertScript(this.headerLink);
};
...
Now this generated file is supposed to be imported in several html pages, and when I did my research I found that it could only be loaded using require like this:
<script data-main="/sdk/sdk.js" src="/sdk/require.js"></script>
What I want is a way to load my script without the use of any library, to be loaded like any regular plain javascript file.
If you don't want to use a module system (although I highly recommend you look into using one) you should remove export from your class (and from any other symbol in your file) , this will make your module be treated as a simple script file.
You should also add "module": "none" to your tsconfig.json to let the compiler know you will not be using a module system. This should trigger errors anywhere your code depends on modules (either because you export something or you use an import)
Note Since you will not be using a module system any class/variable/function you declare in your script file will be in the global scope (as they would be for any js file). You may want to consider using namespaces to organize your code and get out of the global scope.
How can I extend third-party declaration files?
for example, I want to extend Context from #types/koa and add an extra field(resource) to it.
I tried this:
// global.d.ts
declare namespace koa {
interface Context {
resource: any;
}
}
But it doesn't work:
error TS2339: Property 'resource' does not exist on type 'Context'.
Update
a simplified version of my code which produces this error:
import {Context} from 'koa';
import User from './Models/User';
class Controller {
async list(ctx: Context) {
ctx.resources = await User.findAndCountAll();
ctx.body = ctx.resources.rows;
ctx.set('X-Total-Count', ctx.resources.count.toString());
ctx.status = 200;
}
}
typescript v2.4
// tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"exclude": [
"node_modules"
]
}
You have to use module augmentation as described here:
import { Context } from "koa";
declare module "koa" {
interface Context {
resource: any;
}
}
If I have a TypeScript module saved as my-function.ts as follows :
export function myFunction (param: number): number { return param }
This will be compiled to JavaScript in whichever way and loose its type definitions. I am then able to create a index.d.ts file which declare this module's definitions, but this seems a bit tedious to redefine/redeclare the definitions.
Are there ways to generate the type definitions automatically from the my-function.ts file to a index.d.ts file?
If you compile with the --declaration flag, TypeScript will automatically generate .d.ts files for you.
This mode will require that you certain types are visible so that they can be described in your .d.ts files.
Here's how I managed to solve it:
Creating the infra
Create an new Node package with typescript for the infra.
Inside the new package, make sure to configure a tsconfig.json with declaration:true Doing so will cause typescript to generate definition files which can be consumed by the users of this infra.
My tsconfig.json:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"outDir": "./tsOutputs"
},
"include": [
"lib/**/*.ts",
"index.ts"
],
"exclude": [
"test/**/*.ts"
]
}
Create a an "index.ts" file which will export the public API of the infra.
Note: In order to be able to cast & create instances of objects, you need to have two different exports per each entity. Once as type and another as const.
Here's my index.ts:
import {HttpClient as HC} from "./lib/http/http-client";
import {HttpRequest as HReq, HttpResponse as HRes} from "./lib/http/contracts";
export namespace MyJsInfra {
export type HttpClient = HC;
export namespace Entities {
export type HttpRequest = HReq;
export const HttpRequest = HReq;
export type HttpResponse = HRes;
export const HttpResponse = HRes;
}
}
You can read more info on the reasoning behind this dual declaration in here:
https://github.com/Microsoft/TypeScript/issues/10058#issuecomment-236458961
After all of the following, when we'll run build we should have the corresponding "*.d.ts" files per each type. Now we have to handle the package.json of the infra, in order to pack all the items.
Inside the package.json make sure to set the types, main to point to the generated index.d.ts & index.js files.
In addition, you have to make sure that the "*.d.ts" files are being packaged as part of infra. In my case, I've specified the following pattern in the files property: "tsOutputs/**/*.d.ts"
Here's my package.json:
{
"name": "my-js-infra",
"version": "1.0.0",
"description": "Infrastructure code.",
"scripts": {
"build":"./node_modules/.bin/tsc -p .",
"prepublish":"npm run build",
},
"homepage": "https://github.com/Nadav/My.JS.Infra#readme",
"devDependencies": {
...
"typescript": "^2.4.2",
...
},
"dependencies": {
...
"needle": "^1.4.2",
...
},
"files": [
"tsOutputs/**/*.js",
"tsOutputs/**/*.d.ts",
"tsOutputs/index.d.ts"
],
"types":"tsOutputs/index.d.ts",
"main":"tsOutputs/index.js"
}
All done. Now you can publish your common code.
Consuming the code
Install the infra. In our case the user have to use: npm install my-js-infra --save
Modify the tsconfig.json of the consuming application to load the modules using the Node module resolution. You do so by setting moduleResolution:true inside the file.
Here's my tsconfig.json:
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "es6"],
"module": "umd",
"sourceMap": true,
"watch": false,
"outDir": "./tsOutputs",
"moduleResolution":"node" /* This must be specified in order for typescript to find the my-js-infra. Another option is to use "paths" and "baseUrl". Something like:
...
"baseUrl": ".", // This must be specified if "paths" is used.
"paths":{
"my-js-infra":["node_modules/my-js-infra/tsOutputs/index.d.ts"]
}
...
*/
}
}
You can read more on module resolution in Typescript in here:
https://www.typescriptlang.org/docs/handbook/module-resolution.html
Start using the code. For example:
import {MyJsInfra } from "my-js-infra";
public doMagic(cmd, callback) {
try {
var request: MyJsInfra.Entities.HttpRequest = {
verb: "GET",
url: "http://www.google.com",
};
var client = new MyJsInfra.HttpClient();
client.doRequest(request, (err, data) => {
if (err)
return callback(err, null)
return callback(null, data);
})
} catch (err) {
callback(err);
}
}