nostics 0.0.0 → 0.0.4
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 +145 -0
- package/dist/dev-reporter.d.mts +7 -0
- package/dist/dev-reporter.mjs +9 -0
- package/dist/dev-reporter.mjs.map +1 -0
- package/dist/diagnostics-CSUYJHfP.d.mts +59 -0
- package/dist/format-CKWeokpQ.d.mts +10 -0
- package/dist/format-DYh8khIQ.mjs +21 -0
- package/dist/format-DYh8khIQ.mjs.map +1 -0
- package/dist/formatters/ansi.d.mts +15 -0
- package/dist/formatters/ansi.mjs +30 -0
- package/dist/formatters/ansi.mjs.map +1 -0
- package/dist/formatters/json.d.mts +7 -0
- package/dist/formatters/json.mjs +6 -0
- package/dist/formatters/json.mjs.map +1 -0
- package/dist/index.d.mts +38 -0
- package/dist/index.mjs +192 -0
- package/dist/index.mjs.map +1 -0
- package/dist/reporter-DDQiiD6l.d.mts +9 -0
- package/dist/unplugin.d.mts +28 -0
- package/dist/unplugin.mjs +207 -0
- package/dist/unplugin.mjs.map +1 -0
- package/package.json +84 -7
- package/skills/add-diagnostic/SKILL.md +48 -0
- package/skills/nostics/SKILL.md +297 -0
- package/skills/nostics/references/documentation-site.md +203 -0
- package/index.js +0 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createUnplugin } from "unplugin";
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import MagicString from "magic-string";
|
|
5
|
+
import { parseSync } from "oxc-parser";
|
|
6
|
+
//#region src/code-transform/transform.ts
|
|
7
|
+
/**
|
|
8
|
+
* Transforms code that imports from `nostics`:
|
|
9
|
+
* - Adds `\/*#__PURE__*\/` to `defineDiagnostics()` and `createLogger()` call expressions
|
|
10
|
+
* - Prepends `process.env.NODE_ENV !== 'production' &&` to expression statements using logger variables
|
|
11
|
+
*
|
|
12
|
+
* Also handles cross-file patterns: if a file imports a variable that was
|
|
13
|
+
* created from `createLogger()` or `defineDiagnostics()` in another file,
|
|
14
|
+
* the usage is tracked and wrapped.
|
|
15
|
+
*/
|
|
16
|
+
function transform(code, id, options, trackedExportsMap) {
|
|
17
|
+
const packageName = options?.packageName ?? "nostics";
|
|
18
|
+
const ast = parseSync(id, code).program;
|
|
19
|
+
const importedNames = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const node of ast.body) if (node.type === "ImportDeclaration" && node.source.value === packageName) {
|
|
21
|
+
for (const spec of node.specifiers) if (spec.type === "ImportSpecifier") {
|
|
22
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
23
|
+
importedNames.set(spec.local.name, importedName);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const crossFileTracked = /* @__PURE__ */ new Set();
|
|
27
|
+
if (trackedExportsMap) {
|
|
28
|
+
for (const node of ast.body) if (node.type === "ImportDeclaration" && node.source.value !== packageName) {
|
|
29
|
+
const source = node.source.value;
|
|
30
|
+
if (!source.startsWith(".")) continue;
|
|
31
|
+
const resolvedPath = resolveModulePath(source, id);
|
|
32
|
+
if (!resolvedPath) continue;
|
|
33
|
+
if (!trackedExportsMap.has(resolvedPath)) analyzeModule(resolvedPath, packageName, trackedExportsMap);
|
|
34
|
+
const trackedNames = trackedExportsMap.get(resolvedPath);
|
|
35
|
+
if (trackedNames) {
|
|
36
|
+
for (const spec of node.specifiers) if (spec.type === "ImportSpecifier") {
|
|
37
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
38
|
+
if (trackedNames.has(importedName)) crossFileTracked.add(spec.local.name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (importedNames.size === 0 && crossFileTracked.size === 0) return void 0;
|
|
44
|
+
const s = new MagicString(code);
|
|
45
|
+
const trackedVars = new Set(crossFileTracked);
|
|
46
|
+
walkStatements(ast.body, s, importedNames, trackedVars);
|
|
47
|
+
if (!s.hasChanged()) return void 0;
|
|
48
|
+
if (trackedExportsMap) {
|
|
49
|
+
const exportedTracked = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const node of ast.body) if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
|
|
51
|
+
for (const decl of node.declaration.declarations) if (decl.id?.type === "Identifier" && trackedVars.has(decl.id.name)) exportedTracked.add(decl.id.name);
|
|
52
|
+
}
|
|
53
|
+
if (exportedTracked.size > 0) trackedExportsMap.set(id, exportedTracked);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
code: s.toString(),
|
|
57
|
+
map: s.generateMap({ hires: "boundary" })
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const CONDITION = "process.env.NODE_ENV !== 'production'";
|
|
61
|
+
/**
|
|
62
|
+
* Check if an expression has lower precedence than `&&` and needs inner parens
|
|
63
|
+
* when used as the right-hand side of `guard && expr`.
|
|
64
|
+
*/
|
|
65
|
+
function expressionNeedsParens(node) {
|
|
66
|
+
if (node.type === "ConditionalExpression") return true;
|
|
67
|
+
if (node.type === "LogicalExpression" && (node.operator === "||" || node.operator === "??")) return true;
|
|
68
|
+
if (node.type === "SequenceExpression") return true;
|
|
69
|
+
if (node.type === "AssignmentExpression") return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const EXTENSIONS = [
|
|
73
|
+
".ts",
|
|
74
|
+
".tsx",
|
|
75
|
+
".js",
|
|
76
|
+
".jsx",
|
|
77
|
+
".mts",
|
|
78
|
+
".mjs"
|
|
79
|
+
];
|
|
80
|
+
function resolveModulePath(source, importer) {
|
|
81
|
+
const base = join(dirname(importer), source);
|
|
82
|
+
if (existsSync(base)) return base;
|
|
83
|
+
for (const ext of EXTENSIONS) {
|
|
84
|
+
const candidate = base + ext;
|
|
85
|
+
if (existsSync(candidate)) return candidate;
|
|
86
|
+
}
|
|
87
|
+
for (const ext of EXTENSIONS) {
|
|
88
|
+
const candidate = join(base, `index${ext}`);
|
|
89
|
+
if (existsSync(candidate)) return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Analyze a module to find exported variables derived from nostics calls.
|
|
94
|
+
* Results are cached in trackedExportsMap.
|
|
95
|
+
*/
|
|
96
|
+
function analyzeModule(filePath, packageName, trackedExportsMap) {
|
|
97
|
+
trackedExportsMap.set(filePath, /* @__PURE__ */ new Set());
|
|
98
|
+
let source;
|
|
99
|
+
try {
|
|
100
|
+
source = readFileSync(filePath, "utf-8");
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!source.includes(packageName)) return;
|
|
105
|
+
const ast = parseSync(filePath, source).program;
|
|
106
|
+
const importedNames = /* @__PURE__ */ new Set();
|
|
107
|
+
for (const node of ast.body) if (node.type === "ImportDeclaration" && node.source.value === packageName) {
|
|
108
|
+
for (const spec of node.specifiers) if (spec.type === "ImportSpecifier") importedNames.add(spec.local.name);
|
|
109
|
+
}
|
|
110
|
+
if (importedNames.size === 0) return;
|
|
111
|
+
const trackedExports = /* @__PURE__ */ new Set();
|
|
112
|
+
for (const node of ast.body) if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
|
|
113
|
+
for (const decl of node.declaration.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee?.type === "Identifier" && importedNames.has(decl.init.callee.name) && decl.id?.type === "Identifier") trackedExports.add(decl.id.name);
|
|
114
|
+
}
|
|
115
|
+
if (trackedExports.size > 0) trackedExportsMap.set(filePath, trackedExports);
|
|
116
|
+
}
|
|
117
|
+
function walkStatements(body, s, importedNames, trackedVars) {
|
|
118
|
+
for (const stmt of body) {
|
|
119
|
+
if (stmt.type === "VariableDeclaration") {
|
|
120
|
+
for (const decl of stmt.declarations) if (decl.init?.type === "CallExpression" && decl.init.callee?.type === "Identifier" && importedNames.has(decl.init.callee.name) && decl.id?.type === "Identifier") {
|
|
121
|
+
trackedVars.add(decl.id.name);
|
|
122
|
+
s.appendLeft(decl.init.start, "/*#__PURE__*/ ");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (stmt.type === "ExpressionStatement") {
|
|
126
|
+
if (expressionUsesTrackedVar(stmt.expression, trackedVars)) if (expressionNeedsParens(stmt.expression)) {
|
|
127
|
+
s.appendLeft(stmt.expression.start, `${CONDITION} && (`);
|
|
128
|
+
s.appendRight(stmt.expression.end, `)`);
|
|
129
|
+
} else s.appendLeft(stmt.expression.start, `${CONDITION} && `);
|
|
130
|
+
}
|
|
131
|
+
if (stmt.type === "BlockStatement" || stmt.type === "Program") walkStatements(stmt.body, s, importedNames, trackedVars);
|
|
132
|
+
if (stmt.type === "IfStatement") {
|
|
133
|
+
if (stmt.consequent?.type === "BlockStatement") walkStatements(stmt.consequent.body, s, importedNames, trackedVars);
|
|
134
|
+
if (stmt.alternate?.type === "BlockStatement") walkStatements(stmt.alternate.body, s, importedNames, trackedVars);
|
|
135
|
+
}
|
|
136
|
+
if (stmt.type === "ForStatement" || stmt.type === "WhileStatement" || stmt.type === "DoWhileStatement") {
|
|
137
|
+
if (stmt.body?.type === "BlockStatement") walkStatements(stmt.body.body, s, importedNames, trackedVars);
|
|
138
|
+
}
|
|
139
|
+
if (stmt.type === "FunctionDeclaration" || stmt.type === "ArrowFunctionExpression") {
|
|
140
|
+
if (stmt.body?.type === "BlockStatement") walkStatements(stmt.body.body, s, importedNames, trackedVars);
|
|
141
|
+
}
|
|
142
|
+
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) walkStatements([stmt.declaration], s, importedNames, trackedVars);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if an expression references a tracked variable as the root of a member/call chain.
|
|
147
|
+
*/
|
|
148
|
+
function expressionUsesTrackedVar(node, trackedVars) {
|
|
149
|
+
if (!node) return false;
|
|
150
|
+
if (node.type === "Identifier") return trackedVars.has(node.name);
|
|
151
|
+
if (node.type === "MemberExpression") return expressionUsesTrackedVar(node.object, trackedVars);
|
|
152
|
+
if (node.type === "CallExpression") return expressionUsesTrackedVar(node.callee, trackedVars);
|
|
153
|
+
if (node.type === "LogicalExpression") return expressionUsesTrackedVar(node.left, trackedVars) || expressionUsesTrackedVar(node.right, trackedVars);
|
|
154
|
+
if (node.type === "ConditionalExpression") return expressionUsesTrackedVar(node.consequent, trackedVars) || expressionUsesTrackedVar(node.alternate, trackedVars);
|
|
155
|
+
if (node.type === "UnaryExpression") return expressionUsesTrackedVar(node.argument, trackedVars);
|
|
156
|
+
if (node.type === "AwaitExpression") return expressionUsesTrackedVar(node.argument, trackedVars);
|
|
157
|
+
if (node.type === "SequenceExpression") return node.expressions.some((expr) => expressionUsesTrackedVar(expr, trackedVars));
|
|
158
|
+
if (node.type === "ParenthesizedExpression") return expressionUsesTrackedVar(node.expression, trackedVars);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/code-transform/server-plugin.ts
|
|
163
|
+
const logsSDKServer = createUnplugin((options) => {
|
|
164
|
+
const logFile = options?.logFile ?? ".nostics.log";
|
|
165
|
+
return {
|
|
166
|
+
name: "nostics-server",
|
|
167
|
+
enforce: "pre",
|
|
168
|
+
vite: { configureServer(server) {
|
|
169
|
+
server.ws.on("nostics:report", (data) => {
|
|
170
|
+
try {
|
|
171
|
+
appendFileSync(logFile, `${JSON.stringify(data)}\n`);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`[nostics]: Failed to write log to "${logFile}":`, err);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
} }
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/code-transform/unplugin.ts
|
|
181
|
+
const JS_EXTENSIONS_RE = /\.[jt]sx?$/;
|
|
182
|
+
const NODE_MODULES_RE = /\/node_modules\//;
|
|
183
|
+
const unpluginFactory = (options) => {
|
|
184
|
+
const trackedExportsMap = /* @__PURE__ */ new Map();
|
|
185
|
+
return {
|
|
186
|
+
name: "nostics",
|
|
187
|
+
transform: {
|
|
188
|
+
filter: { id: {
|
|
189
|
+
include: JS_EXTENSIONS_RE,
|
|
190
|
+
exclude: NODE_MODULES_RE
|
|
191
|
+
} },
|
|
192
|
+
handler(code, id) {
|
|
193
|
+
const result = transform(code, id, options, trackedExportsMap);
|
|
194
|
+
if (!result) return;
|
|
195
|
+
return {
|
|
196
|
+
code: result.code,
|
|
197
|
+
map: result.map
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
const logsSDK = /* @__PURE__ */ createUnplugin(unpluginFactory);
|
|
204
|
+
//#endregion
|
|
205
|
+
export { logsSDK as default, logsSDK, logsSDKServer };
|
|
206
|
+
|
|
207
|
+
//# sourceMappingURL=unplugin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unplugin.mjs","names":[],"sources":["../src/code-transform/transform.ts","../src/code-transform/server-plugin.ts","../src/code-transform/unplugin.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport MagicString from 'magic-string'\nimport { parseSync } from 'oxc-parser'\n\nexport interface TransformResult {\n code: string\n map: ReturnType<MagicString['generateMap']>\n}\n\nexport interface TransformOptions {\n /**\n * The package name to detect imports from.\n * @default 'nostics'\n */\n packageName?: string\n}\n\n/**\n * Cross-file state: maps file paths to sets of exported variable names\n * that are derived from nostics function calls (createLogger, defineDiagnostics, etc.)\n */\nexport type TrackedExportsMap = Map<string, Set<string>>\n\n/**\n * Transforms code that imports from `nostics`:\n * - Adds `\\/*#__PURE__*\\/` to `defineDiagnostics()` and `createLogger()` call expressions\n * - Prepends `process.env.NODE_ENV !== 'production' &&` to expression statements using logger variables\n *\n * Also handles cross-file patterns: if a file imports a variable that was\n * created from `createLogger()` or `defineDiagnostics()` in another file,\n * the usage is tracked and wrapped.\n */\nexport function transform(\n code: string,\n id: string,\n options?: TransformOptions,\n trackedExportsMap?: TrackedExportsMap,\n): TransformResult | undefined {\n const packageName = options?.packageName ?? 'nostics'\n\n const result = parseSync(id, code)\n const ast = result.program\n\n // Step 1: Find direct imports from the package\n const importedNames = new Map<string, string>() // localName -> importedName\n for (const node of ast.body) {\n if (node.type === 'ImportDeclaration' && node.source.value === packageName) {\n for (const spec of node.specifiers) {\n if (spec.type === 'ImportSpecifier') {\n const importedName = spec.imported.type === 'Identifier' ? spec.imported.name : spec.imported.value\n importedNames.set(spec.local.name, importedName)\n }\n }\n }\n }\n\n // Step 2: Find cross-file tracked imports\n const crossFileTracked = new Set<string>()\n if (trackedExportsMap) {\n for (const node of ast.body) {\n if (node.type === 'ImportDeclaration' && node.source.value !== packageName) {\n const source = node.source.value as string\n // Only resolve relative imports\n if (!source.startsWith('.'))\n continue\n\n const resolvedPath = resolveModulePath(source, id)\n if (!resolvedPath)\n continue\n\n // Analyze the imported module if not already cached\n if (!trackedExportsMap.has(resolvedPath)) {\n analyzeModule(resolvedPath, packageName, trackedExportsMap)\n }\n\n const trackedNames = trackedExportsMap.get(resolvedPath)\n if (trackedNames) {\n for (const spec of node.specifiers) {\n if (spec.type === 'ImportSpecifier') {\n const importedName = spec.imported.type === 'Identifier' ? spec.imported.name : spec.imported.value\n if (trackedNames.has(importedName)) {\n crossFileTracked.add(spec.local.name)\n }\n }\n }\n }\n }\n }\n }\n\n if (importedNames.size === 0 && crossFileTracked.size === 0)\n return undefined\n\n const s = new MagicString(code)\n const trackedVars = new Set<string>(crossFileTracked)\n\n // Step 3: Walk all statements recursively\n walkStatements(ast.body, s, importedNames, trackedVars)\n\n if (!s.hasChanged())\n return undefined\n\n // Step 4: Record exported tracked vars for cross-file tracking\n if (trackedExportsMap) {\n const exportedTracked = new Set<string>()\n for (const node of ast.body) {\n if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {\n for (const decl of node.declaration.declarations) {\n if (decl.id?.type === 'Identifier' && trackedVars.has(decl.id.name)) {\n exportedTracked.add(decl.id.name)\n }\n }\n }\n }\n if (exportedTracked.size > 0) {\n trackedExportsMap.set(id, exportedTracked)\n }\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: 'boundary' }),\n }\n}\n\nconst CONDITION = 'process.env.NODE_ENV !== \\'production\\''\n\n/**\n * Check if an expression has lower precedence than `&&` and needs inner parens\n * when used as the right-hand side of `guard && expr`.\n */\nfunction expressionNeedsParens(node: any): boolean {\n if (node.type === 'ConditionalExpression')\n return true\n if (node.type === 'LogicalExpression' && (node.operator === '||' || node.operator === '??'))\n return true\n if (node.type === 'SequenceExpression')\n return true\n if (node.type === 'AssignmentExpression')\n return true\n return false\n}\n\nconst EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs']\n\nfunction resolveModulePath(source: string, importer: string): string | undefined {\n const dir = dirname(importer)\n const base = join(dir, source)\n\n // Try exact path (for imports with extension)\n if (existsSync(base))\n return base\n\n // Try with extensions\n for (const ext of EXTENSIONS) {\n const candidate = base + ext\n if (existsSync(candidate))\n return candidate\n }\n\n // Try index files\n for (const ext of EXTENSIONS) {\n const candidate = join(base, `index${ext}`)\n if (existsSync(candidate))\n return candidate\n }\n\n return undefined\n}\n\n/**\n * Analyze a module to find exported variables derived from nostics calls.\n * Results are cached in trackedExportsMap.\n */\nfunction analyzeModule(filePath: string, packageName: string, trackedExportsMap: TrackedExportsMap): void {\n // Mark as analyzed (even if no tracked exports) to avoid re-analysis\n trackedExportsMap.set(filePath, new Set())\n\n let source: string\n try {\n source = readFileSync(filePath, 'utf-8')\n }\n catch {\n return\n }\n\n if (!source.includes(packageName))\n return\n\n const result = parseSync(filePath, source)\n const ast = result.program\n\n // Find imports from the package\n const importedNames = new Set<string>()\n for (const node of ast.body) {\n if (node.type === 'ImportDeclaration' && node.source.value === packageName) {\n for (const spec of node.specifiers) {\n if (spec.type === 'ImportSpecifier') {\n importedNames.add(spec.local.name)\n }\n }\n }\n }\n\n if (importedNames.size === 0)\n return\n\n // Find exported variables assigned from imported function calls\n const trackedExports = new Set<string>()\n for (const node of ast.body) {\n if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {\n for (const decl of node.declaration.declarations) {\n if (\n decl.init?.type === 'CallExpression'\n && decl.init.callee?.type === 'Identifier'\n && importedNames.has(decl.init.callee.name)\n && decl.id?.type === 'Identifier'\n ) {\n trackedExports.add(decl.id.name)\n }\n }\n }\n }\n\n if (trackedExports.size > 0) {\n trackedExportsMap.set(filePath, trackedExports)\n }\n}\n\nfunction walkStatements(\n body: any[],\n s: MagicString,\n importedNames: Map<string, string>,\n trackedVars: Set<string>,\n): void {\n for (const stmt of body) {\n // Variable declaration: const x = importedFn(...)\n if (stmt.type === 'VariableDeclaration') {\n for (const decl of stmt.declarations) {\n if (\n decl.init?.type === 'CallExpression'\n && decl.init.callee?.type === 'Identifier'\n && importedNames.has(decl.init.callee.name)\n && decl.id?.type === 'Identifier'\n ) {\n // Track the variable\n trackedVars.add(decl.id.name)\n // Add /*#__PURE__*/ before the call expression\n s.appendLeft(decl.init.start, '/*#__PURE__*/ ')\n }\n }\n }\n\n // Expression statement using a tracked variable\n if (stmt.type === 'ExpressionStatement') {\n if (expressionUsesTrackedVar(stmt.expression, trackedVars)) {\n const needsParens = expressionNeedsParens(stmt.expression)\n if (needsParens) {\n s.appendLeft(stmt.expression.start, `${CONDITION} && (`)\n s.appendRight(stmt.expression.end, `)`)\n }\n else {\n s.appendLeft(stmt.expression.start, `${CONDITION} && `)\n }\n }\n }\n\n // Recurse into block-containing statements\n if (stmt.type === 'BlockStatement' || stmt.type === 'Program') {\n walkStatements(stmt.body, s, importedNames, trackedVars)\n }\n if (stmt.type === 'IfStatement') {\n if (stmt.consequent?.type === 'BlockStatement') {\n walkStatements(stmt.consequent.body, s, importedNames, trackedVars)\n }\n if (stmt.alternate?.type === 'BlockStatement') {\n walkStatements(stmt.alternate.body, s, importedNames, trackedVars)\n }\n }\n if (stmt.type === 'ForStatement' || stmt.type === 'WhileStatement' || stmt.type === 'DoWhileStatement') {\n if (stmt.body?.type === 'BlockStatement') {\n walkStatements(stmt.body.body, s, importedNames, trackedVars)\n }\n }\n if (stmt.type === 'FunctionDeclaration' || stmt.type === 'ArrowFunctionExpression') {\n if (stmt.body?.type === 'BlockStatement') {\n walkStatements(stmt.body.body, s, importedNames, trackedVars)\n }\n }\n // Handle ExportNamedDeclaration wrapping a VariableDeclaration\n if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {\n walkStatements([stmt.declaration], s, importedNames, trackedVars)\n }\n }\n}\n\n/**\n * Check if an expression references a tracked variable as the root of a member/call chain.\n */\nfunction expressionUsesTrackedVar(node: any, trackedVars: Set<string>): boolean {\n if (!node)\n return false\n\n // Direct identifier reference\n if (node.type === 'Identifier') {\n return trackedVars.has(node.name)\n }\n\n // Member expression: check the object (root of the chain)\n if (node.type === 'MemberExpression') {\n return expressionUsesTrackedVar(node.object, trackedVars)\n }\n\n // Call expression: check the callee\n if (node.type === 'CallExpression') {\n return expressionUsesTrackedVar(node.callee, trackedVars)\n }\n\n // Logical expression: check either side\n if (node.type === 'LogicalExpression') {\n return expressionUsesTrackedVar(node.left, trackedVars)\n || expressionUsesTrackedVar(node.right, trackedVars)\n }\n\n // Conditional (ternary): check consequent or alternate\n if (node.type === 'ConditionalExpression') {\n return expressionUsesTrackedVar(node.consequent, trackedVars)\n || expressionUsesTrackedVar(node.alternate, trackedVars)\n }\n\n // Unary expression: check argument\n if (node.type === 'UnaryExpression') {\n return expressionUsesTrackedVar(node.argument, trackedVars)\n }\n\n // Await expression: check argument\n if (node.type === 'AwaitExpression') {\n return expressionUsesTrackedVar(node.argument, trackedVars)\n }\n\n // Sequence expression: check any element\n if (node.type === 'SequenceExpression') {\n return node.expressions.some((expr: any) => expressionUsesTrackedVar(expr, trackedVars))\n }\n\n // Parenthesized expression: unwrap\n if (node.type === 'ParenthesizedExpression') {\n return expressionUsesTrackedVar(node.expression, trackedVars)\n }\n\n return false\n}\n","import type { UnpluginInstance } from 'unplugin'\nimport { appendFileSync } from 'node:fs'\nimport { createUnplugin } from 'unplugin'\n\nexport interface LogsSdkServerOptions {\n /**\n * Path to the log file.\n * @default '.nostics.log'\n */\n logFile?: string\n}\n\nexport const logsSDKServer: UnpluginInstance<LogsSdkServerOptions | undefined> = createUnplugin((options) => {\n const logFile = options?.logFile ?? '.nostics.log'\n\n return {\n name: 'nostics-server',\n enforce: 'pre',\n\n vite: {\n configureServer(server) {\n server.ws.on('nostics:report', (data) => {\n try {\n // TODO: validate data shape\n appendFileSync(logFile, `${JSON.stringify(data)}\\n`)\n }\n catch (err: unknown) {\n console.error(`[nostics]: Failed to write log to \"${logFile}\":`, err)\n }\n })\n },\n },\n }\n})\n","import type { UnpluginFactory, UnpluginInstance } from 'unplugin'\nimport type { TrackedExportsMap, TransformOptions } from './transform'\nimport { createUnplugin } from 'unplugin'\nimport { transform } from './transform'\n\nexport type LogsSdkPluginOptions = TransformOptions\n\nconst JS_EXTENSIONS_RE = /\\.[jt]sx?$/\nconst NODE_MODULES_RE = /\\/node_modules\\//\n\nconst unpluginFactory: UnpluginFactory<LogsSdkPluginOptions | undefined> = (options) => {\n const trackedExportsMap: TrackedExportsMap = new Map()\n\n return {\n name: 'nostics',\n\n transform: {\n filter: {\n id: {\n include: JS_EXTENSIONS_RE,\n exclude: NODE_MODULES_RE,\n },\n },\n handler(code, id) {\n const result = transform(code, id, options, trackedExportsMap)\n if (!result)\n return\n return {\n code: result.code,\n map: result.map as any,\n }\n },\n },\n }\n}\n\nexport const logsSDK: UnpluginInstance<LogsSdkPluginOptions | undefined> = /* #__PURE__ */ createUnplugin<LogsSdkPluginOptions | undefined>(unpluginFactory)\nexport default logsSDK\n\nexport { logsSDKServer } from './server-plugin'\nexport type { LogsSdkServerOptions } from './server-plugin'\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nostics",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.4",
|
|
5
|
+
"description": "Structured diagnostic code library",
|
|
6
|
+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/vercel-labs/nostics#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/vercel-labs/nostics.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/vercel-labs/nostics/issues",
|
|
7
14
|
"keywords": [],
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/index.mjs",
|
|
18
|
+
"./dev-reporter": "./dist/dev-reporter.mjs",
|
|
19
|
+
"./formatters/ansi": "./dist/formatters/ansi.mjs",
|
|
20
|
+
"./formatters/json": "./dist/formatters/json.mjs",
|
|
21
|
+
"./unplugin": "./dist/unplugin.mjs",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"skills"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"magic-string": ">=0.30.0",
|
|
31
|
+
"oxc-parser": ">=0.50.0",
|
|
32
|
+
"unplugin": ">=2.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"magic-string": {
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"oxc-parser": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"unplugin": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"magic-string": "^0.30.21",
|
|
47
|
+
"oxc-parser": "^0.124.0",
|
|
48
|
+
"unplugin": "^3.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@antfu/eslint-config": "^8.1.0",
|
|
52
|
+
"@antfu/ni": "^30.0.0",
|
|
53
|
+
"@antfu/utils": "^9.3.0",
|
|
54
|
+
"@posva/prompts": "^2.4.4",
|
|
55
|
+
"@types/node": "^25.5.2",
|
|
56
|
+
"@types/semver": "^7.7.1",
|
|
57
|
+
"bumpp": "^11.0.1",
|
|
58
|
+
"conventional-changelog": "^7.2.0",
|
|
59
|
+
"conventional-changelog-angular": "^8.3.1",
|
|
60
|
+
"esbuild": "^0.28.0",
|
|
61
|
+
"eslint": "^10.2.0",
|
|
62
|
+
"lint-staged": "^16.4.0",
|
|
63
|
+
"publint": "^0.3.18",
|
|
64
|
+
"semver": "^7.7.4",
|
|
65
|
+
"simple-git-hooks": "^2.13.1",
|
|
66
|
+
"tsdown": "^0.21.7",
|
|
67
|
+
"tsnapi": "^0.1.1",
|
|
68
|
+
"typescript": "^6.0.2",
|
|
69
|
+
"vite": "^8.0.7",
|
|
70
|
+
"vitest": "^4.1.3",
|
|
71
|
+
"nostics": "0.0.4"
|
|
72
|
+
},
|
|
73
|
+
"simple-git-hooks": {
|
|
74
|
+
"pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged"
|
|
75
|
+
},
|
|
76
|
+
"lint-staged": {
|
|
77
|
+
"*": "eslint --fix"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsdown",
|
|
81
|
+
"dev": "vitest",
|
|
82
|
+
"play": "pnpm -C playground run dev",
|
|
83
|
+
"lint": "eslint",
|
|
84
|
+
"release": "node scripts/release.ts",
|
|
85
|
+
"test": "pnpm run build && pnpm -r run build && tsc && eslint && vitest run",
|
|
86
|
+
"typecheck": "tsc"
|
|
87
|
+
}
|
|
11
88
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-diagnostic
|
|
3
|
+
description: "Add a new diagnostic code following the defineDiagnostics() conventions from nostics"
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Read Grep Glob Edit Write
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Add a New Diagnostic Code
|
|
9
|
+
|
|
10
|
+
## Step 1: Find the Target File
|
|
11
|
+
|
|
12
|
+
Locate the file containing the `defineDiagnostics()` call where the new code should be added.
|
|
13
|
+
|
|
14
|
+
Use Grep to search for `defineDiagnostics` across the project.
|
|
15
|
+
|
|
16
|
+
## Step 2: Determine the Code Identifier
|
|
17
|
+
|
|
18
|
+
Diagnostic codes follow the pattern `PREFIX_XNNNN`:
|
|
19
|
+
|
|
20
|
+
- **PREFIX** — project/domain name in uppercase (e.g., `NUXT`, `MATH`, `I18N`)
|
|
21
|
+
- **X** — category letter: `B` (build), `R` (runtime), `C` (config), `E` (error), `W` (warning), `D` (deprecation), `I` (info)
|
|
22
|
+
- **NNNN** — numeric sequence
|
|
23
|
+
|
|
24
|
+
Look at existing codes in the target file to determine the prefix, category, and next available number. Codes must never be reused once published.
|
|
25
|
+
|
|
26
|
+
## Step 3: Add the Definition
|
|
27
|
+
|
|
28
|
+
Add the new entry to the `codes` object inside `defineDiagnostics()`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
CODE_NAME: {
|
|
32
|
+
message: 'Static message.',
|
|
33
|
+
// OR with parameters:
|
|
34
|
+
message: (p: { paramName: string }) => `Template with ${p.paramName}.`,
|
|
35
|
+
fix: 'How to resolve the issue.', // optional but recommended
|
|
36
|
+
why: 'Why this diagnostic was triggered.', // optional
|
|
37
|
+
hint: 'Additional guidance.', // optional
|
|
38
|
+
level: 'error', // 'error' | 'warn' | 'suggestion' | 'deprecation'
|
|
39
|
+
// defaults to 'error' if omitted
|
|
40
|
+
},
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
- `message` is the only required field
|
|
45
|
+
- Parameters can appear in any template field (`message`, `fix`, `why`, `hint`) — TypeScript unions them all
|
|
46
|
+
- Always provide `fix` when the solution is known
|
|
47
|
+
- Set `level` explicitly if it's not `'error'`
|
|
48
|
+
- Use typed arrow functions for parameterized templates: `(p: { key: Type }) => string`
|