From 7be246a400b3b3d96ea095dd5747c3afd2f89651 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Tue, 19 Jan 2021 15:13:42 +0100 Subject: [PATCH] Module activation; batch compilation --- packages/compiler/src/compiler/codegen.ts | 96 +++++++++++--- packages/compiler/src/compiler/grammar.ts | 17 ++- packages/compiler/src/syntax/matcher.ts | 8 ++ packages/compiler/src/syntax/template.ts | 2 +- packages/core/src/runtime/dataspace.ts | 35 ++++- packages/core/src/runtime/ground.ts | 18 +-- .../syndicatec/examples/javascript/.gitignore | 1 + .../syndicatec/examples/javascript/index.html | 13 ++ .../examples/javascript/package.json | 11 +- .../examples/javascript/rollup.config.js | 15 +++ .../{index.syndicate.js => src/box.js} | 15 +-- .../examples/javascript/src/client.js | 26 ++++ .../examples/javascript/src/index.js | 27 ++++ .../examples/javascript/src/protocol.js | 22 +++ packages/syndicatec/package.json | 1 + packages/syndicatec/src/cli.ts | 125 +++++++++++++----- 16 files changed, 351 insertions(+), 81 deletions(-) create mode 100644 packages/syndicatec/examples/javascript/index.html create mode 100644 packages/syndicatec/examples/javascript/rollup.config.js rename packages/syndicatec/examples/javascript/{index.syndicate.js => src/box.js} (75%) mode change 100755 => 100644 create mode 100644 packages/syndicatec/examples/javascript/src/client.js create mode 100644 packages/syndicatec/examples/javascript/src/index.js create mode 100644 packages/syndicatec/examples/javascript/src/protocol.js diff --git a/packages/compiler/src/compiler/codegen.ts b/packages/compiler/src/compiler/codegen.ts index 8d0c40d..b5688b8 100644 --- a/packages/compiler/src/compiler/codegen.ts +++ b/packages/compiler/src/compiler/codegen.ts @@ -1,8 +1,8 @@ import { - isToken, isTokenType, replace, commaJoin, startPos, fixPos, + isToken, isTokenType, replace, commaJoin, startPos, fixPos, joinItems, Items, Pattern, Templates, Substitution, TokenType, - SourceMap, StringScanner, LaxReader, CodeWriter, TemplateFunction, + SourceMap, StringScanner, LaxReader, CodeWriter, TemplateFunction, Token, } from '../syntax/index.js'; import { FacetAction, Statement, @@ -21,6 +21,9 @@ import { reactStatement, bootStatement, stopStatement, + Identifier, + activationImport, + ActivationImport, } from './grammar.js'; import { BootProc, @@ -54,7 +57,18 @@ function receiverFor(s: FacetAction): Substitution { return (s.implicitFacet) ? 'thisFacet.' : '.'; } -export function expand(tree: Items, moduleType: ModuleType): Items { +export interface ActivationRecord { + activation: ActivationImport; + activationScriptId: Identifier; +} + +export interface ExpansionContext { + moduleType: ModuleType; + activationRecords: Array; + hasBootProc: boolean; +} + +export function expand(tree: Items, ctx: ExpansionContext): Items { const macro = new Templates(); function terminalWrap(t: TemplateFunction, isTerminal: boolean, body: Statement): Statement { @@ -73,7 +87,7 @@ export function expand(tree: Items, moduleType: ModuleType): Items { x(p, (v, t) => t`${receiverFor(v)}${f(v, t)}`); } - const walk = (tree: Items): Items => expand(tree, moduleType); + const walk = (tree: Items): Items => expand(tree, ctx); const maybeWalk = (tree?: Items) : Items | undefined => (tree === void 0) ? tree : walk(tree); xf(duringStatement, (s, t) => { @@ -173,14 +187,29 @@ export function expand(tree: Items, moduleType: ModuleType): Items { xf(reactStatement, (s, t) => t`addChildFacet(function (thisFacet) {${walk(s.body)}});`); + x(activationImport, (s, t) => { + const activationScriptId: Token = { + start: s.activationKeyword.start, + end: s.activationKeyword.end, + text: `__SYNDICATE__activationScript${'' + ctx.activationRecords.length}`, + type: TokenType.ATOM + }; + ctx.activationRecords.push({ activation: s, activationScriptId }); + return []; + }), + x(bootStatement, (s, t) => { - switch (moduleType) { + ctx.hasBootProc = true; + const activationStatements = ctx.activationRecords.map(({ activationScriptId: id }) => + t`thisFacet.activate(${[id]}); `); + const body = t`${joinItems(activationStatements)}${walk(s)}`; + switch (ctx.moduleType) { case 'es6': - return t`export function ${BootProc}(thisFacet) {${walk(s)}}`; + return t`export function ${BootProc}(thisFacet) {${body}}`; case 'require': - return t`module.exports.${BootProc} = function (thisFacet) {${walk(s)}};`; + return t`module.exports.${BootProc} = function (thisFacet) {${body}};`; case 'global': - return t`function ${BootProc}(thisFacet) {${walk(s)}}`; + return t`function ${BootProc}(thisFacet) {${body}}`; } }); @@ -195,34 +224,67 @@ export function compile(options: CompileOptions): CompilerOutput { const moduleType = options.module ?? 'es6'; const start = startPos(inputFilename); - const scanner = new StringScanner(start, source); const reader = new LaxReader(scanner); let tree = stripShebang(reader.readToEnd()); + const end = tree.length > 0 ? tree[tree.length - 1].end : start; + let macro = new Templates(); - const end = tree.length > 0 ? tree[tree.length - 1].end : start; + const ctx: ExpansionContext = { + moduleType, + activationRecords: [], + hasBootProc: false, + } + + tree = expand(tree, ctx); + + const ts = macro.template(fixPos(start)); + const te = macro.template(fixPos(end)); + + if (ctx.hasBootProc) { + let bp; + switch (moduleType) { + case 'es6': + case 'global': + bp = BootProc; + break; + case 'require': + bp = te`module.exports.${BootProc}`; + break; + } + tree = te`${tree}\nif (typeof module !== 'undefined' && ((typeof require === 'undefined' ? {main: void 0} : require).main === module)) __SYNDICATE__.bootModule(${bp});`; + } + + const activationImports = ctx.activationRecords.map(r => { + const a = r.activation; + const t = macro.template(a.activationKeyword.start); + switch (a.target.type) { + case 'import': + return t`import { ${BootProc} as ${[r.activationScriptId]} } from ${[a.target.moduleName]};\n`; + case 'expr': + return t`const ${[r.activationScriptId]} = (${a.target.moduleExpr}).${BootProc};\n`; + } + }); + tree = ts`${joinItems(activationImports)}${tree}`; { const runtime = options.runtime ?? '@syndicate-lang/core'; - const t = macro.template(fixPos(start)); switch (moduleType) { case 'es6': - tree = t`import * as __SYNDICATE__ from ${JSON.stringify(runtime)};\n${tree}`; + tree = ts`import * as __SYNDICATE__ from ${JSON.stringify(runtime)};\n${tree}`; break; case 'require': - tree = t`const __SYNDICATE__ = require(${JSON.stringify(runtime)});\n${tree}`; + tree = ts`const __SYNDICATE__ = require(${JSON.stringify(runtime)});\n${tree}`; break; case 'global': - tree = t`const __SYNDICATE__ = ${runtime};\n${tree}`; + tree = ts`const __SYNDICATE__ = ${runtime};\n${tree}`; break; } } - tree = macro.template(fixPos(end))`${tree}\nif (typeof module !== 'undefined' && ((typeof require === 'undefined' ? {main: void 0} : require).main === module)) __SYNDICATE__.bootModule(${BootProc});`; - const cw = new CodeWriter(inputFilename); - cw.emit(expand(tree, moduleType)); + cw.emit(tree); return { text: cw.text, diff --git a/packages/compiler/src/compiler/grammar.ts b/packages/compiler/src/compiler/grammar.ts index 486cb8b..dace1c5 100644 --- a/packages/compiler/src/compiler/grammar.ts +++ b/packages/compiler/src/compiler/grammar.ts @@ -6,7 +6,7 @@ import { scope, bind, seq, alt, upTo, atom, atomString, group, exec, repeat, option, withoutSpace, map, mapm, rest, discard, - value, succeed, fail, separatedBy, anything, not, + value, succeed, fail, separatedBy, anything, not, follows, } from '../syntax/index.js'; import * as Matcher from '../syntax/matcher.js'; import { Path, Skeleton } from './internals.js'; @@ -244,6 +244,21 @@ export const bootStatement: Pattern = // Principal: Facet export const stopStatement = blockFacetAction(atom('stop')); +export interface ActivationImport { + activationKeyword: Identifier; + target: { type: 'import', moduleName: Token } | { type: 'expr', moduleExpr: Expr }; +} + +// Principal: none +export const activationImport: Pattern = + scope(o => seq(bind(o, 'activationKeyword', atom('activate')), + follows(alt(seq(atom('import'), + upTo(seq( + map(atom(void 0, { tokenType: TokenType.STRING }), + n => o.target = { type: 'import', moduleName: n }), + statementBoundary))), + map(expr(), e => o.target = { type: 'expr', moduleExpr: e }))))); + //--------------------------------------------------------------------------- // Syntax of patterns over Value, used in endpoints diff --git a/packages/compiler/src/syntax/matcher.ts b/packages/compiler/src/syntax/matcher.ts index 1b7d531..ee3efc3 100644 --- a/packages/compiler/src/syntax/matcher.ts +++ b/packages/compiler/src/syntax/matcher.ts @@ -56,6 +56,14 @@ export function not(p: Pattern, v: T): Pattern { return i => p(i) === null ? [v, i] : null; } +export function follows(p: Pattern): Pattern { + return i => { + const r = p(i); + if (r === null) return null; + return [r[0], i]; + }; +} + export function seq(... patterns: Pattern[]): Pattern { return i => { for (const p of patterns) { diff --git a/packages/compiler/src/syntax/template.ts b/packages/compiler/src/syntax/template.ts index bd406bf..8031e19 100644 --- a/packages/compiler/src/syntax/template.ts +++ b/packages/compiler/src/syntax/template.ts @@ -53,7 +53,7 @@ export class Templates { } } -export function joinItems(itemss: Items[], separator0: Substitution): Items { +export function joinItems(itemss: Items[], separator0: Substitution = ''): Items { if (itemss.length === 0) return []; const separator = toItems(separator0, startPos(null)); const acc = itemss[0]; diff --git a/packages/core/src/runtime/dataspace.ts b/packages/core/src/runtime/dataspace.ts index 1a12694..c63fa49 100644 --- a/packages/core/src/runtime/dataspace.ts +++ b/packages/core/src/runtime/dataspace.ts @@ -68,6 +68,8 @@ export function _canonicalizeDataflowDependent(i: DataflowDependent): string { return '' + i.id; } +export type ActivationScript = Script; + export abstract class Dataspace { nextId: ActorId = 0; index = new Skeleton.Index(); @@ -77,6 +79,7 @@ export abstract class Dataspace { runnable: Array = []; pendingTurns: Array; actors: IdentityMap = new IdentityMap(); + activations: IdentitySet = new IdentitySet(); constructor(bootProc: Script) { this.pendingTurns = [new Turn(null, [new Spawn(null, bootProc, new Set())])]; @@ -390,6 +393,23 @@ class DeferredTurn extends Action { } } +class Activation extends Action { + readonly script: ActivationScript; + readonly name: any; + + constructor(script: ActivationScript, name: any) { + super(); + this.script = script; + this.name = name; + } + + perform(ds: Dataspace, ac: Actor | null): void { + if (ds.activations.has(this.script)) return; + ds.activations.add(this.script); + ds.addActor(this.name, rootFacet => rootFacet.addStartScript(this.script), new Set(), ac); + } +} + export class Turn { readonly actor: Actor | null; readonly actions: Array; @@ -623,9 +643,9 @@ export class Facet { } } - ensureNonFacetSetup(what: string, keyword: string) { + ensureNonFacetSetup(what: string) { if (!this.inScript) { - throw new Error(`Cannot ${what} during facet setup; are you missing \`${keyword} { ... }\`?`); + throw new Error(`Cannot ${what} during facet setup; are you missing \`on start { ... }\`?`); } } @@ -635,7 +655,7 @@ export class Facet { } send(body: any) { - this.ensureNonFacetSetup('`send`', 'on start'); + this.ensureNonFacetSetup('`send`'); this.enqueueScriptAction(new Message(body)); } @@ -645,15 +665,20 @@ export class Facet { } spawn(name: any, bootProc: Script, initialAssertions?: Set) { - this.ensureNonFacetSetup('`spawn`', 'on start'); + this.ensureNonFacetSetup('`spawn`'); this.enqueueScriptAction(new Spawn(name, bootProc, initialAssertions)); } deferTurn(continuation: Script) { - this.ensureNonFacetSetup('`deferTurn`', 'on start'); + this.ensureNonFacetSetup('`deferTurn`'); this.enqueueScriptAction(new DeferredTurn(this.wrap(continuation))); } + activate(script: ActivationScript, name?: any) { + this.ensureNonFacetSetup('`activate`'); + this.enqueueScriptAction(new Activation(script, name ?? null)); + } + scheduleScript(script: Script, priority?: Priority) { this.actor.scheduleTask(this.wrap(script), priority); } diff --git a/packages/core/src/runtime/ground.ts b/packages/core/src/runtime/ground.ts index d22d59b..b7f2249 100644 --- a/packages/core/src/runtime/ground.ts +++ b/packages/core/src/runtime/ground.ts @@ -16,7 +16,7 @@ // along with this program. If not, see . //--------------------------------------------------------------------------- -import { Dataspace, Script } from './dataspace.js'; +import { ActivationScript, Dataspace } from './dataspace.js'; export type StopHandler = (ds: D) => void; @@ -33,7 +33,7 @@ export class Ground extends Dataspace { stopHandlers: Array> = []; backgroundTaskCount = 0; - constructor(bootProc: Script) { + constructor(bootProc: ActivationScript) { super(function (rootFacet) { rootFacet.addStartScript(bootProc); }); if (typeof window !== 'undefined') { window._ground = this; @@ -118,12 +118,14 @@ export class Ground extends Dataspace { // if (k) k(g); // } -export function bootModule(bootProc: Script): Ground { +export function bootModule(bootProc: ActivationScript): Ground { const g = new Ground(bootProc); - if (typeof document !== 'undefined') { - document.addEventListener('DOMContentLoaded', () => g.start()); - } else { - g.start(); - } + Ground.laterCall(() => { + if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', () => g.start()); + } else { + g.start(); + } + }); return g; } diff --git a/packages/syndicatec/examples/javascript/.gitignore b/packages/syndicatec/examples/javascript/.gitignore index 012a3cd..2a6b3ff 100644 --- a/packages/syndicatec/examples/javascript/.gitignore +++ b/packages/syndicatec/examples/javascript/.gitignore @@ -1 +1,2 @@ index.js +index.js.map diff --git a/packages/syndicatec/examples/javascript/index.html b/packages/syndicatec/examples/javascript/index.html new file mode 100644 index 0000000..67d20c4 --- /dev/null +++ b/packages/syndicatec/examples/javascript/index.html @@ -0,0 +1,13 @@ + + + + Demo + + +

Look in the JavaScript console for output.

+
+
+ + diff --git a/packages/syndicatec/examples/javascript/package.json b/packages/syndicatec/examples/javascript/package.json index 768598a..de1fdbb 100644 --- a/packages/syndicatec/examples/javascript/package.json +++ b/packages/syndicatec/examples/javascript/package.json @@ -4,9 +4,10 @@ "description": "Simple syndicatec example", "main": "index.js", "scripts": { - "prepare": "npm run compile", - "compile": "npx syndicatec -o index.js --module global --runtime Syndicate index.syndicate.js", - "clean": "rm -f index.js" + "prepare": "npm run compile && npm run rollup", + "compile": "npx syndicatec -d lib -b src 'src/**/*.js'", + "rollup": "npx rollup -c", + "clean": "rm -rf lib/ index.js index.js.map" }, "author": "Tony Garnock-Jones ", "license": "GPL-3.0+", @@ -14,6 +15,8 @@ "@syndicate-lang/core": "file:../../../core" }, "devDependencies": { - "@syndicate-lang/syndicatec": "file:../.." + "@syndicate-lang/syndicatec": "file:../..", + "rollup": "^2.37.0", + "rollup-plugin-sourcemaps": "^0.6.3" } } diff --git a/packages/syndicatec/examples/javascript/rollup.config.js b/packages/syndicatec/examples/javascript/rollup.config.js new file mode 100644 index 0000000..1924b9f --- /dev/null +++ b/packages/syndicatec/examples/javascript/rollup.config.js @@ -0,0 +1,15 @@ +import sourcemaps from 'rollup-plugin-sourcemaps'; + +export default { + input: 'lib/index.js', + plugins: [sourcemaps()], + output: { + file: 'index.js', + format: 'umd', + name: 'Main', + sourcemap: true, + globals: { + '@syndicate-lang/core': 'Syndicate', + }, + }, +}; diff --git a/packages/syndicatec/examples/javascript/index.syndicate.js b/packages/syndicatec/examples/javascript/src/box.js old mode 100755 new mode 100644 similarity index 75% rename from packages/syndicatec/examples/javascript/index.syndicate.js rename to packages/syndicatec/examples/javascript/src/box.js index 81fa28e..5b6603e --- a/packages/syndicatec/examples/javascript/index.syndicate.js +++ b/packages/syndicatec/examples/javascript/src/box.js @@ -16,12 +16,7 @@ // along with this program. If not, see . //--------------------------------------------------------------------------- -assertion type BoxState(value); -message type SetBox(newValue); - -const N = 100000; - -console.time('box-and-client-' + N.toString()); +import { BoxState, SetBox, N } from './protocol.js'; boot { spawn named 'box' { @@ -31,12 +26,4 @@ boot { console.log('terminated box root facet'); on message SetBox($v) => this.value = v; } - - spawn named 'client' { - on asserted BoxState($v) => send message SetBox(v + 1); - on retracted BoxState(_) => console.log('box gone'); - } - - thisFacet.actor.dataspace.addStopHandler(() => - console.timeEnd('box-and-client-' + N.toString())); } diff --git a/packages/syndicatec/examples/javascript/src/client.js b/packages/syndicatec/examples/javascript/src/client.js new file mode 100644 index 0000000..da07a67 --- /dev/null +++ b/packages/syndicatec/examples/javascript/src/client.js @@ -0,0 +1,26 @@ +//--------------------------------------------------------------------------- +// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. +// Copyright (C) 2016-2021 Tony Garnock-Jones +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +//--------------------------------------------------------------------------- + +import { BoxState, SetBox } from './protocol.js'; + +boot { + spawn named 'client' { + on asserted BoxState($v) => send message SetBox(v + 1); + on retracted BoxState(_) => console.log('box gone'); + } +} diff --git a/packages/syndicatec/examples/javascript/src/index.js b/packages/syndicatec/examples/javascript/src/index.js new file mode 100644 index 0000000..15dfedd --- /dev/null +++ b/packages/syndicatec/examples/javascript/src/index.js @@ -0,0 +1,27 @@ +//--------------------------------------------------------------------------- +// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. +// Copyright (C) 2016-2021 Tony Garnock-Jones +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +//--------------------------------------------------------------------------- + +import { N } from './protocol.js'; +activate import './box.js'; +activate import './client.js'; + +console.time('box-and-client-' + N.toString()); +boot { + thisFacet.actor.dataspace.addStopHandler(() => + console.timeEnd('box-and-client-' + N.toString())); +} diff --git a/packages/syndicatec/examples/javascript/src/protocol.js b/packages/syndicatec/examples/javascript/src/protocol.js new file mode 100644 index 0000000..ae85661 --- /dev/null +++ b/packages/syndicatec/examples/javascript/src/protocol.js @@ -0,0 +1,22 @@ +//--------------------------------------------------------------------------- +// @syndicate-lang/core, an implementation of Syndicate dataspaces for JS. +// Copyright (C) 2016-2021 Tony Garnock-Jones +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +//--------------------------------------------------------------------------- + +export assertion type BoxState(value); +export message type SetBox(newValue); + +export const N = 100000; diff --git a/packages/syndicatec/package.json b/packages/syndicatec/package.json index f450e6b..15b364f 100644 --- a/packages/syndicatec/package.json +++ b/packages/syndicatec/package.json @@ -16,6 +16,7 @@ "dependencies": { "@syndicate-lang/compiler": "file:../compiler", "@syndicate-lang/core": "file:../core", + "glob": "^7.1.6", "yargs": "^16.2.0" }, "bin": { diff --git a/packages/syndicatec/src/cli.ts b/packages/syndicatec/src/cli.ts index 3a1d858..3ba6392 100644 --- a/packages/syndicatec/src/cli.ts +++ b/packages/syndicatec/src/cli.ts @@ -2,6 +2,9 @@ import yargs from 'yargs/yargs'; import { Argv } from 'yargs'; import fs from 'fs'; +import path from 'path'; +import { glob } from 'glob'; + import { compile } from '@syndicate-lang/compiler'; export type ModuleChoice = 'es6' | 'require' | 'global'; @@ -9,7 +12,9 @@ const moduleChoices: ReadonlyArray = ['es6', 'require', 'global']; export type CommandLineArguments = { input: string | undefined; - output: string | undefined; + outputDirectory?: string | undefined; + rootDirectory?: string; + rename: string | undefined; map: boolean; mapExtension?: string; runtime: string; @@ -22,19 +27,62 @@ function checkModuleChoice(t: T & { module: string }): T & { module: ModuleCh throw new Error("Illegal --module argument: " + t.module); } +function makeRenamer(outputDir: string, + rootDir: string, + renamePattern: string | undefined): (f: string) => string +{ + const rewrites: Array<(f: string) => (string | null)> = + (renamePattern === void 0 ? [] : renamePattern.split(/,/)).map(p => { + const [ from, to ] = p.split(/:/); + let mFrom = /([^%]*)%([^%]*)/.exec(from); + let mTo = /([^%]*)%([^%]*)/.exec(to); + if (mFrom === null && mTo === null) { + return f => (f === from) ? to : null; + } else if (mFrom === null || mTo === null) { + throw new Error(`Invalid --rename pattern: ${JSON.stringify(p)}`); + } else { + const [fh, ft] = mFrom.slice(1); + const [th, tt] = mTo.slice(1); + return f => + (f.startsWith(fh) && f.endsWith(ft)) + ? th + f.substring(fh.length, f.length - ft.length) + tt + : null; + } + }); + const relocate = (f: string) => path.join(outputDir, path.relative(rootDir, f)); + return f => { + for (const rewrite of rewrites) { + const t = rewrite(f); + if (t !== null) return relocate(t); + } + return relocate(f); + }; +} + export function main(argv: string[]) { const options: CommandLineArguments = checkModuleChoice(yargs(argv) .command('$0 [input]', - 'Compile a single file', + 'Compile away Syndicate extensions', yargs => yargs .positional('input', { type: 'string', - description: 'Input filename', + description: 'Input filename or glob (stdin if omitted)', }) - .option('output', { - alias: 'o', + .option('root-directory', { + alias: 'b', type: 'string', - description: 'Output filename (stdout if omitted)', + description: 'Root directory for input files', + default: '.', + }) + .option('output-directory', { + alias: 'd', + type: 'string', + description: 'Output directory (if omitted: stdout if stdin as input, else cwd)', + }) + .option('rename', { + type: 'string', + description: 'Rewrite input filenames', + default: '%.syndicate.js:%.js,%.syndicate.ts:%.ts', }) .option('map', { type: 'boolean', @@ -59,37 +107,52 @@ export function main(argv: string[]) { argv => argv) .argv); - const inputFilename = options.input ?? '/dev/stdin'; - const source = fs.readFileSync(inputFilename, 'utf-8'); + const rename = makeRenamer(options.outputDirectory ?? '', + options.rootDirectory ?? '.', + options.rename); - const { text, map } = compile({ - source, - name: inputFilename, - runtime: options.runtime, - module: options.module, - }); - map.sourcesContent = [source]; + const STDIN = '/dev/stdin'; - function mapDataURL() { - const mapData = Buffer.from(JSON.stringify(map)).toString('base64') - return `data:application/json;base64,${mapData}`; - } + const inputGlob = options.input ?? STDIN; + const inputFilenames = glob.sync(inputGlob); + + for (const inputFilename of inputFilenames) { + const outputFilename = + (inputFilename === STDIN) ? '/dev/stdout' : + (inputFilename[0] === '/') ? (() => { throw new Error("Absolute input paths are not supported"); })() : + rename(inputFilename); + + if (inputFilenames.indexOf(outputFilename) !== -1) { + throw new Error(`Output from ${JSON.stringify(inputFilename)} would trample on existing input file ${JSON.stringify(outputFilename)}`); + } + + const source = fs.readFileSync(inputFilename, 'utf-8'); + + const { text, map } = compile({ + source, + name: inputFilename, + runtime: options.runtime, + module: options.module, + }); + map.sourcesContent = [source]; + + function mapDataURL() { + const mapData = Buffer.from(JSON.stringify(map)).toString('base64') + return `data:application/json;base64,${mapData}`; + } + + if (inputFilename !== STDIN) { + fs.mkdirSync(path.dirname(outputFilename), { recursive: true }); + } - if (options.output !== void 0) { if (!options.map) { - fs.writeFileSync(options.output, text); - } else if (options.mapExtension) { - const mapFilename = options.output + options.mapExtension; - fs.writeFileSync(options.output, text + `\n//# sourceMappingURL=${mapFilename}`); + fs.writeFileSync(outputFilename, text); + } else if (options.mapExtension && inputFilename !== STDIN) { + const mapFilename = outputFilename + options.mapExtension; + fs.writeFileSync(outputFilename, text + `\n//# sourceMappingURL=${mapFilename}`); fs.writeFileSync(mapFilename, JSON.stringify(map)); } else { - fs.writeFileSync(options.output, text + `\n//# sourceMappingURL=${mapDataURL()}`); - } - } else { - if (!options.map) { - console.log(text); - } else { - console.log(text + `\n//# sourceMappingURL=${mapDataURL()}`); + fs.writeFileSync(outputFilename, text + `\n//# sourceMappingURL=${mapDataURL()}`); } } }