@plugjs/plug 0.1.4 → 0.2.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.
Files changed (115) hide show
  1. package/README.md +2 -2
  2. package/cli/plug.mjs +1397 -0
  3. package/cli/ts-loader.mjs +273 -0
  4. package/cli/tsrun.mjs +136 -0
  5. package/dist/asserts.cjs +1 -0
  6. package/dist/asserts.cjs.map +1 -1
  7. package/dist/asserts.mjs +1 -0
  8. package/dist/asserts.mjs.map +1 -1
  9. package/dist/build.cjs +51 -24
  10. package/dist/build.cjs.map +2 -2
  11. package/dist/build.d.ts +7 -3
  12. package/dist/build.mjs +50 -24
  13. package/dist/build.mjs.map +2 -2
  14. package/dist/files.cjs +9 -0
  15. package/dist/files.cjs.map +1 -1
  16. package/dist/files.mjs +9 -0
  17. package/dist/files.mjs.map +1 -1
  18. package/dist/fork.cjs +2 -0
  19. package/dist/fork.cjs.map +1 -1
  20. package/dist/fork.mjs +2 -0
  21. package/dist/fork.mjs.map +1 -1
  22. package/dist/fs.cjs +11 -4
  23. package/dist/fs.cjs.map +1 -1
  24. package/dist/fs.mjs +7 -4
  25. package/dist/fs.mjs.map +1 -1
  26. package/dist/helpers.cjs +1 -1
  27. package/dist/helpers.cjs.map +1 -1
  28. package/dist/helpers.mjs +1 -1
  29. package/dist/helpers.mjs.map +1 -1
  30. package/dist/index.cjs +7 -0
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.mjs +2 -0
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/logging/levels.cjs.map +1 -1
  36. package/dist/logging/levels.mjs.map +1 -1
  37. package/dist/logging/logger.cjs.map +1 -1
  38. package/dist/logging/logger.mjs.map +1 -1
  39. package/dist/logging/options.cjs +5 -0
  40. package/dist/logging/options.cjs.map +1 -1
  41. package/dist/logging/options.mjs +5 -0
  42. package/dist/logging/options.mjs.map +1 -1
  43. package/dist/logging/report.cjs.map +1 -1
  44. package/dist/logging/report.mjs.map +1 -1
  45. package/dist/logging/spinner.cjs +12 -0
  46. package/dist/logging/spinner.cjs.map +1 -1
  47. package/dist/logging/spinner.mjs +12 -0
  48. package/dist/logging/spinner.mjs.map +1 -1
  49. package/dist/paths.cjs.map +1 -1
  50. package/dist/paths.mjs.map +1 -1
  51. package/dist/pipe.cjs +34 -0
  52. package/dist/pipe.cjs.map +1 -1
  53. package/dist/pipe.mjs +34 -0
  54. package/dist/pipe.mjs.map +1 -1
  55. package/dist/plugs/debug.cjs.map +1 -1
  56. package/dist/plugs/debug.mjs.map +1 -1
  57. package/dist/plugs/edit.cjs +1 -1
  58. package/dist/plugs/edit.cjs.map +1 -1
  59. package/dist/plugs/edit.d.ts +2 -2
  60. package/dist/plugs/edit.mjs +1 -1
  61. package/dist/plugs/edit.mjs.map +1 -1
  62. package/dist/plugs/esbuild/fix-extensions.cjs +4 -0
  63. package/dist/plugs/esbuild/fix-extensions.cjs.map +1 -1
  64. package/dist/plugs/esbuild/fix-extensions.mjs.map +1 -1
  65. package/dist/plugs/esbuild.cjs +6 -2
  66. package/dist/plugs/esbuild.cjs.map +1 -1
  67. package/dist/plugs/esbuild.mjs +6 -2
  68. package/dist/plugs/esbuild.mjs.map +1 -1
  69. package/dist/plugs/exec.cjs.map +1 -1
  70. package/dist/plugs/exec.mjs.map +1 -1
  71. package/dist/plugs/exports.cjs +122 -0
  72. package/dist/plugs/exports.cjs.map +6 -0
  73. package/dist/plugs/exports.d.ts +17 -0
  74. package/dist/plugs/exports.mjs +105 -0
  75. package/dist/plugs/exports.mjs.map +6 -0
  76. package/dist/plugs/filter.cjs.map +1 -1
  77. package/dist/plugs/filter.mjs.map +1 -1
  78. package/dist/plugs.cjs +1 -0
  79. package/dist/plugs.cjs.map +1 -1
  80. package/dist/plugs.d.ts +1 -0
  81. package/dist/plugs.mjs +1 -0
  82. package/dist/plugs.mjs.map +1 -1
  83. package/dist/types.d.ts +5 -1
  84. package/dist/utils/exec.cjs +9 -0
  85. package/dist/utils/exec.cjs.map +1 -1
  86. package/dist/utils/exec.mjs +5 -0
  87. package/dist/utils/exec.mjs.map +1 -1
  88. package/dist/utils/match.cjs +5 -0
  89. package/dist/utils/match.cjs.map +1 -1
  90. package/dist/utils/match.mjs +1 -0
  91. package/dist/utils/match.mjs.map +1 -1
  92. package/dist/utils/options.cjs.map +1 -1
  93. package/dist/utils/options.mjs.map +1 -1
  94. package/dist/utils/walk.cjs.map +1 -1
  95. package/dist/utils/walk.mjs.map +1 -1
  96. package/extra/plug.mts +373 -0
  97. package/extra/ts-loader.mts +545 -0
  98. package/extra/tsrun.mts +64 -0
  99. package/extra/utils.ts +168 -0
  100. package/package.json +9 -19
  101. package/src/build.ts +66 -33
  102. package/src/fork.ts +1 -0
  103. package/src/helpers.ts +1 -1
  104. package/src/index.ts +7 -0
  105. package/src/logging/spinner.ts +2 -0
  106. package/src/plugs/edit.ts +4 -3
  107. package/src/plugs/esbuild.ts +0 -1
  108. package/src/plugs/exports.ts +175 -0
  109. package/src/plugs.ts +1 -0
  110. package/src/types.ts +5 -1
  111. package/types/plugjs.d.ts +0 -2
  112. package/LICENSE.md +0 -211
  113. package/NOTICE.md +0 -13
  114. package/extra/cli.mjs +0 -1356
  115. package/extra/ts-loader.mjs +0 -223
@@ -0,0 +1,545 @@
1
+ /* ========================================================================== *
2
+ * HACK BEYOND REDEMPTION: TRANSPILE .ts FILES (the esm loader) *
3
+ * -------------------------------------------------------------------------- *
4
+ * This relies on the Node's `--experimental-loader` feature, and uses *
5
+ * ESBuild build magic to quickly transpile TypeScript files into JavaScript. *
6
+ * *
7
+ * The plan as it stands is as follows: *
8
+ * - `.mts` files always get transpiled to ESM modules *
9
+ * - `.cts` files always get transpiled to CJS modules *
10
+ * - `.ts` files are treanspiled according to what's in `package.json` *
11
+ * *
12
+ * Additionally, when transpiling to ESM modules, we can't rely on the magic *
13
+ * that Node's `require(...)` call uses to figure out which file to import. *
14
+ * We need to _actually verify_ on disk what's the correct file to import. *
15
+ * *
16
+ * This is a single module, only available as ESM, and it will _both_ behave *
17
+ * as a NodeJS' loader, _and_ inject the CJS extension handlers (hack) found *
18
+ * in the `_extensions` of `node:module` (same as `require.extensions`). *
19
+ * ========================================================================== */
20
+
21
+ // NodeJS dependencies
22
+ import _fs from 'node:fs'
23
+ import _module from 'node:module'
24
+ import _path from 'node:path'
25
+ import _url from 'node:url'
26
+ import _util from 'node:util'
27
+
28
+ // ESBuild is the only external dependency
29
+ import _esbuild from 'esbuild'
30
+
31
+ // Local imports
32
+ import { isDirectory, isFile } from './utils.js'
33
+
34
+ /* ========================================================================== *
35
+ * DEBUGGING AND ERRORS *
36
+ * ========================================================================== */
37
+
38
+ /** Supported types from `package.json` */
39
+ export type Type = 'commonjs' | 'module'
40
+ /** Constant identifying a `commonjs` module */
41
+ const CJS = 'commonjs'
42
+ /** Constant identifying an ESM `module` */
43
+ const ESM = 'module'
44
+
45
+ /** Setup debugging */
46
+ const _debugLog = _util.debuglog('plug:ts-loader')
47
+ const _debug = _debugLog.enabled
48
+
49
+ /** Emit some logs if `DEBUG_TS_LOADER` is set to `true` */
50
+ function _log(type: Type | null, arg: string, ...args: any []): void {
51
+ if (! _debug) return
52
+
53
+ const t = type === ESM ? 'esm' : type === CJS ? 'cjs' : '---'
54
+ _debugLog(`[${t}] ${arg}`, ...args)
55
+ }
56
+
57
+ /** Fail miserably */
58
+ function _throw(
59
+ type: Type | null,
60
+ message: string,
61
+ options: { start?: Function, code?: string, cause?: any } = {},
62
+ ): never {
63
+ const t = type === ESM ? 'esm' : type === CJS ? 'cjs' : '---'
64
+ const prefix = `[ts-loader|${t}|pid=${process.pid}]`
65
+
66
+ const { start = _throw, ...extra } = options
67
+ const error = new Error(`${prefix} ${message}`)
68
+ Error.captureStackTrace(error, start)
69
+ Object.assign(error, extra)
70
+
71
+ throw error
72
+ }
73
+
74
+
75
+ /* ========================================================================== *
76
+ * MODULE TYPES AND FORCING TYPE *
77
+ * ========================================================================== */
78
+
79
+ function _checkType(type: string): Type {
80
+ if (type === CJS) return CJS
81
+ if (type === ESM) return ESM
82
+ _throw(null, `Invalid type "${process.env.__TS_LOADER_FORCE_TYPE}"`)
83
+ }
84
+
85
+ let _type: Type = ((): Type => {
86
+ if (process.env.__TS_LOADER_FORCE_TYPE) {
87
+ const type = process.env.__TS_LOADER_FORCE_TYPE
88
+ _log(null, `Forcing type to "${type}" from environment`)
89
+ return _checkType(type)
90
+ }
91
+
92
+ const findType = (directory: string): Type => {
93
+ const packageFile = _path.join(directory, 'package.json')
94
+ try {
95
+ const packageData = _fs.readFileSync(packageFile, 'utf-8')
96
+ const packageJson = JSON.parse(packageData)
97
+ const packageType = packageJson.type
98
+ switch (packageType) {
99
+ case undefined:
100
+ _log(null, `File "${packageFile}" does not declare a default type`)
101
+ return CJS
102
+
103
+ case CJS:
104
+ case ESM:
105
+ _log(null, `File "${packageFile}" declares type as "${CJS}"`)
106
+ return packageType
107
+
108
+ default:
109
+ _log(null, `File "${packageFile}" specifies unknown type "${packageType}"`)
110
+ return CJS
111
+ }
112
+ } catch (cause: any) {
113
+ if ((cause.code !== 'ENOENT') && (cause.code !== 'EISDIR')) {
114
+ _throw(null, `Unable to read or parse "${packageFile}"`, { cause, start: findType })
115
+ }
116
+ }
117
+
118
+ const parent = _path.dirname(directory)
119
+ if (directory !== parent) return findType(directory)
120
+
121
+ _log(null, `Type defaulted to "${CJS}"`)
122
+ return CJS
123
+ }
124
+
125
+ return findType(process.cwd())
126
+ })()
127
+
128
+ /* The `tsLoaderMarker` (symbol in global) will be a setter/getter for type */
129
+ const tsLoaderMarker = Symbol.for('plugjs:tsLoader')
130
+
131
+ Object.defineProperty(globalThis, tsLoaderMarker, {
132
+ set(type: string): void {
133
+ _log(null, `Setting type to "${type}"`)
134
+ process.env.__TS_LOADER_FORCE_TYPE = _type = _checkType(type)
135
+ },
136
+ get(): Type {
137
+ return _type
138
+ },
139
+ })
140
+
141
+
142
+ /* ========================================================================== *
143
+ * ESBUILD HELPERS *
144
+ * ========================================================================== */
145
+
146
+ /**
147
+ * Take an ESBuild `BuildResult` or `BuildFailure` (they both have arrays
148
+ * of `Message` in both `warnings` and `errors`), format them and print them
149
+ * out nicely. Then fail if any error was detected.
150
+ */
151
+ function _esbReport(
152
+ kind: 'error' | 'warning',
153
+ messages: _esbuild.Message[] = [],
154
+ ): void {
155
+ const output = process.stderr
156
+ const options = { color: !!output.isTTY, terminalWidth: output.columns || 80 }
157
+
158
+ const array = _esbuild.formatMessagesSync(messages, { kind, ...options })
159
+ array.forEach((message) => output.write(`${message}\n`))
160
+ }
161
+
162
+ /**
163
+ * Transpile with ESBuild
164
+ */
165
+ function _esbTranpile(filename: string, type: Type): string {
166
+ _log(type, `Transpiling "${filename}`)
167
+
168
+ const [ format, __fileurl ] = type === ESM ?
169
+ [ 'esm', 'import.meta.url' ] as const :
170
+ [ 'cjs', '__filename' ] as const
171
+
172
+ /* ESbuild options */
173
+ const options: _esbuild.TransformOptions = {
174
+ sourcefile: filename, // the original filename we're parsing
175
+ format, // what are we actually transpiling to???
176
+ loader: 'ts', // the format is always "typescript"
177
+ sourcemap: 'inline', // always inline source maps
178
+ sourcesContent: false, // do not include sources content in sourcemap
179
+ platform: 'node', // d'oh! :-)
180
+ logLevel: 'silent', // catching those in our _esbReport below
181
+ target: `node${process.versions['node']}`, // target _this_ version
182
+ define: { __fileurl }, // from "globals.d.ts"
183
+ }
184
+
185
+ /* Emit a line on the console when loading in debug mode */
186
+ if (_debug) {
187
+ if (format === 'esm') {
188
+ options.banner = `;(await import('node:util')).debuglog('plug:ts-loader')('[esm] Loaded "%s"', ${__fileurl});`
189
+ } else if (format === 'cjs') {
190
+ options.banner = `;require('node:util').debuglog('plug:ts-loader')('[cjs] Loaded "%s"', ${__fileurl});`
191
+ }
192
+ }
193
+
194
+ /* Transpile our TypeScript file into some JavaScript stuff */
195
+ let result
196
+ try {
197
+ const source = _fs.readFileSync(filename, 'utf-8')
198
+ result = _esbuild.transformSync(source, options)
199
+ } catch (cause: any) {
200
+ _esbReport('error', (cause as _esbuild.TransformFailure).errors)
201
+ _esbReport('warning', (cause as _esbuild.TransformFailure).warnings)
202
+ _throw(type, `ESBuild error transpiling "${filename}"`, { cause, start: _esbTranpile })
203
+ }
204
+
205
+ /* Log transpile warnings if debugging */
206
+ if (_debug) _esbReport('warning', result.warnings)
207
+
208
+ /* Done! */
209
+ return result.code
210
+ }
211
+
212
+
213
+ /* ========================================================================== *
214
+ * ESM VERSION *
215
+ * ========================================================================== */
216
+
217
+ /** The formats that can be handled by NodeJS' loader */
218
+ type Format = 'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
219
+
220
+ /* ========================================================================== */
221
+
222
+ /** The type identifying a NodeJS' loader `resolve` hook. */
223
+ type ResolveHook = (
224
+ /** Whatever was requested to be imported (module, relative file, ...). */
225
+ specifier: string,
226
+ /** Context information around this `resolve` hook call. */
227
+ context: ResolveContext,
228
+ /** The subsequent resolve hook in the chain, or the Node.js default one. */
229
+ nextResolve: ResolveNext,
230
+ ) => ResolveResult | Promise<ResolveResult>
231
+
232
+ /** Context information around a `resolve` hook call. */
233
+ interface ResolveContext {
234
+ importAssertions: object
235
+ /** Export conditions of the relevant `package.json`. */
236
+ conditions: string[]
237
+ /** The module importing this one, or undefined if this is the entry point. */
238
+ parentURL?: string | undefined
239
+ }
240
+
241
+ /** The subsequent resolve hook in the chain, or the Node.js default one. */
242
+ type ResolveNext = (specifier: string, context: ResolveContext) => ResolveResult | Promise<ResolveResult>
243
+
244
+ /** A type describing the required results from a `resolve` hook */
245
+ interface ResolveResult {
246
+ /** The absolute URL to which this input resolves. */
247
+ url: string
248
+ /** A format hint to the `load` hook (it might be ignored). */
249
+ format?: Format | null | undefined
250
+ /** A signal that this hook intends to terminate the chain of resolve hooks. */
251
+ shortCircuit?: boolean | undefined
252
+ }
253
+
254
+ /* ========================================================================== */
255
+
256
+ /** The type identifying a NodeJS' loader `load` hook. */
257
+ type LoadHook = (
258
+ /** The URL returned by the resolve chain. */
259
+ url: string,
260
+ /** Context information around this `load` hook call. */
261
+ context: LoadContext,
262
+ /** The subsequent load hook in the chain, or the Node.js default one. */
263
+ nextLoad: LoadNext,
264
+ ) => LoadResult | Promise<LoadResult>
265
+
266
+ /** Context information around a `load` hook call. */
267
+ interface LoadContext {
268
+ importAssertions: object
269
+ /** Export conditions of the relevant `package.json` */
270
+ conditions: string[]
271
+ /** The format hint from the `resolve` hook. */
272
+ format?: ResolveResult['format']
273
+ }
274
+
275
+ /** The subsequent load hook in the chain, or the Node.js default one. */
276
+ type LoadNext = (url: string, context: LoadContext) => LoadResult | Promise<LoadResult>
277
+
278
+ /** A type describing the required results from a `resolve` hook */
279
+ type LoadResult = {
280
+ /** The format of the code being loaded. */
281
+ format: Format
282
+ /** A signal that this hook intends to terminate the chain of load hooks. */
283
+ shortCircuit?: boolean | undefined
284
+ } & ({
285
+ format: 'builtin' | 'commonjs'
286
+ /** When the source is `builtin` or `commonjs` no source must be returned */
287
+ source?: never | undefined
288
+ } | {
289
+ format: 'json' | 'module'
290
+ /** When the source is `json` or `module` the source can include strings */
291
+ source: string | ArrayBuffer | NodeJS.TypedArray
292
+ } | {
293
+ format: 'wasm'
294
+ /** When the source is `wasm` the source must not be a string */
295
+ source: ArrayBuffer | NodeJS.TypedArray
296
+ })
297
+
298
+ /* ========================================================================== */
299
+
300
+ /**
301
+ * Our main `resolve` hook: here we need to check for a couple of options
302
+ * when importing ""
303
+ */
304
+ export const resolve: ResolveHook = (specifier, context, nextResolve): ResolveResult | Promise<ResolveResult> => {
305
+ _log(ESM, `Resolving "${specifier}" from "${context.parentURL}"`)
306
+
307
+ /* We only resolve relative paths ("./xxx" or "../xxx") */
308
+ if (! specifier.match(/^\.\.?\//)) return nextResolve(specifier, context)
309
+
310
+ /* We only resolve if we _do_ have a parent URL and it's a file */
311
+ const parentURL = context.parentURL
312
+ if (! parentURL) return nextResolve(specifier, context)
313
+ if (! parentURL.startsWith('file:')) return nextResolve(specifier, context)
314
+
315
+ /* We only resolve here if the importer is a ".ts" or ".mts" file */
316
+ if (! parentURL.match(/\.m?ts$/)) return nextResolve(specifier, context)
317
+
318
+ /* The resolved URL is the specifier resolved against the parent */
319
+ const url = new URL(specifier, parentURL).href
320
+ const path = _url.fileURLToPath(url)
321
+
322
+ /*
323
+ * Here we are sure that:
324
+ *
325
+ * 1) we are resolving a local path (not a module)
326
+ * 2) the importer is a file, ending with ".ts" or ".mts"
327
+ *
328
+ * Now we can check if "import 'foo'" resolves to:
329
+ *
330
+ * 1) directly to a file, e.g. "import './foo.js'" or "import './foo.mts'"
331
+ * 2) import a "pseudo-JS file", e.g. "import './foo.js'" becomes "import './foo.ts'"
332
+ * 3) imports a file without extension as if it were "import './foo.ts'"
333
+ * 4) imports a directory as in "import './foo/index.ts'"
334
+ *
335
+ * We resolve the _final_ specifier that will be passed to the next resolver
336
+ * for further potential resolution accordingly.
337
+ *
338
+ * We start with the easiest case: is this a real file on the disk?
339
+ */
340
+ if (isFile(path)) {
341
+ _log(ESM, `Positive match for "${specifier}" as "${path}" (1)`)
342
+ return nextResolve(specifier, context) // straight on
343
+ }
344
+
345
+ /*
346
+ * TypeScript allows us to import "./foo.js", and internally resolves this to
347
+ * "./foo.ts" (yeah, nice, right?) and while we normally wouldn't want to deal
348
+ * with this kind of stuff, the "node16" module resolution mode _forces_ us to
349
+ * use this syntax.
350
+ */
351
+ const match = specifier.match(/(.*)(\.[mc]?js$)/)
352
+ if (match) {
353
+ const [ , base, ext ] = match
354
+ const tsspecifier = base + ext!.replace('js', 'ts')
355
+ const tsurl = new URL(tsspecifier, parentURL).href
356
+ const tspath = _url.fileURLToPath(tsurl)
357
+
358
+ if (isFile(tspath)) {
359
+ _log(ESM, `Positive match for "${specifier}" as "${tspath}" (2)`)
360
+ return nextResolve(tsspecifier, context) // straight on
361
+ }
362
+ }
363
+
364
+ /* Check if the import is actually a file with a ".ts" extension */
365
+ if (isFile(`${path}.ts`)) {
366
+ _log(ESM, `Positive match for "${specifier}.ts" as "${path}.ts" (3)`)
367
+ return nextResolve(`${specifier}.ts`, context)
368
+ }
369
+
370
+ /* If the file is a directory, then see if we have an "index.ts" in there */
371
+ if (isDirectory(path)) {
372
+ const file = _path.resolve(path, 'index.ts') // resolve, as path is absolute
373
+ if (isFile(file)) {
374
+ _log(ESM, `Positive match for "${specifier}" as "${file}" (4)`)
375
+ const spec = _url.pathToFileURL(file).pathname
376
+ return nextResolve(spec, context)
377
+ }
378
+ }
379
+
380
+ /* There's really nothing else we can do */
381
+ return nextResolve(specifier, context)
382
+ }
383
+
384
+ /** Our main `load` hook */
385
+ export const load: LoadHook = (url, context, nextLoad): LoadResult | Promise<LoadResult> => {
386
+ _log(ESM, `Attempting to load "${url}"`)
387
+
388
+ /* We only load from disk, so ignore everything else */
389
+ if (! url.startsWith('file:')) return nextLoad(url, context)
390
+
391
+ /* Figure our the extension (especially ".ts", ".mts" or ".cts")... */
392
+ const ext = url.match(/\.[cm]?ts$/)?.[0]
393
+
394
+ /* Quick and easy bail-outs for non-TS or ".cts" (always `commonjs`) */
395
+ if (! ext) return nextLoad(url, context)
396
+
397
+ if (ext === '.cts') {
398
+ if (_debug) {
399
+ _log(null, `Switching type from "module" to "commonjs" for "${url}"`)
400
+ _log(null, 'Please note that named import WILL NOT WORK in this case, as Node.js performs a')
401
+ _log(null, 'static analisys on the CommonJS source code, and this file is transpiled from.')
402
+ _log(null, 'TypeScript to CommonJS dynamically.')
403
+ }
404
+ return { format: CJS, shortCircuit: true }
405
+ }
406
+
407
+ /* Convert the url into a file name, any error gets ignored */
408
+ const filename = _url.fileURLToPath(url)
409
+
410
+ /* If the file is a ".ts", we need to figure out the default type */
411
+ if (ext === '.ts') {
412
+ if (_type === CJS) {
413
+ if (_debug) {
414
+ _log(null, `Switching type from "module" to "commonjs" for "${url}"`)
415
+ _log(null, 'Please note that named import WILL NOT WORK in this case, as Node.js performs a')
416
+ _log(null, 'static analisys on the CommonJS source code, and this file is transpiled from.')
417
+ _log(null, 'TypeScript to CommonJS dynamically.')
418
+ }
419
+ return { format: CJS, shortCircuit: true }
420
+ }
421
+ }
422
+
423
+ /* Transpile with ESBuild and return our source code */
424
+ const source = _esbTranpile(filename, ESM)
425
+ return { source, format: ESM, shortCircuit: true }
426
+ }
427
+
428
+
429
+ /* ========================================================================== *
430
+ * CJS VERSION *
431
+ * ========================================================================== */
432
+
433
+ /** The extension handler type, loading CJS modules */
434
+ type ExtensionHandler = (module: NodeJS.Module, filename: string) => void
435
+
436
+ /* Add the `_compile(...)` method to NodeJS' `Module` interface */
437
+ declare global {
438
+ namespace NodeJS {
439
+ interface Module {
440
+ _compile: (contents: string, filename: string) => void
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Add the `_extensions[...]` and `resolveFilename(...)` members to the
447
+ * definition of `node:module`.
448
+ */
449
+ declare module 'node:module' {
450
+ const _extensions: Record<`.${string}`, ExtensionHandler>
451
+ function _resolveFilename(
452
+ request: string,
453
+ parent: _module | undefined,
454
+ isMain: boolean,
455
+ options?: any,
456
+ ): string
457
+ }
458
+
459
+ /* ========================================================================== */
460
+
461
+ const loader: ExtensionHandler = (module, filename): void => {
462
+ _log(CJS, `Attempting to load "${filename}"`)
463
+
464
+ /* Figure our the extension (".ts" or ".cts")... */
465
+ const ext = _path.extname(filename)
466
+
467
+ /* If the file is a ".ts", we need to figure out the default type */
468
+ if (ext === '.ts') {
469
+ /* If the _default_ module type is CJS then load as such! */
470
+ if (_type === ESM) {
471
+ _throw(CJS, `Must use import to load ES Module: ${filename}`, { code: 'ERR_REQUIRE_ESM' })
472
+ }
473
+ } else if (ext !== '.cts') {
474
+ _throw(CJS, `Unsupported filename "${filename}"`)
475
+ }
476
+
477
+ const source = _esbTranpile(filename, CJS)
478
+
479
+ /* Let node do its thing, but wrap any error it throws */
480
+ try {
481
+ module._compile(source, filename)
482
+ } catch (cause) {
483
+ // eslint-disable-next-line no-console
484
+ console.error(`Error compiling module "${filename}"`, cause)
485
+ }
486
+ }
487
+
488
+ /* Remember to load our loader for .TS/.CTS as CommonJS modules */
489
+ _module._extensions['.ts'] = _module._extensions['.cts'] = loader
490
+
491
+ /**
492
+ * Replace _module._resolveFilename with our own.
493
+ *
494
+ * This is a _HACK BEYOND REDEMPTION_ and I'm ashamed of even _thinking_ about
495
+ * it, but, well, it makes things work.
496
+ *
497
+ * TypeScript allows us to import "./foo.js", and internally resolves this to
498
+ * "./foo.ts" (yeah, nice, right?) and while we normally wouldn't want to deal
499
+ * with this kind of stuff, the "node16" module resolution mode _forces_ us to
500
+ * use this syntax.
501
+ *
502
+ * And we _need_ the "node16" module resolution to properly consume "export
503
+ * conditions" from other packages. Since ESBuild's plugins only work in async
504
+ * mode, changing those import statements on the fly is out of the question, so
505
+ * we need to hack our way into Node's own resolver.
506
+ *
507
+ * See my post: https://twitter.com/ianosh/status/1559484168685379590
508
+ * ESBuild related fix: https://github.com/evanw/esbuild/commit/0cdc005e3d1c765a084f206741bc4bff78e30ec4
509
+ */
510
+ const _oldResolveFilename = _module._resolveFilename
511
+ _module._resolveFilename = function(
512
+ request: string,
513
+ parent: _module | undefined,
514
+ ...args: [ isMain: boolean, options: any ]
515
+ ): any {
516
+ try {
517
+ /* First call the old _resolveFilename to see what Node thinks */
518
+ return _oldResolveFilename.call(this, request, parent, ...args)
519
+ } catch (error: any) {
520
+ /* If the error was anything but "MODULE_NOT_FOUND" bail out */
521
+ if (error.code !== 'MODULE_NOT_FOUND') throw error
522
+
523
+ /* Check if the "request" ends with ".js", ".mjs" or ".cjs" */
524
+ const match = request.match(/(.*)(\.[mc]?js$)/)
525
+
526
+ /*
527
+ * If the file matches our extension, _and_ we have a parent, we simply
528
+ * try with a new extension (e.g. ".js" becomes ".ts")...
529
+ */
530
+ if (parent && match) {
531
+ const [ , name, ext ] = match
532
+ const tsrequest = name + ext!.replace('js', 'ts')
533
+ try {
534
+ const result = _oldResolveFilename.call(this, tsrequest, parent, ...args)
535
+ _log(CJS, `Resolution for "${request}" intercepted as "${tsrequest}`)
536
+ return result
537
+ } catch (discard) {
538
+ throw error // throw the _original_ error in this case
539
+ }
540
+ }
541
+
542
+ /* We have no parent, or we don't match our extension, throw! */
543
+ throw error
544
+ }
545
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ import _path from 'node:path'
5
+
6
+ import { $blu, $gry, $rst, $und, $wht, main, version } from './utils.js'
7
+
8
+ /** Our minimalistic help */
9
+ function help(): never {
10
+ console.log(`${$blu}${$und}Usage:${$rst}
11
+
12
+ ${$wht}tsrun${$rst} ${$gry}[${$rst}--options${$gry}] script.ts [...${$rst}script args${$gry}]${$rst}
13
+
14
+ ${$blu}${$und}Options:${$rst}
15
+
16
+ ${$wht}-h --help${$rst} Help! You're reading it now!
17
+ ${$wht}-v --version${$rst} Version! This one: ${version()}!
18
+ ${$wht} --force-esm${$rst} Force transpilation of ".ts" files to EcmaScript modules
19
+ ${$wht} --force-cjs${$rst} Force transpilation of ".ts" files to CommonJS modules
20
+
21
+ ${$blu}${$und}Description:${$rst}
22
+
23
+ ${$wht}tsrun${$rst} is a minimalistic TypeScript loader, using "esbuild" to transpile TS
24
+ code to JavaScript, and running it. Being extremely un-sofisticated, it's
25
+ not meant to to be in any way a replacement for more complete alternatives
26
+ like "ts-node".
27
+ `)
28
+
29
+ process.exit(1)
30
+ }
31
+
32
+ /** Process the command line */
33
+ main((args: string[]): void => {
34
+ let script: string | undefined
35
+ let scriptArgs: string[] = []
36
+
37
+ // Parse options, leaving script and scriptArgs with our code to run
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i]
40
+
41
+ if ((arg === '-h') || (arg === '--help')) help()
42
+ if ((arg === '-v') || (arg === '--version')) {
43
+ console.log(`v${version()}`)
44
+ process.exit(1)
45
+ }
46
+
47
+ if (arg!.startsWith('-')) {
48
+ console.log(`${$wht}tsrun${$rst}: Uknown option "${$wht}${arg}${$rst}"`)
49
+ process.exit(1)
50
+ }
51
+
52
+ ([ script, ...scriptArgs ] = args.slice(i))
53
+ break
54
+ }
55
+
56
+ // No script? Then help
57
+ if (! script) help()
58
+
59
+ // Resolve the _full_ path of the script, and tweak our process.argv
60
+ // arguments, them simply import the script and let Node do its thing...
61
+ script = _path.resolve(process.cwd(), script)
62
+ process.argv = [ process.argv0, script, ...scriptArgs ]
63
+ import(script)
64
+ })