diff --git a/README.md b/README.md index 82dc81a..b5ddcd0 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ The build bootstraps the `.bs` CLI sources with the BynixScript compiler from th `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. +The build also bundles the shared compiler with the browser integration entry +into `dist/browser.js`; browser configuration uses the default extensions. ### Using CDN ```html diff --git a/js/browser.js b/js/browser.js index 88af974..dfb68f1 100644 --- a/js/browser.js +++ b/js/browser.js @@ -1,382 +1,29 @@ -let parsingResults; -let allowJs; -const style = document.createElement("style"); -style.textContent = ` - bynix { - display: none; - } - `; -style.classList.add("bynix-style"); -document.head.appendChild(style); -const milown = document.createElement("script"); -const ajax = document.createElement("script"); -const fontawesome = document.createElement("link"); -const brython = document.createElement("script"); -milown.src = ''; -ajax.src = 'https://cdnjs.cloudflare.com/ajax/libs/jquery/4.0.0-beta.2/jquery.min.js"'; -fontawesome.rel = "stylesheet"; -fontawesome.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css'; -brython.type = "type/javascript"; -brython.src = 'https://cdn.jsdelivr.net/npm/brython@3.9.6/brython.min.js'; -document.head.appendChild(ajax); -document.head.appendChild(fontawesome); -document.head.appendChild(milown); -document.head.appendChild(brython); -document.addEventListener("DOMContentLoaded", function() { - const bynixTags = document.querySelectorAll("bynix"); - bynixTags.forEach(tag => { - var code = ''; - const src = tag.getAttribute('src'); - const allowJsTag = tag.getAttribute('allowJs'); - - if (allowJsTag === null) { - allowJs = "false"; - } else { - allowJs = allowJsTag; - } - - // Jika src ada, lakukan fetch - if (src) { - const end = src.substring(src.lastIndexOf('.') + 1); - - if (src.endsWith(".bs") || src.endsWith(".bynixscript") || src.endsWith(".mbs")) { - fetch(src) - .then(response => response.text()) - .then(srcCode => { - code = parseCode(srcCode); - parsingResults = parsing(code); - code = addSemicolons(code); - executeCode(code); - tag.remove(); - }) - .catch(error => console.error('Error fetching BynixScript code:', error)); - } else { - console.error("Invalid extension '." + end + "'"); - } - } else { - // Jika src tidak ada, ambil kode dari textContent - code = parseCode(tag.textContent); - parsingResults = parsing(code); - code = addSemicolons(code); - executeCode(code); - tag.remove(); - } - }); -}); - -function executeCode(code) { - if (parsingResults === "false") { - console.error(`Please check your code and try again.`); - } else { - // function - code = code.replace(/func\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\):/g, (match, p1, p2) => `function ${p1}(${p2}) {`); - - // if, elif, else - code = code.replace(/elif\s+(.+?):/g, (match, p1) => `} else if (${p1}) {`); - code = code.replace(/if\s+(.+?):/g, (match, p1) => `if (${p1}) {`); - code = code.replace(/else:/g, "} else {"); - - // forEach - code = code.replace(/forEach\((.+?)\)\:/g, (match, p1) => `forEach(${p1} => {`); - code = code.replace(/forEach\((.+?)\s=>\s(.+?)\)/g, (match, p1, p2) => `forEach(${p1} => ${p2})`); - - // var, const - code = code.replace(/var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+?);/g, (match, p1, p2) => `var ${p1} = ${p2};`); - code = code.replace(/const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+?);/g, (match, p1, p2) => `const ${p1} = ${p2};`); - - // print, print.err, print.info, print.debug, print.trace, print.warn, print.assert - code = code.replace(/print(\.[a-zA-Z]+)?\((.+?)\)/g, (match, p1, p2) => { - const type = p1 || ''; - - if (type.includes(".info") || type.includes(".debug") || type.includes(".trace") || type.includes(".warn") || type.includes(".assert")) { - return `console${p1}(${p2})` - } else if (type.includes(".err")) { - return `console.error(${p2})` - } else if (match.includes("/")) { - return match; - } else { - return `console.log(${p2})` +const { compileSource } = require('../scripts/compile.cjs') + +const style = document.createElement('style') +style.textContent = 'bynix { display: none; }' +style.classList.add('bynix-style') +document.head.appendChild(style) + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('bynix').forEach(async tag => { + const sourcePath = tag.getAttribute('src') + try { + if (sourcePath && !['.bs', '.bys', '.bynixscript', '.mbs'].some(extension => sourcePath.endsWith(extension))) { + throw new Error(`Unsupported BynixScript file: ${sourcePath}`) } - }); - - // touch, ask, confirm - code = code.replace(/touch\((.+?)\)/g, (match, p1) => `alert(${p1});`); - code = code.replace(/ask\((.+?)\)/g, (match, p1) => `prompt(${p1});`); - code = code.replace(/confirm\((.+?)\)/g, (match, p1) => `confirm(${p1});`); - - // rand, roundDown, roundUp, maxOf, minOf - code = code.replace(/rand\((.+?)\)/g, (match, p1) => `Math.random(${p1})`); - code = code.replace(/roundDown\((.+?)\)/g, (match, p1) => `Math.floor(${p1})`); - code = code.replace(/roundUp\((.+?)\)/g, (match, p1) => `Math.ceil(${p1})`); - code = code.replace(/maxOf\((.+?)\)/g, (match, p1) => `Math.min(${p1})`); - code = code.replace(/minOf\((.+?)\)/g, (match, p1) => `Math.max(${p1})`); - - // getElement, getAllElement, createElement, addClass, addElement, listener - code = code.replace(/getElement\((.+?)\)/g, (match, p1) => `document.querySelector(${p1});`); - code = code.replace(/getAllElement\((.+?)\)/g, (match, p1) => `document.querySelectorAll(${p1});`); - code = code.replace(/createElement\((.+?)\)/g, (match, p1) => `document.createElement(${p1});`); - code = code.replace(/\.addClass\((.+?)\)/g, (match, p1) => `.classList.add(${p1})`); - code = code.replace(/([^\s].+)\.addElement\((.+?)\)/g, (match, p1, p2) => `document.${p1}.appendChild(${p2});`); - code = code.replace(/(.+?)\.listener\((.+?)\)\:/g, (match, p1, p2) => `${p1}.addEventListener(${p2}, () => {`) - code = code.replace(/listener\((.+?)\)\:/g, (match, p1) => `document.addEventListener(${p1}, function() {`) - - // change - code = code.replace(/(.+?)\.change\((.+?), \((.+?)\)\)\:/g, (match, p1, p2, p3) => `${p1}.replace(${p2}, (${p3}) => {`) - code = code.replace(/(.+?)\.change\((.+)\)/g, (match, p1, p2) => `${p1}.replace(${p2})`); - - // text - code = code.replace(/(.+?)\.text/g, (match, p1) => `${p1}.textContent`) - code = code.replace(/(.+?)\.text = (.+?)/g, (match, p1, p2) => `${p1}.textContent = ${p2}`); - code = code.replace(/(? `//${p1}`); - code = code.replace(/\*\*(.+?)/g, (match, p1) => `/*${p1}`); - code = code.replace(/(.+?)\*\*/g, (match, p1) => `${p1}*/`); - - // condition property - code = code.replace(/is_includes\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1) => `includes(${p1})`); - code = code.replace(/is_start\s===\s([a-zA-Z0-9"'`!?*#@$_&-+|.,:;=×÷%/]+)/g, (match, p1) => `startsWith(${p1})`); - code = code.replace(/is_end\s===\s([a-zA-Z0-9"'`!?*#@$_&-+|.,:;=×÷%/]+)/g, (match, p1) => `endsWith(${p1})`); - code = code.replace(/is_matched\s===\s"(.+?)"/g, (match, p1) => `match(${p1})`); - code = code.replace(/is_matched\s===\s'(.+?)'/g, (match, p1) => `match(${p1})`); - code = code.replace(/is_value\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1) => `value === ${p1}`); - code = code.replace(/is_design.(.+?)\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1, p2) => `style.${p1} === ${p2}`); - code = code.replace(/(.+?)\.design\.(.+?)\s=\s(.+?)/g, (match, p1, p2, p3) => `${p1}.style.${p2} = ${p3}`); - code = code.replace(/(.+?)\.design\s=\s(.+?)/g, (match, p1, p2) => `${p1}.style = ${p2}`); - code = code.replace(/is_action\s([=!><]+)\s(["'])checked(["'])/g, (match) => `checked`); - code = code.replace(/is_action\s([=!><]+)\s(["'])selected(["'])/g, (match) => `selected`); - code = code.replace(/is_action\s([=!><]+)\s(["'])open(["'])/g, (match) => `open`); - code = code.replace(/is_have\s([=!><]+)\s(["'])required(["'])/g, (match) => `required`); - code = code.replace(/is_have\s([=!><]+)\s(["'])readOnly(["'])/g, (match) => `readOnly`); - code = code.replace(/is_have\s([=!><]+)\s(["'])autofocus(["'])/g, (match) => `autofocus`); - code = code.replace(/is_have\s([=!><]+)\s(["'])disabled(["'])/g, (match) => `disabled`); - code = code.replace(/is_have\s([=!><]+)\s(["'])multiple(["'])/g, (match) => `multiple`); - code = code.replace(/is_have\s([=!><]+)\s(["'])hidden(["'])/g, (match) => `hidden`); - code = code.replace(/delay\:\n\s+(.+?)/g, (match, p1) => `setTimeout(function() {\n ${p1}`); - - // loop - code = code.replace(/repeat\:\n\s+(.+?)/g, (match, p1) => `setInterval(function() {\n ${p1}`); - code = code.replace(/for (.+?)=(.+?) to (.+?)\:/g, (match, p1, p2, p3) => `for (let ${p1} = ${p2}; ${p1} < ${p3}; ${p1}++) {`); - code = code.replace(/for (.+?) in (.+?)\:/g, (match, p1, p2) => `for (let ${p1} in ${p2}) {`); - code = code.replace(/for (.+?) of (.+?)\:/g, (match, p1, p2) => `for (let ${p1} of ${p2}) {`); - code = code.replace(/(? `switch ((${p1})) {`) - code = code.replace("switch ((", "switch (") - code = code.replace(")) {", ") {") - code = code.replace(/case (.+?):\n(.+?)/g, (match, p1, p2) => `case ${p1}: \n${p2}`) - code = code.replace(/([^\s].+?)\.image\s*=\s*(['"`][^'"`]*['"``])/g, (match, p1, p2) => `image(${p1}, ${p2})`); - code = code.replace(/process\.argv\[(\d+)\]/g, (match, p1) => { - var a = Number(p1) + 0; - return `process.argv[${a}]`; - }); - code = code.replace(/int\((.+?)\)/g, (match, p1) => `parseInt(${p1})`) - code = code.replace(/num\((.+?)\)/g, (match, p1) => `Number(${p1})`) - code = code.replace(/str\((.+?)\)/g, (match, p1) => `String(${p1})`) - code = code.replace(/float\((.+?)\)/g, (match, p1) => `parseFloat(${p1})`) - code = code.replace(/json\((.+?)\)/g, (match, p1) => `JSON.parse(${p1})`); - code = code.replace(/(.+?)\.map\((.+?)\)\:/g, (match, p1, p2) => `${p1}.map(function(${p2}) {`) - code = code.replace(/\.map\((.+?)\)/g, (match, p1) => `.map(${p1})`); - code = code.replace(/(.+?)\.filter\((.+?)\)\:/g, (match, p1, p2) => `${p1}.filter(function(${p2}) {`); - code = code.replace(/\.filter\((.+?)\)/g, (match, p1) => `.filter(${p1})`); - code = code.replace(/(.+?)\.reduce\((.+?)\)\:/g, (match, p1, p2) => `${p1}.reduce(function(${p2}) {`) - code = code.replace(/\.reduce\((.+?)\)/g, (match, p1) => `.reduce(${p1})`); - code = code.replace(/(? `typeof ${p1}`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_type\s===\s([a-zA-Z0-9"'`]+)/g, (match, p1, p2) => `typeof ${p1} === ${p2}`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])type(["'])/g, (match, p1) => `${p1} instanceof TypeError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])reference(["'])/g, (match, p1) => `${p1} instanceof ReferenceError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])syntax(["'])/g, (match, p1) => `${p1} instanceof SyntaxError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])range(["'])/g, (match, p1) => `${p1} instanceof RangeError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])uri(["'])/g, (match, p1) => `${p1} instanceof URIError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])eval(["'])/g, (match, p1) => `${p1} instanceof EvalError`) - code = code.replace(/([a-zA-Z0-9_$]+)\.is_error\s===\s(["'])(.+?)(["'])/g, (match, p1, p2, p3) => `${p1} instanceof ${p3}`) - code = code.replace(/constructor\((.+?)\)\:/g, (match, p1) => `constructor(${p1}) {`); - code = code.replace(/(.+?)\(\)\:/g, (match, p1) => `${p1}() {`); - code = code.replace(/class\s(.+?)\:/g, (match, p1) => `class ${p1} {`); - code = code.replace(/class\s(.+?)\sextends\s(.+?)\:/g, (match, p1, p2) => `class ${p1} extends ${p2} {`); - code = code.replace(/static\((.+?)\)\ {/g, (match, p1) => `static(${p1}) {`); - code = code.replace(/super\((.+?)\)\:/g, (match, p1) => `super(${p1})`) - code = code.replace(/throw (.+?)/g, (match, p1) => `throw ${p1}`) - code = code.replace(/new (.+?)/g, (match, p1) => `new ${p1}`) - code = code.replace(/end\:(.+?)/g, (match, p1) => `}, ${p1});`); - code = code.replace(/handle\:/g, 'try {') - code = code.replace(/recovery\s\((.+?)\)\:/g, (match, p1) => `} catch (${p1}) {`) - code = code.replace(/final\:/g, "} finally {") - code = code.replace(/handle\:/g, 'try {') - code = code.replace(/recovery\s*\((.+?)\)\:/g, (match, p1) => `} catch (${p1}) {`) - code = code.replace(/final\:/g, "} finally {") - code = code.replace(/Err\((.+?)\)/g, (match, p1) => `Error(${p1})`) - code = code.replace(/URIErr\((.+?)\)/g, (match, p1) => `URIError(${p1})`) - code = code.replace(/EvalErr\((.+?)\)/g, (match, p1) => `EvalError(${p1})`) - code = code.replace(/TypeErr\((.+?)\)/g, (match, p1) => `TypeError(${p1})`) - code = code.replace(/RangeErr\((.+?)\)/g, (match, p1) => `RangeError(${p1})`) - code = code.replace(/SyntaxErr\((.+?)\)/g, (match, p1) => `Error(${p1})`) - code = code.replace(/InternalErr\((.+?)\)/g, (match, p1) => `InternalError(${p1})`) - code = code.replace(/ReferenceErr\((.+?)\)/g, (match, p1) => `ReferenceError(${p1})`) - - const script = document.createElement("script"); - script.textContent = code; - document.body.appendChild(script); - } -} - -function image(element, imageClass) { - if (!element) { - console.error(`Element tidak ditemukan.`); - return; - } - - const icon = document.createElement("i"); - - // Pisahkan class yang diberikan berdasarkan spasi - const classes = imageClass.split(' '); - classes.forEach(cls => icon.classList.add(cls)); - - element.appendChild(icon); -} - -function parsing(variable) { - const patterns = [ - /let\s*(.+?)=(.+?)/g, - /function\s*(.+?)\((.+?)\)/g, - /if\s*\((.+?)\)\s*{/g, - /else if\s*\((.+?)\)\s*{/g, - /else\s*{\s*/g, - /console(.+?)\((.+?)\)/g, - /alert\((.+?)\)/g, - /prompt\((.+?)\)/g, - /Math\.random\((.+?)\)/g, - /Math\.floor\((.+?)\)/g, - /Math\.ceil\((.+?)\)/g, - /Math\.min\((.+?)\)/g, - /Math\.max\((.+?)\)/g, - /document\.createElement\((.+?)\)/g, - /document\.(.+?)\.appendChild\((.+?)\)/g, - /(.+?).replace\((.+?)\)/g, - /\/\//g, - /(.+?)\.textContent\s*=\s*(.+?)/g, - /\/\*(.+?)\*\//g, - /document\.addEventListener\((.+?), function\(\) \{/g, - /forEach\((.+?) \=\> \{/g, - /(.+?)\.includes\((.+?)\)/g, - /(.+?)\.match\((.+?)\)/g, - /(.+?)\.value ===/g, - /(.+?)\.value ==/g, - /(.+?)\.value =/g, - /(.+?)\.value !===/g, - /(.+?)\.value !==/g, - /(.+?)\.value !=/g, - /(.+?)\.value >/g, - /(.+?)\.value /g, - /(.+?)\.value <==/g, - /(.+?)\.style ===/g, - /(.+?)\.style ==/g, - /(.+?)\.style =/g, - /(.+?)\.style !===/g, - /(.+?)\.style !==/g, - /(.+?)\.style !=/g, - /(.+?)\.style >/g, - /(.+?)\.style /g, - /(.+?)\.style <==/g, - /(.+?)\.style\.(.+?) =/g, - /(.+?)\.style =/g, - /(.+?)\.checked/g, - /(.+?)\.required/g, - /setTimeout\(function\(\) \{/g, - /setInterval\(function\(\) \{/g, - /for \((.+?)\) \{/g, - /classList\.add\((.+?)\)/g, - /switch\s*\((.+?)\)/g, - /image\((.+?), (.+?)\)/g, - /class\s*(.+?)\s*\{/g, - /class\s*(.+?)\s*extends\s*(.+?)\s*\{/g, - /constructor(.+?)\s*\{/g, - /(.+?)\(\)\s*\{/g, - /static(.+?)\s*\{/g, - /typeof\s*(.+?)\s*(.+?)\s*(.+?)/g, - /\.map\(function\((.+?)\)\s*\{/g, - /\.filter\(function\((.+?)\)\s*\{/g, - /\.reduce\(function\((.+?)\)\s*\{/g, - /try\s*{/g, - /}\s*catch\s*(.+?)\s*{/g, - /}\s*finally\s*{/g, - ]; - - if (allowJs === "false") { - const matches = patterns.map(pattern => { - const match = variable.match(pattern); - return match ? match[0] : null; - }).filter(Boolean); - - if (matches.length > 0) { - parsingMsg = "Invalid function '" + matches.join("', '") + "'"; - const error = new Error(parsingMsg); - console.error(error); - return "false"; - } else { - parsingMsg = ""; - return "true"; - } - } -} - -function parseCode(code) { - const lines = code.split('\n'); - const output = []; - const stack = []; - - const regex = { - listener: /(.+?)\.listener/, - classFunc: /(.+?)\(\)\:/, - change: /(.+?)\.change\((.+?), \((.+?)\)\)\:/, - map: /(.+?)\.map\((.+?)\)\:/, - filter: /(.+?)\.filter\((.+?)\)\:/, - forEach: /forEach\((.+?)\)\:/, - func: /func\s*(.+?)\((.+?)\)/, - blockStart: /^(if|elif|else|class|constructor|static|final|recovery|func)/, - }; - - for (let line of lines) { - const trimmedLine = line.trim(); - - if (regex.blockStart.test(trimmedLine)) { - stack.push('block'); - output.push(line); - } else if (regex.listener.test(trimmedLine) || regex.change.test(trimmedLine) || - regex.map.test(trimmedLine) || regex.filter.test(trimmedLine) || regex.forEach.test(trimmedLine)) { - stack.push('listener'); - output.push(line); - } else if (regex.func.test(trimmedLine)) { - stack.push('func'); - output.push(line); - } else if (trimmedLine === 'end') { - const indent = line.match(/^\s*/)[0]; - const lastBlock = stack.pop(); - output.push(lastBlock === 'listener' ? `${indent}});` : `${indent}}`); - } else { - output.push(line); - } - } - - return output.join('\n'); -} - -function addSemicolons(code) { - const lines = code.split('\n'); - const output = []; - - const regex = { - needsSemicolon: /^(var|const|return|throw|break|continue|.*\=.*|[^;{}]+\))$/, - alreadyTerminated: /[:;{}]\s*$/ - }; - - for (let line of lines) { - const trimmedLine = line.trim(); - - if (regex.needsSemicolon.test(trimmedLine) && !regex.alreadyTerminated.test(trimmedLine)) { - output.push(line + ';'); - } else { - output.push(line); - } + const source = sourcePath + ? await fetch(sourcePath).then(response => { + if (!response.ok) throw new Error(`Failed to fetch ${sourcePath}: ${response.status}`) + return response.text() + }) + : tag.textContent + const script = document.createElement('script') + script.textContent = compileSource(source, tag.getAttribute('allowJs') === 'true') + document.body.appendChild(script) + tag.remove() + } catch (error) { + console.error('Error executing BynixScript code:', error) } - - return output.join('\n'); -} + }) +}) diff --git a/js/bsc.js b/js/bsc.js index 87ab6a0..46c72e1 100644 --- a/js/bsc.js +++ b/js/bsc.js @@ -1,39 +1 @@ -const fs = require('fs'); - -const defaultConfig = { - extension: { - primary: '.bys', - secondary: '.bynixscript', - module: '.mbs' - }, - readFolder: './', - toFolder: './', - watch: false, - allowJs: false, - strict: false, - translate: 'javascript' -}; - -let config; - -try { - const data = fs.readFileSync('./bsc.json', 'utf8'); - config = JSON.parse(data); -} catch (err) { - config = defaultConfig; -} - -const extensions = { - primary: config.extension?.primary || defaultConfig.extension.primary, - secondary: config.extension?.secondary || defaultConfig.extension.secondary, - module: config.extension?.module || defaultConfig.extension.module -}; - -const readFolder = config.readFolder || defaultConfig.readFolder; -const toFolder = config.toFolder || defaultConfig.toFolder; -const watch = config.watch || defaultConfig.watch; -const allowJs = config.allowJs || defaultConfig.allowJs; -const strict = config.strict || defaultConfig.strict; -const translate = config.translate || defaultConfig.translate; - -module.exports = { defaultConfig, config, extensions, toFolder, readFolder, watch, allowJs, strict, translate }; +module.exports = require('./config.js').loadConfig('./bsc.json', '.bys') diff --git a/js/bsconfig.js b/js/bsconfig.js index ad6211c..765c9af 100644 --- a/js/bsconfig.js +++ b/js/bsconfig.js @@ -1,39 +1 @@ -const fs = require('fs'); - -const defaultConfig = { - extension: { - primary: '.bs', - secondary: '.bynixscript', - module: '.mbs' - }, - readFolder: './', - toFolder: './', - watch: false, - allowJs: false, - strict: false, - translate: 'javascript' -}; - -let config; - -try { - const data = fs.readFileSync('./bsconfig.json', 'utf8'); - config = JSON.parse(data); -} catch (err) { - config = defaultConfig; -} - -const extensions = { - primary: config.extension?.primary || defaultConfig.extension.primary, - secondary: config.extension?.secondary || defaultConfig.extension.secondary, - module: config.extension?.module || defaultConfig.extension.module -}; - -const readFolder = config.readFolder || defaultConfig.readFolder; -const toFolder = config.toFolder || defaultConfig.toFolder; -const watch = config.watch || defaultConfig.watch; -const allowJs = config.allowJs || defaultConfig.allowJs; -const strict = config.strict || defaultConfig.strict; -const translate = config.translate || defaultConfig.translate; - -module.exports = { defaultConfig, config, extensions, toFolder, readFolder, watch, allowJs, strict, translate }; +module.exports = require('./config.js').loadConfig('./bsconfig.json', '.bs') diff --git a/js/config.js b/js/config.js new file mode 100644 index 0000000..b5017e5 --- /dev/null +++ b/js/config.js @@ -0,0 +1,31 @@ +const fs = require('node:fs'); + +function loadConfig(filename, primary) { + const defaultConfig = { + extension: { primary, secondary: '.bynixscript', module: '.mbs' }, + readFolder: './', toFolder: './', watch: false, + allowJs: false, strict: false, translate: 'javascript' + } + var config + try { + config = JSON.parse(fs.readFileSync(filename, 'utf8')); + } catch (error) { + config = defaultConfig; + } + const extensions = { + primary: config.extension?.primary || defaultConfig.extension.primary, + secondary: config.extension?.secondary || defaultConfig.extension.secondary, + module: config.extension?.module || defaultConfig.extension.module + } + return { + defaultConfig, config, extensions, + readFolder: config.readFolder || defaultConfig.readFolder, + toFolder: config.toFolder || defaultConfig.toFolder, + watch: config.watch || defaultConfig.watch, + allowJs: config.allowJs || defaultConfig.allowJs, + strict: config.strict || defaultConfig.strict, + translate: config.translate || defaultConfig.translate + } +} + +module.exports = { loadConfig } diff --git a/js/dirProcess.js b/js/dirProcess.js index a05a4bf..46ae5e8 100644 --- a/js/dirProcess.js +++ b/js/dirProcess.js @@ -1,47 +1,29 @@ -const fs = require('fs'); -const path = require('path'); -const vm = require('vm'); -const { translate } = require('./parser.js'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const { compileSource } = require('../scripts/compile.cjs'); function readAndProcessFilesInDirectory(directory) { - // Baca semua file dalam direktori - fs.readdir(directory, (err, files) => { - if (err) { - console.error("Error reading directory:", err); - return; + fs.readdir(directory, (error, files) => { + if (error) { + console.error('Error reading directory:', error); + return; + } + files.filter(file => path.extname(file) === '.bs').forEach(file => { + const filePath = path.join(directory, file); + fs.readFile(filePath, 'utf8', (readError, data) => { + if (readError) { + console.error(`Error reading file ${file}:`, readError) + return; } - - // Filter hanya file .bs - const bsFiles = files.filter(file => path.extname(file) === '.bs'); - - // Proses setiap file .bs - bsFiles.forEach(file => { - const filePath = path.join(directory, file); - - // Baca isi file - fs.readFile(filePath, 'utf8', (err, data) => { - if (err) { - console.error(`Error reading file ${file}:`, err); - return; - } - - // Konversi "print" menjadi "console.log" - let processedData = translate(code); - - // Eksekusi kode yang telah diproses menggunakan vm - try { - const script = new vm.Script(processedData); - script.runInThisContext(); - } catch (err) { - console.error(`Error executing file ${file}:`, err); - } - }); - }); - }); + try { + new vm.Script(compileSource(data), { filename: filePath }).runInThisContext() + } catch (executionError) { + console.error(`Error executing file ${file}:`, executionError) + } + }) + }) + }) } -// Panggil fungsi ini dengan direktori yang berisi file .bs -readAndProcessFilesInDirectory('./src'); - -// Ekspor fungsi jika dibutuhkan untuk digunakan di file lain -module.exports = { readAndProcessFilesInDirectory }; \ No newline at end of file +module.exports = { readAndProcessFilesInDirectory } diff --git a/js/download.js b/js/download.js index 36749df..2a008e9 100644 --- a/js/download.js +++ b/js/download.js @@ -1,22 +1,15 @@ -const https = require('https'); +const https = require('node:https'); function getDownloads(packageName, time) { const url = `https://api.npmjs.org/downloads/point/last-${time}/${packageName}`; - - https.get(url, (res) => { + https.get(url, response => { let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { + response.on('data', chunk => { data += chunk }); + response.on('end', () => { const jsonData = JSON.parse(data); - console.log(`Package ${packageName} downloaded ${jsonData.downloads} times.`); - }); - }).on('error', (e) => { - console.error(`Error: ${e.message}`); - }); + console.log(`Package ${packageName} downloaded ${jsonData.downloads} times.`) + }) + }).on('error', error => { console.error(`Error: ${error.message}`) }); } -module.exports = { https, getDownloads }; +module.exports = { https, getDownloads } diff --git a/js/error.js b/js/error.js index 018b5ee..a0df077 100644 --- a/js/error.js +++ b/js/error.js @@ -1,58 +1,34 @@ function stackParsing(filename, errorMsg, errorStack, code, type, modulLocation) { - if (code !== undefined && code !== null) { - const stackLines = errorStack.split('\n'); - let codeLines = code.split('\n'); - - const errorLine = stackLines[0]; - const parts = errorLine.split(':'); - let line; - let column - let tabs; - let arrow; - let errorCode; - if (type === "SyntaxError") { - const errorLine = stackLines[0]; - const parts = errorLine.split(':'); - line = parseInt(parts[1].trim()); - let l = line - 2; - errorCode = codeLines[l].trim(); - - return `${type}: ${errorMsg}\n at file ${filename}\n\nSnippet:\n${errorCode}\n`; - } else if (type === "TypeError" || type === "ReferenceError" || type === "RangeError" || type === "URIError" || type === "EvalError") { - const errorLine = stackLines[5]; - const parts = errorLine.split(':'); - line = parseInt(parts[1].trim()); - column = parseInt(parts[2].trim()); - - if (column > 6) { - column -= 6; - } - - let l = line - 1; - const codeNoTrim = codeLines[l] - errorCode = codeLines[l].trim(); - const spaces = codeNoTrim.match(/^ +/); - const spacesCount = spaces ? spaces[0].length : 0; - let x = column - spacesCount - 1; - if (x < 0) { - x = Math.abs(x); - x -= 2; - } - tabs = ' '.repeat(x); - arrow = tabs + '^'; - - return `${type}: ${errorMsg}\n at file ${filename}\n at line ${line}, column ${column}\n\nSnippet:\n${errorCode}\n${arrow}`; - } else { - let commonErr = errorMsg - commonErr = commonErr.replace(/\sstack/g, ''); - commonErr = commonErr.replace(/bst\.js/g, filename); - commonErr = commonErr.replace(/bsr\.js/g, filename); - commonErr = commonErr.replace(/bsp\.js/g, filename); - return `${type}: ${commonErr}`; + if (code === undefined || code === null) { + return errorMsg + } + const stackLines = errorStack.split('\n'); + const codeLines = code.split('\n'); + if (type === 'SyntaxError') { + const line = parseInt(stackLines[0].split(':')[1].trim()); + const errorCode = codeLines[line - 2].trim(); + return `${type}: ${errorMsg}\n at file ${filename}\n\nSnippet:\n${errorCode}\n` + } else if (type === 'TypeError' || type === 'ReferenceError' || type === 'RangeError' || type === 'URIError' || type === 'EvalError') { + const parts = stackLines[5].split(':'); + const line = parseInt(parts[1].trim()); + let column = parseInt(parts[2].trim()); + if (column > 6) { + column -= 6; + } + const codeNoTrim = codeLines[line - 1]; + const errorCode = codeNoTrim.trim(); + const spaces = codeNoTrim.match(/^ +/); + const spacesCount = spaces ? spaces[0].length : 0; + let offset = column - spacesCount - 1; + if (offset < 0) { + offset = Math.abs(offset) - 2; } + const arrow = ' '.repeat(offset) + '^'; + return `${type}: ${errorMsg}\n at file ${filename}\n at line ${line}, column ${column}\n\nSnippet:\n${errorCode}\n${arrow}` } else { - return errorMsg; + const commonErr = errorMsg.replace(/\sstack/g, '').replace(/bst\.js|bsr\.js|bsp\.js/g, filename); + return `${type}: ${commonErr}` } } -module.exports = { stackParsing }; \ No newline at end of file +module.exports = { stackParsing } diff --git a/js/watcher.js b/js/watcher.js index 4f46d89..98f338e 100644 --- a/js/watcher.js +++ b/js/watcher.js @@ -1,93 +1,43 @@ const chokidar = require('chokidar'); -const fs = require('fs'); -const path = require('path'); -const { parsing, parsingMsg, parseCode } = require('./parser.js') -const { funcReplace, condReplace, forEachReplace, reassignReplace, assignReplace, logReplace, interactReplace, mathReplace, domReplace, commentReplace, utilityReplace, asyncReplace, flowReplace, forReplace, convReplace, checkReplace, oopReplace, excepReplace, otherReplace } = require('../raw/parser/parsingDecl.js') +const fs = require('node:fs'); +const path = require('node:path'); +const { compileSource } = require('../scripts/compile.cjs'); function watching(directory, directory2) { - let code; - - const watcher = chokidar.watch(directory, { - ignored: /(^|[\/\\])\../, - persistent: true - }); - - watcher - .on('add', filePath => { - code = fs.readFileSync(filePath, 'utf8'); - code = parseCode(code); - const parsingResults = parsing(code); - if (parsingResults === false) { - console.log("Please check your code and try again.") - } else { - code = flowReplace(code) - code = utilityReplace(code) - code = funcReplace(code) - code = condReplace(code) - code = forEachReplace(code) - code = reassignReplace(code) - code = assignReplace(code) - code = logReplace(code) - code = interactReplace(code) - code = mathReplace(code) - code = commentReplace(code) - code = asyncReplace(code) - code = forReplace(code) - code = convReplace(code) - code = checkReplace(code) - code = oopReplace(code) - code = excepReplace(code) - code = domReplace(code) - var newFilePath = path.join(directory2, path.basename(filePath)); - newFilePath = newFilePath.replace(".bs", ".js"); - fs.writeFileSync(newFilePath, code); - - console.log(`File ${newFilePath} has been created.`); - } - }) - .on('change', filePath => { - code = fs.readFileSync(filePath, 'utf8'); - code = parseCode(code); - const parsingResults = parsing(code); - if (parsingResults === false) { - console.log("Please check your code and try again.") - } else { - code = flowReplace(code) - code = utilityReplace(code) - code = funcReplace(code) - code = condReplace(code) - code = forEachReplace(code) - code = reassignReplace(code) - code = assignReplace(code) - code = logReplace(code) - code = interactReplace(code) - code = mathReplace(code) - code = commentReplace(code) - code = asyncReplace(code) - code = forReplace(code) - code = convReplace(code) - code = checkReplace(code) - code = oopReplace(code) - code = excepReplace(code) - code = domReplace(code) - var newFilePath = path.join(directory2, path.basename(filePath)); - newFilePath = newFilePath.replace(".bs", ".js"); - fs.writeFileSync(newFilePath, code); - - console.log(`File ${newFilePath} has been changed.`); - } - }) - .on('unlink', filePath => { - var newFilePath = path.join(directory2, path.basename(filePath)); - newFilePath = newFilePath.replace(".bs", ".js"); - fs.unlinkSync(newFilePath); - - console.log(`File ${newFilePath} has been deleted.`); - }); - - watcher - .on('error', error => console.log(`Watcher error: ${error}`)) - .on('ready', () => console.log('Initial scan complete. Ready for changes')); + const watcher = chokidar.watch(directory, { ignored: /(^|[\/\\])\../, persistent: true }); + const extensions = ['.bs', '.bys', '.bynixscript', '.mbs']; + function outputPath(filePath) { + const extension = path.extname(filePath); + if (!extensions.includes(extension)) { + return null + } + return path.join(directory2, path.basename(filePath, extension) + (extension === '.mbs' ? '.mjs' : '.js')); + } + function compileFile(filePath) { + const output = outputPath(filePath); + if (!output) { + return; + } + try { + const source = fs.readFileSync(filePath, 'utf8'); + const code = compileSource(source); + fs.writeFileSync(output, code); + console.log(`File ${output} has been created or changed.`) + } catch (error) { + console.error(`Error compiling ${filePath}:`, error) + } + } + function removeFile(filePath) { + const output = outputPath(filePath); + if (output && fs.existsSync(output)) { + fs.unlinkSync(output); + console.log(`File ${output} has been deleted.`) + } + } + watcher.on('add', compileFile).on('change', compileFile).on('unlink', removeFile); + watcher.on('error', error => console.error('Watcher error:', error)); + watcher.on('ready', () => console.log('Initial scan complete. Ready for changes')); + return watcher } -module.exports = { watching }; \ No newline at end of file +module.exports = { watching } diff --git a/package.json b/package.json index 6ca3865..e06ab7e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.2.2-next", "description": "This is new Programming Language.", "type": "commonjs", - "browser": "js/browser.js", + "browser": "dist/browser.js", "main": "./index.js", "scripts": { "build": "node scripts/build.cjs", diff --git a/scripts/build.cjs b/scripts/build.cjs index a411f77..203dbba 100644 --- a/scripts/build.cjs +++ b/scripts/build.cjs @@ -7,9 +7,15 @@ 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') +const modules = ['config', 'download', 'error', 'dirProcess', 'watcher'] async function main() { try { + for (const name of modules) { + const code = compileSource(fs.readFileSync(path.join(source, `${name}.bs`), 'utf8')) + fs.writeFileSync(path.join(root, 'js', `${name}.js`), + code.endsWith('\n') ? code : code + '\n') + } 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')), @@ -27,6 +33,23 @@ async function main() { target: 'node20' }) fs.chmodSync(output, 0o755) + await build({ + entryPoints: [path.join(root, 'js', 'browser.js')], + outfile: path.join(root, 'dist', 'browser.js'), + platform: 'browser', + format: 'iife', + bundle: true, + plugins: [{ + name: 'browser-config', + setup(bundle) { + bundle.onResolve({ filter: /^fs$/ }, () => ({ path: 'fs', namespace: 'browser-config' })) + bundle.onLoad({ filter: /.*/, namespace: 'browser-config' }, () => ({ + contents: 'module.exports = { readFileSync() { throw new Error("No config file in browser") } }', + loader: 'js' + })) + } + }] + }) } finally { fs.rmSync(temporary, { recursive: true, force: true }) } diff --git a/scripts/compile.cjs b/scripts/compile.cjs index 6587b6b..73810ed 100644 --- a/scripts/compile.cjs +++ b/scripts/compile.cjs @@ -92,10 +92,10 @@ function protectSource(source) { return { masked, restore: text => text.replace(token, (match, index) => protectedText[Number(index)]) } } -function compileSource(source) { +function compileSource(source, allowJs = false) { const { masked, restore } = protectSource(source) let code = addSemicolons(parseCode(masked)) - if (parsing(code) === false) { + if (!allowJs && parsing(code) === false) { throw new Error(parsingMsg.message) } for (const pass of passes) { diff --git a/src/config.bs b/src/config.bs new file mode 100644 index 0000000..cc869f4 --- /dev/null +++ b/src/config.bs @@ -0,0 +1,31 @@ +const fs = require('node:fs') + +func loadConfig(filename, primary): + const defaultConfig = { + extension: { primary, secondary: '.bynixscript', module: '.mbs' }, + readFolder: './', toFolder: './', watch: false, + allowJs: false, strict: false, translate: 'javascript' + } + var config + handle: + config = JSON.parse(fs.readFileSync(filename, 'utf8')) + recovery (error): + config = defaultConfig + end + const extensions = { + primary: config.extension?.primary || defaultConfig.extension.primary, + secondary: config.extension?.secondary || defaultConfig.extension.secondary, + module: config.extension?.module || defaultConfig.extension.module + } + return { + defaultConfig, config, extensions, + readFolder: config.readFolder || defaultConfig.readFolder, + toFolder: config.toFolder || defaultConfig.toFolder, + watch: config.watch || defaultConfig.watch, + allowJs: config.allowJs || defaultConfig.allowJs, + strict: config.strict || defaultConfig.strict, + translate: config.translate || defaultConfig.translate + } +end + +module.exports = { loadConfig } diff --git a/src/dirProcess.bs b/src/dirProcess.bs new file mode 100644 index 0000000..74ed5cf --- /dev/null +++ b/src/dirProcess.bs @@ -0,0 +1,29 @@ +const fs = require('node:fs') +const path = require('node:path') +const vm = require('node:vm') +const { compileSource } = require('../scripts/compile.cjs') + +func readAndProcessFilesInDirectory(directory): + fs.readdir(directory, (error, files) => { + if (error) { + console.error('Error reading directory:', error) + return + } + files.filter(file => path.extname(file) === '.bs').forEach(file => { + const filePath = path.join(directory, file) + fs.readFile(filePath, 'utf8', (readError, data) => { + if (readError) { + console.error(`Error reading file ${file}:`, readError) + return + } + handle: + new vm.Script(compileSource(data), { filename: filePath }).runInThisContext() + recovery (executionError): + console.error(`Error executing file ${file}:`, executionError) + end + }) + }) + }) +end + +module.exports = { readAndProcessFilesInDirectory } diff --git a/src/download.bs b/src/download.bs new file mode 100644 index 0000000..bd22c06 --- /dev/null +++ b/src/download.bs @@ -0,0 +1,15 @@ +const https = require('node:https') + +func getDownloads(packageName, time): + const url = `https://api.npmjs.org/downloads/point/last-${time}/${packageName}` + https.get(url, response => { + let data = '' + response.on('data', chunk => { data += chunk }) + response.on('end', () => { + const jsonData = JSON.parse(data) + console.log(`Package ${packageName} downloaded ${jsonData.downloads} times.`) + }) + }).on('error', error => { console.error(`Error: ${error.message}`) }) +end + +module.exports = { https, getDownloads } diff --git a/src/error.bs b/src/error.bs new file mode 100644 index 0000000..8f9498f --- /dev/null +++ b/src/error.bs @@ -0,0 +1,34 @@ +func stackParsing(filename, errorMsg, errorStack, code, type, modulLocation): + if code === undefined || code === null: + return errorMsg + end + const stackLines = errorStack.split('\n') + const codeLines = code.split('\n') + if type === 'SyntaxError': + const line = parseInt(stackLines[0].split(':')[1].trim()) + const errorCode = codeLines[line - 2].trim() + return `${type}: ${errorMsg}\n at file ${filename}\n\nSnippet:\n${errorCode}\n` + elif type === 'TypeError' || type === 'ReferenceError' || type === 'RangeError' || type === 'URIError' || type === 'EvalError': + const parts = stackLines[5].split(':') + const line = parseInt(parts[1].trim()) + let column = parseInt(parts[2].trim()) + if column > 6: + column -= 6 + end + const codeNoTrim = codeLines[line - 1] + const errorCode = codeNoTrim.trim() + const spaces = codeNoTrim.match(/^ +/) + const spacesCount = spaces ? spaces[0].length : 0 + let offset = column - spacesCount - 1 + if offset < 0: + offset = Math.abs(offset) - 2 + end + const arrow = ' '.repeat(offset) + '^' + return `${type}: ${errorMsg}\n at file ${filename}\n at line ${line}, column ${column}\n\nSnippet:\n${errorCode}\n${arrow}` + else: + const commonErr = errorMsg.replace(/\sstack/g, '').replace(/bst\.js|bsr\.js|bsp\.js/g, filename) + return `${type}: ${commonErr}` + end +end + +module.exports = { stackParsing } diff --git a/src/translate.bs b/src/translate.bs index 1dfc864..20e6090 100644 --- a/src/translate.bs +++ b/src/translate.bs @@ -1,6 +1,6 @@ const fs = require('node:fs') const path = require('node:path') -const { extensions } = require('../js/bsc.js') +const { extensions } = require('../js/config.js').loadConfig('./bsc.json', '.bys') const { compileSource } = require('../scripts/compile.cjs') func resolveFile(file): diff --git a/src/watcher.bs b/src/watcher.bs new file mode 100644 index 0000000..724f580 --- /dev/null +++ b/src/watcher.bs @@ -0,0 +1,43 @@ +const chokidar = require('chokidar') +const fs = require('node:fs') +const path = require('node:path') +const { compileSource } = require('../scripts/compile.cjs') + +func watching(directory, directory2): + const watcher = chokidar.watch(directory, { ignored: /(^|[\/\\])\../, persistent: true }) + const extensions = ['.bs', '.bys', '.bynixscript', '.mbs'] + func outputPath(filePath): + const extension = path.extname(filePath) + if !extensions.includes(extension): + return null + end + return path.join(directory2, path.basename(filePath, extension) + (extension === '.mbs' ? '.mjs' : '.js')) + end + func compileFile(filePath): + const output = outputPath(filePath) + if !output: + return + end + handle: + const source = fs.readFileSync(filePath, 'utf8') + const code = compileSource(source) + fs.writeFileSync(output, code) + console.log(`File ${output} has been created or changed.`) + recovery (error): + console.error(`Error compiling ${filePath}:`, error) + end + end + func removeFile(filePath): + const output = outputPath(filePath) + if output && fs.existsSync(output): + fs.unlinkSync(output) + console.log(`File ${output} has been deleted.`) + end + end + watcher.on('add', compileFile).on('change', compileFile).on('unlink', removeFile) + watcher.on('error', error => console.error('Watcher error:', error)) + watcher.on('ready', () => console.log('Initial scan complete. Ready for changes')) + return watcher +end + +module.exports = { watching } diff --git a/tests/cli.test.cjs b/tests/cli.test.cjs index bf46a91..30d6fcd 100644 --- a/tests/cli.test.cjs +++ b/tests/cli.test.cjs @@ -31,7 +31,8 @@ test('build produces the packaged executable', () => { }) test('every CLI source compiles through the build compiler', () => { - for (const name of ['index', 'bsr', 'bst', 'bsp', 'bsd', 'translate']) { + for (const name of ['index', 'bsr', 'bst', 'bsp', 'bsd', 'translate', + 'config', 'download', 'error', 'dirProcess', 'watcher']) { const source = fs.readFileSync(path.join(root, 'src', `${name}.bs`), 'utf8') const code = compileSource(source) assert.match(source, /\bfunc \w+\(|\bhandle:/) @@ -229,3 +230,171 @@ test('conflicting legacy commands fail without changing files', context => { assert.match(result.stderr, /Specify only one command/) assert.ok(fs.existsSync(path.join(directory, 'safe.bys'))) }) + +test('generated modules match their BynixScript sources', () => { + for (const name of ['config', 'download', 'error', 'dirProcess', 'watcher']) { + const source = fs.readFileSync(path.join(root, 'src', `${name}.bs`), 'utf8') + const code = compileSource(source) + assert.equal(fs.readFileSync(path.join(root, 'js', `${name}.js`), 'utf8'), + code.endsWith('\n') ? code : code + '\n') + } +}) + +test('configuration wrappers preserve separate defaults and public exports', context => { + const directory = fixture(context) + const { loadConfig } = require('../js/config.js') + const legacy = require('../js/bsconfig.js') + const current = require('../js/bsc.js') + assert.equal(legacy.extensions.primary, '.bs') + assert.equal(current.extensions.primary, '.bys') + assert.equal(legacy.defaultConfig.extension.primary, '.bs') + assert.equal(current.defaultConfig.extension.primary, '.bys') + for (const name of ['defaultConfig', 'config', 'extensions', 'readFolder', 'toFolder', + 'watch', 'allowJs', 'strict', 'translate']) { + assert.ok(Object.hasOwn(current, name)) + assert.ok(Object.hasOwn(legacy, name)) + } + const file = path.join(directory, 'custom.json') + fs.writeFileSync(file, JSON.stringify({ extension: { secondary: '.custom' }, readFolder: 'input', watch: true })) + const loaded = loadConfig(file, '.bys') + assert.equal(loaded.extensions.primary, '.bys') + assert.equal(loaded.extensions.secondary, '.custom') + assert.equal(loaded.readFolder, 'input') + assert.equal(loaded.watch, true) + assert.equal(loadConfig(path.join(directory, 'missing.json'), '.bs').extensions.primary, '.bs') +}) + +test('generated error formatter preserves its public behavior', () => { + const { stackParsing } = require('../js/error.js') + assert.equal(stackParsing('test.bys', 'missing', '', null, 'Error'), 'missing') + assert.equal(stackParsing('test.bys', 'failed in bst.js', '', 'code', 'Error'), + 'Error: failed in test.bys') + assert.equal(stackParsing('test.bys', 'invalid', 'test.bys:3:1', 'first\nsecond\nthird', 'SyntaxError'), + 'SyntaxError: invalid\n at file test.bys\n\nSnippet:\nsecond\n') +}) + +test('generated download utility keeps its https export and reports counts', () => { + const { EventEmitter } = require('node:events') + const { https, getDownloads } = require('../js/download.js') + const originalGet = https.get + const originalLog = console.log + const messages = [] + try { + console.log = message => messages.push(message) + https.get = (url, callback) => { + assert.equal(url, 'https://api.npmjs.org/downloads/point/last-week/bynixscript') + const response = new EventEmitter() + callback(response) + response.emit('data', '{"downloads":42}') + response.emit('end') + return new EventEmitter() + } + getDownloads('bynixscript', 'week') + assert.deepEqual(messages, ['Package bynixscript downloaded 42 times.']) + } finally { + https.get = originalGet + console.log = originalLog + } +}) + +test('directory processor does not run on import and compiles file contents', async context => { + const directory = fixture(context) + fs.writeFileSync(path.join(directory, 'example.bs'), 'globalThis.bynixDirectoryTest = "print(1)"\n# print("unsafe")\n') + const modulePath = path.join(root, 'js', 'dirProcess.js') + const importOnly = spawnSync(process.execPath, ['-e', `require(${JSON.stringify(modulePath)})`], { + cwd: directory, encoding: 'utf8' + }) + assert.equal(importOnly.status, 0, importOnly.stderr) + assert.equal(importOnly.stdout, '') + assert.equal(importOnly.stderr, '') + const { readAndProcessFilesInDirectory } = require(modulePath) + context.after(() => { delete globalThis.bynixDirectoryTest }) + readAndProcessFilesInDirectory(directory) + for (let attempt = 0; attempt < 100 && globalThis.bynixDirectoryTest === undefined; attempt++) { + await new Promise(resolve => setTimeout(resolve, 10)) + } + assert.equal(globalThis.bynixDirectoryTest, 'print(1)') +}) + +test('watcher compiles with the shared compiler and removes generated output', async context => { + const source = fixture(context) + const output = fixture(context) + const { watching } = require('../js/watcher.js') + const watcher = watching(source, output) + context.after(() => watcher.close()) + await new Promise((resolve, reject) => { + watcher.once('ready', resolve) + watcher.once('error', reject) + }) + const file = path.join(source, 'watched.bys') + const generated = path.join(output, 'watched.js') + fs.writeFileSync(file, 'print("watch")\n# print("not executed")\n') + async function waitUntil(predicate) { + for (let attempt = 0; attempt < 100; attempt++) { + if (predicate()) return + await new Promise(resolve => setTimeout(resolve, 20)) + } + assert.fail('watcher did not update the output') + } + await waitUntil(() => fs.existsSync(generated)) + assert.equal(fs.readFileSync(generated, 'utf8'), compileSource(fs.readFileSync(file, 'utf8'))) + fs.unlinkSync(file) + await waitUntil(() => !fs.existsSync(generated)) +}) + +test('browser bundle runs the shared compiler without changing strings or comments', async () => { + const browser = fs.readFileSync(path.join(root, 'dist', 'browser.js'), 'utf8') + const scripts = [] + let onReady + let removed = false + const tag = { + textContent: 'const text = "print(123)"\n# print("unsafe")\nprint(text)', + getAttribute: () => null, + remove: () => { removed = true } + } + const document = { + createElement: () => ({ classList: { add() {} } }), + head: { appendChild() {} }, + body: { appendChild: script => scripts.push(script.textContent) }, + addEventListener: (name, callback) => { onReady = callback }, + querySelectorAll: () => [tag] + } + runInNewContext(browser, { document, console }) + onReady() + await new Promise(resolve => setImmediate(resolve)) + assert.equal(removed, true) + assert.equal(scripts.length, 1) + assert.ok(scripts[0].includes('"print(123)"')) + assert.ok(scripts[0].includes('// print("unsafe")')) + const printed = [] + runInNewContext(scripts[0], { console: { log: value => printed.push(value) } }) + assert.deepEqual(printed, ['print(123)']) +}) + +test('browser bundle fetches sources and honors allowJs', async () => { + const browser = fs.readFileSync(path.join(root, 'dist', 'browser.js'), 'utf8') + const scripts = [] + let onReady + const tag = { + getAttribute: name => ({ src: 'remote.bys', allowJs: 'true' })[name] ?? null, + remove() {} + } + const document = { + createElement: () => ({ classList: { add() {} } }), + head: { appendChild() {} }, + body: { appendChild: script => scripts.push(script.textContent) }, + addEventListener: (name, callback) => { onReady = callback }, + querySelectorAll: () => [tag] + } + runInNewContext(browser, { + document, console, + fetch: async file => { + assert.equal(file, 'remote.bys') + return { ok: true, text: async () => 'const message = "print(1)"\nprint(message)\n' } + } + }) + onReady() + await new Promise(resolve => setImmediate(resolve)) + assert.equal(scripts.length, 1) + assert.ok(scripts[0].includes('"print(1)"')) +})