diff --git a/.gitignore b/.gitignore index cb7d0b9ef8..57082f26a3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ out/ raw/ .history *.backup +.vscode \ No newline at end of file diff --git a/bin/asinit b/bin/asinit index 7752a8d531..557fc9c492 100755 --- a/bin/asinit +++ b/bin/asinit @@ -42,6 +42,7 @@ 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 @@ -84,6 +85,9 @@ console.log([ 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.", "", @@ -108,6 +112,7 @@ function createProject(answer) { ensureIndexJs(); ensureTestsDirectory(); ensureTestsIndexJs(); + ensureAsconfigJson(); console.log([ colors.green("Done!"), "", @@ -207,6 +212,35 @@ function ensureTsconfigJson() { 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, + optimize: true + } + }, + 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)) { @@ -253,8 +287,8 @@ function ensureGitignore() { 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 + " -b build/untouched.wasm -t build/untouched.wat --sourceMap --debug"; - const buildOptimized = "asc " + entryPath + " -b build/optimized.wasm -t build/optimized.wat --sourceMap --optimize"; + const buildUntouched = "asc " + entryPath + " --target debug"; + const buildOptimized = "asc " + entryPath + " --target release"; const buildAll = "npm run asbuild:untouched && npm run asbuild:optimized"; if (!fs.existsSync(packageFile)) { fs.writeFileSync(packageFile, JSON.stringify({ diff --git a/cli/asc.js b/cli/asc.js index 50d5b95cb5..780dd0dcd3 100644 --- a/cli/asc.js +++ b/cli/asc.js @@ -194,7 +194,8 @@ exports.main = function main(argv, options, callback) { if (!stderr) throw Error("'options.stderr' must be specified"); const opts = optionsUtil.parse(argv, exports.options); - const args = opts.options; + let args = opts.options; + argv = opts.arguments; if (args.noColors) { colorsUtil.stdout.supported = @@ -270,6 +271,90 @@ exports.main = function main(argv, options, callback) { // Set up base directory const baseDir = args.baseDir ? path.resolve(args.baseDir) : "."; + const target = args.target; + + // Once the baseDir is calculated, we can resolve the config, and its extensions + let asconfig = getAsconfig(args.config, baseDir, readFile); + let asconfigDir = baseDir; + + const seenAsconfig = new Set(); + seenAsconfig.add(path.join(baseDir, args.config)); + + while (asconfig) { + // merge target first, then merge options, then merge extended asconfigs + if (asconfig.targets && asconfig.targets[target]) { + args = optionsUtil.merge(exports.options, asconfig.targets[target], args); + } + if (asconfig.options) { + if (asconfig.options.transform) { + // ensure that a transform's path is relative to the current config + asconfig.options.transform = asconfig.options.transform.map(p => { + if (!path.isAbsolute(p)) { + if (p.startsWith(".")) { + return path.join(asconfigDir, p); + } + return require.resolve(p); + } + return p; + }); + } + args = optionsUtil.merge(exports.options, args, asconfig.options); + } + + // entries are added to the compilation + if (asconfig.entries) { + for (const entry of asconfig.entries) { + argv.push( + path.isAbsolute(entry) + ? entry + // the entry is relative to the asconfig directory + : path.join(asconfigDir, entry) + ); + } + } + + // asconfig "extends" another config, merging options of it's parent + if (asconfig.extends) { + asconfigDir = path.isAbsolute(asconfig.extends) + // absolute extension path means we know the exact directory and location + ? path.dirname(asconfig.extends) + // relative means we need to calculate a relative asconfigDir + : path.join(asconfigDir, path.dirname(asconfig.extends)); + const fileName = path.basename(asconfig.extends); + const filePath = path.join(asconfigDir, fileName); + if (seenAsconfig.has(filePath)) { + asconfig = null; + } else { + seenAsconfig.add(filePath); + asconfig = getAsconfig(fileName, asconfigDir, readFile); + } + } else { + asconfig = null; // finished resolving the configuration chain + } + } + + // If showConfig print args and exit + if (args.showConfig) { + stderr.write(JSON.stringify(args, null, 2)); + return callback(null); + } + + // This method resolves a path relative to the baseDir instead of process.cwd() + function resolveBasedir(arg) { + return path.resolve(baseDir, arg); + } + + // create a unique set of values + function unique(values) { + return [...new Set(values)]; + } + + // returns a relative path from baseDir + function makeRelative(arg) { + return path.relative(baseDir, arg); + } + // postprocess we need to get absolute file locations argv + argv = unique(argv.map(resolveBasedir)).map(makeRelative); // Set up options const compilerOptions = assemblyscript.newOptions(); @@ -347,7 +432,7 @@ exports.main = function main(argv, options, callback) { const transforms = []; if (args.transform) { let tsNodeRegistered = false; - let transformArgs = args.transform; + let transformArgs = unique(args.transform.map(resolveBasedir)); for (let i = 0, k = transformArgs.length; i < k; ++i) { let filename = transformArgs[i].trim(); if (!tsNodeRegistered && filename.endsWith(".ts")) { // ts-node requires .ts specifically @@ -376,6 +461,7 @@ exports.main = function main(argv, options, callback) { } } } + function applyTransform(name, ...args) { for (let i = 0, k = transforms.length; i < k; ++i) { let transform = transforms[i]; @@ -400,11 +486,12 @@ exports.main = function main(argv, options, callback) { assemblyscript.parse(program, exports.libraryFiles[libPath], exports.libraryPrefix + libPath + extension.ext, false); }); }); - const customLibDirs = []; + let customLibDirs = []; if (args.lib) { let lib = args.lib; - if (typeof lib === "string") lib = lib.split(","); - Array.prototype.push.apply(customLibDirs, lib.map(lib => lib.trim())); + if (typeof lib === "string") lib = lib.trim().split(/\s*,\s*/); + customLibDirs.push(...lib.map(resolveBasedir)); + customLibDirs = unique(customLibDirs); // `lib` and `customLibDirs` may include duplicates for (let i = 0, k = customLibDirs.length; i < k; ++i) { // custom let libDir = customLibDirs[i]; let libFiles; @@ -574,7 +661,7 @@ exports.main = function main(argv, options, callback) { const filename = argv[i]; let sourcePath = String(filename).replace(/\\/g, "/").replace(extension.re, "").replace(/[\\/]$/, ""); - + // Setting the path to relative path sourcePath = path.isAbsolute(sourcePath) ? path.relative(baseDir, sourcePath) : sourcePath; @@ -925,6 +1012,56 @@ exports.main = function main(argv, options, callback) { } }; +const toString = Object.prototype.toString; + +function isObject(arg) { + return toString.call(arg) === "[object Object]"; +} + +function getAsconfig(file, baseDir, readFile) { + const contents = readFile(file, baseDir); + const location = path.join(baseDir, file); + if (!contents) return null; + + // obtain the configuration + let config; + try { + config = JSON.parse(contents); + } catch(ex) { + throw new Error("Asconfig is not valid json: " + location); + } + + // validate asconfig shape + if (config.options && !isObject(config.options)) { + throw new Error("Asconfig.options is not an object: " + location); + } + + if (config.include && !Array.isArray(config.include)) { + throw new Error("Asconfig.include is not an array: " + location); + } + + if (config.targets) { + if (!isObject(config.targets)) { + throw new Error("Asconfig.targets is not an object: " + location); + } + const targets = Object.keys(config.targets); + for (let i = 0; i < targets.length; i++) { + const target = targets[i]; + if (!isObject(config.targets[target])) { + throw new Error("Asconfig.targets." + target + " is not an object: " + location); + } + } + } + + if (config.extends && typeof config.extends !== "string") { + throw new Error("Asconfig.extends is not a string: " + location); + } + + return config; +} + +exports.getAsconfig = getAsconfig; + /** Checks diagnostics emitted so far for errors. */ function checkDiagnostics(program, stderr) { var diagnostic; diff --git a/cli/asc.json b/cli/asc.json index fbbe218b70..9b194d7861 100644 --- a/cli/asc.json +++ b/cli/asc.json @@ -17,6 +17,18 @@ "type": "b", "default": false }, + "config": { + "category": "General", + "description": "Configuration file to apply. CLI arguments take precedence.", + "type": "s", + "default": "asconfig.json" + }, + "target": { + "category": "General", + "description": "Target configuration to use. Defaults to 'release'.", + "type": "s", + "default": "release" + }, "optimize": { "category": "Optimization", @@ -288,6 +300,11 @@ "type": "b", "default": false }, + "showConfig": { + "description": "Print computed compiler options and exit.", + "type": "b", + "default": false + }, "measure": { "description": "Prints measuring information on I/O and compile times.", "type": "b", diff --git a/package.json b/package.json index fbc5f3b32e..1f287a3138 100644 --- a/package.json +++ b/package.json @@ -59,11 +59,12 @@ "check:config": "tsc --noEmit -p src --diagnostics --listFiles", "check:require": "tsc --noEmit --target ESNEXT --module commonjs --experimentalDecorators tests/require/index", "check:lint": "eslint --max-warnings 0 --ext js . && eslint --max-warnings 0 --ext ts .", - "test": "npm run test:parser && npm run test:compiler && npm run test:packages && npm run test:extension", + "test": "npm run test:parser && npm run test:compiler && npm run test:packages && npm run test:extension && npm run test:asconfig", "test:parser": "node tests/parser", "test:compiler": "node tests/compiler", "test:packages": "cd tests/packages && npm run test", "test:extension": "cd tests/extension && npm run test", + "test:asconfig": "cd tests/asconfig && npm run test", "make": "npm run clean && npm test && npm run build && npm test", "all": "npm run check && npm run make", "docs": "typedoc --tsconfig tsconfig-docs.json --mode modules --name \"AssemblyScript Compiler API\" --out ./docs/api --ignoreCompilerErrors --excludeNotExported --excludePrivate --excludeExternals --exclude **/std/** --includeDeclarations --readme src/README.md", diff --git a/tests/asconfig/complicated/asconfig.json b/tests/asconfig/complicated/asconfig.json new file mode 100644 index 0000000000..8f622493dc --- /dev/null +++ b/tests/asconfig/complicated/asconfig.json @@ -0,0 +1,16 @@ +{ + "targets": { + "release": { + "optimize": true, + "runtime": "stub", + "initialMemory": 30, + "explicitStart": true, + "measure": true, + "pedantic": true + } + }, + "options": { + "initialMemory": 100, + "enable": ["simd"] + } +} \ No newline at end of file diff --git a/tests/asconfig/complicated/assembly/index.ts b/tests/asconfig/complicated/assembly/index.ts new file mode 100644 index 0000000000..ae67e9d6f5 --- /dev/null +++ b/tests/asconfig/complicated/assembly/index.ts @@ -0,0 +1,6 @@ +assert(ASC_OPTIMIZE_LEVEL == 3); +assert(ASC_SHRINK_LEVEL == 1); +assert(ASC_FEATURE_SIMD); +let size = memory.size(); +trace("size", 1, size); +assert(size == 30); diff --git a/tests/asconfig/complicated/package.json b/tests/asconfig/complicated/package.json new file mode 100644 index 0000000000..f98424bae9 --- /dev/null +++ b/tests/asconfig/complicated/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../index.js" + } +} diff --git a/tests/asconfig/cyclical/asconfig.json b/tests/asconfig/cyclical/asconfig.json new file mode 100644 index 0000000000..98aae677ca --- /dev/null +++ b/tests/asconfig/cyclical/asconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./extends.json" +} diff --git a/tests/asconfig/cyclical/assembly/index.ts b/tests/asconfig/cyclical/assembly/index.ts new file mode 100644 index 0000000000..4d1656bdf6 --- /dev/null +++ b/tests/asconfig/cyclical/assembly/index.ts @@ -0,0 +1 @@ +assert(true); diff --git a/tests/asconfig/cyclical/extends.json b/tests/asconfig/cyclical/extends.json new file mode 100644 index 0000000000..3420bc71bc --- /dev/null +++ b/tests/asconfig/cyclical/extends.json @@ -0,0 +1,3 @@ +{ + "extends": "./asconfig.json" +} diff --git a/tests/asconfig/cyclical/package.json b/tests/asconfig/cyclical/package.json new file mode 100644 index 0000000000..f98424bae9 --- /dev/null +++ b/tests/asconfig/cyclical/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../index.js" + } +} diff --git a/tests/asconfig/entry-points/asconfig.json b/tests/asconfig/entry-points/asconfig.json new file mode 100644 index 0000000000..f83851ac73 --- /dev/null +++ b/tests/asconfig/entry-points/asconfig.json @@ -0,0 +1,3 @@ +{ + "entries": ["assembly/globals.ts"] +} diff --git a/tests/asconfig/entry-points/assembly/globals.ts b/tests/asconfig/entry-points/assembly/globals.ts new file mode 100644 index 0000000000..ef2ca6b2f9 --- /dev/null +++ b/tests/asconfig/entry-points/assembly/globals.ts @@ -0,0 +1,3 @@ +// @ts-ignore: decorator +@global const answerToLife = 42; +assert(answerToLife); diff --git a/tests/asconfig/entry-points/assembly/index.ts b/tests/asconfig/entry-points/assembly/index.ts new file mode 100644 index 0000000000..8d57706af7 --- /dev/null +++ b/tests/asconfig/entry-points/assembly/index.ts @@ -0,0 +1,2 @@ + +assert(answerToLife == 42); diff --git a/tests/asconfig/entry-points/nested/asconfig.json b/tests/asconfig/entry-points/nested/asconfig.json new file mode 100644 index 0000000000..92d84fcc96 --- /dev/null +++ b/tests/asconfig/entry-points/nested/asconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../asconfig.json" +} diff --git a/tests/asconfig/entry-points/nested/assembly/index.ts b/tests/asconfig/entry-points/nested/assembly/index.ts new file mode 100644 index 0000000000..48b64eeb05 --- /dev/null +++ b/tests/asconfig/entry-points/nested/assembly/index.ts @@ -0,0 +1 @@ +assert(answerToLife == 42); diff --git a/tests/asconfig/entry-points/nested/package.json b/tests/asconfig/entry-points/nested/package.json new file mode 100644 index 0000000000..7603e9ad4d --- /dev/null +++ b/tests/asconfig/entry-points/nested/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../../index.js" + } +} diff --git a/tests/asconfig/entry-points/package.json b/tests/asconfig/entry-points/package.json new file mode 100644 index 0000000000..e835996ed6 --- /dev/null +++ b/tests/asconfig/entry-points/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../index.js && cd nested && npm run test" + } +} diff --git a/tests/asconfig/extends/asconfig.json b/tests/asconfig/extends/asconfig.json new file mode 100644 index 0000000000..d052236111 --- /dev/null +++ b/tests/asconfig/extends/asconfig.json @@ -0,0 +1,11 @@ +{ + "targets": { + "release": { + "shrinkLevel": 2 + } + }, + "options": { + "enable": ["simd"] + }, + "extends": "./extends.json" +} diff --git a/tests/asconfig/extends/assembly/index.ts b/tests/asconfig/extends/assembly/index.ts new file mode 100644 index 0000000000..afd4eb590d --- /dev/null +++ b/tests/asconfig/extends/assembly/index.ts @@ -0,0 +1,3 @@ +assert(ASC_OPTIMIZE_LEVEL == 1); +assert(ASC_SHRINK_LEVEL == 2); +assert(ASC_FEATURE_SIMD); diff --git a/tests/asconfig/extends/extends.json b/tests/asconfig/extends/extends.json new file mode 100644 index 0000000000..44d1411849 --- /dev/null +++ b/tests/asconfig/extends/extends.json @@ -0,0 +1,11 @@ +{ + "targets": { + "release": { + "shrinkLevel": 1, + "optimizeLevel": 1 + } + }, + "options": { + "disable": ["simd"] + } +} diff --git a/tests/asconfig/extends/package.json b/tests/asconfig/extends/package.json new file mode 100644 index 0000000000..f98424bae9 --- /dev/null +++ b/tests/asconfig/extends/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../index.js" + } +} diff --git a/tests/asconfig/index.js b/tests/asconfig/index.js new file mode 100644 index 0000000000..6519ef1300 --- /dev/null +++ b/tests/asconfig/index.js @@ -0,0 +1,33 @@ +const asc = require("../../cli/asc"); +const loader = require("../../lib/loader"); +const args = process.argv.slice(2); + +/** @type {Uint8Array} */ +let binary; +asc.main(["assembly/index.ts", "--outFile", "output.wasm", "--explicitStart", ...args], { + writeFile(name, contents) { + if (name === "output.wasm") { + binary = contents; + } + } +}, (err) => { + if (err) { + console.error(err); + process.exit(1); + } + + if (!binary) { + console.error("No binary was generated for the asconfig test in " + process.cwd()); + process.exit(1); + } + + const theModule = loader.instantiateSync(binary); + + try { + theModule.exports._start(); + } catch (err) { + console.error("The wasm module _start() function failed in " + process.cwd()); + process.exit(1); + } + return 0; +}); diff --git a/tests/asconfig/package.json b/tests/asconfig/package.json new file mode 100644 index 0000000000..58789240bc --- /dev/null +++ b/tests/asconfig/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "scripts": { + "test": "npm run test:use-consts && npm run test:target && npm run test:entry-points && npm run test:complicated", + "test:use-consts": "cd use-consts && npm run test", + "test:entry-points": "cd entry-points && npm run test", + "test:respect-inheritence": "cd respect-inheritence && npm run test", + "test:target": "cd target && npm run test", + "test:cyclical": "cd cyclical && npm run test", + "test:complicated": "cd complicated && npm run test" + } +} diff --git a/tests/asconfig/respect-inheritence/asconfig.json b/tests/asconfig/respect-inheritence/asconfig.json new file mode 100644 index 0000000000..a171baf9dd --- /dev/null +++ b/tests/asconfig/respect-inheritence/asconfig.json @@ -0,0 +1,12 @@ +{ + "targets": { + "release": { + "optimizeLevel": 2, + "enable": ["simd"] + } + }, + "options": { + "optimizeLevel": 1, + "disable": ["simd"] + } +} diff --git a/tests/asconfig/respect-inheritence/assembly/index.ts b/tests/asconfig/respect-inheritence/assembly/index.ts new file mode 100644 index 0000000000..1c0b3966b2 --- /dev/null +++ b/tests/asconfig/respect-inheritence/assembly/index.ts @@ -0,0 +1,5 @@ +// --optimizeLevel 3 +assert(ASC_OPTIMIZE_LEVEL == 3); +// enabled in target release which is default +assert(ASC_FEATURE_SIMD); +// disable: ["simd"] is set in asconfig under general options, it should be ignored diff --git a/tests/asconfig/respect-inheritence/package.json b/tests/asconfig/respect-inheritence/package.json new file mode 100644 index 0000000000..6d14e2db7a --- /dev/null +++ b/tests/asconfig/respect-inheritence/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../index.js --optimizeLevel 3" + } +} diff --git a/tests/asconfig/target/asconfig.json b/tests/asconfig/target/asconfig.json new file mode 100644 index 0000000000..aaadbf8119 --- /dev/null +++ b/tests/asconfig/target/asconfig.json @@ -0,0 +1,13 @@ +{ + "targets": { + "release": { + "optimizeLevel": 3, + "shrinkLevel": 1, + "enable": ["simd"] + }, + "dev": { + "debug": true + } + }, + "options": {} +} diff --git a/tests/asconfig/target/assembly/index.ts b/tests/asconfig/target/assembly/index.ts new file mode 100644 index 0000000000..5c03e37224 --- /dev/null +++ b/tests/asconfig/target/assembly/index.ts @@ -0,0 +1,3 @@ +assert(ASC_OPTIMIZE_LEVEL == 3); +assert(ASC_SHRINK_LEVEL == 1); +assert(ASC_FEATURE_SIMD); diff --git a/tests/asconfig/target/package.json b/tests/asconfig/target/package.json new file mode 100644 index 0000000000..f98424bae9 --- /dev/null +++ b/tests/asconfig/target/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../index.js" + } +} diff --git a/tests/asconfig/use-consts/asconfig.json b/tests/asconfig/use-consts/asconfig.json new file mode 100644 index 0000000000..7fe2318a0b --- /dev/null +++ b/tests/asconfig/use-consts/asconfig.json @@ -0,0 +1,9 @@ +{ + "options": { + "use": [ + "A=1", + "B=2", + "C=3" + ] + } +} diff --git a/tests/asconfig/use-consts/assembly/index.ts b/tests/asconfig/use-consts/assembly/index.ts new file mode 100644 index 0000000000..827accff2d --- /dev/null +++ b/tests/asconfig/use-consts/assembly/index.ts @@ -0,0 +1,3 @@ +assert(A == 1); +assert(B == 2); +assert(C == 3); diff --git a/tests/asconfig/use-consts/package.json b/tests/asconfig/use-consts/package.json new file mode 100644 index 0000000000..f98424bae9 --- /dev/null +++ b/tests/asconfig/use-consts/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../index.js" + } +}