Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ name: 🚀 Release Plugin

on:
pull_request:
types: [ closed ]
branches: [ main ]
types: [closed]
branches: [main]

permissions:
contents: write

jobs:
release:
if: >-
github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v')
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'release/v')
runs-on: ubuntu-latest
timeout-minutes: 15

Expand Down Expand Up @@ -47,7 +48,7 @@ jobs:
ROBLOX_API_KEY: ${{ secrets.ROBLOX_API_KEY }}

- name: 🛠️ Build plugin
run: lune run build
run: lune run build --release
env:
DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}

Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: 🧪 Tests

on:
pull_request:
push:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
tests:
name: Run Tests
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: 📥 Checkout repository
uses: actions/checkout@v4

- name: 🔧 Install Rokit
uses: CompeyDev/setup-rokit@v0.1.2
with:
cache: true
token: ${{ secrets.GITHUB_TOKEN }}

- name: 📋 Verify tools
run: rokit list

- name: Run runtime setup
run: |
lune setup
lute setup

- name: 📦 Setup project
run: lune run setup
env:
ROBLOX_API_KEY: ${{ secrets.ROBLOX_API_KEY }}

- name: 🔎 Run tests
run: lute run test
env:
ROBLOX_API_KEY: ${{ secrets.ROBLOX_API_KEY }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
coverage/
node_modules/
Packages/

**/generated

Expand All @@ -8,3 +9,4 @@ node_modules/
cspell.json
.env
*.rbxm
*.rbxl
37 changes: 33 additions & 4 deletions .lune/commands/install/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ local ROBLOX_PACKAGES = {
"Dash",
}

local function runLuteInstall(): (boolean, string)
local result = process.exec("lute", { "pkg", "install" })
if not result.ok then
return false, result.stderr
end
return true, result.stdout
end

local function runWallyInstall(): (boolean, string)
local result = process.exec("wally", { "install" }, {
cwd = "plugin",
Expand Down Expand Up @@ -94,6 +102,19 @@ function install.run(options_: Options?): boolean
local options: Options = options_ or {}
local quiet = if options.quiet ~= nil then options.quiet else false

if not quiet then
print(color.dim("Installing Lute packages..."))
end

local luteResult, luteStdout = runLuteInstall()
if not luteResult then
if not quiet then
print(color.bold(color.red("Failed to install Lute packages")))
stdio.write(luteStdout)
end
return false
end

if not quiet then
print(color.dim("Installing Wally packages..."))
end
Expand Down Expand Up @@ -161,13 +182,21 @@ function install.run(options_: Options?): boolean

-- Wally has no support for deduping packages published by different
-- authors. We pull React from haedrix in plugin/wally.toml, but our
-- transitive deps (and Foundation's own React.lua shims) still point
-- at jsdotlua/react. Collapse the duplicates onto haedrix here so
-- there is a single canonical React across the package graph.
-- transitive deps (Foundation's React shims, ReactCharm/ReactFlow/
-- Storyteller, and jest's pretty-format -> react-is chain in
-- DevPackages) still point at jsdotlua/react. Both indexes resolve
-- shared transitives via the same runtime Packages._Index, so we
-- collapse duplicates across BOTH _Index dirs to land on a single
-- haedrix React identity.
if not quiet then
print(color.dim("Redirecting jsdotlua packages to haedrix..."))
end
local redirectResult = redirectPackages.byAuthor("plugin/Packages/_Index", "jsdotlua", "haedrix", quiet)
local redirectResult = redirectPackages.byAuthor(
{ "plugin/Packages/_Index", "plugin/DevPackages/_Index" },
"jsdotlua",
"haedrix",
quiet
)
if not redirectResult then
if not quiet then
print(color.bold(color.red("Failed to redirect packages")))
Expand Down
106 changes: 73 additions & 33 deletions .lune/commands/install/redirectPackages.luau
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@
Wally has no support for deduping packages published by different
authors, or for patching transitive dependency references after
install. This module fills that gap: it deletes a set of installed
package directories under `<packagesDir>/_Index/` and rewrites every
top-level `*.lua` shim so references to the removed names are
redirected to their replacements.
package directories under one or more `<packagesDir>/` indexes and
rewrites every top-level `*.lua` shim so references to the removed
names are redirected to their replacements.

The module operates on a *list* of package index directories so a
single redirect pass can converge shims across both `Packages/_Index`
and `DevPackages/_Index` (which share the same runtime `_Index`
folder via Wally's shared-packages config).

Two entry points:

redirectPackages.apply(packagesDir, { ["old@1"] = "new@2" }, quiet)
redirectPackages.apply(packageDirs, { ["old@1"] = "new@2" }, quiet)
- Low-level primitive. Caller supplies the rename map directly.

redirectPackages.byAuthor(packagesDir, "fromAuthor", "toAuthor", quiet)
redirectPackages.byAuthor(packageDirs, "fromAuthor", "toAuthor", quiet)
- Convenience. Discovers `<fromAuthor>_<name>@<v>` directories and
pairs each with the unique `<toAuthor>_<name>@<v>` sibling, then
delegates to `apply`. This matches the canonical case of
converging two registries that publish overlapping packages.
pairs each with the unique `<toAuthor>_<name>@<v>` sibling found
anywhere in the union of `packageDirs`, then delegates to
`apply`. This matches the canonical case of converging two
registries that publish overlapping packages.
]]

local fs = require("@lune/fs")
Expand Down Expand Up @@ -77,22 +83,30 @@ local redirectPackages = {}

--[[
Applies an explicit rename map: deletes each `from` directory under
`packagesDir` and rewrites every shim to point at the `to` directory.
every `packageDirs` index where it exists, and rewrites every shim
in every index to point at the `to` directory.

@param packagesDir Path to the package index (e.g. "plugin/Packages/_Index")
@param packageDirs Paths to the package indexes (e.g.
{ "plugin/Packages/_Index", "plugin/DevPackages/_Index" })
@param renames Map of directory names to redirect (from -> to)
@param quiet Whether to suppress output
@return True on success, false on error
]]
function redirectPackages.apply(packagesDir: string, renames: { [string]: string }, quiet: boolean?): boolean
function redirectPackages.apply(packageDirs: { string }, renames: { [string]: string }, quiet: boolean?): boolean
if next(renames) == nil then
return true
end

if not fs.isDir(packagesDir) then
if not quiet then
print(color.yellow(` Package directory not found: {packagesDir}`))
local existingDirs: { string } = {}
for _, dir in packageDirs do
if fs.isDir(dir) then
table.insert(existingDirs, dir)
elseif not quiet then
print(color.yellow(` Package directory not found: {dir}`))
end
end

if #existingDirs == 0 then
return false
end

Expand All @@ -102,57 +116,83 @@ function redirectPackages.apply(packagesDir: string, renames: { [string]: string
end
end

-- Delete first, then rewrite. Walking the index after deletion means we
-- never try to read a shim inside a directory that no longer exists.
-- Delete first, then rewrite. Walking each index after deletion means
-- we never try to read a shim inside a directory that no longer
-- exists. A `from` directory may exist in zero, one, or several
-- indexes; remove every occurrence.
for from in renames do
local pkgPath = `{packagesDir}/{from}`
if fs.isDir(pkgPath) then
fs.removeDir(pkgPath)
for _, dir in existingDirs do
local pkgPath = `{dir}/{from}`
if fs.isDir(pkgPath) then
fs.removeDir(pkgPath)
end
end
end

rewriteAllShims(packagesDir, renames)
for _, dir in existingDirs do
rewriteAllShims(dir, renames)
end

return true
end

--[[
Builds a rename map by pairing every `<fromAuthor>_<name>@<v>` directory
in `packagesDir` with its unique `<toAuthor>_<name>@<v>` sibling, then
delegates to `redirectPackages.apply`.
found anywhere in `packageDirs` with its unique `<toAuthor>_<name>@<v>`
sibling found anywhere in `packageDirs`, then delegates to
`redirectPackages.apply`.

Authors without a matching sibling are left alone (no entry in the map).
Errors if more than one sibling matches a single from-author package.

@param packagesDir Path to the package index (e.g. "plugin/Packages/_Index")
@param packageDirs Paths to the package indexes
@param fromAuthor The author whose packages should be removed
@param toAuthor The author whose packages should be kept
@param quiet Whether to suppress output
@return True on success, false on error
]]
function redirectPackages.byAuthor(packagesDir: string, fromAuthor: string, toAuthor: string, quiet: boolean?): boolean
if not fs.isDir(packagesDir) then
function redirectPackages.byAuthor(
packageDirs: { string },
fromAuthor: string,
toAuthor: string,
quiet: boolean?
): boolean
-- Union the directory entries from every existing index. We only
-- need the set of directory names for rename discovery; which index
-- each entry came from doesn't matter because the runtime `_Index`
-- is shared across them.
local entrySet: { [string]: true } = {}
local anyDirExists = false
for _, dir in packageDirs do
if not fs.isDir(dir) then
continue
end
anyDirExists = true
for _, entry in fs.readDir(dir) do
if fs.isDir(`{dir}/{entry}`) then
entrySet[entry] = true
end
end
end

if not anyDirExists then
return true
end

local entries = fs.readDir(packagesDir)
local fromPattern = `^{fromAuthor}_(.-)@`
local toPrefix = `{toAuthor}_`

local renames: { [string]: string } = {}
for _, entry in entries do
for entry in entrySet do
local depName = entry:match(fromPattern)
if depName == nil then
continue
end
if not fs.isDir(`{packagesDir}/{entry}`) then
continue
end

local siblingPrefix = `{toPrefix}{depName}@`
local matches: { string } = {}
for _, candidate in entries do
if candidate:sub(1, #siblingPrefix) == siblingPrefix and fs.isDir(`{packagesDir}/{candidate}`) then
for candidate in entrySet do
if candidate:sub(1, #siblingPrefix) == siblingPrefix then
table.insert(matches, candidate)
end
end
Expand All @@ -176,7 +216,7 @@ function redirectPackages.byAuthor(packagesDir: string, fromAuthor: string, toAu
renames[entry] = matches[1]
end

return redirectPackages.apply(packagesDir, renames, quiet)
return redirectPackages.apply(packageDirs, renames, quiet)
end

return redirectPackages
12 changes: 12 additions & 0 deletions .lute/.config.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
return {
luau = {
languagemode = "strict",
lint = { ["*"] = true },
linterrors = true,
aliases = {
batteries = "../Packages/lute@v1.0.0/batteries",
dotenv = "../Packages/dotenv@v0.1.2/src",
std = "~/.lute/typedefs/1.0.0/std",
},
},
}
Loading
Loading