diff --git a/README.md b/README.md index 73b58c7..82dc81a 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ npm ci npm run build npm test ``` +The build bootstraps the `.bs` CLI sources with the BynixScript compiler from the +`bynixscript` development dependency, then bundles the compiled JavaScript with +esbuild into `dist/index.min.cjs`. `npm test` also checks that every CLI source +compiles through the same compiler path. ### Using CDN ```html diff --git a/docs/markdown/command.md b/docs/markdown/command.md index 9f1e796..2e9ded2 100644 --- a/docs/markdown/command.md +++ b/docs/markdown/command.md @@ -13,4 +13,4 @@ By default, input files use `.bys`, `.bynixscript`, or `.mbs`. Paths are relativ Use `bynix --help` for all commands, `bynix --help` for command-specific help, and `bynix --version` for the version. The earlier option forms `bynix -r `, `bynix -c `, `bynix -p `, and `bynix -d ` remain available. -For development, run `npm ci` followed by `npm run build` to produce `dist/index.min.cjs`. `npm test` builds it and runs the CLI tests. +For development, run `npm ci` followed by `npm run build`. The build uses the `bynixscript` development dependency as a bootstrap compiler to translate the `.bs` CLI sources through BynixScript's normal parser and replacement passes, then bundles the resulting JavaScript with esbuild into `dist/index.min.cjs`. `npm test` builds the executable, checks that all CLI sources compile through that same path, and runs the command tests. diff --git a/scripts/build.cjs b/scripts/build.cjs index bfeb90f..a411f77 100644 --- a/scripts/build.cjs +++ b/scripts/build.cjs @@ -1,33 +1,19 @@ const fs = require('node:fs') const path = require('node:path') const { build } = require('esbuild') -const { parseCode, addSemicolons } = require('../js/parser.js') -const { funcReplace } = require('bynixscript/dist/parser/funcDecl.js') +const { compileSource } = require('./compile.cjs') const root = path.resolve(__dirname, '..') const temporary = fs.mkdtempSync(path.join(root, '.bynix-build-')) const source = path.join(root, 'src') const output = path.join(root, 'dist', 'index.min.cjs') -function translateParser(sourceCode) { - return funcReplace(addSemicolons(parseCode(sourceCode))).replace(/\.change\(/g, '.replace(') -} - async function main() { try { for (const name of fs.readdirSync(source, { withFileTypes: true })) { if (name.isFile() && name.name.endsWith('.bs')) { fs.writeFileSync(path.join(temporary, name.name.replace(/\.bs$/, '.js')), - fs.readFileSync(path.join(source, name.name), 'utf8')) - } - } - - const parserOutput = path.join(temporary, 'parser') - fs.mkdirSync(parserOutput) - for (const name of fs.readdirSync(path.join(source, 'parser'))) { - if (name.endsWith('.bs')) { - const code = fs.readFileSync(path.join(source, 'parser', name), 'utf8') - fs.writeFileSync(path.join(parserOutput, name.replace(/\.bs$/, '.js')), translateParser(code)) + compileSource(fs.readFileSync(path.join(source, name.name), 'utf8'))) } } diff --git a/scripts/compile.cjs b/scripts/compile.cjs new file mode 100644 index 0000000..853c895 --- /dev/null +++ b/scripts/compile.cjs @@ -0,0 +1,23 @@ +const { parsing, parsingMsg, parseCode, addSemicolons } = require('bynixscript/js/parser.js') +const replacements = require('bynixscript/dist/parser/parsingDecl.js') + +const passes = [ + 'flowReplace', 'utilityReplace', 'funcReplace', 'condReplace', + 'forEachReplace', 'reassignReplace', 'assignReplace', 'logReplace', + 'interactReplace', 'mathReplace', 'commentReplace', 'asyncReplace', + 'forReplace', 'convReplace', 'checkReplace', 'oopReplace', + 'excepReplace', 'domReplace' +] + +function compileSource(source) { + let code = addSemicolons(parseCode(source)) + if (parsing(code) === false) { + throw new Error(parsingMsg.message) + } + for (const pass of passes) { + code = replacements[pass](code) + } + return code +} + +module.exports = { compileSource } diff --git a/src/bsd.bs b/src/bsd.bs index 4fa860c..3542db6 100644 --- a/src/bsd.bs +++ b/src/bsd.bs @@ -1,10 +1,10 @@ const fs = require('node:fs') const { resolveFile } = require('./translate.js') -function bsdCommand(file) { +func bsdCommand(file): const filePath = resolveFile(file) fs.unlinkSync(filePath) - console.log(`Deleted ${file}`) -} + print(`Deleted ${file}`) +end module.exports = { bsdCommand } diff --git a/src/bsp.bs b/src/bsp.bs index ebd2903..d17bcda 100644 --- a/src/bsp.bs +++ b/src/bsp.bs @@ -1,8 +1,8 @@ const { readSource, translate } = require('./translate.js') -function bspCommand(file) { +func bspCommand(file): const { source } = readSource(file) - console.log(translate(source)) -} + print(translate(source)) +end module.exports = { bspCommand } diff --git a/src/bsr.bs b/src/bsr.bs index a367e2d..2aaae85 100644 --- a/src/bsr.bs +++ b/src/bsr.bs @@ -2,14 +2,11 @@ const vm = require('node:vm') const { createRequire } = require('node:module') const { readSource, translate } = require('./translate.js') -function bsrCommand(file) { +func bsrCommand(file): const { source, filePath } = readSource(file) const code = translate(source) - vm.runInNewContext(code, { - console, - process, - require: createRequire(filePath) - }, { filename: filePath }) -} + const sandbox = { console, process, require: createRequire(filePath) } + vm.runInNewContext(code, sandbox, { filename: filePath }) +end module.exports = { bsrCommand } diff --git a/src/bst.bs b/src/bst.bs index 798e0db..67d01db 100644 --- a/src/bst.bs +++ b/src/bst.bs @@ -2,14 +2,13 @@ const fs = require('node:fs') const path = require('node:path') const { readSource, translate, extensions } = require('./translate.js') -function bstCommand(file) { +func bstCommand(file): const { source, filePath } = readSource(file) const code = translate(source) const extension = path.extname(filePath) - const output = filePath.slice(0, -extension.length) + - (extension === extensions.module ? '.mjs' : '.js') + const output = filePath.slice(0, -extension.length) + (extension === extensions.module ? '.mjs' : '.js') fs.writeFileSync(output, code, 'utf8') - console.log(`Compiled ${file} to ${output}`) -} + print(`Compiled ${file} to ${output}`) +end module.exports = { bstCommand } diff --git a/src/index.bs b/src/index.bs index 0aed073..e41a652 100644 --- a/src/index.bs +++ b/src/index.bs @@ -20,26 +20,37 @@ program.option('-r, --run ', 'Run a file') program.option('-c, --compile ', 'Compile a file') program.option('-p, --print ', 'Print translated JavaScript') program.option('-d, --delete ', 'Delete a file') -program.action(() => {}) +func noOp(): +end +program.action(noOp) -try { +handle: program.parse() const options = program.opts() - const selected = [ - ['run', bsrCommand], ['compile', bstCommand], - ['print', bspCommand], ['delete', bsdCommand] - ].filter(([name]) => options[name] !== undefined) + const selected = [] + for const name of ['run', 'compile', 'print', 'delete']: + if options[name] !== undefined: + selected.push(name) + end + end - if (selected.length > 1) { - throw new Error('Specify only one command at a time.') - } - if (selected.length === 1) { - const [name, command] = selected[0] - command(options[name]) - } else if (process.argv.length === 2) { + if selected.length > 1: + throw new Err('Specify only one command at a time.') + elif selected.length === 1: + const name = selected[0] + if name === 'run': + bsrCommand(options[name]) + elif name === 'compile': + bstCommand(options[name]) + elif name === 'print': + bspCommand(options[name]) + else: + bsdCommand(options[name]) + end + elif process.argv.length === 2: program.error('A command is required.') - } -} catch (error) { - console.error(`Error: ${error.message}`) + end +recovery (error): + process.stderr.write(`Error: ${error.message}\n`) process.exitCode = 1 -} +end diff --git a/src/translate.bs b/src/translate.bs index 9451d04..c2b7f65 100644 --- a/src/translate.bs +++ b/src/translate.bs @@ -2,45 +2,40 @@ const fs = require('node:fs') const path = require('node:path') const { extensions } = require('../js/bsc.js') const { parsing, parsingMsg, parseCode, addSemicolons } = require('../js/parser.js') -const replacements = require('./parser/parsingDecl.js') +const replacements = require('bynixscript/dist/parser/parsingDecl.js') -function resolveFile(file) { - if (typeof file !== 'string' || file.trim() === '') { - throw new Error('A file path is required.') - } - if (![extensions.primary, extensions.secondary, extensions.module].some(extension => file.endsWith(extension))) { - throw new Error(`Unsupported file extension: ${file}. Use ${extensions.primary}, ${extensions.secondary}, or ${extensions.module}.`) - } +func resolveFile(file): + if typeof file !== 'string' || file.trim() === '': + throw new Err('A file path is required.') + end + if !file.is_end(extensions.primary) && !file.is_end(extensions.secondary) && !file.is_end(extensions.module): + throw new Err(`Unsupported file extension: ${file}. Use ${extensions.primary}, ${extensions.secondary}, or ${extensions.module}.`) + end const filePath = path.resolve(file) - if (!fs.existsSync(filePath)) { - throw new Error(`File not found: ${file}`) - } - if (!fs.statSync(filePath).isFile()) { - throw new Error(`Not a file: ${file}`) - } + if !fs.existsSync(filePath): + throw new Err(`File not found: ${file}`) + end + if !fs.statSync(filePath).isFile(): + throw new Err(`Not a file: ${file}`) + end return filePath -} +end -function readSource(file) { +func readSource(file): const filePath = resolveFile(file) return { filePath, source: fs.readFileSync(filePath, 'utf8') } -} +end -function translate(source) { - let code = addSemicolons(parseCode(source)) - if (parsing(code) === false) { - throw new Error(parsingMsg.message) - } - for (const name of [ - 'flowReplace', 'utilityReplace', 'funcReplace', 'condReplace', - 'forEachReplace', 'reassignReplace', 'assignReplace', 'logReplace', - 'interactReplace', 'mathReplace', 'commentReplace', 'asyncReplace', - 'forReplace', 'convReplace', 'checkReplace', 'oopReplace', - 'excepReplace', 'domReplace' - ]) { - code = replacements[name](code) - } +func translate(source): + var code = addSemicolons(parseCode(source)) + if parsing(code) === false: + throw new Err(parsingMsg.message) + end + const passes = 'flowReplace utilityReplace funcReplace condReplace forEachReplace reassignReplace assignReplace logReplace interactReplace mathReplace commentReplace asyncReplace forReplace convReplace checkReplace oopReplace excepReplace domReplace'.split(' ') + for const pass of passes: + code = replacements[pass](code) + end return code -} +end module.exports = { resolveFile, readSource, translate, extensions } diff --git a/tests/cli.test.cjs b/tests/cli.test.cjs index 020f7db..8d85392 100644 --- a/tests/cli.test.cjs +++ b/tests/cli.test.cjs @@ -4,6 +4,8 @@ const fs = require('node:fs') const os = require('node:os') const path = require('node:path') const { spawnSync } = require('node:child_process') +const { Script } = require('node:vm') +const { compileSource } = require('../scripts/compile.cjs') const root = path.resolve(__dirname, '..') const binary = path.join(root, 'dist', 'index.min.cjs') @@ -28,6 +30,19 @@ test('build produces the packaged executable', () => { assert.equal(invoke(['--version']).status, 0) }) +test('every CLI source compiles through the build compiler', () => { + for (const name of ['index', 'bsr', 'bst', 'bsp', 'bsd', 'translate']) { + const source = fs.readFileSync(path.join(root, 'src', `${name}.bs`), 'utf8') + const code = compileSource(source) + assert.match(source, /\bfunc \w+\(|\bhandle:/) + assert.doesNotMatch(source, /\bfunction\s+\w+\s*\(|\btry\s*\{/) + assert.match(code, /function \w+\(|try \{/) + assert.doesNotThrow(() => new Script(code, { filename: `${name}.js` })) + } + assert.match(compileSource('func valid(value):\n print(value)\nend'), /function valid\(value\) \{/) + assert.throws(() => new Script(compileSource('func invalid():\n print("x")\n')), SyntaxError) +}) + test('global and command help succeeds', () => { assert.match(invoke(['--help']).stdout, /run /) for (const command of ['run', 'compile', 'print', 'delete']) { @@ -124,3 +139,12 @@ test('legacy file options still work', context => { assert.equal(invoke(['-d', 'legacy.bys'], directory).status, 0) assert.equal(fs.existsSync(path.join(directory, 'legacy.bys')), false) }) + +test('conflicting legacy commands fail without changing files', context => { + const directory = fixture(context) + fs.writeFileSync(path.join(directory, 'safe.bys'), 'print("safe")\n') + const result = invoke(['-p', 'safe.bys', '-d', 'safe.bys'], directory) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /Specify only one command/) + assert.ok(fs.existsSync(path.join(directory, 'safe.bys'))) +})