tsnite 0.1.0 → 0.1.1

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/dist/cache.js CHANGED
@@ -1,17 +1,61 @@
1
- import { stat } from 'node:fs/promises';
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, rm, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
2
4
  export const resolveCache = new Map();
3
5
  const statCache = new Map();
6
+ function getTranspileCacheDir() {
7
+ return path.join(process.cwd(), 'node_modules', '.cache', 'tsnite');
8
+ }
9
+ function getTSConfigPath() {
10
+ return path.join(process.cwd(), 'tsconfig.json');
11
+ }
12
+ function hash(value) {
13
+ return createHash('sha1').update(value).digest('hex');
14
+ }
4
15
  export async function existsWithCache(filePath) {
5
16
  const cached = statCache.get(filePath);
6
17
  if (cached)
7
18
  return cached.exists;
8
19
  try {
9
20
  const stats = await stat(filePath);
10
- statCache.set(filePath, { exists: true, mtime: stats.mtimeMs });
11
- return true;
21
+ const exists = stats.isFile();
22
+ statCache.set(filePath, { exists, mtime: stats.mtimeMs });
23
+ return exists;
12
24
  }
13
25
  catch {
14
26
  statCache.set(filePath, { exists: false });
15
27
  return false;
16
28
  }
17
29
  }
30
+ export function clearResolveCache() {
31
+ resolveCache.clear();
32
+ }
33
+ export function invalidateStatCache(filePath) {
34
+ statCache.delete(filePath);
35
+ }
36
+ export function getTranspileCacheFile(filePath) {
37
+ return path.join(getTranspileCacheDir(), `${hash(filePath)}.json`);
38
+ }
39
+ export async function ensureTranspileCacheDir() {
40
+ await mkdir(getTranspileCacheDir(), { recursive: true });
41
+ }
42
+ export async function clearTranspileCache() {
43
+ await rm(getTranspileCacheDir(), { recursive: true, force: true });
44
+ }
45
+ export function isTSConfigPath(filePath) {
46
+ return path.resolve(filePath) === getTSConfigPath();
47
+ }
48
+ export async function invalidateFileCaches(filePath) {
49
+ invalidateStatCache(filePath);
50
+ clearResolveCache();
51
+ if (isTSConfigPath(filePath)) {
52
+ await clearTranspileCache();
53
+ return;
54
+ }
55
+ try {
56
+ await rm(getTranspileCacheFile(filePath), { force: true });
57
+ }
58
+ catch {
59
+ // Ignore cache eviction failures during watch restarts.
60
+ }
61
+ }
package/dist/cli.js CHANGED
@@ -1,24 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from 'commander';
3
- import { extname, join, relative } from 'node:path';
3
+ import { extname, isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { fork } from 'node:child_process';
6
6
  import { watch } from 'chokidar';
7
7
  import { name, description, version } from './metadata.js';
8
+ import { clearResolveCache, invalidateFileCaches } from './cache.js';
9
+ import { debounce, yellow } from './util.js';
8
10
  const DEFAULT_INCLUDE_PATHS = ['.'];
9
11
  const DEFAULT_EXCLUDE_PATHS = ['dist', 'build', 'coverage'];
10
12
  const DEFAULT_WATCH_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'json'];
11
13
  const INTERNAL_IGNORED_PATHS = ['node_modules', '.git'];
12
- const pids = new Set();
14
+ const WATCH_DEBOUNCE_MS = 100;
15
+ const CHILD_EXIT_TIMEOUT_MS = 300;
16
+ const children = new Set();
13
17
  program
14
18
  .name(name)
15
19
  .description(description)
16
20
  .version(version, '-v, --version', 'Output the current version')
17
21
  .showSuggestionAfterError();
18
- function cleanup() {
19
- for (const pid of pids.values()) {
22
+ function cleanup(signal) {
23
+ process.stdout.write(`${yellow(`Received ${signal}. Stopping watcher and child processes...`)}\n`);
24
+ for (const child of children.values()) {
20
25
  try {
21
- process.kill(pid, 'SIGTERM');
26
+ child.kill('SIGTERM');
22
27
  }
23
28
  catch {
24
29
  //
@@ -26,10 +31,14 @@ function cleanup() {
26
31
  }
27
32
  process.exit(0);
28
33
  }
29
- process.on('SIGINT', cleanup);
30
- process.on('SIGTERM', cleanup);
34
+ process.on('SIGINT', function () {
35
+ cleanup('SIGINT');
36
+ });
37
+ process.on('SIGTERM', function () {
38
+ cleanup('SIGTERM');
39
+ });
31
40
  function spawn(entry, nodeArgs) {
32
- const { pid } = fork(join(process.cwd(), entry), {
41
+ const child = fork(join(process.cwd(), entry), {
33
42
  stdio: 'inherit',
34
43
  execArgv: [
35
44
  '--enable-source-maps',
@@ -39,8 +48,25 @@ function spawn(entry, nodeArgs) {
39
48
  ...nodeArgs
40
49
  ]
41
50
  });
42
- if (pid)
43
- pids.add(pid);
51
+ children.add(child);
52
+ child.once('exit', function () {
53
+ children.delete(child);
54
+ });
55
+ }
56
+ async function waitForChildExit(child) {
57
+ if (child.exitCode !== null || child.signalCode !== null)
58
+ return true;
59
+ return await new Promise(function (resolve) {
60
+ const timeout = setTimeout(function () {
61
+ child.off('exit', handleExit);
62
+ resolve(false);
63
+ }, CHILD_EXIT_TIMEOUT_MS);
64
+ function handleExit() {
65
+ clearTimeout(timeout);
66
+ resolve(true);
67
+ }
68
+ child.once('exit', handleExit);
69
+ });
44
70
  }
45
71
  function normalizePath(value) {
46
72
  return value
@@ -51,11 +77,25 @@ function normalizePath(value) {
51
77
  function normalizeExt(value) {
52
78
  return value.trim().replace(/^\./, '').toLowerCase();
53
79
  }
80
+ function hasIgnoredSegment(filePath, ignoredSegments) {
81
+ let start = 0;
82
+ for (let index = 0; index <= filePath.length; index++) {
83
+ if (index !== filePath.length && filePath[index] !== '/')
84
+ continue;
85
+ if (ignoredSegments.has(filePath.slice(start, index)))
86
+ return true;
87
+ start = index + 1;
88
+ }
89
+ return false;
90
+ }
54
91
  function createWatchConfig(options) {
55
92
  const includePaths = options.include.map((value) => join(options.sourceRoot, value));
56
93
  const excludePaths = options.exclude.map(normalizePath).filter(Boolean);
57
- const excludeSet = new Set(excludePaths);
94
+ const excludedExactPaths = new Set(excludePaths);
95
+ const excludedSegments = new Set(excludePaths.filter((value) => !value.includes('/')));
96
+ const excludedPrefixes = excludePaths.map((value) => value + '/');
58
97
  const allowedExts = new Set(options.ext.map(normalizeExt).filter(Boolean));
98
+ const internalIgnoredSegments = new Set(INTERNAL_IGNORED_PATHS);
59
99
  function toRelativeFromRoot(sourceRoot, filePath) {
60
100
  const rel = normalizePath(relative(sourceRoot, filePath));
61
101
  return rel === '' ? '.' : rel;
@@ -66,14 +106,16 @@ function createWatchConfig(options) {
66
106
  return true;
67
107
  if (rel === '.')
68
108
  return false;
69
- const segments = rel.split('/');
70
- if (segments.some((segment) => INTERNAL_IGNORED_PATHS.includes(segment)))
109
+ if (hasIgnoredSegment(rel, internalIgnoredSegments))
71
110
  return true;
72
- if (segments.some((segment) => excludeSet.has(segment)))
111
+ if (hasIgnoredSegment(rel, excludedSegments))
73
112
  return true;
74
- const isExcludedByPath = excludePaths.some((excluded) => rel === excluded || rel.startsWith(excluded + '/'));
75
- if (isExcludedByPath)
113
+ if (excludedExactPaths.has(rel))
76
114
  return true;
115
+ for (const excludedPrefix of excludedPrefixes) {
116
+ if (rel.startsWith(excludedPrefix))
117
+ return true;
118
+ }
77
119
  if (!stats || stats.isDirectory())
78
120
  return false;
79
121
  const extension = extname(rel).slice(1).toLowerCase();
@@ -88,7 +130,37 @@ function createWatchConfig(options) {
88
130
  };
89
131
  }
90
132
  async function handler(entry, options, nodeArgs, isWatch) {
133
+ async function restart(reason) {
134
+ process.stdout.write('\x1Bc');
135
+ if (reason) {
136
+ console.log(yellow(reason));
137
+ }
138
+ for (const child of children.values()) {
139
+ try {
140
+ child.kill('SIGTERM');
141
+ }
142
+ catch {
143
+ continue;
144
+ }
145
+ const exited = await waitForChildExit(child);
146
+ if (exited)
147
+ continue;
148
+ console.log(yellow(`${reason ?? 'Restart requested.'} Process hasn't exited. Killing process...`));
149
+ try {
150
+ child.kill('SIGKILL');
151
+ }
152
+ catch {
153
+ //
154
+ }
155
+ }
156
+ clearResolveCache();
157
+ spawn(entry, nodeArgs);
158
+ }
159
+ const restartDebounced = debounce(restart, WATCH_DEBOUNCE_MS);
91
160
  process.stdout.write('\x1Bc');
161
+ if (isWatch) {
162
+ console.log(yellow('Watching for changes...'));
163
+ }
92
164
  spawn(entry, nodeArgs);
93
165
  if (!isWatch)
94
166
  return;
@@ -98,18 +170,14 @@ async function handler(entry, options, nodeArgs, isWatch) {
98
170
  ignoreInitial: true,
99
171
  ignored
100
172
  });
101
- watcher.on('change', async function () {
102
- process.stdout.write('\x1Bc');
103
- for (const pid of pids.values()) {
104
- try {
105
- process.kill(pid);
106
- }
107
- catch {
108
- //
109
- }
173
+ watcher.on('all', async function (eventName, changedPath) {
174
+ if (eventName !== 'change' &&
175
+ eventName !== 'add' &&
176
+ eventName !== 'unlink') {
177
+ return;
110
178
  }
111
- pids.clear();
112
- spawn(entry, nodeArgs);
179
+ await invalidateFileCaches(isAbsolute(changedPath) ? changedPath : (resolve(process.cwd(), changedPath)));
180
+ restartDebounced(`Change detected (${eventName}): ${changedPath}`);
113
181
  });
114
182
  }
115
183
  function parseCsv(value) {
package/dist/loader.js CHANGED
@@ -1,19 +1,81 @@
1
1
  import { transformFile } from '@swc/core';
2
2
  import { fileURLToPath, pathToFileURL } from 'node:url';
3
- import { readFile } from 'node:fs/promises';
3
+ import { readFile, stat, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { createHash } from 'node:crypto';
5
6
  import { parse } from './parse.js';
6
- import { resolveCache, existsWithCache } from './cache.js';
7
+ import { clearTranspileCache, ensureTranspileCacheDir, existsWithCache, getTranspileCacheFile, resolveCache } from './cache.js';
7
8
  const tsconfigCache = { paths: null, baseUrl: null };
9
+ const transpileCache = new Map();
10
+ const MAX_TRANSPILE_CACHE_ENTRIES = 256;
11
+ const TS_SOURCE_RE = /\.(cts|mts|tsx|ts)$/;
12
+ function hash(value) {
13
+ return createHash('sha1').update(value).digest('hex');
14
+ }
15
+ function getMemoryCachedTranspile(filename, mtimeMs, size, configHash) {
16
+ const memoryCached = transpileCache.get(filename);
17
+ if (!memoryCached ||
18
+ memoryCached.mtimeMs !== mtimeMs ||
19
+ memoryCached.size !== size ||
20
+ memoryCached.configHash !== configHash) {
21
+ return null;
22
+ }
23
+ transpileCache.delete(filename);
24
+ transpileCache.set(filename, memoryCached);
25
+ return memoryCached.code;
26
+ }
27
+ function setMemoryCachedTranspile(filename, entry) {
28
+ if (transpileCache.has(filename)) {
29
+ transpileCache.delete(filename);
30
+ }
31
+ transpileCache.set(filename, entry);
32
+ if (transpileCache.size <= MAX_TRANSPILE_CACHE_ENTRIES)
33
+ return;
34
+ const oldestKey = transpileCache.keys().next().value;
35
+ if (oldestKey)
36
+ transpileCache.delete(oldestKey);
37
+ }
38
+ async function readCachedTranspile(filename, mtimeMs, size, configHash) {
39
+ const memoryCached = getMemoryCachedTranspile(filename, mtimeMs, size, configHash);
40
+ if (memoryCached !== null)
41
+ return memoryCached;
42
+ try {
43
+ const raw = await readFile(getTranspileCacheFile(filename), 'utf8');
44
+ const diskCached = JSON.parse(raw);
45
+ if (diskCached.mtimeMs !== mtimeMs ||
46
+ diskCached.size !== size ||
47
+ diskCached.configHash !== configHash) {
48
+ return null;
49
+ }
50
+ setMemoryCachedTranspile(filename, diskCached);
51
+ return diskCached.code;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function writeCachedTranspile(filename, entry) {
58
+ setMemoryCachedTranspile(filename, entry);
59
+ try {
60
+ await ensureTranspileCacheDir();
61
+ await writeFile(getTranspileCacheFile(filename), JSON.stringify(entry));
62
+ }
63
+ catch {
64
+ // Ignore cache write failures and continue with fresh output.
65
+ }
66
+ }
8
67
  async function loadTSConfig() {
9
68
  if (tsconfigCache.paths !== null && tsconfigCache.baseUrl !== null) {
10
69
  return tsconfigCache;
11
70
  }
12
71
  try {
13
72
  const data = await readFile(path.join(process.cwd(), 'tsconfig.json'), 'utf-8');
14
- const { compilerOptions: { paths, baseUrl } } = parse(data);
73
+ const { compilerOptions } = parse(data);
74
+ const paths = compilerOptions?.paths ?? null;
75
+ const baseUrl = compilerOptions?.baseUrl;
15
76
  tsconfigCache.paths = paths || null;
16
- tsconfigCache.baseUrl = path.join(process.cwd(), baseUrl) || process.cwd();
77
+ tsconfigCache.baseUrl =
78
+ baseUrl ? path.resolve(process.cwd(), baseUrl) : process.cwd();
17
79
  return tsconfigCache;
18
80
  }
19
81
  catch {
@@ -28,7 +90,10 @@ export async function resolve(specifier, ctx, next) {
28
90
  }
29
91
  const cacheKey = `${ctx.parentURL}::${specifier}`;
30
92
  const cached = resolveCache.get(cacheKey);
31
- if (cached) {
93
+ if (cached !== undefined) {
94
+ if (cached === null) {
95
+ return next(specifier, ctx);
96
+ }
32
97
  return {
33
98
  url: cached,
34
99
  format: 'module',
@@ -42,12 +107,18 @@ export async function resolve(specifier, ctx, next) {
42
107
  basePath,
43
108
  basePath + '.ts',
44
109
  basePath + '.tsx',
110
+ basePath + '.mts',
111
+ basePath + '.cts',
45
112
  basePath + '.js',
46
113
  basePath + '.mjs',
114
+ basePath + '.cjs',
47
115
  path.join(basePath, 'index.ts'),
48
116
  path.join(basePath, 'index.tsx'),
117
+ path.join(basePath, 'index.mts'),
118
+ path.join(basePath, 'index.cts'),
49
119
  path.join(basePath, 'index.js'),
50
- path.join(basePath, 'index.mjs')
120
+ path.join(basePath, 'index.mjs'),
121
+ path.join(basePath, 'index.cjs')
51
122
  ];
52
123
  for (const file of tryFiles) {
53
124
  if (await existsWithCache(file)) {
@@ -60,14 +131,21 @@ export async function resolve(specifier, ctx, next) {
60
131
  };
61
132
  }
62
133
  }
134
+ resolveCache.set(cacheKey, null);
63
135
  return next(specifier, ctx);
64
136
  }
65
137
  export async function load(url, ctx, next) {
66
- if (!url.startsWith('file://') || !url.endsWith('.ts')) {
138
+ if (!url.startsWith('file://') || !TS_SOURCE_RE.test(url)) {
67
139
  return next(url, ctx);
68
140
  }
69
141
  const { paths, baseUrl } = await loadTSConfig();
70
142
  const filename = fileURLToPath(url);
143
+ const fileStats = await stat(filename);
144
+ const configHash = hash(JSON.stringify({ baseUrl: baseUrl || process.cwd(), paths: paths ?? {} }));
145
+ const cachedCode = await readCachedTranspile(filename, fileStats.mtimeMs, fileStats.size, configHash);
146
+ if (cachedCode !== null) {
147
+ return { format: 'module', source: cachedCode, shortCircuit: true };
148
+ }
71
149
  const { code } = await transformFile(filename, {
72
150
  filename,
73
151
  jsc: {
@@ -100,5 +178,20 @@ export async function load(url, ctx, next) {
100
178
  },
101
179
  sourceMaps: 'inline'
102
180
  });
181
+ await writeCachedTranspile(filename, {
182
+ code,
183
+ mtimeMs: fileStats.mtimeMs,
184
+ size: fileStats.size,
185
+ configHash
186
+ });
103
187
  return { format: 'module', source: code, shortCircuit: true };
104
188
  }
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
+ }
package/dist/util.js ADDED
@@ -0,0 +1,14 @@
1
+ export function debounce(callback, waitMs) {
2
+ let timer;
3
+ return function (...args) {
4
+ if (timer)
5
+ clearTimeout(timer);
6
+ timer = setTimeout(() => callback(...args), waitMs);
7
+ };
8
+ }
9
+ function color(code, value) {
10
+ return `\u001B[${code}m${value}\u001B[0m`;
11
+ }
12
+ export function yellow(value) {
13
+ return color(33, value);
14
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tsnite",
3
3
  "description": "TypeScript at full throttle—fast, safe, unstoppable. 🚀",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "esm",
@@ -32,30 +32,29 @@
32
32
  "prepare": "husky"
33
33
  },
34
34
  "dependencies": {
35
- "@swc/core": "^1.15.18",
35
+ "@swc/core": "^1.15.24",
36
36
  "chokidar": "^5.0.0",
37
37
  "commander": "^14.0.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@commitlint/cli": "^20.4.3",
41
- "@commitlint/config-conventional": "^20.4.3",
40
+ "@commitlint/cli": "^20.5.0",
41
+ "@commitlint/config-conventional": "^20.5.0",
42
42
  "@eslint/js": "^10.0.1",
43
43
  "@jest/globals": "^30.3.0",
44
44
  "@swc/jest": "^0.2.39",
45
- "eslint": "^10.0.3",
46
- "eslint-plugin-jest": "^29.15.0",
45
+ "eslint": "^10.2.0",
46
+ "eslint-plugin-jest": "^29.15.2",
47
47
  "eslint-plugin-prettier": "^5.5.5",
48
- "globals": "^17.4.0",
48
+ "globals": "^17.5.0",
49
49
  "husky": "^9.1.7",
50
50
  "jest": "^30.3.0",
51
51
  "jest-environment-node": "^30.3.0",
52
- "jest-mock-extended": "^4.0.0",
53
- "lint-staged": "^16.3.3",
54
- "prettier": "^3.8.1",
52
+ "lint-staged": "^16.4.0",
53
+ "prettier": "^3.8.2",
55
54
  "rimraf": "^6.1.3",
56
55
  "tsc-alias": "^1.8.16",
57
- "typescript": "^5.9.3",
58
- "typescript-eslint": "^8.57.0"
56
+ "typescript": "^6.0.2",
57
+ "typescript-eslint": "^8.58.1"
59
58
  },
60
59
  "engines": {
61
60
  "node": "^20.9.0 || ^22.11.0 || ^24.11.0"