@forinda/kickjs-lint 4.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Felix Orinda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @forinda/kickjs-lint
2
+
3
+ Lint rules for KickJS conventions — DI tokens, plugin/adapter naming, and other framework patterns.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @forinda/kickjs-lint
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ ```bash
14
+ # adopter project — lints `src/`, warns on third-party kick/ squatting
15
+ kick-lint
16
+
17
+ # framework / first-party packages — lints `packages/`, errors on missing kick/ prefix
18
+ kick-lint --first-party
19
+
20
+ # custom scope
21
+ kick-lint --scope src,libs
22
+ ```
23
+
24
+ ## Programmatic
25
+
26
+ ```ts
27
+ import { runLint, formatViolations } from '@forinda/kickjs-lint'
28
+
29
+ const result = await runLint({ cwd: process.cwd(), firstParty: false })
30
+ if (result.violations.length > 0) console.error(formatViolations(result.violations))
31
+ ```
32
+
33
+ ## Rules
34
+
35
+ | Rule | Default | Description |
36
+ | --------------------------- | -------- | ---------------------------------------------------------------------------------- |
37
+ | `di-token-symbol` | `error` | DI tokens must use `createToken<T>()` instead of `Symbol(...)` in token files |
38
+ | `token-kick-prefix` | `error` | First-party tokens must start with the reserved `kick/` prefix |
39
+ | `token-reserved-prefix` | `warn` | Third-party tokens must not squat the reserved `kick/` prefix |
40
+
41
+ Inline-disable a rule on a single line:
42
+
43
+ ```ts
44
+ export const FOO = Symbol('Legacy') // kick-lint-disable di-token-symbol
45
+ ```
46
+
47
+ ## Why
48
+
49
+ See [architecture.md §22](https://github.com/forinda/kick-js/blob/main/architecture.md#22-di-token-convention--symbol-to-string-migration) for the convention rationale and the [v3→v4 migration guide](https://forinda.github.io/kick-js/guide/migration-v3-to-v4) for what changed.
package/bin.mjs ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `kick-lint` CLI entry. Wraps {@link runLint} from the package's
4
+ * programmatic API with argv parsing + process exit translation.
5
+ *
6
+ * Usage:
7
+ * kick-lint lint adopter project (cwd, scope=src)
8
+ * kick-lint --first-party lint framework code (cwd, scope=packages)
9
+ * kick-lint --scope src,libs override scoped subdirectories
10
+ * kick-lint --cwd /path/to/repo override working directory
11
+ */
12
+
13
+ import { runLint, formatViolations } from './dist/index.mjs'
14
+ import process from 'node:process'
15
+
16
+ const args = process.argv.slice(2)
17
+ let cwd = process.cwd()
18
+ let firstParty = false
19
+ let scope = ['src']
20
+
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i]
23
+ if (arg === '--first-party') {
24
+ firstParty = true
25
+ if (scope.length === 1 && scope[0] === 'src') scope = ['packages']
26
+ } else if (arg === '--scope') {
27
+ const next = args[++i]
28
+ if (!next) {
29
+ console.error('kick-lint: --scope requires a value')
30
+ process.exit(2)
31
+ }
32
+ scope = next
33
+ .split(',')
34
+ .map((s) => s.trim())
35
+ .filter(Boolean)
36
+ } else if (arg === '--cwd') {
37
+ const next = args[++i]
38
+ if (!next) {
39
+ console.error('kick-lint: --cwd requires a value')
40
+ process.exit(2)
41
+ }
42
+ cwd = next
43
+ } else if (arg === '--help' || arg === '-h') {
44
+ console.log(
45
+ 'Usage: kick-lint [--first-party] [--scope dir1,dir2] [--cwd path]\n' +
46
+ '\n' +
47
+ ' --first-party enable framework-strict ruleset (default scope: packages)\n' +
48
+ ' --scope <dirs> comma-separated subdirectories to walk (default: src)\n' +
49
+ ' --cwd <path> working directory override\n',
50
+ )
51
+ process.exit(0)
52
+ } else {
53
+ console.error(`kick-lint: unknown arg ${arg}`)
54
+ process.exit(2)
55
+ }
56
+ }
57
+
58
+ const result = await runLint({ cwd, firstParty, scope })
59
+ const errors = result.violations.filter((v) => v.severity === 'error').length
60
+
61
+ if (result.violations.length === 0) {
62
+ console.log(`kick-lint: ${result.filesScanned} file(s) scanned, no violations`)
63
+ process.exit(0)
64
+ }
65
+
66
+ console.error(formatViolations(result.violations))
67
+ process.exit(errors > 0 ? 1 : 0)
@@ -0,0 +1,49 @@
1
+
2
+ import { LintContext, Rule, Violation, diTokenSymbol, rules, tokenKickPrefix, tokenReservedPrefix } from "./rules.mjs";
3
+
4
+ //#region src/index.d.ts
5
+ /**
6
+ * Filenames that historically declare DI tokens. Lint runs only on
7
+ * these to keep false positives out of unrelated files (e.g. a class
8
+ * named `Symbol` in a math library).
9
+ */
10
+ declare const TOKEN_FILE_NAMES: Set<string>;
11
+ interface LintOptions {
12
+ /** Root directory to walk. Required. */
13
+ cwd: string;
14
+ /**
15
+ * Whether the project being linted is a first-party `@forinda/kickjs-*`
16
+ * package (enables `token-kick-prefix`, disables `token-reserved-prefix`).
17
+ * Defaults to `false` so adopter projects use the friendlier ruleset.
18
+ */
19
+ firstParty?: boolean;
20
+ /**
21
+ * Only lint files inside these subdirectories (relative to `cwd`).
22
+ * Defaults to `['packages']` for first-party use; pass `['src']` or
23
+ * `['.']` for adopter projects.
24
+ */
25
+ scope?: readonly string[];
26
+ /** Override the rule set. Defaults to every rule shipped by the package. */
27
+ rules?: readonly Rule[];
28
+ /** Filenames to lint (defaults to {@link TOKEN_FILE_NAMES}). */
29
+ filenames?: ReadonlySet<string>;
30
+ }
31
+ interface LintResult {
32
+ violations: Violation[];
33
+ filesScanned: number;
34
+ }
35
+ /**
36
+ * Walk `cwd` (constrained to `scope` directories), apply every rule to
37
+ * each token-bearing file, and return the aggregated violations + the
38
+ * number of files actually checked.
39
+ */
40
+ declare function runLint(opts: LintOptions): Promise<LintResult>;
41
+ /**
42
+ * Format a violation list for terminal output. One block per violation,
43
+ * trailing summary line. Returns `''` when there are no violations so
44
+ * callers can branch on truthiness.
45
+ */
46
+ declare function formatViolations(violations: readonly Violation[]): string;
47
+ //#endregion
48
+ export { type LintContext, LintOptions, LintResult, type Rule, TOKEN_FILE_NAMES, type Violation, rules as defaultRules, diTokenSymbol, formatViolations, runLint, tokenKickPrefix, tokenReservedPrefix };
49
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;cA0Ba,gBAAA,EAAgB,GAAA;AAAA,UAWZ,WAAA;;EAEf,GAAA;EAoBA;;;;;EAdA,UAAA;EAuB2B;;;;;EAjB3B,KAAA;EAiBuD;EAfvD,KAAA,YAAiB,IAAA;EAeW;EAb5B,SAAA,GAAY,WAAA;AAAA;AAAA,UAGG,UAAA;EACf,UAAA,EAAY,SAAA;EACZ,YAAA;AAAA;;;;;;iBAQoB,OAAA,CAAQ,IAAA,EAAM,WAAA,GAAc,OAAA,CAAQ,UAAA;;;;;;iBAkD1C,gBAAA,CAAiB,UAAA,WAAqB,SAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @forinda/kickjs-lint v4.0.0
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ import { diTokenSymbol, rules, tokenKickPrefix, tokenReservedPrefix } from "./rules.mjs";
12
+ import { readFile, readdir } from "node:fs/promises";
13
+ import { join, relative } from "node:path";
14
+ //#region src/index.ts
15
+ /**
16
+ * Programmatic entry point for `@forinda/kickjs-lint`.
17
+ *
18
+ * Walks a directory tree, applies every enabled rule from
19
+ * {@link ./rules} to each file that matches the token-bearing filename
20
+ * heuristic, and returns the aggregated violations.
21
+ *
22
+ * The CLI binary (`bin.mjs`) wraps this with a process-exit translation
23
+ * layer; downstream tools (devtools, editor extensions) call this
24
+ * directly.
25
+ *
26
+ * @module @forinda/kickjs-lint
27
+ */
28
+ /**
29
+ * Filenames that historically declare DI tokens. Lint runs only on
30
+ * these to keep false positives out of unrelated files (e.g. a class
31
+ * named `Symbol` in a math library).
32
+ */
33
+ const TOKEN_FILE_NAMES = new Set([
34
+ "types.ts",
35
+ "tokens.ts",
36
+ "constants.ts",
37
+ "service.ts",
38
+ "adapter.ts"
39
+ ]);
40
+ /** Directories the walker skips wholesale. */
41
+ const SKIP_DIRS = new Set([
42
+ "node_modules",
43
+ "dist",
44
+ "__tests__",
45
+ ".kickjs"
46
+ ]);
47
+ /**
48
+ * Walk `cwd` (constrained to `scope` directories), apply every rule to
49
+ * each token-bearing file, and return the aggregated violations + the
50
+ * number of files actually checked.
51
+ */
52
+ async function runLint(opts) {
53
+ const cwd = opts.cwd;
54
+ const firstParty = opts.firstParty ?? false;
55
+ const scope = opts.scope ?? ["packages"];
56
+ const ruleset = opts.rules ?? rules;
57
+ const filenames = opts.filenames ?? TOKEN_FILE_NAMES;
58
+ const violations = [];
59
+ let filesScanned = 0;
60
+ for (const root of scope) await walk(join(cwd, root), async (file) => {
61
+ const base = file.slice(file.lastIndexOf("/") + 1);
62
+ if (!filenames.has(base)) return;
63
+ const source = await readFile(file, "utf-8");
64
+ const rel = relative(cwd, file);
65
+ filesScanned++;
66
+ for (const rule of ruleset) violations.push(...rule.check({
67
+ source,
68
+ file: rel,
69
+ firstParty
70
+ }));
71
+ });
72
+ return {
73
+ violations,
74
+ filesScanned
75
+ };
76
+ }
77
+ async function walk(dir, visit) {
78
+ let entries;
79
+ try {
80
+ entries = await readdir(dir, { withFileTypes: true });
81
+ } catch {
82
+ return;
83
+ }
84
+ for (const entry of entries) {
85
+ const full = join(dir, entry.name);
86
+ if (entry.isDirectory()) {
87
+ if (SKIP_DIRS.has(entry.name)) continue;
88
+ await walk(full, visit);
89
+ } else if (entry.isFile()) await visit(full);
90
+ }
91
+ }
92
+ /**
93
+ * Format a violation list for terminal output. One block per violation,
94
+ * trailing summary line. Returns `''` when there are no violations so
95
+ * callers can branch on truthiness.
96
+ */
97
+ function formatViolations(violations) {
98
+ if (violations.length === 0) return "";
99
+ const lines = [];
100
+ for (const v of violations) {
101
+ const tag = v.severity === "error" ? "error" : "warn";
102
+ lines.push(` [${tag}] ${v.file}:${v.line} ${v.ruleId}`);
103
+ lines.push(` ${v.message}`);
104
+ if (v.suggestion) lines.push(` -> ${v.suggestion}`);
105
+ lines.push("");
106
+ }
107
+ const errors = violations.filter((v) => v.severity === "error").length;
108
+ const warnings = violations.length - errors;
109
+ lines.push(`${violations.length} violation(s): ${errors} error, ${warnings} warn`);
110
+ return lines.join("\n");
111
+ }
112
+ //#endregion
113
+ export { TOKEN_FILE_NAMES, rules as defaultRules, diTokenSymbol, formatViolations, runLint, tokenKickPrefix, tokenReservedPrefix };
114
+
115
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["defaultRules"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Programmatic entry point for `@forinda/kickjs-lint`.\n *\n * Walks a directory tree, applies every enabled rule from\n * {@link ./rules} to each file that matches the token-bearing filename\n * heuristic, and returns the aggregated violations.\n *\n * The CLI binary (`bin.mjs`) wraps this with a process-exit translation\n * layer; downstream tools (devtools, editor extensions) call this\n * directly.\n *\n * @module @forinda/kickjs-lint\n */\n\nimport { readdir, readFile } from 'node:fs/promises'\nimport { join, relative } from 'node:path'\nimport { rules as defaultRules, type Rule, type Violation } from './rules'\n\nexport type { LintContext, Rule, Violation } from './rules'\nexport { diTokenSymbol, tokenKickPrefix, tokenReservedPrefix, rules as defaultRules } from './rules'\n\n/**\n * Filenames that historically declare DI tokens. Lint runs only on\n * these to keep false positives out of unrelated files (e.g. a class\n * named `Symbol` in a math library).\n */\nexport const TOKEN_FILE_NAMES = new Set([\n 'types.ts',\n 'tokens.ts',\n 'constants.ts',\n 'service.ts',\n 'adapter.ts',\n])\n\n/** Directories the walker skips wholesale. */\nconst SKIP_DIRS = new Set(['node_modules', 'dist', '__tests__', '.kickjs'])\n\nexport interface LintOptions {\n /** Root directory to walk. Required. */\n cwd: string\n /**\n * Whether the project being linted is a first-party `@forinda/kickjs-*`\n * package (enables `token-kick-prefix`, disables `token-reserved-prefix`).\n * Defaults to `false` so adopter projects use the friendlier ruleset.\n */\n firstParty?: boolean\n /**\n * Only lint files inside these subdirectories (relative to `cwd`).\n * Defaults to `['packages']` for first-party use; pass `['src']` or\n * `['.']` for adopter projects.\n */\n scope?: readonly string[]\n /** Override the rule set. Defaults to every rule shipped by the package. */\n rules?: readonly Rule[]\n /** Filenames to lint (defaults to {@link TOKEN_FILE_NAMES}). */\n filenames?: ReadonlySet<string>\n}\n\nexport interface LintResult {\n violations: Violation[]\n filesScanned: number\n}\n\n/**\n * Walk `cwd` (constrained to `scope` directories), apply every rule to\n * each token-bearing file, and return the aggregated violations + the\n * number of files actually checked.\n */\nexport async function runLint(opts: LintOptions): Promise<LintResult> {\n const cwd = opts.cwd\n const firstParty = opts.firstParty ?? false\n const scope = opts.scope ?? ['packages']\n const ruleset = opts.rules ?? defaultRules\n const filenames = opts.filenames ?? TOKEN_FILE_NAMES\n\n const violations: Violation[] = []\n let filesScanned = 0\n\n for (const root of scope) {\n const start = join(cwd, root)\n await walk(start, async (file) => {\n const base = file.slice(file.lastIndexOf('/') + 1)\n if (!filenames.has(base)) return\n const source = await readFile(file, 'utf-8')\n const rel = relative(cwd, file)\n filesScanned++\n for (const rule of ruleset) {\n violations.push(...rule.check({ source, file: rel, firstParty }))\n }\n })\n }\n\n return { violations, filesScanned }\n}\n\nasync function walk(dir: string, visit: (file: string) => Promise<void>): Promise<void> {\n let entries\n try {\n entries = await readdir(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue\n await walk(full, visit)\n } else if (entry.isFile()) {\n await visit(full)\n }\n }\n}\n\n/**\n * Format a violation list for terminal output. One block per violation,\n * trailing summary line. Returns `''` when there are no violations so\n * callers can branch on truthiness.\n */\nexport function formatViolations(violations: readonly Violation[]): string {\n if (violations.length === 0) return ''\n const lines: string[] = []\n for (const v of violations) {\n const tag = v.severity === 'error' ? 'error' : 'warn'\n lines.push(` [${tag}] ${v.file}:${v.line} ${v.ruleId}`)\n lines.push(` ${v.message}`)\n if (v.suggestion) lines.push(` -> ${v.suggestion}`)\n lines.push('')\n }\n const errors = violations.filter((v) => v.severity === 'error').length\n const warnings = violations.length - errors\n lines.push(`${violations.length} violation(s): ${errors} error, ${warnings} warn`)\n return lines.join('\\n')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAa,mBAAmB,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,MAAM,YAAY,IAAI,IAAI;CAAC;CAAgB;CAAQ;CAAa;CAAU,CAAC;;;;;;AAiC3E,eAAsB,QAAQ,MAAwC;CACpE,MAAM,MAAM,KAAK;CACjB,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,QAAQ,KAAK,SAAS,CAAC,WAAW;CACxC,MAAM,UAAU,KAAK,SAASA;CAC9B,MAAM,YAAY,KAAK,aAAa;CAEpC,MAAM,aAA0B,EAAE;CAClC,IAAI,eAAe;AAEnB,MAAK,MAAM,QAAQ,MAEjB,OAAM,KADQ,KAAK,KAAK,KAAK,EACX,OAAO,SAAS;EAChC,MAAM,OAAO,KAAK,MAAM,KAAK,YAAY,IAAI,GAAG,EAAE;AAClD,MAAI,CAAC,UAAU,IAAI,KAAK,CAAE;EAC1B,MAAM,SAAS,MAAM,SAAS,MAAM,QAAQ;EAC5C,MAAM,MAAM,SAAS,KAAK,KAAK;AAC/B;AACA,OAAK,MAAM,QAAQ,QACjB,YAAW,KAAK,GAAG,KAAK,MAAM;GAAE;GAAQ,MAAM;GAAK;GAAY,CAAC,CAAC;GAEnE;AAGJ,QAAO;EAAE;EAAY;EAAc;;AAGrC,eAAe,KAAK,KAAa,OAAuD;CACtF,IAAI;AACJ,KAAI;AACF,YAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,MAAM,CAAC;SAC/C;AACN;;AAEF,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;AAClC,MAAI,MAAM,aAAa,EAAE;AACvB,OAAI,UAAU,IAAI,MAAM,KAAK,CAAE;AAC/B,SAAM,KAAK,MAAM,MAAM;aACd,MAAM,QAAQ,CACvB,OAAM,MAAM,KAAK;;;;;;;;AAUvB,SAAgB,iBAAiB,YAA0C;AACzE,KAAI,WAAW,WAAW,EAAG,QAAO;CACpC,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,KAAK,YAAY;EAC1B,MAAM,MAAM,EAAE,aAAa,UAAU,UAAU;AAC/C,QAAM,KAAK,MAAM,IAAI,IAAI,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI,EAAE,SAAS;AACzD,QAAM,KAAK,OAAO,EAAE,UAAU;AAC9B,MAAI,EAAE,WAAY,OAAM,KAAK,UAAU,EAAE,aAAa;AACtD,QAAM,KAAK,GAAG;;CAEhB,MAAM,SAAS,WAAW,QAAQ,MAAM,EAAE,aAAa,QAAQ,CAAC;CAChE,MAAM,WAAW,WAAW,SAAS;AACrC,OAAM,KAAK,GAAG,WAAW,OAAO,iBAAiB,OAAO,UAAU,SAAS,OAAO;AAClF,QAAO,MAAM,KAAK,KAAK"}
@@ -0,0 +1,64 @@
1
+
2
+ //#region src/rules.d.ts
3
+ /**
4
+ * Pure rule definitions. Each rule takes a source file's content + path
5
+ * and returns the violations it found. Stateless and synchronous so the
6
+ * runner can compose them freely (CLI, devtools, editor extension).
7
+ */
8
+ interface Violation {
9
+ ruleId: string;
10
+ severity: 'error' | 'warn';
11
+ file: string;
12
+ /** 1-based line number of the offending construct. */
13
+ line: number;
14
+ message: string;
15
+ suggestion?: string;
16
+ }
17
+ interface LintContext {
18
+ source: string;
19
+ file: string;
20
+ /**
21
+ * Whether the file lives inside `packages/<name>/src/` of the
22
+ * `@forinda/kickjs-*` monorepo. First-party files are held to the
23
+ * stricter `kick/` prefix; third-party adopter code is exempt.
24
+ */
25
+ firstParty: boolean;
26
+ }
27
+ interface Rule {
28
+ id: string;
29
+ description: string;
30
+ defaultSeverity: 'error' | 'warn';
31
+ check(ctx: LintContext): Violation[];
32
+ }
33
+ /**
34
+ * Forbid `export const X = Symbol('...')` declarations in token-bearing
35
+ * files. The §22 v4 migration replaced first-party DI tokens with
36
+ * `createToken<T>()`; this rule blocks regression in framework code and
37
+ * encourages adopters to follow the same pattern.
38
+ *
39
+ * Inline-disable: append `// kick-lint-disable di-token-symbol` to the
40
+ * declaration line.
41
+ */
42
+ declare const diTokenSymbol: Rule;
43
+ /**
44
+ * For first-party files, every `createToken('literal')` literal must
45
+ * start with the reserved `kick/` prefix. Third-party adopter code is
46
+ * exempt -- they use their own scope (`mycorp/`, `acme/`, etc.).
47
+ *
48
+ * Allowed back-compat exception: literals starting with `kickjs.` or
49
+ * `kickjs/` (the dotted convention pre-dates section 22 and gets
50
+ * migrated alongside the symbol-to-string work).
51
+ */
52
+ declare const tokenKickPrefix: Rule;
53
+ /**
54
+ * Third-party adopters must NOT squat the reserved `kick/` prefix --
55
+ * mirror image of {@link tokenKickPrefix}. Runs only on non-first-party
56
+ * code and warns rather than errors so the rule is informative without
57
+ * blocking adopter installs.
58
+ */
59
+ declare const tokenReservedPrefix: Rule;
60
+ /** All rules shipped by this package, in run order. */
61
+ declare const rules: readonly Rule[];
62
+ //#endregion
63
+ export { LintContext, Rule, Violation, diTokenSymbol, rules, tokenKickPrefix, tokenReservedPrefix };
64
+ //# sourceMappingURL=rules.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.mts","names":[],"sources":["../src/rules.ts"],"mappings":";;;AAMA;;;;UAAiB,SAAA;EACf,MAAA;EACA,QAAA;EACA,IAAA;EAGA;EADA,IAAA;EACA,OAAA;EACA,UAAA;AAAA;AAAA,UAGe,WAAA;EACf,MAAA;EACA,IAAA;EADA;;;;;EAOA,UAAA;AAAA;AAAA,UAGe,IAAA;EACf,EAAA;EACA,WAAA;EACA,eAAA;EACA,KAAA,CAAM,GAAA,EAAK,WAAA,GAAc,SAAA;AAAA;;;;;;AAmB3B;;;;cAAa,aAAA,EAAe,IAAA;AAmC5B;;;;;AAgCA;;;;AAhCA,cAAa,eAAA,EAAiB,IAAA;AA0D9B;;;;;;AAAA,cA1Ba,mBAAA,EAAqB,IAAA;;cA0BrB,KAAA,WAAgB,IAAA"}
package/dist/rules.mjs ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @forinda/kickjs-lint v4.0.0
3
+ *
4
+ * Copyright (c) Felix Orinda
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ //#region src/rules.ts
12
+ const SYMBOL_DECL_REGEX = /\bexport\s+const\s+([A-Z_][A-Z0-9_]*)\s*=\s*Symbol\s*\(/g;
13
+ const CREATE_TOKEN_LITERAL_REGEX = /createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+)['"`]/g;
14
+ function lineOf(source, index) {
15
+ return source.slice(0, index).split("\n").length;
16
+ }
17
+ /**
18
+ * Forbid `export const X = Symbol('...')` declarations in token-bearing
19
+ * files. The §22 v4 migration replaced first-party DI tokens with
20
+ * `createToken<T>()`; this rule blocks regression in framework code and
21
+ * encourages adopters to follow the same pattern.
22
+ *
23
+ * Inline-disable: append `// kick-lint-disable di-token-symbol` to the
24
+ * declaration line.
25
+ */
26
+ const diTokenSymbol = {
27
+ id: "di-token-symbol",
28
+ description: "DI tokens must use createToken<T>() instead of Symbol(...)",
29
+ defaultSeverity: "error",
30
+ check({ source, file }) {
31
+ const out = [];
32
+ const lines = source.split("\n");
33
+ for (let i = 0; i < lines.length; i++) {
34
+ const line = lines[i];
35
+ if (line.includes("// kick-lint-disable di-token-symbol")) continue;
36
+ SYMBOL_DECL_REGEX.lastIndex = 0;
37
+ const match = SYMBOL_DECL_REGEX.exec(line);
38
+ if (!match) continue;
39
+ out.push({
40
+ ruleId: diTokenSymbol.id,
41
+ severity: diTokenSymbol.defaultSeverity,
42
+ file,
43
+ line: i + 1,
44
+ message: `\`${match[1]} = Symbol(...)\` declares a DI token via Symbol`,
45
+ suggestion: `Use \`createToken<T>('kick/<area>/<key>')\` -- see architecture.md section 22`
46
+ });
47
+ }
48
+ return out;
49
+ }
50
+ };
51
+ /**
52
+ * For first-party files, every `createToken('literal')` literal must
53
+ * start with the reserved `kick/` prefix. Third-party adopter code is
54
+ * exempt -- they use their own scope (`mycorp/`, `acme/`, etc.).
55
+ *
56
+ * Allowed back-compat exception: literals starting with `kickjs.` or
57
+ * `kickjs/` (the dotted convention pre-dates section 22 and gets
58
+ * migrated alongside the symbol-to-string work).
59
+ */
60
+ const tokenKickPrefix = {
61
+ id: "token-kick-prefix",
62
+ description: "First-party DI tokens must start with the reserved `kick/` prefix",
63
+ defaultSeverity: "error",
64
+ check({ source, file, firstParty }) {
65
+ if (!firstParty) return [];
66
+ const out = [];
67
+ CREATE_TOKEN_LITERAL_REGEX.lastIndex = 0;
68
+ let match;
69
+ while ((match = CREATE_TOKEN_LITERAL_REGEX.exec(source)) !== null) {
70
+ const literal = match[1];
71
+ if (literal.startsWith("kick/")) continue;
72
+ if (literal.startsWith("kickjs.") || literal.startsWith("kickjs/")) continue;
73
+ out.push({
74
+ ruleId: tokenKickPrefix.id,
75
+ severity: tokenKickPrefix.defaultSeverity,
76
+ file,
77
+ line: lineOf(source, match.index),
78
+ message: `\`createToken('${literal}')\` is missing the reserved \`kick/\` prefix`,
79
+ suggestion: `Rename to \`'kick/${literal}'\` -- first-party tokens own the \`kick/\` namespace`
80
+ });
81
+ }
82
+ return out;
83
+ }
84
+ };
85
+ /**
86
+ * Third-party adopters must NOT squat the reserved `kick/` prefix --
87
+ * mirror image of {@link tokenKickPrefix}. Runs only on non-first-party
88
+ * code and warns rather than errors so the rule is informative without
89
+ * blocking adopter installs.
90
+ */
91
+ const tokenReservedPrefix = {
92
+ id: "token-reserved-prefix",
93
+ description: "Third-party tokens must not start with the reserved `kick/` prefix",
94
+ defaultSeverity: "warn",
95
+ check({ source, file, firstParty }) {
96
+ if (firstParty) return [];
97
+ const out = [];
98
+ CREATE_TOKEN_LITERAL_REGEX.lastIndex = 0;
99
+ let match;
100
+ while ((match = CREATE_TOKEN_LITERAL_REGEX.exec(source)) !== null) {
101
+ const literal = match[1];
102
+ if (!literal.startsWith("kick/")) continue;
103
+ out.push({
104
+ ruleId: tokenReservedPrefix.id,
105
+ severity: tokenReservedPrefix.defaultSeverity,
106
+ file,
107
+ line: lineOf(source, match.index),
108
+ message: `\`createToken('${literal}')\` squats the reserved \`kick/\` prefix`,
109
+ suggestion: `Pick an org-scoped prefix instead (e.g. \`'mycorp/...'\`)`
110
+ });
111
+ }
112
+ return out;
113
+ }
114
+ };
115
+ /** All rules shipped by this package, in run order. */
116
+ const rules = [
117
+ diTokenSymbol,
118
+ tokenKickPrefix,
119
+ tokenReservedPrefix
120
+ ];
121
+ //#endregion
122
+ export { diTokenSymbol, rules, tokenKickPrefix, tokenReservedPrefix };
123
+
124
+ //# sourceMappingURL=rules.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.mjs","names":[],"sources":["../src/rules.ts"],"sourcesContent":["/**\n * Pure rule definitions. Each rule takes a source file's content + path\n * and returns the violations it found. Stateless and synchronous so the\n * runner can compose them freely (CLI, devtools, editor extension).\n */\n\nexport interface Violation {\n ruleId: string\n severity: 'error' | 'warn'\n file: string\n /** 1-based line number of the offending construct. */\n line: number\n message: string\n suggestion?: string\n}\n\nexport interface LintContext {\n source: string\n file: string\n /**\n * Whether the file lives inside `packages/<name>/src/` of the\n * `@forinda/kickjs-*` monorepo. First-party files are held to the\n * stricter `kick/` prefix; third-party adopter code is exempt.\n */\n firstParty: boolean\n}\n\nexport interface Rule {\n id: string\n description: string\n defaultSeverity: 'error' | 'warn'\n check(ctx: LintContext): Violation[]\n}\n\nconst SYMBOL_DECL_REGEX = /\\bexport\\s+const\\s+([A-Z_][A-Z0-9_]*)\\s*=\\s*Symbol\\s*\\(/g\nconst CREATE_TOKEN_LITERAL_REGEX = /createToken\\s*(?:<[^>]*>)?\\s*\\(\\s*['\"`]([^'\"`]+)['\"`]/g\n\nfunction lineOf(source: string, index: number): number {\n return source.slice(0, index).split('\\n').length\n}\n\n/**\n * Forbid `export const X = Symbol('...')` declarations in token-bearing\n * files. The §22 v4 migration replaced first-party DI tokens with\n * `createToken<T>()`; this rule blocks regression in framework code and\n * encourages adopters to follow the same pattern.\n *\n * Inline-disable: append `// kick-lint-disable di-token-symbol` to the\n * declaration line.\n */\nexport const diTokenSymbol: Rule = {\n id: 'di-token-symbol',\n description: 'DI tokens must use createToken<T>() instead of Symbol(...)',\n defaultSeverity: 'error',\n check({ source, file }) {\n const out: Violation[] = []\n const lines = source.split('\\n')\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]\n if (line.includes('// kick-lint-disable di-token-symbol')) continue\n SYMBOL_DECL_REGEX.lastIndex = 0\n const match = SYMBOL_DECL_REGEX.exec(line)\n if (!match) continue\n out.push({\n ruleId: diTokenSymbol.id,\n severity: diTokenSymbol.defaultSeverity,\n file,\n line: i + 1,\n message: `\\`${match[1]} = Symbol(...)\\` declares a DI token via Symbol`,\n suggestion: `Use \\`createToken<T>('kick/<area>/<key>')\\` -- see architecture.md section 22`,\n })\n }\n return out\n },\n}\n\n/**\n * For first-party files, every `createToken('literal')` literal must\n * start with the reserved `kick/` prefix. Third-party adopter code is\n * exempt -- they use their own scope (`mycorp/`, `acme/`, etc.).\n *\n * Allowed back-compat exception: literals starting with `kickjs.` or\n * `kickjs/` (the dotted convention pre-dates section 22 and gets\n * migrated alongside the symbol-to-string work).\n */\nexport const tokenKickPrefix: Rule = {\n id: 'token-kick-prefix',\n description: 'First-party DI tokens must start with the reserved `kick/` prefix',\n defaultSeverity: 'error',\n check({ source, file, firstParty }) {\n if (!firstParty) return []\n const out: Violation[] = []\n CREATE_TOKEN_LITERAL_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = CREATE_TOKEN_LITERAL_REGEX.exec(source)) !== null) {\n const literal = match[1]\n if (literal.startsWith('kick/')) continue\n if (literal.startsWith('kickjs.') || literal.startsWith('kickjs/')) continue\n out.push({\n ruleId: tokenKickPrefix.id,\n severity: tokenKickPrefix.defaultSeverity,\n file,\n line: lineOf(source, match.index),\n message: `\\`createToken('${literal}')\\` is missing the reserved \\`kick/\\` prefix`,\n suggestion: `Rename to \\`'kick/${literal}'\\` -- first-party tokens own the \\`kick/\\` namespace`,\n })\n }\n return out\n },\n}\n\n/**\n * Third-party adopters must NOT squat the reserved `kick/` prefix --\n * mirror image of {@link tokenKickPrefix}. Runs only on non-first-party\n * code and warns rather than errors so the rule is informative without\n * blocking adopter installs.\n */\nexport const tokenReservedPrefix: Rule = {\n id: 'token-reserved-prefix',\n description: 'Third-party tokens must not start with the reserved `kick/` prefix',\n defaultSeverity: 'warn',\n check({ source, file, firstParty }) {\n if (firstParty) return []\n const out: Violation[] = []\n CREATE_TOKEN_LITERAL_REGEX.lastIndex = 0\n let match: RegExpExecArray | null\n while ((match = CREATE_TOKEN_LITERAL_REGEX.exec(source)) !== null) {\n const literal = match[1]\n if (!literal.startsWith('kick/')) continue\n out.push({\n ruleId: tokenReservedPrefix.id,\n severity: tokenReservedPrefix.defaultSeverity,\n file,\n line: lineOf(source, match.index),\n message: `\\`createToken('${literal}')\\` squats the reserved \\`kick/\\` prefix`,\n suggestion: `Pick an org-scoped prefix instead (e.g. \\`'mycorp/...'\\`)`,\n })\n }\n return out\n },\n}\n\n/** All rules shipped by this package, in run order. */\nexport const rules: readonly Rule[] = [diTokenSymbol, tokenKickPrefix, tokenReservedPrefix]\n"],"mappings":";;;;;;;;;;;AAkCA,MAAM,oBAAoB;AAC1B,MAAM,6BAA6B;AAEnC,SAAS,OAAO,QAAgB,OAAuB;AACrD,QAAO,OAAO,MAAM,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC;;;;;;;;;;;AAY5C,MAAa,gBAAsB;CACjC,IAAI;CACJ,aAAa;CACb,iBAAiB;CACjB,MAAM,EAAE,QAAQ,QAAQ;EACtB,MAAM,MAAmB,EAAE;EAC3B,MAAM,QAAQ,OAAO,MAAM,KAAK;AAChC,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,MAAM,OAAO,MAAM;AACnB,OAAI,KAAK,SAAS,uCAAuC,CAAE;AAC3D,qBAAkB,YAAY;GAC9B,MAAM,QAAQ,kBAAkB,KAAK,KAAK;AAC1C,OAAI,CAAC,MAAO;AACZ,OAAI,KAAK;IACP,QAAQ,cAAc;IACtB,UAAU,cAAc;IACxB;IACA,MAAM,IAAI;IACV,SAAS,KAAK,MAAM,GAAG;IACvB,YAAY;IACb,CAAC;;AAEJ,SAAO;;CAEV;;;;;;;;;;AAWD,MAAa,kBAAwB;CACnC,IAAI;CACJ,aAAa;CACb,iBAAiB;CACjB,MAAM,EAAE,QAAQ,MAAM,cAAc;AAClC,MAAI,CAAC,WAAY,QAAO,EAAE;EAC1B,MAAM,MAAmB,EAAE;AAC3B,6BAA2B,YAAY;EACvC,IAAI;AACJ,UAAQ,QAAQ,2BAA2B,KAAK,OAAO,MAAM,MAAM;GACjE,MAAM,UAAU,MAAM;AACtB,OAAI,QAAQ,WAAW,QAAQ,CAAE;AACjC,OAAI,QAAQ,WAAW,UAAU,IAAI,QAAQ,WAAW,UAAU,CAAE;AACpE,OAAI,KAAK;IACP,QAAQ,gBAAgB;IACxB,UAAU,gBAAgB;IAC1B;IACA,MAAM,OAAO,QAAQ,MAAM,MAAM;IACjC,SAAS,kBAAkB,QAAQ;IACnC,YAAY,qBAAqB,QAAQ;IAC1C,CAAC;;AAEJ,SAAO;;CAEV;;;;;;;AAQD,MAAa,sBAA4B;CACvC,IAAI;CACJ,aAAa;CACb,iBAAiB;CACjB,MAAM,EAAE,QAAQ,MAAM,cAAc;AAClC,MAAI,WAAY,QAAO,EAAE;EACzB,MAAM,MAAmB,EAAE;AAC3B,6BAA2B,YAAY;EACvC,IAAI;AACJ,UAAQ,QAAQ,2BAA2B,KAAK,OAAO,MAAM,MAAM;GACjE,MAAM,UAAU,MAAM;AACtB,OAAI,CAAC,QAAQ,WAAW,QAAQ,CAAE;AAClC,OAAI,KAAK;IACP,QAAQ,oBAAoB;IAC5B,UAAU,oBAAoB;IAC9B;IACA,MAAM,OAAO,QAAQ,MAAM,MAAM;IACjC,SAAS,kBAAkB,QAAQ;IACnC,YAAY;IACb,CAAC;;AAEJ,SAAO;;CAEV;;AAGD,MAAa,QAAyB;CAAC;CAAe;CAAiB;CAAoB"}
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@forinda/kickjs-lint",
3
+ "version": "4.0.0",
4
+ "description": "Lint rules for KickJS — DI token convention, plugin/adapter naming, and other framework conventions",
5
+ "keywords": [
6
+ "kickjs",
7
+ "lint",
8
+ "convention",
9
+ "di",
10
+ "token",
11
+ "static-analysis",
12
+ "@forinda/kickjs",
13
+ "@forinda/kickjs-cli"
14
+ ],
15
+ "type": "module",
16
+ "main": "dist/index.mjs",
17
+ "types": "dist/index.d.mts",
18
+ "bin": {
19
+ "kick-lint": "bin.mjs"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/index.mjs",
24
+ "types": "./dist/index.d.mts"
25
+ },
26
+ "./rules": {
27
+ "import": "./dist/rules.mjs",
28
+ "types": "./dist/rules.d.mts"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "bin.mjs"
34
+ ],
35
+ "wireit": {
36
+ "build": {
37
+ "command": "tsdown",
38
+ "files": [
39
+ "src/**/*.ts",
40
+ "tsdown.config.ts",
41
+ "tsconfig.json",
42
+ "package.json"
43
+ ],
44
+ "output": [
45
+ "dist/**"
46
+ ]
47
+ }
48
+ },
49
+ "dependencies": {},
50
+ "devDependencies": {
51
+ "@types/node": "^25.6.0",
52
+ "typescript": "^6.0.3",
53
+ "vitest": "^4.1.5"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "license": "MIT",
59
+ "author": "Felix Orinda",
60
+ "engines": {
61
+ "node": ">=20.0"
62
+ },
63
+ "homepage": "https://forinda.github.io/kick-js/",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/forinda/kick-js.git",
67
+ "directory": "packages/lint"
68
+ },
69
+ "bugs": {
70
+ "url": "https://github.com/forinda/kick-js/issues"
71
+ },
72
+ "scripts": {
73
+ "build": "wireit",
74
+ "dev": "tsdown --watch",
75
+ "test": "vitest run",
76
+ "typecheck": "tsc --noEmit",
77
+ "clean": "rm -rf dist .wireit"
78
+ }
79
+ }