assemblyscript
Version:
A TypeScript-like language for WebAssembly.
466 lines (434 loc) • 14.8 kB
JavaScript
import fs from "fs";
import path from "path";
import { createRequire } from "module";
import { fileURLToPath } from "url";
import { stdoutColors } from "../util/terminal.js";
import * as optionsUtil from "../util/options.js";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const version = require("../package.json").version; // TODO
const npmDefaultTest = "echo \"Error: no test specified\" && exit 1";
const commands = {
"npm": {
install: "npm install",
run: "npm run",
test: "npm test"
},
"yarn": {
install: "yarn install",
run: "yarn",
test: "yarn test"
},
"pnpm": {
install: "pnpm install",
run: "pnpm run",
test: "pnpm test"
}
};
let pm = "npm";
if (typeof process.env.npm_config_user_agent === "string") {
if (/\byarn\//.test(process.env.npm_config_user_agent)) {
pm = "yarn";
} else if (/\bpnpm\//.test(process.env.npm_config_user_agent)) {
pm = "pnpm";
}
}
const asinitOptions = {
"help": {
"category": "General",
"description": "Prints this help message.",
"type": "b",
"alias": "h"
},
"yes": {
"category": "General",
"description": [
"Answers all questions with their default option",
"for non-interactive usage."
],
"type": "b",
"alias": "y"
},
"noColors": {
"description": "Disables terminal colors.",
"type": "b",
"default": false
},
};
const cliOptions = optionsUtil.parse(process.argv.slice(2), asinitOptions);
if (cliOptions.options.noColors) {
stdoutColors.enabled = false;
}
if (cliOptions.options.help || cliOptions.arguments.length === 0) printHelp();
function printHelp() {
console.log([
"Sets up a new AssemblyScript project or updates an existing one.",
"",
stdoutColors.white("SYNTAX"),
" " + stdoutColors.cyan("asinit") + " directory [options]",
"",
stdoutColors.white("EXAMPLES"),
" " + stdoutColors.cyan("asinit") + " .",
" " + stdoutColors.cyan("asinit") + " ./newProject -y",
"",
stdoutColors.white("OPTIONS"),
optionsUtil.help(asinitOptions, { noCategories: true })
].join("\n"));
process.exit(0);
}
const compilerDir = path.join(dirname, "..");
const projectDir = path.resolve(cliOptions.arguments[0]);
const assemblyDir = path.join(projectDir, "assembly");
const tsconfigFile = path.join(assemblyDir, "tsconfig.json");
const asconfigFile = path.join(projectDir, "asconfig.json");
let tsconfigBase = path.relative(assemblyDir, path.join(compilerDir, "std", "assembly.json"));
if (/^(\.\.[/\\])*node_modules[/\\]assemblyscript[/\\]/.test(tsconfigBase)) {
// Use node resolution if the compiler is a normal dependency
tsconfigBase = "assemblyscript/std/assembly.json";
}
const entryFile = path.join(assemblyDir, "index.ts");
const buildDir = path.join(projectDir, "build");
const testsDir = path.join(projectDir, "tests");
const gitignoreFile = path.join(buildDir, ".gitignore");
const packageFile = path.join(projectDir, "package.json");
const indexHtmlFile = path.join(projectDir, "index.html");
const testsIndexFile = path.join(testsDir, "index.js");
const paths = [
[assemblyDir, "Directory holding the AssemblyScript sources being compiled to WebAssembly."],
[tsconfigFile, "TypeScript configuration inheriting recommended AssemblyScript settings."],
[entryFile, "Example entry file being compiled to WebAssembly to get you started."],
[buildDir, "Build artifact directory where compiled WebAssembly files are stored."],
[gitignoreFile, "Git configuration that excludes compiled binaries from source control."],
[asconfigFile, "Configuration file defining both a 'debug' and a 'release' target."],
[packageFile, "Package info containing the necessary commands to compile to WebAssembly."],
[testsIndexFile, "Starter test to check that the module is functioning."],
[indexHtmlFile, "Starter HTML file that loads the module in a browser."]
];
const formatPath = filePath => "./" + path.relative(projectDir, filePath).replace(/\\/g, "/");
if (fs.existsSync(packageFile)) {
const pkg = JSON.parse(fs.readFileSync(packageFile));
if ("type" in pkg && pkg["type"] !== "module") {
console.error(stdoutColors.red([
`Error: The "type" field in ${formatPath(packageFile)} is set to "${pkg["type"]}".`,
` asinit requires the "type" field to be set to "module" (ES modules).`
].join("\n")));
process.exit(1);
}
}
console.log([
"Version: " + version,
"",
stdoutColors.white([
"This command will make sure that the following files exist in the project",
"directory '" + projectDir + "':"
].join("\n")),
...paths.map(([filePath, description]) => "\n " + stdoutColors.cyan(formatPath(filePath)) + "\n " + description),
"",
"The command will try to update existing files to match the correct settings",
"for this instance of the compiler in '" + compilerDir + "'.",
""
].join("\n"));
function createProject(answer) {
if (!/^y?$/i.test(answer)) {
process.exit(1);
return;
}
console.log();
ensureProjectDirectory();
ensureAssemblyDirectory();
ensureTsconfigJson();
ensureEntryFile();
ensureBuildDirectory();
ensureGitignore();
ensurePackageJson();
ensureAsconfigJson();
ensureTestsDirectory();
ensureTestsIndexJs();
ensureIndexHtml();
console.log([
stdoutColors.green("Done!"),
"",
"Don't forget to install dependencies before you start:",
"",
stdoutColors.white(" " + commands[pm].install),
"",
"To edit the entry file, open '" + stdoutColors.cyan("assembly/index.ts") + "' in your editor of choice.",
"Create as many additional files as necessary and use them as imports.",
"",
"To build the entry file to WebAssembly when you are ready, run:",
"",
stdoutColors.white(" " + commands[pm].run + " asbuild"),
"",
"Running the command above creates the following binaries incl. their respective",
"text format representations and source maps:",
"",
stdoutColors.cyan(" ./build/debug.wasm"),
stdoutColors.cyan(" ./build/debug.wasm.map"),
stdoutColors.cyan(" ./build/debug.wat"),
"",
" ^ The debuggable WebAssembly module as generated by the compiler.",
" This one matches your sources exactly, without any optimizations.",
"",
stdoutColors.cyan(" ./build/release.wasm"),
stdoutColors.cyan(" ./build/release.wasm.map"),
stdoutColors.cyan(" ./build/release.wat"),
"",
" ^ The optimized WebAssembly module using default optimization settings.",
" You can change the optimization settings in '" + stdoutColors.cyan("package.json")+ "'.",
"",
"To run the tests, do:",
"",
stdoutColors.white(" " + commands[pm].test),
"",
"The AssemblyScript documentation covers all the details:",
"",
" https://www.assemblyscript.org",
"",
"Have a nice day!"
].join("\n"));
}
if (cliOptions.options.yes) {
createProject("y");
} else {
const rl = require("readline").createInterface({
input: process.stdin,
output: process.stdout
});
rl.question(stdoutColors.white("Do you want to proceed?") + " [Y/n] ", result => {
rl.close();
createProject(result);
});
}
function ensureProjectDirectory() {
console.log("- Making sure that the project directory exists...");
if (!fs.existsSync(projectDir)) {
fs.mkdirSync(projectDir);
console.log(stdoutColors.green(" Created: ") + projectDir);
} else {
console.log(stdoutColors.yellow(" Exists: ") + projectDir);
}
console.log();
}
function ensureAssemblyDirectory() {
console.log("- Making sure that the 'assembly' directory exists...");
if (!fs.existsSync(assemblyDir)) {
fs.mkdirSync(assemblyDir);
console.log(stdoutColors.green(" Created: ") + assemblyDir);
} else {
console.log(stdoutColors.yellow(" Exists: ") + assemblyDir);
}
console.log();
}
function ensureTsconfigJson() {
console.log("- Making sure that 'assembly/tsconfig.json' is set up...");
const base = tsconfigBase.replace(/\\/g, "/");
if (!fs.existsSync(tsconfigFile)) {
fs.writeFileSync(tsconfigFile, JSON.stringify({
"extends": base,
"include": [
"./**/*.ts"
]
}, null, 2));
console.log(stdoutColors.green(" Created: ") + tsconfigFile);
} else {
let tsconfig = JSON.parse(fs.readFileSync(tsconfigFile, "utf8"));
tsconfig["extends"] = base;
fs.writeFileSync(tsconfigFile, JSON.stringify(tsconfig, null, 2));
console.log(stdoutColors.green(" Updated: ") + tsconfigFile);
}
console.log();
}
function ensureAsconfigJson() {
console.log("- Making sure that 'asconfig.json' is set up...");
if (!fs.existsSync(asconfigFile)) {
fs.writeFileSync(asconfigFile, JSON.stringify({
targets: {
debug: {
// -o build/debug.wasm -t build/debug.wat --sourceMap --debug
outFile: "build/debug.wasm",
textFile: "build/debug.wat",
sourceMap: true,
debug: true
},
release: {
// -o build/release.wasm -t build/release.wat --sourceMap --optimize
outFile: "build/release.wasm",
textFile: "build/release.wat",
sourceMap: true,
optimizeLevel: 3,
shrinkLevel: 0,
converge: false,
noAssert: false
}
},
options: {
bindings: "esm"
}
}, null, 2));
console.log(stdoutColors.green(" Created: ") + asconfigFile);
} else {
console.log(stdoutColors.yellow(" Exists: ") + asconfigFile);
}
console.log();
}
function ensureEntryFile() {
console.log("- Making sure that 'assembly/index.ts' exists...");
if (!fs.existsSync(entryFile)) {
fs.writeFileSync(entryFile, [
"// The entry file of your WebAssembly module.",
"",
"export function add(a: i32, b: i32): i32 {",
" return a + b;",
"}"
].join("\n") + "\n");
console.log(stdoutColors.green(" Created: ") + entryFile);
} else {
console.log(stdoutColors.yellow(" Exists: ") + entryFile);
}
console.log();
}
function ensureBuildDirectory() {
console.log("- Making sure that the 'build' directory exists...");
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir);
console.log(stdoutColors.green(" Created: ") + buildDir);
} else {
console.log(stdoutColors.yellow(" Exists: ") + buildDir);
}
console.log();
}
function ensureGitignore() {
console.log("- Making sure that 'build/.gitignore' is set up...");
if (!fs.existsSync(gitignoreFile)) {
fs.writeFileSync(gitignoreFile, [
"*",
"!.gitignore"
].join("\n") + "\n");
console.log(stdoutColors.green(" Created: ") + gitignoreFile);
} else {
console.log(stdoutColors.yellow(" Exists: ") + gitignoreFile);
}
console.log();
}
function ensurePackageJson() {
console.log("- Making sure that 'package.json' contains the build commands...");
const entryPath = path.relative(projectDir, entryFile).replace(/\\/g, "/");
const buildDebug = "asc " + entryPath + " --target debug";
const buildRelease = "asc " + entryPath + " --target release";
const buildAll = commands[pm].run + " asbuild:debug && " + commands[pm].run + " asbuild:release";
if (!fs.existsSync(packageFile)) {
fs.writeFileSync(packageFile, JSON.stringify({
"type": "module",
"exports": {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
},
"scripts": {
"asbuild:debug": buildDebug,
"asbuild:release": buildRelease,
"asbuild": buildAll,
"test": "node tests",
"start": "npx serve ."
},
"devDependencies": {
"assemblyscript": "^" + version
}
}, null, 2));
console.log(stdoutColors.green(" Created: ") + packageFile);
} else {
let pkg = JSON.parse(fs.readFileSync(packageFile));
let scripts = pkg.scripts || {};
let updated = false;
if (!pkg["type"]) {
pkg["type"] = "module";
updated = true;
}
if (!pkg["exports"]) {
pkg["exports"] = {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
};
}
if (!scripts["asbuild"]) {
scripts["asbuild:debug"] = buildDebug;
scripts["asbuild:release"] = buildRelease;
scripts["asbuild"] = buildAll;
pkg["scripts"] = scripts;
updated = true;
}
if (!scripts["test"] || scripts["test"] == npmDefaultTest) {
scripts["test"] = "node tests";
pkg["scripts"] = scripts;
updated = true;
}
if (!scripts["start"]) {
scripts["start"] = "npx serve .",
pkg["scripts"] = scripts;
updated = true;
}
let devDependencies = pkg["devDependencies"] || {};
if (!devDependencies["assemblyscript"]) {
devDependencies["assemblyscript"] = "^" + version;
pkg["devDependencies"] = devDependencies;
updated = true;
}
if (updated) {
fs.writeFileSync(packageFile, JSON.stringify(pkg, null, 2));
console.log(stdoutColors.green(" Updated: ") + packageFile);
} else {
console.log(stdoutColors.yellow(" Exists: ") + packageFile);
}
}
console.log();
}
function ensureTestsDirectory() {
console.log("- Making sure that the 'tests' directory exists...");
if (!fs.existsSync(testsDir)) {
fs.mkdirSync(testsDir);
console.log(stdoutColors.green(" Created: ") + testsDir);
} else {
console.log(stdoutColors.yellow(" Exists: ") + testsDir);
}
console.log();
}
function ensureTestsIndexJs() {
console.log("- Making sure that 'tests/index.js' exists...");
if (!fs.existsSync(testsIndexFile)) {
fs.writeFileSync(testsIndexFile, [
"import assert from \"assert\";",
"import { add } from \"../build/debug.js\";",
"assert.strictEqual(add(1, 2), 3);",
"console.log(\"ok\");"
].join("\n") + "\n");
console.log(stdoutColors.green(" Created: ") + testsIndexFile);
} else {
console.log(stdoutColors.yellow(" Exists: ") + testsIndexFile);
}
console.log();
}
function ensureIndexHtml() {
console.log("- Making sure that 'index.html' exists...");
if (!fs.existsSync(indexHtmlFile)) {
fs.writeFileSync(indexHtmlFile, [
"<!DOCTYPE html>",
"<html lang=\"en\">",
"<head>",
"<script type=\"module\">",
"import { add } from \"./build/release.js\";",
"document.body.innerText = add(1, 2);",
"</script>",
"</head>",
"<body></body>",
"</html>",
].join("\n") + "\n");
console.log(stdoutColors.green(" Created: ") + indexHtmlFile);
} else {
console.log(stdoutColors.yellow(" Exists: ") + indexHtmlFile);
}
console.log();
}