@activatus/code-standards 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/docs/Guides/_category_.json +2 -2
- package/docs/Guides/migrating.md +50 -25
- package/docs/Guides/setup.md +52 -17
- package/docs/Standards/_category_.json +2 -2
- package/docs/Standards/agent-skills.md +29 -29
- package/docs/Standards/agent-specs.md +17 -32
- package/docs/Standards/direnv.mdx +8 -8
- package/docs/Standards/documentation.md +5 -2
- package/docs/Standards/editorconfig.md +23 -21
- package/docs/Standards/eslint.md +593 -402
- package/docs/Standards/human-readable-names.md +62 -22
- package/docs/Standards/lint-errors.md +3 -7
- package/docs/Standards/prettier.md +35 -26
- package/docs/Standards/readable-code.md +30 -0
- package/docs/Standards/stylelint.md +26 -11
- package/docs/Standards/styling.md +9 -3
- package/docs/Standards/tech-stack.md +3 -2
- package/docs/Structure/_category_.json +2 -2
- package/docs/Structure/component-organization.md +9 -2
- package/docs/Structure/folder-structure.md +26 -13
- package/docs/Structure/naming-conventions.md +0 -2
- package/docs/intro.md +1 -3
- package/docs/ontology.md +21 -26
- package/docs/templates.mdx +12 -49
- package/docs/version-history.md +143 -10
- package/package.json +6 -2
- package/scripts/renameSymbol.mjs +230 -0
- package/static/templates/.editorconfig +8 -11
- package/static/templates/AGENTS.md +16 -13
- package/static/templates/docs/development/coding-standards.md +4 -0
- package/static/templates/docsSite/docusaurus.config.mjs +144 -0
- package/static/templates/docsSite/sidebars.mjs +7 -0
- package/static/templates/docsSite/src/css/custom.css +6 -0
- package/static/templates/docsSite/tsconfig.json +5 -5
- package/static/templates/eslint/base/eslint.config.mjs +95 -70
- package/static/templates/eslint/nextjs/eslint.config.mjs +170 -126
- package/static/templates/eslint/react/eslint.config.mjs +160 -125
- package/static/templates/eslint/typescript/eslint.config.mjs +143 -115
- package/static/templates/globals.css +9 -9
- package/static/templates/next.config.mjs +13 -10
- package/static/templates/prettier.config.mjs +26 -0
- package/static/templates/stylelint/css/stylelint.config.mjs +37 -25
- package/static/templates/stylelint/scss/stylelint.config.mjs +10 -7
- package/docs/Standards/branch-naming.md +0 -66
- package/docs/Standards/conventional-commits.md +0 -189
- package/static/templates/.commitlintrc.json +0 -3
- package/static/templates/docsSite/docusaurus.config.ts +0 -119
- package/static/templates/docsSite/sidebars.ts +0 -8
- package/static/templates/husky/commit-msg +0 -1
- package/static/templates/husky/pre-commit +0 -1
- package/static/templates/prettier.config.js +0 -17
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Renames a TypeScript symbol everywhere it is used, with TypeScript's own rename: the same engine as an
|
|
3
|
+
// editor's Rename Symbol (F2). For agents working from the command line. Never use find-and-replace.
|
|
4
|
+
//
|
|
5
|
+
// Run it from the project root, with the file where the symbol is declared:
|
|
6
|
+
// node node_modules/@activatus/code-standards/scripts/renameSymbol.mjs <file> <currentName> <newName>
|
|
7
|
+
// For example:
|
|
8
|
+
// node node_modules/@activatus/code-standards/scripts/renameSymbol.mjs components/ui/button/types.ts ButtonProperties ButtonProps
|
|
9
|
+
//
|
|
10
|
+
// To do many renames in one run, list them in a text file, one per line: <file> <currentName> <newName>.
|
|
11
|
+
// Blank lines and lines starting with # are skipped. The renames run in order.
|
|
12
|
+
// node node_modules/@activatus/code-standards/scripts/renameSymbol.mjs --list renames.txt
|
|
13
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
// process.argv starts with the node binary and this script's path.
|
|
18
|
+
const FIRST_ARGUMENT_INDEX = 2;
|
|
19
|
+
const LIST_FLAG = "--list";
|
|
20
|
+
// A rename line has three parts: file, current name, new name.
|
|
21
|
+
const RENAME_PARTS = 3;
|
|
22
|
+
const USAGE = [
|
|
23
|
+
"Usage: node renameSymbol.mjs <file> <currentName> <newName>",
|
|
24
|
+
" or: node renameSymbol.mjs --list <file of renames>",
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
// Uses the project's own TypeScript, so the rename matches the project's compiler exactly.
|
|
28
|
+
const requireFromProject = createRequire(
|
|
29
|
+
path.join(process.cwd(), "package.json"),
|
|
30
|
+
);
|
|
31
|
+
const typescript = requireFromProject("typescript");
|
|
32
|
+
|
|
33
|
+
const readProjectSettings = () => {
|
|
34
|
+
const configPath = typescript.findConfigFile(
|
|
35
|
+
process.cwd(),
|
|
36
|
+
typescript.sys.fileExists,
|
|
37
|
+
"tsconfig.json",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (!configPath) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"No tsconfig.json found. Run this from the project root.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const configFile = typescript.readConfigFile(
|
|
47
|
+
configPath,
|
|
48
|
+
typescript.sys.readFile,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return typescript.parseJsonConfigFileContent(
|
|
52
|
+
configFile.config,
|
|
53
|
+
typescript.sys,
|
|
54
|
+
path.dirname(configPath),
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const createLanguageService = ({ projectSettings }) => {
|
|
59
|
+
const host = {
|
|
60
|
+
getScriptFileNames: () => projectSettings.fileNames,
|
|
61
|
+
getScriptVersion: () => "0",
|
|
62
|
+
getScriptSnapshot: (fileName) =>
|
|
63
|
+
typescript.sys.fileExists(fileName)
|
|
64
|
+
? typescript.ScriptSnapshot.fromString(
|
|
65
|
+
typescript.sys.readFile(fileName),
|
|
66
|
+
)
|
|
67
|
+
: undefined,
|
|
68
|
+
getCurrentDirectory: () => process.cwd(),
|
|
69
|
+
getCompilationSettings: () => projectSettings.options,
|
|
70
|
+
getDefaultLibFileName: (options) =>
|
|
71
|
+
typescript.getDefaultLibFilePath(options),
|
|
72
|
+
fileExists: typescript.sys.fileExists,
|
|
73
|
+
readFile: typescript.sys.readFile,
|
|
74
|
+
readDirectory: typescript.sys.readDirectory,
|
|
75
|
+
directoryExists: typescript.sys.directoryExists,
|
|
76
|
+
getDirectories: typescript.sys.getDirectories,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return typescript.createLanguageService(
|
|
80
|
+
host,
|
|
81
|
+
typescript.createDocumentRegistry(),
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// The first place the name appears as a declaration in the file: an interface, type, function, variable or parameter.
|
|
86
|
+
const findDeclarationPosition = ({ sourceFile, currentName }) => {
|
|
87
|
+
const visit = (node) => {
|
|
88
|
+
const isDeclarationName =
|
|
89
|
+
typescript.isIdentifier(node) &&
|
|
90
|
+
node.text === currentName &&
|
|
91
|
+
node.parent?.name === node;
|
|
92
|
+
|
|
93
|
+
return isDeclarationName
|
|
94
|
+
? node.getStart(sourceFile)
|
|
95
|
+
: typescript.forEachChild(node, visit);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return visit(sourceFile);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const endOf = (location) =>
|
|
102
|
+
location.textSpan.start + location.textSpan.length;
|
|
103
|
+
|
|
104
|
+
const applyEdits = ({ renameLocations, newName }) => {
|
|
105
|
+
const locationsByFile = Map.groupBy(
|
|
106
|
+
renameLocations,
|
|
107
|
+
(location) => location.fileName,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
for (const [fileName, locations] of locationsByFile) {
|
|
111
|
+
const originalText = readFileSync(fileName, "utf8");
|
|
112
|
+
const sortedFromStart = locations.toSorted(
|
|
113
|
+
(first, second) => first.textSpan.start - second.textSpan.start,
|
|
114
|
+
);
|
|
115
|
+
const pieces = sortedFromStart.flatMap((location, index) => {
|
|
116
|
+
const previousEnd =
|
|
117
|
+
index === 0 ? 0 : endOf(sortedFromStart[index - 1]);
|
|
118
|
+
const replacement = `${location.prefixText ?? ""}${newName}${location.suffixText ?? ""}`;
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
originalText.slice(previousEnd, location.textSpan.start),
|
|
122
|
+
replacement,
|
|
123
|
+
];
|
|
124
|
+
});
|
|
125
|
+
const lastLocation = sortedFromStart.at(-1);
|
|
126
|
+
|
|
127
|
+
writeFileSync(
|
|
128
|
+
fileName,
|
|
129
|
+
pieces.join("") + originalText.slice(endOf(lastLocation)),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return locationsByFile.size;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const renameSymbol = ({ filePath, currentName, newName }) => {
|
|
137
|
+
const projectSettings = readProjectSettings();
|
|
138
|
+
const languageService = createLanguageService({ projectSettings });
|
|
139
|
+
const fileName = path.resolve(filePath);
|
|
140
|
+
const sourceFile = languageService
|
|
141
|
+
.getProgram()
|
|
142
|
+
?.getSourceFile(fileName);
|
|
143
|
+
|
|
144
|
+
if (!sourceFile) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`${filePath} is not part of the TypeScript project in tsconfig.json.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const position = findDeclarationPosition({
|
|
151
|
+
sourceFile,
|
|
152
|
+
currentName,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (position === undefined) {
|
|
156
|
+
throw new Error(`${currentName} is not declared in ${filePath}.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const renameInfo = languageService.getRenameInfo(
|
|
160
|
+
fileName,
|
|
161
|
+
position,
|
|
162
|
+
{
|
|
163
|
+
allowRenameOfImportPath: false,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!renameInfo.canRename) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`TypeScript cannot rename ${currentName}: ${renameInfo.localizedErrorMessage}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const renameLocations = languageService.findRenameLocations(
|
|
174
|
+
fileName,
|
|
175
|
+
position,
|
|
176
|
+
false,
|
|
177
|
+
false,
|
|
178
|
+
{
|
|
179
|
+
providePrefixAndSuffixTextForRename: true,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
const fileCount = applyEdits({ renameLocations, newName });
|
|
183
|
+
const fileWord = fileCount === 1 ? "file" : "files";
|
|
184
|
+
|
|
185
|
+
return `Renamed ${currentName} to ${newName}: ${renameLocations.length} places in ${fileCount} ${fileWord}.`;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Each line of a list file is one rename: <file> <currentName> <newName>.
|
|
189
|
+
const readRenameList = (listPath) =>
|
|
190
|
+
readFileSync(listPath, "utf8")
|
|
191
|
+
.split("\n")
|
|
192
|
+
.map((line) => line.trim())
|
|
193
|
+
.filter((line) => line !== "" && !line.startsWith("#"))
|
|
194
|
+
.map((line) => {
|
|
195
|
+
const [filePath, currentName, newName] = line.split(
|
|
196
|
+
/\s+/u,
|
|
197
|
+
RENAME_PARTS,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return { filePath, currentName, newName };
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const isCompleteRename = ({ filePath, currentName, newName }) =>
|
|
204
|
+
Boolean(filePath && currentName && newName);
|
|
205
|
+
|
|
206
|
+
const commandArguments = process.argv.slice(FIRST_ARGUMENT_INDEX);
|
|
207
|
+
const [firstArgument, secondArgument] = commandArguments;
|
|
208
|
+
const renames =
|
|
209
|
+
firstArgument === LIST_FLAG && secondArgument
|
|
210
|
+
? readRenameList(secondArgument)
|
|
211
|
+
: [
|
|
212
|
+
{
|
|
213
|
+
filePath: commandArguments[0],
|
|
214
|
+
currentName: commandArguments[1],
|
|
215
|
+
newName: commandArguments[2],
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
renames.length > 0 &&
|
|
221
|
+
renames.every((rename) => isCompleteRename(rename))
|
|
222
|
+
) {
|
|
223
|
+
// Each rename reloads the project, so it sees the renames before it.
|
|
224
|
+
for (const rename of renames) {
|
|
225
|
+
console.log(renameSymbol(rename));
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
console.error(USAGE);
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
}
|
|
@@ -4,25 +4,22 @@ root = true
|
|
|
4
4
|
charset = utf-8
|
|
5
5
|
end_of_line = lf
|
|
6
6
|
indent_size = 2
|
|
7
|
-
indent_style =
|
|
7
|
+
indent_style = tab
|
|
8
8
|
insert_final_newline = true
|
|
9
9
|
trim_trailing_whitespace = true
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Markdown reads a tab as four spaces, so it uses spaces.
|
|
12
|
+
[*.{md,mdx}]
|
|
13
|
+
indent_style = space
|
|
12
14
|
trim_trailing_whitespace = false
|
|
13
15
|
max_line_length = off
|
|
14
16
|
|
|
17
|
+
# YAML does not allow tabs.
|
|
15
18
|
[*.{yml,yaml}]
|
|
19
|
+
indent_style = space
|
|
16
20
|
indent_size = 2
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
indent_size = 2
|
|
20
|
-
|
|
21
|
-
[Makefile]
|
|
22
|
-
indent_style = tab
|
|
23
|
-
|
|
24
|
-
[*.go]
|
|
25
|
-
indent_style = tab
|
|
26
|
-
|
|
22
|
+
# Python's own style guide uses 4 spaces.
|
|
27
23
|
[*.py]
|
|
24
|
+
indent_style = space
|
|
28
25
|
indent_size = 4
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# [Project Name]
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> [The project's most important rule for agents, stated first so every agent sees it. Usually whether an agent may commit, push or make branches. One line, or a short section if it needs more, such as which agents may commit, when, and how the others ask. See "Project workflow" below.]
|
|
4
|
+
|
|
5
|
+
> **"go" means carry on.** When the person sends only "go", carry on with the current task. It is never permission to commit, push or open a Pull Request, and it needs no reply asking what they meant.
|
|
4
6
|
|
|
5
7
|
## Project overview
|
|
6
8
|
|
|
@@ -35,12 +37,15 @@
|
|
|
35
37
|
| Type-check | `pnpm typecheck` |
|
|
36
38
|
| Docs site | `pnpm docs:dev` (then open `/docs`) |
|
|
37
39
|
|
|
38
|
-
##
|
|
40
|
+
## Project workflow
|
|
41
|
+
|
|
42
|
+
[How this project runs its work. These are the project's own decisions, not part of the code standards. Say where they are written up (usually `docs/development/`), then list the rules an agent must follow:]
|
|
39
43
|
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
+
- **Who commits**: [whether AI agents may commit, push or make branches, and when]
|
|
45
|
+
- **Branches**: [how branches are named]
|
|
46
|
+
- **Commit messages**: [the commit message format]
|
|
47
|
+
- **Pull requests**: [review and CI requirements, and where Pull Requests merge to]
|
|
48
|
+
- **Merging and releases**: [merge strategy, and how work reaches production]
|
|
44
49
|
|
|
45
50
|
## Environment variables
|
|
46
51
|
|
|
@@ -89,15 +94,15 @@ Follow these rules in every change:
|
|
|
89
94
|
| Never | Always |
|
|
90
95
|
| --- | --- |
|
|
91
96
|
| `btn`, `msg`, `val`, `(e) =>`, `(item, i) =>` | `button`, `message`, `value`, `(event) =>`, `(item, index) =>` |
|
|
92
|
-
| `
|
|
93
|
-
| `deps`, `repo`, `env`, `timeoutMs`, `MAX_` | `dependencies`, `repository`, `environment`, `timeoutMilliseconds`, `MAXIMUM_` |
|
|
97
|
+
| `deps`, `env`, `timeoutMs`, `MAX_` | `dependencies`, `environment`, `timeoutMilliseconds`, `MAXIMUM_` |
|
|
94
98
|
| `<T>`, `"sm" \| "lg"` | `<ItemType>`, `"small" \| "large"` |
|
|
95
99
|
| `name="email1"` | `name="emailAddress"` |
|
|
96
|
-
| `
|
|
100
|
+
| `helpers/`, `utilities/`, `helpers.ts` | `utils/`, a file named after what it does |
|
|
97
101
|
|
|
98
102
|
- **`id` is a word.** Write `id`, `userId`, `user_id` — never `identifier` and never `userID`. `identifier` is only for an outside system's separate field of that name (Linear's readable `ACT-73`).
|
|
99
|
-
- **Dictionary words stay short:** `id`, `docs`, `props`, `utils`, `app`, `url`, `api`, `uat`, `pdf`, `html`, `css`, `json`, `readme`. Case them as words: `imageUrl`, not `imageURL`. Any other short name is written in full. A new dictionary word is added to the Dictionary in the Human-Readable Names standard first (https://activatus-code-standards.vercel.app/Standards/human-readable-names#dictionary), never just used.
|
|
103
|
+
- **Dictionary words stay short:** `id`, `docs`, `lib`, `props`, `ref`, `repo`, `utils`, `app`, `url`, `api`, `uat`, `pdf`, `html`, `css`, `json`, `readme`. Case them as words: `imageUrl`, not `imageURL`. Any other short name is written in full. A new dictionary word is added to the Dictionary in the Human-Readable Names standard first (https://activatus-code-standards.vercel.app/Standards/human-readable-names#dictionary), never just used.
|
|
100
104
|
- **Names owned by someone else stay exactly as they are** — Linear's `id` and `pageInfo`, Node's `cwd` option and `process.env`, React's `ref` and `children`, shadcn/ui's `cn` helper, Next.js's `params`, `searchParams` and `LayoutProps`, HTML attributes, Tailwind classes. The rule is for names we write, not names from someone else's code. Renaming them breaks the code. Give the value a full-word name as soon as it comes into our code: `const workingDirectory = process.cwd();`, then pass `{ cwd: workingDirectory }`.
|
|
105
|
+
- **When a name clashes with a third-party import, rename our own thing first**, to a more specific full-word name: our `createProjectDatabaseClient`, next to Supabase's `createClient`. Rename the import only when both names are third-party, or ours is fixed by a framework (a Next.js route's `GET`). Then use the owner plus the original name, `Link as LucideLinkIcon`. Never rename an import for taste, and never shorten it.
|
|
101
106
|
- **Form fields especially.** A field has one name everywhere it goes: the input's `name` and `id`, the form schema, the API request, the type and the database column (`emailAddress` / `email_address`). Every field has a visible `<label>` in plain English — never a placeholder instead. Option values are words (`"monthly"`), not numbers. Validation messages name the field and say what to do.
|
|
102
107
|
- **The linter only checks some names.** Folders, props, form fields, database columns, routes and environment variables are not linted — check every one of them yourself.
|
|
103
108
|
- **Always `const`.** Never `var`. Use `let` only when a value must be reassigned, with `// eslint-disable-next-line no-restricted-syntax -- <reason>` on the line above.
|
|
@@ -112,7 +117,7 @@ Follow these rules in every change:
|
|
|
112
117
|
- **No nested ternaries. No boolean flag parameters.**
|
|
113
118
|
- **No `any`, no non-null `!`, no `as` cast without a comment saying why.**
|
|
114
119
|
- **Named exports only**, except where a tool requires a default export.
|
|
115
|
-
- **Type names end in `Props` or `Type
|
|
120
|
+
- **Type names end in `Props` or `Type`**, and say what the thing is, not the job it does. A type that only describes one field of a function's props is written inside the `Props`: `StartAgentRunProps` holds its `dependencies`, with no separate `StartAgentRunDependenciesType`.
|
|
116
121
|
- **One component per file.** Server and client components live in separate files.
|
|
117
122
|
- **Component file order**: imports, component, variables, state, `useMemo` values, `useCallback` functions, effects, render. No inline functions in render.
|
|
118
123
|
- **Reusable functions live in a `utils/` folder** — global, section or component level — one folder per function, with an `index.ts`.
|
|
@@ -152,8 +157,6 @@ Skill files live in `.agents/skills/`, mirrored to `.claude/commands/`.
|
|
|
152
157
|
|
|
153
158
|
## Out of scope for AI agents
|
|
154
159
|
|
|
155
|
-
- **Never commit code.** No `git commit`, `git push`, `git merge`, `git rebase` or `git stash`. Leave every change uncommitted for a person to review.
|
|
156
|
-
- Do not create, switch or delete branches.
|
|
157
160
|
- Do not write secrets, tokens or `.env` values into any file.
|
|
158
161
|
- Do not change dependencies or lockfiles unless the task requires it.
|
|
159
162
|
- Do not edit generated files, or third-party code in `node_modules/` or `vendor/`.
|
|
@@ -12,6 +12,10 @@ This project follows the Activatus Code Standards, installed as the `@activatus/
|
|
|
12
12
|
|
|
13
13
|
[For each standard this project skips or changes: its number and name from the setup list, what is different, and why. For example: "7. Prettier — skipped: the project uses Biome for formatting." If nothing differs, write "No differences".]
|
|
14
14
|
|
|
15
|
+
## Unfinished changes
|
|
16
|
+
|
|
17
|
+
[Changes from a code standards version that are not done yet, each with its version and number, what is left, and where it is being done. For example: "Version 2, change 4: the standard ESLint config is not switched on yet. Being done in ACT-120." If there are none, write "None".]
|
|
18
|
+
|
|
15
19
|
## Related
|
|
16
20
|
|
|
17
21
|
- [Writing Docs](../overview/writing-docs.md)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { themes as prismThemes } from "prism-react-renderer";
|
|
2
|
+
|
|
3
|
+
const TITLE_SEPARATOR = " — ";
|
|
4
|
+
const FIRST_GROUP = "overview";
|
|
5
|
+
|
|
6
|
+
// Each doc is titled "# Group — Menu item". The folder is the menu group and the
|
|
7
|
+
// part after the dash is the menu item. Overview comes first; everything else is alphabetical.
|
|
8
|
+
const generateSidebarItems = async ({
|
|
9
|
+
defaultSidebarItemsGenerator,
|
|
10
|
+
docs: pages,
|
|
11
|
+
...remainingArguments
|
|
12
|
+
}) => {
|
|
13
|
+
const items = await defaultSidebarItemsGenerator({
|
|
14
|
+
docs: pages,
|
|
15
|
+
...remainingArguments,
|
|
16
|
+
});
|
|
17
|
+
const titleById = new Map(
|
|
18
|
+
pages.map((page) => [page.id, page.title]),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const labelled = items.map((item) => {
|
|
22
|
+
if (item.type !== "category") {
|
|
23
|
+
return item;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const children = item.items
|
|
27
|
+
.map((child) => {
|
|
28
|
+
if (child.type !== "doc") {
|
|
29
|
+
return child;
|
|
30
|
+
}
|
|
31
|
+
const title = titleById.get(child.id) ?? child.id;
|
|
32
|
+
const label = title.includes(TITLE_SEPARATOR)
|
|
33
|
+
? title.split(TITLE_SEPARATOR)[1]
|
|
34
|
+
: title;
|
|
35
|
+
return { ...child, label };
|
|
36
|
+
})
|
|
37
|
+
.sort((first, second) =>
|
|
38
|
+
first.label.localeCompare(second.label),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const firstDoc = item.items.find((child) => child.type === "doc");
|
|
42
|
+
const firstTitle = firstDoc
|
|
43
|
+
? (titleById.get(firstDoc.id) ?? "")
|
|
44
|
+
: "";
|
|
45
|
+
const label = firstTitle.includes(TITLE_SEPARATOR)
|
|
46
|
+
? firstTitle.split(TITLE_SEPARATOR)[0]
|
|
47
|
+
: item.label;
|
|
48
|
+
const folder = firstDoc ? firstDoc.id.split("/")[0] : item.label;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...item,
|
|
52
|
+
label,
|
|
53
|
+
items: children,
|
|
54
|
+
customProps: { folder },
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return labelled.sort((first, second) => {
|
|
59
|
+
const firstFolder = first.customProps?.folder ?? "";
|
|
60
|
+
const secondFolder = second.customProps?.folder ?? "";
|
|
61
|
+
if (firstFolder === FIRST_GROUP) {
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
if (secondFolder === FIRST_GROUP) {
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
return String(first.label).localeCompare(String(second.label));
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// TODO: set the project name
|
|
72
|
+
const PROJECT_NAME = "Project Name";
|
|
73
|
+
|
|
74
|
+
/** @type {import('@docusaurus/types').Config} */
|
|
75
|
+
const config = {
|
|
76
|
+
title: `${PROJECT_NAME} Docs`,
|
|
77
|
+
favicon: "images/logo.svg",
|
|
78
|
+
|
|
79
|
+
// The docs are only ever served behind the app's /docs route, so this URL is a placeholder.
|
|
80
|
+
url: "http://localhost:3000",
|
|
81
|
+
baseUrl: "/docs/",
|
|
82
|
+
|
|
83
|
+
// Internal docs must never be indexed by search engines.
|
|
84
|
+
noIndex: true,
|
|
85
|
+
|
|
86
|
+
onBrokenLinks: "throw",
|
|
87
|
+
|
|
88
|
+
markdown: {
|
|
89
|
+
// Plain .md files are read as CommonMark, so < and { in a doc never break the build.
|
|
90
|
+
format: "detect",
|
|
91
|
+
mermaid: true,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
presets: [
|
|
95
|
+
[
|
|
96
|
+
"classic",
|
|
97
|
+
/** @type {import('@docusaurus/preset-classic').Options} */ ({
|
|
98
|
+
docs: {
|
|
99
|
+
// The Markdown lives in the project's root docs/ folder, not inside this site.
|
|
100
|
+
path: "../docs",
|
|
101
|
+
routeBasePath: "/",
|
|
102
|
+
sidebarPath: "./sidebars.mjs",
|
|
103
|
+
sidebarItemsGenerator: generateSidebarItems,
|
|
104
|
+
},
|
|
105
|
+
blog: false,
|
|
106
|
+
theme: {
|
|
107
|
+
customCss: "./src/css/custom.css",
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
],
|
|
111
|
+
],
|
|
112
|
+
|
|
113
|
+
themes: [
|
|
114
|
+
["@getcanary/docusaurus-theme-search-pagefind", {}],
|
|
115
|
+
"@docusaurus/theme-mermaid",
|
|
116
|
+
],
|
|
117
|
+
|
|
118
|
+
themeConfig:
|
|
119
|
+
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({
|
|
120
|
+
tableOfContents: {
|
|
121
|
+
minHeadingLevel: 2,
|
|
122
|
+
maxHeadingLevel: 3,
|
|
123
|
+
},
|
|
124
|
+
colorMode: {
|
|
125
|
+
defaultMode: "light",
|
|
126
|
+
respectPrefersColorScheme: true,
|
|
127
|
+
},
|
|
128
|
+
navbar: {
|
|
129
|
+
title: `${PROJECT_NAME} Docs`,
|
|
130
|
+
logo: {
|
|
131
|
+
alt: "Logo",
|
|
132
|
+
src: "images/logo.svg",
|
|
133
|
+
},
|
|
134
|
+
items: [],
|
|
135
|
+
},
|
|
136
|
+
prism: {
|
|
137
|
+
theme: prismThemes.oneLight,
|
|
138
|
+
darkTheme: prismThemes.oneDark,
|
|
139
|
+
additionalLanguages: ["bash", "json"],
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default config;
|