testeranto
Version:
the AI powered BDD test framework for typescript projects
579 lines (559 loc) • 17 kB
JavaScript
import { createRequire } from 'module';const require = createRequire(import.meta.url);
// src/build.ts
import ansiC from "ansi-colors";
import fs4 from "fs";
import path4 from "path";
import readline from "readline";
import esbuild from "esbuild";
// src/utils.ts
import path from "path";
var getRunnables = (tests, projectName, payload = {
nodeEntryPoints: {},
nodeEntryPointSidecars: {},
webEntryPoints: {},
webEntryPointSidecars: {},
pureEntryPoints: {},
pureEntryPointSidecars: {}
}) => {
return tests.reduce((pt, cv, cndx, cry) => {
if (cv[1] === "node") {
pt.nodeEntryPoints[cv[0]] = path.resolve(
`./testeranto/bundles/node/${projectName}/${cv[0].split(".").slice(0, -1).concat("mjs").join(".")}`
);
} else if (cv[1] === "web") {
pt.webEntryPoints[cv[0]] = path.resolve(
`./testeranto/bundles/web/${projectName}/${cv[0].split(".").slice(0, -1).concat("mjs").join(".")}`
);
} else if (cv[1] === "pure") {
pt.pureEntryPoints[cv[0]] = path.resolve(
`./testeranto/bundles/pure/${projectName}/${cv[0].split(".").slice(0, -1).concat("mjs").join(".")}`
);
}
cv[3].filter((t) => t[1] === "node").forEach((t) => {
pt.nodeEntryPointSidecars[`${t[0]}`] = path.resolve(
`./testeranto/bundles/node/${projectName}/${cv[0].split(".").slice(0, -1).concat("mjs").join(".")}`
);
});
cv[3].filter((t) => t[1] === "web").forEach((t) => {
pt.webEntryPointSidecars[`${t[0]}`] = path.resolve(
`./testeranto/bundles/web/${projectName}/${cv[0].split(".").slice(0, -1).concat("mjs").join(".")}`
);
});
cv[3].filter((t) => t[1] === "pure").forEach((t) => {
pt.pureEntryPointSidecars[`${t[0]}`] = path.resolve(
`./testeranto/bundles/pure/${projectName}/${cv[0].split(".").slice(0, -1).concat("mjs").join(".")}`
);
});
return pt;
}, payload);
};
// src/utils/buildTemplates.ts
var getBaseHtml = (title) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="description" content="Webpage description goes here" />
<meta charset="utf-8" />
<title>${title} - testeranto</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="" />
<script>
function initApp() {
if (window.React && window.ReactDOM && window.App) {
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
} else {
setTimeout(initApp, 100);
}
}
window.addEventListener('DOMContentLoaded', initApp);
</script>
`;
var AppHtml = () => `
${getBaseHtml("Testeranto")}
<link rel="stylesheet" href="App.css" />
<script src="App.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
`;
// src/esbuildConfigs/index.ts
var esbuildConfigs_default = (config) => {
return {
// packages: "external",
target: "esnext",
format: "esm",
splitting: true,
outExtension: { ".js": ".mjs" },
outbase: ".",
jsx: "transform",
bundle: true,
minify: config.minify === true,
write: true,
loader: {
".js": "jsx",
".png": "binary",
".jpg": "binary"
}
};
};
// src/esbuildConfigs/inputFilesPlugin.ts
import fs from "fs";
var otherInputs = {};
var register = (entrypoint, sources) => {
if (!otherInputs[entrypoint]) {
otherInputs[entrypoint] = /* @__PURE__ */ new Set();
}
sources.forEach((s) => otherInputs[entrypoint].add(s));
};
var inputFilesPlugin_default = (platform, testName2) => {
const f = `testeranto/metafiles/${platform}/${testName2}.json`;
if (!fs.existsSync(`testeranto/metafiles/${platform}`)) {
fs.mkdirSync(`testeranto/metafiles/${platform}`, { recursive: true });
}
return {
register,
inputFilesPluginFactory: {
name: "metafileWriter",
setup(build) {
build.onEnd((result) => {
fs.writeFileSync(f, JSON.stringify(result, null, 2));
});
}
}
};
};
// src/esbuildConfigs/featuresPlugin.ts
import path2 from "path";
var featuresPlugin_default = {
name: "feature-markdown",
setup(build) {
build.onResolve({ filter: /\.md$/ }, (args) => {
if (args.resolveDir === "")
return;
return {
path: path2.isAbsolute(args.path) ? args.path : path2.join(args.resolveDir, args.path),
namespace: "feature-markdown"
};
});
build.onLoad(
{ filter: /.*/, namespace: "feature-markdown" },
async (args) => {
return {
contents: `file://${args.path}`,
loader: "text"
// contents: JSON.stringify({ path: args.path }),
// loader: "json",
// contents: JSON.stringify({
// // html: markdownHTML,
// raw: markdownContent,
// filename: args.path, //path.basename(args.path),
// }),
// loader: "json",
};
}
);
}
};
// src/esbuildConfigs/rebuildPlugin.ts
import fs2 from "fs";
var rebuildPlugin_default = (r) => {
return {
name: "rebuild-notify",
setup: (build) => {
build.onEnd((result) => {
console.log(`${r} > build ended with ${result.errors.length} errors`);
if (result.errors.length > 0) {
fs2.writeFileSync(
`./testeranto/reports${r}_build_errors`,
JSON.stringify(result, null, 2)
);
}
});
}
};
};
// src/esbuildConfigs/node.ts
var node_default = (config, entryPoints, testName2) => {
const { inputFilesPluginFactory, register: register2 } = inputFilesPlugin_default(
"node",
testName2
);
return {
...esbuildConfigs_default(config),
splitting: true,
outdir: `testeranto/bundles/node/${testName2}/`,
inject: [`./node_modules/testeranto/dist/cjs-shim.js`],
metafile: true,
supported: {
"dynamic-import": true
},
define: {
"process.env.FLUENTFFMPEG_COV": "0"
},
absWorkingDir: process.cwd(),
banner: {
js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);`
},
platform: "node",
external: ["react", ...config.externals],
entryPoints: [...entryPoints],
plugins: [
featuresPlugin_default,
inputFilesPluginFactory,
rebuildPlugin_default("node"),
...config.nodePlugins.map((p) => p(register2, entryPoints)) || []
]
};
};
// src/esbuildConfigs/web.ts
import { polyfillNode } from "esbuild-plugin-polyfill-node";
import path3 from "path";
var web_default = (config, entryPoints, testName2) => {
const { inputFilesPluginFactory, register: register2 } = inputFilesPlugin_default(
"web",
testName2
);
return {
...esbuildConfigs_default(config),
treeShaking: true,
outdir: `testeranto/bundles/web/${testName2}`,
alias: {
react: path3.resolve("./node_modules/react")
},
metafile: true,
external: [
"path",
"fs",
"stream",
"http",
"constants",
"net",
"assert",
"tls",
"os",
"child_process",
"readline",
"zlib",
"crypto",
"https",
"util",
"process",
"dns"
],
platform: "browser",
entryPoints: [...entryPoints],
loader: config.webLoaders,
plugins: [
featuresPlugin_default,
inputFilesPluginFactory,
polyfillNode({
// You might need to configure specific Node.js modules you want to polyfill
// Example:
// modules: {
// 'util': true,
// 'fs': false,
// }
}),
rebuildPlugin_default("web"),
...(config.webPlugins || []).map((p) => p(register2, entryPoints)) || []
]
};
};
// src/esbuildConfigs/pure.ts
import { isBuiltin } from "node:module";
// src/esbuildConfigs/consoleDetectorPlugin.ts
import fs3 from "fs";
var consoleDetectorPlugin = {
name: "console-detector",
setup(build) {
build.onLoad({ filter: /\.(js|ts)$/ }, async (args) => {
const contents = await fs3.promises.readFile(args.path, "utf8");
const consolePattern = /console\.(log|error|warn|info|debug|trace|dir|dirxml|table|group|groupEnd|clear|count|countReset|assert|profile|profileEnd|time|timeLog|timeEnd|timeStamp|context|memory)/g;
const matches = contents.match(consolePattern);
if (matches) {
const uniqueMethods = [...new Set(matches)];
return {
warnings: uniqueMethods.map((method) => ({
text: `call of "${method}" was detected, which is not supported in the pure runtime.`
// location: {
// file: args.path,
// line:
// contents
// .split("\n")
// .findIndex((line) => line.includes(method)) + 1,
// column: 0,
// },
}))
};
}
return null;
});
build.onEnd((buildResult) => {
if (buildResult.warnings.find((br) => br.pluginName === "console-detector"))
console.warn(
`Warning: An unsupported method call was detected in a source file used to build for the pure runtime. It is possible that this method call is in a comment block. If you really want to use this function, change this test to the "node" runtime.`
);
});
}
};
// src/esbuildConfigs/pure.ts
var pure_default = (config, entryPoints, testName2) => {
const { inputFilesPluginFactory, register: register2 } = inputFilesPlugin_default(
"pure",
testName2
);
return {
...esbuildConfigs_default(config),
drop: [],
splitting: true,
outdir: `testeranto/bundles/pure/${testName2}/`,
// inject: [`./node_modules/testeranto/dist/cjs-shim.js`],
metafile: true,
supported: {
"dynamic-import": true
},
define: {
"process.env.FLUENTFFMPEG_COV": "0"
},
absWorkingDir: process.cwd(),
banner: {
js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);`
},
platform: "node",
external: ["react", ...config.externals],
entryPoints: [...entryPoints],
plugins: [
featuresPlugin_default,
inputFilesPluginFactory,
consoleDetectorPlugin,
// nativeImportDetectorPlugin,
{
name: "native-node-import-filter",
setup(build) {
build.onResolve({ filter: /fs/ }, (args) => {
if (isBuiltin(args.path)) {
throw new Error(
`You attempted to import a node module "${args.path}" into a "pure" test, which is not allowed. If you really want to use this package, convert this test from "pure" to "node"`
);
}
return { path: args.path };
});
}
},
rebuildPlugin_default("pure"),
...(config.nodePlugins || []).map((p) => p(register2, entryPoints)) || []
]
};
};
// src/web.html.ts
var web_html_default = (jsfilePath, htmlFilePath, cssfilePath) => `
<!DOCTYPE html>
<html lang="en">
<head>
<script type="module" src="${jsfilePath}"></script>
<link rel="stylesheet" href="${cssfilePath}">
</head>
<body>
<div id="root">
</div>
</body>
</html>
`;
// src/build.ts
readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY)
process.stdin.setRawMode(true);
var testName = process.argv[2];
var mode = process.argv[3];
if (mode !== "once" && mode !== "dev") {
console.error(`The 4th argument should be 'dev' or 'once', not '${mode}'.`);
process.exit(-1);
}
import(process.cwd() + "/testeranto.config.ts").then(async (module) => {
const pckge = (await import(`${process.cwd()}/package.json`)).default;
const bigConfig = module.default;
const project = bigConfig.projects[testName];
if (!project) {
console.error("no project found for", testName, "in testeranto.config.ts");
process.exit(-1);
}
fs4.writeFileSync(
`${process.cwd()}/testeranto/projects.json`,
JSON.stringify(Object.keys(bigConfig.projects), null, 2)
);
const rawConfig = bigConfig.projects[testName];
const getSecondaryEndpointsPoints = (runtime) => {
const meta = (ts, st) => {
ts.forEach((t) => {
if (t[1] === runtime) {
st.add(t[0]);
}
if (Array.isArray(t[3])) {
meta(t[3], st);
}
});
return st;
};
return Array.from(meta(config.tests, /* @__PURE__ */ new Set()));
};
const config = {
...rawConfig,
buildDir: process.cwd() + "/testeranto/bundles/" + testName
};
console.log(
`Press 'q' to shutdown gracefully. Press 'x' to shutdown forcefully.`
);
process.stdin.on("keypress", (str, key) => {
if (key.name === "q") {
console.log("Testeranto-Build is shutting down...");
mode = "once";
onDone();
} else if (key.name === "x") {
console.log("Testeranto-Build is shutting down forcefully...");
process.exit(-1);
} else {
console.log(
`Press 'q' to shutdown gracefully. Press 'x' to shutdown forcefully.`
);
}
});
let nodeDone = false;
let webDone = false;
let importDone = false;
let status = "build";
const {
nodeEntryPoints,
nodeEntryPointSidecars,
webEntryPoints,
webEntryPointSidecars,
pureEntryPoints,
pureEntryPointSidecars
} = getRunnables(config.tests, testName);
const onNodeDone = () => {
nodeDone = true;
onDone();
};
const onWebDone = () => {
webDone = true;
onDone();
};
const onImportDone = () => {
importDone = true;
onDone();
};
const onDone = async () => {
if (nodeDone && webDone && importDone) {
status = "built";
}
if (nodeDone && webDone && importDone && mode === "once") {
console.log(
ansiC.inverse(
`${testName} was built and the builder exited successfully.`
)
);
process.exit();
}
};
fs4.writeFileSync(`${process.cwd()}/testeranto/projects.html`, AppHtml());
Object.keys(bigConfig.projects).forEach((projectName) => {
console.log(`testeranto/reports/${projectName}`);
if (!fs4.existsSync(`testeranto/reports/${projectName}`)) {
fs4.mkdirSync(`testeranto/reports/${projectName}`);
}
fs4.writeFileSync(
`testeranto/reports/${projectName}/config.json`,
JSON.stringify(config, null, 2)
);
});
Promise.resolve(
Promise.all(
[...getSecondaryEndpointsPoints("web")].map(async (sourceFilePath) => {
const sourceFileSplit = sourceFilePath.split("/");
const sourceDir = sourceFileSplit.slice(0, -1);
const sourceFileName = sourceFileSplit[sourceFileSplit.length - 1];
const sourceFileNameMinusJs = sourceFileName.split(".").slice(0, -1).join(".");
const htmlFilePath = path4.normalize(
`${process.cwd()}/testeranto/bundles/web/${testName}/${sourceDir.join(
"/"
)}/${sourceFileNameMinusJs}.html`
);
const jsfilePath = `./${sourceFileNameMinusJs}.mjs`;
const cssFilePath = `./${sourceFileNameMinusJs}.css`;
return fs4.promises.mkdir(path4.dirname(htmlFilePath), { recursive: true }).then(
(x2) => fs4.writeFileSync(
htmlFilePath,
web_html_default(jsfilePath, htmlFilePath, cssFilePath)
)
);
})
)
);
const x = [
["pure", Object.keys(pureEntryPoints)],
["node", Object.keys(nodeEntryPoints)],
["web", Object.keys(webEntryPoints)]
];
x.forEach(async ([runtime, keys]) => {
keys.forEach(async (k) => {
const folder = `testeranto/reports/${testName}/${k.split(".").slice(0, -1).join(".")}/${runtime}`;
await fs4.mkdirSync(folder, { recursive: true });
});
});
[
[pureEntryPoints, pureEntryPointSidecars, "pure"],
[webEntryPoints, webEntryPointSidecars, "web"],
[nodeEntryPoints, nodeEntryPointSidecars, "node"]
].forEach(
([eps, eps2, runtime]) => {
[...Object.keys(eps), ...Object.keys(eps2)].forEach((ep) => {
const fp = path4.resolve(
`testeranto`,
`reports`,
testName,
ep.split(".").slice(0, -1).join("."),
runtime
);
fs4.mkdirSync(fp, { recursive: true });
});
}
);
await Promise.all([
...[
[
pure_default,
pureEntryPoints,
pureEntryPointSidecars,
onImportDone
],
[
node_default,
nodeEntryPoints,
nodeEntryPointSidecars,
onNodeDone
],
[web_default, webEntryPoints, webEntryPointSidecars, onWebDone]
].map(([configer, entryPoints, sidecars, done]) => {
esbuild.context(
configer(
config,
[...Object.keys(entryPoints), ...Object.keys(sidecars)],
testName
)
).then(async (ctx) => {
if (mode === "dev") {
await ctx.watch().then((v) => {
done();
});
} else {
ctx.rebuild().then((v) => {
done();
});
}
return ctx;
});
})
]);
});