@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/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
+ }