Manually compiling a TypeScript library - javascript

I'm creating a TS library the build for which involves parsing TS code, generating types and additional variables from it in a complex and custom process (ie, not just by running it through a compiling tool).
Specifically, this lib is a set of OpenAPI specs for several different APIs. It's written in TS, and various TS files within the lib export OpenAPI specs which are basically big JSON blobs coming from a TS file. Then I process those blobs using a manual/bespoke compilation process, and turn them into a series of exports, both of endpoint response types and sub-types. None of this can be auto-compiled using tsc, webpack, etc, so I'm aiming to mimic the output shape of such a process with a "manual" compilation script described.
I'm then importing the resulting lib it into a React app which uses TypeScript, and also has Jest tests. Notably, my library doesn't have a default export, because it represents several APIs, and so, rather, is imported like import { SomeAPI } from "my-lib/dist/some-api";. (I'd love to get rid of the /dist/, but one thing at a time.)
I'm running into issues figuring out how to structure my built code so that it can be imported and run by both the app and the app's Jest specs.
My first attempt was to compile everything to just .ts files, which export both variables and types. This worked fine for the app, but threw a cannot use import statement outside of a module error when I tried to import the library and run the specs in the "importing" app, because Jest doesn't babel-process node_modules by default. I could try to get the app to transpile my lib when running the tests -- per Jest's error message, To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config. -- but that seems wrong. Shouldn't the lib be in a directly consumable format when compiled?
Attempts to fix this by declaring "type": "module" in the library package.json file then led my custom compiling script, which is run using ts-node, to throw an Unknown file extension ".ts" error. Attempts to fix this by using ts-node's ESM option led to other issues.
So I took a step back and thought it would be better to split my files into .ts files which use module.exports to export plain JS objects/variables, and accompanying .d.ts files which export matching types with the same name (after reading this SO Post).
That didn't work, either, as I'm now getting warnings that my imported files are not modules, and the types don't appear to be recognized by the importing app at all.
So I'm a little stumped, and wanted to get a sense of what I should be doing here. If I'm exporting a library for consumption in a TS app, what should my export look like?
Is it correct to have .ts and .d.ts files side by side, with the .d.ts files exporting (using export/import syntax) the types for the neighboring variables (exported using modules.export syntax)? Or should I have a parallel types directory exporting just the types? If so, how will the consuming app map types to variables? Should the variable files be .js instead? Do I need to set "type": "module" in my package.json? Etc.
I've tried looking at the code of some other TS libraries, but they all seem to do complex and bespoke things, which are frankly hard to understand. (Eg, I looked at lodash, and there's some strange magic going on there). Is there a basic best-practice here?
Here are some of the relevant files (trimmed down) to give a sense of where I'm currently at:
package.json
{
"version": "1.0.2",
"files": [
"./dist"
],
"scripts": {
"compile:openapi": "ts-node scripts/compileOpenapi.ts"
},
"engines": {
"node": "^16.14.0"
}
}
(The above compile script basically loops through some of my OpenAPI files, and uses fs to write new files to /dist with variables and types inferred from the OpenAPI specs.)
tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"noEmit": true,
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"noImplicitAny": true,
// Allow for newer ES6 JS syntax (eg Set, array.find, etc)
"target": "es6",
"lib": [
// Allow for newer syntax and other variables/tools
"es6",
// Object.entries and Object.values
"es2017.object",
// Window, etc
"dom",
// array.includes
"es2016.array.include",
// Promises, Async/Await
"es2018"
]
},
"ts-node": {
"require": ["tsconfig-paths/register"]
}
}
dist/example.ts
module.exports.defaultExample = {
title: 'whatever',
name: 'Whatever'
};
dist/example.d.ts
export interface Document {
title: string;
name: string;
size?: number;
mimeType?: string;
data?: string;
}
export declare const defaultExample: Document;

I figured out a solution that worked for me at least, by following #Bergi's advice above to use tsc, (tsc --outDir -d dist someFile.ts) to output declarations and JS files, which I could then copy the basic structure of.
Rather than share my super-specific and lengthy compilation code, I think it'd be more useful to just show how things ended up exported. The basic structure was parallel JS files, which followed a exports export syntax, and d.ts files which exported the types (both verbatim matches to JS vars and additional types):
- index.js
- index.d.ts
- folder
|- something.js
|- something.d.ts
All compiled .js files had the following at the top of them:
"use strict";
exports.__esModule = true;
And then exported things either as exports.[varName] or exports.default. They also imported other modules using the const defaultExample = require("./examples/default").default; syntax (or const something = require("./examples/whatever").something;). As in
const defaultExample = require("./examples/default").default;
"use strict";
exports.__esModule = true;
exports.someArray = ["one", "two", "three"];
exports.defaultExample = defaultExample;
The accompanying .d.ts file then looked like this:
export declare interface ExampleInterface {
// ...
}
export declare const defaultExample: ExampleInterface;
export declare const someArray: string[];
// etc
This format of export works correctly in the consuming app. Variables (eg defaultExample) are available, and types are introspected on hover etc. They work smoothly with the specs as well. Weird hacky solution, as generally a library of some sort will do the official compilation, but at least I find it helpful to know how TS modules are simply structured for functional import.

Related

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?

VS Code + jsconfig.json: "Cannot find module..." for NPM modules with built-in types

I have a JavaScript project that I'm editing in Visual Studio Code. I have defined the following jsconfig.json file for it:
{
"compilerOptions": {
"checkJs": true,
"strict": true,
"target": "es2020"
},
"include": [
"api/**/*.js",
"bin/**/*.js",
"lib/**/*.js",
"models/**/*.d.ts",
"models/**/*.js",
"test/**/*.js"
]
}
As with most projects, this one relies on NPM dependencies. Some of those dependencies have built-in type definitions and some do not. For example, two in this project are 'aws-sdk', which comes with its own type definitions, and 'express', which does not (but has an externally-supplied type definition thanks to Definitely Typed).
The thing that has me scratching my head is that VS Code's JavaScript Language Server/TypeScript "Cannot find module 'aws-sdk'" (as in the example below), but there are no such complaints about 'express'.
const express = require('express')
const AWS = require('aws-sdk') // Cannot find module 'aws-sdk'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option? ts(2792)
This is consistent across the entire codebase--dependencies that have their own type definitions are NOT found by VS Code while dependencies that have externally supplied type definitions (whether added explicitly as dependencies, e.g. yarn add -D #types/express, or as automatically acquired types by VS Code) are found just fine by VS Code.
Is there a jsconfig.json setting that I need to investigate to tell VS Code to look inside dependencies for their types (node_modules/**) in addition to those found in node_modules/#types?

Webpack using Typescript: Common settings in both configs, so which take precedence?

I am in the initial stages of converting a javascript (with webpack) project into a typescript project and one of the many confusions I have is that their appears to be some configuration settings that can appear in webpack and typescript, so which takes precedence?
(I'm currently working on a node cli application).
Eg, the primary example is which files are included in compilation.
In webpack config, you can specify which input files are selected with rules:
module: {
rules: [
{
test: /index.ts/,
use: 'shebang-loader'
},
{
test: /\.ts(x?)$/,
use: 'ts-loader'
},
{
test: /\.json$/,
use: 'json-loader'
}
]
},
As you can see from above, I'm using various loaders for different file types.
Then my initial tsconfig.json is as follows:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"noImplicitAny": true,
"lib": [
"es5", "es2015", "es6", "dom"
]
},
"include": [
"lib/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
So what is the true nature of the interaction between webpack and typescript? I have not been able to discern this from typescript or webpack documentation. Both appear to be able to specify the input files that are included in compilation.
Another (probably better example) is the output file. In webpack config I have the following:
output: {
libraryTarget: 'commonjs',
path: path.join(__dirname, 'dist'),
filename: 'application-bundle.js'
}
which defines 'application-bundle.js' is the output file inside the 'dist' folder.
In the tsconfig, you can have something like the following:
"outFile": "./dist/application-bundle.js",
(I currently do not define an outFile property, but I know you can do so, but in being able to do so, brings up ambiguities and hence confusion). So in this scenario, does webpack override typescript or vice-versa? What is the recommended strategy?
There are probably more typescript/webpack crossovers that are going to cause confusion, but the 2 that I have described so far are the most upfront and pressing issues I need to understand, thanks.
EDIT: Having thought about it, I also need another clarification. I am guessing when you build a TS/WP project, the TS compiler runs first creates all the .js files (let's say in the .dist folder). Then webpack bundles all the generated .js files. So assuming this is correct, do I then need to configure webpack to use as its input the .js files in the dist folder instead of the .js files inside the project source (ie everything under ./lib which is where my source js files are)?
I was hoping to convert my project in an incremental manner so what I have just said does not really fit my needs. Webpack would need to pick up some .js files from ./dist and some files from ./lib which have not yet been converted to typescript. I don't know how to modify the project for incremental upgrade.
[...] there appears to be some configuration settings that can appear in webpack and typescript, so which takes precedence?
You are right on the point that there is some natural redundancy in the configuration when Webpack is combined with TypeScript. Lets pick up your first question:
which files are included in compilation? [...] Another example is the output file. [...] So in this scenario, does webpack override typescript or vice-versa? What is the recommended strategy?
Simply spoken, both TypeScript and Webpack process/transform input files and emit the output in a target directory structure or file bundle(s) - both need some input/output configuration info. TypeScript compiler can run on its own, and also be integrated as file processor as part of a bigger Webpack build. With outFile option TypeScript can even be seen as a mini-bundler on its own, as it bundles all .ts-files to a single target file.
To answer the question, if TypeScript or Webpack configuration takes precedence, it is important to understand, how Webpack works. I'll quote one of your assumptions here:
I am guessing when you build a TS/WP project, the TS compiler runs first creates all the .js files (let's say in the .dist folder). Then webpack bundles all the generated .js files.
That is not quite correct. But good you said that, as it sheds more light on your core understanding problem. Everything in Webpack starts with the entry point you specify in the config - if you will, that's the input. From this entry module, webpack considers all other transitively imported modules (e.g. via import or require) and creates a dependency tree.
Every file in the dependency tree can optionally be transformed by Loaders, which can also be chained like a file processing pipeline. So ts-loader and the underlying TypeScript compiler apply transformations for .ts/.tsx files with test: /\.ts(x?)$/ predicate.
All in all you see, that Webpack considers your entry file, which leads to a bunch of further imported .ts/.tsx files (amongst your other file types, we neglect them here). And for each single file, the TypeScript loader will be invoked in the course of the loader processing pipeline. Therefore, it is inherent that TypeScript I/O config will be ignored and Webpack's entry/output configuration takes precedence in the build. All other TypeScript related settings are taken from tsconfig.json, as described in the ts-loader docs: "The tsconfig.json file controls TypeScript-related options so that your IDE, the tsc command, and this loader all share the same options."
I was hoping to convert my project in an incremental manner
It is perfectly fine to migrate from JavaScript to TypeScript in a stepwise manner!
Hope, that helps.

Typescript 1.8 - ES2015 imports in js files

I would like to start using Typescript on an Existing Babel project. My goal is to be able to add typescript to the build process with as less modifications as possible on the existing code. For this reason, I decided to chain typescript(targeting ES2015) and Babel.
With ts1.8's js files support, I Thought I would finally be able to keep everything as is and then convert files one by one.
But here is the first issue I encountered:
error TS8003: 'export=' can only be used in a .ts file.
Typescript doesn't seams to allow es2015 exports syntax:
export default 'foo';.
We are using es2015 syntax for imports/exports and I don't want to change it for the old commonJS symtax. Is there any way to make typescript allow it?
Here is a minimal example demonstrating the issue:
hello.js
export default (name) => console.log(`Hello ${name}`);
tsconfig.json
{
"version": "1.8",
"compilerOptions": {
"module": "es2015",
"allowJs": true,
"target": "es2015"
}
}
command line (using typescript 1.8)
tsc --outDir ../out
result
hello.js(1,1): error TS8003: 'export=' can only be used in a .ts file.
The error you're getting for the default export is a bug in the TypeScript compiler. I've sent out a fix since you filed this issue.
If you want to specify the root module in JavaScript files (which is non-standard and specific to certain module loaders like CommonJS), the way to do this is the same way you'd do this in JavaScript:
module.exports = yourRootExportObjectHere;
The compiler should recognize and respect these as equivalent to export = declarations.

Do I have to reference TypeScript definition in every file?

Is there a way to tell TypeScript to use a certain file (or set of files) as a definition for everything compiled?
My only alternative currently is to add something like this in every single TypeScript file (which seems clunky):
/// <reference path="DefinitelyTyped/requirejs/require.d.ts" />
When using TypeScript's internal module system, you can avoid having any <reference> tags at all in the code. I personally do this because I don't want to encode paths (realtive or absolute) within the code as I constantly move stuff around.
One way to do this is by making sure all required declaration files and TypeScript source files are passed to the compiler as arguments during compile time.
Using gulp together with gulp-typescript simplifies this task. You can set noExternalResolve in gulp-typescript to true, and create gulp tasks that take all your .d.ts files along with your sources and pipe it down to the compiler. When you pull in tsd into your stack, you only need to pass the tsd.d.tsfile that contains references to all other definition files installed via tsd.
UPDATE for TypeScript >= v1.5: you can use a tsconfig.json file, and the compiler will get the ordering of the classes right. This removes the need to use gulp-typescript alltogether. You can either chose to have all files explicitly listed in the tsconfig.json file, or completely leave out the files property to include all *.ts/*.tsx files within the directory the tsconfig.json resides (including all subfolders).
A sample tsconfig.jsonmay look like:
{
"compilerOptions": {
"target": "ES5",
"module": "commonjs",
"lib": [ "es5", "es2015.promise", "dom" ]
},
"include": [
"src/**/*.ts"
]
}
What I've learned so far is that /// < reference >-ing a module with reference comments is not a good method.
For example: in case you have a file Foo and a file Bar. Both files use jquery, but only file Foo has a reference comment to jquery. If file Foo is deleted for some reason, your file Bar is broken, because the reference is missing.
If you are using TypeScript >= 2.0 It is better to define the TypeScript definition files (.d.ts) in your tsconfig.json under the "files" section.
This could look like this:
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": true,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "es5",
"outDir": "./Scripts/"
},
"files": [
"./src/foo.ts",
"./src/bar.ts",
"./Scripts/typings/jquery/jquery.d.ts",
"./Scripts/typings/jqueryui/jqueryui.d.ts",
"./Scripts/MicrosoftMaps/Microsoft.Maps.d.ts"
]
}
Using the /// directive (reference comments) is often used in examples to get you started quickly, but it is not a best practice. Also many examples come from a version < TypeScript 2.0.
Some IDEs auto-detect all the files in a project (Visual Studio).
For everything else, you can create a _references.ts file and put all of your reference comments in there - then you only ever need to add:
/// <reference path="_references.ts" />
...to each file (instead of possibly many).
Your IDE may also support tsconfig files.
This question is a duplicate of Reference typescript definitions in one file, instead of all JS files?
The answer is, for now, add each file you want to reference to your tsconfig.json file's "files" section. It's still many lines, but all in one file.
In future when Typescript 2 is released you can then use the "filesGlob" section and solve the problem in two lines.
I've started recently with TypeScript and as I've understood the internal modules resolution is that yes, you can compile all .ts files from the tsconfig.json's directory and all its subdirectories, provided that you don't have set .ts files in it without /// <references path="" />.
But the order in which the .ts files are compiled into resulting .js files is not determined by the dependencies the files (or classes they contain) have. So it is possible to have a situation where the child class is compiled before the parent one (child inherits from parent relation). Then the code won't run, even though it is compiled successfully. It will complain that it couldn't understand the parent class within the child class. Therefore you need to add a /// <references path="" /> as a hint to the compiler to resolve the dependencies between .ts files.
This is want the Typescript documentation says:
The /// directive is the most common of this group. It serves as a declaration of dependency between files.
Triple-slash references instruct the compiler to include additional files in the compilation process.
They also serve as a method to order the output when using --out or --outFile. Files are emitted to the output file location in the same order as the input after preprocessing pass.

Categories

Resources