├── .eslintignore ├── example ├── client │ └── .gitkeep ├── shared │ └── .gitkeep ├── default.project.json ├── tsconfig.json └── server │ └── index.server.ts ├── .yalc └── roblox-ts-luau │ ├── yalc.sig │ ├── out │ ├── identity.js │ ├── cli.js │ ├── project.js │ └── commands │ │ ├── init.js │ │ └── build.js │ └── package.json ├── assets ├── log.png └── rbxlog.svg ├── luau ├── dist │ ├── default.project.json │ ├── wally.toml │ └── init.lua ├── build.project.json └── README.md ├── .vscode ├── extensions.json └── settings.json ├── foreman.toml ├── yalc.lock ├── .gitignore ├── message-templates ├── README.md ├── src │ ├── index.ts │ ├── MessageTemplateToken.ts │ ├── PlainTextMessageTemplateRenderer.ts │ ├── MessageTemplateRenderer.ts │ ├── MessageTemplate.ts │ ├── MessageTemplateParser.ts │ └── RbxSerializer.ts ├── tsconfig.json ├── package-lock.json └── package.json ├── luau-config.json ├── src ├── Core │ ├── TypeUtils.d.ts │ ├── LogEventCallbackSink.ts │ ├── LogEventPropertyEnricher.ts │ ├── index.ts │ └── LogEventRobloxOutputSink.ts ├── ambient.d.ts ├── Configuration.ts ├── index.ts └── Logger.ts ├── tsconfig.json ├── .eslintrc.json ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | luau -------------------------------------------------------------------------------- /example/client/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/shared/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yalc/roblox-ts-luau/yalc.sig: -------------------------------------------------------------------------------- 1 | 9451cdaa783410ac48c92d106c367d97 -------------------------------------------------------------------------------- /assets/log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roblox-aurora/rbx-log/HEAD/assets/log.png -------------------------------------------------------------------------------- /luau/dist/default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "log", 3 | "tree": { 4 | "$path": "lib" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "roblox-ts.vscode-roblox-ts", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rojo = { source = "rojo-rbx/rojo", version = "7.0.0-alpha.4" } 3 | wally = { source = "UpliftGames/wally", version = "=0.3.0" } -------------------------------------------------------------------------------- /yalc.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1", 3 | "packages": { 4 | "roblox-ts-luau": { 5 | "signature": "9451cdaa783410ac48c92d106c367d97", 6 | "file": true 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /out 3 | /**/include 4 | *.tsbuildinfo 5 | temp 6 | /example/build 7 | *.tgz 8 | 9 | /message-templates/out 10 | 11 | # Luau 12 | /luau/artefacts 13 | /luau/out -------------------------------------------------------------------------------- /message-templates/README.md: -------------------------------------------------------------------------------- 1 | # Message Templates 2 | This is a library for parsing using the [Message Templates](https://messagetemplates.org/) spec in Roblox TypeScript, used primarily by `@rbxts/log`. -------------------------------------------------------------------------------- /.yalc/roblox-ts-luau/out/identity.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.identity = void 0; 4 | function identity(value) { 5 | return value; 6 | } 7 | exports.identity = identity; 8 | -------------------------------------------------------------------------------- /luau/build.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "log", 3 | "tree": { 4 | "$path": "../out", 5 | "TS": { 6 | "$path": "../include", 7 | "node_modules": { 8 | "$path": "../node_modules/@rbxts" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /luau/dist/wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vorlias/log" 3 | description = "Structured logging library for Roblox" 4 | realm = "shared" 5 | license = "MIT" 6 | registry = "https://github.com/upliftgames/wally-index" 7 | authors = [ "vorlias" ] 8 | version = "0.6.4" 9 | -------------------------------------------------------------------------------- /luau/README.md: -------------------------------------------------------------------------------- 1 | ![](../logo.png) 2 | 3 | # Luau Build Directory 4 | This contains all the files used by the `luau` action in github that generates the Luau version of Log. 5 | 6 | ## [build.project.json](build.project.json) 7 | The rojo build project for the Luau version of Log 8 | ## [dist](dist) 9 | Files that are copied into the Luau output. 10 | -------------------------------------------------------------------------------- /message-templates/src/index.ts: -------------------------------------------------------------------------------- 1 | export { MessageTemplateParser } from "./MessageTemplateParser"; 2 | export { MessageTemplateRenderer } from "./MessageTemplateRenderer"; 3 | export { PlainTextMessageTemplateRenderer } from "./PlainTextMessageTemplateRenderer"; 4 | export { Token, PropertyToken, TextToken, TemplateTokenKind } from "./MessageTemplateToken"; 5 | -------------------------------------------------------------------------------- /luau-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "wally": { 3 | "username": "vorlias", 4 | "packageName": "log", 5 | "license": "MIT", 6 | "registry": "https://github.com/upliftgames/wally-index", 7 | "authors": [ 8 | "vorlias" 9 | ], 10 | "realm": "shared", 11 | "description": "Structured logging library for Roblox" 12 | }, 13 | "build": { 14 | "outDir": "out" 15 | } 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 4 | "editor.formatOnSave": true 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 8 | "editor.formatOnSave": true 9 | }, 10 | "eslint.run": "onType", 11 | "eslint.format.enable": true, 12 | "typescript.tsdk": "node_modules/typescript/lib" 13 | } -------------------------------------------------------------------------------- /src/Core/TypeUtils.d.ts: -------------------------------------------------------------------------------- 1 | import { LogEvent } from "index"; 2 | 3 | type SystemDefinedLogPropertyKeys = "Level" | "Template" | "Timestamp"; 4 | 5 | type EnforceNoSystemProps = { readonly [P in SystemDefinedLogPropertyKeys]?: never }; 6 | export type EnforceUserKey = Exclude; 7 | 8 | export type UserDefinedLogProperties = { [P in string]: defined } & 9 | Partial> & 10 | EnforceNoSystemProps; 11 | -------------------------------------------------------------------------------- /src/Core/LogEventCallbackSink.ts: -------------------------------------------------------------------------------- 1 | import { ILogEventSink, LogEvent, LogLevel } from "."; 2 | 3 | export interface ILogEventCallbackSink { 4 | SetMinLogLevel(logLevel: LogLevel): void; 5 | } 6 | 7 | export class LogEventCallbackSink implements ILogEventSink, ILogEventCallbackSink { 8 | private minLogLevel?: LogLevel; 9 | public constructor(private callback: (message: LogEvent) => void) {} 10 | Emit(message: LogEvent): void { 11 | const { minLogLevel } = this; 12 | if (minLogLevel === undefined || message.Level >= minLogLevel) { 13 | this.callback(message); 14 | } 15 | } 16 | SetMinLogLevel(logLevel: LogLevel) { 17 | this.minLogLevel = logLevel; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /message-templates/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // required 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "jsx": "react", 7 | "jsxFactory": "Roact.createElement", 8 | "jsxFragmentFactory": "Roact.Fragment", 9 | "module": "commonjs", 10 | "moduleResolution": "Node", 11 | "noLib": true, 12 | "resolveJsonModule": true, 13 | "strict": true, 14 | "target": "ESNext", 15 | "typeRoots": ["node_modules/@rbxts"], 16 | 17 | // configurable 18 | "rootDir": "src", 19 | "outDir": "out", 20 | "baseUrl": "src", 21 | "incremental": true, 22 | "tsBuildInfoFile": "out/tsconfig.tsbuildinfo", 23 | "declaration": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // required 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "jsx": "react", 7 | "jsxFactory": "Roact.createElement", 8 | "jsxFragmentFactory": "Roact.Fragment", 9 | "module": "commonjs", 10 | "moduleResolution": "Node", 11 | "noLib": true, 12 | "resolveJsonModule": true, 13 | "strict": true, 14 | "stripInternal": true, 15 | "target": "ESNext", 16 | "typeRoots": ["node_modules/@rbxts"], 17 | 18 | // configurable 19 | "rootDir": "src", 20 | "outDir": "out", 21 | "baseUrl": "src", 22 | "declaration": true 23 | }, 24 | "exclude": [ 25 | "example/**/*", 26 | "out/**/*", 27 | "message-templates/**/*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "jsx": true, 5 | "useJSXTextNode": true, 6 | "ecmaVersion": 2018, 7 | "sourceType": "module", 8 | "project": "tsconfig.json" 9 | }, 10 | "plugins": [ 11 | "@typescript-eslint", 12 | "roblox-ts", 13 | "prettier" 14 | ], 15 | "extends": [ 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:roblox-ts/recommended", 18 | "plugin:prettier/recommended" 19 | ], 20 | "rules": { 21 | "prettier/prettier": [ 22 | "warn", 23 | { 24 | "semi": true, 25 | "trailingComma": "all", 26 | "singleQuote": false, 27 | "printWidth": 120, 28 | "tabWidth": 4, 29 | "useTabs": true 30 | } 31 | ] 32 | } 33 | } -------------------------------------------------------------------------------- /message-templates/src/MessageTemplateToken.ts: -------------------------------------------------------------------------------- 1 | export enum TemplateTokenKind { 2 | Text, 3 | Property, 4 | } 5 | 6 | export enum DestructureMode { 7 | Default, 8 | ToString, 9 | Destructure, 10 | } 11 | 12 | export interface Tokens { 13 | [TemplateTokenKind.Text]: TextToken; 14 | [TemplateTokenKind.Property]: PropertyToken; 15 | } 16 | 17 | export interface TextToken { 18 | kind: TemplateTokenKind.Text; 19 | text: string; 20 | } 21 | 22 | export interface PropertyToken { 23 | kind: TemplateTokenKind.Property; 24 | destructureMode: DestructureMode; 25 | propertyName: string; 26 | } 27 | 28 | export type Token = Tokens[keyof Tokens]; 29 | 30 | export function createNode(prop: Tokens[K]) { 31 | return prop; 32 | } 33 | -------------------------------------------------------------------------------- /.yalc/roblox-ts-luau/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roblox-ts-luau", 3 | "version": "1.0.0", 4 | "description": "Tool that builds roblox-ts project into a Luau-compatible project", 5 | "main": "out/cli.js", 6 | "bin": { 7 | "rbxts-luau": "out/cli.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "run": "tsc && node out/cli.js" 12 | }, 13 | "keywords": [], 14 | "author": "Vorlias", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@iarna/toml": "^2.2.5", 18 | "@roblox-ts/rojo-resolver": "^1.0.2", 19 | "@types/hjson": "^2.4.3", 20 | "copy": "^0.3.2", 21 | "execa": "^5.1.1", 22 | "fs-extra": "^10.1.0", 23 | "hjson": "^3.2.2", 24 | "prompts": "^2.4.2" 25 | }, 26 | "yalcSig": "9451cdaa783410ac48c92d106c367d97" 27 | } 28 | -------------------------------------------------------------------------------- /message-templates/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/message-template", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@rbxts/compiler-types": { 8 | "version": "1.1.1-types.3", 9 | "resolved": "https://registry.npmjs.org/@rbxts/compiler-types/-/compiler-types-1.1.1-types.3.tgz", 10 | "integrity": "sha512-jTY9zvwALZOgbfQX6nKazV3clsYds2kO5BIb7KQw7nNq3cibhs0aO8Zy0ePr86IVhZfPe3eQLiioROuOY2y/1g==", 11 | "dev": true 12 | }, 13 | "@rbxts/types": { 14 | "version": "1.0.496", 15 | "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.496.tgz", 16 | "integrity": "sha512-ZYPsIEhoDVqZHedeMY/YJPq9IG7IqZ2oSVfVheADlR8frYuhzZivKDYm7whaW895FrdYXIEVXTtRSPiUruOzNw==", 17 | "dev": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.yalc/roblox-ts-luau/out/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | var __importDefault = (this && this.__importDefault) || function (mod) { 4 | return (mod && mod.__esModule) ? mod : { "default": mod }; 5 | }; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | var yargs_1 = __importDefault(require("yargs")); 8 | void yargs_1.default 9 | // help 10 | .usage("rbxts-luau - A Roblox TypeScript to Luau package converter") 11 | .help("help") 12 | .alias("h", "help") 13 | .describe("help", "show help information") 14 | // version 15 | .version(require("../package.json").version) 16 | .alias("v", "version") 17 | .describe("version", "show version information") 18 | // commands 19 | .commandDir("commands") 20 | .demandCommand() 21 | .parse(); 22 | -------------------------------------------------------------------------------- /message-templates/src/PlainTextMessageTemplateRenderer.ts: -------------------------------------------------------------------------------- 1 | import { DestructureMode, PropertyToken, TextToken } from "MessageTemplateToken"; 2 | import { RbxSerializer } from "RbxSerializer"; 3 | import { MessageTemplateRenderer } from "./MessageTemplateRenderer"; 4 | const HttpService = game.GetService("HttpService"); 5 | 6 | export class PlainTextMessageTemplateRenderer extends MessageTemplateRenderer { 7 | protected RenderPropertyToken(propertyToken: PropertyToken, value: unknown): string { 8 | const serialized = RbxSerializer.Serialize(value, propertyToken.destructureMode); 9 | if (typeIs(serialized, "table")) { 10 | return HttpService.JSONEncode(serialized); 11 | } else { 12 | return tostring(serialized); 13 | } 14 | } 15 | protected RenderTextToken(textToken: TextToken): string { 16 | return textToken.text; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /message-templates/src/MessageTemplateRenderer.ts: -------------------------------------------------------------------------------- 1 | import { PropertyToken, TemplateTokenKind, TextToken, Token } from "MessageTemplateToken"; 2 | 3 | export abstract class MessageTemplateRenderer { 4 | public constructor(private tokens: Token[]) {} 5 | public Render(properties: Record): string { 6 | let result = ""; 7 | for (const token of this.tokens) { 8 | switch (token.kind) { 9 | case TemplateTokenKind.Property: 10 | result += this.RenderPropertyToken(token, properties[token.propertyName]); 11 | break; 12 | case TemplateTokenKind.Text: 13 | result += this.RenderTextToken(token); 14 | } 15 | } 16 | return result; 17 | } 18 | protected abstract RenderPropertyToken(propertyToken: PropertyToken, value: unknown): string; 19 | protected abstract RenderTextToken(textToken: TextToken): string; 20 | } 21 | -------------------------------------------------------------------------------- /src/Core/LogEventPropertyEnricher.ts: -------------------------------------------------------------------------------- 1 | import { ILogEventEnricher, LogEvent, LogLevel } from "./index"; 2 | 3 | export interface ILogEventPropertyEnricher extends ILogEventEnricher { 4 | SetMinLogLevel(minLogLevel: LogLevel): void; 5 | } 6 | 7 | export class LogEventPropertyEnricher implements ILogEventPropertyEnricher { 8 | private minLogLevel?: LogLevel; 9 | 10 | constructor(private props: V) {} 11 | 12 | Enrich(message: Readonly, properties: Map): void { 13 | const minLogLevel = this.minLogLevel; 14 | if (minLogLevel === undefined || message.Level >= minLogLevel) { 15 | for (const [k, v] of pairs(this.props)) { 16 | properties.set(k as string, v); 17 | } 18 | } 19 | } 20 | 21 | public SetMinLogLevel(minLogLevel: LogLevel) { 22 | this.minLogLevel = minLogLevel; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /message-templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/message-templates", 3 | "version": "0.3.2", 4 | "description": "Message template library for Roblox TypeScript", 5 | "main": "out/init.lua", 6 | "scripts": { 7 | "prepublishOnly": "rbxtsc" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "types": "out/index.d.ts", 13 | "files": [ 14 | "out" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/roblox-aurora/rbx-log.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/roblox-aurora/rbx-log/issues" 22 | }, 23 | "homepage": "https://github.com/roblox-aurora/rbx-log#readme", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "devDependencies": { 28 | "@rbxts/compiler-types": "^1.1.1-types.3", 29 | "@rbxts/types": "^1.0.496" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Core/index.ts: -------------------------------------------------------------------------------- 1 | export interface LogEvent { 2 | readonly Level: LogLevel; 3 | readonly Timestamp: string; 4 | readonly SourceContext: string | undefined; 5 | readonly Template: string; 6 | readonly [name: string]: unknown; 7 | } 8 | export enum LogLevel { 9 | Verbose, 10 | Debugging, 11 | Information, 12 | Warning, 13 | Error, 14 | Fatal, 15 | } 16 | export interface LogEventSinkCallback { 17 | (message: Readonly): void; 18 | } 19 | 20 | export interface LogEventEnricherCallback { 21 | (message: LogEvent, properties: Map): void; 22 | } 23 | 24 | export interface ILogEventEnricher { 25 | Enrich(message: Readonly, properties: Map): void; 26 | } 27 | 28 | export interface ILogEventSink { 29 | Emit(message: LogEvent): void; 30 | } 31 | 32 | export type ConfigureOnly = T extends ILogEventEnricher ? Omit : T; 33 | -------------------------------------------------------------------------------- /example/default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "log", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ServerScriptService": { 6 | "$className": "ServerScriptService", 7 | "Server": { 8 | "$path": "build/example/server" 9 | } 10 | }, 11 | "ReplicatedStorage": { 12 | "$className": "ReplicatedStorage", 13 | "TS": { 14 | "$path": "../include", 15 | "node_modules": { 16 | "$path": "../node_modules/@rbxts", 17 | "net": { 18 | "$path": "build/src" 19 | } 20 | } 21 | } 22 | }, 23 | "StarterPlayer": { 24 | "$className": "StarterPlayer", 25 | "StarterPlayerScripts": { 26 | "$className": "StarterPlayerScripts" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /.yalc/roblox-ts-luau/out/project.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.generateWallyToml = void 0; 7 | var fs_extra_1 = __importDefault(require("fs-extra")); 8 | var toml_1 = __importDefault(require("@iarna/toml")); 9 | function generateWallyToml(wallyConfig, version, file) { 10 | var wallyToml = { 11 | package: { 12 | name: wallyConfig.username + "/" + wallyConfig.packageName, 13 | description: wallyConfig.description, 14 | realm: wallyConfig.realm, 15 | license: wallyConfig.license, 16 | registry: wallyConfig.registry, 17 | authors: wallyConfig.authors, 18 | version: version, 19 | } 20 | }; 21 | fs_extra_1.default.writeFileSync(file, toml_1.default.stringify(wallyToml)); 22 | } 23 | exports.generateWallyToml = generateWallyToml; 24 | -------------------------------------------------------------------------------- /src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare interface GettableCores { 2 | TopbarEnabled: boolean; 3 | } 4 | 5 | type ARGLETTER = "s" | "l" | "n" | "f"; 6 | type DebugInfoString = 7 | | `${ARGLETTER}` 8 | | `${ARGLETTER}${ARGLETTER}` 9 | | `${ARGLETTER}${ARGLETTER}${ARGLETTER}` 10 | | `${ARGLETTER}${ARGLETTER}${ARGLETTER}${ARGLETTER}`; 11 | type Split = string extends S 12 | ? string[] 13 | : S extends "" 14 | ? [] 15 | : S extends `${infer T}${D}${infer U}` 16 | ? [T, ...Split] 17 | : [S]; 18 | type Values = { 19 | [P in keyof T]: T[P] extends "s" 20 | ? string 21 | : T[P] extends "l" 22 | ? number 23 | : T[P] extends "n" 24 | ? string 25 | : // : T[P] extends "a" ? [number, boolean] 26 | T[P] extends "f" 27 | ? () => void 28 | : []; 29 | } & 30 | defined[]; 31 | /** 32 | * This is annoyingly hacky. Doesn't work with 'a' (Argument arity) unfortunately. 33 | */ 34 | type DebugInfoResult = Values>; 35 | 36 | // eslint-disable-next-line roblox-ts/no-namespace-merging 37 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // required 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "noLib": true, 7 | "strict": true, 8 | "module": "CommonJS", 9 | "target": "ESNext", 10 | "moduleResolution": "Node", 11 | "experimentalDecorators": true, 12 | "typeRoots": [ 13 | "../node_modules/@rbxts" 14 | ], 15 | "removeComments": true, 16 | 17 | "rootDirs": [ 18 | "server", 19 | "client", 20 | "shared", 21 | "../src", 22 | ], 23 | 24 | "outDir": "build", 25 | "baseUrl": ".", 26 | 27 | "paths": { 28 | "@rbxts/log": ["../src"], 29 | "@rbxts/log/*": ["../src/*"] 30 | }, 31 | "jsx": "react", 32 | "jsxFactory": "Roact.createElement", 33 | "plugins": [ 34 | ] 35 | }, 36 | "include": [ 37 | "**/*", 38 | "../src/**/*" 39 | , "../ignoreMe/MessageTemplate" ] 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Aurora Australis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/server/index.server.ts: -------------------------------------------------------------------------------- 1 | import Log, { Logger, LogLevel } from "@rbxts/log"; 2 | import { MessageTemplateParser, PlainTextMessageTemplateRenderer } from "@rbxts/message-templates"; 3 | 4 | const test = MessageTemplateParser.GetTokens("Hello, {Name}! How is your {TimeOfDay}?"); 5 | const result = new PlainTextMessageTemplateRenderer(test); 6 | 7 | const hour = DateTime.fromUnixTimestamp(os.time()).ToLocalTime().Hour; 8 | print( 9 | result.Render({ 10 | Name: "Vorlias", 11 | TimeOfDay: hour < 6 || hour > 16 ? "Night" : "Day", 12 | }), 13 | ); 14 | 15 | Log.SetLogger( 16 | Logger.configure() 17 | .EnrichWithProperty("Version", PKG_VERSION) 18 | .EnrichWithProperty("Test", 10, (c) => c.SetMinLogLevel(LogLevel.Fatal)) 19 | .WriteTo(Log.RobloxOutput({ ErrorsTreatedAsExceptions: true })) 20 | .WriteToCallback((message) => print(message)) 21 | .Create(), 22 | ); 23 | 24 | const logger = Log.ForScript(); 25 | logger.Info("Basic message with no arguments"); 26 | logger.Info("Basic message using tag with no arguments {Woops}!"); 27 | logger.Info("Hello, {Name}! {@AnArray} {AnObject}", "Vorlias", [1, 2, 3, 4], { A: 10 }); 28 | 29 | function TestingLol() {} 30 | 31 | Log.ForFunction(TestingLol).Info("HUH?"); 32 | 33 | // Throw the message :-) 34 | throw Log.Fatal("Invalid input '{@Input}' - expects something else.", "InputExample"); 35 | -------------------------------------------------------------------------------- /message-templates/src/MessageTemplate.ts: -------------------------------------------------------------------------------- 1 | import { DestructureMode, PropertyToken, TemplateTokenKind, Token } from "./MessageTemplateToken"; 2 | const HttpService = game.GetService("HttpService"); 3 | 4 | export class MessageTemplate { 5 | private properties: readonly PropertyToken[]; 6 | constructor(private template: string, private tokens: readonly Token[]) { 7 | this.properties = tokens.filter((f): f is PropertyToken => f.kind === TemplateTokenKind.Property); 8 | } 9 | 10 | public GetTokens() { 11 | return this.tokens; 12 | } 13 | 14 | public GetProperties() { 15 | return this.properties; 16 | } 17 | 18 | public GetText() { 19 | return this.template; 20 | } 21 | 22 | // /** 23 | // * Renders the template in plain text based on the given properties 24 | // * @param properties 25 | // * @returns 26 | // */ 27 | // public Render(properties: Record) { 28 | // let result = ""; 29 | // for (const token of this.tokens) { 30 | // switch (token.kind) { 31 | // case TemplateTokenKind.Text: 32 | // result += token.text; 33 | // break; 34 | // case TemplateTokenKind.Property: 35 | // const prop = properties[token.propertyName]; 36 | 37 | // if (token.destructureMode === DestructureMode.ToString) { 38 | // result += tostring(prop); 39 | // } else if (token.destructureMode === DestructureMode.Destructure) { 40 | // result += HttpService.JSONEncode(prop); 41 | // } else { 42 | // if (typeIs(prop, "Instance")) { 43 | // result += prop.GetFullName(); 44 | // } else if (typeIs(prop, "table")) { 45 | // result += HttpService.JSONEncode(prop); 46 | // } else { 47 | // result += tostring(prop); 48 | // } 49 | // } 50 | // } 51 | // } 52 | // return result; 53 | // } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/log", 3 | "version": "0.6.4", 4 | "description": "Structured logging library for Roblox", 5 | "main": "out/init.lua", 6 | "scripts": { 7 | "build": "rbxtsc", 8 | "prepare": "npm run build:templates && rbxtsc", 9 | "build:example": "cross-env NODE_ENV=development rbxtsc-dev --type=game -p ./example -i ./include", 10 | "build:luau": "rbxts-luau build", 11 | "publish:luau": "rbxts-luau build --publish", 12 | "watch:example": "cross-env NODE_ENV=development TYPE=TestTS rbxtsc-dev -w --type=game -p ./example -i ./include", 13 | "serve:example": "rojo serve ./example/default.project.json --port 34567", 14 | "dev:example": "concurrently npm:watch:example npm:serve:example", 15 | "build:templates": "cd message-templates && rbxtsc" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/roblox-aurora/rbx-log.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/roblox-aurora/rbx-log/issues" 25 | }, 26 | "homepage": "https://github.com/roblox-aurora/rbx-log#readme", 27 | "license": "MIT", 28 | "types": "out/index.d.ts", 29 | "files": [ 30 | "out" 31 | ], 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "dependencies": { 36 | "@rbxts/message-templates": "^0.3.1" 37 | }, 38 | "devDependencies": { 39 | "@rbxts/compiler-types": "^1.1.1-types.3", 40 | "@rbxts/types": "^1.0.568", 41 | "@typescript-eslint/eslint-plugin": "^4.28.0", 42 | "@typescript-eslint/parser": "^4.28.0", 43 | "concurrently": "^6.2.0", 44 | "cross-env": "^7.0.3", 45 | "eslint": "^7.29.0", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-prettier": "^3.4.0", 48 | "eslint-plugin-roblox-ts": "0.0.27", 49 | "prettier": "^2.3.1", 50 | "roblox-ts-luau": "file:.yalc/roblox-ts-luau", 51 | "typescript": "^4.3.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Core/LogEventRobloxOutputSink.ts: -------------------------------------------------------------------------------- 1 | import { MessageTemplateParser, PlainTextMessageTemplateRenderer } from "@rbxts/message-templates"; 2 | import { ILogEventSink, LogEvent, LogLevel } from "../Core"; 3 | 4 | export interface RobloxOutputOptions { 5 | /** 6 | * The tag format 7 | * - `short` - `DBG`, `INF`, `WRN`, `ERR`, `FTL` 8 | * - `full` - `DEBUG`, `INFO`, `WARNING`, `ERROR`, `FATAL` 9 | */ 10 | TagFormat?: "short" | "full"; 11 | 12 | /** 13 | * Ignores logging errors. 14 | * 15 | * Use this if you use the `throw Log.Error(...)` or `throw Log.Fatal(...)` patterns. 16 | */ 17 | ErrorsTreatedAsExceptions?: boolean; 18 | 19 | /** 20 | * A prefix to add to each output message before the severity tag e.g. `EXAMPLE` will become `[EXAMPLE] [INF]: Example!` 21 | */ 22 | Prefix?: string; 23 | } 24 | 25 | export class LogEventRobloxOutputSink implements ILogEventSink { 26 | public constructor(private options: RobloxOutputOptions) {} 27 | Emit(message: LogEvent): void { 28 | const { TagFormat = "short", ErrorsTreatedAsExceptions, Prefix } = this.options; 29 | 30 | if (message.Level >= LogLevel.Error && ErrorsTreatedAsExceptions) { 31 | return; 32 | } 33 | 34 | const template = new PlainTextMessageTemplateRenderer(MessageTemplateParser.GetTokens(message.Template)); 35 | const time = DateTime.fromIsoDate(message.Timestamp)?.FormatLocalTime("HH:mm:ss", "en-us"); 36 | let tag: string; 37 | switch (message.Level) { 38 | case LogLevel.Verbose: 39 | tag = TagFormat === "short" ? "VRB" : "VERBOSE"; 40 | break; 41 | case LogLevel.Debugging: 42 | tag = TagFormat === "short" ? "DBG" : "DEBUG"; 43 | break; 44 | case LogLevel.Information: 45 | tag = TagFormat === "short" ? "INF" : "INFO"; 46 | break; 47 | case LogLevel.Warning: 48 | tag = TagFormat === "short" ? "WRN" : "WARNING"; 49 | break; 50 | case LogLevel.Error: 51 | tag = TagFormat === "short" ? "ERR" : "ERROR"; 52 | break; 53 | case LogLevel.Fatal: 54 | tag = TagFormat === "short" ? "FTL" : "FATAL"; 55 | break; 56 | } 57 | 58 | const messageRendered = template.Render(message); 59 | const formattedMessage = 60 | Prefix !== undefined ? `[${Prefix}] [${tag}] ${messageRendered}` : `[${tag}] ${messageRendered}`; 61 | 62 | if (message.Level >= LogLevel.Warning) { 63 | warn(formattedMessage); 64 | } else { 65 | print(formattedMessage); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Configuration.ts: -------------------------------------------------------------------------------- 1 | import { ILogEventPropertyEnricher, LogEventPropertyEnricher } from "./Core/LogEventPropertyEnricher"; 2 | import { LogEventSinkCallback, LogLevel, ILogEventEnricher, ILogEventSink, ConfigureOnly } from "./Core"; 3 | import { Logger } from "./Logger"; 4 | import { ILogEventCallbackSink, LogEventCallbackSink } from "./Core/LogEventCallbackSink"; 5 | const RunService = game.GetService("RunService"); 6 | 7 | export class LogConfiguration { 8 | private sinks = new Array(); 9 | private enrichers = new Array(); 10 | private logLevel = RunService.IsStudio() ? LogLevel.Debugging : LogLevel.Information; 11 | public constructor(private logger: Logger) {} 12 | 13 | /** 14 | * Adds an output sink (e.g. A console or analytics provider) 15 | * @param sink The sink to add 16 | * @param configure Configure the specified sink 17 | */ 18 | public WriteTo(sink: TSink, configure?: (value: Omit) => void) { 19 | configure?.(sink); 20 | this.sinks.push(sink); 21 | return this; 22 | } 23 | 24 | /** 25 | * Adds a callback based sink 26 | * @param sinkCallback The sink callback 27 | */ 28 | public WriteToCallback(sinkCallback: LogEventSinkCallback, configure?: (value: ILogEventCallbackSink) => void) { 29 | const sink = new LogEventCallbackSink(sinkCallback); 30 | configure?.(sink); 31 | this.sinks.push(sink); 32 | return this; 33 | } 34 | 35 | /** 36 | * Adds an "enricher", which adds extra properties to a log event. 37 | */ 38 | public Enrich(enricher: ILogEventEnricher) { 39 | if (typeIs(enricher, "function")) { 40 | } else { 41 | this.enrichers.push(enricher); 42 | } 43 | 44 | return this; 45 | } 46 | 47 | /** 48 | * Adds a static property value to each message 49 | * @param propertyName The property name 50 | * @param value The value of the property 51 | */ 52 | public EnrichWithProperty( 53 | propertyName: string, 54 | value: V, 55 | configure?: (enricher: ConfigureOnly>) => void, 56 | ) { 57 | return this.EnrichWithProperties( 58 | { 59 | [propertyName]: value, 60 | }, 61 | configure, 62 | ); 63 | } 64 | 65 | /** 66 | * Adds static property values to each message 67 | * @param props The properties to add to this logger 68 | */ 69 | public EnrichWithProperties( 70 | props: TProps, 71 | configure?: (enricher: ConfigureOnly>) => void, 72 | ) { 73 | const enricher = new LogEventPropertyEnricher(props); 74 | configure?.(enricher); 75 | this.enrichers.push(enricher); 76 | return this; 77 | } 78 | 79 | /** 80 | * Sets the minimum log level 81 | * @param logLevel The minimum log level to display 82 | */ 83 | public SetMinLogLevel(logLevel: LogLevel) { 84 | this.logLevel = logLevel; 85 | return this; 86 | } 87 | 88 | public Create() { 89 | this.logger.SetSinks(this.sinks); 90 | this.logger.SetEnrichers(this.enrichers); 91 | this.logger.SetMinLogLevel(this.logLevel); 92 | return this.logger; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /assets/rbxlog.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 28 | 34 | 40 | 41 | 61 | 63 | 64 | 66 | image/svg+xml 67 | 69 | 70 | 71 | 72 | 73 | 77 | 85 | 89 | LOG 97 | 98 | 99 | -------------------------------------------------------------------------------- /message-templates/src/MessageTemplateParser.ts: -------------------------------------------------------------------------------- 1 | import { MessageTemplate } from "./MessageTemplate"; 2 | import { 3 | createNode, 4 | DestructureMode, 5 | PropertyToken, 6 | TemplateTokenKind, 7 | TextToken, 8 | Token, 9 | } from "./MessageTemplateToken"; 10 | 11 | export namespace MessageTemplateParser { 12 | export function GetTokens(message: string) { 13 | const tokens = new Array(); 14 | for (const token of tokenize(message)) { 15 | tokens.push(token); 16 | } 17 | return tokens; 18 | } 19 | 20 | function* tokenize(messageTemplate: string): Generator { 21 | if (messageTemplate.size() === 0) { 22 | yield identity({ kind: TemplateTokenKind.Text, text: "" }); 23 | return; 24 | } 25 | 26 | let nextIndex = 0; 27 | while (true) { 28 | let startIndex = nextIndex; 29 | let textToken: TextToken; 30 | [nextIndex, textToken] = parseText(nextIndex, messageTemplate); 31 | 32 | if (nextIndex > startIndex) { 33 | yield textToken; 34 | } 35 | 36 | if (nextIndex >= messageTemplate.size()) break; 37 | 38 | startIndex = nextIndex; 39 | let propertyToken: PropertyToken | TextToken | undefined; 40 | [nextIndex, propertyToken] = parseProperty(nextIndex, messageTemplate); 41 | 42 | if (startIndex < nextIndex) { 43 | yield propertyToken; 44 | } 45 | 46 | if (nextIndex > messageTemplate.size()) { 47 | break; 48 | } 49 | } 50 | } 51 | 52 | function parseText(startAt: number, messageTemplate: string) { 53 | const results = new Array(); 54 | do { 55 | const char = messageTemplate.sub(startAt, startAt); 56 | if (char === "{") { 57 | const nextChar = messageTemplate.sub(startAt + 1, startAt + 1); 58 | if (nextChar === "{") { 59 | results.push(char); 60 | startAt++; 61 | } else { 62 | break; 63 | } 64 | } else { 65 | results.push(char); 66 | 67 | const nextChar = messageTemplate.sub(startAt + 1, startAt + 1); 68 | if (char === "}") { 69 | if (nextChar === "}") { 70 | startAt++; 71 | } 72 | } 73 | } 74 | startAt++; 75 | } while (startAt <= messageTemplate.size()); 76 | return [startAt, identity({ kind: TemplateTokenKind.Text, text: results.join("") })] as const; 77 | } 78 | 79 | function readWhile(startAt: number, text: string, condition: (char: string) => boolean) { 80 | let result = ""; 81 | while (startAt < text.size() && condition(string.sub(text, startAt, startAt))) { 82 | const char = string.sub(text, startAt, startAt); 83 | result += char; 84 | startAt++; 85 | } 86 | 87 | return [startAt, result] as const; 88 | } 89 | 90 | function isValidNameCharacter(char: string) { 91 | return char.match("[%w_]")[0] !== undefined; 92 | } 93 | 94 | function isValidDestructureHint(char: string) { 95 | return char.match("[@$]")[0] !== undefined; 96 | } 97 | 98 | function parseProperty(index: number, messageTemplate: string): [number, PropertyToken | TextToken] { 99 | index++; // Skip { 100 | 101 | let propertyName: string; 102 | [index, propertyName] = readWhile( 103 | index, 104 | messageTemplate, 105 | (c) => isValidDestructureHint(c) || (isValidNameCharacter(c) && c !== "}"), 106 | ); 107 | 108 | if (index > messageTemplate.size()) { 109 | return [index, identity({ kind: TemplateTokenKind.Text, text: propertyName })]; 110 | } 111 | 112 | let destructureMode = DestructureMode.Default; 113 | const char = propertyName.sub(1, 1); 114 | if (isValidDestructureHint(char)) { 115 | switch (char) { 116 | case "@": 117 | destructureMode = DestructureMode.Destructure; 118 | break; 119 | case "$": 120 | destructureMode = DestructureMode.ToString; 121 | break; 122 | default: 123 | destructureMode = DestructureMode.Default; 124 | } 125 | propertyName = propertyName.sub(2); 126 | } 127 | 128 | return [ 129 | index + 1, // skip } 130 | identity({ kind: TemplateTokenKind.Property, propertyName, destructureMode }), 131 | ]; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | _Not associated with BLAMMO ;-)_ 5 |
6 | 7 | Structured logging library for Roblox, akin to (and inspired by) [serilog](https://github.com/serilog/serilog). It uses the [Message Templates](https://messagetemplates.org/) spec for handling logging. 8 | 9 | ## Setup 10 | To begin, you will need to install the package. This can be done via 11 | ``` 12 | npm i @rbxts/log 13 | ``` 14 | 15 | Once installed, to begin logging you will need to configure the logger. (The server/client will need separate configurations, this is the recommended way of doing it) 16 | 17 | Basic setup: 18 | ```ts 19 | import Log, { Logger } from "@rbxts/log"; 20 | Log.SetLogger( 21 | Logger.configure() 22 | .WriteTo(Log.RobloxOutput()) // WriteTo takes a sink and writes to it 23 | .Create() // Creates the logger from the configuration 24 | ); 25 | 26 | Log.Info("Hello, Log!"); 27 | ``` 28 | 29 | The main power of this library comes from the structured event data logging: 30 | ```ts 31 | const startPoint = new Vector2(0, 0) 32 | const position = new Vector2(25, 134); 33 | const distance = position.sub(startPoint).Magnitude; 34 | 35 | Log.Info("Walked to {@Position}, travelling a distance of {Distance}", position, distance); 36 | ``` 37 | 38 | Log uses [message templates](https://messagetemplates.org/), like serilog and will format strings with _named_ parameters (positional coming soon). 39 | 40 | The example above has two properties, `Position` and `Distance`, in the log event the `@` operator in front of position tells Log to _serialize_ the object passed in, rather than using `tostring(value)`. The listed data types this library can serialize is listed below. 41 | 42 | Rendered into JSON using `HttpService`, these properties appear alongside the Timestamp, Level and Template like: 43 | 44 | ```json 45 | {"Position": {"X": 25, "Y": 134}, "Distance": 136.32 } 46 | ``` 47 | 48 | The structured nature of the data means that it is easily searched and filtered by external tools (as well as roblox-based libraries like `Zircon`) 49 | 50 | Of course, this data can be logged to the roblox console or another supported console directly if need be, the default Roblox Output sink for example displays the above as such: 51 | ``` 52 | 08:29:20 [INF] Walked to {"X": 25, "Y": 134}, travelling a distance of 136.32 53 | ``` 54 | 55 | ## Features 56 | - Level-based logging, with levels like `Debug`, `Information`, `Warning` and `Error`. 57 | - Support for custom sinks, like logging to your own external server or to a console like the roblox output and Zircon. 58 | - The ability to enrich logging events using `EnrichWithProperty` or `Enrich`. E.g. add the version to your logging events: 59 | ```ts 60 | Log.SetLogger( 61 | Logger.configure() 62 | // ... 63 | .EnrichWithProperty("Version", PKG_VERSION) // Will add "Version" to the event data 64 | // ... 65 | .Create() 66 | ); 67 | ``` 68 | - A global `Log` object, with the ability to create individual `Logger` objects. 69 | 70 | ## Supported Sinks 71 | | Sink Name | Via | Information | 72 | |--------|-------------|----------| 73 | | Roblox Output | `Log.RobloxOutput()` | Built in sink which will write to the output + dev console | 74 | | [Zircon](https://github.com/roblox-aurora/zircon) | `Zircon.Log.Console()` | Runtime Debugging Console for Roblox | 75 | 76 | ## Use with [Flamework](https://fireboltofdeath.dev/docs) 77 | Flamework is a very useful dependency injection transformer for roblox-ts, in which we can use `@rbxts/log` quite extensively like you would with regular DI. 78 | 79 | 80 | A simple approach to the DI logging is to just use `ForContext` - however, this is a bit more work and more explicit. 81 | ```ts 82 | @Service() 83 | export class MyService { 84 | public readonly logger = Log.ForContext(MyService); 85 | } 86 | ``` 87 | 88 | 89 | Instead, we can use the [dependency resolution](https://fireboltofdeath.dev/docs/flamework/modding/guides/dependency-resolution) feature of Flamework so that we can just refer to the `Logger` object from the constructor :- 90 | 91 | (e.g. in `index.server.ts` & `index.client.ts`) 92 | ```ts 93 | import Log, { Logger } from "@rbxts/log"; 94 | Modding.registerDependency((ctor) => { 95 | return Log.ForContext(ctor); // will register this under the given DI class 96 | }); 97 | ``` 98 | 99 | Then in our above example: 100 | ```ts 101 | @Service() 102 | export class MyService { 103 | public constructor(private readonly logger: Logger) {} 104 | } 105 | ``` 106 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { UserDefinedLogProperties } from "./Core/TypeUtils"; 2 | import { LogConfiguration } from "./Configuration"; 3 | import { LogEventRobloxOutputSink, RobloxOutputOptions } from "./Core/LogEventRobloxOutputSink"; 4 | import { Logger, LoggerContext } from "./Logger"; 5 | import { LogLevel } from "./Core"; 6 | export { Logger } from "./Logger"; 7 | export { LogLevel, LogEvent } from "./Core"; 8 | 9 | namespace Log { 10 | let defaultLogger: Logger = Logger.default(); 11 | 12 | export function SetLogger(logger: Logger) { 13 | defaultLogger = logger; 14 | } 15 | 16 | export function Default() { 17 | return defaultLogger; 18 | } 19 | 20 | /** 21 | * Configure a custom logger 22 | */ 23 | export function Configure() { 24 | return Logger.configure(); 25 | } 26 | 27 | /** 28 | * Creates a custom logger 29 | * @returns The logger configuration, use `Initialize` to get the logger once configured 30 | * @deprecated Use {@link Configure}. This will be removed in future. 31 | */ 32 | export const Create = Configure; 33 | 34 | /** 35 | * The default roblox output sink 36 | * @param options Options for the sink 37 | */ 38 | export const RobloxOutput = (options: RobloxOutputOptions = {}) => new LogEventRobloxOutputSink(options); 39 | 40 | /** 41 | * Write a "Fatal" message to the default logger 42 | * @param template 43 | * @param args 44 | */ 45 | export function Fatal(template: string, ...args: unknown[]) { 46 | return defaultLogger.Fatal(template, ...args); 47 | } 48 | 49 | /** 50 | * Write a "Verbose" message to the default logger 51 | * @param template 52 | * @param args 53 | */ 54 | export function Verbose(template: string, ...args: unknown[]) { 55 | defaultLogger.Verbose(template, ...args); 56 | } 57 | 58 | /** 59 | * Write an "Information" message to the default logger 60 | * @param template 61 | * @param args 62 | */ 63 | export function Info(template: string, ...args: unknown[]) { 64 | defaultLogger.Info(template, ...args); 65 | } 66 | 67 | /** 68 | * Write a "Debugging" message to the default logger 69 | * @param template 70 | * @param args 71 | */ 72 | export function Debug(template: string, ...args: unknown[]) { 73 | defaultLogger.Debug(template, ...args); 74 | } 75 | 76 | /** 77 | * Write a "Warning" message to the default logger 78 | * @param template 79 | * @param args 80 | */ 81 | export function Warn(template: string, ...args: unknown[]) { 82 | defaultLogger.Warn(template, ...args); 83 | } 84 | 85 | /** 86 | * Write an "Error" message to the default logger 87 | * @param template 88 | * @param args 89 | */ 90 | export function Error(template: string, ...args: unknown[]) { 91 | return defaultLogger.Error(template, ...args); 92 | } 93 | 94 | /** 95 | * Creates a logger that enriches log events with the specified context as the property `SourceContext`. 96 | * @param context The tag to use 97 | */ 98 | export function ForContext( 99 | context: LoggerContext, 100 | contextConfiguration?: (configuration: Omit) => void, 101 | ) { 102 | return defaultLogger.ForContext(context, contextConfiguration); 103 | } 104 | 105 | /** 106 | * Creates a logger that nriches log events with the specified property 107 | * @param name The name of the property 108 | * @param value The value of the property 109 | */ 110 | export function ForProperty(name: K, value: UserDefinedLogProperties[K]) { 111 | return defaultLogger.ForProperty(name, value); 112 | } 113 | 114 | /** 115 | * Creates a logger that enriches log events with the specified properties 116 | * @param props The properties 117 | */ 118 | export function ForProperties(props: TProps) { 119 | return defaultLogger.ForProperties(props); 120 | } 121 | 122 | /** 123 | * Creates a logger that enriches log events with the `SourceContext` as the containing script 124 | */ 125 | export function ForScript(scriptContextConfiguration?: (configuration: Omit) => void) { 126 | // Unfortunately have to duplicate here, since `debug.info`. 127 | const [s] = debug.info(2, "s"); 128 | const copy = defaultLogger.Copy(); 129 | scriptContextConfiguration?.(copy); 130 | return copy 131 | .EnrichWithProperties({ 132 | SourceContext: s, 133 | SourceKind: "Script", 134 | }) 135 | .Create(); 136 | } 137 | 138 | /** 139 | * Set the minimum log level for the default logger 140 | */ 141 | export function SetMinLogLevel(logLevel: LogLevel) { 142 | defaultLogger.SetMinLogLevel(logLevel); 143 | } 144 | 145 | /** 146 | * Creates a logger that enriches log events with `SourceContext` as the specified function 147 | */ 148 | export function ForFunction( 149 | func: () => void, 150 | funcContextConfiguration?: (configuration: Omit) => void, 151 | ) { 152 | return defaultLogger.ForFunction(func, funcContextConfiguration); 153 | } 154 | } 155 | export default Log; 156 | -------------------------------------------------------------------------------- /message-templates/src/RbxSerializer.ts: -------------------------------------------------------------------------------- 1 | import { DestructureMode } from "./MessageTemplateToken"; 2 | 3 | /** 4 | * Handles serialization of Roblox objects for use in event data 5 | */ 6 | export namespace RbxSerializer { 7 | const HttpService = game.GetService("HttpService"); 8 | 9 | export interface SerializedVector { 10 | readonly X: number; 11 | readonly Y: number; 12 | readonly Z: number; 13 | } 14 | export function SerializeVector3(value: Vector3 | Vector3int16): SerializedVector { 15 | return { X: value.X, Y: value.Y, Z: value.Z }; 16 | } 17 | 18 | export interface SerializedVector2 { 19 | readonly X: number; 20 | readonly Y: number; 21 | } 22 | export function SerializeVector2(value: Vector2 | Vector2int16): SerializedVector2 { 23 | return { X: value.X, Y: value.Y }; 24 | } 25 | 26 | export interface SerializedNumberRange { 27 | readonly Min: number; 28 | readonly Max: number; 29 | } 30 | export function SerializeNumberRange(numberRange: NumberRange): SerializedNumberRange { 31 | return { Min: numberRange.Min, Max: numberRange.Max }; 32 | } 33 | 34 | export function SerializeDateTime(dateTime: DateTime): string { 35 | return dateTime.ToIsoDate(); 36 | } 37 | 38 | export function SerializeEnumItem(enumItem: EnumItem): string { 39 | return tostring(enumItem); 40 | } 41 | 42 | export interface SerializedUDim { 43 | readonly Offset: number; 44 | readonly Scale: number; 45 | } 46 | export function SerializeUDim(value: UDim): SerializedUDim { 47 | return { Offset: value.Offset, Scale: value.Scale }; 48 | } 49 | 50 | export interface SerializedUDim2 { 51 | readonly X: SerializedUDim; 52 | readonly Y: SerializedUDim; 53 | } 54 | export function SerializeUDim2(value: UDim2): SerializedUDim2 { 55 | return { X: SerializeUDim(value.X), Y: SerializeUDim(value.Y) }; 56 | } 57 | 58 | export interface SerializedColor3 { 59 | readonly R: number; 60 | readonly G: number; 61 | readonly B: number; 62 | } 63 | export function SerializeColor3(color3: Color3) { 64 | return { R: color3.R, G: color3.G, B: color3.B }; 65 | } 66 | 67 | export function SerializeBrickColor(color: BrickColor) { 68 | return SerializeColor3(color.Color); 69 | } 70 | 71 | export interface SerializedRect { 72 | readonly RectMin: SerializedVector2; 73 | readonly RectMax: SerializedVector2; 74 | readonly RectHeight: number; 75 | readonly RectWidth: number; 76 | } 77 | export function SerializeRect(value: Rect): SerializedRect { 78 | return { 79 | RectMin: SerializeVector2(value.Min), 80 | RectMax: SerializeVector2(value.Max), 81 | RectHeight: value.Height, 82 | RectWidth: value.Width, 83 | }; 84 | } 85 | 86 | export interface SerializedPathWaypoint { 87 | readonly WaypointAction: string; 88 | readonly WaypointPosition: SerializedVector; 89 | } 90 | export function SerializePathWaypoint(value: PathWaypoint): SerializedPathWaypoint { 91 | return { WaypointAction: SerializeEnumItem(value.Action), WaypointPosition: SerializeVector3(value.Position) }; 92 | } 93 | 94 | export interface SerializedColorSequenceKeypoint { 95 | readonly ColorTime: number; 96 | readonly ColorValue: SerializedColor3; 97 | } 98 | export function SerializeColorSequenceKeypoint(value: ColorSequenceKeypoint): SerializedColorSequenceKeypoint { 99 | return { ColorTime: value.Time, ColorValue: SerializeColor3(value.Value) }; 100 | } 101 | 102 | export function SerializeColorSequence(value: ColorSequence) { 103 | return { ColorKeypoints: value.Keypoints.map((v) => SerializeColorSequenceKeypoint(v)) }; 104 | } 105 | 106 | export interface SerializedNumberSequenceKeypoint { 107 | readonly NumberTime: number; 108 | readonly NumberValue: number; 109 | } 110 | export function SerializeNumberSequenceKeypoint(value: NumberSequenceKeypoint): SerializedNumberSequenceKeypoint { 111 | return { NumberTime: value.Time, NumberValue: value.Value }; 112 | } 113 | 114 | export function SerializeNumberSequence(value: NumberSequence) { 115 | return { NumberKeypoints: value.Keypoints.map((v) => SerializeNumberSequenceKeypoint(v)) }; 116 | } 117 | 118 | export function Serialize(value: unknown, destructureMode = DestructureMode.Default) { 119 | if (destructureMode === DestructureMode.ToString) { 120 | return tostring(value); 121 | } 122 | 123 | if (typeIs(value, "Instance")) { 124 | return value.GetFullName(); 125 | } else if (typeIs(value, "vector") || typeIs(value, "Vector3int16")) { 126 | return SerializeVector3(value); 127 | } else if (typeIs(value, "Vector2") || typeIs(value, "Vector2int16")) { 128 | return SerializeVector2(value); 129 | } else if (typeIs(value, "DateTime")) { 130 | return SerializeDateTime(value); 131 | } else if (typeIs(value, "EnumItem")) { 132 | return SerializeEnumItem(value); 133 | } else if (typeIs(value, "NumberRange")) { 134 | return SerializeNumberRange(value); 135 | } else if (typeIs(value, "UDim")) { 136 | return SerializeUDim(value); 137 | } else if (typeIs(value, "UDim2")) { 138 | return SerializeUDim2(value); 139 | } else if (typeIs(value, "Color3")) { 140 | return SerializeColor3(value); 141 | } else if (typeIs(value, "BrickColor")) { 142 | return SerializeBrickColor(value); 143 | } else if (typeIs(value, "Rect")) { 144 | return SerializeRect(value); 145 | } else if (typeIs(value, "PathWaypoint")) { 146 | return SerializePathWaypoint(value); 147 | } else if (typeIs(value, "ColorSequenceKeypoint")) { 148 | return SerializeColorSequenceKeypoint(value); 149 | } else if (typeIs(value, "ColorSequence")) { 150 | return SerializeColorSequence(value); 151 | } else if (typeIs(value, "NumberSequenceKeypoint")) { 152 | return SerializeNumberSequenceKeypoint(value); 153 | } else if (typeIs(value, "NumberSequence")) { 154 | return SerializeNumberSequence(value); 155 | } else if (typeIs(value, "number") || typeIs(value, "string") || typeIs(value, "boolean")) { 156 | return value; 157 | } else if (typeIs(value, "table")) { 158 | return HttpService.JSONEncode(value); 159 | } else if (typeIs(value, "nil")) { 160 | return undefined; 161 | } else { 162 | throw `Destructuring of '${typeOf(value)}' not supported by Serializer`; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /.yalc/roblox-ts-luau/out/commands/init.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | var yargs_1 = __importDefault(require("yargs")); 42 | var prompts_1 = __importDefault(require("prompts")); 43 | var fs_extra_1 = __importDefault(require("fs-extra")); 44 | var identity_1 = require("../identity"); 45 | var path_1 = __importDefault(require("path")); 46 | var hjson_1 = __importDefault(require("hjson")); 47 | var project_1 = require("../project"); 48 | function cleanName(name) { 49 | return name.replace("@", "").replace("/", "-"); 50 | } 51 | function init(argv) { 52 | var _a; 53 | return __awaiter(this, void 0, void 0, function () { 54 | var cwd, workingPath, distPath, tsconfig, packageJson, packageInfo, tsconfigInfo, wallyConfigs, fullName, configuration, rojoConfig, jsonConfig; 55 | return __generator(this, function (_b) { 56 | switch (_b.label) { 57 | case 0: 58 | cwd = path_1.default.join(process.cwd(), argv.rootPath); 59 | workingPath = path_1.default.join(cwd, argv.luauDir); 60 | distPath = path_1.default.join(workingPath, "dist"); 61 | tsconfig = path_1.default.join(cwd, "tsconfig.json"); 62 | packageJson = path_1.default.join(cwd, "package.json"); 63 | if (!fs_extra_1.default.existsSync(packageJson)) { 64 | return [2 /*return*/]; 65 | } 66 | if (!fs_extra_1.default.existsSync(tsconfig)) { 67 | return [2 /*return*/]; 68 | } 69 | packageInfo = require(packageJson); 70 | tsconfigInfo = hjson_1.default.parse(fs_extra_1.default.readFileSync(tsconfig).toString()); 71 | fs_extra_1.default.ensureDirSync(distPath); 72 | if (!fs_extra_1.default.pathExistsSync(workingPath)) { 73 | fs_extra_1.default.mkdirSync(workingPath); 74 | } 75 | return [4 /*yield*/, prompts_1.default([ 76 | { 77 | name: "publishName", 78 | type: "text", 79 | message: "What github username do you want to publish the package under?", 80 | }, 81 | { 82 | name: "packageName", 83 | type: "text", 84 | message: "What do you want to name the package?", 85 | initial: (_a = argv.packageName) !== null && _a !== void 0 ? _a : cleanName(packageInfo.name), 86 | }, 87 | ])]; 88 | case 1: 89 | wallyConfigs = _b.sent(); 90 | fullName = wallyConfigs.publishName + "/" + wallyConfigs.packageName; 91 | configuration = identity_1.identity({ 92 | wally: { 93 | username: wallyConfigs.publishName, 94 | packageName: wallyConfigs.packageName, 95 | license: packageInfo.license, 96 | registry: "https://github.com/upliftgames/wally-index", 97 | authors: [wallyConfigs.publishName], 98 | realm: "shared", 99 | description: packageInfo.description, 100 | }, 101 | build: { 102 | outDir: tsconfigInfo.compilerOptions.outDir, 103 | }, 104 | }); 105 | rojoConfig = identity_1.identity({ 106 | name: wallyConfigs.packageName, 107 | tree: { 108 | $path: ".", 109 | }, 110 | }); 111 | jsonConfig = { quotes: "all", separator: true, space: "\t", bracesSameLine: true }; 112 | fs_extra_1.default.writeFileSync(path_1.default.join(cwd, "luau-config.json"), hjson_1.default.stringify(configuration, jsonConfig)); 113 | fs_extra_1.default.writeFileSync(path_1.default.join(distPath, "default.project.json"), hjson_1.default.stringify(rojoConfig, jsonConfig)); 114 | project_1.generateWallyToml(configuration.wally, packageInfo.version, path_1.default.join(distPath, "wally.toml")); 115 | return [2 /*return*/]; 116 | } 117 | }); 118 | }); 119 | } 120 | module.exports = identity_1.identity({ 121 | command: "init", 122 | describe: "Setup Luau project", 123 | builder: function () { 124 | return yargs_1.default 125 | .option("rootPath", { 126 | type: "string", 127 | describe: "The path of your project - defaults to current directory", 128 | default: ".", 129 | }) 130 | .option("luauDir", { 131 | default: "luau", 132 | describe: "The name of the Luau directory", 133 | type: "string", 134 | }) 135 | .option("packageName", { 136 | describe: "The name of the package", 137 | type: "string", 138 | }); 139 | }, 140 | handler: function (argv) { return init(argv); }, 141 | }); 142 | -------------------------------------------------------------------------------- /luau/dist/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Luau exports file for Log 3 | ]] 4 | type void = nil 5 | type LogEnricher = {} 6 | 7 | type LogEvent = { 8 | Level: LogLevel, 9 | Timestamp: string, 10 | SourceContext: string?, 11 | Template: string, 12 | [string]: any?, 13 | } 14 | type LogLevel = number 15 | type LogSink = { 16 | Emit: (self: LogSink, message: LogEvent) -> void 17 | } 18 | 19 | type Constructor = { new: (...any) -> any } 20 | type ToString = { toString: () -> string } 21 | type LoggerContext = Constructor | ToString | Instance 22 | 23 | type Logger = { 24 | Write: (self: Logger, logLevel: LogLevel, template: string, ...any) -> void, 25 | Info: (self: Logger, template: string, ...any) -> void, 26 | Warn: (self: Logger, template: string, ...any) -> void, 27 | Error: (self: Logger, template: string, ...any) -> string, 28 | Fatal: (self: Logger, template: string, ...any) -> string, 29 | Debug: (self: Logger, template: string, ...any) -> void, 30 | Verbose: (self: Logger, template: string, ...any) -> void, 31 | ForContext: (self: Logger, context: LoggerContext) -> Logger, 32 | ForScript: (self: Logger) -> Logger, 33 | ForFunction: (self: Logger, fn: () -> void) -> Logger, 34 | ForProperty: (self: Logger, name: string, value: any) -> Logger, 35 | } 36 | type LogConfiguration = { 37 | WriteTo: (self: LogConfiguration, sink: LogSink, configure: ((sink: LogSink) -> void)?) -> LogConfiguration, 38 | WriteToCallback: (self: LogConfiguration, sinkCallback: () -> void) -> LogConfiguration, 39 | Enrich: (self: LogConfiguration, enricher: LogEnricher) -> LogConfiguration, 40 | EnrichWithProperty: (self: LogConfiguration, propertyName: string, value: any) -> LogConfiguration, 41 | SetMinLogLevel: (self: LogConfiguration, minLogLevel: LogLevel) -> LogConfiguration, 42 | Create: (self: LogConfiguration) -> Logger 43 | } 44 | 45 | type LogLevelEnum = { 46 | [LogLevel]: string, 47 | Verbose: LogLevel, 48 | Debugging: LogLevel, 49 | Information: LogLevel, 50 | Warning: LogLevel, 51 | Error: LogLevel, 52 | Fatal: LogLevel 53 | } 54 | 55 | type LogNamespace = { 56 | RobloxOutput: () -> LogSink, 57 | SetLogger: (logger: Logger) -> void, 58 | Default: () -> Logger, 59 | Create: () -> LogConfiguration, 60 | Level: LogLevelEnum, 61 | 62 | Info: (template: string, ...any) -> void, 63 | Warn: (template: string, ...any) -> void, 64 | Error: (template: string, ...any) -> string, 65 | Fatal: (template: string, ...any) -> string, 66 | Debug: (template: string, ...any) -> void, 67 | Verbose: (template: string, ...any) -> void, 68 | ForContext: (context: LoggerContext) -> Logger, 69 | ForScript: () -> Logger, 70 | ForFunction: (fn: () -> void) -> Logger, 71 | ForProperty: (name: string, value: any) -> Logger, 72 | } 73 | export type Log = LogNamespace; 74 | 75 | -- Compiled with roblox-ts v1.3.3 76 | local TS = require(script.TS.RuntimeLib) 77 | local exports = {} 78 | local LogEventRobloxOutputSink = TS.import(script, script, "Core", "LogEventRobloxOutputSink").LogEventRobloxOutputSink 79 | local Logger = TS.import(script, script, "Logger").Logger 80 | exports.Logger = TS.import(script, script, "Logger").Logger 81 | exports.LogLevel = TS.import(script, script, "Core").LogLevel 82 | local Log = {} 83 | do 84 | local _container = Log 85 | local defaultLogger = Logger:default() 86 | local function SetLogger(logger) 87 | defaultLogger = logger 88 | end 89 | _container.SetLogger = SetLogger 90 | local function Default() 91 | return defaultLogger 92 | end 93 | _container.Default = Default 94 | --[[ 95 | * 96 | * Configure a custom logger 97 | ]] 98 | local function Configure() 99 | return Logger:configure() 100 | end 101 | _container.Configure = Configure 102 | --[[ 103 | * 104 | * Creates a custom logger 105 | * @returns The logger configuration, use `Initialize` to get the logger once configured 106 | * @deprecated Use {@link Configure}. This will be removed in future. 107 | ]] 108 | local Create = Configure 109 | _container.Create = Create 110 | --[[ 111 | * 112 | * The default roblox output sink 113 | * @param options Options for the sink 114 | ]] 115 | local RobloxOutput = function(options) 116 | if options == nil then 117 | options = {} 118 | end 119 | return LogEventRobloxOutputSink.new(options) 120 | end 121 | _container.RobloxOutput = RobloxOutput 122 | --[[ 123 | * 124 | * Write a "Fatal" message to the default logger 125 | * @param template 126 | * @param args 127 | ]] 128 | local function Fatal(template, ...) 129 | local args = { ... } 130 | return defaultLogger:Fatal(template, unpack(args)) 131 | end 132 | _container.Fatal = Fatal 133 | --[[ 134 | * 135 | * Write a "Verbose" message to the default logger 136 | * @param template 137 | * @param args 138 | ]] 139 | local function Verbose(template, ...) 140 | local args = { ... } 141 | defaultLogger:Verbose(template, unpack(args)) 142 | end 143 | _container.Verbose = Verbose 144 | --[[ 145 | * 146 | * Write an "Information" message to the default logger 147 | * @param template 148 | * @param args 149 | ]] 150 | local function Info(template, ...) 151 | local args = { ... } 152 | defaultLogger:Info(template, unpack(args)) 153 | end 154 | _container.Info = Info 155 | --[[ 156 | * 157 | * Write a "Debugging" message to the default logger 158 | * @param template 159 | * @param args 160 | ]] 161 | local function Debug(template, ...) 162 | local args = { ... } 163 | defaultLogger:Debug(template, unpack(args)) 164 | end 165 | _container.Debug = Debug 166 | --[[ 167 | * 168 | * Write a "Warning" message to the default logger 169 | * @param template 170 | * @param args 171 | ]] 172 | local function Warn(template, ...) 173 | local args = { ... } 174 | defaultLogger:Warn(template, unpack(args)) 175 | end 176 | _container.Warn = Warn 177 | --[[ 178 | * 179 | * Write an "Error" message to the default logger 180 | * @param template 181 | * @param args 182 | ]] 183 | local function Error(template, ...) 184 | local args = { ... } 185 | return defaultLogger:Error(template, unpack(args)) 186 | end 187 | _container.Error = Error 188 | --[[ 189 | * 190 | * Creates a logger that enriches log events with the specified context as the property `SourceContext`. 191 | * @param context The tag to use 192 | ]] 193 | local function ForContext(context, contextConfiguration) 194 | return defaultLogger:ForContext(context, contextConfiguration) 195 | end 196 | _container.ForContext = ForContext 197 | --[[ 198 | * 199 | * Creates a logger that nriches log events with the specified property 200 | * @param name The name of the property 201 | * @param value The value of the property 202 | ]] 203 | local function ForProperty(name, value) 204 | return defaultLogger:ForProperty(name, value) 205 | end 206 | _container.ForProperty = ForProperty 207 | --[[ 208 | * 209 | * Creates a logger that enriches log events with the specified properties 210 | * @param props The properties 211 | ]] 212 | local function ForProperties(props) 213 | return defaultLogger:ForProperties(props) 214 | end 215 | _container.ForProperties = ForProperties 216 | --[[ 217 | * 218 | * Creates a logger that enriches log events with the `SourceContext` as the containing script 219 | ]] 220 | local function ForScript(scriptContextConfiguration) 221 | -- Unfortunately have to duplicate here, since `debug.info`. 222 | local s = debug.info(2, "s") 223 | local copy = defaultLogger:Copy() 224 | local _result = scriptContextConfiguration 225 | if _result ~= nil then 226 | _result(copy) 227 | end 228 | return copy:EnrichWithProperties({ 229 | SourceContext = s, 230 | SourceKind = "Script", 231 | }):Create() 232 | end 233 | _container.ForScript = ForScript 234 | --[[ 235 | * 236 | * Set the minimum log level for the default logger 237 | ]] 238 | local function SetMinLogLevel(logLevel) 239 | defaultLogger:SetMinLogLevel(logLevel) 240 | end 241 | _container.SetMinLogLevel = SetMinLogLevel 242 | --[[ 243 | * 244 | * Creates a logger that enriches log events with `SourceContext` as the specified function 245 | ]] 246 | local function ForFunction(func, funcContextConfiguration) 247 | return defaultLogger:ForFunction(func, funcContextConfiguration) 248 | end 249 | _container.ForFunction = ForFunction 250 | end 251 | 252 | Log.LogLevel = exports.LogLevel 253 | Log.Logger = exports.Logger 254 | return Log :: Log -------------------------------------------------------------------------------- /.yalc/roblox-ts-luau/out/commands/build.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __importDefault = (this && this.__importDefault) || function (mod) { 39 | return (mod && mod.__esModule) ? mod : { "default": mod }; 40 | }; 41 | var yargs_1 = __importDefault(require("yargs")); 42 | var fs_extra_1 = __importDefault(require("fs-extra")); 43 | var identity_1 = require("../identity"); 44 | var path_1 = __importDefault(require("path")); 45 | var hjson_1 = __importDefault(require("hjson")); 46 | var execa_1 = __importDefault(require("execa")); 47 | var util_1 = __importDefault(require("util")); 48 | var project_1 = require("../project"); 49 | var copy_1 = __importDefault(require("copy")); 50 | var copy = util_1.default.promisify(copy_1.default); 51 | function build(argv) { 52 | var _a; 53 | return __awaiter(this, void 0, void 0, function () { 54 | var rootPath, luauPath, luauDistPath, luauOutputPath, luauArtefactPath, config, configuration, packageJson, projectPath, result, copyCallback, libPath, _i, _b, packageName; 55 | return __generator(this, function (_c) { 56 | switch (_c.label) { 57 | case 0: 58 | rootPath = path_1.default.join(process.cwd(), argv.rootPath); 59 | luauPath = path_1.default.join(rootPath, argv.luauDir); 60 | luauDistPath = path_1.default.join(luauPath, "dist"); 61 | luauOutputPath = path_1.default.join(luauPath, "out"); 62 | luauArtefactPath = path_1.default.join(luauPath, "artefacts"); 63 | config = path_1.default.join(rootPath, "luau-config.json"); 64 | if (!fs_extra_1.default.existsSync(config)) { 65 | return [2 /*return*/]; 66 | } 67 | configuration = hjson_1.default.parse(fs_extra_1.default.readFileSync(config).toString()); 68 | packageJson = require(path_1.default.join(rootPath, "package.json")); 69 | console.log("generating wally.toml..."); 70 | project_1.generateWallyToml(configuration.wally, packageJson.version, path_1.default.join(luauDistPath, "wally.toml")); 71 | projectPath = path_1.default.join(luauPath, "build.project.json"); 72 | if (!fs_extra_1.default.existsSync(projectPath)) { 73 | return [2 /*return*/]; 74 | } 75 | result = execa_1.default.command("rbxtsc --verbose --type=model --rojo=\"" + projectPath + "\""); 76 | (_a = result.stdout) === null || _a === void 0 ? void 0 : _a.on("data", function (data) { 77 | process.stdout.write("" + data); 78 | }); 79 | return [4 /*yield*/, result]; 80 | case 1: 81 | _c.sent(); 82 | console.log("compiled " + configuration.wally.packageName); 83 | copyCallback = function (prefix) { 84 | if (prefix === void 0) { prefix = "emit"; } 85 | return function (err, files) { 86 | if (files) { 87 | for (var _i = 0, files_1 = files; _i < files_1.length; _i++) { 88 | var file = files_1[_i]; 89 | console.log(prefix, file.path); 90 | } 91 | } 92 | if (err) { 93 | console.error(err); 94 | } 95 | }; 96 | }; 97 | console.log("copying output files..."); 98 | libPath = path_1.default.join(luauOutputPath, "lib"); 99 | return [4 /*yield*/, copy(path_1.default.join(rootPath, configuration.build.outDir) + "/**/*.lua", libPath)]; 100 | case 2: 101 | _c.sent(); 102 | return [4 /*yield*/, copy(luauDistPath + "/*.*", luauOutputPath)]; 103 | case 3: 104 | _c.sent(); 105 | return [4 /*yield*/, copy(rootPath + "/include/*.lua", path_1.default.join(libPath, "TS"))]; 106 | case 4: 107 | _c.sent(); 108 | _i = 0, _b = Object.entries(packageJson.dependencies); 109 | _c.label = 5; 110 | case 5: 111 | if (!(_i < _b.length)) return [3 /*break*/, 8]; 112 | packageName = _b[_i][0]; 113 | return [4 /*yield*/, copy(rootPath + "/node_modules/" + packageName + "/**/*.lua", path_1.default.join(libPath, "TS", packageName))]; 114 | case 6: 115 | _c.sent(); 116 | _c.label = 7; 117 | case 7: 118 | _i++; 119 | return [3 /*break*/, 5]; 120 | case 8: 121 | fs_extra_1.default.ensureDir(luauArtefactPath); 122 | execa_1.default.commandSync("wally package --project-path " + luauOutputPath + " --output " + path_1.default.join(luauArtefactPath, configuration.wally.packageName + "-luau.zip")); 123 | execa_1.default.commandSync("rojo build " + projectPath + " --output " + path_1.default.join(luauArtefactPath, configuration.wally.packageName + "-luau.rbxm")); 124 | if (argv.publish) { 125 | execa_1.default.commandSync("wally publish --project-path " + luauOutputPath); 126 | } 127 | return [2 /*return*/]; 128 | } 129 | }); 130 | }); 131 | } 132 | module.exports = identity_1.identity({ 133 | command: "build", 134 | describe: "Build Luau project", 135 | builder: function () { 136 | return yargs_1.default 137 | .option("rootPath", { 138 | type: "string", 139 | describe: "The path of your project - defaults to current directory", 140 | default: ".", 141 | }) 142 | .option("luauDir", { 143 | default: "luau", 144 | describe: "The name of the Luau directory", 145 | type: "string", 146 | }) 147 | .option("publish", { 148 | type: "boolean", 149 | default: false, 150 | }); 151 | }, 152 | handler: function (argv) { return build(argv); }, 153 | }); 154 | -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | import { MessageTemplateParser } from "@rbxts/message-templates/out/MessageTemplateParser"; 2 | import { DestructureMode, PropertyToken, TemplateTokenKind } from "@rbxts/message-templates/out/MessageTemplateToken"; 3 | import { LogLevel, ILogEventEnricher, ILogEventSink, LogEvent } from "./Core"; 4 | import { LogConfiguration } from "./Configuration"; 5 | import { PlainTextMessageTemplateRenderer } from "@rbxts/message-templates"; 6 | import { RbxSerializer } from "@rbxts/message-templates/out/RbxSerializer"; 7 | import { EnforceUserKey, UserDefinedLogProperties } from "./Core/TypeUtils"; 8 | 9 | export class Logger { 10 | private sinks: ReadonlyArray; 11 | private enrichers: ReadonlyArray; 12 | 13 | private logLevel: LogLevel = LogLevel.Information; 14 | private constructor() { 15 | this.sinks = []; 16 | this.enrichers = []; 17 | } 18 | 19 | public static configure() { 20 | return new LogConfiguration(new Logger()); 21 | } 22 | 23 | /** @internal */ 24 | public SetSinks(sinks: ReadonlyArray) { 25 | this.sinks = sinks; 26 | } 27 | 28 | /** @internal */ 29 | public SetEnrichers(enrichers: ReadonlyArray) { 30 | this.enrichers = enrichers; 31 | } 32 | 33 | /** 34 | * Set the minimum log level for this logger 35 | */ 36 | public SetMinLogLevel(logLevel: LogLevel) { 37 | this.logLevel = logLevel; 38 | } 39 | 40 | private static defaultLogger = new Logger(); 41 | public static default() { 42 | return this.defaultLogger; 43 | } 44 | 45 | private _serializeValue(value: defined): defined { 46 | if (typeIs(value, "Vector3")) { 47 | return { X: value.X, Y: value.Y, Z: value.Z }; 48 | } else if (typeIs(value, "Vector2")) { 49 | return { X: value.X, Y: value.Y }; 50 | } else if (typeIs(value, "Instance")) { 51 | return value.GetFullName(); 52 | } else if (typeIs(value, "EnumItem")) { 53 | return tostring(value); 54 | } else if ( 55 | typeIs(value, "string") || 56 | typeIs(value, "number") || 57 | typeIs(value, "boolean") || 58 | typeIs(value, "table") 59 | ) { 60 | return value; 61 | } else { 62 | return tostring(value); 63 | } 64 | } 65 | 66 | /** 67 | * Writes a log event 68 | * @param logLevel The log level of this event 69 | * @param template The template message using the [Message Templates](https://messagetemplates.org/) spec. 70 | * @param args The values to apply to the template 71 | * @returns The message formatted using the `PlainTextMessageTemplateRenderer` 72 | */ 73 | public Write(logLevel: LogLevel, template: string, ...args: unknown[]) { 74 | const message: Writable = { 75 | Level: logLevel, 76 | SourceContext: undefined, 77 | Template: template, 78 | Timestamp: DateTime.now().ToIsoDate(), 79 | }; 80 | 81 | const tokens = MessageTemplateParser.GetTokens(template); 82 | const propertyTokens = tokens.filter((t): t is PropertyToken => t.kind === TemplateTokenKind.Property); 83 | 84 | let idx = 0; 85 | for (const token of propertyTokens) { 86 | const arg = args[idx++]; 87 | 88 | if (idx <= args.size()) { 89 | if (arg !== undefined) { 90 | if (token.destructureMode === DestructureMode.ToString) { 91 | message[token.propertyName] = tostring(arg); 92 | } else { 93 | message[token.propertyName] = typeIs(arg, "table") ? arg : RbxSerializer.Serialize(arg); 94 | } 95 | } 96 | } 97 | } 98 | 99 | for (const enricher of this.enrichers) { 100 | const toApply = new Map(); 101 | enricher.Enrich(message, toApply); 102 | for (const [key, value] of toApply) { 103 | message[key] = typeIs(value, "table") ? value : RbxSerializer.Serialize(value); 104 | } 105 | } 106 | 107 | for (const sink of this.sinks) { 108 | sink.Emit(message); 109 | } 110 | 111 | return new PlainTextMessageTemplateRenderer(tokens).Render(message); 112 | } 113 | 114 | /** 115 | * Returns the log level of this logger 116 | */ 117 | public GetLevel() { 118 | return this.logLevel; 119 | } 120 | 121 | /** 122 | * Writes a verbose message to this log stream 123 | * @param template The template message using the [Message Templates](https://messagetemplates.org/) spec. 124 | * @param args The values to apply to the template 125 | */ 126 | public Verbose(template: string, ...args: unknown[]) { 127 | if (this.GetLevel() > LogLevel.Verbose) return; 128 | this.Write(LogLevel.Verbose, template, ...args); 129 | } 130 | 131 | /** 132 | * Writes an information message to this log stream 133 | * @param template The template message using the [Message Templates](https://messagetemplates.org/) spec. 134 | * @param args The values to apply to the template 135 | */ 136 | public Info(template: string, ...args: unknown[]) { 137 | if (this.GetLevel() > LogLevel.Information) return; 138 | this.Write(LogLevel.Information, template, ...args); 139 | } 140 | 141 | /** 142 | * Writes a debug message to this log stream 143 | * @param template The template message using the [Message Templates](https://messagetemplates.org/) spec. 144 | * @param args The values to apply to the template 145 | */ 146 | public Debug(template: string, ...args: unknown[]) { 147 | if (this.GetLevel() > LogLevel.Debugging) return; 148 | this.Write(LogLevel.Debugging, template, ...args); 149 | } 150 | 151 | /** 152 | * Writes a warning message to this log stream 153 | * @param template The template message using the [Message Templates](https://messagetemplates.org/) spec. 154 | * @param args The values to apply to the template 155 | */ 156 | public Warn(template: string, ...args: unknown[]) { 157 | if (this.GetLevel() > LogLevel.Warning) return; 158 | this.Write(LogLevel.Warning, template, ...args); 159 | } 160 | 161 | /** 162 | * Writes a error message to this log stream 163 | * @param template The template message using the [Message Templates](https://messagetemplates.org/) spec. 164 | * @param args The values to apply to the template 165 | */ 166 | public Error(template: string, ...args: unknown[]) { 167 | if (this.GetLevel() > LogLevel.Error) return; 168 | return this.Write(LogLevel.Error, template, ...args); 169 | } 170 | 171 | /** 172 | * Writes a fatal message to this log stream 173 | * @param template The template message using the [Message Templates](https://messagetemplates.org/) spec. 174 | * @param args The values to apply to the template 175 | */ 176 | public Fatal(template: string, ...args: unknown[]) { 177 | return this.Write(LogLevel.Fatal, template, ...args); 178 | } 179 | 180 | /** 181 | * Creates a copy of this logger, and allows you to configure it 182 | * @returns The configuration for a copy of this logger 183 | */ 184 | public Copy() { 185 | const config = new LogConfiguration(new Logger()); 186 | config.SetMinLogLevel(this.GetLevel()); 187 | for (const sink of this.sinks) { 188 | config.WriteTo(sink); 189 | } 190 | for (const enricher of this.enrichers) { 191 | config.Enrich(enricher); 192 | } 193 | return config; 194 | } 195 | 196 | /** 197 | * Creates a logger that enriches log events with the specified context as the property `SourceContext`. 198 | * 199 | * ```ts 200 | * class MyService { 201 | * private logger = Log.ForContext(MyService); 202 | * public exampleFunction() { 203 | * // Sets `SourceContext` of this to `MyService@[SourceFile]`. 204 | * this.logger.Info("Hello from exampleFunction!"); 205 | * } 206 | * } 207 | * ``` 208 | * 209 | * @param context The tag to use 210 | * @param contextConfiguration Configurator for this contextual logger 211 | */ 212 | public ForContext( 213 | context: LoggerContext, 214 | contextConfiguration?: (configuration: Omit) => void, 215 | ) { 216 | const copy = this.Copy(); 217 | 218 | let sourceContext: string; 219 | if (typeIs(context, "Instance")) { 220 | sourceContext = context.GetFullName(); 221 | } else { 222 | sourceContext = tostring(context); 223 | } 224 | 225 | contextConfiguration?.(copy); 226 | return copy 227 | .EnrichWithProperties({ 228 | SourceContext: sourceContext, 229 | SourceKind: "Context", 230 | }) 231 | .Create(); 232 | } 233 | 234 | /** 235 | * Creates a logger that enriches log events with the `SourceContext` as the containing script 236 | * @param scriptContextConfiguration The configuration for this contextual logger 237 | */ 238 | public ForScript(scriptContextConfiguration?: (configuration: Omit) => void) { 239 | const [s] = debug.info(2, "s"); 240 | const copy = this.Copy(); 241 | scriptContextConfiguration?.(copy); 242 | return copy 243 | .EnrichWithProperties({ 244 | SourceContext: s, 245 | SourceKind: "Script", 246 | }) 247 | .Create(); 248 | } 249 | 250 | /** 251 | * Creates a logger that enriches log events with `SourceContext` as the specified function 252 | */ 253 | public ForFunction( 254 | func: () => void, 255 | funcContextConfiguration?: (configuration: Omit) => void, 256 | ) { 257 | const [funcName, funcLine, funcSource] = debug.info(func, "nls"); 258 | const copy = this.Copy(); 259 | funcContextConfiguration?.(copy); 260 | return copy 261 | .EnrichWithProperties({ 262 | SourceContext: `function '${funcName ?? "(anonymous)"}'`, 263 | SourceLine: funcLine, 264 | SourceFile: funcSource, 265 | SourceKind: "Function", 266 | }) 267 | .Create(); 268 | } 269 | 270 | /** 271 | * Creates a logger that enriches log events with the specified property 272 | * @param name The name of the property 273 | * @param value The value of the property 274 | */ 275 | public ForProperty(name: K, value: UserDefinedLogProperties[K]) { 276 | return this.Copy().EnrichWithProperty(name, value).Create(); 277 | } 278 | 279 | /** 280 | * Creates a logger that enriches log events with the specified properties 281 | * @param props The properties 282 | */ 283 | public ForProperties(props: TProps) { 284 | return this.Copy().EnrichWithProperties(props).Create(); 285 | } 286 | } 287 | 288 | export type LoggerContext = Instance | (new (...args: any[]) => any) | { toString(): string }; 289 | --------------------------------------------------------------------------------