Co-authored-by: Luca Casonato <lucacasonato@yahoo.com>
10 KiB
Runtime compiler APIs
⚠️ The runtime compiler API is unstable (and requires the
--unstable
flag to be used to enable it).
The runtime compiler API allows access to the internals of Deno to be able to
type check, transpile and bundle JavaScript and TypeScript. As of Deno 1.7,
several disparate APIs we consolidated into a single API, Deno.emit()
.
Deno.emit()
The API is defined in the Deno
namespace as:
function emit(
rootSpecifier: string | URL,
options?: EmitOptions,
): Promise<EmitResult>;
The emit options are defined in the Deno
namespace as:
interface EmitOptions {
/** Indicate that the source code should be emitted to a single file
* JavaScript bundle that is a single ES module (`"esm"`) or a single file
* self contained script we executes in an immediately invoked function
* when loaded (`"iife"`). */
bundle?: "esm" | "iife";
/** If `true` then the sources will be typed checked, returning any
* diagnostic errors in the result. If `false` type checking will be
* skipped. Defaults to `true`.
*
* *Note* by default, only TypeScript will be type checked, just like on
* the command line. Use the `compilerOptions` options of `checkJs` to
* enable type checking of JavaScript. */
check?: boolean;
/** A set of options that are aligned to TypeScript compiler options that
* are supported by Deno. */
compilerOptions?: CompilerOptions;
/** An [import-map](https://deno.land/manual/linking_to_external_code/import_maps#import-maps)
* which will be applied to the imports. */
importMap?: ImportMap;
/** An absolute path to an [import-map](https://deno.land/manual/linking_to_external_code/import_maps#import-maps).
* Required to be specified if an `importMap` is specified to be able to
* determine resolution of relative paths. If a `importMap` is not
* specified, then it will assumed the file path points to an import map on
* disk and will be attempted to be loaded based on current runtime
* permissions.
*/
importMapPath?: string;
/** A record of sources to use when doing the emit. If provided, Deno will
* use these sources instead of trying to resolve the modules externally. */
sources?: Record<string, string>;
}
The emit result is defined in the Deno
namespace as:
interface EmitResult {
/** Diagnostic messages returned from the type checker (`tsc`). */
diagnostics: Diagnostic[];
/** Any emitted files. If bundled, then the JavaScript will have the
* key of `deno:///bundle.js` with an optional map (based on
* `compilerOptions`) in `deno:///bundle.js.map`. */
files: Record<string, string>;
/** An optional array of any compiler options that were ignored by Deno. */
ignoredOptions?: string[];
/** An array of internal statistics related to the emit, for diagnostic
* purposes. */
stats: Array<[string, number]>;
}
The API is designed to support several use cases, which are described in the sections below.
Using external sources
Using external sources, both local and remote, Deno.emit()
can behave like
deno cache
does on the command line, resolving those external dependencies,
type checking those dependencies, and providing an emitted output.
By default, Deno.emit()
will utilise external resources. The rootSpecifier
supplied as the first argument will determine what module will be used as the
root. The root module is similar to what you would provide on the command line.
For example if you did:
> deno run mod.ts
You could do something similar with Deno.emit()
:
try {
const { files } = await Deno.emit("mod.ts");
for (const [fileName, text] of Object.entries(files)) {
console.log(`emitted ${fileName} with a length of ${text.length}`);
}
} catch (e) {
// something went wrong, inspect `e` to determine
}
Deno.emit()
will use the same on disk cache for remote modules that the
standard CLI does, and it inherits the permissions and cache options of the
process that executes it.
If the rootSpecifier is a relative path, then the current working directory of the Deno process will be used to resolve the specifier. (Not relative to the current module!)
The rootSpecifier can be a string file path, a string URL, or a URL.
Deno.emit()
supports the same protocols for URLs that Deno supports, which are
currently file
, http
, https
, and data
.
Providing sources
Instead of resolving modules externally, you can provide Deno.emit()
with the
sources directly. This is especially useful for a server to be able to provide
on demand compiling of code supplied by a user, where the Deno process has
collected all the code it wants to emit.
The sources are passed in the sources property of the Deno.emit()
options
argument:
const { files } = await Deno.emit("/mod.ts", {
sources: {
"/mod.ts": `import * as a from "./a.ts";\nconsole.log(a);\n`,
"/a.ts": `export const a: Record<string, string> = {};\n`,
},
});
When sources are provided, Deno will no longer look externally and will try to resolve all modules from within the map of sources provided, though the module resolution follow the same rules as if the modules were external. For example all module specifiers need their full filename. Also, because there are no media types, if you are providing remote URLs in the sources, the path should end with the appropriate extension, so that Deno can determine how to handle the file.
Type checking and emitting
By default, Deno.emit()
will type check any TypeScript (and TSX) it
encounters, just like on the command line. It will also attempt to transpile
JSX, but will leave JavaScript "alone". This behavior can be changed by changing
the compiler options. For example if you wanted Deno to type check your
JavaScript as well, you could set the checkJs option to true
in the compiler
options:
const { files, diagnostics } = await Deno.emit("./mod.js", {
compilerOptions: {
checkJs: true,
},
});
The Deno.emit()
result provides any diagnostic messages about the code
supplied. On the command line, any diagnostic messages get logged to stderr and
the Deno process terminates, but with Deno.emit()
they are returned to the
caller.
Typically you will want to check if there are any diagnostics and handle them
appropriately. You can introspect the diagnostics individually, but there is a
handy formatting function available to make it easier to potentially log the
diagnostics to the console for the user called Deno.formatDiagnostics()
:
const { files, diagnostics } = await Deno.emit("./mod.ts");
if (diagnostics.length) {
// there is something that impacted the emit
console.warn(Deno.formatDiagnostics(diagnostics));
}
Bundling
Deno.emit()
is also capable of providing output similar to deno bundle
on
the command line. This is enabled by setting the bundle option to "esm"
or
"iife"
. Currently Deno supports bundling as a single file ES module ("esm"
)
or a single file self contained legacy script ("iife"
).
const { files, diagnostics } = await Deno.emit("./mod.ts", {
bundle: "esm",
});
The files of the result will contain a single key named deno:///bundle.js
of
which the value with be the resulting bundle.
⚠️ Just like with
deno bundle
, the bundle will not include things like dynamic imports or worker scripts, and those would be expected to be resolved and available when the code is run.
Import maps
Deno.emit()
supports import maps as well, just like on the command line. This
is a really powerful feature that can be used even more effectively to emit and
bundle code.
Because of the way import maps work, when using with Deno.emit()
you also have
to supply an absolute URL for the import map. This allows Deno to resolve any
relative URLs specified in the import map. This needs to be supplied even if the
import map doesn't contain any relative URLs. The URL does not need to really
exist, it is just feed to the API.
An example might be that I want to use a bare specifier to load a special version of lodash I am using with my project. I could do the following:
const { files } = await Deno.emit("mod.ts", {
bundle: "esm",
importMap: {
imports: {
"lodash": "https://deno.land/x/lodash",
},
},
importMapPath: "file:///import-map.json",
});
⚠️ If you are not bundling your code, the emitted code specifiers do not get rewritten, that means that whatever process will consume the code, Deno or a browser for example, would need to support import maps and have that map available at runtime.
Skip type checking/transpiling only
Deno.emit()
supports skipping type checking similar to the --no-check
flag
on the command line. This is accomplished by setting the check property to
false
:
const { files } = await Deno.emit("./mod.ts", {
check: false,
});
Setting check to false
will instruct Deno to not utilise the TypeScript
compiler to type check the code and emit it, instead only transpiling the code
from within Deno. This can be significantly quicker than doing the full type
checking.
Compiler options
Deno.emit()
supports quite a few compiler options that can impact how code is
type checked and emitted. They are similar to the options supported by a
tsconfig.json
in the compilerOptions
section, but there are several options
that are not supported. This is because they are either meaningless in Deno or
would cause Deno to not be able to work properly. The defaults for Deno.emit()
are the same defaults that are on the command line. The options are
documented here
along with their default values and are built into the Deno types.
If you are type checking your code, the compiler options will be type checked
for you, but if for some reason you are either dynamically providing the
compiler options or are not type checking, then the result of Deno.emit()
will
provide you with an array of ignoredOptions if there are any.
⚠️ we have only tried to disable/remove options that we know won't work, that does not mean we extensively test all options in all configurations under
Deno.emit()
. You may find that some behaviors do not match what you can get fromtsc
or are otherwise incompatible. If you do find something that doesn't work, please do feel free to raise an issue.