js-slang
Version:
Javascript-based implementations of Source, written in Typescript
134 lines (132 loc) • 5.75 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadModuleTabsAsync = exports.loadModuleBundleAsync = exports.memoizedGetModuleDocsAsync = exports.memoizedGetModuleManifestAsync = exports.docsImporter = exports.setModulesStaticURL = exports.MODULES_STATIC_URL = void 0;
const misc_1 = require("../../utils/misc");
const errors_1 = require("../errors");
const requireProvider_1 = require("./requireProvider");
/** Default modules static url. Exported for testing. */
exports.MODULES_STATIC_URL = 'https://source-academy.github.io/modules';
function setModulesStaticURL(url) {
exports.MODULES_STATIC_URL = url;
// Changing the module backend should clear these
exports.memoizedGetModuleDocsAsync.cache.clear();
exports.memoizedGetModuleManifestAsync.reset();
}
exports.setModulesStaticURL = setModulesStaticURL;
function wrapImporter(func) {
/*
Browsers natively support esm's import() but Jest and Node do not. So we need
to change which import function we use based on the environment.
For the browser, we use the function constructor to hide the import calls from
webpack so that webpack doesn't try to compile them away.
Browsers automatically cache import() calls, so we add a query parameter with the
current time to always invalidate the cache and handle the memoization ourselves
*/
return async (p) => {
try {
const result = await (0, misc_1.timeoutPromise)(func(p), 10000);
return result;
}
catch (error) {
// Before calling this function, the import analyzer should've been used to make sure
// that the module being imported already exists, so the following errors should
// be thrown only if the modules server is unreachable
if (
// In the browser, import statements should throw TypeError
(typeof window !== 'undefined' && error instanceof TypeError) ||
// In Node a different error is thrown with the given code instead
error.code === 'MODULE_NOT_FOUND' ||
// Thrown specifically by jest
error.code === 'ENOENT') {
throw new errors_1.ModuleConnectionError();
}
throw error;
}
};
}
// Exported for testing
exports.docsImporter = wrapImporter(async (p) => {
// TODO: USe import attributes when they become supported
// Import Assertions and Attributes are not widely supported by all
// browsers yet, so we use fetch in the meantime
const resp = await fetch(p);
if (resp.status !== 200 && resp.status !== 304) {
throw new errors_1.ModuleConnectionError();
}
const result = await resp.json();
return { default: result };
});
// lodash's memoize function memoizes on errors. This is undesirable,
// so we have our own custom memoization that won't memoize on errors
function getManifestImporter() {
let manifest = null;
async function func() {
if (manifest !== null) {
return manifest;
}
;
({ default: manifest } = await (0, exports.docsImporter)(`${exports.MODULES_STATIC_URL}/modules.json`));
return manifest;
}
func.reset = () => {
manifest = null;
};
return func;
}
function getMemoizedDocsImporter() {
const docs = new Map();
async function func(moduleName, throwOnError) {
if (docs.has(moduleName)) {
return docs.get(moduleName);
}
try {
const { default: loadedDocs } = await (0, exports.docsImporter)(`${exports.MODULES_STATIC_URL}/jsons/${moduleName}.json`);
docs.set(moduleName, loadedDocs);
return loadedDocs;
}
catch (error) {
if (throwOnError)
throw error;
console.warn(`Failed to load documentation for ${moduleName}:`, error);
return null;
}
}
func.cache = docs;
return func;
}
exports.memoizedGetModuleManifestAsync = getManifestImporter();
exports.memoizedGetModuleDocsAsync = getMemoizedDocsImporter();
const bundleAndTabImporter = wrapImporter(typeof window !== 'undefined' && process.env.NODE_ENV !== 'test'
? new Function('path', 'return import(`${path}?q=${Date.now()}`)')
: p => Promise.resolve(require(p)));
async function loadModuleBundleAsync(moduleName, context, node) {
const { default: result } = await bundleAndTabImporter(`${exports.MODULES_STATIC_URL}/bundles/${moduleName}.js`);
try {
const loadedModule = result((0, requireProvider_1.getRequireProvider)(context));
return Object.entries(loadedModule).reduce((res, [name, value]) => {
if (typeof value === 'function') {
const repr = `function ${name} {\n\t[Function from ${moduleName}\n\tImplementation hidden]\n}`;
value[Symbol.toStringTag] = () => repr;
value.toString = () => repr;
}
return {
...res,
[name]: value
};
}, {});
}
catch (error) {
throw new errors_1.ModuleInternalError(moduleName, error, node);
}
}
exports.loadModuleBundleAsync = loadModuleBundleAsync;
async function loadModuleTabsAsync(moduleName) {
const manifest = await (0, exports.memoizedGetModuleManifestAsync)();
const moduleInfo = manifest[moduleName];
return Promise.all(moduleInfo.tabs.map(async (tabName) => {
const { default: result } = await bundleAndTabImporter(`${exports.MODULES_STATIC_URL}/tabs/${tabName}.js`);
return result;
}));
}
exports.loadModuleTabsAsync = loadModuleTabsAsync;
//# sourceMappingURL=loaders.js.map
;