@marko/compiler
Version:
Marko template to JS compiler.
414 lines (361 loc) • 12.6 kB
JavaScript
;var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");exports.__esModule = true;exports.default = void 0;var _traverse = _interopRequireDefault(require("@babel/traverse"));
var _crypto = require("crypto");
var _path = _interopRequireDefault(require("path"));
var t = _interopRequireWildcard(require("../babel-types"));
var _diagnostics = require("../babel-utils/diagnostics");
var _getFile = require("../babel-utils/get-file");
var _tags = require("../babel-utils/tags");
var _taglib = require("../taglib");
var _config = _interopRequireDefault(require("../taglib/config"));
var _buildCodeFrame = require("../util/build-code-frame");
var _mergeErrors = _interopRequireDefault(require("../util/merge-errors"));
var _shouldOptimize = _interopRequireDefault(require("../util/should-optimize"));
var _tryLoadTranslator = _interopRequireDefault(require("../util/try-load-translator"));
var _file = require("./file");
var _parser = require("./parser");
var _migrate = require("./plugins/migrate");
var _transform = require("./plugins/transform");function _getRequireWildcardCache(e) {if ("function" != typeof WeakMap) return null;var r = new WeakMap(),t = new WeakMap();return (_getRequireWildcardCache = function (e) {return e ? t : r;})(e);}function _interopRequireWildcard(e, r) {if (!r && e && e.__esModule) return e;if (null === e || "object" != typeof e && "function" != typeof e) return { default: e };var t = _getRequireWildcardCache(r);if (t && t.has(e)) return t.get(e);var n = { __proto__: null },a = Object.defineProperty && Object.getOwnPropertyDescriptor;for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) {var i = a ? Object.getOwnPropertyDescriptor(e, u) : null;i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u];}return n.default = e, t && t.set(e, n), n;}
const SOURCE_FILES = new WeakMap();var _default =
(api, markoOpts) => {
api.assertVersion(7);
const translator = markoOpts.translator = (0, _tryLoadTranslator.default)(
markoOpts.translator
);
markoOpts.output = markoOpts.output || "html";
if (markoOpts.optimize === undefined) {
api.cache.using(_shouldOptimize.default);
markoOpts.optimize = (0, _shouldOptimize.default)();
}
if (!translator || !translator.translate) {
throw new Error(
"@marko/compiler: translator must provide a translate visitor object"
);
}
if (
markoOpts.output === "hydrate" &&
typeof markoOpts.resolveVirtualDependency !== "function")
{
throw new Error(
`@marko/compiler: the "resolveVirtualDependency" option must be supplied when output is "hydrate".`
);
}
let curOpts;
return {
name: "marko",
manipulateOptions(opts) {
// We need to allow these for now since we are not parsing the entire file
// These types of syntax errors will be picked up by the bundler / runtime anyways.
opts.parserOpts.allowAwaitOutsideFunction =
opts.parserOpts.allowImportExportEverywhere =
opts.parserOpts.allowReturnOutsideFunction =
opts.parserOpts.allowSuperOutsideMethod =
opts.parserOpts.allowUndeclaredExports =
opts.parserOpts.allowNewTargetOutsideFunction =
true;
curOpts = opts;
},
parserOverride(code) {
const file = getMarkoFile(code, curOpts, markoOpts);
const finalAst = t.cloneNode(file.ast, true);
SOURCE_FILES.set(finalAst, file);
return finalAst;
},
pre(file) {
const { buildError: prevBuildError } = file.hub;
const { buildCodeFrameError: prevCodeFrameError } = file;
const prevFS = _config.default.fs;
const prevFile = (0, _getFile.getFileInternal)();
_config.default.fs = markoOpts.fileSystem;
(0, _getFile.setFileInternal)(file);
curOpts = undefined;
try {
const { ast, metadata } = file;
const sourceFile = SOURCE_FILES.get(ast);
metadata.marko = shallowClone(sourceFile.metadata.marko);
if (isMarkoOutput(markoOpts.output)) {
finalizeMeta(metadata.marko);
return;
}
const taglibLookup = sourceFile.___taglibLookup;
const rootTranslators = [];
file.buildCodeFrameError = _file.MarkoFile.prototype.buildCodeFrameError;
file.hub.buildError = file.buildCodeFrameError.bind(file);
file.markoOpts = markoOpts;
file.___taglibLookup = taglibLookup;
file.___getMarkoFile = getMarkoFile;
if (markoOpts.output !== "hydrate") {
for (const id in taglibLookup.taglibsById) {
addPlugin(
metadata.marko,
rootTranslators,
taglibLookup.taglibsById[id].translator
);
}
}
rootTranslators.push(translator.translate);
file.___compileStage = "translate";
traverseAll(file, rootTranslators);
finalizeMeta(metadata.marko);
file.path.scope.crawl(); // Ensure all scopes are accurate for subsequent babel plugins
} finally {
_config.default.fs = prevFS;
(0, _getFile.setFileInternal)(prevFile);
file.buildCodeFrameError = prevCodeFrameError;
file.hub.buildError = prevBuildError;
file.markoOpts =
file.___taglibLookup =
file.___getMarkoFile =
undefined;
}
},
visitor:
markoOpts.stripTypes && isMarkoOutput(markoOpts.output) ?
{
MarkoClass(path) {
// We replace the MarkoClass with a regular class declaration so babel can strip it's types.
path.replaceWith(
t.classDeclaration(t.identifier(""), null, path.node.body)
);
},
ExportNamedDeclaration: {
exit(path) {
const { node } = path;
// The babel typescript plugin will add an empty export declaration
// if there are no other imports/exports in the file.
// This is not needed for Marko file outputs since there is always
// a default export.
if (!(node.declaration || node.specifiers.length))
path.remove();
}
}
} :
undefined
};
};exports.default = _default;
function getMarkoFile(code, fileOpts, markoOpts) {
const { translator } = markoOpts;
let compileCache = markoOpts.cache.get(translator);
if (!compileCache) {
markoOpts.cache.set(translator, compileCache = new Map());
}
const { filename } = fileOpts;
const isSource = markoOpts.output === "source";
const isMigrate = markoOpts.output === "migrate";
const canCache = !(isSource || isMigrate);
const id = (0, _tags.getTemplateId)(markoOpts, filename);
const contentHash = canCache && (0, _crypto.createHash)("MD5").update(code).digest("hex");
const cacheKey = canCache && (0, _crypto.createHash)("MD5").update(id).digest("hex");
let cached = canCache && compileCache.get(cacheKey);
if (cached) {
if (cached.contentHash !== contentHash) {
// File content changed, invalidate the cache.
cached = undefined;
} else {
for (const watchFile of cached.file.metadata.marko.watchFiles) {
let mtime = Infinity;
try {
mtime = markoOpts.fileSystem.statSync(watchFile).mtime;
} catch {
// ignore
}
if (mtime > cached.time) {
// Some dependency changed, invalidate the cache.
cached = undefined;
break;
}
}
}
}
if (cached) {
return cached.file;
}
const prevFs = _config.default.fs;
const prevFile = (0, _getFile.getFileInternal)();
_config.default.fs = markoOpts.fileSystem;
try {
const taglibLookup = (0, _taglib.buildLookup)(_path.default.dirname(filename), translator);
const file = (0, _getFile.setFileInternal)(
new _file.MarkoFile(fileOpts, {
code,
ast: {
type: "File",
program: {
type: "Program",
sourceType: "module",
body: [],
directives: [],
params: [t.identifier("input")]
}
}
})
);
const meta = file.metadata.marko = {
id,
deps: [],
tags: [],
watchFiles: [],
diagnostics: []
};
file.markoOpts = markoOpts;
file.___taglibLookup = taglibLookup;
file.___getMarkoFile = getMarkoFile;
file.___compileStage = "parse";
(0, _parser.parseMarko)(file);
if (isSource) {
return file;
}
file.path.scope.crawl(); // Initialize bindings.
const rootMigrators = [];
for (const id in taglibLookup.taglibsById) {
for (const migrator of taglibLookup.taglibsById[id].migrators) {
addPlugin(meta, rootMigrators, migrator);
}
}
rootMigrators.push(_migrate.visitor);
file.___compileStage = "migrate";
traverseAll(file, rootMigrators);
if (file.___hasParseErrors) {
if (markoOpts.errorRecovery) {
t.traverseFast(file.path.node, (node) => {
if (node.type === "MarkoParseError") {
(0, _diagnostics.diagnosticError)(file.path, {
label: node.label,
loc: node.errorLoc || node.loc
});
}
});
} else {
let errors = [];
t.traverseFast(file.path.node, (node) => {
if (node.type === "MarkoParseError") {
errors.push(
(0, _buildCodeFrame.buildCodeFrameError)(
file.opts.filename,
file.code,
node.errorLoc || node.loc,
node.label
)
);
}
});
(0, _mergeErrors.default)(errors);
}
}
if (isMigrate) {
return file;
}
const rootTransformers = [];
for (const id in taglibLookup.taglibsById) {
for (const transformer of taglibLookup.taglibsById[id].transformers) {
addPlugin(meta, rootTransformers, transformer);
}
}
rootTransformers.push(_transform.visitor);
if (translator.transform) {
rootTransformers.push(translator.transform);
}
file.___compileStage = "transform";
traverseAll(file, rootTransformers);
for (const taglibId in taglibLookup.taglibsById) {
const { filePath } = taglibLookup.taglibsById[taglibId];
if (
filePath[filePath.length - 5] === "." &&
filePath.endsWith("marko.json"))
{
meta.watchFiles.push(filePath);
}
}
compileCache.set(cacheKey, {
time: Date.now(),
file,
contentHash
});
if (translator.analyze) {
try {
file.___compileStage = "analyze";
traverseAll(file, translator.analyze);
} catch (e) {
compileCache.delete(cacheKey);
throw e;
}
}
return file;
} finally {
_config.default.fs = prevFs;
(0, _getFile.setFileInternal)(prevFile);
}
}
function shallowClone(data) {
const clone = {};
for (const key in data) {
const v = data[key];
if (v !== undefined) {
if (v === null || typeof v !== "object") {
clone[key] = v;
} else {
const Ctor = v.constructor;
switch (Ctor) {
case Array:
clone[key] = [...v];
break;
case Object:
case null:
clone[key] = { ...v };
break;
case Map:
case Set:
case Date:
case RegExp:
clone[key] = new Ctor(v);
break;
default:
throw new Error(`Unsupported metadata type of ${Ctor.name}`);
}
}
}
}
return clone;
}
function mergeVisitors(all) {
if (Array.isArray(all)) {
if (all.length === 1) {
all = all[0];
} else {
return _traverse.default.visitors.merge(all);
}
}
return _traverse.default.visitors.explode(all);
}
function traverseAll(file, visitors) {
const program = file.path;
(0, _traverse.default)(
program.node,
mergeVisitors(visitors),
program.scope,
program.state = {},
program,
true
);
}
function addPlugin(meta, arr, plugin) {
if (plugin) {
const hook = plugin.hook.default || plugin.hook;
if (plugin.path) {
meta.watchFiles.push(plugin.path);
}
if (Array.isArray(hook)) {
arr.push(...hook);
} else {
arr.push(hook);
}
}
}
function isMarkoOutput(output) {
return output === "source" || output === "migrate";
}
function finalizeMeta(meta) {
meta.watchFiles = [...new Set(meta.watchFiles)];
if (meta.analyzedTags) {
meta.analyzedTags = [...meta.analyzedTags];
}
}