mirror of
https://github.com/denoland/deno.git
synced 2025-02-01 20:25:12 -05:00
Add ability to load JSON as modules (#1065)
This commit is contained in:
parent
0fbee30f05
commit
2422e52625
6 changed files with 162 additions and 45 deletions
124
js/compiler.ts
124
js/compiler.ts
|
@ -79,7 +79,7 @@ export interface Ts {
|
||||||
*/
|
*/
|
||||||
export class ModuleMetaData implements ts.IScriptSnapshot {
|
export class ModuleMetaData implements ts.IScriptSnapshot {
|
||||||
public deps?: ModuleFileName[];
|
public deps?: ModuleFileName[];
|
||||||
public readonly exports = {};
|
public exports = {};
|
||||||
public factory?: AmdFactory;
|
public factory?: AmdFactory;
|
||||||
public gatheringDeps = false;
|
public gatheringDeps = false;
|
||||||
public hasRun = false;
|
public hasRun = false;
|
||||||
|
@ -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
|
/** A singleton class that combines the TypeScript Language Service host API
|
||||||
* with Deno specific APIs to provide an interface for compiling and running
|
* with Deno specific APIs to provide an interface for compiling and running
|
||||||
* TypeScript and JavaScript modules.
|
* TypeScript and JavaScript modules.
|
||||||
|
@ -153,11 +162,12 @@ export class DenoCompiler
|
||||||
>();
|
>();
|
||||||
// TODO ideally this are not static and can be influenced by command line
|
// TODO ideally this are not static and can be influenced by command line
|
||||||
// arguments
|
// arguments
|
||||||
private readonly _options: Readonly<ts.CompilerOptions> = {
|
private readonly _options: ts.CompilerOptions = {
|
||||||
allowJs: true,
|
allowJs: true,
|
||||||
checkJs: true,
|
checkJs: true,
|
||||||
module: ts.ModuleKind.AMD,
|
module: ts.ModuleKind.AMD,
|
||||||
outDir: "$deno$",
|
outDir: "$deno$",
|
||||||
|
resolveJsonModule: true,
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
stripComments: true,
|
stripComments: true,
|
||||||
target: ts.ScriptTarget.ESNext
|
target: ts.ScriptTarget.ESNext
|
||||||
|
@ -198,7 +208,15 @@ export class DenoCompiler
|
||||||
);
|
);
|
||||||
assert(moduleMetaData.hasRun === false, "Module has already been run.");
|
assert(moduleMetaData.hasRun === false, "Module has already been run.");
|
||||||
// asserts not tracked by TypeScripts, so using not null operator
|
// 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;
|
moduleMetaData.hasRun = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,50 +439,74 @@ export class DenoCompiler
|
||||||
if (!recompile && moduleMetaData.outputCode) {
|
if (!recompile && moduleMetaData.outputCode) {
|
||||||
return moduleMetaData.outputCode;
|
return moduleMetaData.outputCode;
|
||||||
}
|
}
|
||||||
const { fileName, sourceCode, moduleId } = moduleMetaData;
|
const { fileName, sourceCode, mediaType, moduleId } = moduleMetaData;
|
||||||
console.warn("Compiling", moduleId);
|
console.warn("Compiling", moduleId);
|
||||||
const service = this._service;
|
const service = this._service;
|
||||||
const output = service.getEmitOutput(fileName);
|
// Instead of using TypeScript to transpile JSON modules, we will just do
|
||||||
|
// it directly.
|
||||||
// Get the relevant diagnostics - this is 3x faster than
|
if (mediaType === MediaType.Json) {
|
||||||
// `getPreEmitDiagnostics`.
|
moduleMetaData.outputCode = jsonAmdTemplate(sourceCode, fileName);
|
||||||
const diagnostics = [
|
} else {
|
||||||
...service.getCompilerOptionsDiagnostics(),
|
assert(
|
||||||
...service.getSyntacticDiagnostics(fileName),
|
mediaType === MediaType.TypeScript || mediaType === MediaType.JavaScript
|
||||||
...service.getSemanticDiagnostics(fileName)
|
|
||||||
];
|
|
||||||
if (diagnostics.length > 0) {
|
|
||||||
const errMsg = this._ts.formatDiagnosticsWithColorAndContext(
|
|
||||||
diagnostics,
|
|
||||||
this
|
|
||||||
);
|
);
|
||||||
console.log(errMsg);
|
// TypeScript is overly opinionated that only CommonJS modules kinds can
|
||||||
// All TypeScript errors are terminal for deno
|
// support JSON imports. Allegedly this was fixed in
|
||||||
this._os.exit(1);
|
// 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.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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}.`
|
|
||||||
);
|
|
||||||
|
|
||||||
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";
|
moduleMetaData.scriptVersion = "1";
|
||||||
this._os.codeCache(fileName, sourceCode, outputCode, sourceMap);
|
this._os.codeCache(
|
||||||
|
fileName,
|
||||||
|
sourceCode,
|
||||||
|
moduleMetaData.outputCode,
|
||||||
|
moduleMetaData.sourceMap
|
||||||
|
);
|
||||||
return moduleMetaData.outputCode;
|
return moduleMetaData.outputCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import * as ts from "typescript";
|
||||||
// We use a silly amount of `any` in these tests...
|
// We use a silly amount of `any` in these tests...
|
||||||
// tslint:disable:no-any
|
// tslint:disable:no-any
|
||||||
|
|
||||||
const { DenoCompiler } = (deno as any)._compiler;
|
const { DenoCompiler, jsonAmdTemplate } = (deno as any)._compiler;
|
||||||
|
|
||||||
interface ModuleInfo {
|
interface ModuleInfo {
|
||||||
moduleName: string | undefined;
|
moduleName: string | undefined;
|
||||||
|
@ -118,6 +118,11 @@ const fooBazTsOutput = `define(["require", "exports", "./bar.ts"], function (req
|
||||||
|
|
||||||
// This is not a valid map, just mock data
|
// 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 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
|
// tslint:enable:max-line-length
|
||||||
|
|
||||||
const moduleMap: {
|
const moduleMap: {
|
||||||
|
@ -148,6 +153,14 @@ const moduleMap: {
|
||||||
"console.log();",
|
"console.log();",
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
|
),
|
||||||
|
"loadConfig.ts": mockModuleInfo(
|
||||||
|
"/root/project/loadConfig.ts",
|
||||||
|
"/root/project/loadConfig.ts",
|
||||||
|
MediaType.TypeScript,
|
||||||
|
loadConfigSource,
|
||||||
|
null,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"/root/project/foo/baz.ts": {
|
"/root/project/foo/baz.ts": {
|
||||||
|
@ -166,6 +179,16 @@ const moduleMap: {
|
||||||
"/root/project/modB.ts": {
|
"/root/project/modB.ts": {
|
||||||
"./modA.ts": modAModuleInfo
|
"./modA.ts": modAModuleInfo
|
||||||
},
|
},
|
||||||
|
"/root/project/loadConfig.ts": {
|
||||||
|
"./config.json": mockModuleInfo(
|
||||||
|
"/root/project/config.json",
|
||||||
|
"/root/project/config.json",
|
||||||
|
MediaType.Json,
|
||||||
|
configJsonSource,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
},
|
||||||
"/moduleKinds": {
|
"/moduleKinds": {
|
||||||
"foo.ts": mockModuleInfo(
|
"foo.ts": mockModuleInfo(
|
||||||
"foo",
|
"foo",
|
||||||
|
@ -280,7 +303,7 @@ const osMock = {
|
||||||
return mockModuleInfo(null, null, null, null, null, null);
|
return mockModuleInfo(null, null, null, null, null, null);
|
||||||
},
|
},
|
||||||
exit(code: number): never {
|
exit(code: number): never {
|
||||||
throw new Error(`os.exit(${code})`);
|
throw new Error(`Unexpected call to os.exit(${code})`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const tsMock = {
|
const tsMock = {
|
||||||
|
@ -289,9 +312,9 @@ const tsMock = {
|
||||||
},
|
},
|
||||||
formatDiagnosticsWithColorAndContext(
|
formatDiagnosticsWithColorAndContext(
|
||||||
diagnostics: ReadonlyArray<ts.Diagnostic>,
|
diagnostics: ReadonlyArray<ts.Diagnostic>,
|
||||||
host: ts.FormatDiagnosticsHost
|
_host: ts.FormatDiagnosticsHost
|
||||||
): string {
|
): string {
|
||||||
return "";
|
return JSON.stringify(diagnostics.map(({ messageText }) => messageText));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -374,6 +397,23 @@ function teardown() {
|
||||||
Object.assign(compilerInstance, originals);
|
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() {
|
test(function compilerInstance() {
|
||||||
assert(DenoCompiler != null);
|
assert(DenoCompiler != null);
|
||||||
assert(DenoCompiler.instance() != null);
|
assert(DenoCompiler.instance() != null);
|
||||||
|
@ -479,6 +519,29 @@ test(function compilerRunCircularDependency() {
|
||||||
teardown();
|
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() {
|
test(function compilerResolveModule() {
|
||||||
setup();
|
setup();
|
||||||
const moduleMetaData = compilerInstance.resolveModule(
|
const moduleMetaData = compilerInstance.resolveModule(
|
||||||
|
@ -544,6 +607,7 @@ test(function compilerGetCompilationSettings() {
|
||||||
"checkJs",
|
"checkJs",
|
||||||
"module",
|
"module",
|
||||||
"outDir",
|
"outDir",
|
||||||
|
"resolveJsonModule",
|
||||||
"sourceMap",
|
"sourceMap",
|
||||||
"stripComments",
|
"stripComments",
|
||||||
"target"
|
"target"
|
||||||
|
|
3
tests/020_json_modules.ts
Normal file
3
tests/020_json_modules.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import * as config from "./subdir/config.json";
|
||||||
|
|
||||||
|
console.log(JSON.stringify(config));
|
1
tests/020_json_modules.ts.out
Normal file
1
tests/020_json_modules.ts.out
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"foo":{"bar":true,"baz":["qat",1]}}
|
6
tests/subdir/config.json
Normal file
6
tests/subdir/config.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"foo": {
|
||||||
|
"bar": true,
|
||||||
|
"baz": ["qat", 1]
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
|
|
Loading…
Add table
Reference in a new issue