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/LinkCard.astro b/packages/docs/src/components/LinkCard.astro new file mode 100644 index 000000000..2d2823c86 --- /dev/null +++ b/packages/docs/src/components/LinkCard.astro @@ -0,0 +1,79 @@ +--- +import {Icon} from '@astrojs/starlight/components'; +import type {Props} from '@astrojs/starlight/props'; + +const {title, description, ...attributes} = Astro.props; +--- + +
+ + + + + { + description ? ( + + ) : ( + + + + ) + } + + +
+ + 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" +┌────────────┬─────────┬──────────────────────────────────────────┬────────────┐ +│ Name │ Version │ Description │ Resolved │ +├────────────┼─────────┼──────────────────────────────────────────┼────────────┤ +│ (built-in) │ 0.0.0 │ Default behavior for midnight-smoker │ ../../pac… │ +├────────────┼─────────┼──────────────────────────────────────────┼────────────┤ +│ example │ 1.0.0 │ Provides a rule which validates licenses │ index.ts │ +└────────────┴─────────┴──────────────────────────────────────────┴────────────┘ + +``` + +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" +┌────────────────────────┬─────────────────────────────────────────┬───────────┐ +│ Name │ Description │ Plugin │ +├────────────────────────┼─────────────────────────────────────────┼───────────┤ +│ no-banned-files │ Ensures banned files won't be published │ (builtin) │ +│ │ to the registry │ │ +├────────────────────────┼─────────────────────────────────────────┼───────────┤ +│ no-missing-entry-point │ Checks that the package contains an │ (builtin) │ +│ │ entry point; only applies to CJS │ │ +│ │ packages without an "exports" field │ │ +├────────────────────────┼─────────────────────────────────────────┼───────────┤ +│ no-missing-exports │ Checks that all files in the "exports" │ (builtin) │ +│ │ field (if present) exist │ │ +├────────────────────────┼─────────────────────────────────────────┼───────────┤ +│ no-missing-pkg-files │ Checks that files referenced in │ (builtin) │ +│ │ package.json exist in the tarball │ │ +├────────────────────────┼─────────────────────────────────────────┼───────────┤ +│ example/no-unlicensed │ Checks that a package has a license │ example │ +└────────────────────────┴─────────────────────────────────────────┴───────────┘ + +``` + +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" -}