-
Notifications
You must be signed in to change notification settings - Fork 582
Add emitter scaffolding #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
61a3e78
Add emitter scaffolding
rbuckton 7de9901
Use LF for printer tests
rbuckton aee068c
Update WriteFile support for VFS
rbuckton 9ee248b
Move Program.emit to program.go
rbuckton 5457882
Merge branch 'main' into emitter
rbuckton 29f1c1d
PR feedback
rbuckton a7ed9c2
Lowercase 'outdir'
rbuckton 921ffc2
Remove unused fields in emitHost
rbuckton 2e4c08e
Fix outdir
rbuckton 48efd7a
Merge remote-tracking branch 'upstream/main' into emitter
rbuckton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package compiler | ||
|
||
import ( | ||
"github.com/microsoft/typescript-go/internal/ast" | ||
"github.com/microsoft/typescript-go/internal/core" | ||
) | ||
|
||
type WriteFileData struct { | ||
SourceMapUrlPos int | ||
// BuildInfo BuildInfo | ||
Diagnostics []*ast.Diagnostic | ||
DiffersOnlyInMap bool | ||
SkippedDtsWrite bool | ||
} | ||
|
||
// NOTE: EmitHost operations must be thread-safe | ||
type EmitHost interface { | ||
Options() *core.CompilerOptions | ||
SourceFiles() []*ast.SourceFile | ||
UseCaseSensitiveFileNames() bool | ||
GetCurrentDirectory() string | ||
CommonSourceDirectory() string | ||
IsEmitBlocked(file string) bool | ||
WriteFile(fileName string, text string, writeByteOrderMark bool, relatedSourceFiles []*ast.SourceFile, data *WriteFileData) error | ||
} | ||
|
||
var _ EmitHost = (*emitHost)(nil) | ||
|
||
// NOTE: emitHost operations must be thread-safe | ||
type emitHost struct { | ||
program *Program | ||
} | ||
|
||
func (host *emitHost) Options() *core.CompilerOptions { return host.program.Options() } | ||
func (host *emitHost) SourceFiles() []*ast.SourceFile { return host.program.SourceFiles() } | ||
func (host *emitHost) GetCurrentDirectory() string { return host.program.host.GetCurrentDirectory() } | ||
func (host *emitHost) CommonSourceDirectory() string { return host.program.CommonSourceDirectory() } | ||
func (host *emitHost) UseCaseSensitiveFileNames() bool { | ||
return host.program.host.FS().UseCaseSensitiveFileNames() | ||
} | ||
func (host *emitHost) IsEmitBlocked(file string) bool { | ||
// !!! | ||
return false | ||
} | ||
func (host *emitHost) WriteFile(fileName string, text string, writeByteOrderMark bool, _ []*ast.SourceFile, _ *WriteFileData) error { | ||
return host.program.host.FS().WriteFile(fileName, text, writeByteOrderMark) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
package compiler | ||
|
||
import ( | ||
"github.com/microsoft/typescript-go/internal/ast" | ||
"github.com/microsoft/typescript-go/internal/compiler/diagnostics" | ||
"github.com/microsoft/typescript-go/internal/core" | ||
"github.com/microsoft/typescript-go/internal/printer" | ||
"github.com/microsoft/typescript-go/internal/tspath" | ||
) | ||
|
||
type emitOnly byte | ||
|
||
const ( | ||
emitAll emitOnly = iota | ||
emitOnlyJs | ||
emitOnlyDts | ||
emitOnlyBuildInfo | ||
) | ||
|
||
type emitter struct { | ||
host EmitHost | ||
emitOnly emitOnly | ||
emittedFilesList []string | ||
emitterDiagnostics DiagnosticsCollection | ||
emitSkipped bool | ||
sourceMapDataList []*sourceMapEmitResult | ||
writer printer.EmitTextWriter | ||
paths *outputPaths | ||
sourceFile *ast.SourceFile | ||
} | ||
|
||
func (e *emitter) emit() { | ||
// !!! tracing | ||
e.emitJsFile(e.sourceFile, e.paths.jsFilePath, e.paths.sourceMapFilePath) | ||
e.emitDeclarationFile(e.sourceFile, e.paths.declarationFilePath, e.paths.declarationMapPath) | ||
e.emitBuildInfo(e.paths.buildInfoPath) | ||
} | ||
|
||
func (e *emitter) emitJsFile(sourceFile *ast.SourceFile, jsFilePath string, sourceMapFilePath string) { | ||
options := e.host.Options() | ||
|
||
if sourceFile == nil || e.emitOnly != emitAll && e.emitOnly != emitOnlyJs || len(jsFilePath) == 0 { | ||
return | ||
} | ||
if options.NoEmit == core.TSTrue || e.host.IsEmitBlocked(jsFilePath) { | ||
return | ||
} | ||
|
||
// !!! mark linked references | ||
// !!! transform the source files? | ||
|
||
printerOptions := printer.PrinterOptions{ | ||
NewLine: options.NewLine, | ||
// !!! | ||
} | ||
|
||
// create a printer to print the nodes | ||
printer := printer.NewPrinter(printerOptions, printer.PrintHandlers{ | ||
// !!! | ||
}) | ||
|
||
e.printSourceFile(jsFilePath, sourceMapFilePath, sourceFile, printer) | ||
|
||
if e.emittedFilesList != nil { | ||
e.emittedFilesList = append(e.emittedFilesList, jsFilePath) | ||
if sourceMapFilePath != "" { | ||
e.emittedFilesList = append(e.emittedFilesList, sourceMapFilePath) | ||
} | ||
} | ||
} | ||
|
||
func (e *emitter) emitDeclarationFile(sourceFile *ast.SourceFile, declarationFilePath string, declarationMapPath string) { | ||
// !!! | ||
} | ||
|
||
func (e *emitter) emitBuildInfo(buildInfoPath string) { | ||
// !!! | ||
} | ||
|
||
func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, sourceFile *ast.SourceFile, printer *printer.Printer) bool { | ||
// !!! sourceMapGenerator | ||
// !!! bundles not implemented, may be deprecated | ||
sourceFiles := []*ast.SourceFile{sourceFile} | ||
|
||
printer.Write(sourceFile.AsNode(), sourceFile, e.writer /*, sourceMapGenerator*/) | ||
|
||
// !!! add sourceMapGenerator to sourceMapDataList | ||
// !!! append sourceMappingURL to output | ||
// !!! write the source map | ||
e.writer.WriteLine() | ||
|
||
// Write the output file | ||
text := e.writer.String() | ||
data := &WriteFileData{} // !!! | ||
err := e.host.WriteFile(jsFilePath, text, e.host.Options().EmitBOM == core.TSTrue, sourceFiles, data) | ||
if err != nil { | ||
e.emitterDiagnostics.add(ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, jsFilePath, err.Error())) | ||
} | ||
|
||
// Reset state | ||
e.writer.Clear() | ||
return !data.SkippedDtsWrite | ||
} | ||
|
||
func getOutputExtension(fileName string, jsx core.JsxEmit) string { | ||
switch { | ||
case tspath.FileExtensionIs(fileName, tspath.ExtensionJson): | ||
return tspath.ExtensionJson | ||
case jsx == core.JsxEmitPreserve && tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionJsx, tspath.ExtensionTsx}): | ||
return tspath.ExtensionJsx | ||
case tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionMjs}): | ||
return tspath.ExtensionMjs | ||
case tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionCts, tspath.ExtensionCjs}): | ||
return tspath.ExtensionCjs | ||
default: | ||
return tspath.ExtensionJs | ||
} | ||
} | ||
|
||
func getSourceFilePathInNewDir(fileName string, newDirPath string, currentDirectory string, commonSourceDirectory string, useCaseSensitiveFileNames bool) string { | ||
sourceFilePath := tspath.GetNormalizedAbsolutePath(fileName, currentDirectory) | ||
commonSourceDirectory = tspath.EnsureTrailingDirectorySeparator(commonSourceDirectory) | ||
isSourceFileInCommonSourceDirectory := tspath.ContainsPath(commonSourceDirectory, sourceFilePath, tspath.ComparePathsOptions{ | ||
UseCaseSensitiveFileNames: useCaseSensitiveFileNames, | ||
CurrentDirectory: currentDirectory, | ||
}) | ||
if isSourceFileInCommonSourceDirectory { | ||
sourceFilePath = sourceFilePath[len(commonSourceDirectory):] | ||
} | ||
return tspath.CombinePaths(newDirPath, sourceFilePath) | ||
} | ||
|
||
func getOwnEmitOutputFilePath(fileName string, host EmitHost, extension string) string { | ||
compilerOptions := host.Options() | ||
var emitOutputFilePathWithoutExtension string | ||
if len(compilerOptions.OutDir) > 0 { | ||
currentDirectory := host.GetCurrentDirectory() | ||
emitOutputFilePathWithoutExtension = tspath.RemoveFileExtension(getSourceFilePathInNewDir( | ||
fileName, | ||
compilerOptions.OutDir, | ||
currentDirectory, | ||
host.CommonSourceDirectory(), | ||
host.UseCaseSensitiveFileNames(), | ||
)) | ||
} else { | ||
emitOutputFilePathWithoutExtension = tspath.RemoveFileExtension(fileName) | ||
} | ||
return emitOutputFilePathWithoutExtension + extension | ||
} | ||
|
||
func getSourceMapFilePath(jsFilePath string, options *core.CompilerOptions) string { | ||
// !!! | ||
return "" | ||
} | ||
|
||
func getDeclarationEmitOutputFilePath(file string, host EmitHost) string { | ||
// !!! | ||
return "" | ||
} | ||
|
||
type outputPaths struct { | ||
jsFilePath string | ||
sourceMapFilePath string | ||
declarationFilePath string | ||
declarationMapPath string | ||
buildInfoPath string | ||
} | ||
|
||
func getOutputPathsFor(sourceFile *ast.SourceFile, host EmitHost, forceDtsEmit bool) *outputPaths { | ||
options := host.Options() | ||
// !!! bundle not implemented, may be deprecated | ||
ownOutputFilePath := getOwnEmitOutputFilePath(sourceFile.FileName(), host, getOutputExtension(sourceFile.FileName(), options.Jsx)) | ||
isJsonFile := isJsonSourceFile(sourceFile) | ||
// If json file emits to the same location skip writing it, if emitDeclarationOnly skip writing it | ||
isJsonEmittedToSameLocation := isJsonFile && | ||
tspath.ComparePaths(sourceFile.FileName(), ownOutputFilePath, tspath.ComparePathsOptions{ | ||
CurrentDirectory: host.GetCurrentDirectory(), | ||
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(), | ||
}) == 0 | ||
paths := &outputPaths{} | ||
if options.EmitDeclarationOnly != core.TSTrue && !isJsonEmittedToSameLocation { | ||
paths.jsFilePath = ownOutputFilePath | ||
if !isJsonSourceFile(sourceFile) { | ||
paths.sourceMapFilePath = getSourceMapFilePath(paths.jsFilePath, options) | ||
} | ||
} | ||
if forceDtsEmit || options.GetEmitDeclarations() && !isJsonFile { | ||
paths.declarationFilePath = getDeclarationEmitOutputFilePath(sourceFile.FileName(), host) | ||
if options.GetAreDeclarationMapsEnabled() { | ||
paths.declarationMapPath = paths.declarationFilePath + ".map" | ||
} | ||
} | ||
return paths | ||
} | ||
|
||
func forEachEmittedFile(host EmitHost, action func(emitFileNames *outputPaths, sourceFile *ast.SourceFile) bool, sourceFiles []*ast.SourceFile, options *EmitOptions) bool { | ||
// !!! outFile not yet implemented, may be deprecated | ||
for _, sourceFile := range sourceFiles { | ||
if action(getOutputPathsFor(sourceFile, host, options.forceDtsEmit), sourceFile) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func sourceFileMayBeEmitted(sourceFile *ast.SourceFile, host EmitHost, forceDtsEmit bool) bool { | ||
// !!! Js files are emitted only if option is enabled | ||
|
||
// Declaration files are not emitted | ||
if sourceFile.IsDeclarationFile { | ||
return false | ||
} | ||
|
||
// !!! Source file from node_modules are not emitted | ||
|
||
// forcing dts emit => file needs to be emitted | ||
if forceDtsEmit { | ||
return true | ||
} | ||
|
||
// !!! Source files from referenced projects are not emitted | ||
|
||
// Any non json file should be emitted | ||
if !isJsonSourceFile(sourceFile) { | ||
return true | ||
} | ||
|
||
// !!! Should JSON input files be emitted | ||
return false | ||
} | ||
|
||
func getSourceFilesToEmit(host EmitHost, targetSourceFile *ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile { | ||
// !!! outFile not yet implemented, may be deprecated | ||
var sourceFiles []*ast.SourceFile | ||
if targetSourceFile != nil { | ||
sourceFiles = []*ast.SourceFile{targetSourceFile} | ||
} else { | ||
sourceFiles = host.SourceFiles() | ||
} | ||
return core.Filter(sourceFiles, func(sourceFile *ast.SourceFile) bool { | ||
return sourceFileMayBeEmitted(sourceFile, host, forceDtsEmit) | ||
}) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we ever going to have more than one implementation of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% certain yet. This was something we allowed users to override when consuming the API, so that all depends on our long term API story (as in, could someone build on top of our go package). I opted to leave it as-is to more faithfully port portions of the emitter.