diff --git a/.config/typedoc.workspace.js b/.config/typedoc.workspace.js
index 24a534591..178b47709 100644
--- a/.config/typedoc.workspace.js
+++ b/.config/typedoc.workspace.js
@@ -1,7 +1,6 @@
// @ts-check
const path = require('node:path');
-const fs = require('node:fs');
/**
* Generates `entryPoints` for TypeDoc for a workspace.
@@ -18,15 +17,12 @@ module.exports = function (cwd) {
/**
* @type {import('type-fest').PackageJson}
*/
- const pkgJson = JSON.parse(
- fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'),
- );
+ const pkgJson = require(path.join(cwd, 'package.json'));
+
const {exports} = pkgJson;
- if (!exports) {
- throw new TypeError('package.json does not have exports');
- }
- if (typeof exports !== 'object') {
- throw new TypeError('package.json exports is not an object');
+ if (!exports || typeof exports !== 'object') {
+ // should make typedoc barf
+ return {};
}
return {
@@ -48,5 +44,6 @@ module.exports = function (cwd) {
),
excludeInternal: true,
excludePrivate: true,
+ excludeExternals: true,
};
};
diff --git a/docs/index.html b/docs/index.html
deleted file mode 100644
index c2e56aa6e..000000000
--- a/docs/index.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
- Document
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/package.json b/package.json
index 656d8f83a..7060a1d8f 100644
--- a/package.json
+++ b/package.json
@@ -23,8 +23,7 @@
"build": "tsc -b",
"clean": "shx rm -rf \"packages/*/dist\" \"packages/*/*.tsbuildinfo\" \"packages/src/*.tsbuildinfo\" \"packages/*/test/*.tsbuildinfo\" \"packages/scripts/*.tsbuildinfo\"",
"dev": "tsc -b --watch",
- "dev:docs": "npx docsify-cli serve docs",
- "docs:build": "shx cp ./packages/midnight-smoker/README.md ./docs/README.md",
+ "dev:docs": "npm run -w packages/docs dev",
"husky-install": "husky install",
"lint": "run-p lint:eslint lint:md lint:deps",
"lint:commit": "commitlint",
diff --git a/packages/docs/.config/typedoc.json b/packages/docs/.config/typedoc.json
new file mode 100644
index 000000000..136b4dd60
--- /dev/null
+++ b/packages/docs/.config/typedoc.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://typedoc.org/schema.json",
+ "entryPoints": [
+ "../../midnight-smoker",
+ "../../plugin-default",
+ "../../test-util"
+ ],
+ "out": "../src/content/docs/api",
+ "entryPointStrategy": "packages",
+ "plugin": [
+ "typedoc-plugin-zod",
+ "typedoc-plugin-mdn-links",
+ "typedoc-plugin-markdown"
+ ],
+ "logLevel": "Info"
+}
diff --git a/packages/docs/LICENSE b/packages/docs/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/packages/docs/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/docs/astro.config.ts b/packages/docs/astro.config.ts
new file mode 100644
index 000000000..c0db6afa5
--- /dev/null
+++ b/packages/docs/astro.config.ts
@@ -0,0 +1,89 @@
+import type {RemarkPlugin} from '@astrojs/markdown-remark';
+
+import starlight from '@astrojs/starlight';
+import {defineConfig} from 'astro/config';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+import remarkCodeImport from 'remark-code-import';
+import starlightTypeDocPlugin, {typeDocSidebarGroup} from 'starlight-typedoc';
+
+import {rehypeAutolink} from './plugin/rehype-autolink';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const exampleDir = path.resolve(__dirname, '..', '..', 'example');
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [
+ starlight({
+ components: {
+ Footer: './src/components/Footer.astro',
+ },
+ customCss: ['./src/styles/main.css'],
+ logo: {
+ src: './src/assets/logo.svg',
+ },
+ plugins: [
+ starlightTypeDocPlugin({
+ entryPoints: [
+ '../midnight-smoker',
+ '../plugin-default',
+ '../test-util',
+ ],
+ sidebar: {
+ collapsed: true,
+ },
+ tsconfig: '../../tsconfig.json',
+ typeDoc: {
+ entryPointStrategy: 'packages',
+ hideGenerator: true,
+ plugin: ['typedoc-plugin-zod', 'typedoc-plugin-mdn-links'],
+ // broken in v4.0.0-next.38
+ // useCodeBlocks: true,
+ },
+ }),
+ ],
+ sidebar: [
+ {
+ items: [
+ {label: 'Getting Started', link: '/guides/getting-started'},
+ {label: 'General Usage', link: '/guides/usage'},
+ {label: 'Configuration', link: '/guides/config'},
+ {label: 'Using Custom Scripts', link: '/guides/custom-scripts'},
+ {label: 'Using Plugins', link: '/guides/plugins'},
+ ],
+ label: 'Guides',
+ },
+ {
+ items: [
+ {label: 'Overview', link: '/reference'},
+ {label: 'CLI', link: '/reference/cli'},
+ {label: 'Configuration', link: '/reference/config'},
+ {label: 'Rules', link: '/reference/rules'},
+ ],
+ label: 'Reference',
+ },
+ {
+ items: [
+ {label: 'Overview', link: '/developer'},
+ {label: 'Developing Plugins', link: '/developer/plugin-overview'},
+ {label: 'Creating Rules', link: '/developer/plugin-rule'},
+ {label: 'Creating Reporters', link: '/developer/plugin-reporter'},
+ ],
+ label: 'Development',
+ },
+ typeDocSidebarGroup,
+ ],
+ social: {
+ github: 'https://github.com/boneskull/midnight-smoker',
+ },
+ title: 'midnight-smoker',
+ }),
+ ],
+ markdown: {
+ rehypePlugins: [...rehypeAutolink()],
+ remarkPlugins: [[remarkCodeImport as RemarkPlugin, {rootDir: exampleDir}]],
+ },
+ site: 'https://boneskull.github.io/midnight-smoker',
+});
diff --git a/packages/docs/package.json b/packages/docs/package.json
new file mode 100644
index 000000000..91518cd34
--- /dev/null
+++ b/packages/docs/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@midnight-smoker/docs",
+ "version": "0.0.1",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "astro": "astro",
+ "build": "astro check && astro build",
+ "build:api": "typedoc",
+ "dev": "astro dev",
+ "preview": "astro preview",
+ "start": "astro dev"
+ },
+ "dependencies": {
+ "@astrojs/starlight": "0.16.0",
+ "astro": "4.2.4",
+ "hast": "1.0.0",
+ "hast-util-to-string": "3.0.0",
+ "hastscript": "9.0.0",
+ "html-escaper": "3.0.3",
+ "rehype-autolink-headings": "7.1.0",
+ "remark-code-import": "1.2.0",
+ "starlight-typedoc": "0.8.0",
+ "typedoc": "0.25.7",
+ "typedoc-plugin-markdown": "4.0.0-next.38",
+ "typedoc-plugin-mdn-links": "3.1.11",
+ "typedoc-plugin-zod": "1.1.1",
+ "unified": "11.0.4",
+ "unist-util-visit": "5.0.0"
+ },
+ "devDependencies": {
+ "@types/html-escaper": "3.0.2"
+ }
+}
diff --git a/packages/docs/plugin/rehype-autolink.ts b/packages/docs/plugin/rehype-autolink.ts
new file mode 100644
index 000000000..829b92bfa
--- /dev/null
+++ b/packages/docs/plugin/rehype-autolink.ts
@@ -0,0 +1,81 @@
+import type {RehypePlugins} from 'astro';
+import type {Root} from 'hast';
+import type {Transformer} from 'unified';
+
+import {toString} from 'hast-util-to-string';
+import {h} from 'hastscript';
+import {escape} from 'html-escaper';
+import rehypeAutolinkHeadings, {
+ type Options as AutolinkOptions,
+} from 'rehype-autolink-headings';
+import {visit} from 'unist-util-visit';
+
+const AnchorLinkIcon = h(
+ 'span',
+ {ariaHidden: 'true', class: 'anchor-icon'},
+ h(
+ 'svg',
+ {height: 16, viewBox: '0 0 24 24', width: 16},
+ h('path', {
+ d: 'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z',
+ fill: 'currentcolor',
+ }),
+ ),
+);
+
+const createSROnlyLabel = (text: string) => {
+ return h(
+ 'span',
+ {class: 'sr-only', 'is:raw': true},
+ `Section titled ${escape(text)}`,
+ );
+};
+
+/**
+ * Configuration for the `rehype-autolink-headings` plugin. This set-up was
+ * informed by https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
+ */
+const autolinkConfig: AutolinkOptions = {
+ behavior: 'after',
+ content: (heading) => [AnchorLinkIcon, createSROnlyLabel(toString(heading))],
+ group: ({tagName}) =>
+ h('div', {class: `heading-wrapper level-${tagName}`, tabIndex: -1}),
+ properties: {class: 'anchor-link'},
+};
+
+/**
+ * Rehype plugin to translate the headings' anchors according to the currently
+ * selected language.
+ */
+function rehypeAutolinkHeadingsTransformer() {
+ const transformer: Transformer = (tree) => {
+ const englishText = 'Section titled';
+
+ // Find anchor links
+ visit(tree, 'element', (node) => {
+ if (node.tagName === 'a' && node.properties?.class === 'anchor-link') {
+ // Find a11y text labels
+ visit(node, 'text', (text) => {
+ const heading = text.value.replace(englishText, '');
+
+ const title = englishText;
+
+ text.value = title + heading;
+ });
+ }
+ });
+ };
+
+ return function attacher() {
+ return transformer;
+ };
+}
+
+/**
+ * Configure heading anchor links. Spread this into Astro's
+ * `markdown.rehypePlugins` option.
+ */
+export const rehypeAutolink = (): RehypePlugins => [
+ [rehypeAutolinkHeadings, autolinkConfig],
+ rehypeAutolinkHeadingsTransformer(),
+];
diff --git a/packages/docs/public/favicon.svg b/packages/docs/public/favicon.svg
new file mode 100644
index 000000000..753dd6390
--- /dev/null
+++ b/packages/docs/public/favicon.svg
@@ -0,0 +1,4944 @@
+
\ No newline at end of file
diff --git a/docs/_media/logo.png b/packages/docs/src/assets/logo.png
similarity index 100%
rename from docs/_media/logo.png
rename to packages/docs/src/assets/logo.png
diff --git a/packages/docs/src/assets/logo.svg b/packages/docs/src/assets/logo.svg
new file mode 100644
index 000000000..ad659ce37
--- /dev/null
+++ b/packages/docs/src/assets/logo.svg
@@ -0,0 +1,5166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/components/Footer.astro b/packages/docs/src/components/Footer.astro
new file mode 100644
index 000000000..3c8077a70
--- /dev/null
+++ b/packages/docs/src/components/Footer.astro
@@ -0,0 +1,31 @@
+---
+import type { Props } from '@astrojs/starlight/props';
+
+import EditLink from '@astrojs/starlight/components/EditLink.astro';
+import LastUpdated from '@astrojs/starlight/components/LastUpdated.astro';
+import Pagination from '@astrojs/starlight/components/Pagination.astro';
+import Legal from './Legal.astro';
+---
+
+
+
+
diff --git a/packages/docs/src/components/Legal.astro b/packages/docs/src/components/Legal.astro
new file mode 100644
index 000000000..b1087b1eb
--- /dev/null
+++ b/packages/docs/src/components/Legal.astro
@@ -0,0 +1,19 @@
+---
+const url = 'https://github.com/boneskull';
+const author = 'Christopher Hiller'
+---
+
+
+
+
diff --git a/packages/docs/src/components/RuleExamples.astro b/packages/docs/src/components/RuleExamples.astro
new file mode 100644
index 000000000..f70370bc2
--- /dev/null
+++ b/packages/docs/src/components/RuleExamples.astro
@@ -0,0 +1,53 @@
+---
+import {TabItem, Tabs} from '@astrojs/starlight/components';
+import {getCollection} from 'astro:content';
+
+const RULE_EXAMPLES_COLLECTION = 'rule-examples';
+const DEFAULTS_LABEL = 'Defaults';
+const DISABLED_LABEL = 'Disabled';
+
+export type Props = {
+ rule: string;
+};
+
+const {rule} = Astro.props;
+
+const defaultDescriptions: Record = {
+ [DEFAULTS_LABEL]: `This is the default behavior of the ${rule} rule.`,
+ [DISABLED_LABEL]: `This disables the ${rule} rule entirely.`,
+};
+
+const exampleData = await getCollection(
+ RULE_EXAMPLES_COLLECTION,
+ ({data}) => data.rule === rule,
+);
+
+const examples = await Promise.all(
+ exampleData.map(
+ async (entry) =>
+ await entry.render().then(({Content}) => ({Content, data: entry.data})),
+ ),
+);
+
+// Sort examples so that the "Defaults" example is always first and "Disabled" is always last
+examples.sort((a, b) => {
+ if (a.data.label === DEFAULTS_LABEL || b.data.label === DISABLED_LABEL)
+ return -1;
+ if (b.data.label === DEFAULTS_LABEL || a.data.label === DISABLED_LABEL)
+ return 1;
+ return 0;
+});
+---
+
+
+ {
+ examples.map(({Content, data}) => (
+
+
+
+
+
+
+ ))
+ }
+
diff --git a/packages/docs/src/content/config.ts b/packages/docs/src/content/config.ts
new file mode 100644
index 000000000..48eab8a2e
--- /dev/null
+++ b/packages/docs/src/content/config.ts
@@ -0,0 +1,14 @@
+import {docsSchema} from '@astrojs/starlight/schema';
+import {z} from 'astro/zod';
+import {defineCollection} from 'astro:content';
+
+export const collections = {
+ docs: defineCollection({schema: docsSchema()}),
+ 'rule-examples': defineCollection({
+ schema: z.object({
+ description: z.string().min(1).optional(),
+ label: z.string().min(1),
+ rule: z.string().min(1).optional(),
+ }),
+ }),
+};
diff --git a/packages/docs/src/content/docs/developer/index.mdx b/packages/docs/src/content/docs/developer/index.mdx
new file mode 100644
index 000000000..435db96d4
--- /dev/null
+++ b/packages/docs/src/content/docs/developer/index.mdx
@@ -0,0 +1,18 @@
+---
+title: Developer Portal
+description: Docs for those wanting to build on/with/around midnight-smoker
+---
+
+import LinkCard from '../../../components/LinkCard.astro';
+
+> _This is what a "portal" is._
+
+Interested in writing your own plugins for `midnight-smoker`? Or even _contributing_ to `midnight-smoker`? This is where that stuff lives.
+
+
+ How to create your own **midnight-smoker** plugins
+
+
+
+ **midnight-smoker**'s programmatic API
+
diff --git a/packages/docs/src/content/docs/developer/plugin-overview.mdx b/packages/docs/src/content/docs/developer/plugin-overview.mdx
new file mode 100644
index 000000000..a4d2f80ec
--- /dev/null
+++ b/packages/docs/src/content/docs/developer/plugin-overview.mdx
@@ -0,0 +1,144 @@
+---
+title: Developing Plugins
+description: How to create your own plugins for `midnight-smoker`
+---
+
+import {LinkCard, CardGrid} from '@astrojs/starlight/components';
+
+This guide describes how to create your own plugins for `midnight-smoker`.
+
+:::tip[STFU & Give Me Examples]
+
+Here: [example plugins for `midnight-smoker`](https://github.com/boneskull/midnight-smoker/tree/main/example).
+
+:::
+
+## Who This Guide is For
+
+If you like `midnight-smoker`, but it doesn't do _quite_ what you want, you could bridge the gap by creating a plugin. For exampled, if you want to:
+
+- Create custom rules (automated lint checks) to run against your package artifacts
+- Add support for an unsupported package manager
+- Create a reporter for a custom file format
+- Create a different strategy for invoking custom scripts (e.g., run them in parallel)
+
+...then this guide is for _you_.
+
+## What is a Plugin?
+
+Pedantically speaking, a plugin is an object implementing the this interface:
+
+```ts
+interface Plugin {
+ /**
+ * Defines the plugin
+ */
+ plugin: (api: PluginAPI) => void | Promise;
+
+ /**
+ * Custom name; otherwise derived from closest `package.json`
+ */
+ name?: string;
+
+ /**
+ * Custom description; otherwise derived from closest `package.json`
+ */
+ description?: string;
+}
+```
+
+Typically, an [_ECMAScript Module_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) or a _CommonJS script_ will implement this interface. For ESM, these may be named exports; for CJS, they may be properties on the [`exports`](https://nodejs.org/api/modules.html#exports-shortcut) object.
+
+Such an object implementing this interface could instead be the _default_ export in a ES module, or the value of `module.exports` in a CommonJS script (or even an `exports.default` value, because everything is terrible).
+
+The implementation of a `Plugin` is expected to use the `PluginAPI` object to register one or more [_components_](#whats-a-component).
+
+:::tip[Use the Types]
+
+It's _strongly recommended_ to use TypeScript (either TS proper or [JSDoc](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html)) when writing plugins, as it will help you navigate the `PluginAPI` object.
+
+`midnight-smoker` _can_ load a TypeScript plugin directly—but this feature should only be used for development!
+
+:::
+
+### What's a Component?
+
+`midnight-smoker` is ~~overengineered~~ modular, and most functionality is provided by _components_. A component is typically a function or object implementing one of a handful of interfaces defined by `midnight-smoker`.
+
+A component may be one of:
+
+- A lint **rule** — analyzes the installed package artifact
+- A **reporter** — emits output
+- A **package manager** adapter — logic for packing, installing, and running custom scripts
+- A **script runner** — orchestrates custom script runs
+- A **rule runner** — invokes rules (dubious use-case)
+- An **executor** — spawns processes
+
+:::note[Mea Culpa]
+
+A "component" means something entirely different in the context of `midnight-smoker` than it does in a web context. Please [send suggestions](https://github.com/boneskull/midnight-smoker/issues/new) for a better word.
+
+:::
+
+Next, let's [take a closer look](#the-plugin-api) at the `PluginAPI` object.
+
+## The Plugin API
+
+The `PluginAPI` object is the "glue" between a plugin and `midnight-smoker`. It contains functions for defining new components, utilities, and creating object schemas.
+
+### Component Definition Functions
+
+`PluginAPI` contains `define*` functions for each [component kind](#whats-a-component). A plugin calls these to register its components.
+
+These functions are all synchronous.
+
+### Plugin Metadata
+
+The `PluginAPI.metadata` property contains metadata (a `PluginMetadata` object) about _this_ plugin. This object will be updated _as the [component definition functions](#component-definition-functions) are called_.
+
+It contains information such as the parsed `package.json`, any custom name and description, the user-requested module identifier and its corresponding resolved module path.
+
+Of note, it contains `Map`s for each component kind, where the key is the component _name_. The value of each entry depends on the component kind. It will contain an `id` property created and assigned by `midnight-smoker`.
+
+:::note[Component Names]
+
+Rule and package manager components _must_ be explicitly named, but others will receive a name of `default` if not otherwise specified. See [scoped names](#scoped-names) for more information.
+
+:::
+
+Since the most common use case will probably be a custom rule, let's take a look at that first.
+
+### Other Plugins
+
+While integration with other plugins isn't well-defined (yet), a plugin can get basic information about the other plugins which have been loaded via the `PluginAPI.plugins` property. This property is computed, so its value depends on _when_ the plugin accesses it; other plugins may be loaded before or after yours!
+
+### Component-Specific Namespaces
+
+These properties are TS _namespaces_ corresponding to each component kind. They're useful for accessing types and helpers specific to a component implementation.
+
+### Errors, Events
+
+Certain component kind expect certain `Error`s to be thrown and certain events to be emitted. Plugins may subclass `midnight-smoker`'s `Error`s, if needed--but it's recommended to _not_ throw vanilla or other random `Error`s, as `midnight-smoker`'s `Error`s contain custom behavior and formatting.
+
+### Junk Drawer
+
+There's handful of utilities in there, too. Poke around, I guess?
+
+Next, we'll learn how to [create a plugin which registers a custom rule](/developer/writing-rules).
+
+## Going Further
+
+From here, you might want to:
+
+
+
+
+
diff --git a/packages/docs/src/content/docs/developer/plugin-reporter.mdx b/packages/docs/src/content/docs/developer/plugin-reporter.mdx
new file mode 100644
index 000000000..109d1a6fe
--- /dev/null
+++ b/packages/docs/src/content/docs/developer/plugin-reporter.mdx
@@ -0,0 +1,121 @@
+---
+title: Creating a Reporter
+description: How to write a custom reporter for midnight-smoker
+---
+
+A _reporter_ is a component which attaches listener functions to an [`EventEmitter`](https://nodejs.org/api/events.html#class-eventemitter)-like object, and generates output from the events it receives.
+
+Since most of the implementation of a reporter will likely be concentrated on listening for events, let's [learn about the events](#events) that `midnight-smoker` emits.
+
+## Events
+
+Reporters can listen for events emitted during _both_ the [`lint` operation](/reference/cli#command-lint) _and_ the [`run-script` operation](/reference/cli#command-run-script-run).
+
+Events marked with ❌ are considered "fatal" and will _always_ cause `smoker` to exit with a non-zero exit code.
+
+:::tip[Packages With an S]
+
+Recall that `midnight-smoker` can run against multiple packages (workspaces) _and_ multiple package managers. Take care to note which events can be emitted an exponential number of times!
+
+:::
+
+### Top-Level Events
+
+_Events related to the entire run._
+
+> These events are emitted _only once_.
+
+- `SmokeBegin`: emitted just before `midnight-smoker` will begin linting and/or running scripts
+- `SmokeOk`: emitted after `midnight-smoker` has completed successfully
+- ❌ `SmokeFailed`: emitted if `midnight-smoker` fails for one of the following reasons:
+ - `PackFailed` was emitted
+ - `InstallFailed` was emitted
+ - `RunRuleFailed` was emitted _and_ the event severity was `error`
+ - `RuleError` was emitted
+ - `UnknownError` was emitted
+- `Lingered`: emitted if the [`linger` option](/reference/config#linger) is `true`
+- `End`: emitted just before `midnight-smoker` will exit
+
+### Packing Events
+
+_Events related to creating [package artifacts](#package-artifacts)._
+
+> These events are emitted _only once_.
+
+- `PackBegin`: emitted just before packages will be packed
+- `PackOk`: emitted after packages have been packed successfully
+- ❌ `PackFailed`: emitted if a package fails to pack
+
+### Installation Events
+
+_Events related to installing package artifact._
+
+> These events are emitted _only once_.
+
+- `InstallBegin`: emitted just before `midnight-smoker` will install package artifacts
+- `InstallOk`: emitted after a package artifact has been installed successfully
+- ❌ `InstallFailed`: emitted if a package artifact fails to install
+
+### Linting/Rule Events
+
+_Events related to linting a package artifact._
+
+> These events are emitted _only once_.
+
+- `RunRulesBegin`: emitted just before `midnight-smoker` will run rules against package artifacts
+- `RunRulesOk`: emitted after linting succeeds without issue
+- `RunRulesFailed`: emitted if linting fails with one or more issues
+- ❌ `RuleError`: emitted if a rule throws an exception or rejects
+
+> These events are emitted _once per rule per package_.
+
+- `RunRuleBegin`: emitted just before a _specific_ rule is run against a package artifact
+- `RunRuleOk`: emitted after _specific_ rule succeeds without issue
+- `RunRuleFailed`: emitted if a _specific_ rule fails with one or more issues
+
+### Custom Script Events
+
+_Events related to execution of custom script(s)._
+
+> These events are emitted _only once_.
+
+- `RunScriptsBegin`: emitted just before `midnight-smoker` will run custom scripts
+- `RunScriptsOk`: emitted after custom scripts have been run successfully
+- ❌ `RunScriptsFailed`: emitted if a custom script fails
+
+> These events are emitted _once per script per package per package manager_.
+
+- `RunScriptBegin`: emitted just before a _specific_ script is run
+- `RunScriptOk`: emitted after a _specific_ script succeeds
+- ❌ `RunScriptFailed`: emitted if a _specific_ script fails
+
+## Example Reporter: `dot`
+
+Here's an example reporter which mimics the `dot` reporter from [Mocha](https://mochajs.org):
+
+{/* prettier-ignore */}
+```ts file=../../../../../../example/plugin-reporter/index.ts title="plugin-reporter.ts"
+```
+
+We see that we don't need to type these event names by hand, since we can get them from the `PluginAPI.Events.SmokerEvent` object, which is an enum-like object (but not a TS `enum`; you're welcome):
+
+{/* prettier-ignore */}
+```ts {4,10-11}
+import type {PluginFactory} from 'midnight-smoker/plugin';
+
+export const plugin: PluginFactory = (api) => {
+ const {SmokerEvent} = api.Event;
+ // ...
+ api.defineReporter({
+ // ...
+ reporter({emitter}) {
+ emitter
+ .once(SmokerEvent.PackOk, dot)
+ .once(SmokerEvent.PackFailed, bang)
+ // ...
+ }
+ })
+}
+```
+
+Of course, this example reporter does not actually _do_ anything with the events it receives other than shuffle them into "dot" and "bang" buckets. However, each event payload is fully-typed, so if you _did_ want to do something like—print useful information—that's _totally_ on the table.
diff --git a/packages/docs/src/content/docs/developer/plugin-rule.mdx b/packages/docs/src/content/docs/developer/plugin-rule.mdx
new file mode 100644
index 000000000..a8edd7a41
--- /dev/null
+++ b/packages/docs/src/content/docs/developer/plugin-rule.mdx
@@ -0,0 +1,188 @@
+---
+title: Writing rules
+description: Writing custom rules for midnight-smoker
+---
+
+import {Tabs, TabItem} from '@astrojs/starlight/components';
+
+## Writing a Rule
+
+Much like [ESLint](https://eslint.org), a custom rule allows the implementor to write a function which performs a "check" and potentially raises an issue. But instead of running against an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree), `midnight-smoker` runs against an [_installed package artifact._](#package-artifacts).
+
+First, we'll hope to better understand what a rule is _for_ and what it _should_, _should not_ or _must not_ (!) do.
+
+### Rule Rights & Responsibilities
+
+- A rule **should perform I/O**.
+
+ Pure static analysis of a `package.json` file _may_ be better suited for something like an ESLint plugin. A `midnight-smoker` rule wants to skulk around in the filesystem and/or create child processes.
+
+- A rule **must not throw** exceptions (or reject).
+
+ Catch them, and instead call `RuleContext.addIssueFromError()`—a convenience method for this very purpose. An exception thrown from a rule _will cause `midnight-smoker` to exit immediately_ with a non-zero exit code.
+
+- A rule **must not print** anything to the console.
+
+ We have reporters for that. I mean—I _guess_ it's fine for you console debuggers out there.
+
+- As of this writing, a rule **should not query the severity** and change its behavior based on its value.
+
+ Whether there's an actual use-case for this is TBD; `RuleContext.severity` may be removed in the future!
+
+- A rule **should not attempt to reference the original package**.
+
+ Unless you're doing some sort of "diff", this is to be avoided. `midnight-smoker` is designed to run against the [_installed package artifact_](#package-artifacts) only.
+
+### Package Artifacts
+
+A _package artifact_ is the result of running [`npm pack`](https://docs.npmjs.com/cli/v10/commands/npm-pack) in a package—it's just a tarball (`.tgz` file). Roughly, `midnight-smoker` creates a temporary directory and installs this tarball into it as if it were a dependency.
+
+The directory where the tarball was installed (typically `node_modules/`) is the _installed package artifact_. This is the context in which `midnight-smoker`, uh… smokes.
+
+### Example Rule: `no-unlicensed`
+
+To create a rule, we'll use the `api.defineRule()` function. The example plugin below defines a rule which asserts the published package will ship with license information in its `package.json` _and_ an external license file.
+
+
+
+
+ {/* prettier-ignore */}
+ ```ts file=../../../../../../example/plugin-rule/ts/index.ts title="plugin-rule.ts"
+ ```
+
+
+
+
+ {/* prettier-ignore */}
+ ```ts file=../../../../../../example/plugin-rule/esm/index.js title="plugin-rule.mjs"
+ ```
+
+
+
+
+The first parameter to the `check` function is the `RuleContext` object. This object contains information about the package artifact, the severity of the rule, and the `addIssue()` function, which we'll discuss next.
+
+### `addIssue()`
+
+The `addIssue()` function is used to report an issue. In most cases, passing this function a message will suffice. In other cases, it may be useful to provide a second parameter which contains additional information about the issue. This "extra data" is only shown when `midnight-smoker` is run with the `--verbose` flag.
+
+Because the _severity_ is user-controlled, a rule implementation needn't report an issue as a "warning" or an "error". Thus, merely calling `addIssue()` implies nothing about the exit code of `smoker`.
+
+### Rule Options
+
+A rule definition _may_ define its own options on the `schema` property. These options are user-configurable via a [config file](/reference/config).
+
+`midnight-smoker` provides [Zod](https://zod.dev) for defining option schemas. We have some rules (haha?) about these an options schemas:
+
+1. It _must_ be an _object_, e.g., created with `z.object()`.
+2. It _must_ be fully optional (even if you don't write it as such, it will be _made_ optional).
+3. It _should_ provide defaults for its properties.
+4. It _should_ leverage `z.describe()` to provide descriptions for its properties; these will be output in any error messages printed the your rule is misconfigured by an end-user.
+
+If a rule definition has no `schema` property, any options provided by the user will be passed through as-is to the `opts` parameter (second parameter) of the `check()` function.
+
+### Verifying the Plugin
+
+To verify that `midnight-smoker` loads our plugin successfully, try the `list-plugins` command:
+
+
+
+
+ {/* prettier-ignore */}
+ ```shell title="List Plugins"
+ npm exec smoker -- --plugin ./plugin-rule.ts list-plugins
+ ```
+
+
+
+
+ {/* prettier-ignore */}
+ ```shell title="List Plugins"
+ npm exec smoker -- --plugin ./plugin-rule.mjs list-plugins
+ ```
+
+
+
+
+You should see something like this:
+
+```ansi title="Plugin List"
+[0;90m┌────────────┬─────────┬──────────────────────────────────────────┬────────────┐[0m[0m
+[0;90m│[0m [0;36mName[0m [0;90m│[0m [0;36mVersion[0m [0;90m│[0m [0;36mDescription[0m [0;90m│[0m [0;36mResolved[0m [0;90m│[0m[0m
+[0;90m├────────────┼─────────┼──────────────────────────────────────────┼────────────┤[0m[0m
+[0;90m│[0m [0;33;1m(built-in)[0m [0;90m│[0m 0.0.0 [0;90m│[0m Default behavior for midnight-smoker [0;90m│[0m ../../pac… [0;90m│[0m[0m
+[0;90m├────────────┼─────────┼──────────────────────────────────────────┼────────────┤[0m[0m
+[0;90m│[0m [0;33;1mexample[0m [0;90m│[0m 1.0.0 [0;90m│[0m Provides a rule which validates licenses [0;90m│[0m index.ts [0;90m│[0m[0m
+[0;90m└────────────┴─────────┴──────────────────────────────────────────┴────────────┘[0m[0m
+[0m
+```
+
+You can also list all the rules via the `list-rules` command:
+
+
+
+
+ {/* prettier-ignore */}
+ ```shell title="List Rules"
+ npm exec smoker -- --plugin ./plugin-rule.ts list-rules
+ ```
+
+
+
+
+ {/* prettier-ignore */}
+ ```shell title="List Rules"
+ npm exec smoker -- --plugin ./plugin-rule.mjs list-rules
+ ```
+
+
+
+
+Producing:
+
+```ansi title="Rule List"
+[0;90m┌────────────────────────┬─────────────────────────────────────────┬───────────┐[0m[0m
+[0;90m│[0m [0;36mName[0m [0;90m│[0m [0;36mDescription[0m [0;90m│[0m [0;36mPlugin[0m [0;90m│[0m[0m
+[0;90m├────────────────────────┼─────────────────────────────────────────┼───────────┤[0m[0m
+[0;90m│[0m [0;33;1mno-banned-files[0m [0;90m│[0m Ensures banned files won't be published [0;90m│[0m (builtin) [0;90m│[0m[0m
+[0;90m│[0m [0;90m│[0m to the registry [0;90m│[0m [0;90m│[0m[0m
+[0;90m├────────────────────────┼─────────────────────────────────────────┼───────────┤[0m[0m
+[0;90m│[0m [0;33;1mno-missing-entry-point[0m [0;90m│[0m Checks that the package contains an [0;90m│[0m (builtin) [0;90m│[0m[0m
+[0;90m│[0m [0;90m│[0m entry point; only applies to CJS [0;90m│[0m [0;90m│[0m[0m
+[0;90m│[0m [0;90m│[0m packages without an "exports" field [0;90m│[0m [0;90m│[0m[0m
+[0;90m├────────────────────────┼─────────────────────────────────────────┼───────────┤[0m[0m
+[0;90m│[0m [0;33;1mno-missing-exports[0m [0;90m│[0m Checks that all files in the "exports" [0;90m│[0m (builtin) [0;90m│[0m[0m
+[0;90m│[0m [0;90m│[0m field (if present) exist [0;90m│[0m [0;90m│[0m[0m
+[0;90m├────────────────────────┼─────────────────────────────────────────┼───────────┤[0m[0m
+[0;90m│[0m [0;33;1mno-missing-pkg-files[0m [0;90m│[0m Checks that files referenced in [0;90m│[0m (builtin) [0;90m│[0m[0m
+[0;90m│[0m [0;90m│[0m package.json exist in the tarball [0;90m│[0m [0;90m│[0m[0m
+[0;90m├────────────────────────┼─────────────────────────────────────────┼───────────┤[0m[0m
+[0;90m│[0m [0;33;1mexample/no-unlicensed[0m [0;90m│[0m Checks that a package has a license [0;90m│[0m example [0;90m│[0m[0m
+[0;90m└────────────────────────┴─────────────────────────────────────────┴───────────┘[0m
+
+```
+
+Next, [find out why](#scoped-identifiers) the rule is named `example/no-unlicensed` instead of just `no-unlicensed`.
+
+### Scoped Identifiers
+
+The gist is this: **any rules _other_ than the built-in rules have _scoped_ identifiers.**
+
+A rule's scoped identifier is its _unique, canonical name_, and thus how it must be referenced in a config file.
+
+Here's an example config file showing how a rule is referenced by its scoped identifier:
+
+```json title="smoker.config.json"
+{
+ "plugin": ["./plugin-rule.mjs"],
+ "rules": {
+ "example/no-unlicensed": ["error", {"files": ["LICENSE-MIT"]}]
+ }
+}
+```
+
+> The above example a) loads the ESM version of our example plugin, and b) configures the example rule to _only_ consider a file named `LICENSE-MIT` to be a valid license file.
+
+In fact, **all externally-defined components have a scoped identifier**. Some need to be named explicitly (rules, reporters), and others do not (rule runners, script runners).
+
+Next, we'll learn how to [write a custom reporter](/developer/plugin-reporter).
diff --git a/packages/docs/src/content/docs/guides/custom-scripts.md b/packages/docs/src/content/docs/guides/custom-scripts.md
new file mode 100644
index 000000000..dcb1dd91f
--- /dev/null
+++ b/packages/docs/src/content/docs/guides/custom-scripts.md
@@ -0,0 +1,4 @@
+---
+title: Custom Scripts
+description: Working with custom scripts in midnight-smoker
+---
diff --git a/packages/docs/src/content/docs/guides/getting-started.md b/packages/docs/src/content/docs/guides/getting-started.md
new file mode 100644
index 000000000..93193b9d8
--- /dev/null
+++ b/packages/docs/src/content/docs/guides/getting-started.md
@@ -0,0 +1,57 @@
+---
+title: Getting Started
+description: Getting started with midnight-smoker
+---
+
+This brief guide will help you get started with **midnight-smoker**.
+
+## Supported Environments
+
+### Platforms
+
+**midnight-smoker** is intended for use with Node.js.
+
+- Node.js versions supported: `^18.0.0 || ^20.0.0`
+- Supported package managers:
+ - `npm`: `8.6.0+`
+ - `yarn`: any version
+ - `pnpm`: (not yet supported)
+
+:::caution
+
+While versions of Node.js outside the supported range _could_ work, compatibility is not tested. Use at your own _significant peril_.
+
+:::
+
+### Workspaces
+
+**midnight-smoker** supports workspaces as commonly defined in `package.json`.
+
+## Installation
+
+If you're here, you probably don't need to know how to install it, but I feel like I'm supposed to write this anyway?
+
+It's recommended to install `midnight-smoker` as a dev dependency:
+
+```shell title="Installing midnight-smoker"
+npm install midnight-smoker --save-dev
+```
+
+## Basic Usage
+
+To run **midnight-smoker**'s builtin checks against your project, run the following command:
+
+```shell title="Run midnight-smoker"
+npx midnight-smoker
+```
+
+Assuming your package isn't all jacked up, you should see output akin to:
+
+```text title="midnight-smoker happy path output"
+💨 midnight-smoker vX.Y.Z
+✔ Packed 1 unique package using npm@latest…
+✔ Installed 1 unique package from tarball
+✔ Successfully ran 4 checks
+✔ Successfully ran 1 script
+✔ Lovey-dovey! 💖
+```
diff --git a/packages/docs/src/content/docs/guides/plugins.md b/packages/docs/src/content/docs/guides/plugins.md
new file mode 100644
index 000000000..7e93ceb2c
--- /dev/null
+++ b/packages/docs/src/content/docs/guides/plugins.md
@@ -0,0 +1,5 @@
+---
+title: Using Plugins
+---
+
+**`midnight-smoker` does not automatically detect plugins.** To use a plugin, it _must_ be referenced in the [config file](/reference/config) _or_ the [CLI](/reference/cli) via `--plugin ./plugin-rule.ts`.
diff --git a/packages/docs/src/content/docs/guides/usage.md b/packages/docs/src/content/docs/guides/usage.md
new file mode 100644
index 000000000..bfc2e7d83
--- /dev/null
+++ b/packages/docs/src/content/docs/guides/usage.md
@@ -0,0 +1,4 @@
+---
+title: General Usage
+description: A general usage guide of reasonable depth
+---
diff --git a/packages/docs/src/content/docs/index.mdx b/packages/docs/src/content/docs/index.mdx
new file mode 100644
index 000000000..8139a7d9b
--- /dev/null
+++ b/packages/docs/src/content/docs/index.mdx
@@ -0,0 +1,54 @@
+---
+title: 'midnight-smoker'
+description: 'midnight-smoker runs smoke tests, bruh'
+template: splash
+hero:
+ tagline: stop publishing broken packages. stop it
+ image:
+ file: '../../assets/logo.svg'
+ alt: 'midnight-smoker logo'
+ actions:
+ - text: Get Started
+ link: /guides/getting-started
+ icon: right-arrow
+ variant: primary
+ - text: Usage Guide
+ link: /guides/usage
+ icon: open-book
+ - text: Reference
+ link: /reference/cli
+ icon: pencil
+---
+
+import {Card, CardGrid} from '@astrojs/starlight/components';
+
+
+
+
+ **midnight-smoker** is like a _linter for your published packages_. It ships
+ with built-in configurable _rules_ which check for common issues and
+ misconfigurations. _These are the nasties your unit and integration tests
+ can't catch._
+
+
+ Run custom scripts against your package _as a consumer would install it_. Go
+ where static analysis can't take you. _Then go further_. ☮️
+
+
+ **midnight-smoker** was written with monorepos and workspaces in mind. Use it
+ with _specific_ workspaces, _all_ of your workspaces or _no_ workspaces.
+
+
+ **midnight-smoker** has first-class plugin support. Want a new rule? How about
+ several? Got a new package manager? Or XML reporter (I'm sorry)? [Write your
+ own plugin](/developer/plugin-overview) and share it.
+
+
+
+
+
+Ever wished you could easily check compatibility with a specific version of a specific package manager? No? OK, fine—but some of us have, anyway.
+
+You can use **midnight-smoker** and assert your target package manager installs your package properly. Or your target package manager's _last two major versions_. Or this package manager and that other one (but the old version--you know, the one everyone still uses). Or _all_ versions of all package managers, I guess?
+
+
diff --git a/packages/docs/src/content/docs/reference/cli.md b/packages/docs/src/content/docs/reference/cli.md
new file mode 100644
index 000000000..48f2246c6
--- /dev/null
+++ b/packages/docs/src/content/docs/reference/cli.md
@@ -0,0 +1,276 @@
+---
+title: The smoker CLI
+description: midnight-smoker CLI reference
+---
+
+## Summary
+
+**midnight-smoker**'s CLI is `smoker`.
+
+_Not_ `midnight-smoker`. That is too long.
+
+## Conventions
+
+### Array-Type Options
+
+Options which accept multiple values (e.g., [`--workspace`](-w---workspace)) may be provided multiple times. **They cannot be provided as comma-delimited values**.
+
+:::tip[Use Short Aliases for Multiple Values]
+
+It's convenient to use short options (`-w` instead of `--workspace`) when providing multiple values for options. Example:
+
+```shell
+smoker run -w=foo -w=bar -w=baz quux
+```
+
+:::
+
+### Object-Type Options
+
+Any option more complicated than an array of strings is **unsupported on the CLI** and must be provided via a [config file](/reference/config).
+
+### Exit Codes
+
+`smoker` will exit with code `0` if linting passes without an "error" severity issue and if all custom scripts exit with code `0`, and `1` otherwise.
+
+`smoker` will exit with code `1` if packing or installation of any package fails.
+
+### Default Behavior
+
+When executed without a command, `smoker`'s default behavior is to [`lint`](#command-lint).
+
+### Global Options
+
+These options are always available.
+
+#### `--help`
+
+Show the "help" and exit.
+
+#### `--help` Example
+
+```shell title='Help!!!'
+smoker --help
+```
+
+#### `--version`
+
+Print the version of `smoker` and exit.
+
+#### `-P`, `--plugin`, `--plugins`
+
+Load one or more plugins. The plugin must be a resolvable module identifier (e.g., `@boneskull/some-plugin`) or a path to a module.
+
+This option may be provided multiple times.
+
+:::tip[Use a Config File]
+
+Specify the [`plugin` option](/reference/config#plugin) in a [config file](/reference/config) so you don't forget to type it every time.
+
+:::
+
+## Command: `lint`
+
+The `lint` command will apply the set of [enabled rules](/reference/config) against your package artifact(s).
+
+_Plugins may influence the available rules_.
+
+:::tip[Again, Use a Config File]
+
+If you want to disable a rule, change its severity to "warning", or otherwise set rule-specific options: do so in a [config file](/reference/config). See the [Built-in Rules Reference](/reference/rules) for rule-specific options.
+
+Given the complexity of describing this sort of configuration, it's unsuitable for the command-line.
+
+:::
+
+### `lint` Summary
+
+```text
+smoker lint
+
+Lint package artifacts
+
+Input:
+ --all Run in all workspaces [boolean]
+ --include-root Include the workspace root; must provide '--all'
+ [boolean]
+ -w, --workspace Run script in a specific workspace or workspaces
+ [array]
+ -p, --pkg-manager, --pm Use a specific package manager
+ [array] [default: npm@latest]
+
+Output:
+ --json Output JSON only. Alias for "--reporter=json" [boolean]
+ -r, --reporter Reporter(s) to use [array] [default: console]
+ --verbose Enable verbose output [boolean] [default: false]
+
+Options:
+ --version Show version number [boolean]
+ -P, --plugin, --plugins Plugin(s) to use [array]
+ --help Show help [boolean]
+
+```
+
+### `lint` Input Options
+
+These options control which package(s) `midnight-smoker` will lint and with which package manager.
+
+#### `--all`
+
+Run `smoker` in all workspaces. If this option is not provided, `smoker` will run in the current workspace only. Has no effect if no workspaces are defined in the current project.
+
+#### `--include-root`
+
+Include the workspace root (monorepo root). **Must** be used with `--all`.
+
+#### `-w`, `--workspace`
+
+Run `smoker` in a specific workspace or workspaces. If this option is not provided, `smoker` will run in the current workspace only (which may be the workspace root). Has no effect if no workspaces are defined in the current project.
+
+This option may be provided multiple times.
+
+#### `-p`, `--pkg-manager`, `--pm`
+
+Use a specific package manager. If this option is not provided, `smoker` will use the latest version of `npm`. The package manager must be of the form `[@]`, e.g. `yarn@berry`.
+
+_Plugins may influence the available package managers_.
+
+:::note
+
+This option _may_ be provided multiple times, but this would only be useful if a package manager's "pack" behavior differs from that of `npm`. See the [Running Custom Scripts](/guides/custom-scripts.md) guide for a better use-case.
+
+:::
+
+### `lint` Output Options
+
+#### `--json`
+
+Output JSON only. Alias for `--reporter=json`.
+
+If an exception occurs, the output will be a JSON object with an `error` property whose value is the exception object.
+
+
+
+#### `-r`, `--reporter`
+
+Reporter(s) to use. If this option is not provided, `smoker` provides human-readable output via the `console` reporter.
+
+_Plugins may influence the available reporters_.
+
+To see the list of available reporters, see [`list-reporters`](#command-list-reporters).
+
+#### `--verbose`
+
+Enable verbose output. This will cause `smoker` to output additional information when it encounters an issue or error.
+
+## Command: `list-reporters`
+
+The `list-reporters` command will list the available reporters and exit.
+
+Unless paired with [`--plugin`](#-p---plugin---plugins), will list built-in reporters only.
+
+## Command: `list-rules`
+
+The `list-rules` command will list the available rules (for use with [`lint`](#command-lint)) and exit.
+
+Unless paired with [`--plugin`](#-p---plugin---plugins), will list built-in rules only.
+
+
+
+## Command: `run-script`, `run`
+
+The `run-script` command will run one or more custom scripts (defined in the [`scripts`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#scripts) property of your `package.json`) against your package artifact(s).
+
+### `run-script` Summary
+
+```text
+smoker run-script
+
+Run custom script(s) against package artifacts
+
+Positionals:
+ script Custom script(s) to run (from package.json)
+ [array] [required] [default: []]
+
+Input:
+ --all Run in all workspaces [boolean]
+ --include-root Include the workspace root; must provide '--all'
+ [boolean]
+ -w, --workspace Run script in a specific workspace or workspaces
+ [array]
+ -p, --pkg-manager, --pm Use a specific package manager
+ [array] [default: npm@latest]
+
+Output:
+ --json Output JSON only. Alias for "--reporter=json" [boolean]
+ -r, --reporter Reporter(s) to use [array] [default: console]
+ --verbose Enable verbose output [boolean] [default: false]
+
+Script Behavior:
+ --add Additional dependency to provide to script(s)
+ [array]
+ --bail, --fail-fast Halt on first error [boolean]
+ --lint Lint package artifacts after running script(s)
+ [boolean] [default: true]
+ --loose, --if-present Ignore missing scripts (used with --all) [boolean]
+
+Options:
+ --version Show version number [boolean]
+ -P, --plugin, --plugins Plugin(s) to use [array]
+ --help Show help [boolean]
+
+```
+
+:::note[DRY]
+
+You'll notice that the ["Input"](#lint-input-options) and ["Output"](#lint-output-options) options are the same as those for [`lint`](#command-lint); as are [Global Options](#global-options).
+
+Options specific to `run-script` are described below.
+
+:::
+
+### `run-script` Behavior Options
+
+These options control how `smoker` will run your custom script(s).
+
+#### `--add`
+
+Additional dependency to provide to script(s). Each dependency provided this way will be installed--for each package manager specified--as if it were a "sibling" of the package artifact under test.
+
+The value of this option may be [whatever your package manager(s) can install](https://docs.npmjs.com/cli/v10/commands/npm-install#description).
+
+This option may be provided multiple times.
+
+:::caution[Consider This]
+
+It may be tempting to use your favorite test framework or some other tooling here, but doing so means that the testing environment will _not_ be the same as if a consumer installed it in isolation. While `midnight-smoker` takes measures to ensure that transitive dependencies from packages installed via `--add` do not get co-mingled with your installed artifact, it can make no guarantees. How important is this? Up to you.
+
+_If it were me_, I'd probably pair `midnight-smoker` with a static analyzer like [`dependency-cruiser`](https://npm.im/dependency-cruiser) to make sure I've got my dependencies straight.
+
+:::
+
+#### `--bail`, `--fail-fast`
+
+Halt on the first failed script.
+
+If this option is not provided, `smoker` will continue to run scripts even if one or more fail.
+
+`smoker` will still attempt to exit gracefully. Also: "fast" is relative.
+
+#### `--no-lint` / `--lint=false`
+
+**By default**, `smoker run-script` will run the [`lint`](#command-lint) command after custom script execution is complete.
+
+To disable this behavior, provide `--no-lint` or `--lint=false`.
+
+Best used when you only want to run custom scripts--especially against multiple package managers.
+
+#### `--loose`, `--if-present`
+
+Do not fail if a workspace's`package.json` does not contain the script(s) provided.
+
+Only applicable when used with [`--all`](#--all) or [`--workspace`](#-w---workspace).
+
+## Further Reading
+
+See the [CLI Guide](/guides/cli) for more in-depth recipes and examples.
diff --git a/packages/docs/src/content/docs/reference/config.mdx b/packages/docs/src/content/docs/reference/config.mdx
new file mode 100644
index 000000000..b54226ab4
--- /dev/null
+++ b/packages/docs/src/content/docs/reference/config.mdx
@@ -0,0 +1,585 @@
+---
+title: Configuration
+description: 'Configuring midnight-smoker'
+---
+
+import {Tabs, TabItem} from '@astrojs/starlight/components';
+
+I know what you're thinking: _"I just don't have enough config files!"_ **midnight-smoker** solves this problem by providing you _the opportunity to add another one_.
+
+## Conventions
+
+For the purposes of this documentation, we'll use the following conventions:
+
+- `kebab-case` will be used as the "canonical" name.
+- JSON examples will use `kebab-case`.
+- JavaScript examples will use `camelCase`.
+
+Some config file options are only used when run with certain CLI commands. This is denoted in the documentation below with the **Command** label.
+
+## Configuration Files
+
+Configuration may be specified in the `smoker` field of your `package.json`, or one of:
+
+- `.smokerrc.(json|js|cjs|mjs)`
+- `smoker.config.(json|js|cjs|mjs)`
+- `.config/smokerrc.(json|js|cjs|mjs)`
+- `.config/smoker.config.(json|js|cjs|mjs)`
+
+`smoker` will look for the closest of these files from the current working directory.
+
+## Compared to the CLI
+
+This section explains some arguably-important things about how config options relate to CLI options.
+
+### Equivalent Options
+
+_Most_ config options have equivalent [command-line options](/reference/cli).
+
+Generally, _if_ the type of a configuration option is more complex than an array of strings (which can be expressed by repeating the option on the CLI), the option _will not_ be available on the CLI. This is because there's _no good way to express it_. Fight me.
+
+### CLI > Config
+
+The CLI takes precedence over any options set in a config file.
+
+In other words, if an option is provided on the command-line, it will _override_ the equivalent configuration option. Which is probably what you expect to happen.
+
+## Configuration Options
+
+:::note[Option Names]
+
+In a config file, option names may be specified in `camelCase` or `kebab-case`. For example, `includeRoot` and `include-root` are equivalent.
+
+:::
+
+### `add`
+
+- **Summary:** An array of packages to be installed alongside your package(s) prior to running custom smoke tests.
+- **Type:** `string | string[]`
+- **Default:** `[]`
+- **Command**: `run-script` / `run`
+
+:::caution[Here Be Dragons]
+
+Read the [scary stuff about `--add`](/reference/cli#--add) before using this option.
+
+:::
+
+#### `add` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "add": ["mocha", "chai"]
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ add: ['mocha', 'chai'],
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ add: ['mocha', 'chai'],
+};
+```
+
+
+
+
+
+### `all`
+
+**Operate on all workspaces.**
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+The workspace root is excluded by default. To include it, see [`include-root`](#include-root).
+
+#### `all` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "all": true
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ all: true,
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ all: true,
+};
+```
+
+
+
+
+
+### `bail`
+
+**When running custom scripts, abort upon the first non-zero script exit.**
+
+- **Type:** `boolean`
+- **Default:** `false`
+- **Alias:** `fail-fast`
+- **Command**: `run-script` / `run`
+
+#### `bail` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "bail": true
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ bail: true,
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ bail: true,
+};
+```
+
+
+
+
+
+### `lint`
+
+**If `false`, do not run the `lint` command.**
+
+- **Type:** `boolean`
+- **Default:** `true`
+- **Command**: `run-script` / `run`
+
+#### `lint` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "lint": false
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ lint: false,
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ lint: false,
+};
+```
+
+
+
+
+
+### `include-root`
+
+**If [`all`](#all) is `true`, operate on the workspace root as well.**
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+Providing `include-root: true` without `all: true` is considered an error.
+
+#### `include-root` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "all": true,
+ "include-root": true
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ all: true,
+ includeRoot: true,
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ all: true,
+ includeRoot: true,
+};
+```
+
+
+
+
+
+### `json`
+
+**Output results as JSON.**
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+This is an alias for `--reporter=json`; see [`reporter`](#reporter).
+
+#### `json` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "json": true
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ json: true,
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ json: true,
+};
+```
+
+
+
+
+
+### `linger`
+
+**After completion, `midnight-smoker` will leave its temp directories intact; the directories will be printed to `stdout`.**
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+This is handy if you're having a tough time debugging a failure.
+
+:::caution[Don't Let linger Linger]
+
+`linger` is intended to be used with the command-line interface as a one-off. Leaving it in your config file could create a whole lot of temp directories.
+
+:::
+
+#### `linger` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "linger": true
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ linger: true,
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ linger: true,
+};
+```
+
+
+
+
+
+### `loose`
+
+**Do not fail if the specified custom script is not found in a workspace's `package.json`.**
+
+- **Type:** `boolean`
+- **Default:** `false`
+- **Alias:** `if-present`
+- **Command**: `run-script` / `run`
+
+#### `loose` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "loose": true
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ loose: true,
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ loose: true,
+};
+```
+
+
+
+
+
+### `plugin`
+
+**Specify one or more plugins to use.**
+
+- **Type:** `string | string[]`
+- **Default:** `[]`
+
+#### `plugin` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "plugin": ["@midnight-smoker/plugin-typescript"]
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ plugin: ['@midnight-smoker/plugin-typescript'],
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ plugin: ['@midnight-smoker/plugin-typescript'],
+};
+```
+
+
+
+
+
+### `reporter`
+
+**Specify one or more [reporters](/reference/reporters) to use.**
+
+- **Type:** `string | string[]`
+- **Default:** `console`
+
+#### `reporter` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "reporter": ["json"]
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ reporter: ['json'],
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ reporter: ['json'],
+};
+```
+
+
+
+
+
+### `rules`
+
+**Rule-specific configuration.**
+
+- **Type:** `Record`
+
+In the type above, `RuleOptions` refers to rule-specific options; refer to the [Built-in Rules Reference](/reference/rules) for more information.
+
+`RuleSeverity` refers to a [rule severity](#rule-severity) of `error`, `warn`, or `off`.
+
+#### Rule Severity
+
+Each rule can have a user-configured _severity_. The severity of a rule can be one of:
+
+- `off`: The rule is disabled and _will not be run_
+- `warn`: The rule is enabled, but _will not_ cause `smoker` to exit with a non-zero status code
+- `error`: The rule is enabled, and _will_ cause `smoker` to exit with a non-zero status code
+
+The default severity level for each built-in rule is `error`.
+
+Rules _may_ override this behavior (i.e. a default severity of `warn` or `off`).
+
+#### `rules` Examples
+
+
+
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-banned-files": [
+ "error",
+ {
+ "deny": ["file_id.diz"],
+ "allow": ["aws_secret.txt"]
+ }
+ ],
+ "no-missing-entry-point": "off",
+ "no-missing-exports": [
+ "warn",
+ {
+ "types": false,
+ "require": false
+ }
+ ]
+ }
+}
+```
+
+
+
+
+```js title="smoker.config.cjs"
+module.exports = {
+ rules: {
+ 'no-banned-files': [
+ 'error',
+ {
+ deny: ['file_id.diz'],
+ allow: ['aws_secret.txt'],
+ },
+ ],
+ 'no-missing-entry-point': 'off',
+ 'no-missing-exports': [
+ 'warn',
+ {
+ types: false,
+ require: false,
+ },
+ ],
+ },
+};
+```
+
+
+
+
+```js title="smoker.config.mjs"
+export default {
+ rules: {
+ 'no-banned-files': [
+ 'error',
+ {
+ deny: ['file_id.diz'],
+ allow: ['aws_secret.txt'],
+ },
+ ],
+ 'no-missing-entry-point': 'off',
+ 'no-missing-exports': [
+ 'warn',
+ {
+ types: false,
+ require: false,
+ },
+ ],
+ },
+};
+```
+
+
+
+
diff --git a/packages/docs/src/content/docs/reference/index.mdx b/packages/docs/src/content/docs/reference/index.mdx
new file mode 100644
index 000000000..9a364a9c9
--- /dev/null
+++ b/packages/docs/src/content/docs/reference/index.mdx
@@ -0,0 +1,4 @@
+---
+title: Reference
+description: Reference materials for midnight-smoker
+---
diff --git a/packages/docs/src/content/docs/reference/rules.mdx b/packages/docs/src/content/docs/reference/rules.mdx
new file mode 100644
index 000000000..0ba23985b
--- /dev/null
+++ b/packages/docs/src/content/docs/reference/rules.mdx
@@ -0,0 +1,108 @@
+---
+title: 'Built-in Rules'
+description: Reference documentation for midnight-smoker's built-in rules
+---
+
+import RuleExamples from '../../../components/RuleExamples.astro';
+
+`midnight-smoker` contains a built-in collection of ["rules"](#rules) which together behave as a _linter_ for your to-be-published package.
+
+## Rules
+
+The following contains descriptions and examples of the built-in rules provided by `midnight-smoker`.
+
+### `no-banned-files`
+
+**Ensures banned files do not exist in the package artifact.**
+
+`no-banned-files` uses a list of known sensitive files, and can be configured to allow or deny additional files.
+
+#### `no-banned-files` Options
+
+| Name | Type | Default | Description |
+| ------- | ---------- | ------- | --------------------------------------- |
+| `allow` | `string[]` | `[]` | A list of filenames to explicitly allow |
+| `deny` | `string[]` | `[]` | A list of filenames to explicitly deny |
+
+#### `no-banned-files` Configuration Examples
+
+
+
+#### `no-banned-files` Notes
+
+Portions adapted from [ban-sensitive-files](https://github.com/bahmutov/ban-sensitive-files), including `git-deny-patterns.json` and the `reToRegExp` function.
+
+### `no-missing-entry-point`
+
+**Ensures a CommonJS package has a resolvable entry point within the package artifact.**
+
+`no-missing-entry-point` checks the file specified in the `main` field. If the `main` field isn't present in `package.json`, the rule looks for these files in the root of the artifact, in order:
+
+- `index.js`
+- `index.json`
+- `index.node`
+
+This mimics [Node.js' behavior](https://nodejs.org/api/modules.html#all-together).
+
+:::note[CommonJS Only]
+
+This rule has no effect on `"type": "module"` (ESM) packages.
+
+:::
+
+#### `no-missing-entry-point` Options
+
+`no-missing-entry-point` does not accept options.
+
+#### `no-missing-entry-point` Configuration Examples
+
+
+
+### `no-missing-pkg-files`
+
+**Ensures any files referenced in `package.json` exist in the package artifact.**
+
+#### `no-missing-pkg-files` Options
+
+| Name | Type | Default | Description |
+| --------- | ---------- | ------- | ------------------------------------------------------------ |
+| `bin` | `boolean` | `true` | Check the file(s) referenced by the `bin` field, if present |
+| `browser` | `boolean` | `true` | Check the file referenced by the `browser` field, if present |
+| `types` | `boolean` | `true` | Check the file referenced by the `types` field, if present |
+| `unpkg` | `boolean` | `true` | Check the file referenced by the `unpkg` field, if present |
+| `module` | `boolean` | `true` | Check the file referenced by the `module` field, if present |
+| `fields` | `string[]` | `[]` | Check additional file(s) referenced by the given fields |
+
+#### `no-missing-pkg-files` Configuration Examples
+
+
+
+### `no-missing-exports`
+
+**Ensures all files referenced in the `exports` field exist in the package artifact _and_ performs context-specific checks.**
+
+`no-missing-exports` checks the `exports` field for missing files and weirdness in both CJS and ESM packages.
+
+_All_ files and globs referenced are checked for existence in the package artifact. Context-specific checks can be enabled with the below options.
+
+#### `no-missing-exports` Options
+
+| Name | Type | Default | Description |
+| --------- | --------- | ------- | -------------------------------------------------------------------------------------------- |
+| `types` | `boolean` | `true` | Check the file referenced by the conditional `types` export has a `.d.ts` extension |
+| `require` | `boolean` | `true` | Check the file referenced by the conditional `require` export references a CommonJS script |
+| `import` | `boolean` | `true` | Check the file referenced by the conditional `import` export references an ECMAScript module |
+| `order` | `boolean` | `true` | Check the conditional `default` export is the _last_ field in its object |
+| `glob` | `boolean` | `true` | If `false`, disallow glob patterns in the `exports` field |
+
+#### `no-missing-exports` Configuration Examples
+
+
+
+#### `no-missing-exports` Notes
+
+The context-specific checks should probably be in their own rule.
+
+## Caveats
+
+_All_ options are _optional_.
diff --git a/packages/docs/src/content/rule-examples/no-banned-files-custom-allow.md b/packages/docs/src/content/rule-examples/no-banned-files-custom-allow.md
new file mode 100644
index 000000000..b93a05dd7
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-banned-files-custom-allow.md
@@ -0,0 +1,13 @@
+---
+rule: no-banned-files
+label: Explicitly Allow Banned Files
+description: This explicitly allows a SSH private key file in your package artifacts, which is profoundly bad idea. But at least you're printing a warning, right? Good job today.
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-banned-files": ["warn", {"allow": ["id_rsa"]}]
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-banned-files-custom-deny.md b/packages/docs/src/content/rule-examples/no-banned-files-custom-deny.md
new file mode 100644
index 000000000..a0da29f71
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-banned-files-custom-deny.md
@@ -0,0 +1,13 @@
+---
+rule: no-banned-files
+label: Explicitly Ban Files
+description: If file_id.diz and/or .DS_Store is found in the package artifact, the check will fail.
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-banned-files": {"deny": ["file_id.diz", ".DS_Store"]}
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-banned-files-defaults.md b/packages/docs/src/content/rule-examples/no-banned-files-defaults.md
new file mode 100644
index 000000000..5bfc63c8c
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-banned-files-defaults.md
@@ -0,0 +1,12 @@
+---
+rule: no-banned-files
+label: Defaults
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-banned-files": ["error", {"allow": [], "deny": []}]
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-banned-files-disabled.md b/packages/docs/src/content/rule-examples/no-banned-files-disabled.md
new file mode 100644
index 000000000..a4f8c14c5
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-banned-files-disabled.md
@@ -0,0 +1,12 @@
+---
+rule: no-banned-files
+label: Disabled
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-banned-files": "off"
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-entry-point-defaults.md b/packages/docs/src/content/rule-examples/no-missing-entry-point-defaults.md
new file mode 100644
index 000000000..2dfe973f7
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-entry-point-defaults.md
@@ -0,0 +1,12 @@
+---
+rule: no-missing-entry-point
+label: Defaults
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-entry-point": "error"
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-entry-point-disabled.md b/packages/docs/src/content/rule-examples/no-missing-entry-point-disabled.md
new file mode 100644
index 000000000..01d107c81
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-entry-point-disabled.md
@@ -0,0 +1,12 @@
+---
+rule: no-missing-entry-point
+label: Disabled
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-entry-point": "off"
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-exports-defaults.md b/packages/docs/src/content/rule-examples/no-missing-exports-defaults.md
new file mode 100644
index 000000000..910aecaf5
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-exports-defaults.md
@@ -0,0 +1,21 @@
+---
+rule: no-missing-exports
+label: Defaults
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-exports": [
+ "error",
+ {
+ "types": true,
+ "require": true,
+ "import": true,
+ "order": true,
+ "glob": true
+ }
+ ]
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-exports-disabled.md b/packages/docs/src/content/rule-examples/no-missing-exports-disabled.md
new file mode 100644
index 000000000..36de2c656
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-exports-disabled.md
@@ -0,0 +1,12 @@
+---
+rule: no-missing-exports
+label: Disabled
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-exports": "off"
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-pkg-files-custom-fields.md b/packages/docs/src/content/rule-examples/no-missing-pkg-files-custom-fields.md
new file mode 100644
index 000000000..f973517d2
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-pkg-files-custom-fields.md
@@ -0,0 +1,18 @@
+---
+rule: no-missing-pkg-files
+label: Check Custom Fields
+description: If the files referenced by the despair and/or alienation fields in the package's `package.json` are missing from the package artifact, this rule will fail.
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-pkg-files": [
+ "warn",
+ {
+ "fields": ["despair", "alienation"]
+ }
+ ]
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-pkg-files-defaults.md b/packages/docs/src/content/rule-examples/no-missing-pkg-files-defaults.md
new file mode 100644
index 000000000..7f5f252aa
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-pkg-files-defaults.md
@@ -0,0 +1,22 @@
+---
+rule: no-missing-pkg-files
+label: Defaults
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-pkg-files": [
+ "error",
+ {
+ "bin": true,
+ "browser": true,
+ "types": true,
+ "unpkg": true,
+ "module": true,
+ "fields": []
+ }
+ ]
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-pkg-files-disabled.md b/packages/docs/src/content/rule-examples/no-missing-pkg-files-disabled.md
new file mode 100644
index 000000000..749cc3e57
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-pkg-files-disabled.md
@@ -0,0 +1,12 @@
+---
+rule: no-missing-pkg-files
+label: Disabled
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-pkg-files": "off"
+ }
+}
+```
diff --git a/packages/docs/src/content/rule-examples/no-missing-pkg-files-ignore-common-fields.md b/packages/docs/src/content/rule-examples/no-missing-pkg-files-ignore-common-fields.md
new file mode 100644
index 000000000..d68d80622
--- /dev/null
+++ b/packages/docs/src/content/rule-examples/no-missing-pkg-files-ignore-common-fields.md
@@ -0,0 +1,19 @@
+---
+rule: no-missing-pkg-files
+label: Ignore Common Fields
+description: If files referenced by these common fields are missing from the package artifact, this rule will not fail.
+---
+
+```json title="smoker.config.json"
+{
+ "rules": {
+ "no-missing-pkg-files": {
+ "bin": false,
+ "browser": false,
+ "types": false,
+ "unpkg": false,
+ "module": false
+ }
+ }
+}
+```
diff --git a/packages/docs/src/env.d.ts b/packages/docs/src/env.d.ts
new file mode 100644
index 000000000..9c03f0a2c
--- /dev/null
+++ b/packages/docs/src/env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/packages/docs/src/styles/main.css b/packages/docs/src/styles/main.css
new file mode 100644
index 000000000..95fcb5e3d
--- /dev/null
+++ b/packages/docs/src/styles/main.css
@@ -0,0 +1,115 @@
+/* Dark mode colors. */
+:root {
+ --sl-color-accent-low: #1e242c;
+ --sl-color-accent: #566c85;
+ --sl-color-accent-high: #c1c9d3;
+ --sl-color-white: #ffffff;
+ --sl-color-gray-1: #eceef2;
+ --sl-color-gray-2: #c0c2c7;
+ --sl-color-gray-3: #888b96;
+ --sl-color-gray-4: #545861;
+ --sl-color-gray-5: #353841;
+ --sl-color-gray-6: #24272f;
+ --sl-color-black: #17181c;
+ --svg-invert-filter: invert(90%) hue-rotate(180deg);
+}
+/* Light mode colors. */
+:root[data-theme='light'] {
+ --sl-color-accent-low: #d1d7df;
+ --sl-color-accent: #586e87;
+ --sl-color-accent-high: #2a333e;
+ --sl-color-white: #17181c;
+ --sl-color-gray-1: #24272f;
+ --sl-color-gray-2: #353841;
+ --sl-color-gray-3: #545861;
+ --sl-color-gray-4: #888b96;
+ --sl-color-gray-5: #c0c2c7;
+ --sl-color-gray-6: #eceef2;
+ --sl-color-gray-7: #f5f6f8;
+ --sl-color-black: #ffffff;
+
+ --svg-invert-filter: none;
+}
+
+.site-title img,
+.hero img {
+ filter: var(--svg-invert-filter);
+}
+
+.sl-markdown-content h1 {
+ display: none;
+}
+
+/* Custom styles for heading anchor links. */
+.sl-markdown-content :global(.heading-wrapper) {
+ --icon-size: 0.75em;
+ --icon-spacing: 0.25em;
+ line-height: var(--sl-line-height-headings);
+}
+
+/* Set font-size on wrapper element, so line-height, margins etc. match heading size. */
+.sl-markdown-content :global(.level-h2) {
+ font-size: var(--sl-text-h2);
+}
+.sl-markdown-content :global(.level-h3) {
+ font-size: var(--sl-text-h3);
+}
+.sl-markdown-content :global(.level-h4) {
+ font-size: var(--sl-text-h4);
+}
+.sl-markdown-content :global(.level-h5) {
+ font-size: var(--sl-text-h5);
+}
+
+.sl-markdown-content :global(.heading-wrapper > :first-child) {
+ margin-inline-end: calc(var(--icon-size) + var(--icon-spacing));
+ display: inline;
+}
+
+.sl-markdown-content :global(.anchor-link) {
+ margin-inline-start: calc(-1 * (var(--icon-size)));
+ color: var(--sl-color-gray-3);
+}
+.sl-markdown-content :global(.anchor-link:hover),
+.sl-markdown-content :global(.anchor-link:focus) {
+ color: var(--sl-color-text-accent);
+}
+.sl-markdown-content :global(.heading-wrapper svg) {
+ display: inline;
+ width: var(--icon-size);
+}
+
+@media (hover: hover) {
+ .sl-markdown-content :global(.anchor-link) {
+ opacity: 0;
+ }
+}
+.sl-markdown-content :global(.heading-wrapper:hover > .anchor-link),
+.sl-markdown-content :global(.anchor-link:focus) {
+ opacity: 1;
+}
+
+/* Float anchor links to the left of headings on larger screens. */
+@media (min-width: 95em) {
+ .sl-markdown-content :global(.heading-wrapper) {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ gap: var(--icon-spacing);
+ margin-inline-start: calc(-1 * (var(--icon-size) + var(--icon-spacing)));
+ }
+ .sl-markdown-content :global(.heading-wrapper > :first-child),
+ .sl-markdown-content :global(.anchor-link) {
+ margin: 0;
+ }
+}
+
+.sl-markdown-content table:not(:where(.not-content *)) {
+ display: block;
+ overflow: auto;
+ border-collapse: collapse;
+}
+
+.sl-markdown-content :is(th, td):not(:where(.not-content *)) > code {
+ overflow-wrap: normal;
+}
diff --git a/packages/docs/tsconfig.json b/packages/docs/tsconfig.json
new file mode 100644
index 000000000..bcbf8b509
--- /dev/null
+++ b/packages/docs/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "astro/tsconfigs/strict"
+}
diff --git a/packages/plugin-default/.config/typedoc.js b/packages/plugin-default/.config/typedoc.js
new file mode 100644
index 000000000..b9bb2d6b4
--- /dev/null
+++ b/packages/plugin-default/.config/typedoc.js
@@ -0,0 +1,5 @@
+const path = require('node:path');
+
+module.exports = require('../../../.config/typedoc.workspace')(
+ path.resolve(__dirname, '..'),
+);
diff --git a/packages/test-util/.config/typedoc.js b/packages/test-util/.config/typedoc.js
new file mode 100644
index 000000000..b9bb2d6b4
--- /dev/null
+++ b/packages/test-util/.config/typedoc.js
@@ -0,0 +1,5 @@
+const path = require('node:path');
+
+module.exports = require('../../../.config/typedoc.workspace')(
+ path.resolve(__dirname, '..'),
+);
diff --git a/typedoc.json b/typedoc.json
deleted file mode 100644
index 8a398a933..000000000
--- a/typedoc.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "$schema": "https://typedoc.org/schema.json",
- "entryPoints": [
- "./packages/midnight-smoker",
- "./packages/plugin-default",
- "./packages/test-util"
- ],
- "out": "docs/api",
- "entryPointStrategy": "packages",
- "plugin": ["typedoc-plugin-zod"],
- "excludeInternal": true,
- "excludePrivate": true,
- "cleanOutputDir": true,
- "logLevel": "Info"
-}