This monorepo implements a custom build system using a combination of tools:
- Yarn (for dependency management)
wsrun
(for running workspace scripts)- ESBuild (for transpilation, minifaction, and bundling of source codes)
- TypeScript (for static typing)
- ESLint (for TypeScript/JavaScript linting)
These tools work together based upon strong opinions around repo organization and architecture. Read on to learn about these ground rules and how they are managed.
ESBuild works in tandem with tsc
(TypeScript) to generate production-ready code bundles that are published to NPM, along with TypeScript definition files to support strongly-typed integrations with Magic JS packages.
A typical /dist
folder generated by the build system will look like this:
packages/**
└── dist/
├── cjs/
│ ├── index.js # CommonJS entrypoint (package.json#main)
│ └── index.js.map
│
├── es/
│ ├── index.js # ES entrypoint (package.json#module)
│ ├── index.js.map
│ ├── index.mjs # ES entrypoint using .mjs extension (package.json#exports#import)
│ └── index.mjs.map
│
├── react-native/
│ ├── index.native.js # React Native entrypoint (package.json#react-native)
│ └── index.native.js.map
│
├── types/
│ └── *.d.ts # TypeScript definitions (package.json#types)
│
└── some-cdn-bundle.js # CDN entrypoint (package.json#jsdelivr)
We generate multiple outputs in order to maximize backwards compatibility and interoperability across JS runtimes. These outputs are associated with entrypoints configured in package.json
. Here are the relevant configuration values, explained:
{
// One of: node | browser | neutral
//
// node: Targets Node-like runtimes; used for React Native & server-side
// packages (i.e.: `@magic-sdk/react-native-bare`)
//
// browser: Targets web-like runtimes; used for web-only packages
// (i.e.: `magic-sdk`)
//
// neutral: Targets universal runtimes; used for packages that are
// interoperable across React Native & web
// (i.e.: most `@magic-ext/*` packages)
"target": "...",
// If a CDN bundle should be required,
// this determines its global variable name
// (attached to `window` in a browser-based context)
"cdnGlobalName": "...",
// Output location for CJS entrypoint
"main": "./dist/cjs/index.js",
// Output location for ES entrypoint (using `.js` extensions)
"module": "./dist/es/index.js",
// Output location for TypeScript definition files
"types": "./dist/types/index.d.ts",
// Output location for CDN entrypoint
"jsdelivr": "./dist/my-library.js",
// Output location for React Native entrypoint
"react-native": "./dist/react-native/index.native.js",
// Here, we define a standard NodeJS "exports" field...
"exports": {
// Output location for ES entrypoint (using `.mjs` extensions)
"import": "./dist/es/index.mjs",
// Output location for CJS entrypoint
// (this should be exactly the same as the value defined in "main",
// it's repeated here for redundancy and backwards-compatibility reasons)
"require": "./dist/cjs/index.js"
}
}
The build system will choose which outputs to generate based upon the configuration specified (or omitted).
Each entrypoint type (CJS, ES, React Native, and CDN) can define a separate source file as its starting point for bundling, using a unique file identifier.
- CJS:
src/index.cjs.ts
- ES:
src/index.es.ts
- React Native:
src/index.native.ts
- CDN:
src/index.cdn.ts
All entrypoint types will fall back to src/index.ts
if a type-specific source file is undefined.
By default, "dependencies"
and "peerDependencies"
listed in package.json
are marked "external," meaning they won't be bundled, only referenced by the relevant module system (import
or require
). These defaults can be extended through the use of an "externals"
field in package.json
:
{
"externals": {
// Explicitly INCLUDE dependencies that are
// not otherwise externalized by default.
"include": [],
// Explicitly EXCLUDE dependencies that are
// otherwise externalized by default.
// Note: "exclude" takes precedence over "include".
"exclude": []
}
}
An exception to the default externals behavior applies to CDN-bundled packages, in which case all referenced dependencies are bundled regardless of the "exports"
field in package.json
. If a package depends upon magic-sdk
and/or @magic-sdk/commons
, these are not included in the CDN bundle and instead compiled to reference a global variable: window.Magic
. This supports a conventional CDN-based implementation in which consumers are expected to add a Magic SDK version themselves. For example:
<!-- `window.Magic` is assigned here! -->
<script src="https://auth.magic.link/sdk"></script>
<!-- `window.Magic` is referenced here! -->
<script src="https://auth.magic.link/sdk/extension/oauth"></script>
Monorepos tend to require lots of same-y boilerplate. The most common boilerplate in this repo can be found within packages/@magic-ext
to support a number of niche or case-specific Magic SDK extensions. Creating new extensions requires lots of copy/pasting and little tweaks that are prone to human error. So, we created a scaffolding script to make the process a cinch!
Simply run the following command, then answer a few interactive prompts:
yarn scaffold