diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..280204d --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +ROBLOX_API_KEY= +LOG_LEVEL=info diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3f2739..f8dd4b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,27 +19,10 @@ jobs: allow-external-github-orgs: true - name: Install dependencies - run: lune run install + run: lute scripts/install.luau - name: Run unit tests - run: lune run test - - test-e2e: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@v4 - - - uses: Roblox/setup-foreman@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - allow-external-github-orgs: true - - - name: Install dependencies - run: lune run install - - - name: Run end-to-end tests - run: lune run test-e2e + run: lute scripts/test.luau --e2e env: ROBLOX_API_KEY: ${{ secrets.ROBLOX_API_KEY }} @@ -55,10 +38,10 @@ jobs: allow-external-github-orgs: true - name: Install dependencies - run: lune run install + run: lute scripts/install.luau - name: Lint - run: lune run lint + run: lute scripts/lint.luau analyze: runs-on: ubuntu-latest @@ -72,12 +55,9 @@ jobs: allow-external-github-orgs: true - name: Install dependencies - run: lune run install - - - name: Setup Lune typedefs - run: lune setup + run: lute scripts/install.luau # MUS-2103 TODO: This will error in CI until Foreman is updated to install # the correct linux binary for luau-lsp - name: Analyze - run: lune run analyze + run: lute scripts/analyze.luau diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 136fa19..486fa39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,10 +23,10 @@ jobs: allow-external-github-orgs: true - name: Install dependencies - run: lune run install + run: lute scripts/install.luau - name: Build - run: lune run build + run: lute scripts/build.luau - uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index 98ac10b..f80a535 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Secrets +.env + # Build artifacts build temp diff --git a/.luaurc b/.luaurc index 2123a55..6958ba1 100644 --- a/.luaurc +++ b/.luaurc @@ -1,9 +1,10 @@ { "languageMode": "strict", "aliases": { - "root": "src", - "examples": "examples", + "lute": "~/.lute/typedefs/0.1.0/lute/", + "std": "~/.lute/typedefs/0.1.0/std/", "pkg": "pkg", - "lune": "~/.lune/.typedefs/0.9.3" + "root": "src", + "examples": "examples" } } diff --git a/.lune/analyze.luau b/.lune/analyze.luau deleted file mode 100644 index 630bb10..0000000 --- a/.lune/analyze.luau +++ /dev/null @@ -1,9 +0,0 @@ -local run = require("@root/lib/run") - -local foldersToAnalyze = { - ".lune", - "src", - "examples", -} - -run("luau-lsp", { "analyze", `--ignore="**/pkg/**"`, "--platform=standard", table.unpack(foldersToAnalyze) }) diff --git a/.lune/install.luau b/.lune/install.luau deleted file mode 100644 index 99f3afd..0000000 --- a/.lune/install.luau +++ /dev/null @@ -1,32 +0,0 @@ -local fs = require("@lune/fs") - -local run = require("@root/lib/run") - -local packages = "pkg" -local packageIndex = "_Index" - -local repos = { - { git = "itsfrank/frktest@31770327", main = "src" }, - { git = "osyrisrblx/t@1dbfccc1", main = "lib" }, -} - -run("rm", { "-rf", packages }) - -for _, repo in repos do - local parts = repo.git:split("@") - local repoPath = parts[1] - local rev = parts[2] - - local repoParts = repoPath:split("/") - local projectName = repoParts[2] - local dest = `{packages}/{packageIndex}/{projectName}` - - run("git", { "clone", `https://github.com/{repoPath}`, dest }) - - run("git", { "reset", "--hard", rev }, { - cwd = dest, - }) - - local linkerPath = `{packages}/{projectName}.luau` - fs.writeFile(linkerPath, `return require("./{packageIndex}/{projectName}/{repo.main}")`) -end diff --git a/.lune/test-e2e.luau b/.lune/test-e2e.luau deleted file mode 100644 index 0a1f959..0000000 --- a/.lune/test-e2e.luau +++ /dev/null @@ -1,28 +0,0 @@ -local process = require("@lune/process") - -local frktest = require("@pkg/frktest") - -local logging = require("@root/logging") -local run = require("@root/lib/run") - -local reporter = frktest._reporters.lune_console_reporter - -local function loadTestCases(path: string, alias: string) - local output = run("find", { path, "-iname", "*.spec.luau" }) - output = output:gsub(path, alias) - - local files = output:split("\n") - - logging.debug("files", files) - - for _, file in files do - (require :: any)(file) - end -end - -loadTestCases("examples", "@examples") - -reporter.init() -local success = frktest.run() - -process.exit(if success then 0 else 1) diff --git a/.lune/test.luau b/.lune/test.luau deleted file mode 100644 index 378bca8..0000000 --- a/.lune/test.luau +++ /dev/null @@ -1,28 +0,0 @@ -local process = require("@lune/process") - -local frktest = require("@pkg/frktest") - -local logging = require("@root/logging") -local run = require("@root/lib/run") - -local reporter = frktest._reporters.lune_console_reporter - -local function loadTestCases(path: string, alias: string) - local output = run("find", { path, "-iname", "*.spec.luau" }) - output = output:gsub(path, alias) - - local files = output:split("\n") - - logging.debug("files", files) - - for _, file in files do - (require :: any)(file) - end -end - -loadTestCases("src", "@root") - -reporter.init() -local success = frktest.run() - -process.exit(if success then 0 else 1) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..71ee793 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "luau-lsp.fflags.override": { + "LuauSolverV2": "true" + }, + "luau-lsp.platform.type": "standard", + "luau-lsp.ignoreGlobs": ["**/pkg/**"] +} diff --git a/README.md b/README.md index 96dc4f6..085c89a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Deploy rbxm files from GitHub to the Creator Store. # Installation > [!NOTE] -> [Lune](https://github.com/lune-org/lune) v0.9.3+ is required. +> [Lute](https://github.com/luau-lang/lute) v0.1.0-nightly.20251217+ is required. ## Prebuilt zip (recommended) @@ -24,8 +24,8 @@ Run the following commands to clone the repo, install dependencies, and build rb git clone https://github.com/Roblox/rbxasset.git cd rbxasset foreman install -lune run install -lune run build +lute scripts/install.luau +lute scripts/build.luau ``` From there, drag and drop `build/rbxasset` to a place where you will require it via a Luau script. @@ -54,25 +54,27 @@ This defines a `default` asset and a `production` environment to deploy to. Then create a Luau script to handle the deployment: ```luau --- .lune/publish.luau -local process = require("@lune/process") +-- scripts/publish.luau +local process = require("@lute/process") local rbxasset = require("./path/to/rbxasset") -local apiKey = process.args[1] +local args = { ... } + +local apiKey = args[1] assert(apiKey, "argument #1 must be a valid Open Cloud API key") -- The rbxm file needs to be built manually. rbxasset makes no assumptions about -- how your project is setup, it only cares about having a file to upload. Note -- the filename `build.rbxm` matches the `model` field in rbxasset.toml -process.exec("rojo", { "build", "-o", "build.rbxm" }) +process.run("rojo build -o build.rbxm") -- Publish the `default` asset defined in rbxasset.toml -rbxasset.publishPackageAsync(process.cwd, "default", apiKey) +rbxasset.publishPackageAsync(process.cwd(), "default", apiKey) ``` ```sh -$ lune run publish +$ lute scripts/publish.luau ``` Where `` represents an Open Cloud API key that has the following scopes: @@ -94,7 +96,6 @@ Assets define how the asset will be deployed and shown on the Creator Store. | `environment` | `string` | Defines which environment to deploy to. This value must equal one of the environments defined in the `environments` object | | `description` | `string?` | The description of the asset on the Creator Store | | `icon` | `string?` | Path to the icon (png only) to display on the Creator Store | -| `description` | `string?` | The description of the asset on the Creator Store | | `type` | `"Package" \| "Plugin"` | The type of asset to upload to the Creator Store. This must be set before the first publish as asset type is immutable once uploaded. Defaults to `"Package"` | ## Environments diff --git a/examples/package/deploy.luau b/examples/package/deploy.luau index c730c26..6ae8bd9 100644 --- a/examples/package/deploy.luau +++ b/examples/package/deploy.luau @@ -1,12 +1,9 @@ -local rbxasset = require("@root/") +local rbxasset = require("@root/init") -local process = require("@lune/process") +local process = require("@lute/process") -local apiKey = process.args[1] -assert(apiKey, "argument #1 must be a valid Open Cloud API key") +assert(process.cwd():match("examples/package"), "you must be in the `examples/package` folder when running this script") -assert(process.cwd:match("examples/package"), "you must be in the `examples/package` folder when running this script") +process.system("rojo build -o asset.rbxm") -process.exec("rojo", { "build", "-o", "asset.rbxm" }) - -rbxasset.publishPackageAsync(process.cwd, "package", apiKey) +rbxasset.publishPackageAsync(process.cwd(), "package", process.env.ROBLOX_API_KEY) diff --git a/examples/package/e2e.spec.luau b/examples/package/e2e.spec.luau index 3107828..f844857 100644 --- a/examples/package/e2e.spec.luau +++ b/examples/package/e2e.spec.luau @@ -1,27 +1,29 @@ -local frktest = require("@pkg/frktest") -local fs = require("@lune/fs") -local process = require("@lune/process") -local serde = require("@lune/serde") +local expect = require("@std/test/assert") +local fs = require("@lute/fs") +local process = require("@lute/process") +local test = require("@std/test") +local toml = require("@root/lib/toml") local getAssetDetailsAsync = require("@root/requests/getAssetDetailsAsync") local getOrCreateAssetLockfile = require("@root/manifest/getOrCreateAssetLockfile") local readManifest = require("@root/manifest/readManifest") local run = require("@root/lib/run") -local test = frktest.test -local check = frktest.assert.check - local ROOT_PATH = "examples/package" local MANIFEST_PATH = `{ROOT_PATH}/rbxasset.toml` local function appendTextForTesting(path: string, text: string) - local content = fs.readFile(path) + local handle = fs.open(path, "w") + local content = fs.read(handle) content ..= text - fs.writeFile(path, content) + fs.write(handle, content) + fs.close(handle) end local apiKey = process.env.ROBLOX_API_KEY -assert(apiKey, "End-to-end tests can only be run when the ROBLOX_API_KEY envvar is set to a valid Open Cloud API key") +if not apiKey then + error("End-to-end tests can only be run when the ROBLOX_API_KEY envvar is set to a valid Open Cloud API key") +end local lockfile = getOrCreateAssetLockfile(ROOT_PATH) local assetId = lockfile.assets.package.assetId @@ -38,7 +40,10 @@ test.case("uploads rbxm to the Creator Store", function() appendTextForTesting(path, `\nprint("New!")`) local success, result = pcall(function() - run("lune", { "run", "deploy.luau", apiKey }, { + return run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, cwd = ROOT_PATH, }) end) @@ -49,7 +54,7 @@ test.case("uploads rbxm to the Creator Store", function() assetDetails = getAssetDetailsAsync(assetId, apiKey) - check.not_equal(assetDetails.revisionId, prevRevisionId) + expect.neq(assetDetails.revisionId, prevRevisionId) end) test.case("text fields in rbxasest.toml get mirrored to the asset details", function() @@ -62,19 +67,28 @@ test.case("text fields in rbxasest.toml get mirrored to the asset details", func manifest.assets.package.name = `{manifest.assets.package.name}\n({id})` manifest.assets.package.description = `{manifest.assets.package.description}\n({id})` - fs.writeFile(MANIFEST_PATH, serde.encode("toml", manifest)) + local handle = fs.open(MANIFEST_PATH, "w") + fs.write(handle, toml.serialize(manifest)) + fs.close(handle) - run("lune", { "run", "deploy.luau", apiKey }, { - cwd = ROOT_PATH, - }) + local success, result = pcall(function() + return run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, + cwd = ROOT_PATH, + }) + end) run("git", { "restore", MANIFEST_PATH }) + assert(success, result) + assetDetails = getAssetDetailsAsync(assetId, apiKey) - check.equal(assetDetails.displayName, manifest.assets.package.name) - check.equal(assetDetails.description, manifest.assets.package.description) - check.not_equal(assetDetails.revisionId, prevRevisionId) + expect.eq(assetDetails.displayName, manifest.assets.package.name) + expect.eq(assetDetails.description, manifest.assets.package.description) + expect.neq(assetDetails.revisionId, prevRevisionId) end) test.case("does not publish a new version when there are no changes", function() @@ -82,11 +96,14 @@ test.case("does not publish a new version when there are no changes", function() local prevRevisionId = assetDetails.revisionId - run("lune", { "run", "deploy.luau", apiKey }, { + run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, cwd = ROOT_PATH, }) assetDetails = getAssetDetailsAsync(assetId, apiKey) - check.equal(assetDetails.revisionId, prevRevisionId) + expect.eq(assetDetails.revisionId, prevRevisionId) end) diff --git a/examples/plugin/deploy.luau b/examples/plugin/deploy.luau index fa5fcd0..3f30104 100644 --- a/examples/plugin/deploy.luau +++ b/examples/plugin/deploy.luau @@ -1,12 +1,9 @@ -local rbxasset = require("@root/") +local rbxasset = require("@root/init") -local process = require("@lune/process") +local process = require("@lute/process") -local apiKey = process.args[1] -assert(apiKey, "argument #1 must be a valid Open Cloud API key") +assert(process.cwd():match("examples/plugin"), "you must be in the `examples/plugin` folder when running this script") -assert(process.cwd:match("examples/plugin"), "you must be in the `examples/plugin` folder when running this script") +process.system("rojo build -o plugin.rbxm") -process.exec("rojo", { "build", "-o", "plugin.rbxm" }) - -rbxasset.publishPackageAsync(process.cwd, "plugin", apiKey) +rbxasset.publishPackageAsync(process.cwd(), "plugin", process.env.ROBLOX_API_KEY) diff --git a/examples/plugin/e2e.spec.luau b/examples/plugin/e2e.spec.luau index 8c06f90..ad35dd4 100644 --- a/examples/plugin/e2e.spec.luau +++ b/examples/plugin/e2e.spec.luau @@ -1,25 +1,24 @@ -local frktest = require("@pkg/frktest") -local fs = require("@lune/fs") -local process = require("@lune/process") -local serde = require("@lune/serde") +local expect = require("@std/test/assert") +local fs = require("@lute/fs") +local process = require("@lute/process") +local test = require("@std/test") +local toml = require("@root/lib/toml") local getAssetDetailsAsync = require("@root/requests/getAssetDetailsAsync") local getOrCreateAssetLockfile = require("@root/manifest/getOrCreateAssetLockfile") local readManifest = require("@root/manifest/readManifest") local run = require("@root/lib/run") -local test = frktest.test -local check = frktest.assert.check - local ROOT_PATH = "examples/plugin" local MANIFEST_PATH = `{ROOT_PATH}/rbxasset.toml` local function appendTextForTesting(path: string, text: string) - local content = fs.readFile(path) + local handle = fs.open(path, "w") + local content = fs.read(handle) content ..= text - fs.writeFile(path, content) + fs.write(handle, content) + fs.close(handle) end - local apiKey = process.env.ROBLOX_API_KEY assert(apiKey, "End-to-end tests can only be run when the ROBLOX_API_KEY envvar is set to a valid Open Cloud API key") @@ -39,7 +38,10 @@ test.case("uploads to the Creator Store", function() appendTextForTesting(path, `\nprint("New!")`) local success, result = pcall(function() - run("lune", { "run", "deploy.luau", apiKey }, { + return run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, cwd = ROOT_PATH, }) end) @@ -50,7 +52,7 @@ test.case("uploads to the Creator Store", function() assetDetails = getAssetDetailsAsync(assetId, apiKey) - check.not_equal(assetDetails.revisionId, prevRevisionId) + expect.neq(assetDetails.revisionId, prevRevisionId) end) test.case("text fields in rbxasest.toml get mirrored to the asset details", function() @@ -63,10 +65,15 @@ test.case("text fields in rbxasest.toml get mirrored to the asset details", func manifest.assets.plugin.name = `{manifest.assets.plugin.name}\n({id})` manifest.assets.plugin.description = `{manifest.assets.plugin.description}\n({id})` - fs.writeFile(MANIFEST_PATH, serde.encode("toml", manifest)) + local handle = fs.open(MANIFEST_PATH, "w") + fs.write(handle, toml.serialize(manifest)) + fs.close(handle) local success, result = pcall(function() - run("lune", { "run", "deploy.luau", apiKey }, { + return run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, cwd = ROOT_PATH, }) end) @@ -77,9 +84,9 @@ test.case("text fields in rbxasest.toml get mirrored to the asset details", func assetDetails = getAssetDetailsAsync(assetId, apiKey) - check.equal(assetDetails.displayName, manifest.assets.plugin.name) - check.equal(assetDetails.description, manifest.assets.plugin.description) - check.not_equal(assetDetails.revisionId, prevRevisionId) + expect.eq(assetDetails.displayName, manifest.assets.plugin.name) + expect.eq(assetDetails.description, manifest.assets.plugin.description) + expect.neq(assetDetails.revisionId, prevRevisionId) end) test.case("does not publish a new version when there are no changes", function() @@ -87,11 +94,14 @@ test.case("does not publish a new version when there are no changes", function() local prevRevisionId = assetDetails.revisionId - run("lune", { "run", "deploy.luau", apiKey }, { + run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, cwd = ROOT_PATH, }) assetDetails = getAssetDetailsAsync(assetId, apiKey) - check.equal(assetDetails.revisionId, prevRevisionId) + expect.eq(assetDetails.revisionId, prevRevisionId) end) diff --git a/examples/workspace/deploy.luau b/examples/workspace/deploy.luau index 2ee01a2..56a299e 100644 --- a/examples/workspace/deploy.luau +++ b/examples/workspace/deploy.luau @@ -1,21 +1,18 @@ -local rbxasset = require("@root/") +local rbxasset = require("@root/init") -local process = require("@lune/process") - -local apiKey = process.args[1] -assert(apiKey, "argument #1 must be a valid Open Cloud API key") +local process = require("@lute/process") assert( - process.cwd:match("examples/workspace"), + process.cwd():match("examples/workspace"), "you must be in the `examples/workspace` folder when running this script" ) -process.exec("rojo", { "build", "-o", "../package.rbxm" }, { +process.system("rojo build -o ../package.rbxm", { cwd = "package", }) -process.exec("rojo", { "build", "-o", "../plugin.rbxm" }, { +process.system("rojo build -o ../plugin.rbxm", { cwd = "plugin", }) -rbxasset.publishWorkspaceAsync(process.cwd, apiKey) +rbxasset.publishWorkspaceAsync(process.cwd(), process.env.ROBLOX_API_KEY) diff --git a/examples/workspace/e2e.spec.luau b/examples/workspace/e2e.spec.luau index 8477485..27a93a7 100644 --- a/examples/workspace/e2e.spec.luau +++ b/examples/workspace/e2e.spec.luau @@ -1,16 +1,14 @@ -local frktest = require("@pkg/frktest") -local fs = require("@lune/fs") -local process = require("@lune/process") -local serde = require("@lune/serde") +local expect = require("@std/test/assert") +local fs = require("@lute/fs") +local process = require("@lute/process") +local test = require("@std/test") +local toml = require("@root/lib/toml") local getAssetDetailsAsync = require("@root/requests/getAssetDetailsAsync") local getOrCreateAssetLockfile = require("@root/manifest/getOrCreateAssetLockfile") local readManifest = require("@root/manifest/readManifest") local run = require("@root/lib/run") -local test = frktest.test -local check = frktest.assert.check - local ROOT_PATH = "examples/workspace" local MANIFEST_PATH = `{ROOT_PATH}/rbxasset.toml` @@ -22,9 +20,11 @@ local packageAssetId = lockfile.assets.package.assetId local pluginAssetId = lockfile.assets.plugin.assetId local function appendTextForTesting(path: string, text: string) - local content = fs.readFile(path) + local handle = fs.open(path, "w+") + local content = fs.read(handle) content ..= text - fs.writeFile(path, content) + fs.write(handle, content) + fs.close(handle) end run("rojo", { "build", `plugin/default.project.json`, "-o", "plugin.rbxm" }, { @@ -51,7 +51,7 @@ test.case("uploads each workspace member to the Creator Store", function() end local success, result = pcall(function() - run("lune", { "run", "deploy.luau", apiKey }, { + return run("lute", { "deploy.luau", apiKey }, { cwd = ROOT_PATH, }) end) @@ -63,8 +63,8 @@ test.case("uploads each workspace member to the Creator Store", function() packageAssetDetails = getAssetDetailsAsync(packageAssetId, apiKey) pluginAssetDetails = getAssetDetailsAsync(pluginAssetId, apiKey) - check.not_equal(packageAssetDetails.revisionId, prevPackageRevisionId) - check.not_equal(pluginAssetDetails.revisionId, prevPluginRevisionId) + expect.neq(packageAssetDetails.revisionId, prevPackageRevisionId) + expect.neq(pluginAssetDetails.revisionId, prevPluginRevisionId) end) test.case("text fields in rbxasest.toml get mirrored to the asset details", function() @@ -81,10 +81,15 @@ test.case("text fields in rbxasest.toml get mirrored to the asset details", func manifest.assets.plugin.name = `{manifest.assets.plugin.name}\n({id})` manifest.assets.plugin.description = `{manifest.assets.plugin.description}\n({id})` - fs.writeFile(MANIFEST_PATH, serde.encode("toml", manifest)) + local handle = fs.open(MANIFEST_PATH, "w") + fs.write(handle, toml.serialize(manifest)) + fs.close(handle) local success, result = pcall(function() - run("lune", { "run", "deploy.luau", apiKey }, { + return run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, cwd = ROOT_PATH, }) end) @@ -96,11 +101,11 @@ test.case("text fields in rbxasest.toml get mirrored to the asset details", func packageAssetDetails = getAssetDetailsAsync(packageAssetId, apiKey) pluginAssetDetails = getAssetDetailsAsync(pluginAssetId, apiKey) - check.equal(packageAssetDetails.displayName, manifest.assets.package.name) - check.equal(packageAssetDetails.description, manifest.assets.package.description) + expect.eq(packageAssetDetails.displayName, manifest.assets.package.name) + expect.eq(packageAssetDetails.description, manifest.assets.package.description) - check.equal(pluginAssetDetails.displayName, manifest.assets.plugin.name) - check.equal(pluginAssetDetails.description, manifest.assets.plugin.description) + expect.eq(pluginAssetDetails.displayName, manifest.assets.plugin.name) + expect.eq(pluginAssetDetails.description, manifest.assets.plugin.description) end) test.case("only publishes a new version for changed workspace members", function() @@ -116,7 +121,10 @@ test.case("only publishes a new version for changed workspace members", function appendTextForTesting(path, `\nprint("New!")`) local success, result = pcall(function() - run("lune", { "run", "deploy.luau", apiKey }, { + return run("lute", { "deploy.luau" }, { + env = { + ROBLOX_API_KEY = apiKey, + }, cwd = ROOT_PATH, }) end) @@ -128,6 +136,6 @@ test.case("only publishes a new version for changed workspace members", function packageAssetDetails = getAssetDetailsAsync(packageAssetId, apiKey) pluginAssetDetails = getAssetDetailsAsync(pluginAssetId, apiKey) - check.not_equal(packageAssetDetails.revisionId, prevPackageRevisionId) - check.equal(pluginAssetDetails.revisionId, prevPluginRevisionId) + expect.neq(packageAssetDetails.revisionId, prevPackageRevisionId) + expect.eq(pluginAssetDetails.revisionId, prevPluginRevisionId) end) diff --git a/foreman.toml b/foreman.toml index 538f0f3..234045e 100644 --- a/foreman.toml +++ b/foreman.toml @@ -1,6 +1,6 @@ [tools] luau-lsp = { source = "JohnnyMorganz/luau-lsp", version = "=1.53.0" } -lune = { source = "lune-org/lune", version = "=0.9.4" } +lute = { source = "luau-lang/lute", version = "=0.1.0-nightly.20251217" } rojo = { source = "rojo-rbx/rojo", version = "=7.5.0" } selene = { source = "kampfkarren/selene", version = "=0.28.0" } stylua = { source = "johnnymorganz/stylua", version = "=2.1.0" } diff --git a/scripts/analyze.luau b/scripts/analyze.luau new file mode 100644 index 0000000..5edee22 --- /dev/null +++ b/scripts/analyze.luau @@ -0,0 +1,10 @@ +local run = require("@root/lib/run") + +run("luau-lsp", { + "analyze", + "--settings=.vscode/settings.json", + + "examples", + "scripts", + "src", +}) diff --git a/.lune/build.luau b/scripts/build.luau similarity index 89% rename from .lune/build.luau rename to scripts/build.luau index ec5f171..27dead9 100644 --- a/.lune/build.luau +++ b/scripts/build.luau @@ -1,4 +1,4 @@ -local fs = require("@lune/fs") +local fs = require("@lute/fs") local run = require("@root/lib/run") @@ -21,7 +21,7 @@ run("rm", { "-rf", BUILD_PATH }) run("mkdir", { "-p", ARTIFACT_PATH }) for _, includeName in INCLUDES do - if fs.isDir(includeName) then + if fs.type(includeName) == "dir" then run("cp", { "-R", includeName, `{ARTIFACT_PATH}/{includeName}` }) else run("cp", { includeName, ARTIFACT_PATH }) diff --git a/scripts/install.luau b/scripts/install.luau new file mode 100644 index 0000000..d78acb2 --- /dev/null +++ b/scripts/install.luau @@ -0,0 +1,65 @@ +local fs = require("@lute/fs") + +local run = require("@root/lib/run") + +local PACKAGES_PATH = "pkg" +local PACKAGE_INDEX_NAME = "_Index" + +local function installGitDependencies() + local repos = { + { git = "osyrisrblx/t@1dbfccc1", main = "lib" }, + } + + for _, repo in repos do + local parts = repo.git:split("@") + local repoPath = parts[1] + local rev = parts[2] + + local repoParts = repoPath:split("/") + local projectName = repoParts[2] + local dest = `{PACKAGES_PATH}/{PACKAGE_INDEX_NAME}/{projectName}` + + run("git", { "clone", `https://github.com/{repoPath}`, dest }) + + run("git", { "reset", "--hard", rev }, { + cwd = dest, + }) + + local linkerPath = `{PACKAGES_PATH}/{projectName}.luau` + local handle = fs.open(linkerPath, "w+") + fs.write(handle, `return require("./{PACKAGE_INDEX_NAME}/{projectName}/{repo.main}")`) + fs.close(handle) + end +end + +local function installLuteBatteries() + local commit = "fc19ad0" + local batteries = { + "cli.luau", + "pp.luau", + --[[ + TODO: Install the TOML battery once this issue is fixed: + https://github.com/luau-lang/lute/issues/643 + + Currently we use a patched version of this module at + `src/lib/toml.luau` which supports quoted dictionary keys. + ]] + -- "toml.luau", + "richterm.luau", + } + + run("mkdir", { "-p", PACKAGES_PATH }) + for _, battery in batteries do + run("curl", { "-LO", `https://raw.githubusercontent.com/luau-lang/lute/{commit}/batteries/{battery}` }, { + cwd = PACKAGES_PATH, + }) + end +end + +run("rm", { "-rf", PACKAGES_PATH }) + +-- Setup typedefs for the current Lute version +run("lute", { "setup" }) + +installGitDependencies() +installLuteBatteries() diff --git a/scripts/lib/dotenv.luau b/scripts/lib/dotenv.luau new file mode 100644 index 0000000..a256a38 --- /dev/null +++ b/scripts/lib/dotenv.luau @@ -0,0 +1,32 @@ +local fs = require("@lute/fs") +local process = require("@lute/process") + +local DOTENV_FILE_NAME = ".env" + +local function dotenv() + local current = process.cwd() + local env: { [string]: string } = {} + + for _, file in fs.listdir(current) do + local filePath = `{current}/{file.name}` + if file.name == DOTENV_FILE_NAME then + local handle = fs.open(filePath, "r") + local content = fs.read(handle) + + for line in content:gmatch("[^\r\n]+") do + local key, value = line:match("([%u_]+)[%s+]?=[%s+]?(.*)") + + if key and value and value ~= "" then + value = value:gsub('"', "") + env[key] = value + end + end + end + end + + for key, value in env do + process.env[key] = value + end +end + +return dotenv diff --git a/scripts/lib/find.luau b/scripts/lib/find.luau new file mode 100644 index 0000000..e6b8404 --- /dev/null +++ b/scripts/lib/find.luau @@ -0,0 +1,31 @@ +local fs = require("@lute/fs") + +local function find(root: string, filenamePattern: string): { string } + local matches: { string } = {} + + if not fs.exists(root) or fs.type(root) == "file" then + return matches + end + + local function search(path: string) + if fs.type(path) == "dir" then + for _, child in fs.listdir(path) do + local childPath = `{path}/{child.name}` + search(childPath) + end + else + local parts = path:split("/") + local filename = parts[#parts] + + if filename:match(filenamePattern) then + table.insert(matches, path) + end + end + end + + search(root) + + return matches +end + +return find diff --git a/.lune/lint.luau b/scripts/lint.luau similarity index 93% rename from .lune/lint.luau rename to scripts/lint.luau index 7c5bcde..f6455f4 100644 --- a/.lune/lint.luau +++ b/scripts/lint.luau @@ -1,9 +1,9 @@ local run = require("@root/lib/run") local foldersToLint = { - ".lune", - "src", "examples", + "scripts", + "src", } run("selene", foldersToLint) diff --git a/scripts/test.luau b/scripts/test.luau new file mode 100644 index 0000000..8b4d50c --- /dev/null +++ b/scripts/test.luau @@ -0,0 +1,48 @@ +local cli = require("@pkg/cli") +local luau = require("@std/luau") +local process = require("@lute/process") +local test = require("@std/test") + +local dotenv = require("./lib/dotenv") +local find = require("./lib/find") +local logging = require("@root/logging") + +local args = cli.parser() + +args:add("apiKey", "option", { + help = "Open Cloud API key for running end-to-end tests. This can also be supplied by setting the ROBLOX_API_KEY environment variable", +}) +args:add("e2e", "flag", { + help = "Run end-to-end tests", +}) + +args:parse({ ... }) + +dotenv() + +local apiKey = args:get("apiKey") or process.env.ROBLOX_API_KEY + +local function loadTestCases(path: string) + local files = find(path, "%.spec%.luau") + + logging.info("spec files:") + for _, file in files do + logging.info(` {file}`) + end + + for _, file in files do + luau.loadbypath(file) + end +end + +loadTestCases("src") + +if args:has("e2e") then + -- We need to set the API key as an env variable so the E2E tests also have + -- access to it + process.env.ROBLOX_API_KEY = apiKey + + loadTestCases("examples") +end + +test.run() diff --git a/src/lib/join.luau b/src/lib/join.luau index 5c63601..428a8f5 100644 --- a/src/lib/join.luau +++ b/src/lib/join.luau @@ -10,7 +10,7 @@ local function join(...: any): T continue end - for key, value in pairs(dictionary) do + for key, value in dictionary do result[key] = value end end diff --git a/src/lib/run.luau b/src/lib/run.luau index 5b85a10..f8f895a 100644 --- a/src/lib/run.luau +++ b/src/lib/run.luau @@ -1,26 +1,32 @@ -local process = require("@lune/process") - -local logging = require("@root/logging") +local process = require("@lute/process") +local richterm = require("@pkg/richterm") type Options = { cwd: string?, - env: { [string]: any }?, + env: { [string]: string }?, + stdio: any, } local function run(program: string, params: { string }, options: Options?): string - logging.debug(`RUN > {program} {table.concat(params, " ")}`) + local command = `{program} {table.concat(params, " ")}` + + print(richterm.bold(`> {command}`)) - local result = process.exec(program, params, { - stdio = "inherit", + local result = process.run({ + program, + table.unpack(params), + }, { shell = true, + stdio = if options and options.stdio then options.stdio else "inherit", cwd = if options then options.cwd else nil, env = if options then options.env else nil, }) - if result.code == 0 then - return result.stdout:gsub("\n$", "") + if result.ok then + local trimmed = result.stdout:gsub("^\n", ""):gsub("\n$", "") + return trimmed else - error(result.stderr) + error(result.stderr, 2) end end diff --git a/src/lib/toml.luau b/src/lib/toml.luau new file mode 100644 index 0000000..9afa641 --- /dev/null +++ b/src/lib/toml.luau @@ -0,0 +1,226 @@ +-- upstream: https://github.com/luau-lang/lute/blob/fc19ad0b29cd6cae692607995dd6743f229c61b7/batteries/toml.luau + +local toml = {} + +-- serialization +type SerializerState = { + buf: buffer, + cursor: number, +} + +local function serializeValue(value: string | number) + if typeof(value) == "string" then + value = string.gsub(value, "\\", "\\\\") + value = string.gsub(value, "\n", "\\n") + value = string.gsub(value, "\t", "\\t") + return '"' .. value .. '"' + elseif value == math.huge then + return "inf" + elseif value == -math.huge then + return "-inf" + elseif value ~= value then + return "nan" + else + return tostring(value) + end +end + +local function hasNestedTables(tbl: {}) + for _, v in tbl do + if typeof(v) == "table" and next(v) ~= nil then + return true + end + end + return false +end + +local function tableToToml(tbl: {}, parent: string) + local result = "" + local subTables = {} + local hasDirectValues = false + + for k, v in tbl do + if typeof(v) == "table" and next(v) ~= nil then + if #v > 0 then + for _, entry in v do + result ..= "\n[[" .. (parent and parent .. "." or "") .. k .. "]]\n" + result ..= tableToToml(entry, nil) + end + else + subTables[k] = v + end + else + hasDirectValues = true + result ..= k .. " = " .. serializeValue(v) .. "\n" + end + end + + for k, v in subTables do + local key = parent and (parent .. "." .. k) or k + + if hasNestedTables(v) == true then + result ..= tableToToml(v, key) + continue + end + + if hasDirectValues or parent then + result ..= "\n[" .. key .. "]\n" + end + + result ..= tableToToml(v, key) + end + + return result +end + +local function serialize(tbl: {}): string + return tableToToml(tbl, nil) +end + +-- deserialization +type DeserializerState = { + buf: string, + cursor: number, +} + +local function skipWhitespace(state: DeserializerState) + local pos = state.cursor + while pos <= string.len(state.buf) and string.match(string.sub(state.buf, pos, pos), "%s") do + pos += 1 + end + state.cursor = pos +end + +local function readLine(state: DeserializerState) + local nextLine = string.find(state.buf, "\n", state.cursor) or string.len(state.buf) + 1 + local line = string.sub(state.buf, state.cursor, nextLine - 1) + state.cursor = nextLine + 1 + return line +end + +local function deserialize(input: string) + -- Normalize line endings: Replace Windows (\r\n) and old Mac (\r) with Unix (\n) + input = string.gsub(input, "\r\n", "\n") + input = string.gsub(input, "\r", "\n") + + local state: DeserializerState = { + buf = input, + cursor = 1, + } + local result = {} + local currentTable = result + local arrayTables = {} + + while state.cursor <= string.len(state.buf) do + skipWhitespace(state) + local line = readLine(state) + + if line == "" or string.sub(line, 1, 1) == "#" then + continue + end + + if string.match(line, "^%[%[(.-)%]%]$") then + local tableName = string.match(line, "^%[%[(.-)%]%]$") + arrayTables[tableName] = arrayTables[tableName] or {} + + local newEntry = {} + table.insert(arrayTables[tableName], newEntry) + + result[tableName] = arrayTables[tableName] + currentTable = newEntry + elseif string.match(line, "^%[(.-)%]$") then + local tablePath = string.match(line, "^%[(.-)%]$") + local parent = result + + local sections = {} + local pos = 1 + while pos <= #tablePath do + while pos <= #tablePath and string.match(string.sub(tablePath, pos, pos), "%s") do + pos = pos + 1 + end + + if pos > #tablePath then + break + end + + local char = string.sub(tablePath, pos, pos) + if char == '"' or char == "'" then + local endPos = pos + 1 + while endPos <= #tablePath do + if string.sub(tablePath, endPos, endPos) == char then + local escapeCount = 0 + local checkPos = endPos - 1 + while checkPos > pos and string.sub(tablePath, checkPos, checkPos) == "\\" do + escapeCount = escapeCount + 1 + checkPos = checkPos - 1 + end + if escapeCount % 2 == 0 then + break + end + end + endPos = endPos + 1 + end + table.insert(sections, string.sub(tablePath, pos + 1, endPos - 1)) + pos = endPos + 1 + else + local endPos = pos + while endPos <= #tablePath and string.sub(tablePath, endPos, endPos) ~= "." do + endPos = endPos + 1 + end + local section = string.sub(tablePath, pos, endPos - 1) + section = string.match(section, "^%s*(.-)%s*$") + if section ~= "" then + table.insert(sections, section) + end + pos = endPos + 1 + end + + if pos <= #tablePath and string.sub(tablePath, pos, pos) == "." then + pos = pos + 1 + end + end + + for _, section in sections do + if not parent[section] then + parent[section] = {} + end + parent = parent[section] + end + + currentTable = parent + elseif string.match(line, "^(.-)%s*=%s*(.-)$") then + local key, value = string.match(line, "^(.-)%s*=%s*(.-)$") + key = string.match(key, "^%s*(.-)%s*$") + value = string.match(value, "^%s*(.-)%s*$") + + if string.match(value, '^"(.*)"$') or string.match(value, "^'(.*)'$") then + value = string.sub(value, 2, -2) + value = string.gsub(value, "\\\\", "\\") + value = string.gsub(value, "\\n", "\n") + value = string.gsub(value, "\\t", "\t") + elseif tonumber(value) then + value = tonumber(value) + elseif value == "true" then + value = true + elseif value == "false" then + value = false + elseif value == "inf" or value == "+inf" then + value = math.huge + elseif value == "-inf" then + value = -math.huge + elseif value == "nan" then + value = 0 / 0 + end + + currentTable[key] = value + end + end + + return result +end + +-- user-facing +toml.serialize = serialize +toml.deserialize = deserialize + +return table.freeze(toml) diff --git a/src/lib/toml.spec.luau b/src/lib/toml.spec.luau new file mode 100644 index 0000000..9138900 --- /dev/null +++ b/src/lib/toml.spec.luau @@ -0,0 +1,66 @@ +local expect = require("@std/test/assert") +local test = require("@std/test") + +local toml = require("./toml") + +test.case("should parse quoted keys with dots correctly", function() + local input = [[ +[assets.package] +assetId = "120786139379662" + +[images."icon.png"] +assetId = "95124518202499" +hash = "fac185770cf5e824d4c0a9b59e547df42dc10011c5ad139bfc2c7e1507eef631" +]] + + local result = toml.deserialize(input) + + expect.neq(result.assets, nil) + expect.neq(result.assets.package, nil) + expect.eq(result.assets.package.assetId, "120786139379662") + + expect.neq(result.images, nil) + expect.neq(result.images["icon.png"], nil) + expect.eq(result.images["icon.png"].assetId, "95124518202499") + expect.eq(result.images["icon.png"].hash, "fac185770cf5e824d4c0a9b59e547df42dc10011c5ad139bfc2c7e1507eef631") +end) + +test.case("should handle multiple quoted keys in path", function() + local input = [[ +["outer.key"."inner.key"] +value = "test" +]] + + local result = toml.deserialize(input) + + expect.neq(result["outer.key"], nil) + expect.neq(result["outer.key"]["inner.key"], nil) + expect.eq(result["outer.key"]["inner.key"].value, "test") +end) + +test.case("should handle mixed quoted and unquoted keys", function() + local input = [[ +[section."quoted.key".another] +value = "mixed" +]] + + local result = toml.deserialize(input) + + expect.neq(result.section, nil) + expect.neq(result.section["quoted.key"], nil) + expect.neq(result.section["quoted.key"].another, nil) + expect.eq(result.section["quoted.key"].another.value, "mixed") +end) + +test.case("should handle single-quoted keys", function() + local input = [[ +[images.'icon.png'] +assetId = "123" +]] + + local result = toml.deserialize(input) + + expect.neq(result.images, nil) + expect.neq(result.images["icon.png"], nil) + expect.eq(result.images["icon.png"].assetId, "123") +end) diff --git a/src/logging.luau b/src/logging.luau index 8979bea..65c1135 100644 --- a/src/logging.luau +++ b/src/logging.luau @@ -1,10 +1,8 @@ -local process = require("@lune/process") -local stdio = require("@lune/stdio") +local process = require("@lute/process") +local richterm = require("@pkg/richterm") type LogLevel = "info" | "warn" | "err" | "debug" -local LOG_LEVEL = if process.env.LOG_LEVEL then process.env.LOG_LEVEL:lower() else "info" - local LOG_LEVEL_ORDER = { "err", "info", @@ -12,8 +10,17 @@ local LOG_LEVEL_ORDER = { "debug", } +local function getLogLevel(): LogLevel + local logLevel = process.env.LOG_LEVEL + if logLevel and logLevel ~= "" then + return logLevel:lower() :: LogLevel + else + return "info" + end +end + local function canLog(logLevelToCheck: LogLevel): boolean - local maxPriority = table.find(LOG_LEVEL_ORDER, LOG_LEVEL) + local maxPriority = table.find(LOG_LEVEL_ORDER, getLogLevel()) local priorityToCheck = table.find(LOG_LEVEL_ORDER, logLevelToCheck) if maxPriority and priorityToCheck then @@ -27,31 +34,25 @@ local logging = {} function logging.info(...) if canLog("info") then - print(`[info]`, ...) + print(richterm.blue(`[info] {...}`)) end end function logging.warn(...) if canLog("warn") then - stdio.write(stdio.color("yellow")) - print(`[warn]`, ...) - stdio.write(stdio.color("reset")) + print(richterm.yellow(`[warn] {...}`)) end end function logging.err(...) if canLog("err") then - stdio.write(stdio.color("red")) - print(`[err]`, ...) - stdio.write(stdio.color("reset")) + print(richterm.red(`[err] {...}`)) end end function logging.debug(...) if canLog("debug") then - stdio.write(stdio.color("black")) - print(`[debug]`, ...) - stdio.write(stdio.color("reset")) + print(richterm.black(`[debug] {...}`)) end end diff --git a/src/manifest/getOrCreateAssetLockfile.luau b/src/manifest/getOrCreateAssetLockfile.luau index 1494ab7..29f1d2f 100644 --- a/src/manifest/getOrCreateAssetLockfile.luau +++ b/src/manifest/getOrCreateAssetLockfile.luau @@ -1,5 +1,6 @@ -local fs = require("@lune/fs") -local serde = require("@lune/serde") +local fs = require("@lute/fs") +local pp = require("@pkg/pp") +local toml = require("@root/lib/toml") local constants = require("@root/constants") local join = require("@root/lib/join") @@ -15,19 +16,21 @@ local function getOrCreateAssetLockfile(packagePath: string): Lockfile -- on the first upload of the asset. So we just catch any error here local content: string local success = pcall(function() - content = fs.readFile(lockfilePath) + local handle = fs.open(lockfilePath) + content = fs.read(handle) + fs.close(handle) end) - local parsedLockfile = if success then serde.decode("toml", content) else nil - local lockfile: Lockfile = join({ + local parsedLockfile = if success then toml.deserialize(content) else nil + local lockfile = join({ assets = {}, images = {}, - }, parsedLockfile) + }, parsedLockfile) :: Lockfile local isLockfileValid, message = types.validateLockfile(lockfile) assert(isLockfileValid, `failed to parse asset lockfile at {lockfilePath}: {message}`) - logging.debug(`loaded asset lockfile {lockfile}`) + logging.debug(`loaded asset lockfile {pp(lockfile)}`) return lockfile end diff --git a/src/manifest/getOrCreateAssetLockfile.spec.luau b/src/manifest/getOrCreateAssetLockfile.spec.luau index 5e33636..b97eb84 100644 --- a/src/manifest/getOrCreateAssetLockfile.spec.luau +++ b/src/manifest/getOrCreateAssetLockfile.spec.luau @@ -1,17 +1,16 @@ -local frktest = require("@pkg/frktest") -local test = frktest.test -local check = frktest.assert.check +local expect = require("@std/test/assert") +local test = require("@std/test") local getOrCreateAssetLockfile = require("./getOrCreateAssetLockfile") test.case("reads an asset lockfile from disk", function() local lockfile = getOrCreateAssetLockfile("examples/package") - check.equal(lockfile.assets["package"].assetId, "115689279560293") + expect.neq(lockfile.assets["package"].assetId, nil) end) test.case("creates a blank asset lockfile when not found on disk", function() local lockfile = getOrCreateAssetLockfile("/nowhere") - check.table.equal(lockfile, { + expect.tableeq(lockfile :: { [unknown]: unknown }, { assets = {}, images = {}, }) diff --git a/src/manifest/readManifest.luau b/src/manifest/readManifest.luau index acdf801..f518093 100644 --- a/src/manifest/readManifest.luau +++ b/src/manifest/readManifest.luau @@ -1,5 +1,6 @@ -local fs = require("@lune/fs") -local serde = require("@lune/serde") +local fs = require("@lute/fs") +local pp = require("@pkg/pp") +local toml = require("@root/lib/toml") local constants = require("@root/constants") local logging = require("@root/logging") @@ -9,18 +10,20 @@ type Manifest = types.Manifest local function readManifest(packagePath: string): Manifest local manifestPath = `{packagePath}/{constants.ASSET_MANIFEST_FILENAME}` - local content: string - local success, err = pcall(function() - content = fs.readFile(manifestPath) + local success, result = pcall(function() + local handle = fs.open(manifestPath, "r") + local content = fs.read(handle) + fs.close(handle) + return content end) - assert(success, `failed to read asset manifest at {manifestPath}: {err}`) + assert(success, `failed to read asset manifest at {manifestPath}: {result}`) - local parsedContent = serde.decode("toml", content) + local parsedContent = toml.deserialize(result) local isManifestValid, message = types.validateManifest(parsedContent) assert(isManifestValid, `failed to parse asset manifest at {manifestPath}: {message}`) - logging.debug("loaded asset manifest", parsedContent) + logging.debug("loaded asset manifest", pp(parsedContent)) return parsedContent :: Manifest end diff --git a/src/manifest/writeLockfile.luau b/src/manifest/writeLockfile.luau index e15c6c9..0335e2c 100644 --- a/src/manifest/writeLockfile.luau +++ b/src/manifest/writeLockfile.luau @@ -1,5 +1,5 @@ -local fs = require("@lune/fs") -local serde = require("@lune/serde") +local fs = require("@lute/fs") +local toml = require("@root/lib/toml") local constants = require("@root/constants") local logging = require("@root/logging") @@ -9,7 +9,7 @@ type Lockfile = types.Lockfile local function writeLockfile(packagePath: string, lockfile: Lockfile) local lockfilePath = `{packagePath}/{constants.ASSET_LOCKFILE_FILENAME}` - fs.writeFile(lockfilePath, serde.encode("toml", lockfile)) + fs.writestringtofile(lockfilePath, toml.serialize(lockfile)) logging.debug(`wrote asset lockfile {lockfilePath}`) end diff --git a/src/requests/createImageAsync.luau b/src/requests/createImageAsync.luau index 5a38a11..b5db29b 100644 --- a/src/requests/createImageAsync.luau +++ b/src/requests/createImageAsync.luau @@ -1,5 +1,5 @@ -local fs = require("@lune/fs") -local serde = require("@lune/serde") +local fs = require("@lute/fs") +local json = require("@std/json") local createFormData = require("@root/requests/forms/createFormData") local fetch = require("@root/requests/fetch") @@ -15,6 +15,10 @@ local function createImageAsync( ) local creatorIdKey = if creatorType == types.CreatorType.User then "userId" else "groupId" + local imageHandle = fs.open(imagePath, "r") + local imageContent = fs.read(imageHandle) + fs.close(imageHandle) + local formData = createFormData({ request = { value = { @@ -30,7 +34,7 @@ local function createImageAsync( }, fileContent = { filename = imagePath:match("[/\\](.+)$"), - value = fs.readFile(imagePath), + value = imageContent, contentType = "image/png", }, }) @@ -44,7 +48,8 @@ local function createImageAsync( body = formData.body, }) - local body = serde.decode("json", res.body) + -- FIXME: Narrow the return value of `json.deserialize` instead of casting to `any` + local body: any = json.deserialize(res.body) return waitForAssetOperationAsync(body.operationId, apiKey) end diff --git a/src/requests/fetch.luau b/src/requests/fetch.luau index fd386dc..33b92d1 100644 --- a/src/requests/fetch.luau +++ b/src/requests/fetch.luau @@ -1,5 +1,5 @@ -local net = require("@lune/net") -local serde = require("@lune/serde") +local json = require("@std/json") +local net = require("@lute/net") local logging = require("@root/logging") @@ -16,9 +16,10 @@ local function fetch(url: string, options: FetchOptions) logging.debug(`{method} {url}`) - local res = net.request({ - url = url, - method = method, + net.request(url) + + local res = net.request(url, { + method = method :: string, headers = options.headers, body = options.body, }) @@ -26,9 +27,13 @@ local function fetch(url: string, options: FetchOptions) if res.ok then return res else - local body = serde.decode("json", res.body) + -- TODO: This block might need to be revised to work with Lute + + -- FIXME: Narrow the return value of `json.deserialize` instead of + -- casting to `any` + local body: any = json.deserialize(res.body) - local problems = {} + local problems: { string } = {} if body.errors then for _, err in body.errors do table.insert(problems, err.message) @@ -38,7 +43,7 @@ local function fetch(url: string, options: FetchOptions) end error( - `{res.statusCode} {res.statusMessage} when attempting to {method} {url}` + `{res.status} {res.body} when attempting to {method} {url}` .. if #problems > 0 then `: {table.concat(problems, ", ")}` else "", 2 ) diff --git a/src/requests/forms/createFormData.luau b/src/requests/forms/createFormData.luau index 7d0e163..bbf6b55 100644 --- a/src/requests/forms/createFormData.luau +++ b/src/requests/forms/createFormData.luau @@ -1,4 +1,5 @@ -local serde = require("@lune/serde") +local crypto = require("@lute/crypto") +local json = require("@std/json") type FormField = { value: any, @@ -10,7 +11,7 @@ local function createFormDataBody(boundary: string, data: { [string]: FormField local body = {} for key, field in data do - local value = if field.contentType == "application/json" then serde.encode("json", field.value) else field.value + local value = if field.contentType == "application/json" then json.serialize(field.value) else field.value local lines = { `--{boundary}`, @@ -30,7 +31,9 @@ local function createFormDataBody(boundary: string, data: { [string]: FormField end local function createFormData(data: { [string]: FormField }) - local boundary = "LuauFormBoundary" .. serde.hash("md5", tostring(math.random())) + local hash = buffer.tostring(crypto.digest(crypto.hash.md5, tostring(math.random()))) + + local boundary = "LuauFormBoundary" .. hash local body = createFormDataBody(boundary, data) return { diff --git a/src/requests/forms/createFormData.spec.luau b/src/requests/forms/createFormData.spec.luau index 3224f36..48b85c0 100644 --- a/src/requests/forms/createFormData.spec.luau +++ b/src/requests/forms/createFormData.spec.luau @@ -1,6 +1,5 @@ -local frktest = require("@pkg/frktest") -local test = frktest.test -local check = frktest.assert.check +local expect = require("@std/test/assert") +local test = require("@std/test") local createFormData = require("./createFormData") @@ -10,7 +9,7 @@ local function assertStringsMatch(base: string, expected: string) table.insert(baseLines, line) end - local expectedLines = {} + local expectedLines: { string } = {} for line in expected:gmatch("[^\r\n]+") do table.insert(expectedLines, line) end @@ -18,6 +17,7 @@ local function assertStringsMatch(base: string, expected: string) for index, baseLine in baseLines do local expectedLine = expectedLines[index] if baseLine ~= expectedLine then + print(baseLine, expectedLine) error(`strings do not match at line {index}\n- base: "{baseLine}"\n- expected: "{expectedLine}"`, 2) end end @@ -34,9 +34,9 @@ test.case("automatically converts objects to json", function() }, }) - local match = formData.body:match('{"bar":true,"baz":"str"}') + local match = formData.body:match('{ "baz": "str", "bar": true}') - check.not_nil(match) + expect.neq(match, nil) end) test.case("body supports multiple parts", function() @@ -64,7 +64,7 @@ test.case("body supports multiple parts", function() `Content-Disposition: form-data; name="request"`, "Content-Type: application/json", "", - '{"assetType":"Image","creationContext":{"creator":{"userId":12345678}},"displayName":"Display Name"}', + '{ "creationContext": { "creator": { "userId": 12345678 } }, "assetType": "Image", "displayName": "Display Name"}', `--{formData.boundary}`, "", 'Content-Disposition: form-data; name="fileContent"', @@ -84,6 +84,8 @@ test.case("the boundary contains LuauFormBoundary", function() contentType = "text/plain", }, }) + local match = formData.boundary:match("LuauFormBoundary") - check.not_nil(match) + + expect.neq(match, nil) end) diff --git a/src/requests/getAssetDetailsAsync.luau b/src/requests/getAssetDetailsAsync.luau index ad57cec..8b7c68e 100644 --- a/src/requests/getAssetDetailsAsync.luau +++ b/src/requests/getAssetDetailsAsync.luau @@ -1,4 +1,4 @@ -local serde = require("@lune/serde") +local json = require("@std/json") local fetch = require("@root/requests/fetch") @@ -11,7 +11,11 @@ local function getAssetDetailsAsync(assetId: string, apiKey: string) }, }) - return serde.decode("json", res.body) + -- FIXME: Narrow the return value of `json.deserialize` instead of casting + -- to `any` + local assetDetails: any = json.deserialize(res.body) + + return assetDetails end return getAssetDetailsAsync diff --git a/src/requests/publishAssetAsync.luau b/src/requests/publishAssetAsync.luau index b9d14ad..dcce29b 100644 --- a/src/requests/publishAssetAsync.luau +++ b/src/requests/publishAssetAsync.luau @@ -1,5 +1,5 @@ -local fs = require("@lune/fs") -local serde = require("@lune/serde") +local fs = require("@lute/fs") +local json = require("@std/json") local createFormData = require("@root/requests/forms/createFormData") local fetch = require("@root/requests/fetch") @@ -34,6 +34,10 @@ local function publishAssetAsync( local assetId + local modelHandle = fs.open(asset.model) + local modelContent = fs.read(modelHandle) + fs.close(modelHandle) + if assetDetails and assetDetails.assetId then logging.info("publishing new version...") local formData = createFormData({ @@ -45,7 +49,7 @@ local function publishAssetAsync( }, fileContent = { filename = asset.model, - value = fs.readFile(asset.model), + value = modelContent, contentType = "model/x-rbxm", }, }) @@ -59,7 +63,9 @@ local function publishAssetAsync( body = formData.body, }) - local body = serde.decode("json", res.body) + -- FIXME: Narrow the return value of `json.deserialize` instead of casting + -- to `any` + local body: any = json.deserialize(res.body) local operationRes = waitForAssetOperationAsync(body.operationId, apiKey) @@ -83,7 +89,7 @@ local function publishAssetAsync( }, fileContent = { filename = asset.model, - value = fs.readFile(asset.model), + value = modelContent, contentType = "model/x-rbxm", }, }) @@ -97,7 +103,9 @@ local function publishAssetAsync( body = formData.body, }) - local body = serde.decode("json", res.body) + -- FIXME: Narrow the return value of `json.deserialize` instead of casting + -- to `any` + local body: any = json.deserialize(res.body) local operationRes = waitForAssetOperationAsync(body.operationId, apiKey) @@ -112,7 +120,7 @@ local function publishAssetAsync( description = asset.description, }) - local newLockfile = join(lockfile, { + local newLockfile: types.Lockfile = join(lockfile, { assets = join(lockfile.assets, { [assetName] = join(lockfile.assets[assetName], { assetId = assetId, diff --git a/src/requests/publishImagesAsync.luau b/src/requests/publishImagesAsync.luau index 0a84286..9533415 100644 --- a/src/requests/publishImagesAsync.luau +++ b/src/requests/publishImagesAsync.luau @@ -1,5 +1,5 @@ -local fs = require("@lune/fs") -local serde = require("@lune/serde") +local crypto = require("@lute/crypto") +local fs = require("@lute/fs") local createImageAsync = require("@root/requests/createImageAsync") local getOrCreateAssetLockfile = require("@root/manifest/getOrCreateAssetLockfile") @@ -16,6 +16,10 @@ type PendingImage = { assetId: string, } +local function toSha256String(str: string): string + return buffer.tostring(crypto.digest(crypto.hash.sha256, str)) +end + local function publishImagesAsync( projectPath: string, assetConfig: AssetConfig, @@ -30,10 +34,13 @@ local function publishImagesAsync( local icon = assetConfig.icon if icon then local iconPath = `{projectPath}/{icon}` + local iconHandle = fs.open(iconPath, "r") + local iconContent = fs.read(iconHandle) + fs.close(iconHandle) logging.info(`syncing package icon at {iconPath}`) - local iconHash = serde.hash("sha256", fs.readFile(iconPath)) + local iconHash = toSha256String(iconContent) if existingImages and existingImages[icon] and existingImages[icon].hash == iconHash then logging.debug(`hashes match for {iconPath}, skipping...`) @@ -51,11 +58,12 @@ local function publishImagesAsync( local assetId = if operation.response then operation.response.assetId else nil if assetId then - table.insert(pendingImages, { + local pendingImage: PendingImage = { path = iconPath, filename = icon, assetId = assetId, - }) + } + table.insert(pendingImages, pendingImage) logging.debug(`image uploaded successfully`) else @@ -70,7 +78,11 @@ local function publishImagesAsync( for index, thumbnailFilename in thumbnails do local thumbnailPath = `{projectPath}/{thumbnailFilename}` - local thumbnailHash = serde.hash("sha256", fs.readFile(thumbnailPath)) + local thumbnailHandle = fs.open(thumbnailPath, "r") + local thumbnailContent = fs.read(thumbnailHandle) + fs.close(thumbnailHandle) + + local thumbnailHash = toSha256String(thumbnailContent) local existingThumbnail = existingImages and existingImages[thumbnailFilename] if existingThumbnail and existingThumbnail.hash == thumbnailHash then @@ -92,11 +104,12 @@ local function publishImagesAsync( local assetId = operation.response.assetId if assetId then - table.insert(pendingImages, { + local pendingImage: PendingImage = { path = thumbnailPath, filename = thumbnailFilename, assetId = assetId, - }) + } + table.insert(pendingImages, pendingImage) logging.debug(`image uploaded successfully`) else @@ -107,13 +120,17 @@ local function publishImagesAsync( end if #pendingImages > 0 then - local newAssetLockfile = table.clone(assetLockfile or {}) + local newAssetLockfile = table.clone(assetLockfile or {} :: types.Lockfile) newAssetLockfile.images = newAssetLockfile.images or {} for _, pendingImage in pendingImages do + local pendingImageHandle = fs.open(pendingImage.path) + local pendingImageContent = fs.read(pendingImageHandle) + fs.close(pendingImageHandle) + newAssetLockfile.images[pendingImage.filename] = { assetId = pendingImage.assetId, - hash = serde.hash("sha256", fs.readFile(pendingImage.path)), + hash = toSha256String(pendingImageContent), } logging.debug( diff --git a/src/requests/publishPackageAsync.luau b/src/requests/publishPackageAsync.luau index 07f727c..50d9fae 100644 --- a/src/requests/publishPackageAsync.luau +++ b/src/requests/publishPackageAsync.luau @@ -1,4 +1,4 @@ -local process = require("@lune/process") +-- local process = require("@lute/process") local logging = require("@root/logging") local publishAssetAsync = require("@root/requests/publishAssetAsync") @@ -12,7 +12,8 @@ local function publishPackageAsync(projectPath: string, assetName: string, apiKe local asset = manifest.assets[assetName] if not asset then logging.err(`publishing failed, could not find an asset named {assetName} in the manifest`) - process.exit(1) + -- process.exit(1) + error(1) end local environment = manifest.environments[asset.environment] diff --git a/src/requests/setAssetDetailsAsync.luau b/src/requests/setAssetDetailsAsync.luau index d57aab8..2dc0225 100644 --- a/src/requests/setAssetDetailsAsync.luau +++ b/src/requests/setAssetDetailsAsync.luau @@ -1,4 +1,4 @@ -local serde = require("@lune/serde") +local json = require("@std/json") local createFormData = require("@root/requests/forms/createFormData") local fetch = require("@root/requests/fetch") @@ -33,7 +33,8 @@ local function setAssetDetailsAsync(assetId: string, apiKey: string, details: As body = formData.body, }) - local body: OperationResponse = serde.decode("json", res.body) + -- FIXME: Narrow the return value of `json.deserialize` instead of casting to `any` + local body: OperationResponse = json.deserialize(res.body) :: any if body.message then error(body.message) diff --git a/src/requests/setAssetIconAsync.luau b/src/requests/setAssetIconAsync.luau index 68152b2..6701eda 100644 --- a/src/requests/setAssetIconAsync.luau +++ b/src/requests/setAssetIconAsync.luau @@ -1,4 +1,4 @@ -local serde = require("@lune/serde") +local json = require("@std/json") local createFormData = require("@root/requests/forms/createFormData") local fetch = require("@root/requests/fetch") @@ -55,7 +55,9 @@ local function setAssetIconAsync(projectPath: string, assetName: string, assetCo body = formData.body, }) - local body = serde.decode("json", res.body) + -- FIXME: Narrow the return value of `json.deserialize` instead of casting + -- to `any` + local body: any = json.deserialize(res.body) if not body.operationId then logging.err(`failed to set asset icon`) diff --git a/src/requests/waitForAssetOperationAsync.luau b/src/requests/waitForAssetOperationAsync.luau index 47d09c7..bc1ab06 100644 --- a/src/requests/waitForAssetOperationAsync.luau +++ b/src/requests/waitForAssetOperationAsync.luau @@ -1,5 +1,5 @@ -local serde = require("@lune/serde") -local task = require("@lune/task") +local json = require("@std/json") +local task = require("@lute/task") local fetch = require("@root/requests/fetch") local types = require("@root/types") @@ -21,7 +21,9 @@ local function waitForAssetOperationAsync(operationId: string, apiKey: string): }, }) - local body = serde.decode("json", res.body) + -- FIXME: Narrow the return value of `json.deserialize` instead of + -- casting to `any` + local body: any = json.deserialize(res.body) if res.ok and body.done then return body diff --git a/src/types.luau b/src/types.luau index 5db4040..d91228d 100644 --- a/src/types.luau +++ b/src/types.luau @@ -1,4 +1,4 @@ -local t = require("@pkg/t") +local t: any = require("@pkg/t") local types = {} @@ -93,11 +93,11 @@ types.validateManifest = function(value: any): (boolean, string?) return success, message end - if typeof(value.assets) == "table" and countObject(value.assets) == 0 then + if typeof(value.assets) == "table" and countObject(value.assets :: { [any]: any }) == 0 then return false, "at least one asset must be defined in the manifest" end - if typeof(value.environments) == "table" and countObject(value.environments) == 0 then + if typeof(value.environments) == "table" and countObject(value.environments :: { [any]: any }) == 0 then return false, "at least one environment must be defined in the manifest" end @@ -175,4 +175,20 @@ export type OperationResponse = { }?, } & ErrorStatusResponse +export type LuauTaskResponse = { + binaryInput: string, + binaryOutputUri: string, + createTime: string, + enableBinaryOutput: boolean, + output: { + results: { any }, + }, + path: string, + script: string, + state: string, + updateTime: string, + user: string, + error: { [string]: any }?, +} + return types diff --git a/src/types.spec.luau b/src/types.spec.luau index 934a0b0..0b26228 100644 --- a/src/types.spec.luau +++ b/src/types.spec.luau @@ -1,6 +1,5 @@ -local frktest = require("@pkg/frktest") -local test = frktest.test -local check = frktest.assert.check +local expect = require("@std/test/assert") +local test = require("@std/test") local types = require("./types") @@ -24,7 +23,7 @@ test.suite("validateManifest", function() }, }) - check.is_true(success) + expect.eq(success, true) end) test.case("accepts optional manifest values", function() @@ -49,7 +48,7 @@ test.suite("validateManifest", function() }, }) - check.is_true(success) + expect.eq(success, true) end) test.case("denies empty assets object", function() @@ -65,8 +64,8 @@ test.suite("validateManifest", function() }, }) - check.is_false(success) - check.equal(message, "at least one asset must be defined in the manifest") + expect.eq(success, false) + expect.eq(message, "at least one asset must be defined in the manifest") end) test.case("denies empty environments object", function() @@ -84,8 +83,8 @@ test.suite("validateManifest", function() environments = {}, }) - check.is_false(success) - check.equal(message, "at least one environment must be defined in the manifest") + expect.eq(success, false) + expect.eq(message, "at least one environment must be defined in the manifest") end) test.case("assets must map back to a valid environment", function() @@ -110,8 +109,8 @@ test.suite("validateManifest", function() }, }) - check.is_false(success) - check.equal(message, `asset "main" attempts to use the environment "does-not-exist" which does not exist`) + expect.eq(success, false) + expect.eq(message, `asset "main" attempts to use the environment "does-not-exist" which does not exist`) end) test.case("denies optional manifest values of the wrong type", function() @@ -135,13 +134,13 @@ test.suite("validateManifest", function() }, }) - check.is_false(success) + expect.eq(success, false) end) test.case("denies empty object", function() local success = types.validateManifest({}) - check.is_false(success) + expect.eq(success, false) end) test.case("denies invalid types", function() @@ -163,7 +162,7 @@ test.suite("validateManifest", function() }, }) - check.is_false(success) + expect.eq(success, false) end) end) @@ -178,7 +177,7 @@ test.suite("validateLockfile", function() images = {}, }) - check.is_true(success) + expect.eq(success, true) end) test.case("handles storing images", function() @@ -196,12 +195,12 @@ test.suite("validateLockfile", function() }, }) - check.is_true(success) + expect.eq(success, true) end) test.case("denies empty object", function() local success = types.validateLockfile({}) - check.is_false(success) + expect.eq(success, false) end) end)