@plugjs/typescript 0.1.0
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/LICENSE.md +211 -0
- package/NOTICE.md +13 -0
- package/README.md +7 -0
- package/dist/compiler.cjs +76 -0
- package/dist/compiler.cjs.map +6 -0
- package/dist/compiler.d.ts +24 -0
- package/dist/compiler.mjs +45 -0
- package/dist/compiler.mjs.map +6 -0
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.mjs +5 -0
- package/dist/index.mjs.map +6 -0
- package/dist/options.cjs +78 -0
- package/dist/options.cjs.map +6 -0
- package/dist/options.d.ts +8 -0
- package/dist/options.mjs +47 -0
- package/dist/options.mjs.map +6 -0
- package/dist/report.cjs +90 -0
- package/dist/report.cjs.map +6 -0
- package/dist/report.d.ts +5 -0
- package/dist/report.mjs +59 -0
- package/dist/report.mjs.map +6 -0
- package/dist/typescript.cjs +124 -0
- package/dist/typescript.cjs.map +6 -0
- package/dist/typescript.d.ts +9 -0
- package/dist/typescript.mjs +93 -0
- package/dist/typescript.mjs.map +6 -0
- package/package.json +56 -0
- package/src/compiler.ts +70 -0
- package/src/index.ts +58 -0
- package/src/options.ts +93 -0
- package/src/report.ts +80 -0
- package/src/typescript.ts +137 -0
package/src/options.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import ts from 'typescript' // TypeScript does NOT support ESM modules
|
|
2
|
+
import { getAbsoluteParent, resolveAbsolutePath } from '@plugjs/plug/paths'
|
|
3
|
+
|
|
4
|
+
import type { AbsolutePath } from '@plugjs/plug/paths'
|
|
5
|
+
|
|
6
|
+
/* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
export type CompilerOptionsAndDiagnostics = {
|
|
9
|
+
options: ts.CompilerOptions,
|
|
10
|
+
errors: readonly ts.Diagnostic[],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ========================================================================== */
|
|
14
|
+
|
|
15
|
+
function mergeResults(
|
|
16
|
+
base: CompilerOptionsAndDiagnostics,
|
|
17
|
+
override: CompilerOptionsAndDiagnostics,
|
|
18
|
+
): CompilerOptionsAndDiagnostics {
|
|
19
|
+
const options = { ...base.options, ...override.options }
|
|
20
|
+
const errors = [ ...base.errors, ...override.errors ]
|
|
21
|
+
return errors.length ? { options: {}, errors } : { options, errors: [] }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ========================================================================== */
|
|
25
|
+
|
|
26
|
+
async function loadOptions(
|
|
27
|
+
file: AbsolutePath,
|
|
28
|
+
stack: AbsolutePath[] = [ file ],
|
|
29
|
+
): Promise<CompilerOptionsAndDiagnostics> {
|
|
30
|
+
const dir = getAbsoluteParent(file)
|
|
31
|
+
|
|
32
|
+
// Load up our config file and convert is wicked JSON
|
|
33
|
+
const { config, error } = ts.readConfigFile(file, ts.sys.readFile)
|
|
34
|
+
if (error) return { options: {}, errors: [ error ] }
|
|
35
|
+
|
|
36
|
+
// Parse up the configuration file as options
|
|
37
|
+
const { compilerOptions = {}, extends: extendsPath } = config
|
|
38
|
+
const result = ts.convertCompilerOptionsFromJson(compilerOptions, dir, file)
|
|
39
|
+
if (result.errors.length) return result
|
|
40
|
+
|
|
41
|
+
// If we don't extend, we can return our result
|
|
42
|
+
if (!extendsPath) return result
|
|
43
|
+
|
|
44
|
+
// Resolve the name of the file this config extends
|
|
45
|
+
const ext = resolveAbsolutePath(dir, extendsPath)
|
|
46
|
+
|
|
47
|
+
// Triple check that we are not recursively importing this file
|
|
48
|
+
if (stack.includes(ext)) {
|
|
49
|
+
const data = ts.sys.readFile(file)
|
|
50
|
+
return { options: {}, errors: [ {
|
|
51
|
+
messageText: `Circularity detected extending from "${ext}"`,
|
|
52
|
+
category: ts.DiagnosticCategory.Error,
|
|
53
|
+
code: 18000, // copied from typescript internals...
|
|
54
|
+
file: ts.createSourceFile(file, data!, ts.ScriptTarget.JSON, false, ts.ScriptKind.JSON),
|
|
55
|
+
start: undefined,
|
|
56
|
+
length: undefined,
|
|
57
|
+
} ] }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Push our file in the stack and load recursively
|
|
61
|
+
return mergeResults(await loadOptions(ext, [ ...stack, ext ]), result)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ========================================================================== */
|
|
65
|
+
|
|
66
|
+
export async function getCompilerOptions(
|
|
67
|
+
file?: AbsolutePath,
|
|
68
|
+
): Promise<CompilerOptionsAndDiagnostics>
|
|
69
|
+
|
|
70
|
+
export async function getCompilerOptions(
|
|
71
|
+
file: AbsolutePath | undefined,
|
|
72
|
+
overrides: ts.CompilerOptions,
|
|
73
|
+
): Promise<CompilerOptionsAndDiagnostics>
|
|
74
|
+
|
|
75
|
+
/** Load compiler options from a JSON file, and merge in the overrides */
|
|
76
|
+
export async function getCompilerOptions(
|
|
77
|
+
file?: AbsolutePath,
|
|
78
|
+
overrides?: ts.CompilerOptions,
|
|
79
|
+
): Promise<CompilerOptionsAndDiagnostics> {
|
|
80
|
+
const options = ts.getDefaultCompilerOptions()
|
|
81
|
+
let result: CompilerOptionsAndDiagnostics = { options, errors: [] }
|
|
82
|
+
|
|
83
|
+
// If we have a file to parse, load it, otherwise try "tsconfig.json"
|
|
84
|
+
if (file) result = mergeResults(result, await loadOptions(file))
|
|
85
|
+
|
|
86
|
+
// If we have overrides, merge them
|
|
87
|
+
if (overrides) {
|
|
88
|
+
result = mergeResults(result, { options: overrides, errors: [] })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Return all we have
|
|
92
|
+
return result
|
|
93
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import ts from 'typescript' // TypeScript does NOT support ESM modules
|
|
2
|
+
import { ERROR, NOTICE, WARN } from '@plugjs/plug/logging'
|
|
3
|
+
import { resolveAbsolutePath } from '@plugjs/plug/paths'
|
|
4
|
+
|
|
5
|
+
import type { Report, ReportLevel, ReportRecord } from '@plugjs/plug/logging'
|
|
6
|
+
import type { AbsolutePath } from '@plugjs/plug/paths'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function convertMessageChain(chain: ts.DiagnosticMessageChain, indent = 0): string[] {
|
|
10
|
+
const message = `${''.padStart(indent * 2)}${chain.messageText}`
|
|
11
|
+
|
|
12
|
+
if (chain.next) {
|
|
13
|
+
const next = chain.next.map((c) => convertMessageChain(c, indent + 1))
|
|
14
|
+
return [ message, ...next.flat(1) ]
|
|
15
|
+
} else {
|
|
16
|
+
return [ message ]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function convertDiagnostics(
|
|
21
|
+
diagnostics: readonly ts.Diagnostic[],
|
|
22
|
+
directory: AbsolutePath,
|
|
23
|
+
): ReportRecord[] {
|
|
24
|
+
return diagnostics.map((diagnostic): ReportRecord => {
|
|
25
|
+
// console.log(diagnostic)
|
|
26
|
+
void directory
|
|
27
|
+
|
|
28
|
+
// Convert the `DiagnosticCategory` to our level
|
|
29
|
+
let level: ReportLevel
|
|
30
|
+
switch (diagnostic.category) {
|
|
31
|
+
case ts.DiagnosticCategory.Error: level = ERROR; break
|
|
32
|
+
// coverage ignore next / generally not emitted
|
|
33
|
+
case ts.DiagnosticCategory.Warning: level = WARN; break
|
|
34
|
+
// coverage ignore next / message and suggestion
|
|
35
|
+
default: level = NOTICE
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Convert the `messageText` to a string
|
|
39
|
+
let message: string | string[]
|
|
40
|
+
if (typeof diagnostic.messageText === 'string') {
|
|
41
|
+
message = diagnostic.messageText
|
|
42
|
+
} else {
|
|
43
|
+
message = convertMessageChain(diagnostic.messageText)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Simple variables
|
|
47
|
+
const tags = `TS${diagnostic.code}`
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if (diagnostic.file) {
|
|
51
|
+
const { file: sourceFile, start, length } = diagnostic
|
|
52
|
+
const file = resolveAbsolutePath(directory, sourceFile.fileName)
|
|
53
|
+
const source = sourceFile.getFullText()
|
|
54
|
+
|
|
55
|
+
// coverage ignore else
|
|
56
|
+
if (start !== undefined) {
|
|
57
|
+
const position = sourceFile.getLineAndCharacterOfPosition(start)
|
|
58
|
+
let { line, character: column } = position
|
|
59
|
+
column += 1
|
|
60
|
+
line += 1
|
|
61
|
+
|
|
62
|
+
return { level, message, tags, file, source, line, column, length }
|
|
63
|
+
} else {
|
|
64
|
+
return { level, message, tags, file, source }
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
return { level, message, tags }
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Update a report, adding records from an array of {@link ts.Diagnostic} */
|
|
73
|
+
export function updateReport(
|
|
74
|
+
report: Report,
|
|
75
|
+
diagnostics: readonly ts.Diagnostic[],
|
|
76
|
+
directory: AbsolutePath,
|
|
77
|
+
): void {
|
|
78
|
+
const records = convertDiagnostics(diagnostics, directory)
|
|
79
|
+
report.add(...records)
|
|
80
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Reference ourselves, so that the constructor's parameters are correct
|
|
2
|
+
/// <reference path="./index.ts"/>
|
|
3
|
+
|
|
4
|
+
import ts from 'typescript' // TypeScript does NOT support ESM modules
|
|
5
|
+
import { assertPromises, BuildFailure } from '@plugjs/plug/asserts'
|
|
6
|
+
import { Files } from '@plugjs/plug/files'
|
|
7
|
+
import { $p } from '@plugjs/plug/logging'
|
|
8
|
+
import { resolveAbsolutePath, resolveFile } from '@plugjs/plug/paths'
|
|
9
|
+
import { parseOptions, walk } from '@plugjs/plug/utils'
|
|
10
|
+
|
|
11
|
+
import { TypeScriptHost } from './compiler'
|
|
12
|
+
import { getCompilerOptions } from './options'
|
|
13
|
+
import { updateReport } from './report'
|
|
14
|
+
|
|
15
|
+
import type { AbsolutePath } from '@plugjs/plug/paths'
|
|
16
|
+
import type { Context, PipeParameters, Plug } from '@plugjs/plug/pipe'
|
|
17
|
+
import type { ExtendedCompilerOptions } from './index'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/* ========================================================================== *
|
|
21
|
+
* WORKER PLUG *
|
|
22
|
+
* ========================================================================== */
|
|
23
|
+
|
|
24
|
+
export class Tsc implements Plug<Files> {
|
|
25
|
+
private readonly _tsconfig?: string
|
|
26
|
+
private readonly _options: ExtendedCompilerOptions
|
|
27
|
+
|
|
28
|
+
constructor(...args: PipeParameters<'tsc'>) {
|
|
29
|
+
const { params: [ tsconfig ], options } = parseOptions(args, {})
|
|
30
|
+
this._tsconfig = tsconfig
|
|
31
|
+
this._options = options
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async pipe(files: Files, context: Context): Promise<Files> {
|
|
35
|
+
const baseDir = context.resolve('.') // "this" directory, base of all relative paths
|
|
36
|
+
const report = context.log.report('TypeScript Report') // report used throughout
|
|
37
|
+
const { extraTypesDir, ...overrides } = { ...this._options } // clone our options
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* The "tsconfig" file is either specified, or (if existing) first checked
|
|
41
|
+
* alongside the sources, otherwise checked in the current directory.
|
|
42
|
+
*/
|
|
43
|
+
const sourcesConfig = resolveFile(files.directory, 'tsconfig.json')
|
|
44
|
+
const tsconfig = this._tsconfig ? context.resolve(this._tsconfig) :
|
|
45
|
+
sourcesConfig || resolveFile(context.resolve('tsconfig.json'))
|
|
46
|
+
|
|
47
|
+
/* Root directory must always exist */
|
|
48
|
+
let rootDir: AbsolutePath
|
|
49
|
+
if (overrides.rootDir) {
|
|
50
|
+
rootDir = overrides.rootDir = context.resolve(overrides.rootDir)
|
|
51
|
+
} else {
|
|
52
|
+
rootDir = overrides.rootDir = files.directory
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Output directory _also_ must always exist */
|
|
56
|
+
let outDir: AbsolutePath
|
|
57
|
+
if (overrides.outDir) {
|
|
58
|
+
outDir = overrides.outDir = context.resolve(overrides.outDir)
|
|
59
|
+
} else {
|
|
60
|
+
outDir = overrides.outDir = rootDir // default to the root directory
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* All other root paths */
|
|
64
|
+
if (overrides.rootDirs) {
|
|
65
|
+
overrides.rootDirs = overrides.rootDirs.map((dir) => context.resolve(dir))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* The baseURL is resolved, as well */
|
|
69
|
+
if (overrides.baseUrl) overrides.baseUrl = context.resolve(overrides.baseUrl)
|
|
70
|
+
|
|
71
|
+
/* The baseURL is resolved, as well */
|
|
72
|
+
if (overrides.outFile) overrides.outFile = context.resolve(overrides.outFile)
|
|
73
|
+
|
|
74
|
+
/* We can now get our compiler options, and check any and all overrides */
|
|
75
|
+
const { errors, options } = await getCompilerOptions(
|
|
76
|
+
tsconfig, // resolved tsconfig.json from constructor, might be undefined
|
|
77
|
+
overrides) // overrides from constructor, might be an empty object
|
|
78
|
+
|
|
79
|
+
/* Update report and fail on errors */
|
|
80
|
+
updateReport(report, errors, baseDir)
|
|
81
|
+
if (report.errors) report.done(true)
|
|
82
|
+
|
|
83
|
+
/* Prep for compilation */
|
|
84
|
+
const paths = [ ...files.absolutePaths() ]
|
|
85
|
+
for (const path of paths) context.log.trace(`Compiling "${$p(path)}"`)
|
|
86
|
+
context.log.info('Compiling', paths.length, 'files')
|
|
87
|
+
|
|
88
|
+
/* If we have an extra types directory, add all the .d.ts files in there */
|
|
89
|
+
if (extraTypesDir) {
|
|
90
|
+
const directory = context.resolve(extraTypesDir)
|
|
91
|
+
|
|
92
|
+
for await (const file of walk(directory, [ '**/*.d.ts' ])) {
|
|
93
|
+
const path = resolveAbsolutePath(directory, file)
|
|
94
|
+
context.log.debug(`Including extra type file "${$p(path)}"`)
|
|
95
|
+
paths.push(path)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Log out what we'll be our final compilation options */
|
|
100
|
+
context.log.debug('Compliation options', options)
|
|
101
|
+
|
|
102
|
+
/* Typescript host, create program and compile */
|
|
103
|
+
const host = new TypeScriptHost(rootDir)
|
|
104
|
+
const program = ts.createProgram(paths, options, host, undefined, errors)
|
|
105
|
+
const diagnostics = ts.getPreEmitDiagnostics(program)
|
|
106
|
+
|
|
107
|
+
/* Update report and fail on errors */
|
|
108
|
+
updateReport(report, diagnostics, rootDir)
|
|
109
|
+
if (report.errors) report.done(true)
|
|
110
|
+
|
|
111
|
+
/* Write out all files asynchronously */
|
|
112
|
+
const builder = Files.builder(outDir)
|
|
113
|
+
const promises: Promise<void>[] = []
|
|
114
|
+
const result = program.emit(undefined, (fileName, code) => {
|
|
115
|
+
promises.push(builder.write(fileName, code).then((file) => {
|
|
116
|
+
context.log.trace('Written', $p(file))
|
|
117
|
+
}).catch(/* coverage ignore next */ (error) => {
|
|
118
|
+
const outFile = resolveAbsolutePath(outDir, fileName)
|
|
119
|
+
context.log.error('Error writing to', $p(outFile), error)
|
|
120
|
+
throw BuildFailure.fail()
|
|
121
|
+
}))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/* Await for all files to be written and check */
|
|
125
|
+
await assertPromises(promises)
|
|
126
|
+
|
|
127
|
+
/* Update report and fail on errors */
|
|
128
|
+
updateReport(report, result.diagnostics, rootDir)
|
|
129
|
+
/* coverage ignore if / only on write errors */
|
|
130
|
+
if (report.errors) report.done(true)
|
|
131
|
+
|
|
132
|
+
/* All done, build our files and return it */
|
|
133
|
+
const outputs = builder.build()
|
|
134
|
+
context.log.info('TSC produced', outputs.length, 'files into', $p(outputs.directory))
|
|
135
|
+
return outputs
|
|
136
|
+
}
|
|
137
|
+
}
|