From f30b3b677d8f587d4657e5b829392a7d9bf8cd55 Mon Sep 17 00:00:00 2001 From: Mark Marks Date: Sun, 22 Sep 2024 13:18:54 +0200 Subject: [PATCH] fix: Releases, port to new scripts "+ Releases used the lib/ folder instead of a darklua processed dist/ folder, leading to them using unsupported string requires" -m "+ Port lune scripts to new, more awesome and strictly typed scripts" -m "+ Bump to and release v0.1.1" --- .github/workflows/release.yml | 4 + .lune/.lune-defs/datetime.luau | 408 ++++++++++++++++++++++++++ .lune/.lune-defs/fs.luau | 289 +++++++++++++++++++ .lune/.lune-defs/luau.luau | 123 ++++++++ .lune/.lune-defs/net.luau | 321 +++++++++++++++++++++ .lune/.lune-defs/process.luau | 182 ++++++++++++ .lune/.lune-defs/regex.luau | 218 ++++++++++++++ .lune/.lune-defs/roblox.luau | 507 +++++++++++++++++++++++++++++++++ .lune/.lune-defs/serde.luau | 200 +++++++++++++ .lune/.lune-defs/stdio.luau | 161 +++++++++++ .lune/.lune-defs/task.luau | 99 +++++++ .lune/analyze.luau | 16 +- .lune/build.luau | 18 +- .lune/check.luau | 20 +- .lune/dev.luau | 54 +++- .lune/dist.luau | 5 + .lune/install-packages.luau | 18 +- .lune/util/spawn.luau | 39 +++ .lune/util/watch.luau | 44 +++ .zed/settings.json | 3 + wally.toml | 6 +- 21 files changed, 2671 insertions(+), 64 deletions(-) create mode 100644 .lune/.lune-defs/datetime.luau create mode 100644 .lune/.lune-defs/fs.luau create mode 100644 .lune/.lune-defs/luau.luau create mode 100644 .lune/.lune-defs/net.luau create mode 100644 .lune/.lune-defs/process.luau create mode 100644 .lune/.lune-defs/regex.luau create mode 100644 .lune/.lune-defs/roblox.luau create mode 100644 .lune/.lune-defs/serde.luau create mode 100644 .lune/.lune-defs/stdio.luau create mode 100644 .lune/.lune-defs/task.luau create mode 100644 .lune/dist.luau create mode 100644 .lune/util/spawn.luau create mode 100644 .lune/util/watch.luau diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d33102..38a5657 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,10 @@ jobs: - name: Checkout Project uses: actions/checkout@v4 + - name: Prepare for Distribution + run: | + lune run dist + - name: Install Rokit uses: CompeyDev/setup-rokit@v0.1.2 diff --git a/.lune/.lune-defs/datetime.luau b/.lune/.lune-defs/datetime.luau new file mode 100644 index 0000000..8992dab --- /dev/null +++ b/.lune/.lune-defs/datetime.luau @@ -0,0 +1,408 @@ +--[[ + NOTE: We export a couple different DateTimeValues types below to ensure + that types are completely accurate, for method args milliseconds will + always be optional, but for return values millis are always included + + If we figure out some better strategy here where we can + export just a single type while maintaining accuracy we + can change to that in a future breaking semver release +]] + +type OptionalMillisecond = { + millisecond: number?, +} + +type Millisecond = { + millisecond: number, +} + +--[=[ + @interface Locale + @within DateTime + + Enum type representing supported DateTime locales. + + Currently supported locales are: + + - `en` - English + - `de` - German + - `es` - Spanish + - `fr` - French + - `it` - Italian + - `ja` - Japanese + - `pl` - Polish + - `pt-br` - Brazilian Portuguese + - `pt` - Portuguese + - `tr` - Turkish +]=] +export type Locale = "en" | "de" | "es" | "fr" | "it" | "ja" | "pl" | "pt-br" | "pt" | "tr" + +--[=[ + @interface DateTimeValues + @within DateTime + + Individual date & time values, representing the primitives that make up a `DateTime`. + + This is a dictionary that will contain the following values: + + - `year` - Year(s), in the range 1400 -> 9999 + - `month` - Month(s), in the range 1 -> 12 + - `day` - Day(s), in the range 1 -> 31 + - `hour` - Hour(s), in the range 0 -> 23 + - `minute` - Minute(s), in the range 0 -> 59 + - `second` - Second(s), in the range 0 -> 60, where 60 is a leap second + + An additional `millisecond` value may also be included, + and should be within the range `0 -> 999`, but is optional. + + However, any method returning this type should be guaranteed + to include milliseconds - see individual methods to verify. +]=] +export type DateTimeValues = { + year: number, + month: number, + day: number, + hour: number, + minute: number, + second: number, +} + +--[=[ + @interface DateTimeValueArguments + @within DateTime + + Alias for `DateTimeValues` with an optional `millisecond` value. + + Refer to the `DateTimeValues` documentation for additional information. +]=] +export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond + +--[=[ + @interface DateTimeValueReturns + @within DateTime + + Alias for `DateTimeValues` with a mandatory `millisecond` value. + + Refer to the `DateTimeValues` documentation for additional information. +]=] +export type DateTimeValueReturns = DateTimeValues & Millisecond + +local DateTime = { + --- Number of seconds passed since the UNIX epoch. + unixTimestamp = (nil :: any) :: number, + --- Number of milliseconds passed since the UNIX epoch. + unixTimestampMillis = (nil :: any) :: number, +} + +--[=[ + @within DateTime + @tag Method + + Formats this `DateTime` using the given `formatString` and `locale`, as local time. + + The given `formatString` is parsed using a `strftime`/`strptime`-inspired + date and time formatting syntax, allowing tokens such as the following: + + | Token | Example | Description | + |-------|----------|---------------| + | `%Y` | `1998` | Year number | + | `%m` | `04` | Month number | + | `%d` | `29` | Day number | + | `%A` | `Monday` | Weekday name | + | `%M` | `59` | Minute number | + | `%S` | `10` | Second number | + + For a full reference of all available tokens, see the + [chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). + + If not provided, `formatString` and `locale` will default + to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively. + + @param formatString -- A string containing formatting tokens + @param locale -- The locale the time should be formatted in + @return string -- The formatting string +]=] +function DateTime.formatLocalTime(self: DateTime, formatString: string?, locale: Locale?): string + return nil :: any +end + +--[=[ + @within DateTime + @tag Method + + Formats this `DateTime` using the given `formatString` and `locale`, as UTC (universal) time. + + The given `formatString` is parsed using a `strftime`/`strptime`-inspired + date and time formatting syntax, allowing tokens such as the following: + + | Token | Example | Description | + |-------|----------|---------------| + | `%Y` | `1998` | Year number | + | `%m` | `04` | Month number | + | `%d` | `29` | Day number | + | `%A` | `Monday` | Weekday name | + | `%M` | `59` | Minute number | + | `%S` | `10` | Second number | + + For a full reference of all available tokens, see the + [chrono documentation](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). + + If not provided, `formatString` and `locale` will default + to `"%Y-%m-%d %H:%M:%S"` and `"en"` (english) respectively. + + @param formatString -- A string containing formatting tokens + @param locale -- The locale the time should be formatted in + @return string -- The formatting string +]=] +function DateTime.formatUniversalTime( + self: DateTime, + formatString: string?, + locale: Locale? +): string + return nil :: any +end + +--[=[ + @within DateTime + @tag Method + + Formats this `DateTime` as an ISO 8601 date-time string. + + Some examples of ISO 8601 date-time strings are: + + - `2020-02-22T18:12:08Z` + - `2000-01-31T12:34:56+05:00` + - `1970-01-01T00:00:00.055Z` + + @return string -- The ISO 8601 formatted string +]=] +function DateTime.toIsoDate(self: DateTime): string + return nil :: any +end + +--[=[ + @within DateTime + @tag Method + + Extracts separated local date & time values from this `DateTime`. + + The returned table contains the following values: + + | Key | Type | Range | + |---------------|----------|----------------| + | `year` | `number` | `1400 -> 9999` | + | `month` | `number` | `1 -> 12` | + | `day` | `number` | `1 -> 31` | + | `hour` | `number` | `0 -> 23` | + | `minute` | `number` | `0 -> 59` | + | `second` | `number` | `0 -> 60` | + | `millisecond` | `number` | `0 -> 999` | + + @return DateTimeValueReturns -- A table of DateTime values +]=] +function DateTime.toLocalTime(self: DateTime): DateTimeValueReturns + return nil :: any +end + +--[=[ + @within DateTime + @tag Method + + Extracts separated UTC (universal) date & time values from this `DateTime`. + + The returned table contains the following values: + + | Key | Type | Range | + |---------------|----------|----------------| + | `year` | `number` | `1400 -> 9999` | + | `month` | `number` | `1 -> 12` | + | `day` | `number` | `1 -> 31` | + | `hour` | `number` | `0 -> 23` | + | `minute` | `number` | `0 -> 59` | + | `second` | `number` | `0 -> 60` | + | `millisecond` | `number` | `0 -> 999` | + + @return DateTimeValueReturns -- A table of DateTime values +]=] +function DateTime.toUniversalTime(self: DateTime): DateTimeValueReturns + return nil :: any +end + +export type DateTime = typeof(DateTime) + +--[=[ + @class DateTime + + Built-in library for date & time + + ### Example usage + + ```lua + local DateTime = require("@lune/datetime") + + -- Creates a DateTime for the current exact moment in time + local now = DateTime.now() + + -- Formats the current moment in time as an ISO 8601 string + print(now:toIsoDate()) + + -- Formats the current moment in time, using the local + -- time, the French locale, and the specified time string + print(now:formatLocalTime("%A, %d %B %Y", "fr")) + + -- Returns a specific moment in time as a DateTime instance + local someDayInTheFuture = DateTime.fromLocalTime({ + year = 3033, + month = 8, + day = 26, + hour = 16, + minute = 56, + second = 28, + millisecond = 892, + }) + + -- Extracts the current local date & time as separate values (same values as above table) + print(now:toLocalTime()) + + -- Returns a DateTime instance from a given float, where the whole + -- denotes the seconds and the fraction denotes the milliseconds + -- Note that the fraction for millis here is completely optional + DateTime.fromUnixTimestamp(871978212313.321) + + -- Extracts the current universal (UTC) date & time as separate values + print(now:toUniversalTime()) + ``` +]=] +local dateTime = {} + +--[=[ + @within DateTime + @tag Constructor + + Returns a `DateTime` representing the current moment in time. + + @return DateTime -- The new DateTime object +]=] +function dateTime.now(): DateTime + return nil :: any +end + +--[=[ + @within DateTime + @tag Constructor + + Creates a new `DateTime` from the given UNIX timestamp. + + This timestamp may contain both a whole and fractional part - + where the fractional part denotes milliseconds / nanoseconds. + + Example usage of fractions: + + - `DateTime.fromUnixTimestamp(123456789.001)` - one millisecond + - `DateTime.fromUnixTimestamp(123456789.000000001)` - one nanosecond + + Note that the fractional part has limited precision down to exactly + one nanosecond, any fraction that is more precise will get truncated. + + @param unixTimestamp -- Seconds passed since the UNIX epoch + @return DateTime -- The new DateTime object +]=] +function dateTime.fromUnixTimestamp(unixTimestamp: number): DateTime + return nil :: any +end + +--[=[ + @within DateTime + @tag Constructor + + Creates a new `DateTime` from the given date & time values table, in universal (UTC) time. + + The given table must contain the following values: + + | Key | Type | Range | + |----------|----------|----------------| + | `year` | `number` | `1400 -> 9999` | + | `month` | `number` | `1 -> 12` | + | `day` | `number` | `1 -> 31` | + | `hour` | `number` | `0 -> 23` | + | `minute` | `number` | `0 -> 59` | + | `second` | `number` | `0 -> 60` | + + An additional `millisecond` value may also be included, + and should be within the range `0 -> 999`, but is optional. + + Any non-integer values in the given table will be rounded down. + + ### Errors + + This constructor is fallible and may throw an error in the following situations: + + - Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year. + + @param values -- Table containing date & time values + @return DateTime -- The new DateTime object +]=] +function dateTime.fromUniversalTime(values: DateTimeValueArguments): DateTime + return nil :: any +end + +--[=[ + @within DateTime + @tag Constructor + + Creates a new `DateTime` from the given date & time values table, in local time. + + The given table must contain the following values: + + | Key | Type | Range | + |----------|----------|----------------| + | `year` | `number` | `1400 -> 9999` | + | `month` | `number` | `1 -> 12` | + | `day` | `number` | `1 -> 31` | + | `hour` | `number` | `0 -> 23` | + | `minute` | `number` | `0 -> 59` | + | `second` | `number` | `0 -> 60` | + + An additional `millisecond` value may also be included, + and should be within the range `0 -> 999`, but is optional. + + Any non-integer values in the given table will be rounded down. + + ### Errors + + This constructor is fallible and may throw an error in the following situations: + + - Date units (year, month, day) were given that produce an invalid date. For example, January 32nd or February 29th on a non-leap year. + + @param values -- Table containing date & time values + @return DateTime -- The new DateTime object +]=] +function dateTime.fromLocalTime(values: DateTimeValueArguments): DateTime + return nil :: any +end + +--[=[ + @within DateTime + @tag Constructor + + Creates a new `DateTime` from an ISO 8601 date-time string. + + ### Errors + + This constructor is fallible and may throw an error if the given + string does not strictly follow the ISO 8601 date-time string format. + + Some examples of valid ISO 8601 date-time strings are: + + - `2020-02-22T18:12:08Z` + - `2000-01-31T12:34:56+05:00` + - `1970-01-01T00:00:00.055Z` + + @param isoDate -- An ISO 8601 formatted string + @return DateTime -- The new DateTime object +]=] +function dateTime.fromIsoDate(isoDate: string): DateTime + return nil :: any +end + +return dateTime diff --git a/.lune/.lune-defs/fs.luau b/.lune/.lune-defs/fs.luau new file mode 100644 index 0000000..823f6f7 --- /dev/null +++ b/.lune/.lune-defs/fs.luau @@ -0,0 +1,289 @@ +local DateTime = require("./datetime") +type DateTime = DateTime.DateTime + +export type MetadataKind = "file" | "dir" | "symlink" + +--[=[ + @interface MetadataPermissions + @within FS + + Permissions for the given file or directory. + + This is a dictionary that will contain the following values: + + * `readOnly` - If the target path is read-only or not +]=] +export type MetadataPermissions = { + readOnly: boolean, +} + +-- FIXME: We lose doc comments here below in Metadata because of the union type + +--[=[ + @interface Metadata + @within FS + + Metadata for the given file or directory. + + This is a dictionary that will contain the following values: + + * `kind` - If the target path is a `file`, `dir` or `symlink` + * `exists` - If the target path exists + * `createdAt` - The timestamp represented as a `DateTime` object at which the file or directory was created + * `modifiedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last modified + * `accessedAt` - The timestamp represented as a `DateTime` object at which the file or directory was last accessed + * `permissions` - Current permissions for the file or directory + + Note that timestamps are relative to the unix epoch, and + may not be accurate if the system clock is not accurate. +]=] +export type Metadata = { + kind: MetadataKind, + exists: true, + createdAt: DateTime, + modifiedAt: DateTime, + accessedAt: DateTime, + permissions: MetadataPermissions, +} | { + kind: nil, + exists: false, + createdAt: nil, + modifiedAt: nil, + accessedAt: nil, + permissions: nil, +} + +--[=[ + @interface WriteOptions + @within FS + + Options for filesystem APIs what write to files and/or directories. + + This is a dictionary that may contain one or more of the following values: + + * `overwrite` - If the target path should be overwritten or not, in the case that it already exists +]=] +export type WriteOptions = { + overwrite: boolean?, +} + +--[=[ + @class FS + + Built-in library for filesystem access + + ### Example usage + + ```lua + local fs = require("@lune/fs") + + -- Reading a file + local myTextFile: string = fs.readFile("myFileName.txt") + + -- Reading entries (files & dirs) in a directory + for _, entryName in fs.readDir("myDirName") do + if fs.isFile("myDirName/" .. entryName) then + print("Found file " .. entryName) + elseif fs.isDir("myDirName/" .. entryName) then + print("Found subdirectory " .. entryName) + end + end + ``` +]=] +local fs = {} + +--[=[ + @within FS + @tag must_use + + Reads a file at `path`. + + An error will be thrown in the following situations: + + * `path` does not point to an existing file. + * The current process lacks permissions to read the file. + * Some other I/O error occurred. + + @param path The path to the file to read + @return The contents of the file +]=] +function fs.readFile(path: string): string + return nil :: any +end + +--[=[ + @within FS + @tag must_use + + Reads entries in a directory at `path`. + + An error will be thrown in the following situations: + + * `path` does not point to an existing directory. + * The current process lacks permissions to read the contents of the directory. + * Some other I/O error occurred. + + @param path The directory path to search in + @return A list of files & directories found +]=] +function fs.readDir(path: string): { string } + return {} +end + +--[=[ + @within FS + + Writes to a file at `path`. + + An error will be thrown in the following situations: + + * The file's parent directory does not exist. + * The current process lacks permissions to write to the file. + * Some other I/O error occurred. + + @param path The path of the file + @param contents The contents of the file +]=] +function fs.writeFile(path: string, contents: buffer | string) end + +--[=[ + @within FS + + Creates a directory and its parent directories if they are missing. + + An error will be thrown in the following situations: + + * `path` already points to an existing file or directory. + * The current process lacks permissions to create the directory or its missing parents. + * Some other I/O error occurred. + + @param path The directory to create +]=] +function fs.writeDir(path: string) end + +--[=[ + @within FS + + Removes a file. + + An error will be thrown in the following situations: + + * `path` does not point to an existing file. + * The current process lacks permissions to remove the file. + * Some other I/O error occurred. + + @param path The file to remove +]=] +function fs.removeFile(path: string) end + +--[=[ + @within FS + + Removes a directory and all of its contents. + + An error will be thrown in the following situations: + + * `path` is not an existing and empty directory. + * The current process lacks permissions to remove the directory. + * Some other I/O error occurred. + + @param path The directory to remove +]=] +function fs.removeDir(path: string) end + +--[=[ + @within FS + @tag must_use + + Gets metadata for the given path. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `path`. + * Some other I/O error occurred. + + @param path The path to get metadata for + @return Metadata for the path +]=] +function fs.metadata(path: string): Metadata + return nil :: any +end + +--[=[ + @within FS + @tag must_use + + Checks if a given path is a file. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `path`. + * Some other I/O error occurred. + + @param path The file path to check + @return If the path is a file or not +]=] +function fs.isFile(path: string): boolean + return nil :: any +end + +--[=[ + @within FS + @tag must_use + + Checks if a given path is a directory. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `path`. + * Some other I/O error occurred. + + @param path The directory path to check + @return If the path is a directory or not +]=] +function fs.isDir(path: string): boolean + return nil :: any +end + +--[=[ + @within FS + + Moves a file or directory to a new path. + + Throws an error if a file or directory already exists at the target path. + This can be bypassed by passing `true` as the third argument, or a dictionary of options. + Refer to the documentation for `WriteOptions` for specific option keys and their values. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `from` or write at `to`. + * The new path exists on a different mount point. + * Some other I/O error occurred. + + @param from The path to move from + @param to The path to move to + @param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists +]=] +function fs.move(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end + +--[=[ + @within FS + + Copies a file or directory recursively to a new path. + + Throws an error if a file or directory already exists at the target path. + This can be bypassed by passing `true` as the third argument, or a dictionary of options. + Refer to the documentation for `WriteOptions` for specific option keys and their values. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `from` or write at `to`. + * Some other I/O error occurred. + + @param from The path to copy from + @param to The path to copy to + @param overwriteOrOptions Options for the target path, such as if should be overwritten if it already exists +]=] +function fs.copy(from: string, to: string, overwriteOrOptions: (boolean | WriteOptions)?) end + +return fs diff --git a/.lune/.lune-defs/luau.luau b/.lune/.lune-defs/luau.luau new file mode 100644 index 0000000..ab40c4f --- /dev/null +++ b/.lune/.lune-defs/luau.luau @@ -0,0 +1,123 @@ +--[=[ + @interface CompileOptions + @within Luau + + The options passed to the luau compiler while compiling bytecode. + + This is a dictionary that may contain one or more of the following values: + + * `optimizationLevel` - Sets the compiler option "optimizationLevel". Defaults to `1`. + * `coverageLevel` - Sets the compiler option "coverageLevel". Defaults to `0`. + * `debugLevel` - Sets the compiler option "debugLevel". Defaults to `1`. + + Documentation regarding what these values represent can be found [here](https://github.com/Roblox/luau/blob/bd229816c0a82a8590395416c81c333087f541fd/Compiler/include/luacode.h#L13-L39). +]=] +export type CompileOptions = { + optimizationLevel: number?, + coverageLevel: number?, + debugLevel: number?, +} + +--[=[ + @interface LoadOptions + @within Luau + + The options passed while loading a luau chunk from an arbitrary string, or bytecode. + + This is a dictionary that may contain one or more of the following values: + + * `debugName` - The debug name of the closure. Defaults to `luau.load(...)`. + * `environment` - A custom environment to load the chunk in. Setting a custom environment will deoptimize the chunk and forcefully disable codegen. Defaults to the global environment. + * `injectGlobals` - Whether or not to inject globals in the custom environment. Has no effect if no custom environment is provided. Defaults to `true`. + * `codegenEnabled` - Whether or not to enable codegen. Defaults to `false`. +]=] +export type LoadOptions = { + debugName: string?, + environment: { [string]: any }?, + injectGlobals: boolean?, + codegenEnabled: boolean?, +} + +--[=[ + @class Luau + + Built-in library for generating luau bytecode & functions. + + ### Example usage + + ```lua + local luau = require("@lune/luau") + + local bytecode = luau.compile("print('Hello, World!')") + local callableFn = luau.load(bytecode) + + -- Additionally, we can skip the bytecode generation and load a callable function directly from the code itself. + -- local callableFn = luau.load("print('Hello, World!')") + + callableFn() + ``` + + Since luau bytecode is highly compressible, it may also make sense to compress it using the `serde` library + while transmitting large amounts of it. +]=] +local luau = {} + +--[=[ + @within Luau + + Compiles sourcecode into Luau bytecode + + An error will be thrown if the sourcecode given isn't valid Luau code. + + ### Example usage + + ```lua + local luau = require("@lune/luau") + + -- Compile the source to some highly optimized bytecode + local bytecode = luau.compile("print('Hello, World!')", { + optimizationLevel = 2, + coverageLevel = 0, + debugLevel = 1, + }) + ``` + + @param source The string that will be compiled into bytecode + @param compileOptions The options passed to the luau compiler that will output the bytecode + + @return luau bytecode +]=] +function luau.compile(source: string, compileOptions: CompileOptions?): string + return nil :: any +end + +--[=[ + @within Luau + + Generates a function from either bytecode or sourcecode + + An error will be thrown if the sourcecode given isn't valid luau code. + + ### Example usage + + ```lua + local luau = require("@lune/luau") + + local bytecode = luau.compile("print('Hello, World!')") + local callableFn = luau.load(bytecode, { + debugName = "'Hello, World'" + }) + + callableFn() + ``` + + @param source Either luau bytecode or string source code + @param loadOptions The options passed to luau for loading the chunk + + @return luau chunk +]=] +function luau.load(source: string, loadOptions: LoadOptions?): (...any) -> ...any + return nil :: any +end + +return luau diff --git a/.lune/.lune-defs/net.luau b/.lune/.lune-defs/net.luau new file mode 100644 index 0000000..e9b793e --- /dev/null +++ b/.lune/.lune-defs/net.luau @@ -0,0 +1,321 @@ +export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH" + +type HttpQueryOrHeaderMap = { [string]: string | { string } } +export type HttpQueryMap = HttpQueryOrHeaderMap +export type HttpHeaderMap = HttpQueryOrHeaderMap + +--[=[ + @interface FetchParamsOptions + @within Net + + Extra options for `FetchParams`. + + This is a dictionary that may contain one or more of the following values: + + * `decompress` - If the request body should be automatically decompressed when possible. Defaults to `true` +]=] +export type FetchParamsOptions = { + decompress: boolean?, +} + +--[=[ + @interface FetchParams + @within Net + + Parameters for sending network requests with `net.request`. + + This is a dictionary that may contain one or more of the following values: + + * `url` - The URL to send a request to. This is always required + * `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Defaults to `"GET"` + * `body` - The request body + * `query` - A table of key-value pairs representing query parameters in the request path + * `headers` - A table of key-value pairs representing headers + * `options` - Extra options for things such as automatic decompression of response bodies +]=] +export type FetchParams = { + url: string, + method: HttpMethod?, + body: (string | buffer)?, + query: HttpQueryMap?, + headers: HttpHeaderMap?, + options: FetchParamsOptions?, +} + +--[=[ + @interface FetchResponse + @within Net + + Response type for sending network requests with `net.request`. + + This is a dictionary containing the following values: + + * `ok` - If the status code is a canonical success status code, meaning within the range 200 -> 299 + * `statusCode` - The status code returned for the request + * `statusMessage` - The canonical status message for the returned status code, such as `"Not Found"` for status code 404 + * `headers` - A table of key-value pairs representing headers + * `body` - The request body, or an empty string if one was not given +]=] +export type FetchResponse = { + ok: boolean, + statusCode: number, + statusMessage: string, + headers: HttpHeaderMap, + body: string, +} + +--[=[ + @interface ServeRequest + @within Net + + Data type for requests in `net.serve`. + + This is a dictionary containing the following values: + + * `path` - The path being requested, relative to the root. Will be `/` if not specified + * `query` - A table of key-value pairs representing query parameters in the request path + * `method` - The HTTP method verb, such as `"GET"`, `"POST"`, `"PATCH"`, `"PUT"`, or `"DELETE"`. Will always be uppercase + * `headers` - A table of key-value pairs representing headers + * `body` - The request body, or an empty string if one was not given +]=] +export type ServeRequest = { + path: string, + query: { [string]: string? }, + method: HttpMethod, + headers: { [string]: string }, + body: string, +} + +--[=[ + @interface ServeResponse + @within Net + + Response type for requests in `net.serve`. + + This is a dictionary that may contain one or more of the following values: + + * `status` - The status code for the request, in the range `100` -> `599` + * `headers` - A table of key-value pairs representing headers + * `body` - The response body +]=] +export type ServeResponse = { + status: number?, + headers: { [string]: string }?, + body: (string | buffer)?, +} + +type ServeHttpHandler = (request: ServeRequest) -> string | ServeResponse +type ServeWebSocketHandler = (socket: WebSocket) -> () + +--[=[ + @interface ServeConfig + @within Net + + Configuration for `net.serve`. + + This may contain one of or more of the following values: + + * `address` for setting the IP address to serve from. Defaults to the loopback interface (`http://localhost`). + * `handleRequest` for handling normal http requests, equivalent to just passing a function to `net.serve` + * `handleWebSocket` for handling web socket requests, which will receive a `WebSocket` object as its first and only parameter + + When setting `address`, the `handleRequest` callback must also be defined. + + ```lua + net.serve(8080, { + address = "http://0.0.0.0", + handleRequest = function(request) + return { + status = 200, + body = "Echo:\n" .. request.body, + } + end + }) + ``` +]=] +export type ServeConfig = { + address: string?, + handleRequest: ServeHttpHandler?, + handleWebSocket: ServeWebSocketHandler?, +} + +--[=[ + @interface ServeHandle + @within Net + + A handle to a currently running web server, containing a single `stop` function to gracefully shut down the web server. +]=] +export type ServeHandle = { + stop: () -> (), +} + +--[=[ + @interface WebSocket + @within Net + + A reference to a web socket connection. + + The web socket may be in either an "open" or a "closed" state, changing its current behavior. + + When open: + + * Any function on the socket such as `send`, `next` or `close` can be called without erroring + * `next` can be called to yield until the next message is received or the socket becomes closed + + When closed: + + * `next` will no longer return any message(s) and instead instantly return nil + * `send` will throw an error stating that the socket has been closed + + Once the websocket has been closed, `closeCode` will no longer be nil, and will be populated with a close + code according to the [WebSocket specification](https://www.iana.org/assignments/websocket/websocket.xhtml). + This will be an integer between 1000 and 4999, where 1000 is the canonical code for normal, error-free closure. +]=] +export type WebSocket = { + closeCode: number?, + close: (code: number?) -> (), + send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (), + next: () -> string?, +} + +--[=[ + @class Net + + + Built-in library for network access + + ### Example usage + + ```lua + local net = require("@lune/net") + + -- Sending a web request + local response = net.request("https://www.google.com") + print(response.ok) + print(response.statusCode, response.statusMessage) + print(response.headers) + + -- Using a JSON web API + local response = net.request({ + url = "https://dummyjson.com/products/add", + method = "POST", + headers = { ["Content-Type"] = "application/json" }, + body = net.jsonEncode({ + title = "Cool Pencil", + }) + }) + local product = net.jsonDecode(response.body) + print(product.id, "-", product.title) + + -- Starting up a webserver + net.serve(8080, function(request) + return { + status = 200, + body = "Echo:\n" .. request.body, + } + end) + ``` +]=] +local net = {} + +--[=[ + @within Net + + Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received. + + Only throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes. + + @param config The URL or request config to use + @return A dictionary representing the response for the request +]=] +function net.request(config: string | FetchParams): FetchResponse + return nil :: any +end + +--[=[ + @within Net + @tag must_use + + Connects to a web socket at the given URL. + + Throws an error if the server at the given URL does not support + web sockets, or if a miscellaneous network or I/O error occurs. + + @param url The URL to connect to + @return A web socket handle +]=] +function net.socket(url: string): WebSocket + return nil :: any +end + +--[=[ + @within Net + + Creates an HTTP server that listens on the given `port`. + + This will ***not*** block and will keep listening for requests on the given `port` + until the `stop` function on the returned `ServeHandle` has been called. + + @param port The port to use for the server + @param handlerOrConfig The handler function or config to use for the server +]=] +function net.serve(port: number, handlerOrConfig: ServeHttpHandler | ServeConfig): ServeHandle + return nil :: any +end + +--[=[ + @within Net + @tag must_use + + Encodes the given value as JSON. + + @param value The value to encode as JSON + @param pretty If the encoded JSON string should include newlines and spaces. Defaults to false + @return The encoded JSON string +]=] +function net.jsonEncode(value: any, pretty: boolean?): string + return nil :: any +end + +--[=[ + @within Net + @tag must_use + + Decodes the given JSON string into a lua value. + + @param encoded The JSON string to decode + @return The decoded lua value +]=] +function net.jsonDecode(encoded: string): any + return nil :: any +end + +--[=[ + @within Net + @tag must_use + + Encodes the given string using URL encoding. + + @param s The string to encode + @param binary If the string should be treated as binary data and/or is not valid utf-8. Defaults to false + @return The encoded string +]=] +function net.urlEncode(s: string, binary: boolean?): string + return nil :: any +end + +--[=[ + @within Net + @tag must_use + + Decodes the given string using URL decoding. + + @param s The string to decode + @param binary If the string should be treated as binary data and/or is not valid utf-8. Defaults to false + @return The decoded string +]=] +function net.urlDecode(s: string, binary: boolean?): string + return nil :: any +end + +return net diff --git a/.lune/.lune-defs/process.luau b/.lune/.lune-defs/process.luau new file mode 100644 index 0000000..7b82052 --- /dev/null +++ b/.lune/.lune-defs/process.luau @@ -0,0 +1,182 @@ +export type OS = "linux" | "macos" | "windows" +export type Arch = "x86_64" | "aarch64" + +export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none" +export type SpawnOptionsStdio = { + stdout: SpawnOptionsStdioKind?, + stderr: SpawnOptionsStdioKind?, + stdin: string?, +} + +--[=[ + @interface SpawnOptions + @within Process + + A dictionary of options for `process.spawn`, with the following available values: + + * `cwd` - The current working directory for the process + * `env` - Extra environment variables to give to the process + * `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell + * `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info + * `stdin` - Optional standard input to pass to spawned child process +]=] +export type SpawnOptions = { + cwd: string?, + env: { [string]: string }?, + shell: (boolean | string)?, + stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?, + stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change +} + +--[=[ + @interface SpawnResult + @within Process + + Result type for child processes in `process.spawn`. + + This is a dictionary containing the following values: + + * `ok` - If the child process exited successfully or not, meaning the exit code was zero or not set + * `code` - The exit code set by the child process, or 0 if one was not set + * `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written + * `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written +]=] +export type SpawnResult = { + ok: boolean, + code: number, + stdout: string, + stderr: string, +} + +--[=[ + @class Process + + Built-in functions for the current process & child processes + + ### Example usage + + ```lua + local process = require("@lune/process") + + -- Getting the arguments passed to the Lune script + for index, arg in process.args do + print("Process argument #" .. tostring(index) .. ": " .. arg) + end + + -- Getting the currently available environment variables + local PORT: string? = process.env.PORT + local HOME: string? = process.env.HOME + for name, value in process.env do + print("Environment variable " .. name .. " is set to " .. value) + end + + -- Getting the current os and processor architecture + print("Running " .. process.os .. " on " .. process.arch .. "!") + + -- Spawning a child process + local result = process.spawn("program", { + "cli argument", + "other cli argument" + }) + if result.ok then + print(result.stdout) + else + print(result.stderr) + end + ``` +]=] +local process = {} + +--[=[ + @within Process + @prop os OS + @tag read_only + + The current operating system being used. + + Possible values: + + * `"linux"` + * `"macos"` + * `"windows"` +]=] +process.os = (nil :: any) :: OS + +--[=[ + @within Process + @prop arch Arch + @tag read_only + + The architecture of the processor currently being used. + + Possible values: + + * `"x86_64"` + * `"aarch64"` +]=] +process.arch = (nil :: any) :: Arch + +--[=[ + @within Process + @prop args { string } + @tag read_only + + The arguments given when running the Lune script. +]=] +process.args = (nil :: any) :: { string } + +--[=[ + @within Process + @prop cwd string + @tag read_only + + The current working directory in which the Lune script is running. +]=] +process.cwd = (nil :: any) :: string + +--[=[ + @within Process + @prop env { [string]: string? } + @tag read_write + + Current environment variables for this process. + + Setting a value on this table will set the corresponding environment variable. +]=] +process.env = (nil :: any) :: { [string]: string? } + +--[=[ + @within Process + + Exits the currently running script as soon as possible with the given exit code. + + Exit code 0 is treated as a successful exit, any other value is treated as an error. + + Setting the exit code using this function will override any otherwise automatic exit code. + + @param code The exit code to set +]=] +function process.exit(code: number?): never + return nil :: any +end + +--[=[ + @within Process + + Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process. + + The second argument, `params`, can be passed as a list of string parameters to give to the program. + + The third argument, `options`, can be passed as a dictionary of options to give to the child process. + Refer to the documentation for `SpawnOptions` for specific option keys and their values. + + @param program The program to spawn as a child process + @param params Additional parameters to pass to the program + @param options A dictionary of options for the child process + @return A dictionary representing the result of the child process +]=] +function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult + return nil :: any +end + +return process diff --git a/.lune/.lune-defs/regex.luau b/.lune/.lune-defs/regex.luau new file mode 100644 index 0000000..59756f3 --- /dev/null +++ b/.lune/.lune-defs/regex.luau @@ -0,0 +1,218 @@ +--[=[ + @class RegexMatch + + A match from a regular expression. + + Contains the following values: + + - `start` -- The start index of the match in the original string. + - `finish` -- The end index of the match in the original string. + - `text` -- The text that was matched. + - `len` -- The length of the text that was matched. +]=] +local RegexMatch = { + start = 0, + finish = 0, + text = "", + len = 0, +} + +type RegexMatch = typeof(RegexMatch) + +--[=[ + @class RegexCaptures + + Captures from a regular expression. +]=] +local RegexCaptures = {} + +--[=[ + @within RegexCaptures + @tag Method + + Returns the match at the given index, if one exists. + + @param index -- The index of the match to get + @return RegexMatch -- The match, if one exists +]=] +function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch? + return nil :: any +end + +--[=[ + @within RegexCaptures + @tag Method + + Returns the match for the given named match group, if one exists. + + @param group -- The name of the group to get + @return RegexMatch -- The match, if one exists +]=] +function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch? + return nil :: any +end + +--[=[ + @within RegexCaptures + @tag Method + + Formats the captures using the given format string. + + ### Example usage + + ```lua + local regex = require("@lune/regex") + + local re = regex.new("(?[0-9]{2})-(?[0-9]{2})-(?[0-9]{4})") + + local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb."); + assert(caps ~= nil, "Example pattern should match example text") + + local formatted = caps:format("year=$year, month=$month, day=$day") + print(formatted) -- "year=2010, month=03, day=14" + ``` + + @param format -- The format string to use + @return string -- The formatted string +]=] +function RegexCaptures.format(self: RegexCaptures, format: string): string + return nil :: any +end + +export type RegexCaptures = typeof(RegexCaptures) + +local Regex = {} + +--[=[ + @within Regex + @tag Method + + Check if the given text matches the regular expression. + + This method may be slightly more efficient than calling `find` + if you only need to know if the text matches the pattern. + + @param text -- The text to search + @return boolean -- Whether the text matches the pattern +]=] +function Regex.isMatch(self: Regex, text: string): boolean + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Finds the first match in the given text. + + Returns `nil` if no match was found. + + @param text -- The text to search + @return RegexMatch? -- The match object +]=] +function Regex.find(self: Regex, text: string): RegexMatch? + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Finds all matches in the given text as a `RegexCaptures` object. + + Returns `nil` if no matches are found. + + @param text -- The text to search + @return RegexCaptures? -- The captures object +]=] +function Regex.captures(self: Regex, text: string): RegexCaptures? + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Splits the given text using the regular expression. + + @param text -- The text to split + @return { string } -- The split text +]=] +function Regex.split(self: Regex, text: string): { string } + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Replaces the first match in the given text with the given replacer string. + + @param haystack -- The text to search + @param replacer -- The string to replace matches with + @return string -- The text with the first match replaced +]=] +function Regex.replace(self: Regex, haystack: string, replacer: string): string + return nil :: any +end + +--[=[ + @within Regex + @tag Method + + Replaces all matches in the given text with the given replacer string. + + @param haystack -- The text to search + @param replacer -- The string to replace matches with + @return string -- The text with all matches replaced +]=] +function Regex.replaceAll(self: Regex, haystack: string, replacer: string): string + return nil :: any +end + +export type Regex = typeof(Regex) + +--[=[ + @class Regex + + Built-in library for regular expressions + + ### Example usage + + ```lua + local Regex = require("@lune/regex") + + local re = Regex.new("hello") + + if re:isMatch("hello, world!") then + print("Matched!") + end + + local caps = re:captures("hello, world! hello, again!") + + print(#caps) -- 2 + print(caps:get(1)) -- "hello" + print(caps:get(2)) -- "hello" + print(caps:get(3)) -- nil + ``` +]=] +local regex = {} + +--[=[ + @within Regex + @tag Constructor + + Creates a new `Regex` from a given string pattern. + + ### Errors + + This constructor throws an error if the given pattern is invalid. + + @param pattern -- The string pattern to use + @return Regex -- The new Regex object +]=] +function regex.new(pattern: string): Regex + return nil :: any +end + +return regex diff --git a/.lune/.lune-defs/roblox.luau b/.lune/.lune-defs/roblox.luau new file mode 100644 index 0000000..b79ad60 --- /dev/null +++ b/.lune/.lune-defs/roblox.luau @@ -0,0 +1,507 @@ +export type DatabaseScriptability = "None" | "Custom" | "Read" | "ReadWrite" | "Write" + +export type DatabasePropertyTag = + "Deprecated" + | "Hidden" + | "NotBrowsable" + | "NotReplicated" + | "NotScriptable" + | "ReadOnly" + | "WriteOnly" + +export type DatabaseClassTag = + "Deprecated" + | "NotBrowsable" + | "NotCreatable" + | "NotReplicated" + | "PlayerReplicated" + | "Service" + | "Settings" + | "UserSettings" + +export type DatabaseProperty = { + --[=[ + The name of the property. + ]=] + Name: string, + --[=[ + The datatype of the property. + + For normal datatypes this will be a string such as `string`, `Color3`, ... + + For enums this will be a string formatted as `Enum.EnumName`. + ]=] + Datatype: string, + --[=[ + The scriptability of this property, meaning if it can be written / read at runtime. + + All properties are writable and readable in Lune even if scriptability is not. + ]=] + Scriptability: DatabaseScriptability, + --[=[ + Tags describing the property. + + These include information such as if the property can be replicated to players + at runtime, if the property should be hidden in Roblox Studio, and more. + ]=] + Tags: { DatabasePropertyTag }, +} + +export type DatabaseClass = { + --[=[ + The name of the class. + ]=] + Name: string, + --[=[ + The superclass (parent class) of this class. + + May be nil if no parent class exists. + ]=] + Superclass: string?, + --[=[ + Known properties for this class. + ]=] + Properties: { [string]: DatabaseProperty }, + --[=[ + Default values for properties of this class. + + Note that these default properties use Lune's built-in datatype + userdatas, and that if there is a new datatype that Lune does + not yet know about, it may be missing from this table. + ]=] + DefaultProperties: { [string]: any }, + --[=[ + Tags describing the class. + + These include information such as if the class can be replicated + to players at runtime, and top-level class categories. + ]=] + Tags: { DatabaseClassTag }, +} + +export type DatabaseEnum = { + --[=[ + The name of this enum, for example `PartType` or `UserInputState`. + ]=] + Name: string, + --[=[ + Members of this enum. + + Note that this is a direct map of name -> enum values, + and does not actually use the EnumItem datatype itself. + ]=] + Items: { [string]: number }, +} + +export type Database = { + --[=[ + The current version of the reflection database. + + This will follow the format `x.y.z.w`, which most commonly looks something like `0.567.0.123456789` + ]=] + Version: string, + --[=[ + Retrieves a list of all currently known class names. + ]=] + GetClassNames: (self: Database) -> { string }, + --[=[ + Retrieves a list of all currently known enum names. + ]=] + GetEnumNames: (self: Database) -> { string }, + --[=[ + Gets a class with the exact given name, if one exists. + ]=] + GetClass: (self: Database, name: string) -> DatabaseClass?, + --[=[ + Gets an enum with the exact given name, if one exists. + ]=] + GetEnum: (self: Database, name: string) -> DatabaseEnum?, + --[=[ + Finds a class with the given name. + + This will use case-insensitive matching and ignore leading and trailing whitespace. + ]=] + FindClass: (self: Database, name: string) -> DatabaseClass?, + --[=[ + Finds an enum with the given name. + + This will use case-insensitive matching and ignore leading and trailing whitespace. + ]=] + FindEnum: (self: Database, name: string) -> DatabaseEnum?, +} + +type InstanceProperties = { + Parent: Instance?, + ClassName: string, + Name: string, + -- FIXME: This breaks intellisense, but we need some way to access + -- instance properties without casting the entire instance to any... + -- [string]: any, +} + +type InstanceMetatable = { + Clone: (self: Instance) -> Instance, + Destroy: (self: Instance) -> (), + ClearAllChildren: (self: Instance) -> (), + + GetChildren: (self: Instance) -> { Instance }, + GetDebugId: (self: Instance) -> string, + GetDescendants: (self: Instance) -> { Instance }, + GetFullName: (self: Instance) -> string, + + FindFirstAncestor: (self: Instance, name: string) -> Instance?, + FindFirstAncestorOfClass: (self: Instance, className: string) -> Instance?, + FindFirstAncestorWhichIsA: (self: Instance, className: string) -> Instance?, + FindFirstChild: (self: Instance, name: string, recursive: boolean?) -> Instance?, + FindFirstChildOfClass: (self: Instance, className: string, recursive: boolean?) -> Instance?, + FindFirstChildWhichIsA: (self: Instance, className: string, recursive: boolean?) -> Instance?, + + IsA: (self: Instance, className: string) -> boolean, + IsAncestorOf: (self: Instance, descendant: Instance) -> boolean, + IsDescendantOf: (self: Instance, ancestor: Instance) -> boolean, + + GetAttribute: (self: Instance, name: string) -> any, + GetAttributes: (self: Instance) -> { [string]: any }, + SetAttribute: (self: Instance, name: string, value: any) -> (), + + GetTags: (self: Instance) -> { string }, + HasTag: (self: Instance, name: string) -> boolean, + AddTag: (self: Instance, name: string) -> (), + RemoveTag: (self: Instance, name: string) -> (), +} + +export type Instance = typeof(setmetatable( + (nil :: any) :: InstanceProperties, + (nil :: any) :: { __index: InstanceMetatable } +)) + +export type DataModelProperties = {} +export type DataModelMetatable = { + GetService: (self: DataModel, name: string) -> Instance, + FindService: (self: DataModel, name: string) -> Instance?, +} + +export type DataModel = + Instance + & typeof(setmetatable( + (nil :: any) :: DataModelProperties, + (nil :: any) :: { __index: DataModelMetatable } + )) + +--[=[ + @class Roblox + + Built-in library for manipulating Roblox place & model files + + ### Example usage + + ```lua + local fs = require("@lune/fs") + local roblox = require("@lune/roblox") + + -- Reading a place file + local placeFile = fs.readFile("myPlaceFile.rbxl") + local game = roblox.deserializePlace(placeFile) + + -- Manipulating and reading instances - just like in Roblox! + local workspace = game:GetService("Workspace") + for _, child in workspace:GetChildren() do + print("Found child " .. child.Name .. " of class " .. child.ClassName) + end + + -- Writing a place file + local newPlaceFile = roblox.serializePlace(game) + fs.writeFile("myPlaceFile.rbxl", newPlaceFile) + ``` +]=] +local roblox = {} + +--[=[ + @within Roblox + @tag must_use + + Deserializes a place into a DataModel instance. + + This function accepts a string of contents, *not* a file path. + If reading a place file from a file path is desired, `fs.readFile` + can be used and the resulting string may be passed to this function. + + ### Example usage + + ```lua + local fs = require("@lune/fs") + local roblox = require("@lune/roblox") + + local placeFile = fs.readFile("filePath.rbxl") + local game = roblox.deserializePlace(placeFile) + ``` + + @param contents The contents of the place to read +]=] +function roblox.deserializePlace(contents: string): DataModel + return nil :: any +end + +--[=[ + @within Roblox + @tag must_use + + Deserializes a model into an array of instances. + + This function accepts a string of contents, *not* a file path. + If reading a model file from a file path is desired, `fs.readFile` + can be used and the resulting string may be passed to this function. + + ### Example usage + + ```lua + local fs = require("@lune/fs") + local roblox = require("@lune/roblox") + + local modelFile = fs.readFile("filePath.rbxm") + local instances = roblox.deserializeModel(modelFile) + ``` + + @param contents The contents of the model to read +]=] +function roblox.deserializeModel(contents: string): { Instance } + return nil :: any +end + +--[=[ + @within Roblox + @tag must_use + + Serializes a place from a DataModel instance. + + This string can then be written to a file, or sent over the network. + + ### Example usage + + ```lua + local fs = require("@lune/fs") + local roblox = require("@lune/roblox") + + local placeFile = roblox.serializePlace(game) + fs.writeFile("filePath.rbxl", placeFile) + ``` + + @param dataModel The DataModel for the place to serialize + @param xml If the place should be serialized as xml or not. Defaults to `false`, meaning the place gets serialized using the binary format and not xml. +]=] +function roblox.serializePlace(dataModel: DataModel, xml: boolean?): string + return nil :: any +end + +--[=[ + @within Roblox + @tag must_use + + Serializes one or more instances as a model. + + This string can then be written to a file, or sent over the network. + + ### Example usage + + ```lua + local fs = require("@lune/fs") + local roblox = require("@lune/roblox") + + local modelFile = roblox.serializeModel({ instance1, instance2, ... }) + fs.writeFile("filePath.rbxm", modelFile) + ``` + + @param instances The array of instances to serialize + @param xml If the model should be serialized as xml or not. Defaults to `false`, meaning the model gets serialized using the binary format and not xml. +]=] +function roblox.serializeModel(instances: { Instance }, xml: boolean?): string + return nil :: any +end + +--[=[ + @within Roblox + @tag must_use + + Gets the current auth cookie, for usage with Roblox web APIs. + + Note that this auth cookie is formatted for use as a "Cookie" header, + and that it contains restrictions so that it may only be used for + official Roblox endpoints. To get the raw cookie value without any + additional formatting, you can pass `true` as the first and only parameter. + + ### Example usage + + ```lua + local roblox = require("@lune/roblox") + local net = require("@lune/net") + + local cookie = roblox.getAuthCookie() + assert(cookie ~= nil, "Failed to get roblox auth cookie") + + local myPrivatePlaceId = 1234567890 + + local response = net.request({ + url = "https://assetdelivery.roblox.com/v2/assetId/" .. tostring(myPrivatePlaceId), + headers = { + Cookie = cookie, + }, + }) + + local responseTable = net.jsonDecode(response.body) + local responseLocation = responseTable.locations[1].location + print("Download link to place: " .. responseLocation) + ``` + + @param raw If the cookie should be returned as a pure value or not. Defaults to false +]=] +function roblox.getAuthCookie(raw: boolean?): string? + return nil :: any +end + +--[=[ + @within Roblox + @tag must_use + + Gets the bundled reflection database. + + This database contains information about Roblox enums, classes, and their properties. + + ### Example usage + + ```lua + local roblox = require("@lune/roblox") + + local db = roblox.getReflectionDatabase() + + print("There are", #db:GetClassNames(), "classes in the reflection database") + + print("All base instance properties:") + + local class = db:GetClass("Instance") + for name, prop in class.Properties do + print(string.format( + "- %s with datatype %s and default value %s", + prop.Name, + prop.Datatype, + tostring(class.DefaultProperties[prop.Name]) + )) + end + ``` +]=] +function roblox.getReflectionDatabase(): Database + return nil :: any +end + +--[=[ + @within Roblox + + Implements a property for all instances of the given `className`. + + This takes into account class hierarchies, so implementing a property + for the `BasePart` class will also implement it for `Part` and others, + unless a more specific implementation is added to the `Part` class directly. + + ### Behavior + + The given `getter` callback will be called each time the property is + indexed, with the instance as its one and only argument. The `setter` + callback, if given, will be called each time the property should be set, + with the instance as the first argument and the property value as second. + + ### Example usage + + ```lua + local roblox = require("@lune/roblox") + + local part = roblox.Instance.new("Part") + + local propertyValues = {} + roblox.implementProperty( + "BasePart", + "CoolProp", + function(instance) + if propertyValues[instance] == nil then + propertyValues[instance] = 0 + end + propertyValues[instance] += 1 + return propertyValues[instance] + end, + function(instance, value) + propertyValues[instance] = value + end + ) + + print(part.CoolProp) --> 1 + print(part.CoolProp) --> 2 + print(part.CoolProp) --> 3 + + part.CoolProp = 10 + + print(part.CoolProp) --> 11 + print(part.CoolProp) --> 12 + print(part.CoolProp) --> 13 + ``` + + @param className The class to implement the property for. + @param propertyName The name of the property to implement. + @param getter The function which will be called to get the property value when indexed. + @param setter The function which will be called to set the property value when indexed. Defaults to a function that will error with a message saying the property is read-only. +]=] +function roblox.implementProperty( + className: string, + propertyName: string, + getter: (instance: Instance) -> T, + setter: ((instance: Instance, value: T) -> ())? +) + return nil :: any +end + +--[=[ + @within Roblox + + Implements a method for all instances of the given `className`. + + This takes into account class hierarchies, so implementing a method + for the `BasePart` class will also implement it for `Part` and others, + unless a more specific implementation is added to the `Part` class directly. + + ### Behavior + + The given `callback` will be called every time the method is called, + and will receive the instance it was called on as its first argument. + The remaining arguments will be what the caller passed to the method, and + all values returned from the callback will then be returned to the caller. + + ### Example usage + + ```lua + local roblox = require("@lune/roblox") + + local part = roblox.Instance.new("Part") + + roblox.implementMethod("BasePart", "TestMethod", function(instance, ...) + print("Called TestMethod on instance", instance, "with", ...) + end) + + part:TestMethod("Hello", "world!") + --> Called TestMethod on instance Part with Hello, world! + ``` + + @param className The class to implement the method for. + @param methodName The name of the method to implement. + @param callback The function which will be called when the method is called. +]=] +function roblox.implementMethod( + className: string, + methodName: string, + callback: (instance: Instance, ...any) -> ...any +) + return nil :: any +end + +-- TODO: Make typedefs for all of the datatypes as well... +roblox.Instance = (nil :: any) :: { + new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance), +} + +return roblox diff --git a/.lune/.lune-defs/serde.luau b/.lune/.lune-defs/serde.luau new file mode 100644 index 0000000..cd2658d --- /dev/null +++ b/.lune/.lune-defs/serde.luau @@ -0,0 +1,200 @@ +--[=[ + @within Serde + @interface EncodeDecodeFormat + + A serialization/deserialization format supported by the Serde library. + + Currently supported formats: + + | Name | Learn More | + |:-------|:---------------------| + | `json` | https://www.json.org | + | `yaml` | https://yaml.org | + | `toml` | https://toml.io | +]=] +export type EncodeDecodeFormat = "json" | "yaml" | "toml" + +--[=[ + @within Serde + @interface CompressDecompressFormat + + A compression/decompression format supported by the Serde library. + + Currently supported formats: + + | Name | Learn More | + |:---------|:----------------------------------| + | `brotli` | https://github.com/google/brotli | + | `gzip` | https://www.gnu.org/software/gzip | + | `lz4` | https://github.com/lz4/lz4 | + | `zlib` | https://www.zlib.net | +]=] +export type CompressDecompressFormat = "brotli" | "gzip" | "lz4" | "zlib" + +--[=[ + @within Serde + @interface HashAlgorithm + + A hash algorithm supported by the Serde library. + + Currently supported algorithms: + + | Name | Learn More | + |:-----------|:-------------------------------------| + | `md5` | https://en.wikipedia.org/wiki/MD5 | + | `sha1` | https://en.wikipedia.org/wiki/SHA-1 | + | `sha224` | https://en.wikipedia.org/wiki/SHA-2 | + | `sha256` | https://en.wikipedia.org/wiki/SHA-2 | + | `sha384` | https://en.wikipedia.org/wiki/SHA-2 | + | `sha512` | https://en.wikipedia.org/wiki/SHA-2 | + | `sha3-224` | https://en.wikipedia.org/wiki/SHA-3 | + | `sha3-256` | https://en.wikipedia.org/wiki/SHA-3 | + | `sha3-384` | https://en.wikipedia.org/wiki/SHA-3 | + | `sha3-512` | https://en.wikipedia.org/wiki/SHA-3 | + | `blake3` | https://en.wikipedia.org/wiki/BLAKE3 | +]=] +export type HashAlgorithm = + "md5" + | "sha1" + | "sha224" + | "sha256" + | "sha384" + | "sha512" + | "sha3-224" + | "sha3-256" + | "sha3-384" + | "sha3-512" + | "blake3" + +--[=[ + @class Serde + + Built-in library for: + - serialization & deserialization + - encoding & decoding + - compression + + ### Example usage + + ```lua + local fs = require("@lune/fs") + local serde = require("@lune/serde") + + -- Parse different file formats into lua tables + local someJson = serde.decode("json", fs.readFile("myFile.json")) + local someToml = serde.decode("toml", fs.readFile("myFile.toml")) + local someYaml = serde.decode("yaml", fs.readFile("myFile.yaml")) + + -- Write lua tables to files in different formats + fs.writeFile("myFile.json", serde.encode("json", someJson)) + fs.writeFile("myFile.toml", serde.encode("toml", someToml)) + fs.writeFile("myFile.yaml", serde.encode("yaml", someYaml)) + ``` +]=] +local serde = {} + +--[=[ + @within Serde + @tag must_use + + Encodes the given value using the given format. + + See [`EncodeDecodeFormat`] for a list of supported formats. + + @param format The format to use + @param value The value to encode + @param pretty If the encoded string should be human-readable, including things such as newlines and spaces. Only supported for json and toml formats, and defaults to false + @return The encoded string +]=] +function serde.encode(format: EncodeDecodeFormat, value: any, pretty: boolean?): string + return nil :: any +end + +--[=[ + @within Serde + @tag must_use + + Decodes the given string using the given format into a lua value. + + See [`EncodeDecodeFormat`] for a list of supported formats. + + @param format The format to use + @param encoded The string to decode + @return The decoded lua value +]=] +function serde.decode(format: EncodeDecodeFormat, encoded: buffer | string): any + return nil :: any +end + +--[=[ + @within Serde + @tag must_use + + Compresses the given string using the given format. + + See [`CompressDecompressFormat`] for a list of supported formats. + + @param format The format to use + @param s The string to compress + @param level The compression level to use, clamped to the format's limits. The best compression level is used by default + @return The compressed string +]=] +function serde.compress(format: CompressDecompressFormat, s: buffer | string, level: number?): string + return nil :: any +end + +--[=[ + @within Serde + @tag must_use + + Decompresses the given string using the given format. + + See [`CompressDecompressFormat`] for a list of supported formats. + + @param format The format to use + @param s The string to decompress + @return The decompressed string +]=] +function serde.decompress(format: CompressDecompressFormat, s: buffer | string): string + return nil :: any +end + +--[=[ + @within Serde + @tag must_use + + Hashes the given message using the given algorithm + and returns the hash as a hex string. + + See [`HashAlgorithm`] for a list of supported algorithms. + + @param algorithm The algorithm to use + @param message The message to hash + @return The hash as a hex string +]=] +function serde.hash(algorithm: HashAlgorithm, message: string | buffer): string + return nil :: any +end + +--[=[ + @within Serde + @tag must_use + + Hashes the given message using HMAC with the given secret + and algorithm, returning the hash as a base64 string. + + See [`HashAlgorithm`] for a list of supported algorithms. + + @param algorithm The algorithm to use + @param message The message to hash + @return The hash as a base64 string +]=] +function serde.hmac( + algorithm: HashAlgorithm, + message: string | buffer, + secret: string | buffer +): string + return nil :: any +end + +return serde diff --git a/.lune/.lune-defs/stdio.luau b/.lune/.lune-defs/stdio.luau new file mode 100644 index 0000000..e6e88a4 --- /dev/null +++ b/.lune/.lune-defs/stdio.luau @@ -0,0 +1,161 @@ +export type Color = + "reset" + | "black" + | "red" + | "green" + | "yellow" + | "blue" + | "purple" + | "cyan" + | "white" +export type Style = "reset" | "bold" | "dim" + +type PromptFn = ( + (() -> string) + & ((kind: "text", message: string?, defaultOrOptions: string?) -> string) + & ((kind: "confirm", message: string, defaultOrOptions: boolean?) -> boolean) + & ((kind: "select", message: string?, defaultOrOptions: { string }) -> number?) + & ((kind: "multiselect", message: string?, defaultOrOptions: { string }) -> { number }?) +) + +--[=[ + @within Stdio + @function prompt + @tag must_use + + Prompts for user input using the wanted kind of prompt: + + * `"text"` - Prompts for a plain text string from the user + * `"confirm"` - Prompts the user to confirm with y / n (yes / no) + * `"select"` - Prompts the user to select *one* value from a list + * `"multiselect"` - Prompts the user to select *one or more* values from a list + * `nil` - Equivalent to `"text"` with no extra arguments + + @param kind The kind of prompt to use + @param message The message to show the user + @param defaultOrOptions The default value for the prompt, or options to choose from for selection prompts +]=] +local prompt: PromptFn = function(kind: any, message: any, defaultOrOptions: any) + return nil :: any +end + +--[=[ + @class Stdio + + Built-in standard input / output & utility functions + + ### Example usage + + ```lua + local stdio = require("@lune/stdio") + + -- Prompting the user for basic input + local text: string = stdio.prompt("text", "Please write some text") + local confirmed: boolean = stdio.prompt("confirm", "Please confirm this action") + + -- Writing directly to stdout or stderr, without the auto-formatting of print/warn/error + stdio.write("Hello, ") + stdio.write("World! ") + stdio.write("All on the same line") + stdio.ewrite("\nAnd some error text, too") + + -- Reading the entire input from stdin + local input = stdio.readToEnd() + ``` +]=] +local stdio = {} + +stdio.prompt = prompt + +--[=[ + @within Stdio + @tag must_use + + Return an ANSI string that can be used to modify the persistent output color. + + Pass `"reset"` to get a string that can reset the persistent output color. + + ### Example usage + + ```lua + stdio.write(stdio.color("red")) + print("This text will be red") + stdio.write(stdio.color("reset")) + print("This text will be normal") + ``` + + @param color The color to use + @return A printable ANSI string +]=] +function stdio.color(color: Color): string + return nil :: any +end + +--[=[ + @within Stdio + @tag must_use + + Return an ANSI string that can be used to modify the persistent output style. + + Pass `"reset"` to get a string that can reset the persistent output style. + + ### Example usage + + ```lua + stdio.write(stdio.style("bold")) + print("This text will be bold") + stdio.write(stdio.style("reset")) + print("This text will be normal") + ``` + + @param style The style to use + @return A printable ANSI string +]=] +function stdio.style(style: Style): string + return nil :: any +end + +--[=[ + @within Stdio + @tag must_use + + Formats arguments into a human-readable string with syntax highlighting for tables. + + @param ... The values to format + @return The formatted string +]=] +function stdio.format(...: any): string + return nil :: any +end + +--[=[ + @within Stdio + + Writes a string directly to stdout, without any newline. + + @param s The string to write to stdout +]=] +function stdio.write(s: string) end + +--[=[ + @within Stdio + + Writes a string directly to stderr, without any newline. + + @param s The string to write to stderr +]=] +function stdio.ewrite(s: string) end + +--[=[ + @within Stdio + @tag must_use + + Reads the entire input from stdin. + + @return The input from stdin +]=] +function stdio.readToEnd(): string + return nil :: any +end + +return stdio diff --git a/.lune/.lune-defs/task.luau b/.lune/.lune-defs/task.luau new file mode 100644 index 0000000..81bdc2f --- /dev/null +++ b/.lune/.lune-defs/task.luau @@ -0,0 +1,99 @@ +--[=[ + @class Task + + Built-in task scheduler & thread spawning + + ### Example usage + + ```lua + local task = require("@lune/task") + + -- Waiting for a certain amount of time + task.wait(1) + print("Waited for one second") + + -- Running a task after a given amount of time + task.delay(2, function() + print("Ran after two seconds") + end) + + -- Spawning a new task that runs concurrently + task.spawn(function() + print("Running instantly") + task.wait(1) + print("One second passed inside the task") + end) + + print("Running after task.spawn yields") + ``` +]=] +local task = {} + +--[=[ + @within Task + + Stops a currently scheduled thread from resuming. + + @param thread The thread to cancel +]=] +function task.cancel(thread: thread) end + +--[=[ + @within Task + + Defers a thread or function to run at the end of the current task queue. + + @param functionOrThread The function or thread to defer + @return The thread that will be deferred +]=] +function task.defer(functionOrThread: thread | (T...) -> ...any, ...: T...): thread + return nil :: any +end + +--[=[ + @within Task + + Delays a thread or function to run after `duration` seconds. + + @param functionOrThread The function or thread to delay + @return The thread that will be delayed +]=] +function task.delay( + duration: number, + functionOrThread: thread | (T...) -> ...any, + ...: T... +): thread + return nil :: any +end + +--[=[ + @within Task + + Instantly runs a thread or function. + + If the spawned task yields, the thread that spawned the task + will resume, letting the spawned task run in the background. + + @param functionOrThread The function or thread to spawn + @return The thread that was spawned +]=] +function task.spawn(functionOrThread: thread | (T...) -> ...any, ...: T...): thread + return nil :: any +end + +--[=[ + @within Task + + Waits for *at least* the given amount of time. + + The minimum wait time possible when using `task.wait` is limited by the underlying OS sleep implementation. + For most systems this means `task.wait` is accurate down to about 5 milliseconds or less. + + @param duration The amount of time to wait + @return The exact amount of time waited +]=] +function task.wait(duration: number?): number + return nil :: any +end + +return task diff --git a/.lune/analyze.luau b/.lune/analyze.luau index c8debce..3dffcb5 100644 --- a/.lune/analyze.luau +++ b/.lune/analyze.luau @@ -1,15 +1,7 @@ ---!nocheck -local process = require("@lune/process") +--!strict +local spawn = require("util/spawn") -local function start_process(cmd: string) - local arguments = string.split(cmd, " ") - local command = arguments[1] - table.remove(arguments, 1) - - process.spawn(command, arguments, { stdio = "forward" }) -end - -start_process("rojo sourcemap dev.project.json -o sourcemap.json") -start_process( +spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") +spawn.start( "luau-lsp analyze --base-luaurc=.luaurc --sourcemap=sourcemap.json --settings=luau_lsp_settings.json --no-strict-dm-types --ignore Packages/**/*.lua --ignore Packages/**/*.luau --ignore lib/jecs.luau lib/" ) diff --git a/.lune/build.luau b/.lune/build.luau index a98a372..ea82a48 100644 --- a/.lune/build.luau +++ b/.lune/build.luau @@ -1,14 +1,6 @@ ---!nocheck -local process = require("@lune/process") +--!strict +local spawn = require("util/spawn") -local function start_process(cmd: string, env: { string }?) - local arguments = string.split(cmd, " ") - local command = arguments[1] - table.remove(arguments, 1) - - process.spawn(command, arguments, { stdio = "forward", env = env }) -end - -start_process("rojo sourcemap dev.project.json -o sourcemap.json") -start_process("darklua process --config .darklua.json lib/ dist/", { "ROBLOX_DEV=false" }) -start_process("rojo build build.project.json -o build.rbxm") +spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") +spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } }) +spawn.start("rojo build build.project.json -o build.rbxm") diff --git a/.lune/check.luau b/.lune/check.luau index 7bb3126..e4dc495 100644 --- a/.lune/check.luau +++ b/.lune/check.luau @@ -1,15 +1,7 @@ ---!nocheck -local process = require("@lune/process") +--!strict +local spawn = require("util/spawn") -local function start_process(cmd: string) - local arguments = string.split(cmd, " ") - local command = arguments[1] - table.remove(arguments, 1) - - process.spawn(command, arguments, { stdio = "forward" }) -end - -start_process("lune run analyze") -start_process("stylua lib/") -start_process("selene lib/") -start_process("luau test/tests.luau") +spawn.start("lune run analyze") +spawn.start("stylua lib/") +spawn.start("selene lib/") +spawn.start("luau test/tests.luau") diff --git a/.lune/dev.luau b/.lune/dev.luau index 216d8aa..800ca80 100644 --- a/.lune/dev.luau +++ b/.lune/dev.luau @@ -1,14 +1,50 @@ ---!nocheck +--!strict local process = require("@lune/process") +local stdio = require("@lune/stdio") local task = require("@lune/task") -local function start_process(cmd: string, env: { string }?) - local arguments = string.split(cmd, " ") - local command = arguments[1] - table.remove(arguments, 1) +local spawn = require("util/spawn") - process.spawn(command, arguments, { stdio = "forward", env = env }) +spawn.spawn("rojo sourcemap dev.project.json -o sourcemap.json --watch") +spawn.spawn("darklua process --config .darklua.json --watch lib/ dist/", { env = { ROBLOX_DEV = "true" } }) + +task.wait(2.5) + +while true do + local start_commit = stdio.prompt("confirm", "Start commit? -- `y` to start a commit, `n` to exit the script") + if not start_commit then + process.exit(0) + break + end + + local _, check_result = pcall(spawn.start, "lune run check") + if not check_result.ok then + print("Check didn't go ok, aborting commit") + break + end + + local commit_title = stdio.prompt("text", "Commit title -- leave blank to stop committing") + if not commit_title or commit_title == "" then + print("Stopping commit") + break + end + + local commit_messages = "" + while true do + local commit_message = stdio.prompt("text", "Commit message -- added to the description, leave blank to finish") + if not commit_message or commit_message == "" then + break + end + + commit_messages ..= `-m "{commit_message}" ` + end + + local confirm = stdio.prompt("confirm", "Confirm?") + if not confirm then + break + end + + process.spawn("git", { "add", "." }, { stdio = "forward" }) + process.spawn("git", { "commit", "-m", commit_title, commit_messages }, { stdio = "forward" }) + process.spawn("git", { "push" }, { stdio = "forward" }) end - -task.spawn(start_process, "rojo sourcemap dev.project.json -o sourcemap.json --watch") -task.spawn(start_process, "darklua process --config .darklua.json --watch lib/ dist/", { "ROBLOX_DEV=true" }) diff --git a/.lune/dist.luau b/.lune/dist.luau new file mode 100644 index 0000000..ccd0bd8 --- /dev/null +++ b/.lune/dist.luau @@ -0,0 +1,5 @@ +--!strict +local spawn = require("util/spawn") + +spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") +spawn.start("darklua process --config .darklua.json lib/ dist/", { env = { ROBLOX_DEV = "false" } }) diff --git a/.lune/install-packages.luau b/.lune/install-packages.luau index 45103fb..c6b363b 100644 --- a/.lune/install-packages.luau +++ b/.lune/install-packages.luau @@ -1,14 +1,6 @@ ---!nocheck -local process = require("@lune/process") +--!strict +local spawn = require("util/spawn") -local function start_process(cmd: string, cwd: string?) - local arguments = string.split(cmd, " ") - local command = arguments[1] - table.remove(arguments, 1) - - process.spawn(command, arguments, { stdio = "forward", cwd = cwd }) -end - -start_process("wally install") -start_process("rojo sourcemap dev.project.json -o sourcemap.json") -start_process("wally-package-types --sourcemap sourcemap.json Packages/") +spawn.start("wally install") +spawn.start("rojo sourcemap dev.project.json -o sourcemap.json") +spawn.start("wally-package-types --sourcemap sourcemap.json Packages/") diff --git a/.lune/util/spawn.luau b/.lune/util/spawn.luau new file mode 100644 index 0000000..6e46e5d --- /dev/null +++ b/.lune/util/spawn.luau @@ -0,0 +1,39 @@ +--!strict +local process = require("@lune/process") +local task = require("@lune/task") + +--- Start a process with the given command and options +--- ```luau +--- spawn.start("lune run test") +--- ``` +--- @param cmd string +--- @param options process.SpawnOptions? +--- @return process.SpawnResult +local function start_process(cmd: string, options: process.SpawnOptions?): process.SpawnResult + local arguments = string.split(cmd, " ") + local command = arguments[1] + table.remove(arguments, 1) + + local opts: process.SpawnOptions = options ~= nil and options or {} + opts.stdio = opts.stdio ~= nil and opts.stdio or "forward" + + return process.spawn(command, arguments, opts) +end + +--- `task.spawn` a process with the given command and options +--- ```luau +--- spawn.spawn("lune run test") -- process now runs in the background! +--- ``` +--- @param cmd string +--- @param options process.SpawnOptions? +--- @return process.SpawnResult +local function spawn_process(cmd: string, options: process.SpawnOptions?) + task.spawn(start_process, cmd, options) +end + +local spawn = { + start = start_process, + spawn = spawn_process, +} + +return spawn diff --git a/.lune/util/watch.luau b/.lune/util/watch.luau new file mode 100644 index 0000000..be2cdf8 --- /dev/null +++ b/.lune/util/watch.luau @@ -0,0 +1,44 @@ +--!strict +local fs = require("@lune/fs") +local task = require("@lune/task") + +local function watch(path: string, on_change: (path: string, contents: string) -> ()) + local initial_metadata = fs.metadata(path) + if not initial_metadata.exists then + return + end + + local initial_contents = fs.readFile(path) + + local initial_success, why = pcall(on_change :: any, path, initial_contents) -- :: any because otherwise it shits itself and the type doesn't give (boolean, string)?? + if not initial_success then + warn(`There was an error while trying to start the watcher thread:\n{why}`) + return + end + + local last_modification = initial_metadata.modifiedAt + + while true do + local metadata = fs.metadata(path) + + if not metadata.exists then + continue + end + + if metadata.modifiedAt == last_modification then + continue + end + last_modification = metadata.modifiedAt + + local contents = fs.readFile(path) + + local success, err = pcall(on_change :: any, path, contents) + if not success then + warn(err) + end + + task.wait(1) + end +end + +return watch diff --git a/.zed/settings.json b/.zed/settings.json index c3dd6e9..665e15a 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -23,6 +23,9 @@ "@jecs_utils": "lib", "@jecs": "lib/jecs", "@testkit": "test/testkit" + }, + "directoryAliases": { + "@lune": ".lune/.lune-defs/" } } }, diff --git a/wally.toml b/wally.toml index f6c2e9e..8f3a060 100644 --- a/wally.toml +++ b/wally.toml @@ -1,14 +1,14 @@ [package] name = "mark-marks/jecs-utils" -version = "0.1.0" +version = "0.1.1" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" license = "MIT" exclude = ["**"] include = [ "default.project.json", - "lib", - "lib/**", + "dist", + "dist/**", "LICENSE", "wally.toml", "README.md",