Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Sapphire directory and automatic TypeScript augmentation support #536

Open
kyranet opened this issue Sep 19, 2022 · 0 comments
Open
Assignees
Labels
Milestone

Comments

@kyranet
Copy link
Member

kyranet commented Sep 19, 2022

Preface: this RFC (Request For Comments) is meant to gather feedback and opinions for the feature set and some implementation details for @sapphire/framework! While a lot of the things mentioned in this RFC will be opinionated from my side (like what I’d like to see implemented in the module), not everything might be added in the initial version or some extra things might be added as extras.

What is the Sapphire directory?

Some frameworks, such as Nuxt (.nuxt) and Next.js (.next) define an autogenerated directory, this contains types, metadata, and some other artifacts that greatly enhance the user experience. We can follow their convention and implement .sapphire, but we also may use node_modules/.sapphire instead, which is what Prisma does with node_modules/.prisma/client, which seems more suitable for bridge code, explained later in this proposal.

Since Sapphire Framework is a very strict typed library that aims for type safety to avoid silly mistakes such as typos, we require some of the stores to be augmented into the framework's types, this is remarked for preconditions and arguments, however, for ease of use, commands and listeners are not included, which leads to inconsistent type safety, this is done mostly because manually defining a list of commands (and aliases) can be incredibly bothersome and prone to issues.

An ideal solution to this would be similar to Nuxt v3's auto-imports, which generates a TypeScript Definitions file (.d.ts).

CLI

This utility can be integrated with the official Sapphire CLI, and further enhance it since it won't be limited to a single file.

For simplicity, I believe it may be worth checking whether or not we can bring a part of the CLI inside Framework. And if not, a way to extend the CLI with extra packages, so installing @sapphire/cli-examples gives us sapphire examples. We may need the core built-in because of sapphire generate, which will be required for generating bridge and auto-augmentation code.

Large type presets

Big words! The numbers Mason, what do they mean?

Remember when we said Sapphire v2 would be library agnostic? We're at v3 now, and v4 doesn't look like it'll be. Instead, we're attaching to Discord.js more than ever. This harms our future ability to detach and support raw libraries (@discordjs/ws, @discordjs/rest), other libraries (eris), ecosystems (HTTP), or even runtimes (Deno, CF Workers...). And we're doing this deliberately.

But, what EXACTLY is stopping us from being library agnostic?

Types.

Back in v1, when we were the closest we ever were to library agnosticity, Sapphire was straight incredibly low-level and required a lot of usage gotchas, it wasn't your typical framework. Frameworks are made to simplify things, but as far as v1 went, it felt more like an enterprise library that needed to be extended with one's own micro-framework. This is not what users want to do, and is why v2 is designed the way it was.

How would the Sapphire directory fix this?

Simple, not only it can augment the framework with auto-generated user types, it can also solve the aforementioned issues by automatically injecting compatible code and types as a bridge. Imagine sapphire.config.js, where we define library: 'eris', and it changes the Sapphire bridge to use Eris's types instead of Discord.js's. Pieces such as preconditions and arguments would also be replaced to use Eris-compatible pieces rather than Discord.js-compatible ones, and the bridge's automatic type augmentations simplifies many things even further by ensuring that the user doesn't have to do import '@sapphire/framework/register-eris'; or similar to have the types registered, which also would have the gotcha of requiring the file first before anything else.

Where would the presets be at?

We can make packages such as @sapphire/framework-preset-discord.js that include the Discord.js-compatible pieces, as well as bridge types.

Alternative Sapphire directory

We may use node_modules/.sapphire if the bridge code is injected here (most likely will be), but we can alternatively also use .sapphire at the root directory. This can be configurable too.

Compatibility matrix

Just like we can define library in the sapphire.config.js, we can also define the runtime, accepting 'node' | 'deno' and others (such as 'cloudflare-workers'), but we might also want to add a compatibility matrix so the libraries that can be used per runtime are limited, e.g. library: 'discord.js' may not work with runtime: 'cloudflare-workers'.

Command modes

Sapphire v3 features a very large set of types designed to support both message-based commands and slash commands, however, this bloats IntelliSense and makes components such as preconditions less type-safe since we have to define the handlers as optional rather than abstract. There is AllFlowsPrecondition but it's more of an unsafe workaround, as the methods may be renamed and TypeScript won't warn the user about it, resulting on poorer safety. With the conditional type system (which also works like Rust's #[cfg(feature = "...")] at this point), we can define which methods should be abstract by defining an array (commands: ['messages', 'chat-input', 'context-menu']). This feature would also extend to commands.

Enabling 'messages' in this option will also make message-command-listeners be loaded, without extra code.

Disabling 'chat-input' would make Framework not load the chat-input directory, likewise 'context-menu' with the context-menu one.

Conditional loading

Sapphire v3 defines default error listeners which can be opt-out, however, not all users want to disable all the listeners, just a few.

Configuring this should be with load.optional.listeners.errors: boolean | ('ChatInputCommand' | 'CommandApplicationCommandRegistry')[]..., omitting the Core prefix and Error suffix.

For non-optional, we can define load.arguments, load.listeners, and load.preconditions, both taking either a boolean or an array with the names of the pieces to load.

The config file

Referring to the sapphire.config.js, it may import defineSapphireConfiguration from @sapphire/framework or auto-import it, something that is possible with unjs/jiti used by Nuxt v3 RC10 to auto-import defineNuxtConfig in the configuration file.

Furthermore, we may also support:

  • sapphire.config.mts (ESM TypeScript)
  • sapphire.config.mjs (ESM JavaScript)
  • sapphire.config.cts (CJS TypeScript)
  • sapphire.config.cjs (CJS TypeScript)
  • sapphire.config.ts
  • sapphire.config.js
  • sapphire.config.yaml (should we? Transform plugin?)
  • sapphire.config.json (should we? Transform plugin?)

Defaults

The file's defaults may be the closest as v3's defaults for ease of migration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants