Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a name="using-cdn-get-started"></a>
```html
<bynix>
Expand Down
2 changes: 1 addition & 1 deletion docs/markdown/command.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ By default, input files use `.bys`, `.bynixscript`, or `.mbs`. Paths are relativ

Use `bynix --help` for all commands, `bynix <command> --help` for command-specific help, and `bynix --version` for the version. The earlier option forms `bynix -r <file>`, `bynix -c <file>`, `bynix -p <file>`, and `bynix -d <file>` 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.
18 changes: 2 additions & 16 deletions scripts/build.cjs
Original file line number Diff line number Diff line change
@@ -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')))
}
}

Expand Down
23 changes: 23 additions & 0 deletions scripts/compile.cjs
Original file line number Diff line number Diff line change
@@ -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 }
6 changes: 3 additions & 3 deletions src/bsd.bs
Original file line number Diff line number Diff line change
@@ -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 }
6 changes: 3 additions & 3 deletions src/bsp.bs
Original file line number Diff line number Diff line change
@@ -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 }
11 changes: 4 additions & 7 deletions src/bsr.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
9 changes: 4 additions & 5 deletions src/bst.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
45 changes: 28 additions & 17 deletions src/index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,37 @@ program.option('-r, --run <file>', 'Run a file')
program.option('-c, --compile <file>', 'Compile a file')
program.option('-p, --print <file>', 'Print translated JavaScript')
program.option('-d, --delete <file>', '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
59 changes: 27 additions & 32 deletions src/translate.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
24 changes: 24 additions & 0 deletions tests/cli.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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 <file>/)
for (const command of ['run', 'compile', 'print', 'delete']) {
Expand Down Expand Up @@ -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')))
})
Loading