Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c552cebbb6 | ||
|
|
949d72c394 | ||
|
|
dcd1bf25e7 | ||
|
|
45c988c989 | ||
|
|
2815c3e652 | ||
|
|
4b311f8b4c | ||
|
|
4712330faa | ||
|
|
77ed355011 | ||
|
|
974d4a5e69 | ||
|
|
6c8adfea0d | ||
|
|
16599dce42 | ||
|
|
e1febc63f2 | ||
|
|
c7a65d6fbd | ||
|
|
12d3ad3341 | ||
|
|
0443871583 | ||
|
|
1db8f3ccbb | ||
|
|
83df0813c6 | ||
|
|
ec7109a487 | ||
|
|
929faacbc4 | ||
|
|
f964e5a42d | ||
|
|
1d7389f3e8 | ||
|
|
7a59237d45 | ||
|
|
67c1c197b0 | ||
|
|
54f6f3258f | ||
|
|
7b03863d29 | ||
|
|
c7dd5ca0f0 | ||
|
|
e94ba970dd | ||
|
|
e8728caa9b | ||
|
|
a5e6a9c658 | ||
|
|
d3aaa8cd6f | ||
|
|
ed48c4d2de | ||
|
|
5860f34166 | ||
|
|
c3a605a448 | ||
|
|
b5f0c323a6 | ||
|
|
5fb181d136 | ||
|
|
aad02ccd79 | ||
|
|
d60788c90f | ||
|
|
33f91b138b | ||
|
|
7f6979ea72 | ||
|
|
32714e9f25 | ||
|
|
a48ca7b973 | ||
|
|
377284ac59 | ||
|
|
df79538491 | ||
|
|
16e76d96d3 | ||
|
|
3bbb07de77 | ||
|
|
ba292ad7aa | ||
|
|
afafd6a48e | ||
|
|
204bd19673 |
@@ -0,0 +1,167 @@
|
||||
name: Publish Action Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
action-directory:
|
||||
required: true
|
||||
type: string
|
||||
description: 'Path to the action directory (e.g., github-actions/bundle-script)'
|
||||
package-json-path:
|
||||
required: true
|
||||
type: string
|
||||
description: 'Path to package.json relative to repo root'
|
||||
action-name:
|
||||
required: true
|
||||
type: string
|
||||
description: 'Name of the action for display purposes'
|
||||
prefix:
|
||||
required: true
|
||||
type: string
|
||||
description: 'Prefix for version tags (e.g., bundle-script will create tags like bundle-script/v1.0.0)'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
derive_release:
|
||||
name: Derive release metadata
|
||||
runs-on: ubuntu-latest
|
||||
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('./${{ inputs.package-json-path }}').version")
|
||||
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "${{ inputs.action-name }}/package.json version must be semver X.Y.Z, got: $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS='.' read -r major minor patch <<< "$version"
|
||||
release_tag="${{ inputs.prefix }}/v${version}"
|
||||
rolling_minor_tag="${{ inputs.prefix }}/v${major}.${minor}"
|
||||
rolling_major_tag="${{ inputs.prefix }}/v${major}"
|
||||
|
||||
latest_release_tag=$(git tag --list '${{ inputs.prefix }}/v*.*.*' --sort=-version:refname | head -n 1)
|
||||
|
||||
bump_kind="initial"
|
||||
should_publish="true"
|
||||
|
||||
if [[ -n "$latest_release_tag" ]]; then
|
||||
latest_version=${latest_release_tag#${{ inputs.prefix }}/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 "${{ inputs.action-name }}/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 "## ${{ inputs.action-name }} 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 ${{ inputs.action-name }} 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,20 @@
|
||||
name: Publish Bundle Script Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- github-actions/bundle-script/package.json
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish_action:
|
||||
uses: ./.github/workflows/_publish-action-release.yml
|
||||
with:
|
||||
action-directory: github-actions/bundle-script
|
||||
package-json-path: github-actions/bundle-script/package.json
|
||||
action-name: Bundle Script
|
||||
prefix: bundle-script
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Publish Install Lua Addon Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- github-actions/install-lua-addon/package.json
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
## Verify that when the action has been run it actually has the lua addons installed
|
||||
test_action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: github-actions/install-lua-addon/package-lock.json
|
||||
|
||||
- name: Run Action Locally
|
||||
uses: ./github-actions/install-lua-addon
|
||||
with:
|
||||
destination-path: './some/test/lua-addons'
|
||||
|
||||
- name: Verify lua addons were installed
|
||||
run: |
|
||||
echo "Listing contents of the Lua addons directory"
|
||||
ls -la ./some/test/lua-addons
|
||||
|
||||
echo "Checking if dcs-types addon was installed"
|
||||
if [ -d "./some/test/lua-addons/dcs-types" ]; then
|
||||
echo "✓ dcs-types directory found"
|
||||
else
|
||||
echo "✗ dcs-types directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking if config.json exists in dcs-types"
|
||||
if [ -f "./some/test/lua-addons/dcs-types/config.json" ]; then
|
||||
echo "✓ config.json found"
|
||||
else
|
||||
echo "✗ config.json not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All checks passed!"
|
||||
|
||||
publish_action:
|
||||
needs: test_action
|
||||
uses: ./.github/workflows/_publish-action-release.yml
|
||||
with:
|
||||
action-directory: github-actions/install-lua-addon
|
||||
package-json-path: github-actions/install-lua-addon/package.json
|
||||
action-name: Install Lua Addon
|
||||
prefix: install-lua-addon
|
||||
@@ -0,0 +1,87 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish VS Code Extension
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get version from package.json
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' dutchies-dcs-scripting-tools/package.json)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Version: $VERSION"
|
||||
|
||||
- name: Check if tag exists
|
||||
id: tag_check
|
||||
run: |
|
||||
if git show-ref --verify --quiet "refs/tags/v${{ steps.version.outputs.version }}"; then
|
||||
echo "tag_exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Tag v${{ steps.version.outputs.version }} already exists"
|
||||
else
|
||||
echo "tag_exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Tag v${{ steps.version.outputs.version }} does not exist"
|
||||
fi
|
||||
|
||||
- name: Exit if tag exists
|
||||
if: steps.tag_check.outputs.tag_exists == 'true'
|
||||
run: |
|
||||
echo "::warning::Tag already exists, exiting."
|
||||
exit 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
if: steps.tag_check.outputs.tag_exists == 'false'
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.tag_check.outputs.tag_exists == 'false'
|
||||
run: |
|
||||
cd dutchies-dcs-scripting-tools
|
||||
npm install
|
||||
|
||||
- name: Build extension
|
||||
if: steps.tag_check.outputs.tag_exists == 'false'
|
||||
run: |
|
||||
cd dutchies-dcs-scripting-tools
|
||||
npm run compile
|
||||
|
||||
- name: Package extension
|
||||
if: steps.tag_check.outputs.tag_exists == 'false'
|
||||
run: |
|
||||
npm install
|
||||
cd dutchies-dcs-scripting-tools
|
||||
npm install
|
||||
npm run package
|
||||
npx @vscode/vsce package --no-dependencies
|
||||
mv dutchies-dcs-scripting-tools-${{ steps.version.outputs.version }}.vsix ../dutchies-dcs-scripting-tools-${{ steps.version.outputs.version }}.vsix
|
||||
|
||||
- name: Create tag and release
|
||||
if: steps.tag_check.outputs.tag_exists == 'false'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "v${{ steps.version.outputs.version }}"
|
||||
git push origin "v${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Upload release assets
|
||||
if: steps.tag_check.outputs.tag_exists == 'false'
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
name: Release v${{ steps.version.outputs.version }}
|
||||
body: "VS Code Extension Release"
|
||||
draft: false
|
||||
files: |-
|
||||
dutchies-dcs-scripting-tools-${{ steps.version.outputs.version }}.vsix
|
||||
env:
|
||||
NODE_OPTIONS: '--experimental-fetch'
|
||||
|
||||
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}"
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -13,5 +13,6 @@
|
||||
"cSpell.words": [
|
||||
"dutchie",
|
||||
"dutchies"
|
||||
]
|
||||
],
|
||||
"Lua.workspace.library": [],
|
||||
}
|
||||
Vendored
+21
-60
@@ -4,70 +4,31 @@
|
||||
"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",
|
||||
"dependsOn": "compile",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ Get the extension here: https://marketplace.visualstudio.com/items?itemName=dutc
|
||||
### Features:
|
||||
|
||||
- [x] Compile the mission script just like in the VS Code extension, but in a Github Action.
|
||||
- [x] Install Lua addons and type definitions for scripting linter support on agents
|
||||
|
||||
### Usage:
|
||||
### Compile Script:
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dutchie031/DcsMissionScriptingTools/github-action@action/v1
|
||||
- uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/bundle-script@bundle-script/v1
|
||||
with:
|
||||
source-root: 'src'
|
||||
output-file: 'output/compiled.lua'
|
||||
@@ -46,4 +47,19 @@ jobs:
|
||||
path: output/compiled.lua
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Add Lua Types
|
||||
|
||||
```yaml
|
||||
name: Install Lua Addons
|
||||
on: [push]
|
||||
jobs:
|
||||
install-lua-addons:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/install-lua-addon@install-lua-addon/v1
|
||||
with:
|
||||
destination-path: 'lua-addons'
|
||||
```
|
||||
@@ -0,0 +1,600 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CompilationError, CompilationErrorType } 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 lineCursor = 0;
|
||||
let lineCounter = 0;
|
||||
const file: LuaFile = new LuaFile();
|
||||
let currentBlock: CodeBlock = file;
|
||||
|
||||
function advanceCursor(number: number = 1) {
|
||||
leftCursor += number;
|
||||
lineCursor += number;
|
||||
}
|
||||
|
||||
function newLine() {
|
||||
lineCounter++;
|
||||
lineCursor = 0;
|
||||
}
|
||||
|
||||
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 === '-' && 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') {
|
||||
newLine();
|
||||
}
|
||||
advanceCursor()
|
||||
}
|
||||
advanceCursor(2); // Skip the closing ]]
|
||||
} else {
|
||||
// Comment line, skip to end of line
|
||||
while (leftCursor < fileContent.length && fileContent[leftCursor] !== '\n') {
|
||||
advanceCursor();
|
||||
}
|
||||
if (fileContent[leftCursor] === '\n') {
|
||||
newLine();
|
||||
}
|
||||
advanceCursor();
|
||||
}
|
||||
currentWord = '';
|
||||
continue;
|
||||
}
|
||||
else if (currentChar === '"' || currentChar === "'") {
|
||||
// String literal, skip to closing quote
|
||||
const quoteType = currentChar;
|
||||
advanceCursor();
|
||||
while (leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType) {
|
||||
// Add string but don't process it for keywords
|
||||
currentBlockString += fileContent[leftCursor];
|
||||
advanceCursor();
|
||||
}
|
||||
currentBlockString += quoteType; // Add the closing quote
|
||||
advanceCursor(); // 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 = '';
|
||||
}
|
||||
newLine();
|
||||
advanceCursor();
|
||||
continue;
|
||||
}
|
||||
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 = '';
|
||||
|
||||
// Update charEnd to include the closing parenthesis
|
||||
(currentBlock as RequireBlock).charEnd = lineCursor + 1;
|
||||
|
||||
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;
|
||||
advanceCursor();
|
||||
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++;
|
||||
lineCursor = 0;
|
||||
}
|
||||
advanceCursor();
|
||||
}
|
||||
|
||||
// Find newline or other character
|
||||
let tempCursor = leftCursor;
|
||||
let tempLineCursor = lineCursor;
|
||||
while (tempCursor < fileContent.length && fileContent[tempCursor] !== '\n' && fileContent[tempCursor].trim() === '') {
|
||||
currentBlockString += fileContent[tempCursor];
|
||||
tempCursor++;
|
||||
tempLineCursor++;
|
||||
}
|
||||
|
||||
if (tempCursor < fileContent.length && fileContent[tempCursor] === '\n') {
|
||||
currentBlockString += '\n';
|
||||
lineCounter++;
|
||||
lineCursor = 0;
|
||||
leftCursor = tempCursor + 1;
|
||||
} else if (tempCursor > leftCursor) {
|
||||
// We found whitespace but no newline, so position cursor at last whitespace
|
||||
leftCursor = tempCursor;
|
||||
lineCursor = tempLineCursor;
|
||||
}
|
||||
// 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, Math.max(0, lineCursor - trimmedWord.length), lineCursor);
|
||||
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",
|
||||
type: CompilationErrorType.Syntax
|
||||
});
|
||||
} else {
|
||||
currentBlock = parent;
|
||||
}
|
||||
}
|
||||
|
||||
if (blockToAdd) {
|
||||
currentBlock.childBlocks.push(blockToAdd);
|
||||
currentBlock = blockToAdd;
|
||||
}
|
||||
|
||||
currentWord = '';
|
||||
}
|
||||
advanceCursor();
|
||||
}
|
||||
// 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 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,19 @@
|
||||
|
||||
|
||||
export interface CompilationError {
|
||||
filePath: string;
|
||||
line: number;
|
||||
charStart?: number;
|
||||
charEnd?: number;
|
||||
message: string;
|
||||
type: CompilationErrorType;
|
||||
metaData?: Map<string, any>;
|
||||
}
|
||||
|
||||
export enum CompilationErrorType {
|
||||
Syntax = "Syntax",
|
||||
Semantic = "Semantic",
|
||||
Runtime = "Runtime",
|
||||
DependencyCircular = "DependencyCircular",
|
||||
DependencyNotFound = "DependencyNotFound"
|
||||
}
|
||||
+165
-233
@@ -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, CompilationErrorType } from './CompilationError';
|
||||
|
||||
export interface ScriptCompilerOptions {
|
||||
sourcePath: string,
|
||||
@@ -23,6 +17,8 @@ export interface ICompilationLogger {
|
||||
writeLine(message: string): void;
|
||||
}
|
||||
|
||||
export { CompilationError, CompilationErrorType };
|
||||
|
||||
class Metrics {
|
||||
public totalLinesRead : number = 0;
|
||||
public totalLinesWritten: number = 0;
|
||||
@@ -66,9 +62,22 @@ 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.`,
|
||||
type: CompilationErrorType.Semantic
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedFile = new ParsedFile(key, luaFile, fullPath);
|
||||
|
||||
parsedFiles.set(parsedFile.fileKey, parsedFile);
|
||||
metricsMeter.filesRead++;
|
||||
}
|
||||
@@ -83,6 +92,8 @@ export class ScriptCompiler {
|
||||
this.options.onError
|
||||
);
|
||||
|
||||
writer.logDependencyTree();
|
||||
|
||||
const writeStart = Date.now();
|
||||
writer.write(includeDevScript, metricsMeter);
|
||||
const writeEnd = Date.now();
|
||||
@@ -95,203 +106,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 +119,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 +141,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 +249,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`
|
||||
];
|
||||
}
|
||||
|
||||
@@ -415,7 +306,8 @@ class Writer {
|
||||
this.onError({
|
||||
filePath: file.fullPath,
|
||||
line: 0,
|
||||
message: `Circular dependency detected: ${cycle.map(x => x.split('.').pop()).join(' -> ')}`
|
||||
message: `Circular dependency detected: ${cycle.map(x => x.split('.').pop()).join(' -> ')}`,
|
||||
type: CompilationErrorType.DependencyCircular
|
||||
});
|
||||
}
|
||||
return true;
|
||||
@@ -458,12 +350,52 @@ class Writer {
|
||||
line: dep.requiredAtLine,
|
||||
charStart: dep.charStart,
|
||||
charEnd: dep.charEnd,
|
||||
message: `Missing dependency: ${dep.fileKey}`
|
||||
message: `Missing dependency: ${dep.fileKey}`,
|
||||
type: CompilationErrorType.DependencyNotFound,
|
||||
metaData: new Map(
|
||||
[
|
||||
["dependency", dep.fileKey],
|
||||
]
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
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 +406,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');
|
||||
@@ -488,7 +420,7 @@ class Writer {
|
||||
`-- This script can be referenced in DCS. Compiled script will then be loaded dynamically.`,
|
||||
`-- This way you can test the compiled output without having to re-import the script into the mission every time.`,
|
||||
`-- This file will only have to be re-imported when the name or location of the compiled script file changes.`,
|
||||
`assert(loadfile("${actualFileLocation}"))()`
|
||||
`assert(loadfile([[${actualFileLocation}]]))()`
|
||||
];
|
||||
fs.writeFileSync(devFileLocation, devLines.join('\n'), 'utf-8');
|
||||
this.logger.info(`Development script written to ${devFileLocation}`);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -32,7 +32,7 @@ async function main() {
|
||||
format: 'cjs',
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
sourcesContent: true,
|
||||
platform: 'node',
|
||||
outfile: 'dist/extension.js',
|
||||
external: ['vscode'],
|
||||
@@ -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();
|
||||
|
||||
@@ -42,10 +42,107 @@
|
||||
---@field Easting number
|
||||
---@field Northing number
|
||||
|
||||
do --mission table
|
||||
|
||||
---@class MissionTable
|
||||
---@field drawings Drawings
|
||||
---@field coalition Coalitions
|
||||
---@field triggers Triggers
|
||||
---@field theatre string?
|
||||
---@field version number
|
||||
---@field start_time number
|
||||
---@field goals table?
|
||||
---@field weather table?
|
||||
|
||||
do -- drawings
|
||||
|
||||
---@class Drawings
|
||||
---@field options DrawingOptions
|
||||
---@field layers Array<DrawingLayer>
|
||||
|
||||
---@class DrawingOptions
|
||||
---@field hiddenOnF10Map HiddenOnF10Map
|
||||
|
||||
---@class HiddenOnF10Map
|
||||
---@field Observer HiddenOnF10MapSettings
|
||||
---@field Instructor HiddenOnF10MapSettings
|
||||
---@field ForwardObserver HiddenOnF10MapSettings
|
||||
---@field Spectator HiddenOnF10MapSettings
|
||||
---@field ArtilleryCommander HiddenOnF10MapSettings
|
||||
---@field Pilot HiddenOnF10MapSettings
|
||||
|
||||
---@class HiddenOnF10MapSettings
|
||||
---@field Neutral boolean
|
||||
---@field Blue boolean
|
||||
---@field Red boolean
|
||||
|
||||
---@class DrawingLayer
|
||||
---@field name string
|
||||
---@field visible boolean
|
||||
---@field objects Array<DrawingObject>
|
||||
|
||||
end
|
||||
|
||||
do -- coalitions
|
||||
---@class Coalitions
|
||||
---@field red Coalition
|
||||
---@field blue Coalition
|
||||
---@field neutral Coalition
|
||||
|
||||
---@class Coalition
|
||||
---@field bullseye Vec2
|
||||
---@field name string
|
||||
---@field nav_points Array<NavPoint>
|
||||
---@field country Array<Country>
|
||||
|
||||
---@class NavPoint
|
||||
---@field type string
|
||||
---@field comment string
|
||||
---@field callSignStr string
|
||||
---@field id number
|
||||
---@field properties table
|
||||
|
||||
---@class Country
|
||||
---@field id number
|
||||
---@field name string
|
||||
---@field vehicle Groups
|
||||
---@field plane Groups
|
||||
---@field helicopter Groups
|
||||
---@field static Groups
|
||||
---@field ship Groups
|
||||
|
||||
---@class Groups
|
||||
---@field group Array<table>
|
||||
|
||||
end
|
||||
|
||||
do -- triggers
|
||||
---@class Triggers
|
||||
---@field zones Array<MissionTriggerZone>
|
||||
|
||||
---@class MissionTriggerZone
|
||||
---@field radius number
|
||||
---@field zoneId number
|
||||
---@field properties Array<TriggerZoneProperty>
|
||||
---@field hidden boolean
|
||||
---@field x number
|
||||
---@field y number
|
||||
---@field name string
|
||||
---@field type number
|
||||
---@field heading number
|
||||
---@field verticies Array<Vec2>
|
||||
|
||||
---@class TriggerZoneProperty
|
||||
---@field key string
|
||||
---@field value string
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
do -- env
|
||||
---@class env
|
||||
---@field mission table TODO: Mission
|
||||
---@field mission MissionTable
|
||||
---@field warehouses table
|
||||
---@field info fun(log:string, showMessageBox:boolean?) Prints passed log line with prefix 'info'
|
||||
---@field warning fun(log:string, showMessageBox:boolean?) Prints passed log line with prefix 'warning'
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---@meta LfsTypes
|
||||
|
||||
---@alias lfs.AttributeName
|
||||
---|'dev' -- on Unix systems, this represents the device that the inode resides on. On Windows systems, represents the drive number of the disk containing the file
|
||||
---|'ino' -- on Unix systems, this represents the inode number. On Windows systems this has no meaning
|
||||
---|'mode' -- string representing the associated protection mode (the values could be file, directory, link, socket, named pipe, char device, block device or other)
|
||||
---|'nlink' -- number of hard links to the file
|
||||
---|'uid' -- user-id of owner (Unix only, always 0 on Windows)
|
||||
---|'gid' -- group-id of owner (Unix only, always 0 on Windows)
|
||||
---|'rdev' -- on Unix systems, represents the device type, for special file inodes. On Windows systems represents the same as dev
|
||||
---|'access' -- time of last access
|
||||
---|'modification' -- time of last data modification
|
||||
---|'change' -- time of last file status change
|
||||
---|'size' -- file size, in bytes
|
||||
---|'permissions' -- file permissions string
|
||||
---|'blocks' -- block allocated for file; (Unix only)
|
||||
---|'blksize' -- optimal file system I/O blocksize; (Unix only)
|
||||
|
||||
---@alias lfs.AttributeMode
|
||||
---|'file'
|
||||
---|'directory'
|
||||
---|'link'
|
||||
---|'socket'
|
||||
---|'char device'
|
||||
---|"block device"
|
||||
---|"named pipe"
|
||||
|
||||
---@class lfs.Attributes
|
||||
---@field [lfs.AttributeName] any
|
||||
---@field ['mode'] lfs.AttributeMode
|
||||
|
||||
---@alias lfs.FileMode
|
||||
---| "binary"
|
||||
---| "text"
|
||||
|
||||
---@class lfs.Lock
|
||||
---@field free fun() Releases the lock on the file/directory.
|
||||
|
||||
---@class lfs.DirObject
|
||||
---@field next fun(self: lfs.DirObject): string? Returns a directory entry's name as a string, or `nil` if there are no more entries.
|
||||
---@field close fun(self: lfs.DirObject) Explicitly closes the directory before iteration finishes.
|
||||
|
||||
---@class lfs
|
||||
---@field attributes fun(path:string, result_param: lfs.AttributeName | lfs.Attributes | table): lfs.Attributes? Returns a table with the file attributes corresponding to filepath (or `nil` followed by an error message and a system-dependent error code in case of error). If the second optional argument is given and is a string, then only the value of the named attribute is returned (this use is equivalent to lfs.attributes(filepath)[request_name], but the table is not created and only one attribute is retrieved from the O.S.). if a table is passed as the second argument, it (result_table) is filled with attributes and returned instead of a new table
|
||||
---@field chdir fun(path:string) : boolean?, string? Changes the current working directory to the given path. <br> Returns true in case of success or `nil` plus an error string.
|
||||
---@field lock_dir fun(path:string, seconds_stale: number?) : lfs.Lock?, string? Creates a lockfile (called lockfile.lfs) in path if it does not exist and returns the lock. If the lock already exists checks if it's stale, using the second parameter (default for the second parameter is `INT_MAX`, which in practice means the lock will never be stale. To free the the lock call `lock:free()`. <br>In case of any errors it returns `nil` and the error message. In particular, if the lock exists and is not stale it returns the "File exists" message.
|
||||
---@field currentdir fun(): string?, string? Returns a string with the current working directory or `nil` plus an error string.
|
||||
---@field dir fun(path: string): fun(): string?, lfs.DirObject Lua iterator over the entries of a given directory. Each time the iterator is called with `dir_obj` it returns a directory entry's name as a string, or `nil` if there are no more entries. You can also iterate by calling `dir_obj:next()`, and explicitly close the directory before the iteration finished with `dir_obj:close()`. Raises an error if `path` is not a directory.
|
||||
---@field lock fun(filehandle, mode: string, start?: number, length?: number): boolean?, string? Locks a file or a part of it. The mode can be `'r'` (read/shared lock) or `'w'` (write/exclusive lock). Returns `true` if successful, or `nil` plus an error string in case of error.
|
||||
---@field link fun(old: string, new: string, symlink?: boolean): boolean?, string?, number? Creates a link. If the optional third argument is true, creates a symbolic link; otherwise creates a hard link.
|
||||
---@field mkdir fun(dirname: string): boolean?, string?, number? Creates a new directory. Returns `true` in case of success or `nil`, an error message and a system-dependent error code in case of error.
|
||||
---@field rmdir fun(dirname: string): boolean?, string?, number? Removes an existing directory. Returns `true` in case of success or `nil`, an error message and a system-dependent error code in case of error.
|
||||
---@field setmode fun(file, mode: lfs.FileMode): boolean?, string? Sets the writing mode for a file. The mode can be `'binary'` or `'text'`. Returns `true` followed by the previous mode string, or `nil` followed by an error string in case of error. On non-Windows platforms, setting the mode has no effect and is always returned as `'binary'`.
|
||||
---@field symlinkattributes fun(filepath: string, request_name?: lfs.AttributeName | string): lfs.Attributes? | any Gets information about a symlink itself (not the file it refers to). Identical to `lfs.attributes` but also adds a `target` field containing the filename the symlink points to. On Windows, this is identical to `lfs.attributes`.
|
||||
---@field touch fun(filepath: string, atime?: number, mtime?: number): boolean?, string?, number? Sets access and modification times of a file. Times are in seconds (from `os.time()`). If `mtime` is omitted, `atime` is used; if both are omitted, the current time is used. Returns `true` in case of success or `nil`, an error message and a system-dependent error code in case of error.
|
||||
---@field unlock fun(filehandle, start?: number, length?: number): boolean?, string? Unlocks a file or a part of it. Returns `true` if successful, or `nil` plus an error string in case of error.
|
||||
|
||||
---@class DcsLfs : lfs
|
||||
---@field tempdir fun(): string Returns the DCS temporary directory.
|
||||
---@field writedir fun(): string Returns the Saved Games directory.
|
||||
---@field realpath fun(path: string): string Returns the absolute path of a file.
|
||||
---@field normpath fun(path: string): string Returns the normalized path.
|
||||
---@field md5sum fun(path: string): string Returns the MD5 checksum of the file at the given path.
|
||||
---@field locations fun(): table Returns available drives.
|
||||
|
||||
-- In DCS lfs can be removed, hence the option of it being `nil`.
|
||||
---@type DcsLfs|nil
|
||||
lfs = lfs or nil
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "dutchies-dcs-scripting-tools",
|
||||
"displayName": "Dutchies Dcs Scripting Tools",
|
||||
"description": "Scripting tools to create DCS script and frameworks easier",
|
||||
"version": "0.0.3",
|
||||
"version": "0.2.0",
|
||||
"author": {
|
||||
"name": "dutchie031",
|
||||
"email": "54616262+dutchie031@users.noreply.github.com"
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dutchie031/DcsMissionScriptingTools"
|
||||
"url": "https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
@@ -36,7 +36,7 @@
|
||||
"properties": {
|
||||
"dutchies-dcs-scripting-tools.dcsTypes": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"default": false,
|
||||
"description": "Enable or disable DCS Types (adds types for DCS functions and objects)"
|
||||
},
|
||||
"dutchies-dcs-scripting-tools.spearheadTypes": {
|
||||
@@ -64,7 +64,15 @@
|
||||
"type":"string",
|
||||
"default": "${workspaceFolder}/dist",
|
||||
"description": "Where the compiled Lua files will be output. default: ${workspaceFolder}/dist"
|
||||
}
|
||||
},
|
||||
"dutchies-dcs-scripting-tools.globalRequirables": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": [],
|
||||
"description": "List of global requirable Lua scripts that will be included automatically."
|
||||
}
|
||||
}
|
||||
},
|
||||
"grammars": [
|
||||
@@ -114,7 +122,6 @@
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"watch:compiler": "cd ../compiler && npm run watch",
|
||||
"package": "npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"compile-tests": "tsc -p . --outDir out",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
@@ -124,15 +131,15 @@
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.108.1",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "22.x",
|
||||
"typescript-eslint": "^8.52.0",
|
||||
"eslint": "^9.39.2",
|
||||
"esbuild": "^0.27.2",
|
||||
"@types/vscode": "1.108.1",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/node": "26.4.1",
|
||||
"typescript-eslint": "8.70.0",
|
||||
"eslint": "10.9.1",
|
||||
"esbuild": "0.27.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^5.9.3",
|
||||
"@vscode/test-cli": "^0.0.12",
|
||||
"@vscode/test-electron": "^2.5.2"
|
||||
"typescript": "5.9.3",
|
||||
"@vscode/test-cli": "0.0.15",
|
||||
"@vscode/test-electron": "2.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,75 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
import * as vscode from 'vscode';
|
||||
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler';
|
||||
import * as path from 'path';
|
||||
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger, CompilationErrorType } from 'dcs-script-compiler';
|
||||
import { Logger } from './logger';
|
||||
import * as luaAddonsManager from './lua-addons-manager';
|
||||
|
||||
const luaWorkSpaceSettingKey = "Lua.workspace";
|
||||
const librarySettingsKey = "library";
|
||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler');
|
||||
|
||||
let pluginPath : string | undefined;
|
||||
// Diagnostic codes
|
||||
const DIAGNOSTIC_CODE_MISSING_GLOBAL = 'lua-missing-global-dependency';
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
const luaAddonNames: string[] = [ "dcs-types" ];
|
||||
|
||||
const extensionName = 'dutchies-dcs-scripting-tools';
|
||||
const publisherName = 'dutchie031';
|
||||
const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`;
|
||||
|
||||
let luaAddonsManagerInstance: luaAddonsManager.LuaAddonsManager | undefined;
|
||||
|
||||
// State tracking for addon updates
|
||||
let extensionPath: string | undefined;
|
||||
let workspaceRoot: string | undefined;
|
||||
let luaAddonsTargetPath: string | undefined;
|
||||
let currentExtensionVersion: string | undefined;
|
||||
let isUpdatingAddons = false;
|
||||
|
||||
//TODO:
|
||||
// - ENABLE/DISABLE with settings instead of commands (or both)
|
||||
|
||||
// This method is called when your extension is activated
|
||||
// Your extension is activated the very first time the command is executed
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
pluginPath = context.asAbsolutePath('lua-addons');
|
||||
extensionPath = context.extensionPath;
|
||||
workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
|
||||
if (workspaceRoot === undefined) {
|
||||
vscode.window.showErrorMessage('Workspace root not found. Lua addons manager cannot be initialized.');
|
||||
return;
|
||||
}
|
||||
|
||||
luaAddonsTargetPath = path.join(workspaceRoot, '.vscode' , 'lua-addons');
|
||||
luaAddonsManagerInstance = new luaAddonsManager.LuaAddonsManager(luaAddonsTargetPath, context.extensionPath);
|
||||
|
||||
if (!luaAddonsManagerInstance) {
|
||||
vscode.window.showErrorMessage('Failed to initialize Lua addons manager.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize extension version
|
||||
try {
|
||||
currentExtensionVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||
logger.debug(`Extension version: ${currentExtensionVersion}`);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to determine extension version: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.enable', async () => {
|
||||
enableIntellisense();
|
||||
await enableIntellisense();
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.disable', async () => {
|
||||
disableIntellisense();
|
||||
await disableIntellisense();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -55,6 +91,23 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
})
|
||||
);
|
||||
|
||||
// Register code actions provider for quick fixes
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeActionsProvider('lua', new LuaQuickFixProvider(), {
|
||||
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
|
||||
})
|
||||
);
|
||||
|
||||
// Register command to add global requirable
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'dutchies-dcs-scripting-tools.addGlobalRequirable',
|
||||
async (dependency: string) => {
|
||||
await addGlobalRequirable(dependency);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
vscode.workspace.onDidSaveTextDocument(async(document) => {
|
||||
logger.info(`Document saved: ${document.uri.fsPath} | Language: ${document.languageId}`);
|
||||
if (document.languageId === 'lua') {
|
||||
@@ -71,20 +124,28 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
const config = vscode.workspace.getConfiguration(extensionName);
|
||||
const dcsTypesEnabled = config.get<boolean>('dcsTypes') || false;
|
||||
if (dcsTypesEnabled) {
|
||||
addPluginPathToSettings();
|
||||
await updateLuaAddons();
|
||||
} else {
|
||||
removePluginPathFromSettings();
|
||||
await disableIntellisense();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check and update lua-addons on activation if dcsTypes is enabled
|
||||
await checkAndUpdateAddonsOnStartup();
|
||||
|
||||
logger.info('Dutchies DCS Scripting Tools extension activated');
|
||||
}
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export function deactivate()
|
||||
export async function deactivate()
|
||||
{
|
||||
removePluginPathFromSettings();
|
||||
// Clean up addon paths from settings on deactivation
|
||||
try {
|
||||
await removeVersionedPluginPathsFromSettings();
|
||||
} catch (err) {
|
||||
logger.error(`Error cleaning up on deactivation: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
logger.info('Dutchies DCS Scripting Tools extension deactivated');
|
||||
}
|
||||
|
||||
@@ -138,18 +199,43 @@ 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);
|
||||
}
|
||||
|
||||
const globalRequirables: string[] = config.get<string[]>('globalRequirables') || [];
|
||||
|
||||
// Update diagnostics
|
||||
diagnosticCollection.clear();
|
||||
for (const [filePath, errors] of errorsByFile) {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const diagnostics = errors.map(error => {
|
||||
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||
const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error);
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
return diagnostic;
|
||||
});
|
||||
if (error.type === CompilationErrorType.DependencyNotFound) {
|
||||
const dependencyStr = error.metaData?.get("dependency") ?? undefined;
|
||||
if (dependencyStr) {
|
||||
if (globalRequirables.includes(dependencyStr)) {
|
||||
// Do nothing, it is a globally requirable dependency
|
||||
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||
const diagnostic = new vscode.Diagnostic(range, "Unchecked: Globally marked dependency", vscode.DiagnosticSeverity.Hint);
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
return diagnostic;
|
||||
} else {
|
||||
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||
const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error);
|
||||
diagnostic.code = { value: DIAGNOSTIC_CODE_MISSING_GLOBAL, target: vscode.Uri.parse('https://example.com') };
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
// Store the dependency name for the quick fix to access
|
||||
(diagnostic as any).dependency = dependencyStr;
|
||||
return diagnostic;
|
||||
}
|
||||
}
|
||||
return undefined; //Something weird happened, let's ignore it for now.
|
||||
} else {
|
||||
const range = new vscode.Range(error.line, error.charStart ?? 0, error.line, error.charEnd ?? 0);
|
||||
const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error);
|
||||
diagnostic.source = 'DCS Lua Transpiler';
|
||||
return diagnostic;
|
||||
}
|
||||
}).filter(diagnostic => diagnostic !== undefined);
|
||||
diagnosticCollection.set(uri, diagnostics);
|
||||
}
|
||||
}
|
||||
@@ -161,11 +247,12 @@ async function enableIntellisense() {
|
||||
config.update("dcsTypes", true, vscode.ConfigurationTarget.Workspace);
|
||||
}
|
||||
|
||||
addPluginPathToSettings();
|
||||
await updateLuaAddons();
|
||||
await addPluginPathsToSettings();
|
||||
await vscode.commands.executeCommand(
|
||||
"lua.startServer"
|
||||
);
|
||||
vscode.window.showInformationMessage('Dutchies DCS Scripting Tools enabled.');
|
||||
vscode.window.showInformationMessage(`DCS-Types installed. Version ${currentExtensionVersion}.`);
|
||||
}
|
||||
|
||||
async function disableIntellisense() {
|
||||
@@ -174,32 +261,264 @@ async function disableIntellisense() {
|
||||
config.update("dcsTypes", false, vscode.ConfigurationTarget.Workspace);
|
||||
}
|
||||
|
||||
removePluginPathFromSettings();
|
||||
await removeVersionedPluginPathsFromSettings();
|
||||
if(luaAddonsManagerInstance){
|
||||
await luaAddonsManagerInstance.removeAllExtensions(luaAddonNames);
|
||||
}
|
||||
|
||||
await vscode.commands.executeCommand(
|
||||
"lua.startServer"
|
||||
);
|
||||
vscode.window.showInformationMessage('Dutchies DCS Scripting Tools disabled.');
|
||||
}
|
||||
|
||||
function addPluginPathToSettings() {
|
||||
if (pluginPath) {
|
||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||
const librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||
if (!librarySettings.includes(pluginPath)) {
|
||||
librarySettings.push(pluginPath);
|
||||
luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||
/**
|
||||
* Updates lua-addons by copying them from the extension bundle to the workspace
|
||||
* with versioned naming, cleaning old versions, and updating Lua settings
|
||||
*/
|
||||
async function updateLuaAddons(): Promise<void> {
|
||||
// Prevent concurrent update operations
|
||||
if (isUpdatingAddons) {
|
||||
logger.debug('Addon update already in progress, skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extensionPath || !workspaceRoot || !currentExtensionVersion) {
|
||||
logger.warn('Cannot update addons: extension path, workspace root, or version not available.');
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdatingAddons = true;
|
||||
|
||||
if (luaAddonsManagerInstance === undefined) {
|
||||
logger.warn('Lua Addons Manager instance is not available.');
|
||||
isUpdatingAddons = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Starting lua-addons update...');
|
||||
|
||||
for (const addonName of luaAddonNames) {
|
||||
const packageVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||
const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName);
|
||||
|
||||
if (packageVersion && packageVersion !== addonVersion) {
|
||||
logger.warn(
|
||||
`Version mismatch for addon ${addonName}: package=${packageVersion}, installed=${addonVersion}`
|
||||
);
|
||||
|
||||
luaAddonsManagerInstance.removeExtension(addonName);
|
||||
luaAddonsManagerInstance.installExtension(addonName);
|
||||
}
|
||||
}
|
||||
logger.info(`Lua-addons update completed successfully (version ${currentExtensionVersion}).`);
|
||||
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
logger.error(`Failed to update lua-addons: ${errorMsg}`);
|
||||
vscode.window.showErrorMessage(`Failed to update Dcs Lua Types: ${errorMsg}`);
|
||||
} finally {
|
||||
isUpdatingAddons = false;
|
||||
}
|
||||
}
|
||||
|
||||
function removePluginPathFromSettings() {
|
||||
if (pluginPath) {
|
||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||
const librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||
const libIndex = librarySettings.indexOf(pluginPath);
|
||||
if (libIndex !== -1) {
|
||||
librarySettings.splice(libIndex, 1);
|
||||
luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||
/**
|
||||
* Checks on extension startup if lua-addons need to be updated
|
||||
* Runs only if dcsTypes is enabled in the workspace
|
||||
*/
|
||||
async function checkAndUpdateAddonsOnStartup(): Promise<void> {
|
||||
if (!extensionPath || !workspaceRoot || !currentExtensionVersion) {
|
||||
logger.debug('Skipping addon version check on startup: missing initialization data.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = vscode.workspace.getConfiguration(extensionName);
|
||||
const dcsTypesEnabled = config.get<boolean>('dcsTypes') || false;
|
||||
|
||||
if (!dcsTypesEnabled) {
|
||||
logger.debug('dcsTypes not enabled, skipping addon check on startup.');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Checking lua-addons versions on startup...');
|
||||
|
||||
if (!luaAddonsManagerInstance) {
|
||||
logger.debug('Lua Addons Manager instance is not available, skipping addon version check.');
|
||||
return;
|
||||
}
|
||||
|
||||
let updateNeeded = false;
|
||||
for (const addonName of luaAddonNames) {
|
||||
const packageVersion = await luaAddonsManagerInstance.getPackageVersion();
|
||||
const addonVersion = await luaAddonsManagerInstance.getExtensionVersion(addonName);
|
||||
if (packageVersion && packageVersion !== addonVersion) {
|
||||
updateNeeded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateNeeded) {
|
||||
const userChoice = await vscode.window.showInformationMessage(
|
||||
'DCS-Types are not up to date with the current extension version. Do you want to update them?',
|
||||
{ modal: false },
|
||||
'Update'
|
||||
);
|
||||
|
||||
if (userChoice === 'Update') {
|
||||
await updateLuaAddons();
|
||||
vscode.window.showInformationMessage(`DCS-Types updated to version ${currentExtensionVersion}.`);
|
||||
} else {
|
||||
logger.info('User declined DCS-Types update.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Error checking lua-addons on startup: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
// Don't fail activation if check fails; user can manually enable/disable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an absolute path to a workspace-relative path using ${workspaceFolder}
|
||||
*/
|
||||
function convertToWorkspaceFolderPath(absolutePath: string): string {
|
||||
if (!workspaceRoot) {
|
||||
return absolutePath;
|
||||
}
|
||||
// Normalize paths for comparison (handle both forward and back slashes)
|
||||
const normalizedAbsolute = absolutePath.replace(/\\/g, '/');
|
||||
const normalizedWorkspace = workspaceRoot.replace(/\\/g, '/');
|
||||
|
||||
if (normalizedAbsolute.startsWith(normalizedWorkspace)) {
|
||||
const relativePath = normalizedAbsolute.substring(normalizedWorkspace.length);
|
||||
// Remove leading slash if present
|
||||
const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
|
||||
return `\${workspaceFolder}/${cleanPath}`;
|
||||
}
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the lua-addons directory to the Lua workspace library settings.
|
||||
* Lua Language Server automatically discovers versioned addons within this directory.
|
||||
*/
|
||||
async function addPluginPathsToSettings(): Promise<void> {
|
||||
if (!luaAddonsTargetPath) {
|
||||
logger.warn('Lua addons target path not available.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||
|
||||
const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath);
|
||||
|
||||
if (!librarySettings.includes(workspaceFolderPath)) {
|
||||
librarySettings.push(workspaceFolderPath);
|
||||
logger.debug(`Added lua-addons path to Lua settings: ${workspaceFolderPath}`);
|
||||
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||
} else {
|
||||
logger.debug('Lua-addons path already in settings.');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to add lua-addons path to settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the lua-addons directory from the Lua workspace library settings
|
||||
*/
|
||||
async function removeVersionedPluginPathsFromSettings(): Promise<void> {
|
||||
if (!luaAddonsTargetPath) {
|
||||
logger.debug('Lua addons target path not available, skipping removal.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||
|
||||
const workspaceFolderPath = convertToWorkspaceFolderPath(luaAddonsTargetPath);
|
||||
const filteredSettings = librarySettings.filter(path => path !== workspaceFolderPath);
|
||||
|
||||
if (filteredSettings.length !== librarySettings.length) {
|
||||
await luaSettings.update(librarySettingsKey, filteredSettings, vscode.ConfigurationTarget.Workspace);
|
||||
logger.debug(`Removed lua-addons path from Lua settings: ${workspaceFolderPath}`);
|
||||
} else {
|
||||
logger.debug('Lua-addons path not found in settings.');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Failed to remove lua-addons path from settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Code actions provider for Lua quick fixes
|
||||
*/
|
||||
class LuaQuickFixProvider implements vscode.CodeActionProvider {
|
||||
provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
context: vscode.CodeActionContext,
|
||||
): vscode.CodeAction[] {
|
||||
const codeActions: vscode.CodeAction[] = [];
|
||||
|
||||
// Check for missing global dependency diagnostics
|
||||
for (const diagnostic of context.diagnostics) {
|
||||
const codeValue = typeof diagnostic.code === 'object' && diagnostic.code !== null
|
||||
? (diagnostic.code as any).value
|
||||
: diagnostic.code;
|
||||
|
||||
if (codeValue === DIAGNOSTIC_CODE_MISSING_GLOBAL) {
|
||||
const dependency = (diagnostic as any).dependency;
|
||||
if (dependency) {
|
||||
const action = new vscode.CodeAction(
|
||||
`Mark '${dependency}' as globally available`,
|
||||
vscode.CodeActionKind.QuickFix
|
||||
);
|
||||
action.command = {
|
||||
title: `Add '${dependency}' to global requireables`,
|
||||
command: 'dutchies-dcs-scripting-tools.addGlobalRequirable',
|
||||
arguments: [dependency]
|
||||
};
|
||||
action.diagnostics = [diagnostic];
|
||||
codeActions.push(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return codeActions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a dependency to the global requireables list in workspace settings
|
||||
* @param dependency - The dependency name to add
|
||||
*/
|
||||
async function addGlobalRequirable(dependency: string): Promise<void> {
|
||||
try {
|
||||
const config = vscode.workspace.getConfiguration(extensionName);
|
||||
const globalRequirables = config.get<string[]>('globalRequirables') || [];
|
||||
|
||||
if (!globalRequirables.includes(dependency)) {
|
||||
globalRequirables.push(dependency);
|
||||
await config.update('globalRequirables', globalRequirables, vscode.ConfigurationTarget.Workspace);
|
||||
vscode.window.showInformationMessage(`Added '${dependency}' to global requireables.`);
|
||||
|
||||
// Recompile to update diagnostics
|
||||
await compileLuaScripts();
|
||||
} else {
|
||||
vscode.window.showInformationMessage(`'${dependency}' is already in global requireables.`);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
vscode.window.showErrorMessage(`Failed to add global requirable: ${errorMsg}`);
|
||||
logger.error(`Error adding global requirable: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ export class Logger {
|
||||
this.outputChannel.appendLine(`[${new Date().toISOString()}][ERROR] ${message}`);
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
this.outputChannel.appendLine(`[${new Date().toISOString()}][DEBUG] ${message}`);
|
||||
}
|
||||
|
||||
warn(message: string) {
|
||||
this.outputChannel.appendLine(`[${new Date().toISOString()}][WARN] ${message}`);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.outputChannel.clear();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import path from "path";
|
||||
import * as fs from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
/*
|
||||
* Lua addon naming conventions:
|
||||
* <addon-name>.<major>.<minor>.<patch>
|
||||
*/
|
||||
|
||||
export class LuaAddonsManager {
|
||||
|
||||
/** Path where addons are installed */
|
||||
private targetPath: string;
|
||||
|
||||
/** Path where addon sources are located */
|
||||
private sourceExtensionPath: string;
|
||||
|
||||
private extensionContextPath: string;
|
||||
|
||||
/**
|
||||
* Creates a new LuaAddonsManager instance.
|
||||
*
|
||||
* @param extensionPath - Directory where addons are installed
|
||||
* @param sourceExtensionPath - Directory where addon sources are located
|
||||
*/
|
||||
public constructor(luaTargetPath: string, extensionPath: string) {
|
||||
this.targetPath = luaTargetPath;
|
||||
this.sourceExtensionPath = path.join(extensionPath, "lua-addons");
|
||||
this.extensionContextPath = extensionPath;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the version of an installed addon by name.
|
||||
*
|
||||
* Searches for a folder matching the naming pattern `<name>.<major>.<minor>.<patch>`
|
||||
* and returns the version string.
|
||||
*
|
||||
* @param name - The addon name to search for (without version suffix)
|
||||
* @returns The version string in format "major.minor.patch", or undefined if not found
|
||||
* @throws Never throws, returns undefined if addon not found
|
||||
*/
|
||||
async getExtensionVersion(name: string): Promise<string | undefined> {
|
||||
const folders = await fs.readdir(this.targetPath, { withFileTypes: true });
|
||||
for (const folder of folders) {
|
||||
if (!folder.isDirectory()) { continue; }
|
||||
|
||||
const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!versionMatch) { continue; }
|
||||
|
||||
const addonName = versionMatch[1];
|
||||
if (addonName === name) {
|
||||
return `${versionMatch[2]}.${versionMatch[3]}.${versionMatch[4]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the version from the package.json file in the extension path.
|
||||
*
|
||||
* @returns The version string from package.json
|
||||
* @throws Error if package.json cannot be read or is missing the version field
|
||||
*/
|
||||
async getPackageVersion(): Promise<string | undefined> {
|
||||
try {
|
||||
const packageJsonPath = path.join(this.extensionContextPath,'package.json');
|
||||
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
||||
const packageJson = JSON.parse(content);
|
||||
|
||||
const version = packageJson.version as string | undefined;
|
||||
if (!version) {
|
||||
throw new Error('Version field not found in package.json');
|
||||
}
|
||||
|
||||
return version;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to read extension version: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single addon by name.
|
||||
*
|
||||
* Deletes the addon directory matching the given name, regardless of version.
|
||||
*
|
||||
* @param name - The addon name to remove (without version suffix)
|
||||
* @throws Error if the removal operation fails
|
||||
*/
|
||||
async removeExtension(name: string): Promise<void> {
|
||||
return this.removeAllExtensions([name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple addons by name.
|
||||
*
|
||||
* Scans the extension directory for folders matching the addon naming pattern
|
||||
* `<name>.<major>.<minor>.<patch>` and removes those whose base name is in the
|
||||
* provided list. Removes all versions of matched addons.
|
||||
*
|
||||
* @param names - Array of addon names to remove (without version suffix)
|
||||
* @throws Error if the removal operation fails
|
||||
*/
|
||||
async removeAllExtensions(names: string[]): Promise<void> {
|
||||
const folders = await fs.readdir(this.targetPath, { withFileTypes: true });
|
||||
for (const folder of folders) {
|
||||
if (!folder.isDirectory()) { continue; }
|
||||
|
||||
// Check if folder name matches versioning pattern: name.major.minor.patch
|
||||
const versionMatch = folder.name.match(/^(.+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!versionMatch) { continue; }
|
||||
|
||||
// Extract addon name from the versioned folder name
|
||||
const addonName = versionMatch[1];
|
||||
|
||||
if (names.includes(addonName)) {
|
||||
const folderPath = path.join(this.targetPath, folder.name);
|
||||
await fs.rm(folderPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs an addon from source to the target installation directory.
|
||||
*
|
||||
* Copies the addon directory from sourceExtensionPath to extensionPath.
|
||||
* The source directory name should match the addon name.
|
||||
*
|
||||
* @param name - The addon name/directory to install (without version suffix)
|
||||
* @throws Error if source directory not found or copy operation fails
|
||||
*/
|
||||
async installExtension(name: string): Promise<void> {
|
||||
const sourceFolderPath = path.join(this.sourceExtensionPath, name);
|
||||
|
||||
const version = await this.getPackageVersion();
|
||||
const versionedName = `${name}.${version}`;
|
||||
const targetFolderPath = path.join(this.targetPath, versionedName);
|
||||
|
||||
if (!existsSync(sourceFolderPath)) {
|
||||
throw new Error(`Source extension folder not found: ${sourceFolderPath}`);
|
||||
}
|
||||
await fs.mkdir(targetFolderPath, { recursive: true });
|
||||
await fs.cp(sourceFolderPath, targetFolderPath, { recursive: true });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import * as assert from 'assert';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { existsSync } from 'fs';
|
||||
import { LuaAddonsManager } from '../lua-addons-manager';
|
||||
|
||||
/**
|
||||
* Integration tests for LuaAddonsManager
|
||||
* Uses temporary directories to avoid polluting the file system
|
||||
*/
|
||||
|
||||
suite('LuaAddonsManager', () => {
|
||||
let tempDir: string;
|
||||
|
||||
suiteSetup(async () => {
|
||||
tempDir = path.join(os.tmpdir(), `dcs-test-${Date.now()}-${Math.random()}`);
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
if (existsSync(tempDir)) {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
suite('getExtensionVersion', () => {
|
||||
test('should return undefined if addon not found', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getExtensionVersion-1');
|
||||
const sourceDir = path.join(tempDir, 'getExtensionVersion-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getExtensionVersion('nonexistent');
|
||||
|
||||
assert.strictEqual(version, undefined);
|
||||
});
|
||||
|
||||
test('should return version string for installed addon', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getExtensionVersion-2');
|
||||
const sourceDir = path.join(tempDir, 'getExtensionVersion-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'dcs-types.1.2.3'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getExtensionVersion('dcs-types');
|
||||
|
||||
assert.strictEqual(version, '1.2.3');
|
||||
});
|
||||
|
||||
test('should return first matching version if multiple versions exist', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getExtensionVersion-3');
|
||||
const sourceDir = path.join(tempDir, 'getExtensionVersion-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon.2.0.0'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getExtensionVersion('addon');
|
||||
|
||||
// Should return one of the versions (first found)
|
||||
assert.match(version!, /^[12]\.0\.0$/);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getPackageVersion', () => {
|
||||
test('should read version from package.json', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getPackageVersion-1');
|
||||
const sourceDir = path.join(tempDir, 'getPackageVersion-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const packageJson = path.join(extensionDir, 'package.json');
|
||||
await fs.writeFile(packageJson, JSON.stringify({ version: '2.1.0' }));
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
const version = await manager.getPackageVersion();
|
||||
|
||||
assert.strictEqual(version, '2.1.0');
|
||||
});
|
||||
|
||||
test('should throw error if package.json not found', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getPackageVersion-2');
|
||||
const sourceDir = path.join(tempDir, 'getPackageVersion-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await assert.rejects(
|
||||
() => manager.getPackageVersion(),
|
||||
/Failed to read extension version/
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error if version field missing', async () => {
|
||||
const extensionDir = path.join(tempDir, 'getPackageVersion-3');
|
||||
const sourceDir = path.join(tempDir, 'getPackageVersion-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const packageJson = path.join(extensionDir, 'package.json');
|
||||
await fs.writeFile(packageJson, JSON.stringify({ name: 'test' }));
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await assert.rejects(
|
||||
() => manager.getPackageVersion(),
|
||||
/Version field not found/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('removeExtension', () => {
|
||||
test('should remove addon by name', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeExtension-1');
|
||||
const sourceDir = path.join(tempDir, 'removeExtension-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeExtension('addon1');
|
||||
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.strictEqual(remaining.length, 0);
|
||||
});
|
||||
|
||||
test('should not affect other addons', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeExtension-2');
|
||||
const sourceDir = path.join(tempDir, 'removeExtension-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeExtension('addon1');
|
||||
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.strictEqual(remaining.length, 1);
|
||||
assert.strictEqual(remaining[0], 'addon2.1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
suite('removeAllExtensions', () => {
|
||||
test('should remove multiple addons by name', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-1');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon3.1.0.0'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeAllExtensions(['addon1', 'addon2']);
|
||||
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['addon3.1.0.0']);
|
||||
});
|
||||
|
||||
test('should delete all versions of matching addons', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-2');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.1'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.2'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.0.0.3'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon2.1.0.0'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeAllExtensions(['addon1']);
|
||||
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['addon2.1.0.0']);
|
||||
});
|
||||
|
||||
test('should ignore folders with invalid version format', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-3');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'addon1.1.0.0'), { recursive: true });
|
||||
await fs.mkdir(path.join(extensionDir, 'invalid-folder'), { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.removeAllExtensions(['addon1']);
|
||||
|
||||
const remaining = await fs.readdir(extensionDir);
|
||||
assert.deepStrictEqual(remaining.sort(), ['invalid-folder']);
|
||||
});
|
||||
|
||||
test('should not throw if addon does not exist', async () => {
|
||||
const extensionDir = path.join(tempDir, 'removeAllExtensions-4');
|
||||
const sourceDir = path.join(tempDir, 'removeAllExtensions-4-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
// Should not throw
|
||||
await manager.removeAllExtensions(['nonexistent']);
|
||||
});
|
||||
});
|
||||
|
||||
suite('installExtension', () => {
|
||||
test('should copy addon from source to target', async () => {
|
||||
const extensionDir = path.join(tempDir, 'installExtension-1');
|
||||
const sourceDir = path.join(tempDir, 'installExtension-1-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const sourceAddon = path.join(sourceDir, 'addon1');
|
||||
await fs.mkdir(sourceAddon, { recursive: true });
|
||||
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'content');
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.installExtension('addon1');
|
||||
|
||||
const targetAddon = path.join(extensionDir, 'addon1');
|
||||
assert.strictEqual(existsSync(targetAddon), true);
|
||||
assert.strictEqual(existsSync(path.join(targetAddon, 'file.lua')), true);
|
||||
});
|
||||
|
||||
test('should throw error if source directory does not exist', async () => {
|
||||
const extensionDir = path.join(tempDir, 'installExtension-2');
|
||||
const sourceDir = path.join(tempDir, 'installExtension-2-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await assert.rejects(
|
||||
() => manager.installExtension('nonexistent'),
|
||||
/Source extension folder not found/
|
||||
);
|
||||
});
|
||||
|
||||
test('should copy nested directories recursively', async () => {
|
||||
const extensionDir = path.join(tempDir, 'installExtension-3');
|
||||
const sourceDir = path.join(tempDir, 'installExtension-3-source');
|
||||
await fs.mkdir(extensionDir, { recursive: true });
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
const sourceAddon = path.join(sourceDir, 'addon1');
|
||||
await fs.mkdir(path.join(sourceAddon, 'subdir'), { recursive: true });
|
||||
await fs.writeFile(path.join(sourceAddon, 'file.lua'), 'root');
|
||||
await fs.writeFile(path.join(sourceAddon, 'subdir', 'file.lua'), 'nested');
|
||||
|
||||
const manager = new LuaAddonsManager(extensionDir, sourceDir);
|
||||
await manager.installExtension('addon1');
|
||||
|
||||
const targetAddon = path.join(extensionDir, 'addon1');
|
||||
assert.strictEqual(existsSync(path.join(targetAddon, 'file.lua')), true);
|
||||
assert.strictEqual(existsSync(path.join(targetAddon, 'subdir', 'file.lua')), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Generated
-304
@@ -1,304 +0,0 @@
|
||||
{
|
||||
"name": "gh-scripting-compiler",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gh-scripting-compiler",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/http-client": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/exec": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
|
||||
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/io": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/http-client": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
|
||||
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tunnel": "^0.0.6",
|
||||
"undici": "^5.25.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/io": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
|
||||
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/busboy": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
|
||||
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
|
||||
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
|
||||
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
|
||||
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gh-scripting-compiler",
|
||||
"version": "0.0.1",
|
||||
"version": "1.1.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "node esbuild.js",
|
||||
@@ -8,11 +8,13 @@
|
||||
"package": "node esbuild.js --production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0"
|
||||
"@actions/core": "3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.0.0",
|
||||
"esbuild": "^0.27.2"
|
||||
"typescript": "7.0.2",
|
||||
"esbuild": "0.28.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: 'Install Lua Addon'
|
||||
description: 'Installs DCS Lua addons and type definitions for scripting.'
|
||||
inputs:
|
||||
destination-path:
|
||||
description: 'The destination path in the repository where lua-addons should be installed.'
|
||||
required: true
|
||||
default: 'lua-addons'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Build action
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
shell: bash
|
||||
working-directory: ${{ github.action_path }}
|
||||
|
||||
- name: Install lua-addons
|
||||
run: node ${{ github.action_path }}/dist/index.js
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
INPUT_DESTINATION-PATH: ${{ inputs.destination-path }}
|
||||
@@ -0,0 +1,59 @@
|
||||
const esbuild = require("esbuild");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const production = process.argv.includes('--production');
|
||||
const watch = process.argv.includes('--watch');
|
||||
|
||||
async function copyDirectory(src, dest) {
|
||||
await fs.promises.mkdir(dest, { recursive: true });
|
||||
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
await fs.promises.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Copy lua-addons to dist directory
|
||||
const luaAddonsSource = path.join(__dirname, '../../dutchies-dcs-scripting-tools/lua-addons');
|
||||
const luaAddonsDest = path.join(__dirname, 'dist/lua-addons');
|
||||
|
||||
if (fs.existsSync(luaAddonsSource)) {
|
||||
await copyDirectory(luaAddonsSource, luaAddonsDest);
|
||||
console.log('Lua addons copied to dist/');
|
||||
}
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [
|
||||
'src/index.ts'
|
||||
],
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
platform: 'node',
|
||||
outfile: 'dist/index.js',
|
||||
external: ['@actions/core'],
|
||||
logLevel: 'silent',
|
||||
});
|
||||
if (watch) {
|
||||
await ctx.watch();
|
||||
} else {
|
||||
await ctx.rebuild();
|
||||
await ctx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
+1145
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "gh-lua-addon-installer",
|
||||
"version": "1.1.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "node esbuild.js",
|
||||
"watch": "node esbuild.js --watch",
|
||||
"package": "node esbuild.js --production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "7.0.2",
|
||||
"esbuild": "0.28.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
async function copyDirectory(src: string, dest: string): Promise<void> {
|
||||
await fs.mkdir(dest, { recursive: true });
|
||||
const entries = await fs.readdir(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const destinationPath = core.getInput('destination-path');
|
||||
if (!destinationPath) {
|
||||
core.setFailed('Destination path is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reference lua-addons bundled in dist directory
|
||||
// __dirname is dist/, lua-addons is bundled alongside
|
||||
const bundledAddonsDir = path.resolve(__dirname, './lua-addons');
|
||||
|
||||
// Verify bundled addons directory exists
|
||||
if (!existsSync(bundledAddonsDir)) {
|
||||
core.setFailed(`Lua-addons directory not found at: ${bundledAddonsDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the workspace root and resolve destination path
|
||||
const workspaceRoot = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
const absoluteDestinationPath = path.join(workspaceRoot, destinationPath);
|
||||
|
||||
core.info(`Copying lua-addons to: ${absoluteDestinationPath}`);
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
await fs.mkdir(absoluteDestinationPath, { recursive: true });
|
||||
|
||||
// Copy all bundled addons to the destination
|
||||
const addons = await fs.readdir(bundledAddonsDir, { withFileTypes: true });
|
||||
for (const addon of addons) {
|
||||
if (addon.isDirectory()) {
|
||||
const sourceAddonDir = path.join(bundledAddonsDir, addon.name);
|
||||
const destAddonDir = path.join(absoluteDestinationPath, addon.name);
|
||||
|
||||
core.info(`Installing addon: ${addon.name}`);
|
||||
await copyDirectory(sourceAddonDir, destAddonDir);
|
||||
core.info(`✓ ${addon.name} installed`);
|
||||
}
|
||||
}
|
||||
|
||||
core.info('Lua addons installation completed successfully.');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
core.setFailed(error.message);
|
||||
} else {
|
||||
core.setFailed('An unknown error occurred');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"types": ["node"],
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Generated
+787
-179
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -7,7 +7,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "22.x",
|
||||
"@types/node": "^22.20.2",
|
||||
"@types/vscode": "^1.108.1",
|
||||
"@vscode/test-cli": "^0.0.12",
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
|
||||
Reference in New Issue
Block a user