@longzai-intelligence-oxlint/architecture-library-plugin 0.1.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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @longzai-intelligence-oxlint/architecture-library-plugin
2
+
3
+ ## 意图
4
+
5
+ 在静态检查阶段约束库级测试目录结构:测试文件放置规范、非测试文件不得混入测试目录、测试文件须与源文件匹配。
6
+
7
+ ## 定位
8
+
9
+ `architecture` 插件族的库级子插件,承载测试目录结构约束类规则。从原 `plugin-architecture` 单包拆分而来(ADR 0002)。
10
+
11
+ ## 规则
12
+
13
+ - `test-file-placement` — 测试文件放置规范
14
+ - `no-non-test-files-in-tests-directory` — 禁止非测试文件进入测试目录
15
+ - `test-file-matches-source` — 测试文件须与源文件匹配
@@ -0,0 +1,8 @@
1
+ import { OxlintRules } from "@longzai-intelligence-oxlint/core";
2
+ //#region src/configs/recommended-library.config.d.ts
3
+ /**
4
+ * Architecture-library 插件推荐规则配置对象
5
+ */
6
+ declare const recommended: OxlintRules;
7
+ //#endregion
8
+ export { recommended };
@@ -0,0 +1 @@
1
+ const e={"@longzai-intelligence-oxlint/architecture-library-plugin/test-file-placement":`error`,"@longzai-intelligence-oxlint/architecture-library-plugin/no-non-test-files-in-tests-directory":`error`,"@longzai-intelligence-oxlint/architecture-library-plugin/test-file-matches-source":`error`};export{e as recommended};
@@ -0,0 +1,15 @@
1
+ //#region src/rules/index.d.ts
2
+ /**
3
+ * library 规则集合
4
+ */
5
+ declare const libraryRules: {
6
+ 'test-file-placement': import("@oxlint/plugins").Rule;
7
+ 'no-non-test-files-in-tests-directory': import("@oxlint/plugins").Rule;
8
+ 'test-file-matches-source': import("@oxlint/plugins").Rule;
9
+ };
10
+ /**
11
+ * architecture-library 子插件
12
+ */
13
+ declare const architectureLibraryPlugin: import("@oxlint/plugins").Plugin;
14
+ //#endregion
15
+ export { architectureLibraryPlugin, architectureLibraryPlugin as default, libraryRules };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{defineRule as e,eslintCompatPlugin as t}from"@oxlint/plugins";import n from"node:path";import r,{existsSync as i}from"node:fs";const a=[/\.test\.ts$/,/\.test\.tsx$/,/\.spec\.ts$/,/\.spec\.tsx$/],o=[`/node_modules/`,`/dist/`,`/out/`,`/.bun/`];function s(e){return e.replace(/\\/g,`/`)}function c(e){return a.some(t=>t.test(e))}function l(e){let t=s(e);return o.some(e=>t.includes(e))}const u=[{segment:`/src/main/`,process:`main`},{segment:`/src/renderer/`,process:`renderer`},{segment:`/src/preload/`,process:`preload`},{segment:`/src/shared/`,process:`shared`}];function d(e){let t=n.dirname(e),r=n.dirname(t);for(;t!==r;){if(i(n.join(t,`package.json`)))return t;t=n.dirname(t)}return n.dirname(e)}const f={name:`electron-process`,detect(e){let t=s(e);for(let n of u)if(t.includes(n.segment)){let r=d(e),i=p(t,n.segment);return{type:`apps`,isElectron:!0,electronProcess:n.process,srcRoot:i,projectRoot:r}}return null}};function p(e,t){let n=e.indexOf(t);return e.slice(0,n+t.length-1)}const m={name:`package-json`,detect(e){let t=h(e);if(!t)return null;let n=g(t);return{type:_(t),isElectron:!1,srcRoot:n,projectRoot:t}}};function h(e){let t=n.dirname(e),r=n.dirname(t);for(;t!==r;){if(i(n.join(t,`package.json`)))return t;t=n.dirname(t)}return null}function g(e){let t=n.join(e,`src`);return i(t)?t:e}function _(e){let t=s(e);return t.includes(`/packages/`)?`packages`:t.includes(`/domains/`)?`domains`:t.includes(`/apps/`)?`apps`:t.includes(`/infrastructure/`)?`infrastructure`:t.includes(`/templates/`)?`templates`:t.includes(`/database/`)?`database`:`unknown`}function v(){let e=[];return{detectors:e,addDetector(t){return e.push(t),this},detect(t){for(let n of e){let e=n.detect(t);if(e)return e}return null}}}const y=v().addDetector(f).addDetector(m),b=[/\.test\.ts$/,/\.test\.tsx$/,/\.spec\.ts$/,/\.spec\.tsx$/],x=[`.ts`,`.tsx`],S=new Set([`node_modules`,`dist`,`out`,`.bun`,`.turbo`,`.cache`,`generated`]),C=new Set([`bun`,`vitest`]),w=new Set([`unit`,`integration`,`e2e`]),T=new Map;function E(e,t){let i;try{i=r.readdirSync(e,{withFileTypes:!0})}catch{return}for(let r of i){let i=n.join(e,r.name);if(r.isDirectory()){if(S.has(r.name))continue;E(i,t)}else r.isFile()&&t(i)}}function D(e){for(let t of b)if(t.test(e))return e.replace(t,``);return e}function ee(e){let t=[],n=e;for(;n.includes(`.`);){let e=n.lastIndexOf(`.`);n=n.substring(0,e),t.push(n)}return t}function te(e){let t=s(e),r=t.indexOf(`/__tests__/`);return r===-1?n.dirname(e):t.substring(0,r)}function O(e,t,r){let i=s(e),a=i.indexOf(`/__tests__/`);if(a===-1)return null;let o=i.substring(a+11).split(`/`);if(o.length<2)return null;let c=o[0];if(!c)return null;let l=C.has(c),u=r?r.includes(c):w.has(c),d=l||u?o.slice(1,-1).join(`/`):o.slice(0,-1).join(`/`);return d?n.join(t,d):t}function k(e,t){for(let i of x){let a=n.join(t,e+i);if(r.existsSync(a))return a}return null}function A(e,t){let i=n.join(t,e);try{if(r.existsSync(i)&&r.statSync(i).isDirectory())return i}catch{return null}return null}function j(e,t){let i=n.join(t,e);try{if(!r.existsSync(i)||!r.statSync(i).isDirectory())return null}catch{return null}for(let e of x){let t=n.join(i,`index`+e);if(r.existsSync(t))return t}return null}function M(e){let t=T.get(e);if(t)return t;let n=[];return E(e,e=>n.push(e)),T.set(e,n),n}function N(e,t){for(let n of t){let t=k(e,n);if(t)return t}return null}function P(e,t){for(let n of t){let t=A(e,n);if(t)return t}return null}function F(e,t){for(let n of t){let t=j(e,n);if(t)return t}return null}function I(e,t,i,a){let o=[e,...t];for(let e of i){if(!r.existsSync(e))continue;let t=M(e);for(let e of t){let t=n.basename(e);if(!c(t)){for(let n of o)for(let r of x)if(t===n+r)return e;if(a){let n=t;for(let e of x)if(t.endsWith(e)){n=t.slice(0,-e.length);break}for(let t of o)if(t.includes(n)||n.includes(t))return e}}}}return null}function L(e,t,r,i){let a=n.dirname(t),o=n.dirname(a),s=[a,o],c=O(t,r,i?.testTypeSubdirs),l=c?[c,r,o]:[r,o];if(c){let t=k(e,c);if(t)return t;let n=A(e,c);if(n)return n;let r=j(e,c);if(r)return r}let u=N(e,s);if(u)return u;let d=P(e,l);if(d)return d;let f=F(e,l);if(f)return f;let p=ee(e),m=c?[c,...s]:s;for(let e of p){let t=N(e,m);if(t)return t;let n=P(e,l);if(n)return n;let r=F(e,l);if(r)return r}return i?.enableGlobalSearch?I(e,p,[r,a],i?.enableGlobalSearch):null}function R(e,t){return L(D(n.basename(e)),e,te(e),t)?{isMatch:!0}:{isMatch:!1}}function z(e){let t=n.basename(e);return c(t)&&!l(e)&&!t.endsWith(`.d.ts`)}const B=[`**/vitest.setup.ts`,`**/jest.setup.ts`,`**/fixtures/**`,`**/helpers/**`,`**/mocks/**`,`**/__mocks__/**`,`**/*.d.ts`,`**/bun/**`,`**/vitest/**`];function V(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)}function H(e){if(typeof e!=`object`||!e)return!1;if(!(`allowPatterns`in e))return!0;let t=e.allowPatterns;return Array.isArray(t)&&V(t)}function U(e){let t=Array.isArray(e)?e[0]:e;return!H(t)||!t.allowPatterns?{allowPatterns:[...B]}:{allowPatterns:[...B,...t.allowPatterns]}}function W(e){return a.some(t=>t.test(e))}function G(e,t){let n=t.replace(/[.+^${}()|[\]\\]/g,`\\$&`).replace(/\*\*/g,`{{DOUBLE_STAR}}`).replace(/\*/g,`[^/]*`).replace(/{{DOUBLE_STAR}}/g,`.*`);return RegExp(`^${n}$`).test(e)}function K(e,t){return t.some(t=>G(e,t))}const q=e({meta:{type:`problem`,docs:{description:`禁止在 __tests__ 目录下放置非测试文件`}},createOnce(e){return{before(){let t=U(e.options),r=e.filename,i=s(r);if(!i.includes(`/__tests__/`)||W(n.basename(r))||K(i,t.allowPatterns))return!1},Program(t){let r=e.filename,i=n.basename(r);e.report({node:t,message:`文件 "${i}" 位于 __tests__ 目录下但不是测试文件。测试目录应仅包含测试文件(*.test.ts, *.spec.ts)。如果是辅助文件,请移动到 src/test-helpers/ 或 src/testing/ 目录。`})}}}}),J=e({meta:{type:`problem`,docs:{description:`确保测试文件名与被测试的源文件名或目录名保持一致`},schema:[{type:`object`,properties:{testTypeSubdirs:{type:`array`,items:{type:`string`}},enableGlobalSearch:{type:`boolean`}},additionalProperties:!1}]},createOnce(e){let t=e.options?.[0];return{before(){let t=e.filename;if(!z(t))return!1},Program(r){let i=e.filename,a=n.basename(i);R(i,t).isMatch||e.report({node:r,message:`测试文件 "${a}" 未找到匹配的源文件。测试文件名应与源文件名或目录名保持一致(如 user.service.ts -> user.service.test.ts,bridge/ -> bridge.test.ts)。拆分测试文件时,使用点号分隔拆分标识(如 comment.orm-repository.ts -> comment.orm-repository.query.test.ts,而非 comment.orm-repository-query.test.ts)`})}}}});function Y(e){return e.isElectron&&e.electronProcess?`src/${e.electronProcess}/__tests__`:`src/__tests__`}function X(e,t){return s(e).startsWith(`${t.srcRoot}/__tests__/`)}function Z(e,t){let r=s(e),i=n.basename(e),a=r.indexOf(`/src/`);if(a===-1)return`${t.srcRoot}/__tests__/${i}`;let o=r.slice(a+5).split(`/`);if(o.length>1&&o.pop(),t.isElectron&&t.electronProcess){let e=o.indexOf(t.electronProcess);e!==-1&&o.splice(e,1)}let c=o.indexOf(`__tests__`);c!==-1&&o.splice(c,1);let l=o.join(`/`),u=Y(t);return l?`${u}/${l}/${i}`:`${u}/${i}`}const Q={"test-file-placement":e({meta:{type:`problem`,docs:{description:`强制测试文件放置在 src/__tests__ 目录下`}},createOnce(e){return{before(){let t=e.filename,r=n.basename(t);if(!c(r)||l(t)||r.endsWith(`.d.ts`))return!1;let i=y.detect(t);if(!i||i.type===`unknown`||X(t,i))return!1},Program(t){let r=e.filename,i=n.basename(r),a=y.detect(r);if(!a)return;let o=Y(a),s=Z(r,a);e.report({node:t,message:`测试文件 "${i}" 必须放置在 ${o} 目录下。当前路径: "${r}"。建议路径: "${s}"`})}}}}),"no-non-test-files-in-tests-directory":q,"test-file-matches-source":J},$=t({meta:{name:`@longzai-intelligence-oxlint/architecture-library-plugin`},rules:Q});export{$ as architectureLibraryPlugin,$ as default,Q as libraryRules};
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@longzai-intelligence-oxlint/architecture-library-plugin",
3
+ "version": "0.1.0",
4
+ "license": "UNLICENSED",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./configs/recommended-library": {
17
+ "types": "./dist/configs/recommended-library.d.ts",
18
+ "import": "./dist/configs/recommended-library.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "lzi-builder",
23
+ "build:prod": "NODE_ENV=production lzi-builder",
24
+ "prepublishOnly": "bun run build:prod",
25
+ "dev": "lzi-builder --watch",
26
+ "clean": "rimraf dist",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "lint": "oxlint && oxfmt --check",
31
+ "lint:fix": "oxlint --fix && oxfmt",
32
+ "typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
33
+ "typecheck:app": "tsgo --noEmit -p tsconfig/app.json",
34
+ "typecheck:node": "tsgo --noEmit -p tsconfig/node.json",
35
+ "typecheck:test": "tsgo --noEmit -p tsconfig/test.json"
36
+ },
37
+ "dependencies": {
38
+ "@longzai-intelligence-oxlint/core": "0.1.0",
39
+ "@oxlint/plugins": "^1.74.0"
40
+ },
41
+ "devDependencies": {
42
+ "@longzai-intelligence-oxlint/testing": "0.1.0",
43
+ "@vitest/coverage-v8": "^4.1.10",
44
+ "typescript": "^6.0.3"
45
+ },
46
+ "peerDependencies": {
47
+ "oxlint": ">=1.0.0"
48
+ }
49
+ }