Merge branch 'fix-compiler'
publish-vs-code-extensions.yml / Publish VS Code Extension (push) Canceled after 0s
publish-vs-code-extensions.yml / Publish VS Code Extension (push) Canceled after 0s
This commit is contained in:
@@ -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
|
||||
|
||||
Vendored
-2
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -16,7 +16,7 @@
|
||||
"${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js",
|
||||
"${workspaceFolder}/compiler/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
"preLaunchTask": "watch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+20
-60
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
export interface CompilationError {
|
||||
filePath: string;
|
||||
line: number;
|
||||
charStart?: number;
|
||||
charEnd?: number;
|
||||
message: string;
|
||||
}
|
||||
+154
-230
@@ -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');
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user