Skip to content

Commit 8d03397

Browse files
kitsonkry
authored andcommitted
Make bundles fully standalone (#3325)
- Bundles are fully standalone. They now include the shared loader with `deno_typescript`. - Refactor of the loader in `deno_typescript` to perform module instantiation in a more - Change of behaviour when an output file is not specified on the CLI. Previously a default name was determined and the bundle written to that file, now the bundle will be sent to `stdout`. - Refactors in the TypeScript compiler to be able to support the concept of a request type. This provides a cleaner abstraction and makes it easier to support things like single module transpiles to the userland. - Remove a "dangerous" circular dependency between `os.ts` and `deno.ts`, and define `pid` and `noColor` in a better way. - Don't bind early to `console` in `repl.ts`. - Add an integration test for generating a bundle.
1 parent ee1b8dc commit 8d03397

21 files changed

+336
-480
lines changed

cli/compilers/ts.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,23 @@ impl CompiledFileMetadata {
156156
}
157157
/// Creates the JSON message send to compiler.ts's onmessage.
158158
fn req(
159+
request_type: msg::CompilerRequestType,
159160
root_names: Vec<String>,
160161
compiler_config: CompilerConfig,
161-
bundle: Option<String>,
162+
out_file: Option<String>,
162163
) -> Buf {
163164
let j = match (compiler_config.path, compiler_config.content) {
164165
(Some(config_path), Some(config_data)) => json!({
166+
"type": request_type as i32,
165167
"rootNames": root_names,
166-
"bundle": bundle,
168+
"outFile": out_file,
167169
"configPath": config_path,
168170
"config": str::from_utf8(&config_data).unwrap(),
169171
}),
170172
_ => json!({
173+
"type": request_type as i32,
171174
"rootNames": root_names,
172-
"bundle": bundle,
175+
"outFile": out_file,
173176
}),
174177
};
175178

@@ -250,15 +253,20 @@ impl TsCompiler {
250253
self: &Self,
251254
global_state: ThreadSafeGlobalState,
252255
module_name: String,
253-
out_file: String,
256+
out_file: Option<String>,
254257
) -> impl Future<Item = (), Error = ErrBox> {
255258
debug!(
256259
"Invoking the compiler to bundle. module_name: {}",
257260
module_name
258261
);
259262

260263
let root_names = vec![module_name.clone()];
261-
let req_msg = req(root_names, self.config.clone(), Some(out_file));
264+
let req_msg = req(
265+
msg::CompilerRequestType::Bundle,
266+
root_names,
267+
self.config.clone(),
268+
out_file,
269+
);
262270

263271
let worker = TsCompiler::setup_worker(global_state.clone());
264272
let worker_ = worker.clone();
@@ -360,7 +368,12 @@ impl TsCompiler {
360368
);
361369

362370
let root_names = vec![module_url.to_string()];
363-
let req_msg = req(root_names, self.config.clone(), None);
371+
let req_msg = req(
372+
msg::CompilerRequestType::Compile,
373+
root_names,
374+
self.config.clone(),
375+
None,
376+
);
364377

365378
let worker = TsCompiler::setup_worker(global_state.clone());
366379
let worker_ = worker.clone();
@@ -709,7 +722,7 @@ mod tests {
709722
.bundle_async(
710723
state.clone(),
711724
module_name,
712-
String::from("$deno$/bundle.js"),
725+
Some(String::from("$deno$/bundle.js")),
713726
)
714727
.then(|result| {
715728
assert!(result.is_ok());

cli/flags.rs

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use clap::Arg;
66
use clap::ArgMatches;
77
use clap::Shell;
88
use clap::SubCommand;
9-
use deno::ModuleSpecifier;
109
use log::Level;
1110
use std;
1211
use std::str;
@@ -259,11 +258,16 @@ compiler.",
259258
SubCommand::with_name("bundle")
260259
.about("Bundle module and dependencies into single file")
261260
.long_about(
262-
"Output a single JavaScript file with all dependencies
261+
"Output a single JavaScript file with all dependencies.
262+
263+
If a out_file argument is omitted, the output of the bundle will be sent to
264+
standard out.
263265
264266
Example:
265267
266-
deno bundle https://deno.land/std/examples/colors.ts"
268+
deno bundle https://deno.land/std/examples/colors.ts
269+
270+
deno bundle https://deno.land/std/examples/colors.ts colors.bundle.js"
267271
)
268272
.arg(Arg::with_name("source_file").takes_value(true).required(true))
269273
.arg(Arg::with_name("out_file").takes_value(true).required(false)),
@@ -793,32 +797,6 @@ pub enum DenoSubcommand {
793797
Version,
794798
}
795799

796-
fn get_default_bundle_filename(source_file: &str) -> String {
797-
let specifier = ModuleSpecifier::resolve_url_or_path(source_file).unwrap();
798-
let path_segments = specifier.as_url().path_segments().unwrap();
799-
let file_name = path_segments.filter(|s| !s.is_empty()).last().unwrap();
800-
let file_stem = file_name.trim_end_matches(".ts").trim_end_matches(".js");
801-
format!("{}.bundle.js", file_stem)
802-
}
803-
804-
#[test]
805-
fn test_get_default_bundle_filename() {
806-
assert_eq!(get_default_bundle_filename("blah.ts"), "blah.bundle.js");
807-
assert_eq!(
808-
get_default_bundle_filename("http://example.com/blah.ts"),
809-
"blah.bundle.js"
810-
);
811-
assert_eq!(get_default_bundle_filename("blah.js"), "blah.bundle.js");
812-
assert_eq!(
813-
get_default_bundle_filename("http://example.com/blah.js"),
814-
"blah.bundle.js"
815-
);
816-
assert_eq!(
817-
get_default_bundle_filename("http://zombo.com/stuff/"),
818-
"stuff.bundle.js"
819-
);
820-
}
821-
822800
pub fn flags_from_vec(
823801
args: Vec<String>,
824802
) -> (DenoFlags, DenoSubcommand, Vec<String>) {
@@ -835,11 +813,13 @@ pub fn flags_from_vec(
835813
("bundle", Some(bundle_match)) => {
836814
flags.allow_write = true;
837815
let source_file: &str = bundle_match.value_of("source_file").unwrap();
838-
let out_file = bundle_match
839-
.value_of("out_file")
840-
.map(String::from)
841-
.unwrap_or_else(|| get_default_bundle_filename(source_file));
842-
argv.extend(vec![source_file.to_string(), out_file.to_string()]);
816+
let out_file = bundle_match.value_of("out_file").map(String::from);
817+
match out_file {
818+
Some(out_file) => {
819+
argv.extend(vec![source_file.to_string(), out_file.to_string()])
820+
}
821+
_ => argv.extend(vec![source_file.to_string()]),
822+
}
843823
DenoSubcommand::Bundle
844824
}
845825
("completions", Some(completions_match)) => {

cli/js/compiler.ts

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ enum MediaType {
3131
Unknown = 5
3232
}
3333

34+
// Warning! The values in this enum are duplicated in cli/msg.rs
35+
// Update carefully!
36+
enum CompilerRequestType {
37+
Compile = 0,
38+
Bundle = 1
39+
}
40+
3441
// Startup boilerplate. This is necessary because the compiler has its own
3542
// snapshot. (It would be great if we could remove these things or centralize
3643
// them somewhere else.)
@@ -44,16 +51,23 @@ window["denoMain"] = denoMain;
4451

4552
const ASSETS = "$asset$";
4653
const OUT_DIR = "$deno$";
54+
const BUNDLE_LOADER = "bundle_loader.js";
4755

4856
/** The format of the work message payload coming from the privileged side */
49-
interface CompilerReq {
57+
type CompilerRequest = {
5058
rootNames: string[];
51-
bundle?: string;
5259
// TODO(ry) add compiler config to this interface.
5360
// options: ts.CompilerOptions;
5461
configPath?: string;
5562
config?: string;
56-
}
63+
} & (
64+
| {
65+
type: CompilerRequestType.Compile;
66+
}
67+
| {
68+
type: CompilerRequestType.Bundle;
69+
outFile?: string;
70+
});
5771

5872
interface ConfigureResponse {
5973
ignoredOptions?: string[];
@@ -271,7 +285,7 @@ function fetchSourceFiles(
271285
async function processImports(
272286
specifiers: Array<[string, string]>,
273287
referrer = ""
274-
): Promise<void> {
288+
): Promise<SourceFileJson[]> {
275289
if (!specifiers.length) {
276290
return;
277291
}
@@ -287,6 +301,7 @@ async function processImports(
287301
await processImports(sourceFile.imports(), sourceFile.url);
288302
}
289303
}
304+
return sourceFiles;
290305
}
291306

292307
/** Utility function to turn the number of bytes into a human readable
@@ -314,16 +329,36 @@ function cache(extension: string, moduleId: string, contents: string): void {
314329
const encoder = new TextEncoder();
315330

316331
/** Given a fileName and the data, emit the file to the file system. */
317-
function emitBundle(fileName: string, data: string): void {
332+
function emitBundle(
333+
rootNames: string[],
334+
fileName: string | undefined,
335+
data: string,
336+
sourceFiles: readonly ts.SourceFile[]
337+
): void {
318338
// For internal purposes, when trying to emit to `$deno$` just no-op
319-
if (fileName.startsWith("$deno$")) {
339+
if (fileName && fileName.startsWith("$deno$")) {
320340
console.warn("skipping emitBundle", fileName);
321341
return;
322342
}
323-
const encodedData = encoder.encode(data);
324-
console.log(`Emitting bundle to "${fileName}"`);
325-
writeFileSync(fileName, encodedData);
326-
console.log(`${humanFileSize(encodedData.length)} emitted.`);
343+
const loader = fetchAsset(BUNDLE_LOADER);
344+
// when outputting to AMD and a single outfile, TypeScript makes up the module
345+
// specifiers which are used to define the modules, and doesn't expose them
346+
// publicly, so we have to try to replicate
347+
const sources = sourceFiles.map(sf => sf.fileName);
348+
const sharedPath = util.commonPath(sources);
349+
rootNames = rootNames.map(id =>
350+
id.replace(sharedPath, "").replace(/\.\w+$/i, "")
351+
);
352+
const instantiate = `instantiate(${JSON.stringify(rootNames)});\n`;
353+
const bundle = `${loader}\n${data}\n${instantiate}`;
354+
if (fileName) {
355+
const encodedData = encoder.encode(bundle);
356+
console.warn(`Emitting bundle to "${fileName}"`);
357+
writeFileSync(fileName, encodedData);
358+
console.warn(`${humanFileSize(encodedData.length)} emitted.`);
359+
} else {
360+
console.log(bundle);
361+
}
327362
}
328363

329364
/** Returns the TypeScript Extension enum for a given media type. */
@@ -380,17 +415,23 @@ class Host implements ts.CompilerHost {
380415

381416
/** Provides the `ts.HostCompiler` interface for Deno.
382417
*
418+
* @param _rootNames A set of modules that are the ones that should be
419+
* instantiated first. Used when generating a bundle.
383420
* @param _bundle Set to a string value to configure the host to write out a
384421
* bundle instead of caching individual files.
385422
*/
386-
constructor(private _bundle?: string) {
387-
if (this._bundle) {
423+
constructor(
424+
private _requestType: CompilerRequestType,
425+
private _rootNames: string[],
426+
private _outFile?: string
427+
) {
428+
if (this._requestType === CompilerRequestType.Bundle) {
388429
// options we need to change when we are generating a bundle
389430
const bundlerOptions: ts.CompilerOptions = {
390431
module: ts.ModuleKind.AMD,
391-
inlineSourceMap: true,
392432
outDir: undefined,
393433
outFile: `${OUT_DIR}/bundle.js`,
434+
// disabled until we have effective way to modify source maps
394435
sourceMap: false
395436
};
396437
Object.assign(this._options, bundlerOptions);
@@ -531,10 +572,11 @@ class Host implements ts.CompilerHost {
531572
): void {
532573
util.log("compiler::host.writeFile", fileName);
533574
try {
534-
if (this._bundle) {
535-
emitBundle(this._bundle, data);
575+
assert(sourceFiles != null);
576+
if (this._requestType === CompilerRequestType.Bundle) {
577+
emitBundle(this._rootNames, this._outFile, data, sourceFiles!);
536578
} else {
537-
assert(sourceFiles != null && sourceFiles.length == 1);
579+
assert(sourceFiles.length == 1);
538580
const url = sourceFiles![0].fileName;
539581
const sourceFile = SourceFile.get(url);
540582

@@ -579,16 +621,29 @@ class Host implements ts.CompilerHost {
579621
// lazy instantiating the compiler web worker
580622
window.compilerMain = function compilerMain(): void {
581623
// workerMain should have already been called since a compiler is a worker.
582-
window.onmessage = async ({ data }: { data: CompilerReq }): Promise<void> => {
583-
const { rootNames, configPath, config, bundle } = data;
584-
util.log(">>> compile start", { rootNames, bundle });
624+
window.onmessage = async ({
625+
data: request
626+
}: {
627+
data: CompilerRequest;
628+
}): Promise<void> => {
629+
const { rootNames, configPath, config } = request;
630+
util.log(">>> compile start", {
631+
rootNames,
632+
type: CompilerRequestType[request.type]
633+
});
585634

586635
// This will recursively analyse all the code for other imports, requesting
587636
// those from the privileged side, populating the in memory cache which
588637
// will be used by the host, before resolving.
589-
await processImports(rootNames.map(rootName => [rootName, rootName]));
590-
591-
const host = new Host(bundle);
638+
const resolvedRootModules = (await processImports(
639+
rootNames.map(rootName => [rootName, rootName])
640+
)).map(info => info.url);
641+
642+
const host = new Host(
643+
request.type,
644+
resolvedRootModules,
645+
request.type === CompilerRequestType.Bundle ? request.outFile : undefined
646+
);
592647
let emitSkipped = true;
593648
let diagnostics: ts.Diagnostic[] | undefined;
594649

@@ -642,8 +697,9 @@ window.compilerMain = function compilerMain(): void {
642697

643698
// We will only proceed with the emit if there are no diagnostics.
644699
if (diagnostics && diagnostics.length === 0) {
645-
if (bundle) {
646-
console.log(`Bundling "${bundle}"`);
700+
if (request.type === CompilerRequestType.Bundle) {
701+
// warning so it goes to stderr instead of stdout
702+
console.warn(`Bundling "${resolvedRootModules.join(`", "`)}"`);
647703
}
648704
const emitResult = program.emit();
649705
emitSkipped = emitResult.emitSkipped;
@@ -662,7 +718,10 @@ window.compilerMain = function compilerMain(): void {
662718

663719
postMessage(result);
664720

665-
util.log("<<< compile end", { rootNames, bundle });
721+
util.log("<<< compile end", {
722+
rootNames,
723+
type: CompilerRequestType[request.type]
724+
});
666725

667726
// The compiler isolate exits after a single message.
668727
workerClose();

cli/js/deno.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,3 @@ export let pid: number;
112112

113113
/** Reflects the NO_COLOR environment variable: https://no-color.org/ */
114114
export let noColor: boolean;
115-
116-
// TODO(ry) This should not be exposed to Deno.
117-
export function _setGlobals(pid_: number, noColor_: boolean): void {
118-
pid = pid_;
119-
noColor = noColor_;
120-
}

0 commit comments

Comments
 (0)