This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+226
View File
@@ -0,0 +1,226 @@
<div align="center">
<img src="https://res.cloudinary.com/adonisjs/image/upload/q_100/v1558612869/adonis-readme_zscycu.jpg" width="600px">
</div>
<br />
<hr />
<div align="center">
<h1> Typescript Compiler </h1>
<p> In memory Typescript compiler for Node.js with support for <strong>caching</strong> and <strong> custom transformers</strong> </p>
</div>
<br />
<div align="center">
[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url]
</div>
## Introduction
Require ts is a module similar to [ts-node](https://github.com/TypeStrong/ts-node) with a handful of differences.
The idea is to hook into the lifecycle of Node.js `require` calls and compile Typescript on the fly (in memory)
In case, if you are not aware, Node.js has first class support for registering custom [require extensions](https://gist.github.com/jamestalmage/df922691475cff66c7e6) to resolve and compile files with a certain extension. For example:
```ts
require.extenstions['.ts'] = function (module, filename) {
var content = fs.readFileSync(filename, 'utf8')
module._compile(content, filename)
}
```
If we replace the function body of the example with the Typescript compiler API, the we basically get in-memory typescript compilation. However, there are many other things to manage.
- Making source-maps to work, so that the error points to the Typescript code and not the compiled in memory Javascript.
- Support for typescript extensions
- Introducing some sort of caching to avoid re-compiling the unchanged files. Typescript compiler is not one of the fastest compilers, so caching is required.
## Goals
Following are the goals for writing this module
- Able to work with Typescript without setting up a on-disk compiler
- Keeping the in-memory compilation fast. For this, we do not perform type checking. Your IDE or text editor should do it.
- Cache the compiled output on disk so that we can avoid re-compiling the unchanged files. A decent project has 100s of source files and we usually don't change all of them together. Also compiled cache is not same as the compiled output.
- Expose helper functions for watchers to clear the cache. Most of the Node.js apps use some kind of a watcher to watch for file changes and then restart the process. The helpers exposed by this package, allows the watcher to cleanup cache of the changed file.
- Add support for custom transformers.
## Usage
This module is pre-configured with all the AdonisJS applications and ideally you won't have to dig into the setup process yourself. However, if you are using it outside of AdonisJS, then follow the following setup process.
```sh
npm i -D @adonisjs/require-ts
```
And then require it as a Node.js require hook
```sh
node -r @adonisjs/require-ts/build/register app.ts
```
I have personally created a bash alias for the above command.
```sh
alias tsnode="node -r @adonisjs/require-ts/build/register"
```
and then run it as follows
```sh
tsnode app.ts
```
## Programmatic usage
The main goal of this package is to expose a programmatic API that others can use to create their own build tools or commands.
### `register`
```ts
const { register } = require('@adonisjs/require-ts')
/**
* Require ts will resolve the "tsconfig.json" file from this
* path. tsconfig.json file is required to compile the code as * per the project requirements
*/
const appRoot = __dirname
const options = {
cache: true,
cachePath: join(require.resolve('node_modules'), '.cache/your-app-name'),
transformers: {
before: [],
after: [],
afterDeclarations: [],
},
}
register(appRoot, options)
/**
* From here on you can import the typescript code
*/
require('./typescript-app-entrypoint.ts')
```
The `register` method accepts an optional object for configuring the cache and executing transformers.
- `cache`: Whether or not to configure the cache
- `cachePath`: Where to write the cached output
- `transformers`: An object with transformers to be executed at different lifecycles. Read [transformers](#transformers) section.
The register method adds two global properties to the Node.js global namespace.
- `compiler`: Reference to the compiler, that is compiling the source code. You can access it as follows:
```ts
const { symbols } = require('@adonisjs/require-ts')
console.log(global[symbols.compiler])
```
- `config`: Reference to the config parser, that parses the `tsconfig.json` file. You can access it as follows:
```ts
const { symbols } = require('@adonisjs/require-ts')
console.log(global[symbols.config])
```
### `getWatcherHelpers`
The watcher helpers allows the watchers to cleanup the cache at different events. Here's how you can use it
```ts
const { getWatcherHelpers } = require('@adonisjs/require-ts')
/**
* Require ts will resolve the "tsconfig.json" file from this
* path. tsconfig.json file is required to compile the code as * per the project requirements
*/
const appRoot = __dirname
/**
* Same as what you passed to the `register` method
*/
const cachePath = join(require.resolve('node_modules'), '.cache/your-app-name')
const helpers = getWatcherHelpers(appRoot, cachePath)
helpers.clear('./relative/path/from/app/root')
```
This is how you should set up the flow
- Clean the entire cache when you start the watcher for the first time. `helpers.clear()`. No arguments means, clear everything
- Clean the cache for the file that just changed. `helpers.clear('./file/path')`
- Check if the config file has changed in a way that will impact the compiled output. If yes, then clear all the cached files.
```ts
if (helpers.isConfigStale()) {
helpers.clear() // clear all files from cache
}
```
## Caching
Caching is really important for us. Reading the compiled output from the disk is way faster than re-compiling the same file with Typescript.
This is how we perform caching.
- Create a `md5 hash` of the file contents using the [rev-hash](https://www.npmjs.com/package/rev-hash) package.
- Checking the cache output with the same name as the hash.
- If the file exists, pass its output to Node.js `module._compile` method.
- Otherwise, compile the file using the Typescript compiler API and cache it on the disk
The module itself doesn't bother itself with clearing the stale cached files. Meaning, the cache grows like grass.
However, we expose helper functions to cleanup the cache. Usually, you will be using them with a file watcher like `nodemon` to clear the cache for the changed file.
## Differences from ts-node
`ts-node` and `require-ts` has a few but important differences.
- `ts-node` also type checks the Typescript code. They do allow configuring ts-node without type checking. But overall, they pay extra setup cost just by even considering type checking.
- `ts-node` has no concept of on-disk caching. This is a deal breaker for us. **Then why not contribute this feature to ts-node?**. Well, we can. But in order for caching to work properly, the module need to expose the helpers for watchers to cleanup the cache and I don't think, ts-node will bother itself with this.
- `ts-node` ships with inbuilt REPL. We don't want to bother ourselves with this. Again, keeping the codebase focused on a single use case. You can use [@adonisjs/repl](https://github.com/adonisjs/repl) for the REPL support.
These are small differences, but has biggest impact overall.
## Transformers
Typescript compiler API supports transformers to transform/mutate the AST during the compile phase. [Here](https://github.com/madou/typescript-transformer-handbook#writing-your-first-transformer) you can learn about transformers in general.
With `require-ts`, you can register the transformers with in the `tsconfig.json` file or pass them inline, when using the programmatic API.
Following is an example of the tsconfig.json file
```json
{
"compilerOptions": {},
"transformers": {
"before": ["./transformer-before"],
"after": ["./transformer-after"],
"afterDeclarations": ["./transformer-after-declarations"]
}
}
```
The transformer array accepts the relative file name from the `appRoot`. The transformer module must export a function as follows:
```ts
export default transformerBefore(ts: typescript, appRoot: string) {
return function transformerFactory (context) {}
}
```
[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/require-ts/test?style=for-the-badge
[gh-workflow-url]: https://github.com/adonisjs/require-ts/actions/workflows/test.yml 'Github action'
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
[npm-image]: https://img.shields.io/npm/v/@adonisjs/require-ts.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@adonisjs/require-ts 'npm'
[license-image]: https://img.shields.io/npm/l/@adonisjs/require-ts?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md 'license'
[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/require-ts?label=Synk%20Vulnerabilities&style=for-the-badge
[synk-url]: https://snyk.io/test/github/adonisjs/require-ts?targetFile=package.json 'synk'
+40
View File
@@ -0,0 +1,40 @@
import tsStatic from 'typescript';
import { Compiler } from './src/Compiler';
import { Transformers } from './src/Contracts';
/**
* Symbols that can be used to get the global reference of the compiler
*/
export declare const symbols: {
compiler: symbol;
config: symbol;
};
/**
* Returns helpers to along with cache when using a watcher.
*
* - You can check if the tsconfig file inside the cache is stale or not.
* If it is stale, then clear the entire cache
*
* - Clear cache for a given file path.
* - Clear all cache
*/
export declare function getWatcherHelpers(appRoot: string, cachePath?: string): {
clear(filePath?: string): void;
isConfigStale: () => boolean;
};
/**
* Load in-memory typescript compiler
*/
export declare function loadCompiler(appRoot: string, options: {
compilerOptions: tsStatic.CompilerOptions;
transformers?: Transformers;
}): Compiler;
/**
* Register hook to compile typescript files in-memory. When
* caching is enabled, the compiled files will be written
* on the disk
*/
export declare function register(appRoot: string, opts?: {
cache?: boolean;
cachePath?: string;
transformers?: Transformers;
}): void;
+115
View File
@@ -0,0 +1,115 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.register = exports.loadCompiler = exports.getWatcherHelpers = exports.symbols = void 0;
const pirates_1 = require("pirates");
const find_cache_dir_1 = __importDefault(require("find-cache-dir"));
const Cache_1 = require("./src/Cache");
const Config_1 = require("./src/Config");
const Compiler_1 = require("./src/Compiler");
const utils_1 = require("./src/utils");
/**
* Extensions to register require extension for
*/
const EXTS = ['.ts', '.tsx'];
const CACHE_DIR_NAME = 'adonis-require-ts';
/**
* Symbols that can be used to get the global reference of the compiler
*/
exports.symbols = {
compiler: Symbol.for('REQUIRE_TS_COMPILER'),
config: Symbol.for('REQUIRE_TS_CONFIG'),
};
/**
* Returns helpers to along with cache when using a watcher.
*
* - You can check if the tsconfig file inside the cache is stale or not.
* If it is stale, then clear the entire cache
*
* - Clear cache for a given file path.
* - Clear all cache
*/
function getWatcherHelpers(appRoot, cachePath) {
cachePath = cachePath || (0, find_cache_dir_1.default)({ name: CACHE_DIR_NAME });
const cache = new Cache_1.Cache(appRoot, cachePath);
return {
clear(filePath) {
return filePath ? cache.clearForFile(filePath) : cache.clearAll();
},
isConfigStale: () => {
const config = new Config_1.Config(appRoot, cachePath, undefined, true);
const { cached } = config.getCached();
return !cached || cached.version !== Config_1.Config.version;
},
};
}
exports.getWatcherHelpers = getWatcherHelpers;
/**
* Load in-memory typescript compiler
*/
function loadCompiler(appRoot, options) {
const typescript = (0, utils_1.loadTypescript)(appRoot);
return new Compiler_1.Compiler(appRoot, appRoot, typescript, options, false);
}
exports.loadCompiler = loadCompiler;
/**
* Register hook to compile typescript files in-memory. When
* caching is enabled, the compiled files will be written
* on the disk
*/
function register(appRoot, opts) {
/**
* Normalize options
*/
opts = Object.assign({ cache: false, cachePath: '' }, opts);
if (opts.cache && !opts.cachePath) {
opts.cachePath = (0, find_cache_dir_1.default)({ name: CACHE_DIR_NAME });
}
const typescript = (0, utils_1.loadTypescript)(appRoot);
/**
* Parse config
*/
const config = new Config_1.Config(appRoot, opts.cachePath, typescript, !!opts.cache).parse();
/**
* Cannot continue when config has errors
*/
if (config.error) {
process.exit(1);
}
/**
* Merge transformers when defined
*/
if (opts.transformers) {
config.options.transformers = config.options.transformers || {};
if (opts.transformers.before) {
config.options.transformers.before = (config.options.transformers.before || []).concat(opts.transformers.before);
}
if (opts.transformers.after) {
config.options.transformers.after = (config.options.transformers.after || []).concat(opts.transformers.after);
}
if (opts.transformers.afterDeclarations) {
config.options.transformers.afterDeclarations = (config.options.transformers.afterDeclarations || []).concat(opts.transformers.afterDeclarations);
}
}
/**
* Instantiate compiler to compile `.ts` files using the typescript compiler. Currently
* we not resolve `.js` files and will never resolve `.tsx` or `.jsx` files.
*/
const compiler = new Compiler_1.Compiler(appRoot, opts.cachePath, typescript, config.options, !!opts.cache);
global[exports.symbols.compiler] = compiler;
global[exports.symbols.config] = config;
(0, pirates_1.addHook)((code, filename) => {
return compiler.compile(filename, code);
}, { exts: EXTS, matcher: () => true });
}
exports.register = register;
+1
View File
@@ -0,0 +1 @@
export {};
+15
View File
@@ -0,0 +1,15 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const index_1 = require("./index");
const CWD = process.env.REQUIRE_TS_CWD || process.cwd();
(0, index_1.register)(CWD, {
cache: !!process.env.REQUIRE_TS_CACHE,
});
+67
View File
@@ -0,0 +1,67 @@
/**
* Exposes the API to write file parsed contents to disk as cache. Handles
* all the complexity of generate correct paths and creating contents
* hash
*/
export declare class Cache {
private appRoot;
private cacheRoot;
constructor(appRoot: string, cacheRoot: string);
/**
* Generates hash from file contents
*/
generateHash(contents: string): string;
/**
* Makes cache path from a given file path and its contents
*/
makeCachePath(filePath: string, contents: string, extname: '.js' | '.json'): string;
/**
* Returns the file contents from the cache (if exists), otherwise
* returns null
*/
get(cachePath: string): string | null;
/**
* Writes file contents to the disk
*/
set(cachePath: string, contents: string): void;
/**
* Clears all the generate cache for a given file
*/
clearForFile(filePath: string): void;
/**
* Clears the cache root folder
*/
clearAll(): void;
}
/**
* A parallel fake implementation of cache that results in noop. Used
* when caching is disabled.
*/
export declare class FakeCache {
constructor();
/**
* Generates hash from file contents
*/
generateHash(_: string): string;
/**
* Makes cache path from a given file path and its contents
*/
makeCachePath(_: string, __: string, ___: '.js' | '.json'): string;
/**
* Returns the file contents from the cache (if exists), otherwise
* returns null
*/
get(_: string): string | null;
/**
* Writes file contents to the disk
*/
set(_: string, __: string): void;
/**
* Clears all the generate cache for a given file
*/
clearForFile(_: string): void;
/**
* Clears the cache root folder
*/
clearAll(): void;
}
+121
View File
@@ -0,0 +1,121 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FakeCache = exports.Cache = void 0;
const path_1 = require("path");
const rev_hash_1 = __importDefault(require("rev-hash"));
const fs_extra_1 = require("fs-extra");
const utils_1 = require("../utils");
/**
* Exposes the API to write file parsed contents to disk as cache. Handles
* all the complexity of generate correct paths and creating contents
* hash
*/
class Cache {
constructor(appRoot, cacheRoot) {
this.appRoot = appRoot;
this.cacheRoot = cacheRoot;
}
/**
* Generates hash from file contents
*/
generateHash(contents) {
return (0, rev_hash_1.default)(contents);
}
/**
* Makes cache path from a given file path and its contents
*/
makeCachePath(filePath, contents, extname) {
const relativeCachePath = (0, utils_1.getCachePathForFile)(this.appRoot, filePath);
const hash = this.generateHash(contents);
return (0, path_1.join)(this.cacheRoot, relativeCachePath, `${hash}${extname}`);
}
/**
* Returns the file contents from the cache (if exists), otherwise
* returns null
*/
get(cachePath) {
try {
const contents = (0, fs_extra_1.readFileSync)(cachePath, 'utf8');
(0, utils_1.debug)('reading from cache "%s"', cachePath);
return contents;
}
catch (error) {
if (error.code === 'ENOENT') {
return null;
}
throw error;
}
}
/**
* Writes file contents to the disk
*/
set(cachePath, contents) {
(0, utils_1.debug)('writing to cache "%s"', cachePath);
(0, fs_extra_1.outputFileSync)(cachePath, contents);
}
/**
* Clears all the generate cache for a given file
*/
clearForFile(filePath) {
(0, utils_1.debug)('clear cache for "%s"', filePath);
const relativeCachePath = (0, utils_1.getCachePathForFile)(this.appRoot, filePath);
(0, fs_extra_1.removeSync)((0, path_1.join)(this.cacheRoot, relativeCachePath));
}
/**
* Clears the cache root folder
*/
clearAll() {
(0, fs_extra_1.removeSync)(this.cacheRoot);
}
}
exports.Cache = Cache;
/**
* A parallel fake implementation of cache that results in noop. Used
* when caching is disabled.
*/
class FakeCache {
constructor() { }
/**
* Generates hash from file contents
*/
generateHash(_) {
return '';
}
/**
* Makes cache path from a given file path and its contents
*/
makeCachePath(_, __, ___) {
return '';
}
/**
* Returns the file contents from the cache (if exists), otherwise
* returns null
*/
get(_) {
return null;
}
/**
* Writes file contents to the disk
*/
set(_, __) { }
/**
* Clears all the generate cache for a given file
*/
clearForFile(_) { }
/**
* Clears the cache root folder
*/
clearAll() { }
}
exports.FakeCache = FakeCache;
+54
View File
@@ -0,0 +1,54 @@
import tsStatic from 'typescript';
import { Transformers } from '../Contracts';
/**
* Exposes the API compile source files using the tsc compiler. No
* type checking takes place.
*/
export declare class Compiler {
private appRoot;
private cacheRoot;
private ts;
private options;
private usesCache;
/**
* In-memory compiled files cache for source maps to work.
*/
private memCache;
/**
* Disk cache
*/
private cache;
/**
* Dignostic reporter to print program errors
*/
private diagnosticsReporter;
private transformers;
constructor(appRoot: string, cacheRoot: string, ts: typeof tsStatic, options: {
compilerOptions: tsStatic.CompilerOptions;
transformers?: Transformers;
}, usesCache?: boolean);
/**
* Patch compiler options to make source map work properly
*/
private patchCompilerOptions;
/**
* Resolves transformer relative from the app root
*/
private resolverTransformer;
/**
* Resolve transformers
*/
private resolveTransformers;
/**
* Setup source maps support to read from in-memory cache
*/
private setupSourceMaps;
/**
* Compiles the file using the typescript compiler
*/
private compileFile;
/**
* Compile typescript source code using the tsc compiler.
*/
compile(filePath: string, contents: string, virtualFile?: boolean): string;
}
+195
View File
@@ -0,0 +1,195 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Compiler = void 0;
const utils_1 = require("@poppinss/utils");
const source_map_support_1 = __importDefault(require("source-map-support"));
const utils_2 = require("../utils");
const Cache_1 = require("../Cache");
const DiagnosticsReporter_1 = require("../DiagnosticsReporter");
/**
* Exposes the API compile source files using the tsc compiler. No
* type checking takes place.
*/
class Compiler {
constructor(appRoot, cacheRoot, ts, options, usesCache = true) {
this.appRoot = appRoot;
this.cacheRoot = cacheRoot;
this.ts = ts;
this.options = options;
this.usesCache = usesCache;
/**
* In-memory compiled files cache for source maps to work.
*/
this.memCache = new Map();
/**
* Disk cache
*/
this.cache = this.usesCache ? new Cache_1.Cache(this.appRoot, this.cacheRoot) : new Cache_1.FakeCache();
/**
* Dignostic reporter to print program errors
*/
this.diagnosticsReporter = new DiagnosticsReporter_1.DiagnosticsReporter(this.appRoot, this.ts, false);
this.transformers = {};
this.patchCompilerOptions();
this.setupSourceMaps();
this.resolveTransformers();
}
/**
* Patch compiler options to make source map work properly
*/
patchCompilerOptions() {
/**
* Force inline source maps. We need this to avoid manual
* lookups
*/
this.options.compilerOptions.inlineSourceMap = true;
/**
* Inline sources
*/
this.options.compilerOptions.inlineSources = true;
/**
* Remove "outDir" property, so that the source maps paths are generated
* relative from the cwd and not the outDir.
*
* ts-node manually patches the source maps to use absolute paths. We cannot
* do same, since we cache files on the disk and changing the folder name
* of project root will corrupt the absolute path names inside the
* source maps.
*/
delete this.options.compilerOptions.outDir;
/**
* Inline source maps and source map cannot be used together
*/
delete this.options.compilerOptions.sourceMap;
}
/**
* Resolves transformer relative from the app root
*/
resolverTransformer(transformer) {
try {
const value = (0, utils_1.esmRequire)(require.resolve(transformer, { paths: [this.appRoot] }));
if (typeof value !== 'function') {
throw new Error('Transformer module must export a function');
}
return value(this.ts, this.appRoot);
}
catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`Unable to resolve transformer "${transformer}" specified in tsconfig.json file`);
}
throw error;
}
}
/**
* Resolve transformers
*/
resolveTransformers() {
if (!this.options.transformers) {
return;
}
if (this.options.transformers.before) {
this.transformers.before = this.options.transformers.before.map((transformer) => {
return this.resolverTransformer(transformer.transform);
});
}
if (this.options.transformers.after) {
this.transformers.after = this.options.transformers.after.map((transformer) => {
return this.resolverTransformer(transformer.transform);
});
}
if (this.options.transformers.afterDeclarations) {
this.transformers.afterDeclarations = this.options.transformers.afterDeclarations.map((transformer) => {
return this.resolverTransformer(transformer.transform);
});
}
}
/**
* Setup source maps support to read from in-memory cache
*/
setupSourceMaps() {
source_map_support_1.default.install({
environment: 'node',
retrieveFile: (pathOrUrl) => {
(0, utils_2.debug)('reading source for "%s"', pathOrUrl);
return this.memCache.get(pathOrUrl) || '';
},
});
}
/**
* Compiles the file using the typescript compiler
*/
compileFile(filePath, contents, virtualFile) {
(0, utils_2.debug)('compiling file using typescript "%s"', filePath);
let { outputText, diagnostics } = this.ts.transpileModule(contents, {
fileName: filePath,
compilerOptions: virtualFile
? {
...this.options.compilerOptions,
rootDir: undefined,
}
: this.options.compilerOptions,
reportDiagnostics: !virtualFile,
transformers: this.transformers,
});
/**
* Report diagnostics if any
*/
if (diagnostics) {
this.diagnosticsReporter.report(diagnostics);
}
/**
* Write to in-memory cache for sourcemaps to work
*/
this.memCache.set(filePath, outputText);
return outputText;
}
/**
* Compile typescript source code using the tsc compiler.
*/
compile(filePath, contents, virtualFile = false) {
/**
* Do not cache virtual files
*/
if (virtualFile) {
(0, utils_2.debug)('compiling virtual file "%s" (no cache)', filePath);
return this.compileFile(filePath, contents, true);
}
(0, utils_2.debug)('compiling file "%s"', filePath);
const cachePath = this.cache.makeCachePath(filePath, contents, '.js');
/**
* Return the file from cache when it exists
*/
const compiledContent = this.cache.get(cachePath);
if (compiledContent) {
/**
* Write to in-memory cache for sourcemaps to work
*/
this.memCache.set(filePath, compiledContent);
return compiledContent;
}
/**
* Compile file using the compiler
*/
const outputText = this.compileFile(filePath, contents, false);
/**
* Write to cache on disk
*/
this.cache.set(cachePath, outputText);
/**
* Return compiled text
*/
return outputText;
}
}
exports.Compiler = Compiler;
+72
View File
@@ -0,0 +1,72 @@
import tsStatic from 'typescript';
import { Transformers } from '../Contracts';
/**
* Exposes the API to parse tsconfig file and cache it until the
* contents of the file are changed.
*/
export declare class Config {
private appRoot;
private cacheRoot;
private ts?;
private usesCache;
static version: string;
/**
* Hard assumption has been made that config file name
* is "tsconfig.json"
*/
private configFilePath;
/**
* Reference to the cache
*/
private cache;
/**
* Dignostic reporter to print program errors
*/
private diagnosticsReporter;
constructor(appRoot: string, cacheRoot: string, ts?: typeof tsStatic | undefined, usesCache?: boolean);
/**
* Returns the raw contents of the config file. We need to read this
* always to generate the hash and then look for the cached config
* file.
*/
private getConfigRawContents;
/**
* Parses the ts config using the typescript compiler
*/
private parseTsConfig;
/**
* Parses the cached config string as JSON. Errors
* are ignored and hence cache is ignored too
*/
private parseConfigAsJson;
/**
* Extracts transformers the tsconfig file contents
*/
private extractTransformers;
/**
* Returns the config file from the cache or returns null when there is
* no cache
*/
getCached(): {
raw: string;
cachePath: string;
cached: null | {
version: string;
options: {
compilerOptions: tsStatic.CompilerOptions;
transformers?: Transformers;
};
};
};
/**
* Parses config and returns the compiler options
*/
parse(): {
version: string;
options: null | {
compilerOptions: tsStatic.CompilerOptions;
transformers?: Transformers;
};
error: null | tsStatic.Diagnostic[];
};
}
+195
View File
@@ -0,0 +1,195 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Config = void 0;
const path_1 = require("path");
const fs_extra_1 = require("fs-extra");
const utils_1 = require("../utils");
const Cache_1 = require("../Cache");
const DiagnosticsReporter_1 = require("../DiagnosticsReporter");
/**
* Exposes the API to parse tsconfig file and cache it until the
* contents of the file are changed.
*/
class Config {
constructor(appRoot, cacheRoot, ts, usesCache = true) {
this.appRoot = appRoot;
this.cacheRoot = cacheRoot;
this.ts = ts;
this.usesCache = usesCache;
/**
* Hard assumption has been made that config file name
* is "tsconfig.json"
*/
this.configFilePath = (0, path_1.join)(this.appRoot, 'tsconfig.json');
/**
* Reference to the cache
*/
this.cache = this.usesCache ? new Cache_1.Cache(this.appRoot, this.cacheRoot) : new Cache_1.FakeCache();
/**
* Dignostic reporter to print program errors
*/
this.diagnosticsReporter = this.ts
? new DiagnosticsReporter_1.DiagnosticsReporter(this.appRoot, this.ts, false)
: undefined;
}
/**
* Returns the raw contents of the config file. We need to read this
* always to generate the hash and then look for the cached config
* file.
*/
getConfigRawContents() {
(0, utils_1.debug)('checking for tsconfig "%s"', this.configFilePath);
try {
return (0, fs_extra_1.readFileSync)(this.configFilePath, 'utf-8');
}
catch (error) {
if (error.code === 'ENOENT') {
throw new Error('"@adonisjs/require-ts" expects the "tsconfig.json" file to exists in the app root');
}
throw error;
}
}
/**
* Parses the ts config using the typescript compiler
*/
parseTsConfig() {
let exception = null;
(0, utils_1.debug)('parse tsconfig file');
if (!this.ts) {
throw new Error('Cannot parse typescript config. Make sure to instantiate Config class with typescript compiler');
}
/**
* Parse config using typescript compiler
*/
const config = this.ts.getParsedCommandLineOfConfigFile(this.configFilePath, {}, {
...this.ts.sys,
useCaseSensitiveFileNames: true,
getCurrentDirectory: () => this.appRoot,
onUnRecoverableConfigFileDiagnostic: (error) => (exception = error),
});
/**
* Return exception as it is
*/
if (exception) {
return {
error: exception,
options: null,
};
}
/**
* Return diagnostic errors if any
*/
if (config.errors && config.errors.length) {
return {
error: config.errors,
options: null,
};
}
/**
* Return compiler options
*/
return {
error: null,
options: config.options,
};
}
/**
* Parses the cached config string as JSON. Errors
* are ignored and hence cache is ignored too
*/
parseConfigAsJson(config) {
if (!config) {
return null;
}
try {
return JSON.parse(config);
}
catch (error) {
return null;
}
}
/**
* Extracts transformers the tsconfig file contents
*/
extractTransformers(rawConfig) {
try {
const transformers = JSON.parse(rawConfig).transformers || {};
return {
before: transformers.before,
after: transformers.after,
afterDeclarations: transformers.afterDeclarations,
};
}
catch (error) { }
}
/**
* Returns the config file from the cache or returns null when there is
* no cache
*/
getCached() {
const rawContents = this.getConfigRawContents();
const cachePath = this.cache.makeCachePath(this.configFilePath, rawContents, '.json');
return {
raw: rawContents,
cachePath,
cached: this.parseConfigAsJson(this.cache.get(cachePath)),
};
}
/**
* Parses config and returns the compiler options
*/
parse() {
if (!this.diagnosticsReporter) {
throw new Error('Cannot parse typescript config. Make sure to instantiate Config class with typescript compiler');
}
/**
* Cache exists and is upto date
*/
const { cached, raw, cachePath } = this.getCached();
if (cached && cached.version === Config.version) {
return {
version: cached.version,
error: null,
options: {
compilerOptions: cached.options.compilerOptions,
transformers: cached.options.transformers,
},
};
}
/**
* Parse the config using the compiler
*/
const config = this.parseTsConfig();
if (config.error) {
this.diagnosticsReporter.report(config.error);
return {
version: Config.version,
options: null,
error: config.error,
};
}
/**
* Write to cache to avoid future parsing
*/
const parsed = {
version: Config.version,
error: null,
options: {
compilerOptions: config.options,
transformers: this.extractTransformers(raw),
},
};
this.cache.set(cachePath, JSON.stringify(parsed));
return parsed;
}
}
exports.Config = Config;
Config.version = 'v1';
+14
View File
@@ -0,0 +1,14 @@
/**
* Custom transformers extracted from the package.json file
*/
export declare type Transformers = {
before?: {
transform: string;
}[];
after?: {
transform: string;
}[];
afterDeclarations?: {
transform: string;
}[];
};
+10
View File
@@ -0,0 +1,10 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,15 @@
import tsStatic from 'typescript';
/**
* Exposes the API to report/print typescript diagnostic reports
*/
export declare class DiagnosticsReporter {
private appRoot;
private ts;
private pretty;
/**
* Diagnostics host
*/
private host;
constructor(appRoot: string, ts: typeof tsStatic, pretty: boolean);
report(diagnostics: tsStatic.Diagnostic[]): void;
}
@@ -0,0 +1,43 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticsReporter = void 0;
/**
* Exposes the API to report/print typescript diagnostic reports
*/
class DiagnosticsReporter {
constructor(appRoot, ts, pretty) {
this.appRoot = appRoot;
this.ts = ts;
this.pretty = pretty;
/**
* Diagnostics host
*/
this.host = {
getNewLine: () => this.ts.sys.newLine,
getCurrentDirectory: () => this.appRoot,
getCanonicalFileName: this.ts.sys.useCaseSensitiveFileNames
? (fileName) => fileName
: (fileName) => fileName.toLowerCase(),
};
}
report(diagnostics) {
if (!diagnostics.length) {
return;
}
if (this.pretty) {
console.log(this.ts.formatDiagnosticsWithColorAndContext(diagnostics, this.host));
}
else {
console.log(this.ts.formatDiagnostics(diagnostics, this.host));
}
}
}
exports.DiagnosticsReporter = DiagnosticsReporter;
+12
View File
@@ -0,0 +1,12 @@
export declare const debug: any;
/**
* Returns the cache directory path for a given file. The idea is to
* use the filename as a directory and then drop files with their
* hashes inside that directory. In case the file gets changed,
* we just need to drop the directory
*/
export declare function getCachePathForFile(cwd: string, location: string): any;
/**
* Loads typescript from the user project dependencies
*/
export declare function loadTypescript(cwd: string): any;
+48
View File
@@ -0,0 +1,48 @@
"use strict";
/*
* @adonisjs/require-ts
*
* (c) Harminder Virk <virk@adonisjs.comharminder@cav.ai>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadTypescript = exports.getCachePathForFile = exports.debug = void 0;
const debug_1 = __importDefault(require("debug"));
const normalize_path_1 = __importDefault(require("normalize-path"));
exports.debug = (0, debug_1.default)('adonis:require-ts');
/**
* Returns the cache directory path for a given file. The idea is to
* use the filename as a directory and then drop files with their
* hashes inside that directory. In case the file gets changed,
* we just need to drop the directory
*/
function getCachePathForFile(cwd, location) {
const tokens = (0, normalize_path_1.default)(location.replace(cwd, '')).split('/');
const fileName = tokens.pop();
tokens.shift();
if (!tokens.length) {
return fileName.replace(/\.\w+$/, '');
}
return `${tokens.join('-')}-${fileName.replace(/\.\w+$/, '')}`;
}
exports.getCachePathForFile = getCachePathForFile;
/**
* Loads typescript from the user project dependencies
*/
function loadTypescript(cwd) {
try {
return require(require.resolve('typescript', { paths: [cwd] }));
}
catch (error) {
if (error.code === 'ENOENT') {
throw new Error('"@adonisjs/require-ts" expects the "typescript" to be installed');
}
throw error;
}
}
exports.loadTypescript = loadTypescript;
+144
View File
@@ -0,0 +1,144 @@
{
"name": "@adonisjs/require-ts",
"version": "2.0.13",
"description": "In memory typescript compiler",
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "cross-env FORCE_COLOR=true node -r ts-node/register/transpile-only bin/test.ts",
"clean": "del-cli build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/require-ts"
},
"keywords": [
"typescript",
"ts",
"tsc",
"ts-node"
],
"author": "virk,adonisjs",
"license": "MIT",
"devDependencies": {
"@adonisjs/mrm-preset": "^5.0.3",
"@japa/assert": "^1.3.6",
"@japa/run-failed-tests": "^1.1.0",
"@japa/runner": "^2.2.1",
"@japa/spec-reporter": "^1.3.1",
"@poppinss/dev-utils": "^2.0.3",
"@types/node": "^18.7.18",
"@types/source-map-support": "^0.5.6",
"benchmark": "^2.1.4",
"commitizen": "^4.2.5",
"cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-adonis": "^2.1.1",
"eslint-plugin-prettier": "^4.2.1",
"github-label-sync": "^2.2.0",
"husky": "^8.0.1",
"mrm": "^4.1.6",
"np": "^7.6.2",
"prettier": "^2.7.1",
"test-console": "^2.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.8.3"
},
"nyc": {
"exclude": [
"test"
],
"extension": [
".ts"
]
},
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js",
"build/register.js",
"build/register.d.ts"
],
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"dependencies": {
"@poppinss/utils": "^5.0.0",
"debug": "^4.3.4",
"find-cache-dir": "^3.3.2",
"fs-extra": "^10.1.0",
"normalize-path": "^3.0.0",
"pirates": "^4.0.5",
"rev-hash": "^3.0.0",
"source-map-support": "^0.5.21"
},
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/adonisjs/require-ts.git"
},
"bugs": {
"url": "https://github.com/adonisjs/require-ts/issues"
},
"homepage": "https://github.com/adonisjs/require-ts#readme",
"mrmConfig": {
"core": true,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "14.15.4",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": true
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
}
}