Skip to content

Commit 5207bcb

Browse files
jtennerMaxGraeyWillem Wyndham
authored
feat: Support asconfig.json (#1238)
Co-authored-by: Max Graey <[email protected]> Co-authored-by: Willem Wyndham <[email protected]>
1 parent 1b38b2a commit 5207bcb

34 files changed

+403
-9
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ out/
66
raw/
77
.history
88
*.backup
9+
.vscode

Diff for: bin/asinit

+36-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const compilerDir = path.join(__dirname, "..");
4242
const compilerVersion = require(path.join(compilerDir, "package.json")).version;
4343
const assemblyDir = path.join(projectDir, "assembly");
4444
const tsconfigFile = path.join(assemblyDir, "tsconfig.json");
45+
const asconfigFile = path.join(projectDir, "asconfig.json");
4546
let tsconfigBase = path.relative(assemblyDir, path.join(compilerDir, "std", "assembly.json"));
4647
if (/^(\.\.[/\\])*node_modules[/\\]assemblyscript[/\\]/.test(tsconfigBase)) {
4748
// Use node resolution if the compiler is a normal dependency
@@ -84,6 +85,9 @@ console.log([
8485
colors.cyan(" ./tests/index.js"),
8586
" Example test to check that your module is indeed working.",
8687
"",
88+
colors.cyan(" ./asconfig.json"),
89+
" Configuration file defining both a 'debug' and a 'release' target.",
90+
"",
8791
colors.cyan(" ./package.json"),
8892
" Package info containing the necessary commands to compile to WebAssembly.",
8993
"",
@@ -108,6 +112,7 @@ function createProject(answer) {
108112
ensureIndexJs();
109113
ensureTestsDirectory();
110114
ensureTestsIndexJs();
115+
ensureAsconfigJson();
111116
console.log([
112117
colors.green("Done!"),
113118
"",
@@ -207,6 +212,35 @@ function ensureTsconfigJson() {
207212
console.log();
208213
}
209214

215+
function ensureAsconfigJson() {
216+
console.log("- Making sure that 'asconfig.json' is set up...");
217+
if (!fs.existsSync(asconfigFile)) {
218+
fs.writeFileSync(asconfigFile, JSON.stringify({
219+
targets: {
220+
debug: {
221+
// -b build/untouched.wasm -t build/untouched.wat --sourceMap --debug
222+
binaryFile: "build/untouched.wasm",
223+
textFile: "build/untouched.wat",
224+
sourceMap: true,
225+
debug: true
226+
},
227+
release: {
228+
// -b build/optimized.wasm -t build/optimized.wat --sourceMap --optimize
229+
binaryFile: "build/optimized.wasm",
230+
textFile: "build/optimized.wat",
231+
sourceMap: true,
232+
optimize: true
233+
}
234+
},
235+
options: {}
236+
}, null, 2));
237+
console.log(colors.green(" Created: ") + asconfigFile);
238+
} else {
239+
console.log(colors.yellow(" Exists: ") + asconfigFile);
240+
}
241+
console.log();
242+
}
243+
210244
function ensureEntryFile() {
211245
console.log("- Making sure that 'assembly/index.ts' exists...");
212246
if (!fs.existsSync(entryFile)) {
@@ -253,8 +287,8 @@ function ensureGitignore() {
253287
function ensurePackageJson() {
254288
console.log("- Making sure that 'package.json' contains the build commands...");
255289
const entryPath = path.relative(projectDir, entryFile).replace(/\\/g, "/");
256-
const buildUntouched = "asc " + entryPath + " -b build/untouched.wasm -t build/untouched.wat --sourceMap --debug";
257-
const buildOptimized = "asc " + entryPath + " -b build/optimized.wasm -t build/optimized.wat --sourceMap --optimize";
290+
const buildUntouched = "asc " + entryPath + " --target debug";
291+
const buildOptimized = "asc " + entryPath + " --target release";
258292
const buildAll = "npm run asbuild:untouched && npm run asbuild:optimized";
259293
if (!fs.existsSync(packageFile)) {
260294
fs.writeFileSync(packageFile, JSON.stringify({

Diff for: cli/asc.js

+143-6
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ exports.main = function main(argv, options, callback) {
194194
if (!stderr) throw Error("'options.stderr' must be specified");
195195

196196
const opts = optionsUtil.parse(argv, exports.options);
197-
const args = opts.options;
197+
let args = opts.options;
198+
198199
argv = opts.arguments;
199200
if (args.noColors) {
200201
colorsUtil.stdout.supported =
@@ -270,6 +271,90 @@ exports.main = function main(argv, options, callback) {
270271

271272
// Set up base directory
272273
const baseDir = args.baseDir ? path.resolve(args.baseDir) : ".";
274+
const target = args.target;
275+
276+
// Once the baseDir is calculated, we can resolve the config, and its extensions
277+
let asconfig = getAsconfig(args.config, baseDir, readFile);
278+
let asconfigDir = baseDir;
279+
280+
const seenAsconfig = new Set();
281+
seenAsconfig.add(path.join(baseDir, args.config));
282+
283+
while (asconfig) {
284+
// merge target first, then merge options, then merge extended asconfigs
285+
if (asconfig.targets && asconfig.targets[target]) {
286+
args = optionsUtil.merge(exports.options, asconfig.targets[target], args);
287+
}
288+
if (asconfig.options) {
289+
if (asconfig.options.transform) {
290+
// ensure that a transform's path is relative to the current config
291+
asconfig.options.transform = asconfig.options.transform.map(p => {
292+
if (!path.isAbsolute(p)) {
293+
if (p.startsWith(".")) {
294+
return path.join(asconfigDir, p);
295+
}
296+
return require.resolve(p);
297+
}
298+
return p;
299+
});
300+
}
301+
args = optionsUtil.merge(exports.options, args, asconfig.options);
302+
}
303+
304+
// entries are added to the compilation
305+
if (asconfig.entries) {
306+
for (const entry of asconfig.entries) {
307+
argv.push(
308+
path.isAbsolute(entry)
309+
? entry
310+
// the entry is relative to the asconfig directory
311+
: path.join(asconfigDir, entry)
312+
);
313+
}
314+
}
315+
316+
// asconfig "extends" another config, merging options of it's parent
317+
if (asconfig.extends) {
318+
asconfigDir = path.isAbsolute(asconfig.extends)
319+
// absolute extension path means we know the exact directory and location
320+
? path.dirname(asconfig.extends)
321+
// relative means we need to calculate a relative asconfigDir
322+
: path.join(asconfigDir, path.dirname(asconfig.extends));
323+
const fileName = path.basename(asconfig.extends);
324+
const filePath = path.join(asconfigDir, fileName);
325+
if (seenAsconfig.has(filePath)) {
326+
asconfig = null;
327+
} else {
328+
seenAsconfig.add(filePath);
329+
asconfig = getAsconfig(fileName, asconfigDir, readFile);
330+
}
331+
} else {
332+
asconfig = null; // finished resolving the configuration chain
333+
}
334+
}
335+
336+
// If showConfig print args and exit
337+
if (args.showConfig) {
338+
stderr.write(JSON.stringify(args, null, 2));
339+
return callback(null);
340+
}
341+
342+
// This method resolves a path relative to the baseDir instead of process.cwd()
343+
function resolveBasedir(arg) {
344+
return path.resolve(baseDir, arg);
345+
}
346+
347+
// create a unique set of values
348+
function unique(values) {
349+
return [...new Set(values)];
350+
}
351+
352+
// returns a relative path from baseDir
353+
function makeRelative(arg) {
354+
return path.relative(baseDir, arg);
355+
}
356+
// postprocess we need to get absolute file locations argv
357+
argv = unique(argv.map(resolveBasedir)).map(makeRelative);
273358

274359
// Set up options
275360
const compilerOptions = assemblyscript.newOptions();
@@ -347,7 +432,7 @@ exports.main = function main(argv, options, callback) {
347432
const transforms = [];
348433
if (args.transform) {
349434
let tsNodeRegistered = false;
350-
let transformArgs = args.transform;
435+
let transformArgs = unique(args.transform.map(resolveBasedir));
351436
for (let i = 0, k = transformArgs.length; i < k; ++i) {
352437
let filename = transformArgs[i].trim();
353438
if (!tsNodeRegistered && filename.endsWith(".ts")) { // ts-node requires .ts specifically
@@ -376,6 +461,7 @@ exports.main = function main(argv, options, callback) {
376461
}
377462
}
378463
}
464+
379465
function applyTransform(name, ...args) {
380466
for (let i = 0, k = transforms.length; i < k; ++i) {
381467
let transform = transforms[i];
@@ -400,11 +486,12 @@ exports.main = function main(argv, options, callback) {
400486
assemblyscript.parse(program, exports.libraryFiles[libPath], exports.libraryPrefix + libPath + extension.ext, false);
401487
});
402488
});
403-
const customLibDirs = [];
489+
let customLibDirs = [];
404490
if (args.lib) {
405491
let lib = args.lib;
406-
if (typeof lib === "string") lib = lib.split(",");
407-
Array.prototype.push.apply(customLibDirs, lib.map(lib => lib.trim()));
492+
if (typeof lib === "string") lib = lib.trim().split(/\s*,\s*/);
493+
customLibDirs.push(...lib.map(resolveBasedir));
494+
customLibDirs = unique(customLibDirs); // `lib` and `customLibDirs` may include duplicates
408495
for (let i = 0, k = customLibDirs.length; i < k; ++i) { // custom
409496
let libDir = customLibDirs[i];
410497
let libFiles;
@@ -574,7 +661,7 @@ exports.main = function main(argv, options, callback) {
574661
const filename = argv[i];
575662

576663
let sourcePath = String(filename).replace(/\\/g, "/").replace(extension.re, "").replace(/[\\/]$/, "");
577-
664+
578665
// Setting the path to relative path
579666
sourcePath = path.isAbsolute(sourcePath) ? path.relative(baseDir, sourcePath) : sourcePath;
580667

@@ -925,6 +1012,56 @@ exports.main = function main(argv, options, callback) {
9251012
}
9261013
};
9271014

1015+
const toString = Object.prototype.toString;
1016+
1017+
function isObject(arg) {
1018+
return toString.call(arg) === "[object Object]";
1019+
}
1020+
1021+
function getAsconfig(file, baseDir, readFile) {
1022+
const contents = readFile(file, baseDir);
1023+
const location = path.join(baseDir, file);
1024+
if (!contents) return null;
1025+
1026+
// obtain the configuration
1027+
let config;
1028+
try {
1029+
config = JSON.parse(contents);
1030+
} catch(ex) {
1031+
throw new Error("Asconfig is not valid json: " + location);
1032+
}
1033+
1034+
// validate asconfig shape
1035+
if (config.options && !isObject(config.options)) {
1036+
throw new Error("Asconfig.options is not an object: " + location);
1037+
}
1038+
1039+
if (config.include && !Array.isArray(config.include)) {
1040+
throw new Error("Asconfig.include is not an array: " + location);
1041+
}
1042+
1043+
if (config.targets) {
1044+
if (!isObject(config.targets)) {
1045+
throw new Error("Asconfig.targets is not an object: " + location);
1046+
}
1047+
const targets = Object.keys(config.targets);
1048+
for (let i = 0; i < targets.length; i++) {
1049+
const target = targets[i];
1050+
if (!isObject(config.targets[target])) {
1051+
throw new Error("Asconfig.targets." + target + " is not an object: " + location);
1052+
}
1053+
}
1054+
}
1055+
1056+
if (config.extends && typeof config.extends !== "string") {
1057+
throw new Error("Asconfig.extends is not a string: " + location);
1058+
}
1059+
1060+
return config;
1061+
}
1062+
1063+
exports.getAsconfig = getAsconfig;
1064+
9281065
/** Checks diagnostics emitted so far for errors. */
9291066
function checkDiagnostics(program, stderr) {
9301067
var diagnostic;

Diff for: cli/asc.json

+17
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@
1717
"type": "b",
1818
"default": false
1919
},
20+
"config": {
21+
"category": "General",
22+
"description": "Configuration file to apply. CLI arguments take precedence.",
23+
"type": "s",
24+
"default": "asconfig.json"
25+
},
26+
"target": {
27+
"category": "General",
28+
"description": "Target configuration to use. Defaults to 'release'.",
29+
"type": "s",
30+
"default": "release"
31+
},
2032

2133
"optimize": {
2234
"category": "Optimization",
@@ -288,6 +300,11 @@
288300
"type": "b",
289301
"default": false
290302
},
303+
"showConfig": {
304+
"description": "Print computed compiler options and exit.",
305+
"type": "b",
306+
"default": false
307+
},
291308
"measure": {
292309
"description": "Prints measuring information on I/O and compile times.",
293310
"type": "b",

Diff for: package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@
5959
"check:config": "tsc --noEmit -p src --diagnostics --listFiles",
6060
"check:require": "tsc --noEmit --target ESNEXT --module commonjs --experimentalDecorators tests/require/index",
6161
"check:lint": "eslint --max-warnings 0 --ext js . && eslint --max-warnings 0 --ext ts .",
62-
"test": "npm run test:parser && npm run test:compiler && npm run test:packages && npm run test:extension",
62+
"test": "npm run test:parser && npm run test:compiler && npm run test:packages && npm run test:extension && npm run test:asconfig",
6363
"test:parser": "node tests/parser",
6464
"test:compiler": "node tests/compiler",
6565
"test:packages": "cd tests/packages && npm run test",
6666
"test:extension": "cd tests/extension && npm run test",
67+
"test:asconfig": "cd tests/asconfig && npm run test",
6768
"make": "npm run clean && npm test && npm run build && npm test",
6869
"all": "npm run check && npm run make",
6970
"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 for: tests/asconfig/complicated/asconfig.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"targets": {
3+
"release": {
4+
"optimize": true,
5+
"runtime": "stub",
6+
"initialMemory": 30,
7+
"explicitStart": true,
8+
"measure": true,
9+
"pedantic": true
10+
}
11+
},
12+
"options": {
13+
"initialMemory": 100,
14+
"enable": ["simd"]
15+
}
16+
}

Diff for: tests/asconfig/complicated/assembly/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
assert(ASC_OPTIMIZE_LEVEL == 3);
2+
assert(ASC_SHRINK_LEVEL == 1);
3+
assert(ASC_FEATURE_SIMD);
4+
let size = memory.size();
5+
trace("size", 1, size);
6+
assert(size == 30);

Diff for: tests/asconfig/complicated/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"private": true,
3+
"scripts": {
4+
"test": "node ../index.js"
5+
}
6+
}

Diff for: tests/asconfig/cyclical/asconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./extends.json"
3+
}

Diff for: tests/asconfig/cyclical/assembly/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
assert(true);

Diff for: tests/asconfig/cyclical/extends.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./asconfig.json"
3+
}

Diff for: tests/asconfig/cyclical/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"private": true,
3+
"scripts": {
4+
"test": "node ../index.js"
5+
}
6+
}

Diff for: tests/asconfig/entry-points/asconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"entries": ["assembly/globals.ts"]
3+
}

Diff for: tests/asconfig/entry-points/assembly/globals.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// @ts-ignore: decorator
2+
@global const answerToLife = 42;
3+
assert(answerToLife);

Diff for: tests/asconfig/entry-points/assembly/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
assert(answerToLife == 42);

Diff for: tests/asconfig/entry-points/nested/asconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../asconfig.json"
3+
}

Diff for: tests/asconfig/entry-points/nested/assembly/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
assert(answerToLife == 42);

Diff for: tests/asconfig/entry-points/nested/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"private": true,
3+
"scripts": {
4+
"test": "node ../../index.js"
5+
}
6+
}

0 commit comments

Comments
 (0)