@smitejs/snippets 2.0.0-SNAPSHOT

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.
@@ -0,0 +1,209 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ /** A folder of files matching a predicate, sorted by path. */
4
+ export const collectFiles = async (directory, predicate) => {
5
+ const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
6
+ if (error instanceof Error &&
7
+ "code" in error &&
8
+ error.code === "ENOENT") {
9
+ return [];
10
+ }
11
+ throw error;
12
+ });
13
+ const files = [];
14
+ for (const entry of entries) {
15
+ const entryPath = path.join(directory, entry.name);
16
+ if (entry.isDirectory()) {
17
+ files.push(...(await collectFiles(entryPath, predicate)));
18
+ continue;
19
+ }
20
+ if (entry.isFile() && predicate(entryPath)) {
21
+ files.push(entryPath);
22
+ }
23
+ }
24
+ return files.sort();
25
+ };
26
+ const sectionPattern = /^\s*\/\/\s*#section\s*-\s*(.+)\s*$/u;
27
+ const endsectionPattern = /^\s*\/\/\s*#endsection\s*$/u;
28
+ /**
29
+ * Extracts every `#section ... #endsection` snippet from a test source. Nested
30
+ * or unclosed sections throw.
31
+ *
32
+ * @group Snippets
33
+ */
34
+ export const extractSnippets = (source, filePath) => {
35
+ const snippets = [];
36
+ const lines = source.split("\n");
37
+ let current;
38
+ lines.forEach((line, index) => {
39
+ const section = line.match(sectionPattern);
40
+ if (section) {
41
+ if (current) {
42
+ throw new Error(`Nested documentation snippet in ${filePath}:${index + 1}`);
43
+ }
44
+ current = {
45
+ code: [],
46
+ filePath,
47
+ startLine: index + 1,
48
+ title: section[1]?.trim() ?? "",
49
+ };
50
+ return;
51
+ }
52
+ if (endsectionPattern.test(line)) {
53
+ if (current) {
54
+ snippets.push({
55
+ ...current,
56
+ code: dedent(current.code.join("\n")).trim(),
57
+ expected: null,
58
+ slug: slugify(current.title),
59
+ });
60
+ }
61
+ current = undefined;
62
+ return;
63
+ }
64
+ if (current) {
65
+ current.code.push(line);
66
+ }
67
+ });
68
+ if (current) {
69
+ throw new Error(`Unclosed documentation snippet in ${filePath}:${current.startLine}`);
70
+ }
71
+ return snippets;
72
+ };
73
+ /**
74
+ * Extracts the expected-result lines that immediately follow a snippet's
75
+ * `#endsection` marker (the assertions testing it), as plain code. Returns
76
+ * `null` when there is nothing to show.
77
+ *
78
+ * @group Snippets
79
+ */
80
+ export const extractSnippetExpected = (testSource, snippet) => {
81
+ const lines = testSource.split("\n");
82
+ const sectionLine = lines.findIndex((line, idx) => idx >= snippet.startLine - 1 && endsectionPattern.test(line));
83
+ if (sectionLine === -1) {
84
+ return null;
85
+ }
86
+ const expectedLines = [];
87
+ const sectionIndent = lines[sectionLine]?.match(/^\s*/u)?.[0] ?? "";
88
+ let i = sectionLine + 1;
89
+ for (; i < lines.length; i++) {
90
+ const line = lines[i] ?? "";
91
+ if (line.trim().length === 0) {
92
+ continue;
93
+ }
94
+ const lineIndent = line.match(/^\s*/u)?.[0] ?? "";
95
+ if (lineIndent.length < sectionIndent.length) {
96
+ break;
97
+ }
98
+ expectedLines.push(line);
99
+ }
100
+ if (expectedLines.length === 0) {
101
+ return null;
102
+ }
103
+ const code = dedent(expectedLines.join("\n")).trim();
104
+ return code.length > 0 ? code : null;
105
+ };
106
+ /**
107
+ * Collects every tested snippet from the `*.test.ts` files under `srcDir`,
108
+ * returning both the flat list and a title index. Duplicate titles throw.
109
+ *
110
+ * @group Snippets
111
+ */
112
+ export const collectTestSnippets = async (input) => {
113
+ const testFiles = await collectFiles(input.srcDir, (filePath) => filePath.endsWith(".test.ts"));
114
+ const snippets = [];
115
+ for (const filePath of testFiles) {
116
+ const source = await readFile(filePath, "utf8");
117
+ const relativePath = path.relative(input.rootDir, filePath);
118
+ const extracted = extractSnippets(source, relativePath);
119
+ for (const snippet of extracted) {
120
+ const raw = extractSnippetExpected(source, snippet);
121
+ snippets.push({
122
+ ...snippet,
123
+ expected: raw ? transformExpectedToResult(raw) : null,
124
+ });
125
+ }
126
+ }
127
+ return {
128
+ snippetIndex: buildSnippetIndex(snippets, input.packageName),
129
+ snippets,
130
+ };
131
+ };
132
+ /**
133
+ * Builds the title → snippet index, throwing on duplicate or empty titles.
134
+ *
135
+ * @group Snippets
136
+ */
137
+ export const buildSnippetIndex = (snippets, packageName) => {
138
+ const index = new Map();
139
+ for (const snippet of snippets) {
140
+ const key = normalizeExampleName(snippet.title);
141
+ if (index.has(key)) {
142
+ throw new Error(`Duplicate tested snippet "${snippet.title}" in ${packageName}.`);
143
+ }
144
+ index.set(key, snippet);
145
+ }
146
+ return index;
147
+ };
148
+ /** Normalizes a snippet title into its lookup key. */
149
+ export const normalizeExampleName = (value) => value.trim().toLowerCase();
150
+ /** Converts a snippet title into a URL-safe slug. */
151
+ export const slugify = (value) => value
152
+ .replace(/^@/u, "")
153
+ .replaceAll("/", "-")
154
+ .replace(/[^a-zA-Z0-9-]+/gu, "-")
155
+ .toLowerCase();
156
+ const findMatchingCloseParen = (code, openIndex) => {
157
+ let depth = 1;
158
+ for (let i = openIndex + 1; i < code.length; i++) {
159
+ if (code[i] === "(") {
160
+ depth++;
161
+ }
162
+ else if (code[i] === ")") {
163
+ depth--;
164
+ if (depth === 0) {
165
+ return i;
166
+ }
167
+ }
168
+ }
169
+ return -1;
170
+ };
171
+ /**
172
+ * Reduces a block of `expect(...).toBe(...)` assertions into just the expected
173
+ * values. Falls back to the raw code when no `expect` calls are present.
174
+ */
175
+ export const transformExpectedToResult = (code) => {
176
+ const results = [];
177
+ let idx = 0;
178
+ while (idx < code.length) {
179
+ const expectIdx = code.indexOf("expect(", idx);
180
+ if (expectIdx === -1) {
181
+ break;
182
+ }
183
+ const expectOpen = expectIdx + "expect(".length;
184
+ const expectClose = findMatchingCloseParen(code, expectOpen);
185
+ if (expectClose === -1) {
186
+ break;
187
+ }
188
+ const matcherOpen = code.indexOf("(", expectClose + 1);
189
+ if (matcherOpen === -1) {
190
+ break;
191
+ }
192
+ const matcherClose = findMatchingCloseParen(code, matcherOpen);
193
+ if (matcherClose === -1) {
194
+ break;
195
+ }
196
+ const value = code.slice(matcherOpen + 1, matcherClose).trim();
197
+ results.push(value);
198
+ idx = matcherClose + 1;
199
+ }
200
+ return results.length > 0 ? results.join("\n") : code;
201
+ };
202
+ const dedent = (value) => {
203
+ const lines = value.replace(/\s+$/u, "").split("\n");
204
+ const indentation = lines
205
+ .filter((line) => line.trim().length > 0)
206
+ .map((line) => line.match(/^\s*/u)?.[0].length ?? 0);
207
+ const size = indentation.length === 0 ? 0 : Math.min(...indentation);
208
+ return lines.map((line) => line.slice(size)).join("\n");
209
+ };
package/docs/index.md ADDED
@@ -0,0 +1,22 @@
1
+ # @smitejs/snippets
2
+
3
+ Generate docs, JSDoc examples, and code from **tried-and-tested** code — the
4
+ `#section … #endsection` blocks living inside your test files.
5
+
6
+ Use `smite-snippets` (or the `@smitejs/snippets` exports) to:
7
+
8
+ - **inject** — expand `@example <Title>` JSDoc lines in declarations/sources
9
+ with the tested snippet, rendered as a fenced ```ts block.
10
+ - **document** — expand `@example <Title>` lines in concept markdown through
11
+ `expandMarkdown`, replacing them with placeholders you render to HTML.
12
+ - **generate** — expand `// @snippet <Title>` marker lines in template/code
13
+ files with the tested snippet via `expandTemplate`.
14
+
15
+ `create-smite-app` uses this machinery to ship `http`/`serverless` templates
16
+ whose app code is byte-for-byte the tested snippets.
17
+
18
+ ```bash
19
+ smite-snippets index packages/http/src --package @smitejs/http
20
+ smite-snippets inject packages/http/src dist --package @smitejs/http
21
+ smite-snippets template paths/to/template out --index index.json
22
+ ```
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@smitejs/snippets",
3
+ "version": "2.0.0-SNAPSHOT",
4
+ "description": "Generate docs, JSDoc examples, and code from tried-and-tested #section snippets.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/githiago-f/smite.git"
9
+ },
10
+ "homepage": "https://github.com/githiago-f/smite#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/githiago-f/smite/issues"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "smite-snippets": "./dist/cli.js"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": ["dist", "!dist/**/*.test.*", "!dist/.tsbuildinfo"],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -b",
30
+ "test": "vitest run"
31
+ },
32
+ "dependencies": {
33
+ "commander": "^15.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.10.0"
37
+ },
38
+ "sideEffects": false
39
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { Command } from "commander";
6
+ import { expandTemplate } from "./expand.js";
7
+ import { injectIntoFiles } from "./inject.js";
8
+ import {
9
+ buildSnippetIndex,
10
+ collectFiles,
11
+ collectTestSnippets,
12
+ } from "./snippets.js";
13
+ import type { Snippet } from "./snippets.js";
14
+
15
+ const program = new Command();
16
+ const colorEnabled =
17
+ process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
18
+ const paint =
19
+ (code: number) =>
20
+ (text: string): string =>
21
+ colorEnabled ? `\u001b[${code}m${text}\u001b[0m` : text;
22
+ const green = paint(32);
23
+ const cyan = paint(36);
24
+ const red = paint(31);
25
+
26
+ const collectFor = async (src: string, packageName: string) => {
27
+ const { snippetIndex } = await collectTestSnippets({
28
+ packageName,
29
+ rootDir: process.cwd(),
30
+ srcDir: resolve(process.cwd(), src),
31
+ });
32
+ return snippetIndex;
33
+ };
34
+
35
+ const loadIndex = async (
36
+ file: string,
37
+ ): Promise<ReadonlyMap<string, Snippet>> => {
38
+ const raw = JSON.parse(await readFile(resolve(process.cwd(), file), "utf8"));
39
+ return buildSnippetIndex(raw as Snippet[], "index");
40
+ };
41
+
42
+ program
43
+ .name("smite-snippets")
44
+ .description(
45
+ "Generate docs, JSDoc examples, and code from tested #section snippets.",
46
+ )
47
+ .version("0.1.0");
48
+
49
+ program
50
+ .command("index")
51
+ .description(
52
+ "Collect tested snippets from a package's src test files as JSON",
53
+ )
54
+ .argument("<src>", "package source directory holding *.test.ts files")
55
+ .requiredOption("--package <name>", "package name used in error messages")
56
+ .action(async (src: string, options: { package: string }) => {
57
+ const { snippets } = await collectTestSnippets({
58
+ packageName: options.package,
59
+ rootDir: process.cwd(),
60
+ srcDir: resolve(process.cwd(), src),
61
+ });
62
+ process.stdout.write(`${JSON.stringify(snippets, null, 2)}\n`);
63
+ });
64
+
65
+ program
66
+ .command("inject")
67
+ .description(
68
+ "Expand @example JSDoc references in declaration/source files in place",
69
+ )
70
+ .argument("<src>", "package source directory holding *.test.ts files")
71
+ .argument("<target>", "directory to expand @example into, defaults to ./dist")
72
+ .requiredOption("--package <name>", "package name used in error messages")
73
+ .action(async (src: string, target: string, options: { package: string }) => {
74
+ const snippetIndex = await collectFor(src, options.package);
75
+ const written = await injectIntoFiles({
76
+ targetDir: resolve(process.cwd(), target),
77
+ snippetIndex,
78
+ packageName: options.package,
79
+ });
80
+ for (const filePath of written) {
81
+ console.log(`${green("Injected")} ${cyan(filePath)}`);
82
+ }
83
+ });
84
+
85
+ program
86
+ .command("template")
87
+ .description(
88
+ "Expand // @snippet markers in a template tree into an output directory",
89
+ )
90
+ .argument("<src>", "template directory (files may carry // @snippet markers)")
91
+ .argument("<out>", "output directory for the expanded tree")
92
+ .requiredOption(
93
+ "--index <file>",
94
+ "JSON snippet index produced by `smite-snippets index`",
95
+ )
96
+ .action(async (src: string, out: string, options: { index: string }) => {
97
+ const snippetIndex = await loadIndex(options.index);
98
+ const sourceDir = resolve(process.cwd(), src);
99
+ const outDir = resolve(process.cwd(), out);
100
+ const files = await collectFiles(sourceDir, () => true);
101
+
102
+ for (const filePath of files) {
103
+ const source = await readFile(filePath, "utf8");
104
+ const relativePath = filePath.slice(sourceDir.length + 1);
105
+ const expanded = expandTemplate(
106
+ source,
107
+ snippetIndex,
108
+ "template",
109
+ relativePath,
110
+ );
111
+ const targetPath = join(outDir, relativePath);
112
+ await mkdir(dirname(targetPath), { recursive: true });
113
+ await writeFile(targetPath, expanded);
114
+ }
115
+ });
116
+
117
+ try {
118
+ await program.parseAsync(process.argv);
119
+ } catch (error) {
120
+ console.error(red(error instanceof Error ? error.message : String(error)));
121
+ process.exitCode = 1;
122
+ }
@@ -0,0 +1,68 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ collectFiles,
6
+ collectTestSnippets,
7
+ expandExamples,
8
+ normalizeExampleName,
9
+ } from "./index.js";
10
+
11
+ const packageName = "@smitejs/snippets";
12
+ const rootDir = process.cwd();
13
+ const srcDir = join(rootDir, "packages/snippets/src");
14
+
15
+ const exampleReferences = async () => {
16
+ const { snippetIndex } = await collectTestSnippets({
17
+ packageName,
18
+ rootDir,
19
+ srcDir,
20
+ });
21
+ const files = await collectFiles(srcDir, (filePath) =>
22
+ filePath.endsWith(".ts"),
23
+ );
24
+ const references: Array<{ filePath: string; title: string }> = [];
25
+
26
+ for (const filePath of files) {
27
+ const source = await readFile(filePath, "utf8");
28
+
29
+ for (const match of source.matchAll(
30
+ /^(\s*\*[^\S\r\n]*)@example[^\S\r\n]+([^\r\n]+?)\s*$/gmu,
31
+ )) {
32
+ references.push({ filePath, title: match[2]?.trim() ?? "" });
33
+ }
34
+ }
35
+
36
+ return { snippetIndex, references };
37
+ };
38
+
39
+ describe("documentation integrity", () => {
40
+ it("every @example resolves to a tested #section snippet", async () => {
41
+ const { snippetIndex, references } = await exampleReferences();
42
+ const missing = references
43
+ .filter(({ title }) => !snippetIndex.has(normalizeExampleName(title)))
44
+ .map(
45
+ ({ filePath, title }) =>
46
+ `- Missing tested snippet "${title}" in ${filePath}`,
47
+ );
48
+ expect(missing).toEqual([]);
49
+ });
50
+
51
+ it("renders each referenced snippet into a code block", async () => {
52
+ const { snippetIndex } = await collectTestSnippets({
53
+ packageName,
54
+ rootDir,
55
+ srcDir,
56
+ });
57
+
58
+ for (const title of snippetIndex.keys()) {
59
+ const rendered = expandExamples(
60
+ ` * @example ${title}\n`,
61
+ snippetIndex,
62
+ packageName,
63
+ "synthetic.d.ts",
64
+ );
65
+ expect(rendered).toContain("```ts");
66
+ }
67
+ });
68
+ });
package/src/expand.ts ADDED
@@ -0,0 +1,165 @@
1
+ import { normalizeExampleName } from "./snippets.js";
2
+ import type { Snippet } from "./snippets.js";
3
+
4
+ /**
5
+ * A placeholder/render pair produced by {@link expandMarkdownExamples}. The
6
+ * caller scans a rendered document for each placeholder and replaces it with
7
+ * the rendered snippet markup.
8
+ *
9
+ * @group Markdown
10
+ */
11
+ export interface MarkdownInjectable {
12
+ readonly placeholder: string;
13
+ readonly snippet: Snippet;
14
+ }
15
+
16
+ const exampleTag = /^(\s*\*[^\S\r\n]*)@example[^\S\r\n]+([^\r\n]+?)\s*$/gmu;
17
+
18
+ /**
19
+ * Expands every JSDoc `@example <Title>` line in a source string with the
20
+ * matching tested snippet, rendered as a fenced code block. Throws when a
21
+ * referenced snippet is missing. The code must not contain a closing JSDoc
22
+ * comment sequence (asterisk followed by a slash), which would close the
23
+ * comment early.
24
+ *
25
+ * @group Inline
26
+ * @example Expand @example declarations
27
+ */
28
+ export const expandExamples = (
29
+ source: string,
30
+ snippetIndex: ReadonlyMap<string, Snippet>,
31
+ packageName: string,
32
+ filePath: string,
33
+ ): string => {
34
+ const unresolved: string[] = [];
35
+ const expanded = source.replace(
36
+ exampleTag,
37
+ (line, prefix: string, title: string) => {
38
+ const snippet = snippetIndex.get(normalizeExampleName(title));
39
+
40
+ if (!snippet) {
41
+ unresolved.push(title);
42
+ return line;
43
+ }
44
+
45
+ if (snippet.code.includes("*/")) {
46
+ throw new Error(
47
+ [
48
+ `Cannot expand @example "${title}" in ${filePath} (${packageName}).`,
49
+ 'The snippet code contains "*/", which would close the JSDoc',
50
+ "comment early and corrupt the emitted declaration.",
51
+ 'Rewrite the snippet to avoid "*/" (e.g. a cron schedule without',
52
+ 'a "*/" step) before releasing.',
53
+ ].join("\n"),
54
+ );
55
+ }
56
+
57
+ return renderExample(prefix, snippet.code);
58
+ },
59
+ );
60
+
61
+ if (unresolved.length > 0) {
62
+ throw new Error(
63
+ [
64
+ `Cannot expand @example references in ${filePath} (${packageName}).`,
65
+ ...unresolved.map((title) => `- Missing tested snippet: ${title}`),
66
+ ].join("\n"),
67
+ );
68
+ }
69
+
70
+ return expanded;
71
+ };
72
+
73
+ /** Renders a snippet into a JSDoc fenced ```ts block with the given prefix. */
74
+ export const renderExample = (prefix: string, code: string): string =>
75
+ [
76
+ `${prefix}@example`,
77
+ `${prefix}\`\`\`ts`,
78
+ ...code.split("\n").map((line) => `${prefix}${line}`),
79
+ `${prefix}\`\`\``,
80
+ ].join("\n");
81
+
82
+ /**
83
+ * Expands `// @snippet <Title>` marker lines in a template/code file by
84
+ * replacing each with the tested snippet code. The route either keeps files
85
+ * untouched when they carry no markers. Throws on unresolved markers.
86
+ *
87
+ * @group Template
88
+ * @example Expand @snippet into a source file
89
+ */
90
+ export const expandTemplate = (
91
+ source: string,
92
+ snippetIndex: ReadonlyMap<string, Snippet>,
93
+ packageName: string,
94
+ filePath: string,
95
+ ): string => {
96
+ const unresolved: string[] = [];
97
+
98
+ const expanded = source.replace(
99
+ /(\/\/\s*@snippet\s+[^\r\n]+)(?:\r?\n|$)/gu,
100
+ (marker, markerText: string) => {
101
+ const title = markerText.replace(/^\/\/\s*@snippet\s+/u, "").trim();
102
+ const snippet = snippetIndex.get(normalizeExampleName(title));
103
+
104
+ if (!snippet) {
105
+ unresolved.push(title);
106
+ return marker;
107
+ }
108
+
109
+ return `${snippet.code}\n`;
110
+ },
111
+ );
112
+
113
+ if (unresolved.length > 0) {
114
+ throw new Error(
115
+ [
116
+ `Cannot expand @snippet references in ${filePath} (${packageName}).`,
117
+ ...unresolved.map((title) => `- Missing tested snippet: ${title}`),
118
+ ].join("\n"),
119
+ );
120
+ }
121
+
122
+ return expanded;
123
+ };
124
+
125
+ /**
126
+ * Scans a concept-document markdown source for `@example <Title>` lines,
127
+ * replacing them with placeholder lines and returning the list of injectables.
128
+ * The caller renders the placeholders to HTML after markdown conversion so
129
+ * code fences survive the renderer.
130
+ *
131
+ * @group Markdown
132
+ */
133
+ export const expandMarkdown = (
134
+ source: string,
135
+ snippetIndex: ReadonlyMap<string, Snippet>,
136
+ packageName: string,
137
+ filePath: string,
138
+ ): { body: string; examples: readonly MarkdownInjectable[] } => {
139
+ const injectables: MarkdownInjectable[] = [];
140
+ const scrubbed: string[] = [];
141
+
142
+ for (const line of source.split("\n")) {
143
+ const example = line.match(/^\s*@example\s+(.+?)\s*$/u);
144
+
145
+ if (example) {
146
+ const title = example[1]?.trim() ?? "";
147
+ const snippet = snippetIndex.get(normalizeExampleName(title));
148
+
149
+ if (!snippet) {
150
+ throw new Error(
151
+ `Missing tested snippet "${title}" referenced by ${filePath} (${packageName}).`,
152
+ );
153
+ }
154
+
155
+ const placeholder = `SMITE_EXAMPLE_${injectables.length}`;
156
+ injectables.push({ placeholder, snippet });
157
+ scrubbed.push("", placeholder, "");
158
+ continue;
159
+ }
160
+
161
+ scrubbed.push(line);
162
+ }
163
+
164
+ return { body: scrubbed.join("\n"), examples: injectables };
165
+ };