Skip to content

Commit

Permalink
perf(extract): splits transpiler availability from execution (#804)
Browse files Browse the repository at this point in the history
  • Loading branch information
sverweij authored Apr 30, 2023
1 parent ecd5563 commit 4fdaf68
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 142 deletions.
75 changes: 74 additions & 1 deletion src/extract/transpile/index.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,77 @@
import { getWrapper } from "./meta.mjs";
/* eslint-disable import/exports-last */
/* eslint security/detect-object-injection : 0*/
import javaScriptWrap from "./javascript-wrap.mjs";
import typeScriptWrap from "./typescript-wrap.mjs";
import liveScriptWrap from "./livescript-wrap.mjs";
import coffeeWrap from "./coffeescript-wrap.mjs";
import vueWrap from "./vue-template-wrap.cjs";
import babelWrap from "./babel-wrap.mjs";
import svelteDingus from "./svelte-wrap.mjs";

const typeScriptVanillaWrap = typeScriptWrap();
const typeScriptESMWrap = typeScriptWrap("esm");
const typeScriptTsxWrap = typeScriptWrap("tsx");
const coffeeVanillaWrap = coffeeWrap();
const litCoffeeWrap = coffeeWrap(true);
const svelteWrap = svelteDingus(typeScriptVanillaWrap);

export const EXTENSION2WRAPPER = {
".js": javaScriptWrap,
".cjs": javaScriptWrap,
".mjs": javaScriptWrap,
".jsx": javaScriptWrap,
".ts": typeScriptVanillaWrap,
".tsx": typeScriptTsxWrap,
".d.ts": typeScriptVanillaWrap,
".cts": typeScriptVanillaWrap,
".d.cts": typeScriptVanillaWrap,
".mts": typeScriptESMWrap,
".d.mts": typeScriptESMWrap,
".vue": vueWrap,
".svelte": svelteWrap,
".ls": liveScriptWrap,
".coffee": coffeeVanillaWrap,
".litcoffee": litCoffeeWrap,
".coffee.md": litCoffeeWrap,
".csx": coffeeVanillaWrap,
".cjsx": coffeeVanillaWrap,
};

const BABELEABLE_EXTENSIONS = [
".js",
".cjs",
".mjs",
".jsx",
".ts",
".tsx",
".d.ts",
];

/**
* returns the babel wrapper if there's a babelConfig in the transpiler
* options for babeleable extensions (javascript and typescript - currently
* not configurable)
*
* returns the wrapper module configured for the extension pExtension if
* not.
*
* returns the javascript wrapper if there's no wrapper module configured
* for the extension.
*
* @param {string} pExtension the extension (e.g. ".ts", ".js", ".litcoffee")
* @param {any} pTranspilerOptions
* @returns {module} the module
*/
export function getWrapper(pExtension, pTranspilerOptions) {
if (
Object.keys(pTranspilerOptions?.babelConfig ?? {}).length > 0 &&
BABELEABLE_EXTENSIONS.includes(pExtension)
) {
return babelWrap;
}

return EXTENSION2WRAPPER[pExtension] || javaScriptWrap;
}

/**
* Transpiles the string pFile with the transpiler configured for extension
Expand Down
137 changes: 52 additions & 85 deletions src/extract/transpile/meta.mjs
Original file line number Diff line number Diff line change
@@ -1,66 +1,58 @@
/* eslint-disable import/exports-last */
/* eslint security/detect-object-injection : 0*/
import meta from "../../meta.js";
import swc from "../parse/to-swc-ast.mjs";
import javaScriptWrap from "./javascript-wrap.mjs";
import typeScriptWrap from "./typescript-wrap.mjs";
import liveScriptWrap from "./livescript-wrap.mjs";
import coffeeWrap from "./coffeescript-wrap.mjs";
import vueWrap from "./vue-template-wrap.cjs";
import babelWrap from "./babel-wrap.mjs";
import svelteDingus from "./svelte-wrap.mjs";
import tryAvailable from "./try-import-available.mjs";

const typeScriptVanillaWrap = typeScriptWrap();
const typeScriptESMWrap = typeScriptWrap("esm");
const typeScriptTsxWrap = typeScriptWrap("tsx");
const coffeeVanillaWrap = coffeeWrap();
const litCoffeeWrap = coffeeWrap(true);
const svelteWrap = svelteDingus(typeScriptVanillaWrap);
function gotCoffee() {
return (
tryAvailable("coffeescript", meta.supportedTranspilers.coffeescript) ||
tryAvailable("coffee-script", meta.supportedTranspilers["coffee-script"])
);
}

const EXTENSION2WRAPPER = {
".js": javaScriptWrap,
".cjs": javaScriptWrap,
".mjs": javaScriptWrap,
".jsx": javaScriptWrap,
".ts": typeScriptVanillaWrap,
".tsx": typeScriptTsxWrap,
".d.ts": typeScriptVanillaWrap,
".cts": typeScriptVanillaWrap,
".d.cts": typeScriptVanillaWrap,
".mts": typeScriptESMWrap,
".d.mts": typeScriptESMWrap,
".vue": vueWrap,
".svelte": svelteWrap,
".ls": liveScriptWrap,
".coffee": coffeeVanillaWrap,
".litcoffee": litCoffeeWrap,
".coffee.md": litCoffeeWrap,
".csx": coffeeVanillaWrap,
".cjsx": coffeeVanillaWrap,
const TRANSPILER2AVAILABLE = {
babel: tryAvailable("@babel/core", meta.supportedTranspilers.babel),
javascript: true,
"coffee-script": gotCoffee(),
coffeescript: gotCoffee(),
livescript: tryAvailable("livescript", meta.supportedTranspilers.livescript),
svelte: tryAvailable("svelte/compiler", meta.supportedTranspilers.svelte),
swc: tryAvailable("@swc/core", meta.supportedTranspilers.swc),
typescript: tryAvailable("typescript", meta.supportedTranspilers.typescript),
"vue-template-compiler": tryAvailable(
"vue-template-compiler",
meta.supportedTranspilers["vue-template-compiler"]
),
"@vue/compiler-sfc": tryAvailable(
"@vue/compiler-sfc",
meta.supportedTranspilers["@vue/compiler-sfc"]
),
};

const TRANSPILER2WRAPPER = {
babel: babelWrap,
javascript: javaScriptWrap,
"coffee-script": coffeeVanillaWrap,
coffeescript: coffeeVanillaWrap,
livescript: liveScriptWrap,
svelte: svelteWrap,
swc,
typescript: typeScriptVanillaWrap,
"vue-template-compiler": vueWrap,
"@vue/compiler-sfc": vueWrap,
export const EXTENSION2AVAILABLE = {
".js": TRANSPILER2AVAILABLE.javascript,
".cjs": TRANSPILER2AVAILABLE.javascript,
".mjs": TRANSPILER2AVAILABLE.javascript,
".jsx": TRANSPILER2AVAILABLE.javascript,
".ts": TRANSPILER2AVAILABLE.typescript,
".tsx": TRANSPILER2AVAILABLE.typescript,
".d.ts": TRANSPILER2AVAILABLE.typescript,
".cts": TRANSPILER2AVAILABLE.typescript,
".d.cts": TRANSPILER2AVAILABLE.typescript,
".mts": TRANSPILER2AVAILABLE.typescript,
".d.mts": TRANSPILER2AVAILABLE.typescript,
".vue":
TRANSPILER2AVAILABLE["vue-template-compiler"] ||
TRANSPILER2AVAILABLE["@vue/compiler-sfc"],
".svelte": TRANSPILER2AVAILABLE.svelte,
".ls": TRANSPILER2AVAILABLE.livescript,
".coffee": gotCoffee(),
".litcoffee": gotCoffee(),
".coffee.md": gotCoffee(),
".csx": gotCoffee(),
".cjsx": gotCoffee(),
};

const BABELEABLE_EXTENSIONS = [
".js",
".cjs",
".mjs",
".jsx",
".ts",
".tsx",
".d.ts",
];

const EXTENSIONS_PER_PARSER = {
swc: [".js", ".cjs", ".mjs", ".jsx", ".ts", ".tsx", ".d.ts"],
// tsc: [".js", ".cjs", ".mjs", ".jsx", ".ts", ".tsx", ".d.ts"],
Expand All @@ -69,44 +61,19 @@ const EXTENSIONS_PER_PARSER = {

function extensionIsAvailable(pExtension) {
return (
EXTENSION2WRAPPER[pExtension].isAvailable() ||
EXTENSION2AVAILABLE[pExtension] ||
// should eventually also check whether swc is enabled as a parser?
(swc.isAvailable() && EXTENSIONS_PER_PARSER.swc.includes(pExtension))
(TRANSPILER2AVAILABLE.swc && EXTENSIONS_PER_PARSER.swc.includes(pExtension))
);
}

/**
* returns the babel wrapper if there's a babelConfig in the transpiler
* options for babeleable extensions (javascript and typescript - currently
* not configurable)
*
* returns the wrapper module configured for the extension pExtension if
* not.
*
* returns the javascript wrapper if there's no wrapper module configured
* for the extension.
*
* @param {string} pExtension the extension (e.g. ".ts", ".js", ".litcoffee")
* @param {any} pTranspilerOptions
* @returns {module} the module
*/
export function getWrapper(pExtension, pTranspilerOptions) {
if (
Object.keys(pTranspilerOptions?.babelConfig ?? {}).length > 0 &&
BABELEABLE_EXTENSIONS.includes(pExtension)
) {
return babelWrap;
}
return EXTENSION2WRAPPER[pExtension] || javaScriptWrap;
}

/**
* all supported extensions and whether or not it is supported
* in the current environment
*
* @type {IAvailableExtension[]}
*/
export const allExtensions = Object.keys(EXTENSION2WRAPPER).map(
export const allExtensions = Object.keys(EXTENSION2AVAILABLE).map(
(pExtension) => ({
extension: pExtension,
available: extensionIsAvailable(pExtension),
Expand All @@ -120,7 +87,7 @@ export const allExtensions = Object.keys(EXTENSION2WRAPPER).map(
* @type {string[]}
*/
export const scannableExtensions =
Object.keys(EXTENSION2WRAPPER).filter(extensionIsAvailable);
Object.keys(EXTENSION2AVAILABLE).filter(extensionIsAvailable);

/**
* returns an array of supported transpilers, with for each transpiler:
Expand All @@ -133,6 +100,6 @@ export function getAvailableTranspilers() {
return Object.keys(meta.supportedTranspilers).map((pTranspiler) => ({
name: pTranspiler,
version: meta.supportedTranspilers[pTranspiler],
available: TRANSPILER2WRAPPER[pTranspiler].isAvailable(),
available: TRANSPILER2AVAILABLE[pTranspiler],
}));
}
46 changes: 46 additions & 0 deletions src/extract/transpile/try-import-available.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import path from "node:path/posix";
import { createRequire } from "node:module";
import { coerce, satisfies } from "semver";

const require = createRequire(import.meta.url);

const PACKAGE_RE = "[^/]+";
const SCOPED_PACKAGE_RE = "@[^/]+(/[^/]+)";
const ROOT_MODULE_RE = new RegExp(`^(${SCOPED_PACKAGE_RE}|${PACKAGE_RE})`, "g");

function extractRootModuleName(pModuleName) {
return (pModuleName.match(ROOT_MODULE_RE) || []).shift();
}

function getVersion(pModuleName) {
// of course we'd love to use something like an import with an import assertion
// (yo, you're import-ing 'json'!), but that's _experimental_, printing scary
// messages to stderr so: ¯\_(ツ)_/¯
// eslint-disable-next-line import/no-dynamic-require, security/detect-non-literal-require
const lManifest = require(path.join(
extractRootModuleName(pModuleName),
"package.json"
));
return lManifest.version;
}

export default function tryImportAvailable(pModuleName, pSemanticVersion) {
try {
if (pSemanticVersion) {
const lVersion = getVersion(pModuleName);
const lCoerced = coerce(lVersion);
if (
lVersion &&
lCoerced &&
!satisfies(lCoerced.version, pSemanticVersion)
) {
return false;
}
}
// of course we'd love to use something like import.meta.resolve, but
// that's _experimental_, so ¯\_(ツ)_/¯
return Boolean(require.resolve(pModuleName));
} catch (pError) {
return false;
}
}
61 changes: 59 additions & 2 deletions test/extract/transpile/index.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect } from "chai";
import transpile from "../../../src/extract/transpile/index.mjs";
import transpile, {
getWrapper,
} from "../../../src/extract/transpile/index.mjs";
import normalizeSource from "../normalize-source.utl.mjs";

import jsWrap from "../../../src/extract/transpile/javascript-wrap.mjs";
import lsWrap from "../../../src/extract/transpile/livescript-wrap.mjs";
import babelWrap from "../../../src/extract/transpile/babel-wrap.mjs";
import vueTemplateWrap from "../../../src/extract/transpile/vue-template-wrap.cjs";

const __dirname = fileURLToPath(new URL(".", import.meta.url));

describe("[I] transpiler", () => {
describe("[I] transpile", () => {
it("As the 'livescript' transpiler is not available, returns the original source", () => {
expect(
transpile({ extension: ".ls", source: "whatever the bever" })
Expand Down Expand Up @@ -87,3 +94,53 @@ describe("[I] transpiler", () => {
).to.equal(lTranspiledFixture);
});
});

describe("[I] transpile/wrapper", () => {
it("returns the 'js' wrapper for unknown extensions", () => {
expect(getWrapper("")).to.deep.equal(jsWrap);
});

it("returns the 'ls' wrapper for livescript", () => {
expect(getWrapper(".ls")).to.deep.equal(lsWrap);
});

it("returns the 'javascript' wrapper for javascript when the babel config is not passed", () => {
expect(getWrapper(".js", {})).to.deep.equal(jsWrap);
});

it("returns the 'javascript' wrapper for javascript when there's just a typscript config", () => {
expect(getWrapper(".js", { tsConfig: {} })).to.deep.equal(jsWrap);
});

it("returns the 'babel' wrapper for javascript when the babel config is empty", () => {
expect(getWrapper(".js", { babelConfig: {} })).to.deep.equal(jsWrap);
});

it("returns the 'babel' wrapper for javascript when the babel config is not empty", () => {
expect(
getWrapper(".js", { babelConfig: { babelrc: false } })
).to.deep.equal(babelWrap);
});

it("returns the 'babel' wrapper for typescript when the babel config is not empty", () => {
expect(
getWrapper(".ts", { babelConfig: { babelrc: false } })
).to.deep.equal(babelWrap);
});

it("returns the 'vue' wrapper for vue templates even when the babel config is not empty", () => {
expect(
getWrapper(".vue", { babelConfig: { babelrc: false } })
).to.deep.equal(vueTemplateWrap);
});

it("returns the 'svelte' wrapper for svelte even when the babel config is not empty", () => {
expect(
getWrapper(".svelte", {
babelConfig: { babelrc: false },
})
.transpile("")
.toString()
).to.contain("generated by Svelte");
});
});
Loading

0 comments on commit 4fdaf68

Please sign in to comment.