Compare commits

...
10 Commits
Author SHA1 Message Date
dutchie031 7f6979ea72 Merge branch 'fix-compiler'
publish-vs-code-extensions.yml / Publish VS Code Extension (push) Canceled after 0s
2026-07-24 16:41:47 +02:00
dutchie031 32714e9f25 Fixed compiler and started automation 2026-07-24 16:40:43 +02:00
dutchie031 a48ca7b973 fixed version tested and runs in DCS now 2026-07-14 20:33:04 +02:00
dutchie031 377284ac59 Fixed keywords and end blocks 2026-07-08 15:48:16 +02:00
dutchie031 df79538491 wip 2026-07-03 13:26:01 +02:00
dutchie031 16e76d96d3 wip 2026-06-15 19:51:59 +02:00
Dutchieanddutchie031 3bbb07de77 Merge pull request #2 from dutchie031/feature/github-action
Feature/GitHub action
2026-04-25 21:53:09 +02:00
dutchie031 ba292ad7aa updated working directory 2026-04-07 23:05:04 +02:00
dutchie031 afafd6a48e updated params to be passed along 2026-04-07 22:58:59 +02:00
dutchie031 204bd19673 updated package-lock 2026-04-07 22:52:23 +02:00
13 changed files with 1479 additions and 298 deletions
+200
View File
@@ -0,0 +1,200 @@
name: Publish GitHub Action
on:
push:
branches:
- main
paths:
- github-action/package.json
permissions:
contents: write
jobs:
verify_action:
name: Verify action
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: github-action/package-lock.json
- name: Install action dependencies
working-directory: github-action
run: npm ci
- name: Create verification fixture
shell: bash
run: |
mkdir -p test-action-input
cat <<'EOF' > test-action-input/main.lua
local message = {}
message.hello = function()
return "hello"
end
return message
EOF
- name: Run action locally
uses: ./github-action
with:
source-root: test-action-input
output-file: test-action-output/compiled.lua
- name: Assert compiled output exists
shell: bash
run: test -f test-action-output/compiled.lua
derive_release:
name: Derive release metadata
runs-on: ubuntu-latest
needs: verify_action
outputs:
version: ${{ steps.derive.outputs.version }}
release_tag: ${{ steps.derive.outputs.release_tag }}
rolling_minor_tag: ${{ steps.derive.outputs.rolling_minor_tag }}
rolling_major_tag: ${{ steps.derive.outputs.rolling_major_tag }}
bump_kind: ${{ steps.derive.outputs.bump_kind }}
should_publish: ${{ steps.derive.outputs.should_publish }}
latest_release_tag: ${{ steps.derive.outputs.latest_release_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Derive version and tags
id: derive
shell: bash
run: |
set -euo pipefail
version=$(node -p "require('./github-action/package.json').version")
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "github-action/package.json version must be semver X.Y.Z, got: $version" >&2
exit 1
fi
IFS='.' read -r major minor patch <<< "$version"
release_tag="action/v${version}"
rolling_minor_tag="action/v${major}.${minor}"
rolling_major_tag="action/v${major}"
latest_release_tag=$(git tag --list 'action/v*.*.*' --sort=-version:refname | head -n 1)
bump_kind="initial"
should_publish="true"
if [[ -n "$latest_release_tag" ]]; then
latest_version=${latest_release_tag#action/v}
if [[ "$version" == "$latest_version" ]]; then
should_publish="false"
bump_kind="duplicate"
else
IFS='.' read -r latest_major latest_minor latest_patch <<< "$latest_version"
if (( major < latest_major )) || \
(( major == latest_major && minor < latest_minor )) || \
(( major == latest_major && minor == latest_minor && patch < latest_patch )); then
echo "github-action/package.json version $version must be greater than latest published $latest_version" >&2
exit 1
fi
if (( major > latest_major )); then
bump_kind="major"
elif (( minor > latest_minor )); then
bump_kind="minor"
elif (( patch > latest_patch )); then
bump_kind="patch"
else
echo "Unable to derive release kind from $latest_version -> $version" >&2
exit 1
fi
fi
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT"
echo "rolling_minor_tag=$rolling_minor_tag" >> "$GITHUB_OUTPUT"
echo "rolling_major_tag=$rolling_major_tag" >> "$GITHUB_OUTPUT"
echo "bump_kind=$bump_kind" >> "$GITHUB_OUTPUT"
echo "should_publish=$should_publish" >> "$GITHUB_OUTPUT"
echo "latest_release_tag=$latest_release_tag" >> "$GITHUB_OUTPUT"
- name: Summarize release plan
shell: bash
run: |
{
echo "## Action release plan"
echo ""
echo "- Version: ${{ steps.derive.outputs.version }}"
echo "- Release tag: ${{ steps.derive.outputs.release_tag }}"
echo "- Rolling minor tag: ${{ steps.derive.outputs.rolling_minor_tag }}"
echo "- Rolling major tag: ${{ steps.derive.outputs.rolling_major_tag }}"
echo "- Bump kind: ${{ steps.derive.outputs.bump_kind }}"
echo "- Latest published tag: ${{ steps.derive.outputs.latest_release_tag || 'none' }}"
echo "- Will publish: ${{ steps.derive.outputs.should_publish }}"
} >> "$GITHUB_STEP_SUMMARY"
publish_tags:
name: Publish tags
runs-on: ubuntu-latest
needs: derive_release
if: ${{ needs.derive_release.outputs.should_publish == 'true' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create immutable release tag
shell: bash
run: |
set -euo pipefail
release_tag='${{ needs.derive_release.outputs.release_tag }}'
if git rev-parse "$release_tag" >/dev/null 2>&1; then
echo "Release tag $release_tag already exists" >&2
exit 1
fi
git tag "$release_tag" "$GITHUB_SHA"
- name: Update rolling tags
shell: bash
run: |
set -euo pipefail
git tag -f '${{ needs.derive_release.outputs.rolling_minor_tag }}' "$GITHUB_SHA"
git tag -f '${{ needs.derive_release.outputs.rolling_major_tag }}' "$GITHUB_SHA"
- name: Push release tags
shell: bash
run: |
set -euo pipefail
git push origin '${{ needs.derive_release.outputs.release_tag }}'
git push origin '${{ needs.derive_release.outputs.rolling_minor_tag }}' --force
git push origin '${{ needs.derive_release.outputs.rolling_major_tag }}' --force
- name: Summarize published tags
shell: bash
run: |
{
echo "## Published action tags"
echo ""
echo "- Immutable: ${{ needs.derive_release.outputs.release_tag }}"
echo "- Rolling minor: ${{ needs.derive_release.outputs.rolling_minor_tag }}"
echo "- Rolling major: ${{ needs.derive_release.outputs.rolling_major_tag }}"
echo "- Bump kind: ${{ needs.derive_release.outputs.bump_kind }}"
} >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,12 @@
on:
push:
branches:
- main
jobs:
publish:
name: Publish VS Code Extension
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
-2
View File
@@ -3,8 +3,6 @@
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"connor4312.esbuild-problem-matchers",
"ms-vscode.extension-test-runner",
"sumneko.lua"
]
}
+1 -1
View File
@@ -16,7 +16,7 @@
"${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js",
"${workspaceFolder}/compiler/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
"preLaunchTask": "watch"
}
]
}
+20 -60
View File
@@ -4,70 +4,30 @@
"version": "2.0.0",
"tasks": [
{
"label": "watch",
"dependsOn": [
"npm: watch:tsc",
"npm: watch:esbuild"
],
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
},
"options": {
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
}
},
{
"type": "npm",
"script": "watch:esbuild",
"group": "build",
"problemMatcher": "$esbuild-watch",
"isBackground": true,
"label": "npm: watch:esbuild",
"presentation": {
"group": "watch",
"reveal": "never"
},
"options": {
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
}
},
{
"type": "npm",
"script": "watch:tsc",
"group": "build",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"label": "npm: watch:tsc",
"presentation": {
"group": "watch",
"reveal": "never"
},
"options": {
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
}
},
{
"label": "compile",
"type": "npm",
"script": "watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
"script": "compile",
"group": {
"kind": "build",
"isDefault": true
},
"group": "build"
"options": {
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
}
},
{
"label": "tasks: watch-tests",
"dependsOn": [
"npm: watch",
"npm: watch-tests"
],
"problemMatcher": []
"label": "watch",
"type": "npm",
"script": "watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": "build",
"problemMatcher": "$tsc-watch",
"options": {
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
}
}
]
}
+579
View File
@@ -0,0 +1,579 @@
import * as fs from 'fs';
import * as path from 'path';
import { CompilationError } from './CompilationError';
/*
Block types.
Require is a special case as it's technically a function call, but it's a special use case.
*/
export enum BlockType {
CodeTextBlock,
Function,
If,
While,
For,
Do,
Return,
Require,
Table,
File
}
const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end', 'local', 'else', 'elseif'];
function isKeyWord(word: string): boolean {
return keywords.includes(word);
}
function isIdentifierCharacter(char: string): boolean {
return /[A-Za-z0-9_]/.test(char);
}
/**
* Trims trailing spaces and tabs, but preserves newlines
*/
function trimEndPreserveNewlines(str: string): string {
return str.replace(/[ \t]+$/gm, '');
}
export abstract class CodeBlock {
protected childBlocks: CodeBlock[] = [];
private parentBlock?: CodeBlock;
private blockType: BlockType;
public readonly sourceLineNumber?: number;
constructor(sourceLineNumber: number, blockType: BlockType, parentBlock?: CodeBlock) {
this.sourceLineNumber = sourceLineNumber;
this.parentBlock = parentBlock;
this.blockType = blockType;
}
public getChildren(): CodeBlock[] {
return this.childBlocks;
}
abstract toLines(): string[];
getParentBlock(): CodeBlock | undefined {
return this.parentBlock;
}
static createFromFile(luaFilePath: string, onError?: (error: CompilationError) => void): LuaFile {
if (!fs.existsSync(luaFilePath)) {
throw new Error(`File not found: ${luaFilePath}`);
}
const fileContent = fs.readFileSync(luaFilePath, 'utf-8');
let leftCursor = 0;
let lineCounter = 0;
const file: LuaFile = new LuaFile();
let currentBlock: CodeBlock = file;
let currentWord = '';
let currentBlockString = '';
while (leftCursor < fileContent.length) {
const currentChar: string = fileContent[leftCursor];
if (isIdentifierCharacter(currentChar)) {
currentWord += currentChar;
} else {
currentWord = '';
}
currentBlockString += currentChar;
const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : '';
if (currentChar === '\n') {
lineCounter++;
}
if (currentChar === '-' && nextChar === '-') {
currentBlockString = currentBlockString.slice(0, -1); // Remove the '-' from the block string
const nextNextChar = leftCursor < fileContent.length - 2 ? fileContent[leftCursor + 2] : '';
if (nextNextChar === '[') {
// Multiline comment, skip to closing ]]
leftCursor += 3; // Skip the --[
while (leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')) {
if (fileContent[leftCursor] === '\n') {
lineCounter++;
}
leftCursor++;
}
leftCursor += 2; // Skip the closing ]]
} else {
// Comment line, skip to end of line
while (leftCursor < fileContent.length && fileContent[leftCursor] !== '\n') {
leftCursor++;
}
if (fileContent[leftCursor] === '\n') {
lineCounter++;
}
}
currentWord = '';
continue;
}
else if (currentChar === '"' || currentChar === "'") {
// String literal, skip to closing quote
const quoteType = currentChar;
leftCursor++;
while (leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType) {
// Add string but don't process it for keywords
currentBlockString += fileContent[leftCursor];
leftCursor++;
}
currentBlockString += quoteType; // Add the closing quote
leftCursor++; // Skip the closing quote
currentWord = '';
continue;
}
// In return block, read till the end of the return block
// else if (currentBlock.blockType === BlockType.Return) {
// //Continue reading until there's at least a new word
// }
else if (currentChar === "\n") {
// Process line
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
//currentBlockString += '\n'; // Add the newline back for the line block
if (currentBlockString !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
currentWord = '';
}
}
else if (currentChar === ")" && currentBlock.blockType === BlockType.Require) {
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
//currentBlockString += '\n'; // Add the newline back for the line block as it will be skipped otherwise
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
currentBlock = currentBlock.getParentBlock()!;
}
//Table blocks
else if (currentChar === "{") {
// Only create LineBlock if there's content before the brace
if (trimEndPreserveNewlines(currentBlockString) !== '' && trimEndPreserveNewlines(currentBlockString) !== '{') {
const currentLineBlock = new CodeTextBlock(lineCounter, trimEndPreserveNewlines(currentBlockString.slice(0, -1)), currentBlock);
currentBlock.childBlocks.push(currentLineBlock);
}
let leadingWhiteSpace = '';
const braceIndex = currentBlockString.lastIndexOf('{');
if (braceIndex > 0) {
let wsStart = braceIndex - 1;
while (wsStart >= 0 && /[ \t]/.test(currentBlockString[wsStart])) {
wsStart--;
}
leadingWhiteSpace = currentBlockString.substring(wsStart + 1, braceIndex);
}
currentBlockString = leadingWhiteSpace + '{'; // Start the new block string with the opening brace
const tableBlock = new TableBlock(lineCounter, currentBlock);
currentBlock.childBlocks.push(tableBlock);
currentBlock = tableBlock;
let braceCounter = 1;
leftCursor++;
while (leftCursor < fileContent.length && braceCounter > 0) {
const char = fileContent[leftCursor];
currentBlockString += char;
if (char === '{') {
braceCounter++;
} else if (char === '}') {
braceCounter--;
}
if (char === '\n') {
let block = trimEndPreserveNewlines(currentBlockString);
const lineBlock = new CodeTextBlock(lineCounter, block, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
lineCounter++;
}
leftCursor++;
}
// Find newline or other character
let tempCursor = leftCursor;
while (tempCursor < fileContent.length && fileContent[tempCursor] !== '\n' && fileContent[tempCursor].trim() === '') {
currentBlockString += fileContent[tempCursor];
tempCursor++;
}
if (tempCursor < fileContent.length && fileContent[tempCursor] === '\n') {
currentBlockString += '\n';
lineCounter++;
} else if (tempCursor > leftCursor) {
// We found whitespace but no newline, so position cursor at last whitespace
leftCursor = tempCursor - 1;
}
// else: no whitespace after table, leave leftCursor where it is
if (trimEndPreserveNewlines(currentBlockString) !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
}
currentBlock = currentBlock.getParentBlock()!;
currentWord = ''; // Reset word accumulator after table block
continue; // Skip the leftCursor++ at the end of the loop since we already incremented it
}
else if (currentWord !== '' && !isIdentifierCharacter(nextChar)) {
// End of a word, check for keywords
const trimmedWord = currentWord.trim();
if (currentBlock.blockType === BlockType.Return && isKeyWord(trimmedWord) && trimmedWord !== 'return') {
// We're in a ReturnBlock and hit a keyword at the statement boundary
// Extract return params and exit the block
(currentBlock as ReturnBlock).extractReturnParams();
currentBlock = currentBlock.getParentBlock()!;
// Don't clear currentBlockString - let normal flow handle the newline finalization
// Continue to process this keyword normally
}
let blockToAdd: CodeBlock | null = null;
if (trimmedWord === 'if') {
blockToAdd = new IfBlock(lineCounter, currentBlock);
}
else if (trimmedWord === 'while') {
blockToAdd = new WhileBlock(lineCounter, currentBlock);
}
else if (trimmedWord === 'for') {
blockToAdd = new ForBlock(lineCounter, currentBlock);
}
else if (trimmedWord === 'do') {
const isForOrWhile = currentBlock.blockType === BlockType.While || currentBlock.blockType === BlockType.For;
if(isForOrWhile && (currentBlock as WhileBlock | ForBlock).passedDoStatement == false) {
// If we're in a While or For block and we've not yet passed a 'do' statement, mark it as passed and don't create a new block
(currentBlock as WhileBlock | ForBlock).passedDoStatement = true;
} else {
blockToAdd = new DoBlock(lineCounter, currentBlock);
}
}
else if (trimmedWord === 'return') {
// Add any text before 'return' to parent block, but preserve newlines
const beforeReturn = currentBlockString.slice(0, -trimmedWord.length);
if (beforeReturn.trim()) {
const line = new CodeTextBlock(lineCounter, beforeReturn, currentBlock);
currentBlock.childBlocks.push(line);
}
blockToAdd = new ReturnBlock(lineCounter, currentBlock);
// Start the return block content with 'return' keyword
currentBlockString = trimmedWord;
}
else if (trimmedWord === 'function') {
blockToAdd = new FunctionBlock(lineCounter, currentBlock);
}
else if (trimmedWord === 'require') {
const beforeRequire = currentBlockString.slice(0, -trimmedWord.length);
if (beforeRequire.trim()) {
const line = new CodeTextBlock(lineCounter, beforeRequire, currentBlock);
currentBlock.childBlocks.push(line);
}
blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor);
currentBlockString = trimmedWord; // Start the require block content with 'require' keyword
}
else if (trimmedWord === 'end') {
const parent = currentBlock.getParentBlock();
if (!parent) {
onError?.({
filePath: luaFilePath,
line: fileContent.substring(0, leftCursor).split('\n').length - 1,
message: "Unexpected 'end' without matching block start"
});
} else {
currentBlock = parent;
}
}
if (blockToAdd) {
currentBlock.childBlocks.push(blockToAdd);
currentBlock = blockToAdd;
}
currentWord = '';
}
leftCursor++;
}
// Handle case where file ends while in a ReturnBlock
if (currentBlock.blockType === BlockType.Return) {
if (trimEndPreserveNewlines(currentBlockString) !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
}
(currentBlock as ReturnBlock).extractReturnParams();
}
return file;
}
}
export class CodeTextBlock extends CodeBlock {
constructor(sourceLineNumber: number, private line: string, parent?: CodeBlock) {
super(sourceLineNumber, BlockType.CodeTextBlock, parent);
}
toLines(): string[] {
return [this.line];
}
}
export class IfBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.If, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class WhileBlock extends CodeBlock {
public passedDoStatement: boolean = false;
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.While, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class ForBlock extends CodeBlock {
public passedDoStatement: boolean = false;
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.For, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class LuaFile extends CodeBlock {
constructor() {
super(0, BlockType.File);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class DoBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Do, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class ReturnBlock extends CodeBlock {
public readonly returnParams: string[] = [];
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Return, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
/**
* Extracts return parameters from the return statement.
* Splits by commas while respecting nesting of {} and ().
* Populates the returnParams array.
*/
extractReturnParams(): void {
// Reconstruct the return content from all children
let content = this.childBlocks
.map(child => {
if (child instanceof CodeTextBlock) {
return child.toLines()[0];
} else if (child instanceof TableBlock) {
// For tables, use their full content
return child.toLines().join('');
} else {
// For other block types, use their full content
return child.toLines().join('');
}
})
.join('')
.trim();
if (!content) {
this.returnParams.length = 0;
return;
}
content = content.replace(/^return\s+/, '').trim(); // Remove the 'return' keyword if present
// Split by commas while respecting nesting
const params: string[] = [];
let currentParam = '';
let braceDepth = 0;
let parenDepth = 0;
for (let i = 0; i < content.length; i++) {
const char = content[i];
const nextChar = i < content.length - 1 ? content[i + 1] : '';
// Skip strings to avoid counting delimiters inside them
if (char === '"' || char === "'") {
const quoteType = char;
currentParam += char;
i++;
while (i < content.length && content[i] !== quoteType) {
if (content[i] === '\\' && i + 1 < content.length) {
currentParam += content[i];
i++;
currentParam += content[i];
} else {
currentParam += content[i];
}
i++;
}
if (i < content.length) {
currentParam += content[i];
}
continue;
}
// Track nesting depth
if (char === '{') {
braceDepth++;
} else if (char === '}') {
braceDepth--;
} else if (char === '(') {
parenDepth++;
} else if (char === ')') {
parenDepth--;
} else if (char === ',' && braceDepth === 0 && parenDepth === 0) {
// This is a param separator
const param = currentParam.trim();
if (param) {
params.push(param);
}
currentParam = '';
continue;
}
currentParam += char;
}
// Add the last parameter
const lastParam = currentParam.trim();
if (lastParam) {
params.push(lastParam);
}
this.returnParams.length = 0;
this.returnParams.push(...params);
}
}
export class TableBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Table, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class FunctionBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Function, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
}
export class RequireBlock extends CodeBlock {
constructor(sourceLineNumber: number, parent: CodeBlock, public readonly charStart?: number, public readonly charEnd?: number) {
super(sourceLineNumber, BlockType.Require, parent);
}
toLines(): string[] {
const lines: string[] = [];
for (const child of this.childBlocks) {
lines.push(...child.toLines());
}
return lines;
}
getRequiredString(): string {
if (this.childBlocks.length === 0) {
return '';
}
const firstChild = this.childBlocks[0];
if (firstChild instanceof CodeTextBlock) {
// local module = require("module") -> module
// local module = require('module') -> module
const line = firstChild.toLines()[0];
const requireMatch = line.match(/require\s*\(\s*["']([^"']+)["']\s*\)/);
if (requireMatch && requireMatch[1]) {
return requireMatch[1];
}
}
return '';
}
}
+9
View File
@@ -0,0 +1,9 @@
export interface CompilationError {
filePath: string;
line: number;
charStart?: number;
charEnd?: number;
message: string;
}
+154 -230
View File
@@ -1,13 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
export interface CompilationError {
filePath: string;
line: number;
charStart?: number;
charEnd?: number;
message: string;
}
import { LuaFile, BlockType, CodeBlock, RequireBlock, ReturnBlock, FunctionBlock, TableBlock } from './CodeBlocks';
import { CompilationError } from './CompilationError';
export interface ScriptCompilerOptions {
sourcePath: string,
@@ -23,6 +17,8 @@ export interface ICompilationLogger {
writeLine(message: string): void;
}
export { CompilationError };
class Metrics {
public totalLinesRead : number = 0;
public totalLinesWritten: number = 0;
@@ -66,9 +62,21 @@ export class ScriptCompiler {
if (entry.isFile() && entry.name.endsWith('.lua')) {
const fullPath = path.join(entry.parentPath, entry.name);
const relativePath = path.relative(this.options.sourcePath, fullPath);
const content = fs.readFileSync(fullPath, 'utf-8');
const parsedFile = this.parseFile(relativePath, content, fullPath, metricsMeter);
const luaFile = LuaFile.createFromFile(fullPath, this.options.onError);
const luaReference = fileReferenceToLuaVariable(relativePath);
const key = luaReference.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', '').toLowerCase();
if(parsedFiles.has(key)){
this.options.onError?.({
filePath: fullPath,
line: 0,
message: `Duplicate file key detected: ${key}. This can happen if two files have different capitalization. Lua is case sensitive, but the compiler treats file keys as case insensitive.`
});
continue;
}
const parsedFile = new ParsedFile(key, luaFile, fullPath);
parsedFiles.set(parsedFile.fileKey, parsedFile);
metricsMeter.filesRead++;
}
@@ -83,6 +91,8 @@ export class ScriptCompiler {
this.options.onError
);
writer.logDependencyTree();
const writeStart = Date.now();
writer.write(includeDevScript, metricsMeter);
const writeEnd = Date.now();
@@ -95,203 +105,11 @@ export class ScriptCompiler {
metricsMeter.totalTimeMs = (end-start);
metricsMeter.log(this.logger);
}
private reportError(filePath: string, line: number, charStart: number | undefined, charEnd: number | undefined, message: string): void {
if (this.options.onError) {
this.options.onError({ filePath, line, charStart, charEnd, message });
}
}
private parseFile(filePath: string, content: string, fullPath: string, metricsMeter: Metrics): ParsedFile {
const dependencies: Dependency[] = [];
const newLines: string[] = [`do --${filePath}`];
content = stripLuaMultilineComments(content);
const lines = content.split('\n').map(line => line.replace(/--.*$/, ''));
const blockStack : string[] = [];
let isInFunction = false;
let foundModuleLevelReturn = false;
let expectingDo = false;
const blockFound = (blockType: string): void => {
blockStack.push(blockType);
if (blockType === 'function') {
isInFunction = true;
}
};
const blockClosed = (): void => {
const closedBlock = blockStack.pop();
if (closedBlock === 'function') {
isInFunction = blockStack.includes('function');
}
};
for (let i = 0; i < lines.length; i++) {
metricsMeter.totalLinesRead++;
let line = lines[i];
const trimmedLine = line.trim();
// Skip comments and blank lines
if (trimmedLine === '' || trimmedLine.startsWith('--')) {
continue;
}
// If we found module-level return, only allow 'end' statements after it
if (foundModuleLevelReturn) {
// Check for 'end' keyword
if (/\bend\b/.test(trimmedLine)) {
const endMatches = trimmedLine.match(/\bend\b/g);
if (endMatches) {
for (let j = 0; j < endMatches.length; j++) {
blockClosed();
}
}
newLines.push(line);
} else {
this.reportError(fullPath, i, undefined, undefined, `Code found after module-level return: ${trimmedLine}`);
newLines.push(line); // Continue processing despite error
}
continue;
}
// Handle require statements
const requireMatch = line.match(/require\(['"](.+?)['"]\)/);
if (requireMatch) {
const textMatch = requireMatch[1];
const requiredModule = fileReferenceToLuaVariable(textMatch);
dependencies.push(new Dependency(textMatch, i, requireMatch.index ?? 0, (requireMatch.index ?? 0) + requireMatch[0].length));
line = line.replace(requireMatch[0], requiredModule);
}
// Track block keywords AND returns - need to process in order they appear
const keywords = [
{ regex: /\bfunction\b/, type: 'function' },
{ regex: /\bif\b/, type: 'if' },
{ regex: /\bfor\b/, type: 'for' },
{ regex: /\bwhile\b/, type: 'while' },
{ regex: /\bdo\b/, type: 'do' },
{ regex: /\bend\b/, type: 'end' },
{ regex: /\breturn\b/, type: 'return' } // Add return to the list!
];
// Find positions of all keywords in the line
const foundKeywords: Array<{ position: number, type: string }> = [];
for (const kw of keywords) {
const matches = [...trimmedLine.matchAll(new RegExp(kw.regex, 'g'))];
for (const match of matches) {
if (match.index !== undefined) {
foundKeywords.push({ position: match.index, type: kw.type });
}
}
}
// Sort by position to process in order
foundKeywords.sort((a, b) => a.position - b.position);
// Process keywords in order
for (const kw of foundKeywords) {
if (kw.type === 'end') {
blockClosed();
expectingDo = false;
} else if (kw.type === 'for' || kw.type === 'while') {
blockFound(kw.type);
expectingDo = true;
} else if (kw.type === 'do') {
if (!expectingDo) {
// Standalone do block
blockFound('do');
}
expectingDo = false;
} else if (kw.type === 'return') {
// Handle return in sequence
if (!isInFunction && !foundModuleLevelReturn) {
// Extract the return value (everything after 'return')
const afterReturnPos = kw.position + 6; // 'return' is 6 chars
const afterReturn = trimmedLine.substring(afterReturnPos).trim();
if (afterReturn === '') {
this.reportError(fullPath, i, afterReturnPos, afterReturnPos, 'Empty return statement at module level');
continue;
}
// Check for multiple return values (commas outside of parentheses/braces/brackets)
let parenDepth = 0;
let braceDepth = 0;
let bracketDepth = 0;
let inString = false;
let stringChar = '';
let hasMultipleValues = false;
for (let j = 0; j < afterReturn.length; j++) {
const char = afterReturn[j];
if (!inString) {
if (char === '"' || char === "'") {
inString = true;
stringChar = char;
} else if (char === '(') {
parenDepth++;
} else if (char === ')') {
parenDepth--;
} else if (char === '{') {
braceDepth++;
} else if (char === '}') {
braceDepth--;
} else if (char === '[') {
bracketDepth++;
} else if (char === ']') {
bracketDepth--;
} else if (char === ',' && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
hasMultipleValues = true;
break;
}
} else {
if (char === stringChar && afterReturn[j - 1] !== '\\') {
inString = false;
}
}
}
if (hasMultipleValues) {
this.reportError(fullPath, i, afterReturnPos, afterReturnPos + afterReturn.length, `Multiple return values not supported: ${trimmedLine}`);
} else {
// Replace return with assignment
const moduleVariable = fileReferenceToLuaVariable(filePath);
const parts = moduleVariable.split('.');
for (let p = 1; p < parts.length; p++) {
const path = parts.slice(0, p + 1).join('.');
newLines.push(`if not ${path} then ${path} = {} end`);
}
line = line.replace(/\breturn\b/, moduleVariable + ' =');
foundModuleLevelReturn = true;
}
}
} else {
// function or if
blockFound(kw.type);
expectingDo = false;
}
}
newLines.push(line);
}
newLines.push(`end --${filePath}`);
return new ParsedFile(filePath, fullPath, newLines, dependencies);
}
}
function stripLuaMultilineComments(content: string): string {
// Matches --[[...]], --[=[...]=], --[==[...]==], etc.
return content.replace(/--\[(=*)\[[\s\S]*?\]\1\]/g, '');
}
class Dependency {
public readonly fileKey: string;
public readonly luaReference: string;
constructor(
public readonly requiredModule: string,
@@ -300,22 +118,11 @@ class Dependency {
public readonly charEnd: number
)
{
this.fileKey = fileReferenceToLuaVariable(requiredModule);
this.luaReference = fileReferenceToLuaVariable(requiredModule);
this.fileKey = this.luaReference.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', '').toLowerCase();
}
}
class ParsedFile {
public readonly fileKey: string;
constructor(
public readonly filePath: string,
public readonly fullPath: string,
public readonly lines: string[],
public readonly dependencies: Dependency[]
) {
this.fileKey = fileReferenceToLuaVariable(filePath);
}
}
function fileReferenceToLuaVariable(fileReference: string): string {
// Remove .lua extension
@@ -333,21 +140,104 @@ function fileReferenceToLuaVariable(fileReference: string): string {
// Split by / to get path parts
const parts = fileReference.split('/');
// Convert to ScriptGlobals.folder.FileName format
let result = LUA_SCRIPT_GLOBAL_KEYWORD;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
// Capitalize first letter of filename
result += '.' + parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
} else {
// Folder names stay lowercase
result += '.' + parts[i];
}
result += '.' + parts[i].toLowerCase();
}
return result;
}
class ParsedFile {
public readonly dependencies: Dependency[] = []
constructor(
public readonly fileKey: string,
public readonly luaFile: LuaFile,
public readonly fullPath: string
){
this.dependencies = ParsedFile.parseDependencies(luaFile);
}
private static parseDependencies(luaFile: CodeBlock): Dependency[] {
// Recursively search for RequireBlocks in the LuaFile and its child blocks
const dependencies: Dependency[] = [];
const searchBlock = (block: CodeBlock) => {
if (block instanceof RequireBlock) {
const requiredString = block.getRequiredString();
if (requiredString) {
dependencies.push(new Dependency(requiredString.toLowerCase(), block.sourceLineNumber ?? 0, block.charStart ?? 0, block.charEnd ?? 0));
}
}
for (const child of block.getChildren()) {
searchBlock(child);
}
}
searchBlock(luaFile);
return dependencies;
}
public replaceRequireWithGlobal(): void {
function replaceRequireInBlock(block: CodeBlock) {
if (block instanceof RequireBlock) {
const requiredString = block.getRequiredString();
if (requiredString) {
const luaReference = fileReferenceToLuaVariable(requiredString);
block.toLines = () => [`${luaReference}\n`];
}
}
for (const child of block.getChildren()) {
replaceRequireInBlock(child);
}
}
for (const child of this.luaFile.getChildren()) {
replaceRequireInBlock(child);
}
}
public replaceModuleReturnWithGlobalAssignement(): void {
const replaceReturnInBlock = (block: CodeBlock) => {
if (block instanceof ReturnBlock) {
const parent = block.getParentBlock();
if (parent) {
const luaReference = fileReferenceToLuaVariable(this.fileKey);
const resultLines : string[] = [];
const splitCount = luaReference.split('.').length;
for(let i = 2; i <= splitCount -1 ; i++){
const partialReference = luaReference.split('.').slice(0, i).join('.');
resultLines.push(`if not ${partialReference} then ${partialReference} = {} end`);
}
resultLines.push(`${luaReference} = ${block.returnParams.join(', ')}`);
block.toLines = () => resultLines.map(line => line + '\n');
}
}
if(block instanceof FunctionBlock || block instanceof TableBlock){
return; // Do not traverse into FunctionBlock or TableBlock
}
for (const child of block.getChildren()) {
replaceReturnInBlock(child);
}
}
for (const child of this.luaFile.getChildren()) {
replaceReturnInBlock(child);
}
}
}
class Writer {
constructor(
public location: string,
@@ -358,8 +248,8 @@ class Writer {
private getStartLines(): string[] {
return [
`-- Transpiled at (UTC): ${new Date().toISOString()}`,
`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}`
`-- Transpiled at (UTC): ${new Date().toISOString()}\n`,
`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}\n`
];
}
@@ -462,8 +352,42 @@ class Writer {
});
}
}
metrics.totalLinesWritten += parsedFile.lines.length;
outputLines.push(...parsedFile.lines);
parsedFile.replaceRequireWithGlobal();
parsedFile.replaceModuleReturnWithGlobalAssignement();
const lines = parsedFile.luaFile.toLines();
const newLineFilteredLines : string[] = [];
function trimEndPreserveNewlines(str: string): string {
return str.replace(/[ \t]+$/gm, '');
}
let wasLastEmpty = false;
let lastEndedWithNewline = false;
for(const line of lines){
if(trimEndPreserveNewlines(line) !== ''){
if(line.trim() === ''){
if(!wasLastEmpty && !lastEndedWithNewline){
newLineFilteredLines.push(line);
wasLastEmpty = true;
}
} else {
newLineFilteredLines.push(line);
wasLastEmpty = false;
lastEndedWithNewline = line.endsWith('\n');
}
}
}
outputLines.push("do -- " + parsedFile.fileKey + "\n");
metrics.totalLinesWritten += newLineFilteredLines.length;
outputLines.push(...newLineFilteredLines);
outputLines.push("\nend -- " + parsedFile.fileKey + "\n");
writtenFiles.add(parsedFile.fileKey);
metrics.filesWritten++;
};
@@ -474,7 +398,7 @@ class Writer {
}
fs.mkdirSync(path.dirname(this.location), { recursive: true });
fs.writeFileSync(this.location, outputLines.join('\n'), 'utf-8');
fs.writeFileSync(this.location, outputLines.join(''), 'utf-8');
if(includeDevScript) {
const devFileLocation = this.location.replace('.lua', '.dev.lua');
+4
View File
@@ -65,6 +65,10 @@ return utils
## Release Notes
### 0.1.0
Complete overhaul of the transpiler for stability and support purpose. <br>
### 0.0.3
- Fixed: Settings filter not correct when opening settings with the extension command.
+1
View File
@@ -47,6 +47,7 @@ async function main() {
});
if (watch) {
await ctx.watch();
console.log('[watch] watching for changes...');
} else {
await ctx.rebuild();
await ctx.dispose();
@@ -138,6 +138,7 @@ async function compileLuaScripts() {
await compiler.compile(includeDevScript);
} catch (err) {
vscode.window.showErrorMessage('Compilation failed: ' + (err as Error).message);
logger.error('Compilation failed: ' + (err as Error).message + '\n' + (err as Error).stack);
}
// Update diagnostics
+11 -3
View File
@@ -13,10 +13,18 @@ inputs:
runs:
using: 'composite'
steps:
- run: |
cd ${{ github.action_path }}
- name: Build action
run: |
npm ci
npm run build
node dist/index.js
shell: bash
working-directory: ${{ github.action_path }}
- name: Run compiler
run: node ${{ github.action_path }}/dist/index.js
shell: bash
working-directory: ${{ github.workspace }}
env:
INPUT_SOURCE-ROOT: ${{ inputs.source-root }}
INPUT_OUTPUT-FILE: ${{ inputs.output-file }}
+487 -2
View File
@@ -1,16 +1,17 @@
{
"name": "gh-scripting-compiler",
"version": "1.0.0",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gh-scripting-compiler",
"version": "1.0.0",
"version": "0.0.1",
"dependencies": {
"@actions/core": "^1.10.0"
},
"devDependencies": {
"esbuild": "^0.27.2",
"ts-node": "^10.9.2",
"typescript": "^5.0.0"
}
@@ -63,6 +64,448 @@
"node": ">=12"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
@@ -189,6 +632,48 @@
"node": ">=0.3.1"
}
},
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",