close
Skip to main content

latest
Works with
It is unknown whether this package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
It is unknown whether this package works with Cloudflare Workers
It is unknown whether this package works with Node.js
It is unknown whether this package works with Deno
It is unknown whether this package works with Bun
It is unknown whether this package works with Browsers
JSR Score29%
License
MIT
Downloads1/wk
Published2 weeks ago (0.1.5)

GDQuest Toolbox

This is a very GDQuest-specific toolbox.

While many of the functions provided are standalone and may be copy-pasted (or used as dependencies), the main purpose of this project is to provide GDQuest with reusable tools.

This project may not respect Semver closely, and it is not recommended to depend on it if you're not GDQuest.

Some tooling may also be highly specific and useless outside of GDQuest's workflow.

How to get started

The toolbox uses Deno.

Use the toolbox in the library

To use it inside the library, it's enough to link it as a local dependency.

For server-side usage, it's fine to import indexers like mod.ts files directly, or to reference the entire library:

import { ...  } from "@gdquest/toolbox";

For client-side usage, it's recommended to import specific files, because it doesn't look like tree-shaking is very reliable.

import { ... } from "../toolbox/any/delay.ts";

I usually do this in a single dependencies.ts file, that acts like a manifest of all the toolbox dependencies. See clientDependencies.ts for an example.

Use the toolbox in the GDQuest dev environment

To use the toolbox in another project, you can import {x} from '@gdquest/toolbox'. But this will only work if you run Deno from the root of the dev env that contains the library and the other projects. Additionally, your editor must also have the root of the multi-project workspace open; and should support Deno workspaces.

Use the toolbox for local development

If you want to develop against a non-published version of the toolbox, you can link it locally. To do this, use this in the deno.json file of your new project:

// deno.json
{
    "imports": {
        "@gdquest/toolbox": "jsr:@gdquest/toolbox"
    },
    "links": ["../../library/toolbox"]
}

This is the preferred and recommended method.

Alternatively, you can specify a full path in an import map:

// link.json
{
  "imports": {
    "jsr:@gdquest/toolbox": "file:///path/to/toolbox/library/toolbox"
  }
}

And then use Deno's --import-map flag to use the import map:

deno run --import-map=link.json main.ts

This second method won't resolve npm modules or other modules needing an import map within the toolbox.

How to use the toolbox elsewhere

To use the toolbox without much complications, it must be published to a registry.

To publish the toolbox, run the following command:

deno run publish

This will publish the toolbox to the default registry (jsr). You need to be authenticated, and have the necessary permissions to publish packages.

You can then head to https://jsr.io/@gdquest/toolbox and copy the necessary snippets to use the toolbox in your own projects. In a Deno project, you can use:

import { ...  } from "jsr:@gdquest/toolbox";

Or use deno add to add the toolbox to your project:

deno add jsr:@gdquest/toolbox

You can then reference it in your project with just import { ... } from "@gdquest/toolbox";, without the jsr: specifier.

How to find the function you need

Unfortunately, at the moment it is complicated to search for the function you want. The naming may not be what you expect, so doing a search might not reveal it.

This would be partially helped if we added comments and testing to every part of the codebase, which is definitely something we could improve; but even then, it would only partially help discoverability.

We could consider adding a searchable auto-generated website with fuzzy search capabilities.

In the meantime, I would recommend taking some time to browse the codebase and learn what functions are available. It's ok to take a day reading the functions and understanding the structure. It's time well invested.

Structure

The project is organized into several directories that represent different contexts:

  • any: Scripts that can be used in any context: edge functions, clients, servers, etc.
  • cli: Scripts that can only be used server-side. Usually depend on Deno APIs, though in certain cases they may be used with other runtimes (Node.js, Bun, etc.)
  • 'content`: Content types and transforms, such as frontmatter checks
  • gdschool_build: CLI scripts related to building courses and other GDQuest materials.
  • supabase: Scripts related to Supabase
  • types: Types used elsewhere, as well as utility types (Exception: the Json type is provided by Supabase).
  • mail: Third-party libraries used by the project to send email and subscribe people to newsletters.
  • web: Scripts destined to run in the client.

the any directory also contains the createElement framework, which is used to render DOM elements in the server and browser both. In the case of such bundles of tools, the file name convention is <file>.web.ts or <file>.cli.ts to differentiate between the contexts.

Dependencies

External dependencies are to be kept minimal. Ideally, none. Consider each dependency as a liability.

They should be referenced in deno.jsonc. When they need to be re-exported, create a file in the appropriate namespace. For example, zod is re-exported in the /any/zod.ts file.

Try to only depend on Deno's stdlib where possible. If not, consider vendoring, or copying the relevant parts of a dependency (and simplifying it -- don't forget to credit the author though and to respect licenses).

Intent and goals

The problem:

  • Monolithic frameworks impose an architecture
  • Dependencies are a liability (supply chain attacks) and a maintenance burden

The solution: Instead, the toolbox aims to be a collection of small, reusable scripts.

Always look in here for common tasks before writing new code.

  • If you do not find what you need: consider adding units of code in the toolbox for reuse.
  • If you find something that almost fits your needs, but requires a variation:
    • If the code branching is minimal, add a flag
    • If the code branching is significant, or the change otherwise burdensom, consider adding a completely new function. Feel free to call it ..._alternate() or .._2() if you cannot find a better name.
  • If you find something that can be improved without changing its signature or output: improve it immediately.
  • If it requires breaking changes; open an issue.

Goals:

  • As few dependencies as possible. Dependencies are a liability.
  • A library made of mostly functions. Functions are composable at call site instead of requiring configuration.
  • Files that are mostly:
    • small: each functionality should be easy to vendor and change locally, per project. See below for more details.
    • self-contained and independent. We don't want to force to load the entire jungle for one banana.
  • Performance where it matters, and not focusing on it when it doesn't.

Non-Goals:

  • Catering to every possible use case. Make your functions precise, and do not hesitate to add a different function for a different use case. Two functions with distinct use cases are easier to maintain than one big function with branches. Yes, that does increase the breadth of the library, and makes functions harder to find.
  • Configuration: prefer function composition. Functions should have few arguments, and we don't want to keep a lot of state unless it is unavoidable.

What is a good size?

Not too big, not too small.

What's too small? For example, if your function is this:

export const sortByName = <T extends { name: string }>(arr: T[]) => arr.sort(
  (a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name)
);

Consider exporting only the important part, which is the comparator function:

export const compareByName = (a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name);

Or, more generic:

export const compareBy = <T>(key: keyof T) => (a: T, b: T) => {
  const aValue = a[key];
  const bValue = b[key];
  if (typeof aValue === 'string' && typeof bValue === 'string') {
    return aValue.localeCompare(bValue);
  }
  if (aValue < bValue) return -1;
  if (aValue > bValue) return 1;
  return 0;
};

What's too big? If you find yourself needing a lot of dependencies, then consider either:

  • Changing your API so that functions can be composed at call site
  • creating a new system that exposes a small API

For example, if you find yourself writing this:

export const processData = (data: Data) => {
  const transformed = transformData(data);
  const validated = validateData(transformed);
  const result = computeResult(validated);
  return result;
};

Consider exposing the individual steps as separate functions, and letting users compose them:

import { transformData, validateData, computeResult, pipe } from '@gdquest-toolbox';

export const processData = (data: Data) => pipe(
  data,
  transformData,
  validateData,
  computeResult
);

Good examples of "systems" are:

  • A templating system
  • An image processing system

And other systems that are inherently complex and have little value if broken down.

Performance?

Performance is not a primary concern, but if a function is on the hot path, then it should be optimized.

If that requires to change the signature, or otherwise reduce the capabilities of the function, then consider creating a new function with a different name; if you want to replace the older function, do a project-wide search and ensure it isn't used anywhere in the codebase (thankfully, this project isn't open source, so we do not care about breaking changes for external users).

If the change makes the function less readable, please comment on the unclear parts.

Performance changes should be measured, not estimated.

Report package

Please provide a reason for reporting this package. We will review your report and take appropriate action.

Please review the JSR usage policy before submitting a report.