Skip to content

Commit

Permalink
WIP(compiler): raw TS API
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed Jun 1, 2024
1 parent 728836d commit c0307b6
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 111 deletions.
5 changes: 1 addition & 4 deletions packages/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@
"license": "MIT",
"sideEffects": [],
"dependencies": {
"@rnx-kit/typescript-service": "^1.5.8",
"@ts-morph/common": "^0.23.0",
"@typed/core": "workspace:*",
"ts-morph": "^22.0.0"
"@typed/core": "workspace:*"
}
}
78 changes: 33 additions & 45 deletions packages/compiler/src/Compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

import { parse } from "@typed/template/Parser"
import type { Template } from "@typed/template/Template"
import type { SourceFile, TemplateLiteral } from "ts-morph"
import { Project } from "ts-morph"
import ts from "typescript"
import { findTsConfig } from "./typescript/findConfigFile.js"
import type { Project } from "./typescript/Project.js"
import { Service } from "./typescript/Service.js"

/**
* Compiler is an all-in-one cass for compile-time optimization and derivations
Expand All @@ -20,60 +20,31 @@ import { findTsConfig } from "./typescript/findConfigFile.js"
*/
export class Compiler {
private _cmdLine: ts.ParsedCommandLine
private _cache: ts.ModuleResolutionCache

private _service: Service = new Service()
readonly project: Project

constructor(readonly directory: string, readonly tsConfig?: string) {
this._cmdLine = findTsConfig(directory, tsConfig)
this._cache = ts.createModuleResolutionCache(directory, (s) => s, this._cmdLine.options)
this.project = new Project({
compilerOptions: this._cmdLine.options,
resolutionHost: (host, getOptions) => {
return {
...host,
resolveModuleNames: (moduleNames, containingFile, _reusedNames, redirectedReference, options) => {
return moduleNames.map((moduleName) =>
ts.resolveModuleName(
moduleName,
containingFile,
options,
host,
this._cache,
redirectedReference,
ts.ModuleKind.ESNext
).resolvedModule
)
},
getResolvedModuleWithFailedLookupLocationsFromCache: (moduleName, containingFile, resolutionMode) =>
ts.resolveModuleName(
moduleName,
containingFile,
getOptions(),
host,
this._cache,
undefined,
resolutionMode
)
}
}
})
this.project = this._service.openProject(this._cmdLine, this.enhanceLanguageServiceHost)
}

parseTemplates(sourceFile: SourceFile): ReadonlyArray<ParsedTemplate> {
parseTemplates(sourceFile: ts.SourceFile): ReadonlyArray<ParsedTemplate> {
const templates: Array<ParsedTemplate> = []

sourceFile.getDescendantsOfKind(ts.SyntaxKind.TaggedTemplateExpression).forEach((expression) => {
const tag = expression.getTag().getText()
getTaggedTemplateLiteralExpressions(sourceFile).forEach((expression) => {
const tag = expression.tag.getText()
if (tag === "html") {
const literal = expression.getTemplate()
const literal = expression.template
const template = parseTemplateFromNode(literal)
templates.push({ literal, template })
}
})

return templates.sort(sortParsedTemplates)
}

private enhanceLanguageServiceHost = (_host: ts.LanguageServiceHost): void => {
}
}

// Ensure that nested templates are handled first
Expand All @@ -91,28 +62,45 @@ function getSpan(template: ParsedTemplate) {
}

export interface ParsedTemplate {
readonly literal: TemplateLiteral
readonly literal: ts.TemplateLiteral
readonly template: Template
}

function parseTemplateFromNode(node: TemplateLiteral): Template {
if (node.isKind(ts.SyntaxKind.NoSubstitutionTemplateLiteral)) {
function parseTemplateFromNode(node: ts.TemplateLiteral): Template {
if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
return parse([node.getText().slice(1, -1)])
} else {
const [head, syntaxList] = node.getChildren()
const children = syntaxList.getChildren()
const lastChild = children[children.length - 1]
const parts = children.map((child) => {
if (child.isKind(ts.SyntaxKind.TemplateSpan)) {
if (child.kind === ts.SyntaxKind.TemplateSpan) {
const [, literal] = child.getChildren()
const text = literal.getText()
if (child === lastChild) return text.slice(1, -1)
return text.slice(1)
} else {
throw new Error(`Unexpected syntax kind: ${child.getKindName()}`)
throw new Error(`Unexpected syntax kind: ${ts.SyntaxKind[child.kind]}`)
}
})

return parse([head.getText().slice(1, -2), ...parts])
}
}

function getTaggedTemplateLiteralExpressions(node: ts.SourceFile) {
const toProcess: Array<ts.Node> = node.getChildren()
const matches: Array<ts.TaggedTemplateExpression> = []

while (toProcess.length) {
const node = toProcess.shift()!

if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) {
matches.push(node as ts.TaggedTemplateExpression)
}

toProcess.push(...node.getChildren())
}

return matches
}
196 changes: 196 additions & 0 deletions packages/compiler/src/typescript/Project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import ts from "typescript"
import { ExternalFileCache, ProjectFileCache } from "./cache.js"
import type { DiagnosticWriter } from "./diagnostics"

export class Project {
private diagnosticWriter: DiagnosticWriter
private cmdLine: ts.ParsedCommandLine

private projectFiles: ProjectFileCache
private externalFiles: ExternalFileCache

private languageService: ts.LanguageService
private program: ts.Program

constructor(
documentRegistry: ts.DocumentRegistry,
diagnosticWriter: DiagnosticWriter,
cmdLine: ts.ParsedCommandLine,
enhanceLanguageServiceHost?: (host: ts.LanguageServiceHost) => void
) {
this.diagnosticWriter = diagnosticWriter
this.cmdLine = cmdLine

this.projectFiles = new ProjectFileCache(cmdLine.fileNames)
this.externalFiles = new ExternalFileCache()

const languageServiceHost: ts.LanguageServiceHost = {
getCompilationSettings: () => this.cmdLine.options,
// getNewLine?(): string;
// getProjectVersion?(): string;
getScriptFileNames: () => this.projectFiles.getFileNames(),
// getScriptKind?(fileName: string): ts.ScriptKind;
getScriptVersion: (fileName) => this.projectFiles.getVersion(fileName) ?? "0",
getScriptSnapshot: (fileName) =>
this.projectFiles.getSnapshot(fileName) ??
this.externalFiles.getSnapshot(fileName),
getProjectReferences: (): ReadonlyArray<ts.ProjectReference> | undefined => cmdLine.projectReferences,
// getLocalizedDiagnosticMessages?(): any;
// getCancellationToken?(): HostCancellationToken;
getCurrentDirectory: () => process.cwd(),
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
// log: (s: string): void;
// trace: (s: string): void;
// error: (s: string): void;
// useCaseSensitiveFileNames?(): boolean;

/*
* LS host can optionally implement these methods to support completions for module specifiers.
* Without these methods, only completions for ambient modules will be provided.
*/
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile,
realpath: ts.sys.realpath || ((x) => x),
fileExists: ts.sys.fileExists,

/*
* LS host can optionally implement these methods to support automatic updating when new type libraries are installed
*/
// getTypeRootsVersion?(): number;

/*
* LS host can optionally implement this method if it wants to be completely in charge of module name resolution.
* if implementation is omitted then language service will use built-in module resolution logic and get answers to
* host specific questions using 'getScriptSnapshot'.
*
* If this is implemented, `getResolvedModuleWithFailedLookupLocationsFromCache` should be too.
*/
// resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedModule | undefined)[];
// getResolvedModuleWithFailedLookupLocationsFromCache?(modulename: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined;
// resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[];

/*
* Required for full import and type reference completions.
* These should be unprefixed names. E.g. `getDirectories("/foo/bar")` should return `["a", "b"]`, not `["/foo/bar/a", "/foo/bar/b"]`.
*/
getDirectories: ts.sys.getDirectories,

/**
* Gets a set of custom transformers to use during emit.
*/
// getCustomTransformers?(): CustomTransformers | undefined;

// isKnownTypesPackageName?(name: string): boolean;
// installPackage?(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult>;
// writeFile?(fileName: string, content: string): void;

// getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined;

directoryExists: ts.sys.directoryExists
}

if (enhanceLanguageServiceHost) {
enhanceLanguageServiceHost(languageServiceHost)
}

this.languageService = ts.createLanguageService(
languageServiceHost,
documentRegistry
)
this.program = this.languageService.getProgram()!
}

addFile(filePath: string) {
// Add snapshot
this.externalFiles.getSnapshot(filePath)
return this.program.getSourceFile(filePath)!
}

getCommandLine(): ts.ParsedCommandLine {
return this.cmdLine
}

private getFileDiagnostics(fileName: string): Array<ts.Diagnostic> {
return [
...this.languageService.getSyntacticDiagnostics(fileName),
...this.languageService.getSemanticDiagnostics(fileName),
...this.languageService.getSuggestionDiagnostics(fileName)
]
}

validateFile(fileName: string): boolean {
const diagnostics = this.getFileDiagnostics(fileName).filter(
(d) => d.category !== ts.DiagnosticCategory.Suggestion
)
if (Array.isArray(diagnostics) && diagnostics.length > 0) {
diagnostics.forEach((d) => this.diagnosticWriter.print(d))
return false
}
return true
}

validate(): boolean {
// filter down the list of files to be checked
const matcher = this.cmdLine.options.checkJs ? /[.][jt]sx?$/ : /[.]tsx?$/
const files = this.projectFiles
.getFileNames()
.filter((f) => f.match(matcher))

// check each file
let result = true
for (const file of files) {
// always validate the file, even if others have failed
const fileResult = this.validateFile(file)
// combine this file's result with the aggregate result
result = result && fileResult
}
return result
}

emitFile(fileName: string): boolean {
const output = this.languageService.getEmitOutput(fileName)
if (!output || output.emitSkipped) {
this.validateFile(fileName)
return false
}
output.outputFiles.forEach((o) => {
ts.sys.writeFile(o.name, o.text)
})
return true
}

emit(): boolean {
// emit each file
let result = true
for (const file of this.projectFiles.getFileNames()) {
// always emit the file, even if others have failed
const fileResult = this.emitFile(file)
// combine this file's result with the aggregate result
result = result && fileResult
}
return result
}

hasFile(fileName: string): boolean {
return this.projectFiles.has(fileName)
}

setFile(fileName: string, snapshot?: ts.IScriptSnapshot): void {
this.projectFiles.set(fileName, snapshot)
}

removeFile(fileName: string): void {
this.projectFiles.remove(fileName)
}

removeAllFiles(): void {
this.projectFiles.removeAll()
}

dispose(): void {
this.languageService.dispose()

// @ts-expect-error `languageService` cannot be used after calling dispose
this.languageService = null
}
}
25 changes: 25 additions & 0 deletions packages/compiler/src/typescript/Service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ts from "typescript"
import { createDiagnosticWriter } from "./diagnostics.js"
import { Project } from "./Project.js"

export class Service {
private documentRegistry
private diagnosticWriter

constructor(write?: (message: string) => void) {
this.documentRegistry = ts.createDocumentRegistry()
this.diagnosticWriter = createDiagnosticWriter(write)
}

openProject(
cmdLine: ts.ParsedCommandLine,
enhanceLanguageServiceHost?: (host: ts.LanguageServiceHost) => void
): Project {
return new Project(
this.documentRegistry,
this.diagnosticWriter,
cmdLine,
enhanceLanguageServiceHost
)
}
}
Loading

0 comments on commit c0307b6

Please sign in to comment.