#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const colors = require("../cli/util/colors"); const version = require("../package.json").version; const options = require("../cli/util/options"); 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 a 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" } }; const cliOptions = options.parse(process.argv.slice(2), asinitOptions); if (cliOptions.options.help || cliOptions.arguments.length === 0) printHelp(); function printHelp() { console.log([ "Sets up a new AssemblyScript project or updates an existing one.", "For example, to create a new project in the current directory:", "", " " + colors.cyan("asinit") + " .", ].join("\n")); process.exit(0); } const projectDir = path.resolve(cliOptions.arguments[0]); const compilerDir = path.join(__dirname, ".."); const compilerVersion = require(path.join(compilerDir, "package.json")).version; 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 indexFile = path.join(projectDir, "index.js"); const testsIndexFile = path.join(testsDir, "index.js"); console.log([ "Version: " + version, "", colors.white([ "This command will make sure that the following files exist in the project", "directory '" + projectDir + "':" ].join("\n")), "", colors.cyan(" ./assembly"), " Directory holding the AssemblyScript sources being compiled to WebAssembly.", "", colors.cyan(" ./assembly/tsconfig.json"), " TypeScript configuration inheriting recommended AssemblyScript settings.", "", colors.cyan(" ./assembly/index.ts"), " Example entry file being compiled to WebAssembly to get you started.", "", colors.cyan(" ./build"), " Build artifact directory where compiled WebAssembly files are stored.", "", colors.cyan(" ./build/.gitignore"), " Git configuration that excludes compiled binaries from source control.", "", colors.cyan(" ./index.js"), " Main file loading the WebAssembly module and exporting its exports.", "", colors.cyan(" ./tests/index.js"), " Example test to check that your module is indeed working.", "", colors.cyan(" ./asconfig.json"), " Configuration file defining both a 'debug' and a 'release' target.", "", colors.cyan(" ./package.json"), " Package info containing the necessary commands to compile to WebAssembly.", "", "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(); ensureIndexJs(); ensureTestsDirectory(); ensureTestsIndexJs(); ensureAsconfigJson(); console.log([ colors.green("Done!"), "", "Don't forget to install dependencies before you start:", "", colors.white(" " + commands[pm].install), "", "To edit the entry file, open '" + colors.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:", "", colors.white(" " + commands[pm].run + " asbuild"), "", "Running the command above creates the following binaries incl. their respective", "text format representations and source maps:", "", colors.cyan(" ./build/untouched.wasm"), colors.cyan(" ./build/untouched.wasm.map"), colors.cyan(" ./build/untouched.wat"), "", " ^ The untouched WebAssembly module as generated by the compiler.", " This one matches your sources exactly, without any optimizations.", "", colors.cyan(" ./build/optimized.wasm"), colors.cyan(" ./build/optimized.wasm.map"), colors.cyan(" ./build/optimized.wat"), "", " ^ The optimized WebAssembly module using default optimization settings.", " You can change the optimization settings in '" + colors.cyan("package.json")+ "'.", "", "To run the tests, do:", "", colors.white(" " + commands[pm].test), "", "The AssemblyScript documentation covers all the details:", "", " https://docs.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(colors.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(colors.green(" Created: ") + projectDir); } else { console.log(colors.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(colors.green(" Created: ") + assemblyDir); } else { console.log(colors.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(colors.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(colors.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: { // -b build/untouched.wasm -t build/untouched.wat --sourceMap --debug binaryFile: "build/untouched.wasm", textFile: "build/untouched.wat", sourceMap: true, debug: true }, release: { // -b build/optimized.wasm -t build/optimized.wat --sourceMap --optimize binaryFile: "build/optimized.wasm", textFile: "build/optimized.wat", sourceMap: true, optimizeLevel: 3, shrinkLevel: 0, converge: false, noAssert: false } }, options: {} }, null, 2)); console.log(colors.green(" Created: ") + asconfigFile); } else { console.log(colors.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(colors.green(" Created: ") + entryFile); } else { console.log(colors.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(colors.green(" Created: ") + buildDir); } else { console.log(colors.yellow(" Exists: ") + buildDir); } console.log(); } function ensureGitignore() { console.log("- Making sure that 'build/.gitignore' is set up..."); if (!fs.existsSync(gitignoreFile)) { fs.writeFileSync(gitignoreFile, [ "*.wasm", "*.wasm.map", "*.asm.js" ].join("\n") + "\n"); console.log(colors.green(" Created: ") + gitignoreFile); } else { console.log(colors.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 buildUntouched = "asc " + entryPath + " --target debug"; const buildOptimized = "asc " + entryPath + " --target release"; const buildAll = commands[pm].run + " asbuild:untouched && " + commands[pm].run + " asbuild:optimized"; if (!fs.existsSync(packageFile)) { fs.writeFileSync(packageFile, JSON.stringify({ "scripts": { "asbuild:untouched": buildUntouched, "asbuild:optimized": buildOptimized, "asbuild": buildAll, "test": "node tests" }, "dependencies": { "@assemblyscript/loader": "^" + compilerVersion }, "devDependencies": { "assemblyscript": "^" + compilerVersion } }, null, 2)); console.log(colors.green(" Created: ") + packageFile); } else { let pkg = JSON.parse(fs.readFileSync(packageFile)); let scripts = pkg.scripts || {}; let updated = false; if (!scripts["asbuild"]) { scripts["asbuild:untouched"] = buildUntouched; scripts["asbuild:optimized"] = buildOptimized; scripts["asbuild"] = buildAll; pkg["scripts"] = scripts; updated = true; } if (!scripts["test"] || scripts["test"] == npmDefaultTest) { scripts["test"] = "node tests"; pkg["scripts"] = scripts; updated = true; } let dependencies = pkg["dependencies"] || {}; if (!dependencies["@assemblyscript/loader"]) { dependencies["@assemblyscript/loader"] = "^" + compilerVersion; pkg["dependencies"] = dependencies; updated = true; } let devDependencies = pkg["devDependencies"] || {}; if (!devDependencies["assemblyscript"]) { devDependencies["assemblyscript"] = "^" + compilerVersion; pkg["devDependencies"] = devDependencies; updated = true; } if (updated) { fs.writeFileSync(packageFile, JSON.stringify(pkg, null, 2)); console.log(colors.green(" Updated: ") + packageFile); } else { console.log(colors.yellow(" Exists: ") + packageFile); } } console.log(); } function ensureIndexJs() { console.log("- Making sure that 'index.js' exists..."); if (!fs.existsSync(indexFile)) { // since node.js v13.2.0 or v12.17.0 we can use ESM without flags const ver = process.versions.node.split('.'); const maj = parseInt(ver[0]); const min = parseInt(ver[1]); const supportESM = maj >= 14 || (maj == 13 && min >= 2) || (maj == 12 && min >= 17); fs.writeFileSync(indexFile, [ "const fs = require(\"fs\");", "const loader = require(\"@assemblyscript/loader" + (supportESM ? "" : "/umd") + "\");", "const imports = { /* imports go here */ };", "const wasmModule = loader.instantiateSync(fs.readFileSync(__dirname + \"/build/optimized.wasm\"), imports);", "module.exports = wasmModule.exports;" ].join("\n") + "\n"); console.log(colors.green(" Created: ") + indexFile); } else { console.log(colors.yellow(" Exists: ") + indexFile); } console.log(); } function ensureTestsDirectory() { console.log("- Making sure that the 'tests' directory exists..."); if (!fs.existsSync(testsDir)) { fs.mkdirSync(testsDir); console.log(colors.green(" Created: ") + testsDir); } else { console.log(colors.yellow(" Exists: ") + testsDir); } console.log(); } function ensureTestsIndexJs() { console.log("- Making sure that 'tests/index.js' exists..."); if (!fs.existsSync(testsIndexFile)) { fs.writeFileSync(testsIndexFile, [ "const assert = require(\"assert\");", "const myModule = require(\"..\");", "assert.strictEqual(myModule.add(1, 2), 3);", "console.log(\"ok\");" ].join("\n") + "\n"); console.log(colors.green(" Created: ") + testsIndexFile); } else { console.log(colors.yellow(" Exists: ") + testsIndexFile); } console.log(); }