tsnite 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -32
- package/dist/cache.js +5 -5
- package/dist/cli.js +2 -2
- package/dist/loader.js +91 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,63 +5,163 @@
|
|
|
5
5
|
[](https://img.shields.io/badge/Prettier-de9954?logo=prettier&logoColor=ffffff)
|
|
6
6
|
[](https://img.shields.io/github/license/luas10c/tsnite)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
TypeScript runner for Node.js with watch mode, `tsconfig` path alias support, and extensionless TypeScript import resolution.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install --save-dev tsnite
|
|
10
14
|
```
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
## Usage
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
Run a TypeScript entry file:
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
```bash
|
|
21
|
+
npx tsnite src/index.ts
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run in watch mode:
|
|
17
25
|
|
|
18
26
|
```bash
|
|
19
|
-
|
|
27
|
+
npx tsnite watch src/index.ts
|
|
20
28
|
```
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
Pass extra Node.js arguments after the entry file:
|
|
23
31
|
|
|
24
32
|
```bash
|
|
25
|
-
|
|
33
|
+
npx tsnite src/index.ts --env-file=.env
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use in `package.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "tsnite src/index.ts",
|
|
42
|
+
"dev:watch": "tsnite watch src/index.ts"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
```
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
## Features
|
|
29
48
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
49
|
+
- Run `.ts`, `.tsx`, `.mts`, and `.cts` files directly in Node.js
|
|
50
|
+
- Watch mode with automatic restart
|
|
51
|
+
- Reads the current project's `tsconfig.json`
|
|
52
|
+
- Resolves `compilerOptions.paths`
|
|
53
|
+
- Resolves extensionless TypeScript imports
|
|
54
|
+
- ESM-friendly runtime
|
|
55
|
+
- Decorator support in transpilation
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
## Import Resolution
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
`tsnite` resolves TypeScript files without requiring explicit extensions.
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
These imports work:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import './server'
|
|
65
|
+
import './components/App'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
It will try these TypeScript candidates:
|
|
69
|
+
|
|
70
|
+
- `./server.ts`
|
|
71
|
+
- `./server.tsx`
|
|
72
|
+
- `./server.mts`
|
|
73
|
+
- `./server.cts`
|
|
74
|
+
- `./server/index.ts`
|
|
75
|
+
- `./server/index.tsx`
|
|
76
|
+
- `./server/index.mts`
|
|
77
|
+
- `./server/index.cts`
|
|
78
|
+
|
|
79
|
+
Explicit JavaScript imports such as `.js`, `.mjs`, and `.cjs` are delegated to Node.js.
|
|
80
|
+
|
|
81
|
+
## `tsconfig` Paths
|
|
82
|
+
|
|
83
|
+
`tsnite` supports `compilerOptions.paths` aliases.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"compilerOptions": {
|
|
88
|
+
"baseUrl": ".",
|
|
89
|
+
"paths": {
|
|
90
|
+
"#/*": ["src/*"]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then this works without an extension:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { getMetadata } from '#/common/metadata'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If `baseUrl` is not defined, `tsnite` uses the project root by default.
|
|
103
|
+
|
|
104
|
+
## Watch Mode
|
|
105
|
+
|
|
106
|
+
Customize watched paths and extensions:
|
|
40
107
|
|
|
41
108
|
```bash
|
|
42
|
-
npx tsnite
|
|
109
|
+
npx tsnite watch --include src --exclude dist,coverage,uploads --ext ts,tsx,js,jsx,json src/index.ts
|
|
43
110
|
```
|
|
44
111
|
|
|
45
|
-
|
|
112
|
+
Options:
|
|
113
|
+
|
|
114
|
+
- `--include <paths>` comma-separated paths to watch
|
|
115
|
+
- `--exclude <paths>` comma-separated paths to ignore
|
|
116
|
+
- `--ext <extensions>` comma-separated file extensions to watch
|
|
117
|
+
- `--source-root <path>` base path used by the watcher
|
|
118
|
+
|
|
119
|
+
Defaults:
|
|
120
|
+
|
|
121
|
+
- include: `.`
|
|
122
|
+
- exclude: `node_modules,.git,dist,build,coverage`
|
|
123
|
+
- ext: `ts,tsx,js,jsx,json`
|
|
124
|
+
- source root: `.`
|
|
46
125
|
|
|
47
|
-
##
|
|
126
|
+
## Example
|
|
48
127
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
128
|
+
`tsconfig.json`
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"compilerOptions": {
|
|
133
|
+
"target": "es2024",
|
|
134
|
+
"module": "es2022",
|
|
135
|
+
"moduleResolution": "bundler",
|
|
136
|
+
"baseUrl": ".",
|
|
137
|
+
"paths": {
|
|
138
|
+
"#/*": ["src/*"]
|
|
56
139
|
}
|
|
57
140
|
}
|
|
58
|
-
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`src/index.ts`
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { startServer } from '#/server/start'
|
|
148
|
+
|
|
149
|
+
await startServer()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Run it:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx tsnite src/index.ts
|
|
156
|
+
```
|
|
59
157
|
|
|
60
|
-
##
|
|
158
|
+
## Notes
|
|
61
159
|
|
|
62
|
-
|
|
63
|
-
|
|
160
|
+
- `tsnite` is focused on development-time execution
|
|
161
|
+
- the current project's `tsconfig.json` is used
|
|
162
|
+
- watch mode clears cached resolution and transpilation state before restart
|
|
64
163
|
|
|
65
|
-
|
|
164
|
+
## Links
|
|
66
165
|
|
|
67
|
-
|
|
166
|
+
- npm: <https://www.npmjs.com/package/tsnite>
|
|
167
|
+
- repository: <https://github.com/luas10c/tsnite>
|
package/dist/cache.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, rm, stat } from 'node:fs/promises';
|
|
3
|
-
import
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
4
|
export const resolveCache = new Map();
|
|
5
5
|
const statCache = new Map();
|
|
6
6
|
function getTranspileCacheDir() {
|
|
7
|
-
return
|
|
7
|
+
return join(import.meta.dirname, '..', 'node_modules', '.cache', 'tsnite');
|
|
8
8
|
}
|
|
9
9
|
function getTSConfigPath() {
|
|
10
|
-
return
|
|
10
|
+
return join(import.meta.dirname, '..', 'tsconfig.json');
|
|
11
11
|
}
|
|
12
12
|
function hash(value) {
|
|
13
13
|
return createHash('sha1').update(value).digest('hex');
|
|
@@ -34,7 +34,7 @@ export function invalidateStatCache(filePath) {
|
|
|
34
34
|
statCache.delete(filePath);
|
|
35
35
|
}
|
|
36
36
|
export function getTranspileCacheFile(filePath) {
|
|
37
|
-
return
|
|
37
|
+
return join(getTranspileCacheDir(), `${hash(filePath)}.json`);
|
|
38
38
|
}
|
|
39
39
|
export async function ensureTranspileCacheDir() {
|
|
40
40
|
await mkdir(getTranspileCacheDir(), { recursive: true });
|
|
@@ -43,7 +43,7 @@ export async function clearTranspileCache() {
|
|
|
43
43
|
await rm(getTranspileCacheDir(), { recursive: true, force: true });
|
|
44
44
|
}
|
|
45
45
|
export function isTSConfigPath(filePath) {
|
|
46
|
-
return
|
|
46
|
+
return resolve(filePath) === getTSConfigPath();
|
|
47
47
|
}
|
|
48
48
|
export async function invalidateFileCaches(filePath) {
|
|
49
49
|
invalidateStatCache(filePath);
|
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,7 @@ process.on('SIGTERM', function () {
|
|
|
38
38
|
cleanup('SIGTERM');
|
|
39
39
|
});
|
|
40
40
|
function spawn(entry, nodeArgs) {
|
|
41
|
-
const child = fork(join(
|
|
41
|
+
const child = fork(join(import.meta.dirname, '..', entry), {
|
|
42
42
|
stdio: 'inherit',
|
|
43
43
|
execArgv: [
|
|
44
44
|
'--enable-source-maps',
|
|
@@ -176,7 +176,7 @@ async function handler(entry, options, nodeArgs, isWatch) {
|
|
|
176
176
|
eventName !== 'unlink') {
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
|
-
await invalidateFileCaches(isAbsolute(changedPath) ? changedPath : (resolve(
|
|
179
|
+
await invalidateFileCaches(isAbsolute(changedPath) ? changedPath : (resolve(import.meta.dirname, '..', changedPath)));
|
|
180
180
|
restartDebounced(`Change detected (${eventName}): ${changedPath}`);
|
|
181
181
|
});
|
|
182
182
|
}
|
package/dist/loader.js
CHANGED
|
@@ -1,14 +1,72 @@
|
|
|
1
1
|
import { transformFile } from '@swc/core';
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
3
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
4
|
-
import
|
|
4
|
+
import { extname, join, dirname } from 'node:path';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { parse } from './parse.js';
|
|
7
|
-
import {
|
|
7
|
+
import { ensureTranspileCacheDir, existsWithCache, getTranspileCacheFile, resolveCache } from './cache.js';
|
|
8
8
|
const tsconfigCache = { paths: null, baseUrl: null };
|
|
9
9
|
const transpileCache = new Map();
|
|
10
10
|
const MAX_TRANSPILE_CACHE_ENTRIES = 256;
|
|
11
|
-
const
|
|
11
|
+
const TS_EXTENSIONS = ['.cts', '.mts', '.tsx', '.ts'];
|
|
12
|
+
function hasTypeScriptExtension(value) {
|
|
13
|
+
return TS_EXTENSIONS.some((extension) => value.endsWith(extension));
|
|
14
|
+
}
|
|
15
|
+
function isTypeScriptSpecifier(specifier) {
|
|
16
|
+
const extension = extname(specifier);
|
|
17
|
+
return extension === '' || hasTypeScriptExtension(extension);
|
|
18
|
+
}
|
|
19
|
+
function getTypeScriptTryFiles(basePath) {
|
|
20
|
+
return [
|
|
21
|
+
basePath,
|
|
22
|
+
...TS_EXTENSIONS.map((extension) => basePath + extension),
|
|
23
|
+
...TS_EXTENSIONS.map((extension) => join(basePath, 'index' + extension))
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
async function resolveTypeScriptFile(basePath) {
|
|
27
|
+
for (const file of getTypeScriptTryFiles(basePath)) {
|
|
28
|
+
if (await existsWithCache(file)) {
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function matchPathPattern(specifier, pattern) {
|
|
35
|
+
const starIndex = pattern.indexOf('*');
|
|
36
|
+
if (starIndex === -1) {
|
|
37
|
+
return specifier === pattern ? [] : null;
|
|
38
|
+
}
|
|
39
|
+
const prefix = pattern.slice(0, starIndex);
|
|
40
|
+
const suffix = pattern.slice(starIndex + 1);
|
|
41
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return [specifier.slice(prefix.length, specifier.length - suffix.length)];
|
|
45
|
+
}
|
|
46
|
+
function applyPathMapping(target, matches) {
|
|
47
|
+
let mapped = target;
|
|
48
|
+
for (const match of matches) {
|
|
49
|
+
mapped = mapped.replace('*', match);
|
|
50
|
+
}
|
|
51
|
+
return mapped;
|
|
52
|
+
}
|
|
53
|
+
async function resolveTsConfigPath(specifier, paths, baseUrl) {
|
|
54
|
+
if (!paths || !baseUrl || !isTypeScriptSpecifier(specifier)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
58
|
+
const matches = matchPathPattern(specifier, pattern);
|
|
59
|
+
if (!matches)
|
|
60
|
+
continue;
|
|
61
|
+
for (const target of targets) {
|
|
62
|
+
const mappedTarget = applyPathMapping(target, matches);
|
|
63
|
+
const resolved = await resolveTypeScriptFile(join(baseUrl, mappedTarget));
|
|
64
|
+
if (resolved)
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
12
70
|
function hash(value) {
|
|
13
71
|
return createHash('sha1').update(value).digest('hex');
|
|
14
72
|
}
|
|
@@ -69,13 +127,13 @@ async function loadTSConfig() {
|
|
|
69
127
|
return tsconfigCache;
|
|
70
128
|
}
|
|
71
129
|
try {
|
|
72
|
-
const data = await readFile(
|
|
130
|
+
const data = await readFile(join(process.cwd(), 'tsconfig.json'), 'utf-8');
|
|
73
131
|
const { compilerOptions } = parse(data);
|
|
74
132
|
const paths = compilerOptions?.paths ?? null;
|
|
75
133
|
const baseUrl = compilerOptions?.baseUrl;
|
|
76
134
|
tsconfigCache.paths = paths || null;
|
|
77
135
|
tsconfigCache.baseUrl =
|
|
78
|
-
baseUrl ?
|
|
136
|
+
baseUrl ? join(process.cwd(), baseUrl) : process.cwd();
|
|
79
137
|
return tsconfigCache;
|
|
80
138
|
}
|
|
81
139
|
catch {
|
|
@@ -85,9 +143,23 @@ async function loadTSConfig() {
|
|
|
85
143
|
}
|
|
86
144
|
}
|
|
87
145
|
export async function resolve(specifier, ctx, next) {
|
|
146
|
+
const { paths, baseUrl } = await loadTSConfig();
|
|
147
|
+
const resolvedTsConfigPath = await resolveTsConfigPath(specifier, paths, baseUrl);
|
|
148
|
+
if (resolvedTsConfigPath) {
|
|
149
|
+
const url = pathToFileURL(resolvedTsConfigPath).href;
|
|
150
|
+
resolveCache.set(`${ctx.parentURL}::${specifier}`, url);
|
|
151
|
+
return {
|
|
152
|
+
url,
|
|
153
|
+
format: 'module',
|
|
154
|
+
shortCircuit: true
|
|
155
|
+
};
|
|
156
|
+
}
|
|
88
157
|
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
89
158
|
return next(specifier, ctx);
|
|
90
159
|
}
|
|
160
|
+
if (!isTypeScriptSpecifier(specifier)) {
|
|
161
|
+
return next(specifier, ctx);
|
|
162
|
+
}
|
|
91
163
|
const cacheKey = `${ctx.parentURL}::${specifier}`;
|
|
92
164
|
const cached = resolveCache.get(cacheKey);
|
|
93
165
|
if (cached !== undefined) {
|
|
@@ -101,8 +173,8 @@ export async function resolve(specifier, ctx, next) {
|
|
|
101
173
|
};
|
|
102
174
|
}
|
|
103
175
|
const parentPath = fileURLToPath(ctx.parentURL);
|
|
104
|
-
const parentDir =
|
|
105
|
-
const basePath =
|
|
176
|
+
const parentDir = dirname(parentPath);
|
|
177
|
+
const basePath = join(parentDir, specifier);
|
|
106
178
|
const tryFiles = [
|
|
107
179
|
basePath,
|
|
108
180
|
basePath + '.ts',
|
|
@@ -112,13 +184,13 @@ export async function resolve(specifier, ctx, next) {
|
|
|
112
184
|
basePath + '.js',
|
|
113
185
|
basePath + '.mjs',
|
|
114
186
|
basePath + '.cjs',
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
187
|
+
join(basePath, 'index.ts'),
|
|
188
|
+
join(basePath, 'index.tsx'),
|
|
189
|
+
join(basePath, 'index.mts'),
|
|
190
|
+
join(basePath, 'index.cts'),
|
|
191
|
+
join(basePath, 'index.js'),
|
|
192
|
+
join(basePath, 'index.mjs'),
|
|
193
|
+
join(basePath, 'index.cjs')
|
|
122
194
|
];
|
|
123
195
|
for (const file of tryFiles) {
|
|
124
196
|
if (await existsWithCache(file)) {
|
|
@@ -135,13 +207,16 @@ export async function resolve(specifier, ctx, next) {
|
|
|
135
207
|
return next(specifier, ctx);
|
|
136
208
|
}
|
|
137
209
|
export async function load(url, ctx, next) {
|
|
138
|
-
if (!url.startsWith('file://') || !
|
|
210
|
+
if (!url.startsWith('file://') || !hasTypeScriptExtension(url)) {
|
|
139
211
|
return next(url, ctx);
|
|
140
212
|
}
|
|
141
213
|
const { paths, baseUrl } = await loadTSConfig();
|
|
142
214
|
const filename = fileURLToPath(url);
|
|
143
215
|
const fileStats = await stat(filename);
|
|
144
|
-
const configHash = hash(JSON.stringify({
|
|
216
|
+
const configHash = hash(JSON.stringify({
|
|
217
|
+
baseUrl: baseUrl || process.cwd(),
|
|
218
|
+
paths: paths ?? {}
|
|
219
|
+
}));
|
|
145
220
|
const cachedCode = await readCachedTranspile(filename, fileStats.mtimeMs, fileStats.size, configHash);
|
|
146
221
|
if (cachedCode !== null) {
|
|
147
222
|
return { format: 'module', source: cachedCode, shortCircuit: true };
|
|
@@ -186,12 +261,3 @@ export async function load(url, ctx, next) {
|
|
|
186
261
|
});
|
|
187
262
|
return { format: 'module', source: code, shortCircuit: true };
|
|
188
263
|
}
|
|
189
|
-
export async function resetLoaderState(options) {
|
|
190
|
-
tsconfigCache.paths = null;
|
|
191
|
-
tsconfigCache.baseUrl = null;
|
|
192
|
-
transpileCache.clear();
|
|
193
|
-
resolveCache.clear();
|
|
194
|
-
if (!options?.preserveTranspileCache) {
|
|
195
|
-
await clearTranspileCache();
|
|
196
|
-
}
|
|
197
|
-
}
|