diff options
Diffstat (limited to 'js/baba-yaga/experimental/fmt')
-rw-r--r-- | js/baba-yaga/experimental/fmt/fmt-README.md | 338 | ||||
-rw-r--r-- | js/baba-yaga/experimental/fmt/fmt.js | 700 |
2 files changed, 1038 insertions, 0 deletions
diff --git a/js/baba-yaga/experimental/fmt/fmt-README.md b/js/baba-yaga/experimental/fmt/fmt-README.md new file mode 100644 index 0000000..132549b --- /dev/null +++ b/js/baba-yaga/experimental/fmt/fmt-README.md @@ -0,0 +1,338 @@ +# Baba Yaga Code Formatter (`fmt.js`) + +A code formatter for the Baba Yaga programming language, similar to Go's `fmt` tool. It automatically formats Baba Yaga source code according to consistent style guidelines. + +## Features + +- **Consistent Formatting**: Applies standard formatting rules across all Baba Yaga code +- **Automatic Indentation**: Proper indentation for nested structures (functions, when expressions, with blocks) +- **Operator Spacing**: Consistent spacing around operators and punctuation +- **Line Breaking**: Smart line breaking for long expressions and data structures +- **Comment Preservation**: Maintains existing comments (work in progress) +- **Type Annotation Formatting**: Proper formatting of typed function parameters and return types + +## Installation + +The formatter uses the existing Baba Yaga lexer and parser. Ensure you have the core language files: +- `lexer.js` +- `parser.js` +- `fmt.js` + +## Usage + +### Command Line + +```bash +# Format and print to stdout +node fmt.js file.baba + +# Format and write back to file +node fmt.js --write file.baba +node fmt.js -w file.baba + +# Check if file is already formatted (exit code 0 if formatted, 1 if not) +node fmt.js --check file.baba +node fmt.js -c file.baba + +# Custom indentation size (default: 2 spaces) +node fmt.js --indent=4 file.baba +``` + +### As a Module + +```javascript +import { BabaYagaFormatter } from './fmt.js'; + +const formatter = new BabaYagaFormatter({ + indentSize: 2, + maxLineLength: 100, + preserveComments: true +}); + +const source = `x:2+3;y:x*2;`; +const formatted = formatter.format(source); +console.log(formatted); +// Output: +// x : 2 + 3; +// y : x * 2; +``` + +## Formatting Rules + +### Function Body Indentation + +All function bodies are properly indented relative to the function name: + +```baba +// Simple function body +inc : x -> + x + 1; + +// Complex function body with when expression +classify : n -> + when n is + 0 then "zero" + _ then "other"; + +// Function with with header +calculate : a b -> + with ( + sum : a + b; + product : a * b; + ) -> + {sum: sum, product: product}; +``` + +### Basic Declarations + +**Before:** +```baba +x:42;y:"hello"; +``` + +**After:** +```baba +x : 42; +y : "hello"; +``` + +### Functions + +**Before:** +```baba +add:x y->x+y; +multiply:(x:Int,y:Int)->Int->x*y; +``` + +**After:** +```baba +add : x y -> + x + y; + +multiply : (x: Int, y: Int) -> Int -> + x * y; +``` + +### When Expressions + +**Before:** +```baba +check:x->when x is 1 then"one"2 then"two"_ then"other"; +``` + +**After:** +```baba +check : x -> + when x is + 1 then "one" + 2 then "two" + _ then "other"; +``` + +### Then Keyword Alignment + +The formatter ensures all `then` keywords within a `when` expression scope are aligned for maximum readability: + +**Before:** +```baba +processRequest : method path -> + when method path is + "GET" "/" then "Home page" + "GET" "/about" then "About page" + "POST" "/api/users" then "Create user" + "DELETE" "/api/users" then "Delete user" + _ _ then "Not found"; +``` + +**After:** +```baba +processRequest : method path -> + when method path is + "GET" "/" then "Home page" + "GET" "/about" then "About page" + "POST" "/api/users" then "Create user" + "DELETE" "/api/users" then "Delete user" + _ _ then "Not found"; +``` + +This alignment is maintained within each `when` scope, making nested when expressions highly readable. + +### Lists and Tables + +**Before:** +```baba +nums:[1,2,3,4,5]; +person:{name:"Alice",age:30,active:true}; +``` + +**After:** +```baba +nums : [1, 2, 3, 4, 5]; +person : {name: "Alice", age: 30, active: true}; +``` + +For longer structures, the formatter uses multi-line format: +```baba +longList : [ + 1, + 2, + 3, + 4, + 5 +]; + +complexTable : { + name: "Alice", + details: { + age: 30, + city: "Boston" + }, + preferences: ["tea", "books", "coding"] +}; +``` + +### With Headers + +**Before:** +```baba +calc:x y->with(a:x+1;b:y*2;)->a+b; +``` + +**After:** +```baba +calc : x y -> + with ( + a : x + 1; + b : y * 2; + ) -> + a + b; +``` + +### Function Calls + +**Before:** +```baba +result:add 5 3; +complex:map(x->x*2)[1,2,3]; +``` + +**After:** +```baba +result : add 5 3; +complex : map (x -> x * 2) [1, 2, 3]; +``` + +## Supported Node Types + +The formatter handles all major Baba Yaga language constructs: + +- **Declarations**: Variables, functions, types +- **Expressions**: Binary, unary, function calls, member access +- **Literals**: Numbers, strings, booleans, lists, tables +- **Control Flow**: When expressions with pattern matching +- **Advanced Features**: With headers, curried functions, anonymous functions +- **Type Annotations**: Typed parameters and return types + +## Error Handling + +If the formatter encounters a parsing error, it will report the issue and exit with a non-zero status code: + +```bash +$ node fmt.js invalid.baba +Error formatting 'invalid.baba': Formatting failed: Expected token type COLON but got SEMICOLON at 1:5 +``` + +## Integration with Editors + +The formatter can be integrated with various editors: + +### VS Code +Add to your `settings.json`: +```json +{ + "[baba]": { + "editor.defaultFormatter": "none", + "editor.formatOnSave": false + } +} +``` + +Then create a task in `.vscode/tasks.json`: +```json +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Baba Yaga", + "type": "shell", + "command": "node", + "args": ["fmt.js", "--write", "${file}"], + "group": "build", + "presentation": { + "echo": true, + "reveal": "silent", + "focus": false, + "panel": "shared" + } + } + ] +} +``` + +### Command Line Integration +Add to your shell profile (`.bashrc`, `.zshrc`, etc.): +```bash +# Format all .baba files in current directory +alias babafmt='find . -name "*.baba" -exec node /path/to/fmt.js --write {} \;' + +# Check formatting of all .baba files +alias babafmtcheck='find . -name "*.baba" -exec node /path/to/fmt.js --check {} \;' +``` + +## Examples + +### Before Formatting +```baba +// Unformatted Baba Yaga code +factorial:n->when n is 0 then 1 1 then 1 _ then n*(factorial(n-1)); +numbers:[1,2,3,4,5];sum:reduce(acc x->acc+x)0 numbers; +user:{name:"Alice",age:30,calculate:x y->x+y}; +``` + +### After Formatting +```baba +// Unformatted Baba Yaga code +factorial : n -> + when n is + 0 then 1 + 1 then 1 + _ then n * (factorial (n - 1)); + +numbers : [1, 2, 3, 4, 5]; +sum : reduce (acc x -> acc + x) 0 numbers; + +user : { + name: "Alice", + age: 30, + calculate: x y -> x + y +}; +``` + +## Contributing + +The formatter is built using the existing Baba Yaga AST structure. To add support for new language features: + +1. Add the new node type to the `visitNode` method +2. Implement a corresponding `format*` method +3. Add test cases +4. Update this documentation + +## Known Limitations + +- Comment preservation is basic and may not handle all edge cases +- Very complex nested expressions might need manual formatting +- Error recovery could be improved for malformed input + +## License + +Same as the Baba Yaga language implementation. diff --git a/js/baba-yaga/experimental/fmt/fmt.js b/js/baba-yaga/experimental/fmt/fmt.js new file mode 100644 index 0000000..85076b9 --- /dev/null +++ b/js/baba-yaga/experimental/fmt/fmt.js @@ -0,0 +1,700 @@ +#!/usr/bin/env node + +// fmt.js - Baba Yaga code formatter +// Similar to Go's fmt tool, formats Baba Yaga source code according to standard style + +import { createLexer } from './lexer.js'; +import { createParser } from './parser.js'; +import fs from 'fs'; +import path from 'path'; + +/** + * Baba Yaga code formatter + * Formats code according to consistent style rules + */ +class BabaYagaFormatter { + constructor(options = {}) { + this.indentSize = options.indentSize || 2; + this.maxLineLength = options.maxLineLength || 100; + this.preserveComments = options.preserveComments !== false; + } + + /** + * Format source code string + */ + format(source) { + try { + const lexer = createLexer(source); + const tokens = lexer.allTokens(); + + // Extract comments before parsing + const comments = this.extractComments(source); + + const parser = createParser(tokens); + const ast = parser.parse(); + + return this.formatAST(ast, comments, source); + } catch (error) { + throw new Error(`Formatting failed: ${error.message}`); + } + } + + /** + * Extract comments from source with their positions + */ + extractComments(source) { + const comments = []; + const lines = source.split('\n'); + + lines.forEach((line, lineIndex) => { + const commentMatch = line.match(/\/\/(.*)$/); + if (commentMatch) { + const column = line.indexOf('//'); + comments.push({ + line: lineIndex + 1, + column: column, + text: commentMatch[0], + content: commentMatch[1].trim() + }); + } + }); + + return comments; + } + + /** + * Format AST node + */ + formatAST(ast, comments = [], originalSource = '') { + return this.visitNode(ast, 0, comments); + } + + /** + * Visit and format a node + */ + visitNode(node, depth = 0, comments = []) { + if (!node) return ''; + + switch (node.type) { + case 'Program': + return this.formatProgram(node, depth, comments); + case 'TypeDeclaration': + return this.formatTypeDeclaration(node, depth); + case 'VariableDeclaration': + return this.formatVariableDeclaration(node, depth, comments); + case 'FunctionDeclaration': + return this.formatFunctionDeclaration(node, depth, comments); + case 'CurriedFunctionDeclaration': + return this.formatCurriedFunctionDeclaration(node, depth, comments); + case 'WithHeader': + return this.formatWithHeader(node, depth, comments); + case 'WhenExpression': + return this.formatWhenExpression(node, depth, comments); + case 'BinaryExpression': + return this.formatBinaryExpression(node, depth, comments); + case 'UnaryExpression': + return this.formatUnaryExpression(node, depth, comments); + case 'FunctionCall': + return this.formatFunctionCall(node, depth, comments); + case 'AnonymousFunction': + return this.formatAnonymousFunction(node, depth, comments); + case 'ListLiteral': + return this.formatListLiteral(node, depth, comments); + case 'TableLiteral': + return this.formatTableLiteral(node, depth, comments); + case 'MemberExpression': + return this.formatMemberExpression(node, depth, comments); + case 'ResultExpression': + return this.formatResultExpression(node, depth, comments); + case 'NumberLiteral': + return this.formatNumberLiteral(node); + case 'StringLiteral': + return this.formatStringLiteral(node); + case 'BooleanLiteral': + return this.formatBooleanLiteral(node); + case 'Identifier': + return this.formatIdentifier(node); + default: + // Fallback for unknown node types - avoid infinite recursion + if (typeof node === 'string') { + return node; + } + if (typeof node === 'number') { + return node.toString(); + } + if (typeof node === 'boolean') { + return node.toString(); + } + if (node && typeof node === 'object') { + // Try to handle as a literal value + if (node.value !== undefined) { + return node.value.toString(); + } + if (node.name !== undefined) { + return node.name; + } + } + return JSON.stringify(node); + } + } + + /** + * Format program (top level) + */ + formatProgram(node, depth, comments) { + const statements = []; + let lastWasFunction = false; + + node.body.forEach((stmt, index) => { + const formatted = this.visitNode(stmt, depth, comments); + const isFunction = stmt.type === 'FunctionDeclaration' || + stmt.type === 'CurriedFunctionDeclaration'; + + // Add extra spacing between functions and other statements + if (index > 0 && (isFunction || lastWasFunction)) { + statements.push(''); + } + + statements.push(formatted); + lastWasFunction = isFunction; + }); + + return statements.join('\n') + (statements.length > 0 ? '\n' : ''); + } + + /** + * Format type declaration + */ + formatTypeDeclaration(node, depth) { + const indent = this.getIndent(depth); + return `${indent}${node.name} ${node.typeAnnotation};`; + } + + /** + * Format variable declaration + */ + formatVariableDeclaration(node, depth, comments) { + const indent = this.getIndent(depth); + + // Check if the value is a complex expression that should be on its own line + if (node.value.type === 'WhenExpression' || node.value.type === 'WithHeader') { + const value = this.visitNode(node.value, depth + 1, comments); + return `${indent}${node.name} :\n${value};`; + } else { + const value = this.visitNode(node.value, depth, comments); + return `${indent}${node.name} : ${value};`; + } + } + + /** + * Format function declaration + */ + formatFunctionDeclaration(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}${node.name} : `; + + // Format parameters + if (node.params && node.params.length > 0) { + if (this.hasTypedParams(node.params)) { + result += this.formatTypedParameters(node.params); + } else { + result += node.params.map(p => typeof p === 'string' ? p : p.name).join(' '); + } + } + + // Add return type if present + if (node.returnType) { + result += ` -> ${this.formatType(node.returnType)}`; + } + + result += ' ->\n'; + + // Format body with proper indentation + const body = this.visitNode(node.body, depth + 1, comments); + // If the body doesn't start with indentation, add it + if (body && !body.startsWith(this.getIndent(depth + 1))) { + result += this.getIndent(depth + 1) + body; + } else { + result += body; + } + + result += ';'; + return result; + } + + /** + * Format curried function declaration + */ + formatCurriedFunctionDeclaration(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}${node.name} : `; + + // Format first typed parameter + result += `(${node.param.name}: ${this.formatType(node.param.type)})`; + + // Format return type + if (node.returnType) { + result += ` -> ${this.formatType(node.returnType)}`; + } + + result += ' ->\n'; + + // Format body with proper indentation + const body = this.visitNode(node.body, depth + 1, comments); + result += body + ';'; + + return result; + } + + /** + * Format with header + */ + formatWithHeader(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}with`; + + if (node.recursive) { + result += ' rec'; + } + + result += ' (\n'; + + // Format entries + node.entries.forEach((entry, index) => { + const entryIndent = this.getIndent(depth + 1); + if (entry.type === 'WithTypeDecl') { + result += `${entryIndent}${entry.name} ${this.formatType(entry.typeAnnotation)};`; + } else if (entry.type === 'WithAssign') { + const value = this.visitNode(entry.value, depth + 1, comments); + result += `${entryIndent}${entry.name} : ${value};`; + } + + if (index < node.entries.length - 1) { + result += '\n'; + } + }); + + result += `\n${indent}) ->\n`; + const body = this.visitNode(node.body, depth + 1, comments); + // Ensure the body is properly indented + if (body && !body.startsWith(this.getIndent(depth + 1))) { + result += this.getIndent(depth + 1) + body; + } else { + result += body; + } + + return result; + } + + /** + * Format when expression + */ + formatWhenExpression(node, depth, comments) { + const indent = this.getIndent(depth); + let result = `${indent}when `; + + // Format discriminants + const discriminants = node.discriminants.map(d => + this.visitNode(d, 0, comments) + ).join(' '); + result += `${discriminants} is\n`; + + // Calculate the maximum pattern width to align 'then' keywords + const caseIndent = this.getIndent(depth + 1); + const formattedCases = node.cases.map(caseNode => { + const patterns = caseNode.patterns.map(p => + this.formatPattern(p, depth + 1, comments) + ).join(' '); + return { + patterns, + consequent: caseNode.consequent, + originalCase: caseNode + }; + }); + + // Find the maximum pattern length for alignment + const maxPatternLength = Math.max( + ...formattedCases.map(c => c.patterns.length) + ); + + // Format cases with aligned 'then' keywords + formattedCases.forEach((formattedCase, index) => { + const { patterns, consequent } = formattedCase; + + // Pad patterns to align 'then' keywords + const paddedPatterns = patterns.padEnd(maxPatternLength); + result += `${caseIndent}${paddedPatterns} then `; + + // Format consequent - handle nested when expressions specially + if (consequent.type === 'WhenExpression') { + // For nested when expressions, add newline and proper indentation + result += '\n' + this.visitNode(consequent, depth + 2, comments); + } else { + // For simple consequents, add inline + const consequentFormatted = this.visitNode(consequent, 0, comments); + result += consequentFormatted; + } + + // Add newline between cases (but not after the last one) + if (index < formattedCases.length - 1) { + result += '\n'; + } + }); + + return result; + } + + /** + * Format pattern + */ + formatPattern(pattern, depth, comments) { + if (!pattern) return ''; + + switch (pattern.type) { + case 'WildcardPattern': + return '_'; + case 'TypePattern': + return pattern.name; + case 'ResultPattern': + return `${pattern.variant} ${pattern.identifier.name}`; + case 'ListPattern': + const elements = pattern.elements.map(e => + this.formatPattern(e, depth, comments) + ).join(', '); + return `[${elements}]`; + case 'TablePattern': + const properties = pattern.properties.map(prop => + `${prop.key}: ${this.formatPattern(prop.value, depth, comments)}` + ).join(', '); + return `{${properties}}`; + case 'NumberLiteral': + return pattern.value.toString(); + case 'StringLiteral': + return `"${pattern.value}"`; + case 'BooleanLiteral': + return pattern.value.toString(); + case 'Identifier': + return pattern.name; + default: + // For literal patterns, try to format them directly + if (typeof pattern === 'string') { + return pattern; + } + if (typeof pattern === 'number') { + return pattern.toString(); + } + return this.visitNode(pattern, depth, comments); + } + } + + /** + * Format binary expression + */ + formatBinaryExpression(node, depth, comments) { + const left = this.visitNode(node.left, depth, comments); + const right = this.visitNode(node.right, depth, comments); + + // Add spaces around operators + const needsSpaces = !['.', '..'].includes(node.operator); + if (needsSpaces) { + return `${left} ${node.operator} ${right}`; + } else { + return `${left}${node.operator}${right}`; + } + } + + /** + * Format unary expression + */ + formatUnaryExpression(node, depth, comments) { + const operand = this.visitNode(node.operand, depth, comments); + return `${node.operator}${operand}`; + } + + /** + * Format function call + */ + formatFunctionCall(node, depth, comments) { + const callee = this.visitNode(node.callee, depth, comments); + const args = node.arguments.map(arg => + this.visitNode(arg, depth, comments) + ); + + if (args.length === 0) { + return callee; + } + + // Handle parentheses for complex expressions + const formattedArgs = args.map(arg => { + // If argument contains operators or is complex, wrap in parentheses + if (arg.includes(' -> ') || (arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith('['))) { + return `(${arg})`; + } + return arg; + }); + + return `${callee} ${formattedArgs.join(' ')}`; + } + + /** + * Format anonymous function + */ + formatAnonymousFunction(node, depth, comments) { + // Handle both string parameters and object parameters + const params = node.params.map(param => { + if (typeof param === 'string') { + return param; + } else if (param && typeof param === 'object' && param.name) { + return param.name; + } else if (param && typeof param === 'object' && param.type === 'Identifier') { + return param.name; + } else { + return String(param); + } + }).join(' '); + const body = this.visitNode(node.body, depth, comments); + return `${params} -> ${body}`; + } + + /** + * Format list literal + */ + formatListLiteral(node, depth, comments) { + if (node.elements.length === 0) { + return '[]'; + } + + const elements = node.elements.map(el => + this.visitNode(el, depth, comments) + ); + + // Single line if short, multi-line if long + const singleLine = `[${elements.join(', ')}]`; + if (singleLine.length <= 50) { + return singleLine; + } + + const indent = this.getIndent(depth); + const elementIndent = this.getIndent(depth + 1); + let result = '[\n'; + elements.forEach((el, index) => { + result += `${elementIndent}${el}`; + if (index < elements.length - 1) { + result += ','; + } + result += '\n'; + }); + result += `${indent}]`; + return result; + } + + /** + * Format table literal + */ + formatTableLiteral(node, depth, comments) { + if (node.properties.length === 0) { + return '{}'; + } + + const properties = node.properties.map(prop => { + const value = this.visitNode(prop.value, depth + 1, comments); + return `${prop.key}: ${value}`; + }); + + // Single line if short, multi-line if long + const singleLine = `{${properties.join(', ')}}`; + if (singleLine.length <= 50 && !properties.some(p => p.includes('\n'))) { + return singleLine; + } + + const indent = this.getIndent(depth); + const propIndent = this.getIndent(depth + 1); + let result = '{\n'; + properties.forEach((prop, index) => { + result += `${propIndent}${prop}`; + if (index < properties.length - 1) { + result += ','; + } + result += '\n'; + }); + result += `${indent}}`; + return result; + } + + /** + * Format member expression + */ + formatMemberExpression(node, depth, comments) { + const object = this.visitNode(node.object, depth, comments); + const property = this.visitNode(node.property, depth, comments); + return `${object}.${property}`; + } + + /** + * Format result expression + */ + formatResultExpression(node, depth, comments) { + const value = this.visitNode(node.value, depth, comments); + return `${node.variant} ${value}`; + } + + /** + * Format number literal + */ + formatNumberLiteral(node) { + return node.value.toString(); + } + + /** + * Format string literal + */ + formatStringLiteral(node) { + return `"${node.value}"`; + } + + /** + * Format boolean literal + */ + formatBooleanLiteral(node) { + return node.value.toString(); + } + + /** + * Format identifier + */ + formatIdentifier(node) { + return node.name; + } + + // Helper methods + + /** + * Get indentation string + */ + getIndent(depth) { + return ' '.repeat(depth * this.indentSize); + } + + /** + * Check if parameters have type annotations + */ + hasTypedParams(params) { + return params.some(p => + typeof p === 'object' && p.type && p.type !== 'Identifier' + ); + } + + /** + * Format typed parameters + */ + formatTypedParameters(params) { + const formatted = params.map(p => { + if (typeof p === 'string') { + return p; + } else if (p.type && p.type !== 'Identifier') { + return `${p.name}: ${this.formatType(p.type)}`; + } else { + return p.name; + } + }); + return `(${formatted.join(', ')})`; + } + + /** + * Format type annotation + */ + formatType(type) { + if (typeof type === 'string') { + return type; + } + + if (type.type === 'PrimitiveType') { + return type.name; + } + + if (type.type === 'FunctionType') { + const paramTypes = type.paramTypes.map(t => this.formatType(t)).join(', '); + const returnType = this.formatType(type.returnType); + return `(${paramTypes}) -> ${returnType}`; + } + + return 'Unknown'; + } +} + +/** + * CLI interface + */ +function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error('Usage: node fmt.js <file.baba> [options]'); + console.error('Options:'); + console.error(' --write, -w Write result to file instead of stdout'); + console.error(' --check, -c Check if file is already formatted'); + console.error(' --indent=N Set indentation size (default: 2)'); + process.exit(1); + } + + const options = { + write: args.includes('--write') || args.includes('-w'), + check: args.includes('--check') || args.includes('-c'), + indentSize: 2 + }; + + // Parse indent option + const indentArg = args.find(arg => arg.startsWith('--indent=')); + if (indentArg) { + options.indentSize = parseInt(indentArg.split('=')[1]) || 2; + } + + const filename = args.find(arg => + !arg.startsWith('-') && !arg.startsWith('--') + ); + + if (!filename) { + console.error('Error: No input file specified'); + process.exit(1); + } + + if (!fs.existsSync(filename)) { + console.error(`Error: File '${filename}' not found`); + process.exit(1); + } + + try { + const source = fs.readFileSync(filename, 'utf8'); + const formatter = new BabaYagaFormatter(options); + const formatted = formatter.format(source); + + if (options.check) { + if (source.trim() !== formatted.trim()) { + console.error(`File '${filename}' is not formatted`); + process.exit(1); + } else { + console.log(`File '${filename}' is already formatted`); + process.exit(0); + } + } + + if (options.write) { + fs.writeFileSync(filename, formatted); + console.log(`Formatted '${filename}'`); + } else { + process.stdout.write(formatted); + } + + } catch (error) { + console.error(`Error formatting '${filename}': ${error.message}`); + process.exit(1); + } +} + +// Export for use as module +export { BabaYagaFormatter }; + +// Run CLI if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} |