Compare commits
39
Commits
31a5a99d0f
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
// for the documentation about the extensions.json format
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"connor4312.esbuild-problem-matchers",
|
|
||||||
"ms-vscode.extension-test-runner",
|
|
||||||
"sumneko.lua"
|
"sumneko.lua"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
"${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js",
|
"${workspaceFolder}/dutchies-dcs-scripting-tools/dist/**/*.js",
|
||||||
"${workspaceFolder}/compiler/dist/**/*.js"
|
"${workspaceFolder}/compiler/dist/**/*.js"
|
||||||
],
|
],
|
||||||
"preLaunchTask": "${defaultBuildTask}"
|
"preLaunchTask": "watch"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+21
-60
@@ -4,70 +4,31 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "watch",
|
"label": "compile",
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "watch-tests",
|
"script": "compile",
|
||||||
"problemMatcher": "$tsc-watch",
|
"group": {
|
||||||
"isBackground": true,
|
"kind": "build",
|
||||||
"presentation": {
|
"isDefault": true
|
||||||
"reveal": "never",
|
|
||||||
"group": "watchers"
|
|
||||||
},
|
},
|
||||||
"group": "build"
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/dutchies-dcs-scripting-tools"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "tasks: watch-tests",
|
"label": "watch",
|
||||||
"dependsOn": [
|
"type": "npm",
|
||||||
"npm: watch",
|
"script": "watch",
|
||||||
"npm: watch-tests"
|
"dependsOn": "compile",
|
||||||
],
|
"isBackground": true,
|
||||||
"problemMatcher": []
|
"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:
|
### Features:
|
||||||
|
|
||||||
- [x] Compile the mission script just like in the VS Code extension, but in a Github Action.
|
- [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
|
```yaml
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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:
|
with:
|
||||||
source-root: 'src'
|
source-root: 'src'
|
||||||
output-file: 'output/compiled.lua'
|
output-file: 'output/compiled.lua'
|
||||||
@@ -46,4 +47,19 @@ jobs:
|
|||||||
path: output/compiled.lua
|
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,579 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { CompilationError } from './CompilationError';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Block types.
|
||||||
|
|
||||||
|
Require is a special case as it's technically a function call, but it's a special use case.
|
||||||
|
*/
|
||||||
|
export enum BlockType {
|
||||||
|
CodeTextBlock,
|
||||||
|
Function,
|
||||||
|
If,
|
||||||
|
While,
|
||||||
|
For,
|
||||||
|
Do,
|
||||||
|
Return,
|
||||||
|
Require,
|
||||||
|
Table,
|
||||||
|
File
|
||||||
|
}
|
||||||
|
|
||||||
|
const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end', 'local', 'else', 'elseif'];
|
||||||
|
function isKeyWord(word: string): boolean {
|
||||||
|
return keywords.includes(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIdentifierCharacter(char: string): boolean {
|
||||||
|
return /[A-Za-z0-9_]/.test(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trims trailing spaces and tabs, but preserves newlines
|
||||||
|
*/
|
||||||
|
function trimEndPreserveNewlines(str: string): string {
|
||||||
|
return str.replace(/[ \t]+$/gm, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class CodeBlock {
|
||||||
|
protected childBlocks: CodeBlock[] = [];
|
||||||
|
private parentBlock?: CodeBlock;
|
||||||
|
private blockType: BlockType;
|
||||||
|
|
||||||
|
public readonly sourceLineNumber?: number;
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, blockType: BlockType, parentBlock?: CodeBlock) {
|
||||||
|
this.sourceLineNumber = sourceLineNumber;
|
||||||
|
this.parentBlock = parentBlock;
|
||||||
|
this.blockType = blockType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getChildren(): CodeBlock[] {
|
||||||
|
return this.childBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract toLines(): string[];
|
||||||
|
|
||||||
|
getParentBlock(): CodeBlock | undefined {
|
||||||
|
return this.parentBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFromFile(luaFilePath: string, onError?: (error: CompilationError) => void): LuaFile {
|
||||||
|
|
||||||
|
if (!fs.existsSync(luaFilePath)) {
|
||||||
|
throw new Error(`File not found: ${luaFilePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(luaFilePath, 'utf-8');
|
||||||
|
|
||||||
|
let leftCursor = 0;
|
||||||
|
let lineCounter = 0;
|
||||||
|
const file: LuaFile = new LuaFile();
|
||||||
|
let currentBlock: CodeBlock = file;
|
||||||
|
|
||||||
|
let currentWord = '';
|
||||||
|
let currentBlockString = '';
|
||||||
|
|
||||||
|
while (leftCursor < fileContent.length) {
|
||||||
|
const currentChar: string = fileContent[leftCursor];
|
||||||
|
if (isIdentifierCharacter(currentChar)) {
|
||||||
|
currentWord += currentChar;
|
||||||
|
} else {
|
||||||
|
currentWord = '';
|
||||||
|
}
|
||||||
|
currentBlockString += currentChar;
|
||||||
|
const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : '';
|
||||||
|
|
||||||
|
if (currentChar === '\n') {
|
||||||
|
lineCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChar === '-' && nextChar === '-') {
|
||||||
|
currentBlockString = currentBlockString.slice(0, -1); // Remove the '-' from the block string
|
||||||
|
const nextNextChar = leftCursor < fileContent.length - 2 ? fileContent[leftCursor + 2] : '';
|
||||||
|
if (nextNextChar === '[') {
|
||||||
|
// Multiline comment, skip to closing ]]
|
||||||
|
leftCursor += 3; // Skip the --[
|
||||||
|
while (leftCursor < fileContent.length && !(fileContent[leftCursor] === ']' && fileContent[leftCursor + 1] === ']')) {
|
||||||
|
if (fileContent[leftCursor] === '\n') {
|
||||||
|
lineCounter++;
|
||||||
|
}
|
||||||
|
leftCursor++;
|
||||||
|
}
|
||||||
|
leftCursor += 2; // Skip the closing ]]
|
||||||
|
} else {
|
||||||
|
// Comment line, skip to end of line
|
||||||
|
while (leftCursor < fileContent.length && fileContent[leftCursor] !== '\n') {
|
||||||
|
leftCursor++;
|
||||||
|
}
|
||||||
|
if (fileContent[leftCursor] === '\n') {
|
||||||
|
lineCounter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentWord = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (currentChar === '"' || currentChar === "'") {
|
||||||
|
// String literal, skip to closing quote
|
||||||
|
const quoteType = currentChar;
|
||||||
|
leftCursor++;
|
||||||
|
while (leftCursor < fileContent.length && fileContent[leftCursor] !== quoteType) {
|
||||||
|
// Add string but don't process it for keywords
|
||||||
|
currentBlockString += fileContent[leftCursor];
|
||||||
|
leftCursor++;
|
||||||
|
}
|
||||||
|
currentBlockString += quoteType; // Add the closing quote
|
||||||
|
leftCursor++; // Skip the closing quote
|
||||||
|
currentWord = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In return block, read till the end of the return block
|
||||||
|
// else if (currentBlock.blockType === BlockType.Return) {
|
||||||
|
|
||||||
|
// //Continue reading until there's at least a new word
|
||||||
|
// }
|
||||||
|
else if (currentChar === "\n") {
|
||||||
|
// Process line
|
||||||
|
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
|
||||||
|
//currentBlockString += '\n'; // Add the newline back for the line block
|
||||||
|
if (currentBlockString !== '') {
|
||||||
|
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(lineBlock);
|
||||||
|
currentBlockString = '';
|
||||||
|
currentWord = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (currentChar === ")" && currentBlock.blockType === BlockType.Require) {
|
||||||
|
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
|
||||||
|
//currentBlockString += '\n'; // Add the newline back for the line block as it will be skipped otherwise
|
||||||
|
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(lineBlock);
|
||||||
|
currentBlockString = '';
|
||||||
|
|
||||||
|
currentBlock = currentBlock.getParentBlock()!;
|
||||||
|
}
|
||||||
|
//Table blocks
|
||||||
|
else if (currentChar === "{") {
|
||||||
|
// Only create LineBlock if there's content before the brace
|
||||||
|
if (trimEndPreserveNewlines(currentBlockString) !== '' && trimEndPreserveNewlines(currentBlockString) !== '{') {
|
||||||
|
const currentLineBlock = new CodeTextBlock(lineCounter, trimEndPreserveNewlines(currentBlockString.slice(0, -1)), currentBlock);
|
||||||
|
currentBlock.childBlocks.push(currentLineBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
let leadingWhiteSpace = '';
|
||||||
|
const braceIndex = currentBlockString.lastIndexOf('{');
|
||||||
|
if (braceIndex > 0) {
|
||||||
|
let wsStart = braceIndex - 1;
|
||||||
|
while (wsStart >= 0 && /[ \t]/.test(currentBlockString[wsStart])) {
|
||||||
|
wsStart--;
|
||||||
|
}
|
||||||
|
leadingWhiteSpace = currentBlockString.substring(wsStart + 1, braceIndex);
|
||||||
|
}
|
||||||
|
currentBlockString = leadingWhiteSpace + '{'; // Start the new block string with the opening brace
|
||||||
|
|
||||||
|
const tableBlock = new TableBlock(lineCounter, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(tableBlock);
|
||||||
|
currentBlock = tableBlock;
|
||||||
|
|
||||||
|
let braceCounter = 1;
|
||||||
|
leftCursor++;
|
||||||
|
while (leftCursor < fileContent.length && braceCounter > 0) {
|
||||||
|
const char = fileContent[leftCursor];
|
||||||
|
currentBlockString += char;
|
||||||
|
if (char === '{') {
|
||||||
|
braceCounter++;
|
||||||
|
} else if (char === '}') {
|
||||||
|
braceCounter--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '\n') {
|
||||||
|
let block = trimEndPreserveNewlines(currentBlockString);
|
||||||
|
const lineBlock = new CodeTextBlock(lineCounter, block, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(lineBlock);
|
||||||
|
currentBlockString = '';
|
||||||
|
lineCounter++;
|
||||||
|
}
|
||||||
|
leftCursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find newline or other character
|
||||||
|
let tempCursor = leftCursor;
|
||||||
|
while (tempCursor < fileContent.length && fileContent[tempCursor] !== '\n' && fileContent[tempCursor].trim() === '') {
|
||||||
|
currentBlockString += fileContent[tempCursor];
|
||||||
|
tempCursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempCursor < fileContent.length && fileContent[tempCursor] === '\n') {
|
||||||
|
currentBlockString += '\n';
|
||||||
|
lineCounter++;
|
||||||
|
} else if (tempCursor > leftCursor) {
|
||||||
|
// We found whitespace but no newline, so position cursor at last whitespace
|
||||||
|
leftCursor = tempCursor - 1;
|
||||||
|
}
|
||||||
|
// else: no whitespace after table, leave leftCursor where it is
|
||||||
|
|
||||||
|
if (trimEndPreserveNewlines(currentBlockString) !== '') {
|
||||||
|
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(lineBlock);
|
||||||
|
currentBlockString = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBlock = currentBlock.getParentBlock()!;
|
||||||
|
currentWord = ''; // Reset word accumulator after table block
|
||||||
|
continue; // Skip the leftCursor++ at the end of the loop since we already incremented it
|
||||||
|
}
|
||||||
|
else if (currentWord !== '' && !isIdentifierCharacter(nextChar)) {
|
||||||
|
// End of a word, check for keywords
|
||||||
|
const trimmedWord = currentWord.trim();
|
||||||
|
|
||||||
|
|
||||||
|
if (currentBlock.blockType === BlockType.Return && isKeyWord(trimmedWord) && trimmedWord !== 'return') {
|
||||||
|
// We're in a ReturnBlock and hit a keyword at the statement boundary
|
||||||
|
// Extract return params and exit the block
|
||||||
|
(currentBlock as ReturnBlock).extractReturnParams();
|
||||||
|
currentBlock = currentBlock.getParentBlock()!;
|
||||||
|
// Don't clear currentBlockString - let normal flow handle the newline finalization
|
||||||
|
// Continue to process this keyword normally
|
||||||
|
}
|
||||||
|
|
||||||
|
let blockToAdd: CodeBlock | null = null;
|
||||||
|
|
||||||
|
if (trimmedWord === 'if') {
|
||||||
|
blockToAdd = new IfBlock(lineCounter, currentBlock);
|
||||||
|
}
|
||||||
|
else if (trimmedWord === 'while') {
|
||||||
|
blockToAdd = new WhileBlock(lineCounter, currentBlock);
|
||||||
|
}
|
||||||
|
else if (trimmedWord === 'for') {
|
||||||
|
blockToAdd = new ForBlock(lineCounter, currentBlock);
|
||||||
|
}
|
||||||
|
else if (trimmedWord === 'do') {
|
||||||
|
const isForOrWhile = currentBlock.blockType === BlockType.While || currentBlock.blockType === BlockType.For;
|
||||||
|
if(isForOrWhile && (currentBlock as WhileBlock | ForBlock).passedDoStatement == false) {
|
||||||
|
// If we're in a While or For block and we've not yet passed a 'do' statement, mark it as passed and don't create a new block
|
||||||
|
(currentBlock as WhileBlock | ForBlock).passedDoStatement = true;
|
||||||
|
} else {
|
||||||
|
blockToAdd = new DoBlock(lineCounter, currentBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (trimmedWord === 'return') {
|
||||||
|
// Add any text before 'return' to parent block, but preserve newlines
|
||||||
|
const beforeReturn = currentBlockString.slice(0, -trimmedWord.length);
|
||||||
|
if (beforeReturn.trim()) {
|
||||||
|
const line = new CodeTextBlock(lineCounter, beforeReturn, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(line);
|
||||||
|
}
|
||||||
|
blockToAdd = new ReturnBlock(lineCounter, currentBlock);
|
||||||
|
// Start the return block content with 'return' keyword
|
||||||
|
currentBlockString = trimmedWord;
|
||||||
|
}
|
||||||
|
else if (trimmedWord === 'function') {
|
||||||
|
blockToAdd = new FunctionBlock(lineCounter, currentBlock);
|
||||||
|
}
|
||||||
|
else if (trimmedWord === 'require') {
|
||||||
|
const beforeRequire = currentBlockString.slice(0, -trimmedWord.length);
|
||||||
|
if (beforeRequire.trim()) {
|
||||||
|
const line = new CodeTextBlock(lineCounter, beforeRequire, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor);
|
||||||
|
currentBlockString = trimmedWord; // Start the require block content with 'require' keyword
|
||||||
|
}
|
||||||
|
else if (trimmedWord === 'end') {
|
||||||
|
const parent = currentBlock.getParentBlock();
|
||||||
|
if (!parent) {
|
||||||
|
onError?.({
|
||||||
|
filePath: luaFilePath,
|
||||||
|
line: fileContent.substring(0, leftCursor).split('\n').length - 1,
|
||||||
|
message: "Unexpected 'end' without matching block start"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
currentBlock = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockToAdd) {
|
||||||
|
currentBlock.childBlocks.push(blockToAdd);
|
||||||
|
currentBlock = blockToAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWord = '';
|
||||||
|
}
|
||||||
|
leftCursor++;
|
||||||
|
}
|
||||||
|
// Handle case where file ends while in a ReturnBlock
|
||||||
|
if (currentBlock.blockType === BlockType.Return) {
|
||||||
|
if (trimEndPreserveNewlines(currentBlockString) !== '') {
|
||||||
|
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
|
||||||
|
currentBlock.childBlocks.push(lineBlock);
|
||||||
|
}
|
||||||
|
(currentBlock as ReturnBlock).extractReturnParams();
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CodeTextBlock extends CodeBlock {
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, private line: string, parent?: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.CodeTextBlock, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
return [this.line];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IfBlock extends CodeBlock {
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.If, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WhileBlock extends CodeBlock {
|
||||||
|
|
||||||
|
public passedDoStatement: boolean = false;
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.While, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ForBlock extends CodeBlock {
|
||||||
|
|
||||||
|
public passedDoStatement: boolean = false;
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.For, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class LuaFile extends CodeBlock {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(0, BlockType.File);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DoBlock extends CodeBlock {
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.Do, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReturnBlock extends CodeBlock {
|
||||||
|
|
||||||
|
public readonly returnParams: string[] = [];
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.Return, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts return parameters from the return statement.
|
||||||
|
* Splits by commas while respecting nesting of {} and ().
|
||||||
|
* Populates the returnParams array.
|
||||||
|
*/
|
||||||
|
extractReturnParams(): void {
|
||||||
|
// Reconstruct the return content from all children
|
||||||
|
let content = this.childBlocks
|
||||||
|
.map(child => {
|
||||||
|
if (child instanceof CodeTextBlock) {
|
||||||
|
return child.toLines()[0];
|
||||||
|
} else if (child instanceof TableBlock) {
|
||||||
|
// For tables, use their full content
|
||||||
|
return child.toLines().join('');
|
||||||
|
} else {
|
||||||
|
// For other block types, use their full content
|
||||||
|
return child.toLines().join('');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
this.returnParams.length = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.replace(/^return\s+/, '').trim(); // Remove the 'return' keyword if present
|
||||||
|
|
||||||
|
// Split by commas while respecting nesting
|
||||||
|
const params: string[] = [];
|
||||||
|
let currentParam = '';
|
||||||
|
let braceDepth = 0;
|
||||||
|
let parenDepth = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
const char = content[i];
|
||||||
|
const nextChar = i < content.length - 1 ? content[i + 1] : '';
|
||||||
|
|
||||||
|
// Skip strings to avoid counting delimiters inside them
|
||||||
|
if (char === '"' || char === "'") {
|
||||||
|
const quoteType = char;
|
||||||
|
currentParam += char;
|
||||||
|
i++;
|
||||||
|
while (i < content.length && content[i] !== quoteType) {
|
||||||
|
if (content[i] === '\\' && i + 1 < content.length) {
|
||||||
|
currentParam += content[i];
|
||||||
|
i++;
|
||||||
|
currentParam += content[i];
|
||||||
|
} else {
|
||||||
|
currentParam += content[i];
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i < content.length) {
|
||||||
|
currentParam += content[i];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track nesting depth
|
||||||
|
if (char === '{') {
|
||||||
|
braceDepth++;
|
||||||
|
} else if (char === '}') {
|
||||||
|
braceDepth--;
|
||||||
|
} else if (char === '(') {
|
||||||
|
parenDepth++;
|
||||||
|
} else if (char === ')') {
|
||||||
|
parenDepth--;
|
||||||
|
} else if (char === ',' && braceDepth === 0 && parenDepth === 0) {
|
||||||
|
// This is a param separator
|
||||||
|
const param = currentParam.trim();
|
||||||
|
if (param) {
|
||||||
|
params.push(param);
|
||||||
|
}
|
||||||
|
currentParam = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentParam += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last parameter
|
||||||
|
const lastParam = currentParam.trim();
|
||||||
|
if (lastParam) {
|
||||||
|
params.push(lastParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.returnParams.length = 0;
|
||||||
|
this.returnParams.push(...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableBlock extends CodeBlock {
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.Table, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FunctionBlock extends CodeBlock {
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock) {
|
||||||
|
super(sourceLineNumber, BlockType.Function, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequireBlock extends CodeBlock {
|
||||||
|
|
||||||
|
constructor(sourceLineNumber: number, parent: CodeBlock, public readonly charStart?: number, public readonly charEnd?: number) {
|
||||||
|
super(sourceLineNumber, BlockType.Require, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toLines(): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const child of this.childBlocks) {
|
||||||
|
lines.push(...child.toLines());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequiredString(): string {
|
||||||
|
if (this.childBlocks.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const firstChild = this.childBlocks[0];
|
||||||
|
if (firstChild instanceof CodeTextBlock) {
|
||||||
|
// local module = require("module") -> module
|
||||||
|
// local module = require('module') -> module
|
||||||
|
const line = firstChild.toLines()[0];
|
||||||
|
const requireMatch = line.match(/require\s*\(\s*["']([^"']+)["']\s*\)/);
|
||||||
|
if (requireMatch && requireMatch[1]) {
|
||||||
|
return requireMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface CompilationError {
|
||||||
|
filePath: string;
|
||||||
|
line: number;
|
||||||
|
charStart?: number;
|
||||||
|
charEnd?: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
+155
-231
@@ -1,13 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { LuaFile, BlockType, CodeBlock, RequireBlock, ReturnBlock, FunctionBlock, TableBlock } from './CodeBlocks';
|
||||||
export interface CompilationError {
|
import { CompilationError } from './CompilationError';
|
||||||
filePath: string;
|
|
||||||
line: number;
|
|
||||||
charStart?: number;
|
|
||||||
charEnd?: number;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScriptCompilerOptions {
|
export interface ScriptCompilerOptions {
|
||||||
sourcePath: string,
|
sourcePath: string,
|
||||||
@@ -23,6 +17,8 @@ export interface ICompilationLogger {
|
|||||||
writeLine(message: string): void;
|
writeLine(message: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { CompilationError };
|
||||||
|
|
||||||
class Metrics {
|
class Metrics {
|
||||||
public totalLinesRead : number = 0;
|
public totalLinesRead : number = 0;
|
||||||
public totalLinesWritten: number = 0;
|
public totalLinesWritten: number = 0;
|
||||||
@@ -66,9 +62,21 @@ export class ScriptCompiler {
|
|||||||
if (entry.isFile() && entry.name.endsWith('.lua')) {
|
if (entry.isFile() && entry.name.endsWith('.lua')) {
|
||||||
const fullPath = path.join(entry.parentPath, entry.name);
|
const fullPath = path.join(entry.parentPath, entry.name);
|
||||||
const relativePath = path.relative(this.options.sourcePath, fullPath);
|
const relativePath = path.relative(this.options.sourcePath, fullPath);
|
||||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
const luaFile = LuaFile.createFromFile(fullPath, this.options.onError);
|
||||||
|
const luaReference = fileReferenceToLuaVariable(relativePath);
|
||||||
const parsedFile = this.parseFile(relativePath, content, fullPath, metricsMeter);
|
const key = luaReference.replace(LUA_SCRIPT_GLOBAL_KEYWORD + '.', '').toLowerCase();
|
||||||
|
|
||||||
|
if(parsedFiles.has(key)){
|
||||||
|
this.options.onError?.({
|
||||||
|
filePath: fullPath,
|
||||||
|
line: 0,
|
||||||
|
message: `Duplicate file key detected: ${key}. This can happen if two files have different capitalization. Lua is case sensitive, but the compiler treats file keys as case insensitive.`
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedFile = new ParsedFile(key, luaFile, fullPath);
|
||||||
|
|
||||||
parsedFiles.set(parsedFile.fileKey, parsedFile);
|
parsedFiles.set(parsedFile.fileKey, parsedFile);
|
||||||
metricsMeter.filesRead++;
|
metricsMeter.filesRead++;
|
||||||
}
|
}
|
||||||
@@ -83,6 +91,8 @@ export class ScriptCompiler {
|
|||||||
this.options.onError
|
this.options.onError
|
||||||
);
|
);
|
||||||
|
|
||||||
|
writer.logDependencyTree();
|
||||||
|
|
||||||
const writeStart = Date.now();
|
const writeStart = Date.now();
|
||||||
writer.write(includeDevScript, metricsMeter);
|
writer.write(includeDevScript, metricsMeter);
|
||||||
const writeEnd = Date.now();
|
const writeEnd = Date.now();
|
||||||
@@ -95,203 +105,11 @@ export class ScriptCompiler {
|
|||||||
metricsMeter.totalTimeMs = (end-start);
|
metricsMeter.totalTimeMs = (end-start);
|
||||||
metricsMeter.log(this.logger);
|
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 {
|
class Dependency {
|
||||||
public readonly fileKey: string;
|
public readonly fileKey: string;
|
||||||
|
public readonly luaReference: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly requiredModule: string,
|
public readonly requiredModule: string,
|
||||||
@@ -300,22 +118,11 @@ class Dependency {
|
|||||||
public readonly charEnd: number
|
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 {
|
function fileReferenceToLuaVariable(fileReference: string): string {
|
||||||
// Remove .lua extension
|
// Remove .lua extension
|
||||||
@@ -333,21 +140,104 @@ function fileReferenceToLuaVariable(fileReference: string): string {
|
|||||||
|
|
||||||
// Split by / to get path parts
|
// Split by / to get path parts
|
||||||
const parts = fileReference.split('/');
|
const parts = fileReference.split('/');
|
||||||
|
|
||||||
// Convert to ScriptGlobals.folder.FileName format
|
// Convert to ScriptGlobals.folder.FileName format
|
||||||
let result = LUA_SCRIPT_GLOBAL_KEYWORD;
|
let result = LUA_SCRIPT_GLOBAL_KEYWORD;
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
if (i === parts.length - 1) {
|
result += '.' + parts[i].toLowerCase();
|
||||||
// Capitalize first letter of filename
|
|
||||||
result += '.' + parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
|
|
||||||
} else {
|
|
||||||
// Folder names stay lowercase
|
|
||||||
result += '.' + parts[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
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 {
|
class Writer {
|
||||||
constructor(
|
constructor(
|
||||||
public location: string,
|
public location: string,
|
||||||
@@ -358,8 +248,8 @@ class Writer {
|
|||||||
|
|
||||||
private getStartLines(): string[] {
|
private getStartLines(): string[] {
|
||||||
return [
|
return [
|
||||||
`-- Transpiled at (UTC): ${new Date().toISOString()}`,
|
`-- Transpiled at (UTC): ${new Date().toISOString()}\n`,
|
||||||
`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}`
|
`local ${LUA_SCRIPT_GLOBAL_KEYWORD} = {}\n`
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,8 +352,42 @@ class Writer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metrics.totalLinesWritten += parsedFile.lines.length;
|
|
||||||
outputLines.push(...parsedFile.lines);
|
parsedFile.replaceRequireWithGlobal();
|
||||||
|
parsedFile.replaceModuleReturnWithGlobalAssignement();
|
||||||
|
|
||||||
|
const lines = parsedFile.luaFile.toLines();
|
||||||
|
const newLineFilteredLines : string[] = [];
|
||||||
|
|
||||||
|
function trimEndPreserveNewlines(str: string): string {
|
||||||
|
return str.replace(/[ \t]+$/gm, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let wasLastEmpty = false;
|
||||||
|
let lastEndedWithNewline = false;
|
||||||
|
for(const line of lines){
|
||||||
|
if(trimEndPreserveNewlines(line) !== ''){
|
||||||
|
if(line.trim() === ''){
|
||||||
|
if(!wasLastEmpty && !lastEndedWithNewline){
|
||||||
|
newLineFilteredLines.push(line);
|
||||||
|
wasLastEmpty = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newLineFilteredLines.push(line);
|
||||||
|
wasLastEmpty = false;
|
||||||
|
|
||||||
|
lastEndedWithNewline = line.endsWith('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputLines.push("do -- " + parsedFile.fileKey + "\n");
|
||||||
|
|
||||||
|
metrics.totalLinesWritten += newLineFilteredLines.length;
|
||||||
|
outputLines.push(...newLineFilteredLines);
|
||||||
|
outputLines.push("\nend -- " + parsedFile.fileKey + "\n");
|
||||||
writtenFiles.add(parsedFile.fileKey);
|
writtenFiles.add(parsedFile.fileKey);
|
||||||
metrics.filesWritten++;
|
metrics.filesWritten++;
|
||||||
};
|
};
|
||||||
@@ -474,7 +398,7 @@ class Writer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(this.location), { recursive: true });
|
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) {
|
if(includeDevScript) {
|
||||||
const devFileLocation = this.location.replace('.lua', '.dev.lua');
|
const devFileLocation = this.location.replace('.lua', '.dev.lua');
|
||||||
@@ -488,7 +412,7 @@ class Writer {
|
|||||||
`-- This script can be referenced in DCS. Compiled script will then be loaded dynamically.`,
|
`-- 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 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.`,
|
`-- 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');
|
fs.writeFileSync(devFileLocation, devLines.join('\n'), 'utf-8');
|
||||||
this.logger.info(`Development script written to ${devFileLocation}`);
|
this.logger.info(`Development script written to ${devFileLocation}`);
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ return utils
|
|||||||
|
|
||||||
## Release Notes
|
## Release Notes
|
||||||
|
|
||||||
|
### 0.1.0
|
||||||
|
|
||||||
|
Complete overhaul of the transpiler for stability and support purpose. <br>
|
||||||
|
|
||||||
### 0.0.3
|
### 0.0.3
|
||||||
|
|
||||||
- Fixed: Settings filter not correct when opening settings with the extension command.
|
- Fixed: Settings filter not correct when opening settings with the extension command.
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
if (watch) {
|
if (watch) {
|
||||||
await ctx.watch();
|
await ctx.watch();
|
||||||
|
console.log('[watch] watching for changes...');
|
||||||
} else {
|
} else {
|
||||||
await ctx.rebuild();
|
await ctx.rebuild();
|
||||||
await ctx.dispose();
|
await ctx.dispose();
|
||||||
|
|||||||
@@ -42,10 +42,85 @@
|
|||||||
---@field Easting number
|
---@field Easting number
|
||||||
---@field Northing number
|
---@field Northing number
|
||||||
|
|
||||||
|
do --mission table
|
||||||
|
|
||||||
|
---@class MissionTable
|
||||||
|
---@field drawings Drawings
|
||||||
|
---@field coalitions Coalitions
|
||||||
|
---@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 string
|
||||||
|
---@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
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
do -- env
|
do -- env
|
||||||
---@class env
|
---@class env
|
||||||
---@field mission table TODO: Mission
|
---@field mission MissionTable
|
||||||
---@field warehouses table
|
---@field warehouses table
|
||||||
---@field info fun(log:string, showMessageBox:boolean?) Prints passed log line with prefix 'info'
|
---@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'
|
---@field warning fun(log:string, showMessageBox:boolean?) Prints passed log line with prefix 'warning'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "dutchies-dcs-scripting-tools",
|
"name": "dutchies-dcs-scripting-tools",
|
||||||
"displayName": "Dutchies Dcs Scripting Tools",
|
"displayName": "Dutchies Dcs Scripting Tools",
|
||||||
"description": "Scripting tools to create DCS script and frameworks easier",
|
"description": "Scripting tools to create DCS script and frameworks easier",
|
||||||
"version": "0.0.3",
|
"version": "0.1.2",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "dutchie031",
|
"name": "dutchie031",
|
||||||
"email": "54616262+dutchie031@users.noreply.github.com"
|
"email": "54616262+dutchie031@users.noreply.github.com"
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/dutchie031/DcsMissionScriptingTools"
|
"url": "https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Other"
|
"Other"
|
||||||
@@ -124,15 +124,15 @@
|
|||||||
"test": "vscode-test"
|
"test": "vscode-test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.108.1",
|
"@types/vscode": "1.108.1",
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "10.0.10",
|
||||||
"@types/node": "22.x",
|
"@types/node": "26.4.1",
|
||||||
"typescript-eslint": "^8.52.0",
|
"typescript-eslint": "8.70.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "10.9.1",
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "0.27.7",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "5.9.3",
|
||||||
"@vscode/test-cli": "^0.0.12",
|
"@vscode/test-cli": "0.0.15",
|
||||||
"@vscode/test-electron": "^2.5.2"
|
"@vscode/test-electron": "2.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,37 +3,52 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler';
|
import { ScriptCompiler, ScriptCompilerOptions, CompilationError, ICompilationLogger } from 'dcs-script-compiler';
|
||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
|
import * as luaAddonsManager from './lua-addons-manager';
|
||||||
|
|
||||||
const luaWorkSpaceSettingKey = "Lua.workspace";
|
const luaWorkSpaceSettingKey = "Lua.workspace";
|
||||||
const librarySettingsKey = "library";
|
const librarySettingsKey = "library";
|
||||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler');
|
const diagnosticCollection = vscode.languages.createDiagnosticCollection('lua-transpiler');
|
||||||
|
|
||||||
let pluginPath : string | undefined;
|
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
|
|
||||||
const extensionName = 'dutchies-dcs-scripting-tools';
|
const extensionName = 'dutchies-dcs-scripting-tools';
|
||||||
const publisherName = 'dutchie031';
|
const publisherName = 'dutchie031';
|
||||||
const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`;
|
const extensionSettingsFilter = `@ext:${publisherName}.${extensionName}`;
|
||||||
|
|
||||||
|
// State tracking for addon updates
|
||||||
|
let extensionPath: string | undefined;
|
||||||
|
let workspaceRoot: string | undefined;
|
||||||
|
let currentExtensionVersion: string | undefined;
|
||||||
|
let installedAddonPaths: Map<string, string> = new Map();
|
||||||
|
let isUpdatingAddons = false;
|
||||||
|
|
||||||
//TODO:
|
//TODO:
|
||||||
// - ENABLE/DISABLE with settings instead of commands (or both)
|
// - ENABLE/DISABLE with settings instead of commands (or both)
|
||||||
|
|
||||||
// This method is called when your extension is activated
|
// This method is called when your extension is activated
|
||||||
// Your extension is activated the very first time the command is executed
|
// 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;
|
||||||
|
|
||||||
|
// Initialize extension version
|
||||||
|
try {
|
||||||
|
currentExtensionVersion = await luaAddonsManager.getExtensionVersion(extensionPath);
|
||||||
|
logger.debug(`Extension version: ${currentExtensionVersion}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Failed to determine extension version: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.enable', async () => {
|
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.enable', async () => {
|
||||||
enableIntellisense();
|
await enableIntellisense();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.disable', async () => {
|
vscode.commands.registerCommand('dutchies-dcs-scripting-tools.disable', async () => {
|
||||||
disableIntellisense();
|
await disableIntellisense();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -71,20 +86,28 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
const config = vscode.workspace.getConfiguration(extensionName);
|
const config = vscode.workspace.getConfiguration(extensionName);
|
||||||
const dcsTypesEnabled = config.get<boolean>('dcsTypes') || false;
|
const dcsTypesEnabled = config.get<boolean>('dcsTypes') || false;
|
||||||
if (dcsTypesEnabled) {
|
if (dcsTypesEnabled) {
|
||||||
addPluginPathToSettings();
|
await updateLuaAddons();
|
||||||
} else {
|
} 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');
|
logger.info('Dutchies DCS Scripting Tools extension activated');
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method is called when your extension is deactivated
|
// 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');
|
logger.info('Dutchies DCS Scripting Tools extension deactivated');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +161,7 @@ async function compileLuaScripts() {
|
|||||||
await compiler.compile(includeDevScript);
|
await compiler.compile(includeDevScript);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
vscode.window.showErrorMessage('Compilation failed: ' + (err as Error).message);
|
vscode.window.showErrorMessage('Compilation failed: ' + (err as Error).message);
|
||||||
|
logger.error('Compilation failed: ' + (err as Error).message + '\n' + (err as Error).stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update diagnostics
|
// Update diagnostics
|
||||||
@@ -161,11 +185,11 @@ async function enableIntellisense() {
|
|||||||
config.update("dcsTypes", true, vscode.ConfigurationTarget.Workspace);
|
config.update("dcsTypes", true, vscode.ConfigurationTarget.Workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
addPluginPathToSettings();
|
await updateLuaAddons();
|
||||||
await vscode.commands.executeCommand(
|
await vscode.commands.executeCommand(
|
||||||
"lua.startServer"
|
"lua.startServer"
|
||||||
);
|
);
|
||||||
vscode.window.showInformationMessage('Dutchies DCS Scripting Tools enabled.');
|
vscode.window.showInformationMessage(`DCS-Types installed. Version ${currentExtensionVersion}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disableIntellisense() {
|
async function disableIntellisense() {
|
||||||
@@ -174,32 +198,252 @@ async function disableIntellisense() {
|
|||||||
config.update("dcsTypes", false, vscode.ConfigurationTarget.Workspace);
|
config.update("dcsTypes", false, vscode.ConfigurationTarget.Workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
removePluginPathFromSettings();
|
await removeVersionedPluginPathsFromSettings();
|
||||||
|
|
||||||
|
// Clean up all addon versions from workspace
|
||||||
|
if (workspaceRoot) {
|
||||||
|
try {
|
||||||
|
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath || '');
|
||||||
|
for (const addonName of discoveredAddons.keys()) {
|
||||||
|
await luaAddonsManager.deleteAllVersionsOfAddon(workspaceRoot, addonName);
|
||||||
|
logger.debug(`Deleted all versions of ${addonName} from workspace.`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Failed to clean up addons: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await vscode.commands.executeCommand(
|
await vscode.commands.executeCommand(
|
||||||
"lua.startServer"
|
"lua.startServer"
|
||||||
);
|
);
|
||||||
vscode.window.showInformationMessage('Dutchies DCS Scripting Tools disabled.');
|
vscode.window.showInformationMessage('Dutchies DCS Scripting Tools disabled.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPluginPathToSettings() {
|
/**
|
||||||
if (pluginPath) {
|
* Updates lua-addons by copying them from the extension bundle to the workspace
|
||||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
* with versioned naming, cleaning old versions, and updating Lua settings
|
||||||
const librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
*/
|
||||||
if (!librarySettings.includes(pluginPath)) {
|
async function updateLuaAddons(): Promise<void> {
|
||||||
librarySettings.push(pluginPath);
|
// Prevent concurrent update operations
|
||||||
luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('Starting lua-addons update...');
|
||||||
|
|
||||||
|
// Discover bundled addons
|
||||||
|
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
|
||||||
|
|
||||||
|
if (discoveredAddons.size === 0) {
|
||||||
|
logger.info('No lua-addons found in extension bundle.');
|
||||||
|
isUpdatingAddons = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(`Discovered ${discoveredAddons.size} lua-addon(s): ${Array.from(discoveredAddons.keys()).join(', ')}`);
|
||||||
|
|
||||||
|
// Copy addons with versioned names
|
||||||
|
const copiedAddons = await luaAddonsManager.copyLuaAddons(
|
||||||
|
extensionPath,
|
||||||
|
workspaceRoot,
|
||||||
|
currentExtensionVersion,
|
||||||
|
discoveredAddons
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store paths for settings management
|
||||||
|
installedAddonPaths = copiedAddons;
|
||||||
|
logger.debug(`Copied ${copiedAddons.size} addon(s) to workspace.`);
|
||||||
|
|
||||||
|
// Clean up old versions
|
||||||
|
for (const addonName of discoveredAddons.keys()) {
|
||||||
|
await luaAddonsManager.deleteOldVersions(workspaceRoot, addonName, currentExtensionVersion);
|
||||||
|
logger.debug(`Cleaned old versions of ${addonName}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Lua settings with new paths
|
||||||
|
await addVersionedPluginPathsToSettings(copiedAddons);
|
||||||
|
|
||||||
|
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) {
|
* Checks on extension startup if lua-addons need to be updated
|
||||||
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
* Runs only if dcsTypes is enabled in the workspace
|
||||||
const librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
*/
|
||||||
const libIndex = librarySettings.indexOf(pluginPath);
|
async function checkAndUpdateAddonsOnStartup(): Promise<void> {
|
||||||
if (libIndex !== -1) {
|
if (!extensionPath || !workspaceRoot || !currentExtensionVersion) {
|
||||||
librarySettings.splice(libIndex, 1);
|
logger.debug('Skipping addon version check on startup: missing initialization data.');
|
||||||
luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
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...');
|
||||||
|
|
||||||
|
// Get installed addons
|
||||||
|
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
|
||||||
|
const discoveredAddons = await luaAddonsManager.discoverLuaAddons(extensionPath);
|
||||||
|
|
||||||
|
// Check each discovered addon
|
||||||
|
let updateNeeded = false;
|
||||||
|
for (const addonName of discoveredAddons.keys()) {
|
||||||
|
const installed = installedAddons.get(addonName);
|
||||||
|
const installedVersion = installed?.version;
|
||||||
|
|
||||||
|
if (luaAddonsManager.requiresUpdate(installedVersion, currentExtensionVersion)) {
|
||||||
|
logger.info(
|
||||||
|
`Version mismatch for ${addonName}: installed=${installedVersion || 'none'}, current=${currentExtensionVersion}`
|
||||||
|
);
|
||||||
|
updateNeeded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateNeeded) {
|
||||||
|
logger.info('Lua-addons update needed, prompting user...');
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug('Lua-addons versions are current, no update needed.');
|
||||||
|
// Ensure paths are in settings even if no update needed
|
||||||
|
const pathsMap = new Map<string, string>();
|
||||||
|
for (const [name, info] of installedAddons.entries()) {
|
||||||
|
pathsMap.set(name, info.path);
|
||||||
|
}
|
||||||
|
installedAddonPaths = pathsMap;
|
||||||
|
await addVersionedPluginPathsToSettings(installedAddonPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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 versioned addon paths to the Lua workspace library settings
|
||||||
|
* @param addonPaths - Map of addon name to installed path
|
||||||
|
*/
|
||||||
|
async function addVersionedPluginPathsToSettings(addonPaths?: Map<string, string>): Promise<void> {
|
||||||
|
const pathsToAdd = addonPaths || installedAddonPaths;
|
||||||
|
|
||||||
|
if (pathsToAdd.size === 0) {
|
||||||
|
logger.debug('No addon paths to add to settings.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||||
|
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||||
|
|
||||||
|
// Add each addon path if not already present
|
||||||
|
for (const addonPath of pathsToAdd.values()) {
|
||||||
|
const workspaceFolderPath = convertToWorkspaceFolderPath(addonPath);
|
||||||
|
if (!librarySettings.includes(workspaceFolderPath)) {
|
||||||
|
librarySettings.push(workspaceFolderPath);
|
||||||
|
logger.debug(`Added addon path to settings: ${workspaceFolderPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Failed to add addon paths to settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all addon paths from the Lua workspace library settings
|
||||||
|
*/
|
||||||
|
async function removeVersionedPluginPathsFromSettings(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const luaSettings = vscode.workspace.getConfiguration(luaWorkSpaceSettingKey);
|
||||||
|
let librarySettings = luaSettings.get<string[]>(librarySettingsKey) || [];
|
||||||
|
|
||||||
|
if (workspaceRoot) {
|
||||||
|
// Get all installed addons to know what paths to remove
|
||||||
|
const installedAddons = await luaAddonsManager.getInstalledAddons(workspaceRoot);
|
||||||
|
|
||||||
|
for (const addonInfo of installedAddons.values()) {
|
||||||
|
// Try both absolute and workspace-relative paths for compatibility
|
||||||
|
const absolutePath = addonInfo.path;
|
||||||
|
const relativePath = convertToWorkspaceFolderPath(absolutePath);
|
||||||
|
|
||||||
|
// Remove absolute path if present
|
||||||
|
let index = librarySettings.indexOf(absolutePath);
|
||||||
|
if (index !== -1) {
|
||||||
|
librarySettings.splice(index, 1);
|
||||||
|
logger.debug(`Removed addon path from settings: ${absolutePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove relative path if present
|
||||||
|
index = librarySettings.indexOf(relativePath);
|
||||||
|
if (index !== -1) {
|
||||||
|
librarySettings.splice(index, 1);
|
||||||
|
logger.debug(`Removed addon path from settings: ${relativePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await luaSettings.update(librarySettingsKey, librarySettings, vscode.ConfigurationTarget.Workspace);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Failed to remove addon paths from settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ export class Logger {
|
|||||||
this.outputChannel.appendLine(`[${new Date().toISOString()}][ERROR] ${message}`);
|
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() {
|
clear() {
|
||||||
this.outputChannel.clear();
|
this.outputChannel.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages lua-addons: discovery, versioning, copying, and cleanup.
|
||||||
|
* All operations assume semver versioning (X.Y.Z format).
|
||||||
|
* Addon folder names follow the pattern: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface LuaAddonInfo {
|
||||||
|
name: string;
|
||||||
|
sourceDir: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstalledAddonInfo {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the extension's version from package.json
|
||||||
|
* @param extensionRoot - Absolute path to the extension root directory
|
||||||
|
* @returns The semantic version string (e.g., "0.0.5")
|
||||||
|
* @throws Error if package.json cannot be read or version is not found
|
||||||
|
*/
|
||||||
|
export async function getExtensionVersion(extensionRoot: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const packageJsonPath = path.join(extensionRoot, '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)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers all lua-addons in the bundled lua-addons directory
|
||||||
|
* @param extensionPath - Absolute path to the extension installation directory
|
||||||
|
* @returns Map of addon name to source directory path
|
||||||
|
* @throws Error if lua-addons directory cannot be read
|
||||||
|
*/
|
||||||
|
export async function discoverLuaAddons(extensionPath: string): Promise<Map<string, string>> {
|
||||||
|
const luaAddonsDir = path.join(extensionPath, 'lua-addons');
|
||||||
|
const addons = new Map<string, string>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if lua-addons directory exists
|
||||||
|
if (!existsSync(luaAddonsDir)) {
|
||||||
|
return addons; // Empty map if no lua-addons
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const addonName = entry.name;
|
||||||
|
const addonPath = path.join(luaAddonsDir, addonName);
|
||||||
|
addons.set(addonName, addonPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addons;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to discover lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the workspace lua-addons directory path (.vscode/lua-addons)
|
||||||
|
* @param workspaceRoot - Absolute path to the workspace root
|
||||||
|
* @returns Absolute path to .vscode/lua-addons
|
||||||
|
*/
|
||||||
|
export function getWorkspaceLuaAddonsDir(workspaceRoot: string): string {
|
||||||
|
return path.join(workspaceRoot, '.vscode', 'lua-addons');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses installed addon versions from the workspace lua-addons directory
|
||||||
|
* Expects folder names in format: `<addon-name>.<version>` (e.g., dcs-types.0.0.5)
|
||||||
|
* @param workspaceRoot - Absolute path to the workspace root
|
||||||
|
* @returns Map of addon name to version string; returns empty map if directory doesn't exist
|
||||||
|
* @throws Error if directory cannot be read
|
||||||
|
*/
|
||||||
|
export async function getInstalledVersions(
|
||||||
|
workspaceRoot: string
|
||||||
|
): Promise<Map<string, InstalledAddonInfo>> {
|
||||||
|
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||||
|
const installed = new Map<string, InstalledAddonInfo>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Return empty map if directory doesn't exist yet
|
||||||
|
if (!existsSync(luaAddonsDir)) {
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const folderName = entry.name;
|
||||||
|
const parsed = parseAddonFolderName(folderName);
|
||||||
|
|
||||||
|
if (parsed) {
|
||||||
|
installed.set(parsed.name, {
|
||||||
|
name: parsed.name,
|
||||||
|
version: parsed.version,
|
||||||
|
path: path.join(luaAddonsDir, folderName)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return installed;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to read installed versions: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies lua-addons from the extension bundle to the workspace with versioned folder names
|
||||||
|
* @param extensionPath - Absolute path to the extension installation directory
|
||||||
|
* @param workspaceRoot - Absolute path to the workspace root
|
||||||
|
* @param extensionVersion - The current extension version string
|
||||||
|
* @param addonsToCopy - Map of addon name to source directory (from discoverLuaAddons)
|
||||||
|
* @returns Map of addon name to installed path in workspace
|
||||||
|
* @throws Error if copy fails or directory creation fails
|
||||||
|
*/
|
||||||
|
export async function copyLuaAddons(
|
||||||
|
extensionPath: string,
|
||||||
|
workspaceRoot: string,
|
||||||
|
extensionVersion: string,
|
||||||
|
addonsToCopy: Map<string, string>
|
||||||
|
): Promise<Map<string, string>> {
|
||||||
|
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||||
|
const results = new Map<string, string>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure .vscode directory exists
|
||||||
|
const vscodeDir = path.join(workspaceRoot, '.vscode');
|
||||||
|
await ensureDirectoryExists(vscodeDir);
|
||||||
|
|
||||||
|
// Ensure lua-addons directory exists
|
||||||
|
await ensureDirectoryExists(luaAddonsDir);
|
||||||
|
|
||||||
|
// Copy each addon
|
||||||
|
for (const [addonName, sourceDir] of addonsToCopy) {
|
||||||
|
const versionedFolderName = `${addonName}.${extensionVersion}`;
|
||||||
|
const destDir = path.join(luaAddonsDir, versionedFolderName);
|
||||||
|
|
||||||
|
// Remove destination if it already exists (shouldn't happen, but be safe)
|
||||||
|
if (existsSync(destDir)) {
|
||||||
|
await fs.rm(destDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the addon
|
||||||
|
await copyDirectory(sourceDir, destDir);
|
||||||
|
results.set(addonName, destDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to copy lua-addons: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all old versions of a specific addon
|
||||||
|
* @param workspaceRoot - Absolute path to the workspace root
|
||||||
|
* @param addonName - Name of the addon (e.g., "dcs-types")
|
||||||
|
* @param currentVersion - Version to keep (e.g., "0.0.5")
|
||||||
|
* @throws Error if directory operations fail
|
||||||
|
*/
|
||||||
|
export async function deleteOldVersions(
|
||||||
|
workspaceRoot: string,
|
||||||
|
addonName: string,
|
||||||
|
currentVersion: string
|
||||||
|
): Promise<void> {
|
||||||
|
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Return silently if directory doesn't exist
|
||||||
|
if (!existsSync(luaAddonsDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const parsed = parseAddonFolderName(entry.name);
|
||||||
|
|
||||||
|
// Delete if it's an old version of this addon
|
||||||
|
if (parsed && parsed.name === addonName && parsed.version !== currentVersion) {
|
||||||
|
const oldPath = path.join(luaAddonsDir, entry.name);
|
||||||
|
await fs.rm(oldPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to delete old versions of ${addonName}: ${
|
||||||
|
err instanceof Error ? err.message : String(err)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an addon needs to be updated (version mismatch)
|
||||||
|
* @param installedVersion - Currently installed version or undefined if not installed
|
||||||
|
* @param currentExtensionVersion - Current extension version to compare against
|
||||||
|
* @returns true if no version is installed or version doesn't match
|
||||||
|
*/
|
||||||
|
export function requiresUpdate(
|
||||||
|
installedVersion: string | undefined,
|
||||||
|
currentExtensionVersion: string
|
||||||
|
): boolean {
|
||||||
|
return !installedVersion || installedVersion !== currentExtensionVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all versioned addon folders for a specific addon name
|
||||||
|
* @param workspaceRoot - Absolute path to the workspace root
|
||||||
|
* @param addonName - Name of the addon to clean up completely
|
||||||
|
* @throws Error if directory operations fail
|
||||||
|
*/
|
||||||
|
export async function deleteAllVersionsOfAddon(
|
||||||
|
workspaceRoot: string,
|
||||||
|
addonName: string
|
||||||
|
): Promise<void> {
|
||||||
|
const luaAddonsDir = getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Return silently if directory doesn't exist
|
||||||
|
if (!existsSync(luaAddonsDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await fs.readdir(luaAddonsDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const parsed = parseAddonFolderName(entry.name);
|
||||||
|
|
||||||
|
// Delete all versions of this addon
|
||||||
|
if (parsed && parsed.name === addonName) {
|
||||||
|
const addonPath = path.join(luaAddonsDir, entry.name);
|
||||||
|
await fs.rm(addonPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to delete all versions of ${addonName}: ${
|
||||||
|
err instanceof Error ? err.message : String(err)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the currently installed addon info for all addons
|
||||||
|
* @param workspaceRoot - Absolute path to the workspace root
|
||||||
|
* @returns Map of addon name to its latest installed version info
|
||||||
|
*/
|
||||||
|
export async function getInstalledAddons(
|
||||||
|
workspaceRoot: string
|
||||||
|
): Promise<Map<string, InstalledAddonInfo>> {
|
||||||
|
return getInstalledVersions(workspaceRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Private Helper Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an addon folder name in format `<name>.<version>`
|
||||||
|
* @param folderName - The folder name to parse
|
||||||
|
* @returns Object with name and version, or null if format doesn't match
|
||||||
|
*/
|
||||||
|
function parseAddonFolderName(
|
||||||
|
folderName: string
|
||||||
|
): { name: string; version: string } | null {
|
||||||
|
// Split only on the last dot to handle addon names with dots (unlikely, but safe)
|
||||||
|
const lastDotIndex = folderName.lastIndexOf('.');
|
||||||
|
if (lastDotIndex === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = folderName.substring(0, lastDotIndex);
|
||||||
|
const version = folderName.substring(lastDotIndex + 1);
|
||||||
|
|
||||||
|
// Validate semver format (basic check)
|
||||||
|
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, version };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a directory exists, creating it if necessary
|
||||||
|
* @param dirPath - Absolute path to the directory
|
||||||
|
*/
|
||||||
|
async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore EEXIST errors
|
||||||
|
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively copies a directory and its contents
|
||||||
|
* @param srcDir - Source directory path
|
||||||
|
* @param destDir - Destination directory path
|
||||||
|
*/
|
||||||
|
async function copyDirectory(srcDir: string, destDir: string): Promise<void> {
|
||||||
|
await ensureDirectoryExists(destDir);
|
||||||
|
|
||||||
|
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const srcPath = path.join(srcDir, entry.name);
|
||||||
|
const destPath = path.join(destDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await copyDirectory(srcPath, destPath);
|
||||||
|
} else {
|
||||||
|
await fs.copyFile(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
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 * as luaAddonsManager from '../lua-addons-manager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for lua-addons-manager
|
||||||
|
* Uses temporary directories to avoid polluting the file system
|
||||||
|
*/
|
||||||
|
|
||||||
|
suite('lua-addons-manager', () => {
|
||||||
|
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 read version from package.json', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'getExtensionVersion-1');
|
||||||
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
const packageJson = path.join(testDir, 'package.json');
|
||||||
|
await fs.writeFile(packageJson, JSON.stringify({ version: '1.2.3' }));
|
||||||
|
|
||||||
|
const version = await luaAddonsManager.getExtensionVersion(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(version, '1.2.3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error if package.json not found', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'getExtensionVersion-2');
|
||||||
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => luaAddonsManager.getExtensionVersion(testDir),
|
||||||
|
/Failed to read extension version/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error if version field missing', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'getExtensionVersion-3');
|
||||||
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
const packageJson = path.join(testDir, 'package.json');
|
||||||
|
await fs.writeFile(packageJson, JSON.stringify({ name: 'test' }));
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => luaAddonsManager.getExtensionVersion(testDir),
|
||||||
|
/Version field not found/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('discoverLuaAddons', () => {
|
||||||
|
test('should discover lua-addons directories', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'discoverLuaAddons-1');
|
||||||
|
const luaAddonsDir = path.join(testDir, 'lua-addons');
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon2'), { recursive: true });
|
||||||
|
|
||||||
|
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(addons.size, 2);
|
||||||
|
assert.strictEqual(addons.has('addon1'), true);
|
||||||
|
assert.strictEqual(addons.has('addon2'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty map if lua-addons directory does not exist', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'discoverLuaAddons-2');
|
||||||
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
|
||||||
|
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(addons.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore non-directory entries', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'discoverLuaAddons-3');
|
||||||
|
const luaAddonsDir = path.join(testDir, 'lua-addons');
|
||||||
|
await fs.mkdir(luaAddonsDir, { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1'), { recursive: true });
|
||||||
|
await fs.writeFile(path.join(luaAddonsDir, 'file.txt'), 'test');
|
||||||
|
|
||||||
|
const addons = await luaAddonsManager.discoverLuaAddons(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(addons.size, 1);
|
||||||
|
assert.strictEqual(addons.has('addon1'), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('getWorkspaceLuaAddonsDir', () => {
|
||||||
|
test('should return correct .vscode/lua-addons path', () => {
|
||||||
|
const workspaceRoot = '/path/to/workspace';
|
||||||
|
const result = luaAddonsManager.getWorkspaceLuaAddonsDir(workspaceRoot);
|
||||||
|
|
||||||
|
assert.strictEqual(result, path.join(workspaceRoot, '.vscode', 'lua-addons'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('getInstalledVersions', () => {
|
||||||
|
test('should parse installed addon versions from folder names', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'getInstalledVersions-1');
|
||||||
|
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'mission-utils.1.2.3'), { recursive: true });
|
||||||
|
|
||||||
|
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(installed.size, 2);
|
||||||
|
assert.strictEqual(installed.has('dcs-types'), true);
|
||||||
|
assert.strictEqual(installed.has('mission-utils'), true);
|
||||||
|
assert.strictEqual(installed.get('dcs-types')?.version, '0.0.5');
|
||||||
|
assert.strictEqual(installed.get('mission-utils')?.version, '1.2.3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty map if directory does not exist', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'getInstalledVersions-2');
|
||||||
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
|
||||||
|
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(installed.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore folders with invalid version format', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'getInstalledVersions-3');
|
||||||
|
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'invalid-addon'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'bad-format.v1'), { recursive: true });
|
||||||
|
|
||||||
|
const installed = await luaAddonsManager.getInstalledVersions(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(installed.size, 1);
|
||||||
|
assert.strictEqual(installed.has('dcs-types'), true);
|
||||||
|
assert.strictEqual(installed.has('invalid-addon'), false);
|
||||||
|
assert.strictEqual(installed.has('bad-format'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('copyLuaAddons', () => {
|
||||||
|
test('should copy addons with versioned folder names', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'copyLuaAddons-1');
|
||||||
|
const sourceAddon1 = path.join(testDir, 'source-addons', 'addon1');
|
||||||
|
await fs.mkdir(sourceAddon1, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(sourceAddon1, 'file1.lua'), 'content1');
|
||||||
|
|
||||||
|
const addonsToCopy = new Map<string, string>([
|
||||||
|
['addon1', sourceAddon1]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await luaAddonsManager.copyLuaAddons(
|
||||||
|
testDir,
|
||||||
|
testDir,
|
||||||
|
'1.0.0',
|
||||||
|
addonsToCopy
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(result.size, 1);
|
||||||
|
const copiedPath = result.get('addon1');
|
||||||
|
assert.strictEqual(copiedPath !== undefined, true);
|
||||||
|
if (copiedPath) {
|
||||||
|
assert.strictEqual(copiedPath.includes('addon1.1.0.0'), true);
|
||||||
|
assert.strictEqual(existsSync(path.join(copiedPath, 'file1.lua')), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create .vscode directory if it does not exist', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'copyLuaAddons-2');
|
||||||
|
const sourceAddon = path.join(testDir, 'source', 'addon1');
|
||||||
|
await fs.mkdir(sourceAddon, { recursive: true });
|
||||||
|
|
||||||
|
const addonsToCopy = new Map<string, string>([
|
||||||
|
['addon1', sourceAddon]
|
||||||
|
]);
|
||||||
|
|
||||||
|
await luaAddonsManager.copyLuaAddons(
|
||||||
|
testDir,
|
||||||
|
testDir,
|
||||||
|
'1.0.0',
|
||||||
|
addonsToCopy
|
||||||
|
);
|
||||||
|
|
||||||
|
const vscodeDir = path.join(testDir, '.vscode');
|
||||||
|
assert.strictEqual(existsSync(vscodeDir), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should copy nested directories recursively', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'copyLuaAddons-3');
|
||||||
|
const sourceAddon = path.join(testDir, 'source', '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 addonsToCopy = new Map<string, string>([
|
||||||
|
['addon1', sourceAddon]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await luaAddonsManager.copyLuaAddons(
|
||||||
|
testDir,
|
||||||
|
testDir,
|
||||||
|
'1.0.0',
|
||||||
|
addonsToCopy
|
||||||
|
);
|
||||||
|
|
||||||
|
const copiedPath = result.get('addon1')!;
|
||||||
|
assert.strictEqual(existsSync(path.join(copiedPath, 'file.lua')), true);
|
||||||
|
assert.strictEqual(existsSync(path.join(copiedPath, 'subdir', 'file.lua')), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('deleteOldVersions', () => {
|
||||||
|
test('should delete old versions of an addon', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'deleteOldVersions-1');
|
||||||
|
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.3'), { recursive: true });
|
||||||
|
|
||||||
|
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.3');
|
||||||
|
|
||||||
|
const remaining = await fs.readdir(luaAddonsDir);
|
||||||
|
assert.deepStrictEqual(remaining.sort(), ['addon1.0.0.3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not delete other addons', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'deleteOldVersions-2');
|
||||||
|
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
|
||||||
|
|
||||||
|
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.2');
|
||||||
|
|
||||||
|
const remaining = await fs.readdir(luaAddonsDir);
|
||||||
|
assert.strictEqual(remaining.includes('addon1.0.0.2'), true);
|
||||||
|
assert.strictEqual(remaining.includes('addon2.0.0.1'), true);
|
||||||
|
assert.strictEqual(remaining.includes('addon1.0.0.1'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle missing directory gracefully', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'deleteOldVersions-3');
|
||||||
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
await luaAddonsManager.deleteOldVersions(testDir, 'addon1', '0.0.1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('deleteAllVersionsOfAddon', () => {
|
||||||
|
test('should delete all versions of an addon', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'deleteAllVersionsOfAddon-1');
|
||||||
|
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.1'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon1.0.0.2'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'addon2.0.0.1'), { recursive: true });
|
||||||
|
|
||||||
|
await luaAddonsManager.deleteAllVersionsOfAddon(testDir, 'addon1');
|
||||||
|
|
||||||
|
const remaining = await fs.readdir(luaAddonsDir);
|
||||||
|
assert.deepStrictEqual(remaining.sort(), ['addon2.0.0.1']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('requiresUpdate', () => {
|
||||||
|
test('should return true if no version installed', () => {
|
||||||
|
const result = luaAddonsManager.requiresUpdate(undefined, '1.0.0');
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return true if versions do not match', () => {
|
||||||
|
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.1');
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return false if versions match', () => {
|
||||||
|
const result = luaAddonsManager.requiresUpdate('1.0.0', '1.0.0');
|
||||||
|
|
||||||
|
assert.strictEqual(result, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('getInstalledAddons', () => {
|
||||||
|
test('should return installed addon information', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'getInstalledAddons-1');
|
||||||
|
const luaAddonsDir = path.join(testDir, '.vscode', 'lua-addons');
|
||||||
|
await fs.mkdir(path.join(luaAddonsDir, 'dcs-types.0.0.5'), { recursive: true });
|
||||||
|
|
||||||
|
const installed = await luaAddonsManager.getInstalledAddons(testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(installed.size, 1);
|
||||||
|
const addonInfo = installed.get('dcs-types');
|
||||||
|
assert.strictEqual(addonInfo !== undefined, true);
|
||||||
|
if (addonInfo) {
|
||||||
|
assert.strictEqual(addonInfo.name, 'dcs-types');
|
||||||
|
assert.strictEqual(addonInfo.version, '0.0.5');
|
||||||
|
assert.strictEqual(addonInfo.path.includes('dcs-types.0.0.5'), 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:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- run: |
|
- name: Build action
|
||||||
cd ${{ github.action_path }}
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
node dist/index.js
|
|
||||||
shell: bash
|
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",
|
"name": "gh-scripting-compiler",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node esbuild.js",
|
"build": "node esbuild.js",
|
||||||
@@ -8,11 +8,13 @@
|
|||||||
"package": "node esbuild.js --production"
|
"package": "node esbuild.js --production"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0"
|
"@actions/core": "3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "7.0.2",
|
||||||
"esbuild": "^0.27.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.0.2",
|
||||||
|
"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
+792
-179
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
"@types/node": "22.x",
|
"@types/node": "^22.20.2",
|
||||||
"@types/vscode": "^1.108.1",
|
"@types/vscode": "^1.108.1",
|
||||||
"@vscode/test-cli": "^0.0.12",
|
"@vscode/test-cli": "^0.0.12",
|
||||||
"@vscode/test-electron": "^2.5.2",
|
"@vscode/test-electron": "^2.5.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user