microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.11k stars 12.5k forks source link

Expose emitted module specifiers on AMD outfile emit #35052

Open kitsonk opened 5 years ago

kitsonk commented 5 years ago

Search Terms

amd outfile module specifier

Suggestion

Currently when utilising the compiler APIs to emit as AMD to a single outfile, the emitted module specifier in the outfile is not exposed, but does not match the source file SourceFile.fileName or any other attribute.

It appears that there are internals of the TypeScript emitter which determine the module specifier that is included output. For example, if given /foo/bar/baz.ts and /foo/qat/qux.ts being part of the same program, when emitted as AMD in a single bundle the module specifier, the outfile will define() a module named bar/baz and qat/qux. It appears that the algorithm used looks for the common root and removes the extension to determine the module specifier.

Use Cases

When loading the bundle with an AMD loader, knowing what a module specifier is in the outfile is needed to be able to specify a module to load in a require() statement. So if we were automating a custom build, outputting a "bundle" and determining which module to require() to bootstrap the application, we have to "guess" and hope that the emitter doesn't change its algorithm of determining the module specifiers.

Examples

I'm not totally sure, but a public API that would take a SourceFile[] and provide back a string[] array of module specifiers. This could be used in Host.writeFile() to determine what module specifiers are contained in the written file.

Checklist

My suggestion meets these guidelines:

andrewbranch commented 4 years ago

I’m not familiar with this behavior (don’t think I’ve ever targeted AMD before), but just to clarify a few things:

I think I would personally expect module specifiers to be relative to the rootDir, and would feel pretty safe about this not changing. I’m kind of surprised if that’s not already the case, but it seems like I might be missing something about your request?

kitsonk commented 4 years ago

@andrewbranch I've created an example repo which recreates an environment which relates to this request.

  • where is the project root in this example scenario?

I don't believe this has an impact. In my example the project root contains a src directory, but all the module IDs are emitted based on their common root.

  • does baseUrl or rootDir have any impact on the output?

I don't believe so, I experimented in the example project and setting the baseUrl to ./ doesn't add src to the specifiers. It is the same output. I tried rootDir but I only tried one (setting it to [ "./" ] and again no impact.

  • if the output module specifiers were consistently predictable from project file structure, would that be sufficient?

They are, as far as I can tell, and I am using the logic I detected to determine the emitted specifiers. But that means that I am coupling to a behaviour that is not really part of the contract. It feels like something that I should be able to determine from the SourceFile.

I believe that SourceFile.moduleName is populated when you use the AMD directive (e.g. ///<amd-module name="foo"/>) and the module will always be emitted with that name. Typically when you target AMD and don't use outFile, the modules are written without any module name. It is only when you set and outFile or use the directive. If SourceFile.moduleName as populated on writeFile with the module names that were used in the emit, that would ensure that there is no doubt about what the module name is in the out file.

I’m kind of surprised if that’s not already the case, but it seems like I might be missing something about your request?

I was surprised too. 😁 Hopefully that adds a bit of clarity.

andrewbranch commented 4 years ago

Cool, thanks for the example. It looks like this comes from the getCommonSourceDirectory on EmitHost (which is internal), and despite some 4-year-old JSDoc that says

 * The emitted output name can be different from the input if:
 *  1. The module has a /// <amd-module name="<new name>" />
 *  2. --out or --outFile is used, making the name relative to the rootDir

it looks like it’s intentionally finding the common source directory instead of using the rootDir.

If SourceFile.moduleName as populated on writeFile with the module names that were used in the emit, that would ensure that there is no doubt about what the module name is in the out file.

So is it undefined right now? Are you using transpileModule or program.emit? If the former, you can provide a module name as an option. (I guess you’re not using transpileModule since that doesn’t take any kind of host object.) But either way, it seems reasonable to me to add the module name (it’s already been calculated at the top of the function) to the transformed source file here: https://github.com/microsoft/TypeScript/blob/71a91763f49ce168782eac59a65161ed0e1595dc/src/compiler/transformers/module/module.ts#L198-L199

@rbuckton thoughts?

kitsonk commented 4 years ago

So is it undefined right now?

Correct, on writeFile() on the host, the sourceFiles that are all there, just .moduleName is always undefined (I suspect unless I use the directive, but I haven't tried).