Skip to content

Commit

Permalink
Add ability to load JSON as modules (#1065)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk authored and ry committed Oct 31, 2018
1 parent 0fbee30 commit 2422e52
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 44 deletions.
122 changes: 82 additions & 40 deletions js/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export interface Ts {
*/
export class ModuleMetaData implements ts.IScriptSnapshot {
public deps?: ModuleFileName[];
public readonly exports = {};
public exports = {};
public factory?: AmdFactory;
public gatheringDeps = false;
public hasRun = false;
Expand Down Expand Up @@ -129,6 +129,15 @@ function getExtension(
}
}

/** Generate output code for a provided JSON string along with its source. */
export function jsonAmdTemplate(
jsonString: string,
sourceFileName: string
): OutputCode {
// tslint:disable-next-line:max-line-length
return `define([], function() { return JSON.parse(\`${jsonString}\`); });\n//# sourceURL=${sourceFileName}`;
}

/** A singleton class that combines the TypeScript Language Service host API
* with Deno specific APIs to provide an interface for compiling and running
* TypeScript and JavaScript modules.
Expand All @@ -153,11 +162,12 @@ export class DenoCompiler
>();
// TODO ideally this are not static and can be influenced by command line
// arguments
private readonly _options: Readonly<ts.CompilerOptions> = {
private readonly _options: ts.CompilerOptions = {
allowJs: true,
checkJs: true,
module: ts.ModuleKind.AMD,
outDir: "$deno$",
resolveJsonModule: true,
sourceMap: true,
stripComments: true,
target: ts.ScriptTarget.ESNext
Expand Down Expand Up @@ -198,7 +208,15 @@ export class DenoCompiler
);
assert(moduleMetaData.hasRun === false, "Module has already been run.");
// asserts not tracked by TypeScripts, so using not null operator
moduleMetaData.factory!(...this._getFactoryArguments(moduleMetaData));
const exports = moduleMetaData.factory!(
...this._getFactoryArguments(moduleMetaData)
);
// For JSON module support and potential future features.
// TypeScript always imports `exports` and mutates it directly, but the
// AMD specification allows values to be returned from the factory.
if (exports != null) {
moduleMetaData.exports = exports;
}
moduleMetaData.hasRun = true;
}
}
Expand Down Expand Up @@ -421,50 +439,74 @@ export class DenoCompiler
if (!recompile && moduleMetaData.outputCode) {
return moduleMetaData.outputCode;
}
const { fileName, sourceCode, moduleId } = moduleMetaData;
const { fileName, sourceCode, mediaType, moduleId } = moduleMetaData;
console.warn("Compiling", moduleId);
const service = this._service;
const output = service.getEmitOutput(fileName);

// Get the relevant diagnostics - this is 3x faster than
// `getPreEmitDiagnostics`.
const diagnostics = [
...service.getCompilerOptionsDiagnostics(),
...service.getSyntacticDiagnostics(fileName),
...service.getSemanticDiagnostics(fileName)
];
if (diagnostics.length > 0) {
const errMsg = this._ts.formatDiagnosticsWithColorAndContext(
diagnostics,
this
// Instead of using TypeScript to transpile JSON modules, we will just do
// it directly.
if (mediaType === MediaType.Json) {
moduleMetaData.outputCode = jsonAmdTemplate(sourceCode, fileName);
} else {
assert(
mediaType === MediaType.TypeScript || mediaType === MediaType.JavaScript
);
console.log(errMsg);
// All TypeScript errors are terminal for deno
this._os.exit(1);
}
// TypeScript is overly opinionated that only CommonJS modules kinds can
// support JSON imports. Allegedly this was fixed in
// Microsoft/TypeScript#26825 but that doesn't seem to be working here,
// so we will trick the TypeScript compiler.
this._options.module = ts.ModuleKind.AMD;
const output = service.getEmitOutput(fileName);
this._options.module = ts.ModuleKind.CommonJS;

// Get the relevant diagnostics - this is 3x faster than
// `getPreEmitDiagnostics`.
const diagnostics = [
...service.getCompilerOptionsDiagnostics(),
...service.getSyntacticDiagnostics(fileName),
...service.getSemanticDiagnostics(fileName)
];
if (diagnostics.length > 0) {
const errMsg = this._ts.formatDiagnosticsWithColorAndContext(
diagnostics,
this
);
console.log(errMsg);
// All TypeScript errors are terminal for deno
this._os.exit(1);
}

assert(!output.emitSkipped, "The emit was skipped for an unknown reason.");
assert(
!output.emitSkipped,
"The emit was skipped for an unknown reason."
);

assert(
output.outputFiles.length === 2,
`Expected 2 files to be emitted, got ${output.outputFiles.length}.`
);
assert(
output.outputFiles.length === 2,
`Expected 2 files to be emitted, got ${output.outputFiles.length}.`
);

const [sourceMapFile, outputFile] = output.outputFiles;
assert(
sourceMapFile.name.endsWith(".map"),
"Expected first emitted file to be a source map"
);
assert(
outputFile.name.endsWith(".js"),
"Expected second emitted file to be JavaScript"
);
moduleMetaData.outputCode = `${
outputFile.text
}\n//# sourceURL=${fileName}`;
moduleMetaData.sourceMap = sourceMapFile.text;
}

const [sourceMapFile, outputFile] = output.outputFiles;
assert(
sourceMapFile.name.endsWith(".map"),
"Expected first emitted file to be a source map"
);
assert(
outputFile.name.endsWith(".js"),
"Expected second emitted file to be JavaScript"
);
const outputCode = (moduleMetaData.outputCode = `${
outputFile.text
}\n//# sourceURL=${fileName}`);
const sourceMap = (moduleMetaData.sourceMap = sourceMapFile.text);
moduleMetaData.scriptVersion = "1";
this._os.codeCache(fileName, sourceCode, outputCode, sourceMap);
this._os.codeCache(
fileName,
sourceCode,
moduleMetaData.outputCode,
moduleMetaData.sourceMap
);
return moduleMetaData.outputCode;
}

Expand Down
72 changes: 68 additions & 4 deletions js/compiler_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as ts from "typescript";
// We use a silly amount of `any` in these tests...
// tslint:disable:no-any

const { DenoCompiler } = (deno as any)._compiler;
const { DenoCompiler, jsonAmdTemplate } = (deno as any)._compiler;

interface ModuleInfo {
moduleName: string | undefined;
Expand Down Expand Up @@ -118,6 +118,11 @@ const fooBazTsOutput = `define(["require", "exports", "./bar.ts"], function (req

// This is not a valid map, just mock data
const fooBazTsSourcemap = `{"version":3,"file":"baz.js","sourceRoot":"","sources":["file:///root/project/foo/baz.ts"],"names":[],"mappings":""}`;

const loadConfigSource = `import * as config from "./config.json";
console.log(config.foo.baz);
`;
const configJsonSource = `{"foo":{"bar": true,"baz": ["qat", 1]}}`;
// tslint:enable:max-line-length

const moduleMap: {
Expand Down Expand Up @@ -148,6 +153,14 @@ const moduleMap: {
"console.log();",
null,
null
),
"loadConfig.ts": mockModuleInfo(
"/root/project/loadConfig.ts",
"/root/project/loadConfig.ts",
MediaType.TypeScript,
loadConfigSource,
null,
null
)
},
"/root/project/foo/baz.ts": {
Expand All @@ -166,6 +179,16 @@ const moduleMap: {
"/root/project/modB.ts": {
"./modA.ts": modAModuleInfo
},
"/root/project/loadConfig.ts": {
"./config.json": mockModuleInfo(
"/root/project/config.json",
"/root/project/config.json",
MediaType.Json,
configJsonSource,
null,
null
)
},
"/moduleKinds": {
"foo.ts": mockModuleInfo(
"foo",
Expand Down Expand Up @@ -280,7 +303,7 @@ const osMock = {
return mockModuleInfo(null, null, null, null, null, null);
},
exit(code: number): never {
throw new Error(`os.exit(${code})`);
throw new Error(`Unexpected call to os.exit(${code})`);
}
};
const tsMock = {
Expand All @@ -289,9 +312,9 @@ const tsMock = {
},
formatDiagnosticsWithColorAndContext(
diagnostics: ReadonlyArray<ts.Diagnostic>,
host: ts.FormatDiagnosticsHost
_host: ts.FormatDiagnosticsHost
): string {
return "";
return JSON.stringify(diagnostics.map(({ messageText }) => messageText));
}
};

Expand Down Expand Up @@ -374,6 +397,23 @@ function teardown() {
Object.assign(compilerInstance, originals);
}

test(function testJsonAmdTemplate() {
let deps: string[];
let factory: Function;
function define(d: string[], f: Function) {
deps = d;
factory = f;
}

const code = jsonAmdTemplate(`{ "hello": "world", "foo": "bar" }`);
const result = eval(code);
assert(result == null);
assertEqual(deps && deps.length, 0);
assert(factory != null);
const factoryResult = factory();
assertEqual(factoryResult, { hello: "world", foo: "bar" });
});

test(function compilerInstance() {
assert(DenoCompiler != null);
assert(DenoCompiler.instance() != null);
Expand Down Expand Up @@ -479,6 +519,29 @@ test(function compilerRunCircularDependency() {
teardown();
});

test(function compilerLoadJsonModule() {
setup();
const factoryStack: string[] = [];
const configJsonDeps: string[] = [];
const configJsonFactory = () => {
factoryStack.push("configJson");
return JSON.parse(configJsonSource);
};
const loadConfigDeps = ["require", "exports", "./config.json"];
const loadConfigFactory = (_require, _exports, _config) => {
factoryStack.push("loadConfig");
assertEqual(_config, JSON.parse(configJsonSource));
};

mockDepsStack.push(configJsonDeps);
mockFactoryStack.push(configJsonFactory);
mockDepsStack.push(loadConfigDeps);
mockFactoryStack.push(loadConfigFactory);
compilerInstance.run("loadConfig.ts", "/root/project");
assertEqual(factoryStack, ["configJson", "loadConfig"]);
teardown();
});

test(function compilerResolveModule() {
setup();
const moduleMetaData = compilerInstance.resolveModule(
Expand Down Expand Up @@ -544,6 +607,7 @@ test(function compilerGetCompilationSettings() {
"checkJs",
"module",
"outDir",
"resolveJsonModule",
"sourceMap",
"stripComments",
"target"
Expand Down
3 changes: 3 additions & 0 deletions tests/020_json_modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as config from "./subdir/config.json";

console.log(JSON.stringify(config));
1 change: 1 addition & 0 deletions tests/020_json_modules.ts.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"foo":{"bar":true,"baz":["qat",1]}}
6 changes: 6 additions & 0 deletions tests/subdir/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"foo": {
"bar": true,
"baz": ["qat", 1]
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"preserveConstEnums": true,
"pretty": true,
"removeComments": true,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"target": "esnext",
Expand Down

2 comments on commit 2422e52

@ry
Copy link
Member

@ry ry commented on 2422e52 Oct 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears this commit has slowed down execution time. I will let it run for a bit more, but if the trend continues I may revert:
screen shot 2018-10-31 at 2 26 35 pm

@kitsonk
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ry I would revert... it clearly is a regression. I need to dig into it.

Please sign in to comment.