@jterrazz/codestyle 1.1.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/codestyle",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
5
5
  "bin": {
6
6
  "codestyle": "./src/codestyle.sh"
@@ -16,6 +16,7 @@
16
16
  "./oxlint/expo": "./src/oxlint/expo.json",
17
17
  "./oxlint/nextjs": "./src/oxlint/nextjs.json",
18
18
  "./oxlint/architectures/hexagonal": "./src/oxlint/architectures/hexagonal.json",
19
+ "./oxlint/plugins/codestyle": "./src/oxlint/plugins/codestyle.js",
19
20
  "./oxfmt": "./src/oxfmt/index.json"
20
21
  },
21
22
  "publishConfig": {
@@ -26,7 +27,8 @@
26
27
  "lint:fix": "oxlint --fix --ignore-pattern '**/fixtures/**' && oxfmt",
27
28
  "test": "vitest --run",
28
29
  "test:watch": "vitest",
29
- "build": "# no build script"
30
+ "build": "# no build script",
31
+ "postinstall": "node src/postinstall.js"
30
32
  },
31
33
  "dependencies": {
32
34
  "@typescript/native-preview": "^7.0.0-dev",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://oxc.rs/schemas/oxfmt/0.19.0/schema.json",
2
+ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxfmt/configuration_schema.json",
3
3
  "printWidth": 100,
4
4
  "tabWidth": 4,
5
5
  "useTabs": false,
@@ -1,23 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
3
- "jsPlugins": ["../plugins/architecture-boundaries.js"],
4
3
  "rules": {
5
- // Hexagonal Architecture (Ports & Adapters)
6
- //
7
- // Dependency rule: outer layers depend on inner layers, never the reverse
8
- // All layers CAN import domain (it's the core)
9
- //
10
- // ┌─────────────────────────────────────────┐
11
- // │ infrastructure / presentation (outer) │
12
- // │ ┌─────────────────────────────────┐ │
13
- // │ │ application (use-cases, ports) │ │
14
- // │ │ ┌─────────────────────────┐ │ │
15
- // │ │ │ domain (core business) │ │ │
16
- // │ │ └─────────────────────────┘ │ │
17
- // │ └─────────────────────────────────┘ │
18
- // └─────────────────────────────────────────┘
19
-
20
- "architecture-boundaries/architecture-boundaries": [
4
+ "codestyle/arch-hexagonal": [
21
5
  "error",
22
6
  {
23
7
  "rules": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
3
3
  "plugins": ["typescript", "import", "oxc", "unicorn"],
4
- "jsPlugins": ["eslint-plugin-perfectionist"],
4
+ "jsPlugins": ["eslint-plugin-perfectionist", "./plugins/codestyle.js"],
5
5
  "categories": {
6
6
  "correctness": "error",
7
7
  "suspicious": "error",
@@ -2,7 +2,6 @@
2
2
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
3
3
  "extends": ["./base.json"],
4
4
  "plugins": ["typescript", "import", "react"],
5
- "jsPlugins": ["./plugins/remove-ts-extensions.js"],
6
5
  "rules": {
7
6
  "typescript/no-require-imports": [
8
7
  "error",
@@ -10,13 +9,9 @@
10
9
  "allow": ["\\.png$", "\\.jpg$", "\\.jpeg$", "\\.gif$", "\\.webp$"]
11
10
  }
12
11
  ],
13
- "remove-ts-extensions/remove-ts-extensions": "error",
14
-
15
- // React 17+ doesn't require importing React
12
+ "codestyle/imports-without-ext": "error",
16
13
  "react/react-in-jsx-scope": "off",
17
- // Prop spreading is common in React Native
18
14
  "react/jsx-props-no-spreading": "off",
19
- // Style preferences - not worth enforcing
20
15
  "react/jsx-boolean-value": "off",
21
16
  "react/jsx-handler-names": "off",
22
17
  "react/jsx-curly-brace-presence": "off",
@@ -2,9 +2,8 @@
2
2
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
3
3
  "extends": ["./base.json"],
4
4
  "plugins": ["typescript", "import", "react", "nextjs"],
5
- "jsPlugins": ["./plugins/remove-ts-extensions.js"],
6
5
  "ignorePatterns": ["dist/**", "node_modules/**", ".next/**", "next-env.d.ts"],
7
6
  "rules": {
8
- "remove-ts-extensions/remove-ts-extensions": "error"
7
+ "codestyle/imports-without-ext": "error"
9
8
  }
10
9
  }
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
3
3
  "extends": ["./base.json"],
4
- "jsPlugins": ["./plugins/require-js-extensions.js"],
5
4
  "rules": {
6
- "require-js-extensions/require-js-extensions": "error"
5
+ "codestyle/imports-with-ext": "error"
7
6
  }
8
7
  }
@@ -0,0 +1,197 @@
1
+ // Codestyle plugin - custom rules for code quality
2
+ // Contains: architecture boundaries, import extensions
3
+
4
+ import hexagonalConfig from "../architectures/hexagonal.json" with { type: "json" };
5
+
6
+ // ============================================
7
+ // Hexagonal - Architecture boundary rules
8
+ // ============================================
9
+
10
+ function createHexagonalRule() {
11
+ const ruleConfig = hexagonalConfig.rules["codestyle/arch-hexagonal"];
12
+ const defaultRules = Array.isArray(ruleConfig) ? ruleConfig[1]?.rules || [] : [];
13
+
14
+ return {
15
+ meta: {
16
+ type: "problem",
17
+ docs: {
18
+ description: "Enforce hexagonal architecture layer boundaries",
19
+ category: "Best Practices",
20
+ },
21
+ schema: [
22
+ {
23
+ type: "object",
24
+ properties: {
25
+ rules: {
26
+ type: "array",
27
+ items: {
28
+ type: "object",
29
+ properties: {
30
+ from: { type: "string" },
31
+ disallow: { type: "array", items: { type: "string" } },
32
+ message: { type: "string" },
33
+ },
34
+ required: ["from", "disallow"],
35
+ },
36
+ },
37
+ },
38
+ },
39
+ ],
40
+ },
41
+ create(context) {
42
+ const options = context.options[0];
43
+ const rules = options && options.rules ? options.rules : defaultRules;
44
+ const filename = context.getFilename();
45
+
46
+ function checkNode(node) {
47
+ if (!node.source || !node.source.value) {
48
+ return;
49
+ }
50
+
51
+ const importPath = node.source.value;
52
+
53
+ for (const rule of rules) {
54
+ const fromPattern = new RegExp(rule.from);
55
+ if (!fromPattern.test(filename)) {
56
+ continue;
57
+ }
58
+
59
+ for (const disallowPattern of rule.disallow) {
60
+ if (new RegExp(disallowPattern).test(importPath)) {
61
+ context.report({
62
+ node: node.source,
63
+ message: rule.message || "Import violates architecture boundaries",
64
+ });
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ return {
72
+ ImportDeclaration: checkNode,
73
+ ExportNamedDeclaration: checkNode,
74
+ ExportAllDeclaration: checkNode,
75
+ };
76
+ },
77
+ };
78
+ }
79
+
80
+ // ============================================
81
+ // Imports-with-ext - Require .js extensions
82
+ // ============================================
83
+
84
+ const importsWithExtRule = {
85
+ meta: {
86
+ type: "problem",
87
+ docs: {
88
+ description: "Require .js extension in imports for Node.js ESM compatibility",
89
+ category: "Best Practices",
90
+ },
91
+ fixable: "code",
92
+ schema: [],
93
+ },
94
+ create(context) {
95
+ const hasExtension = /\.[a-zA-Z0-9]+$/;
96
+
97
+ function checkNode(node) {
98
+ if (!node.source || !node.source.value) {
99
+ return;
100
+ }
101
+
102
+ const importPath = node.source.value;
103
+
104
+ // Only check relative imports
105
+ if (!importPath.startsWith(".")) {
106
+ return;
107
+ }
108
+
109
+ // Skip if it already has an extension
110
+ if (hasExtension.test(importPath)) {
111
+ return;
112
+ }
113
+
114
+ // Skip type-only imports (they are erased at runtime)
115
+ if (node.importKind === "type") {
116
+ return;
117
+ }
118
+
119
+ context.report({
120
+ node: node.source,
121
+ message: "Missing .js extension in import (required for Node.js ESM)",
122
+ fix(fixer) {
123
+ const newPath = `${importPath}.js`;
124
+ return fixer.replaceText(node.source, `'${newPath}'`);
125
+ },
126
+ });
127
+ }
128
+
129
+ return {
130
+ ImportDeclaration: checkNode,
131
+ ExportNamedDeclaration: checkNode,
132
+ ExportAllDeclaration: checkNode,
133
+ };
134
+ },
135
+ };
136
+
137
+ // ============================================
138
+ // Imports-without-ext - Remove extensions
139
+ // ============================================
140
+
141
+ const importsWithoutExtRule = {
142
+ meta: {
143
+ type: "problem",
144
+ docs: {
145
+ description: "Remove .js, .jsx, .ts, .tsx extensions from imports",
146
+ category: "Best Practices",
147
+ },
148
+ fixable: "code",
149
+ schema: [],
150
+ },
151
+ create(context) {
152
+ const extensionsToRemove = /\.(js|jsx|ts|tsx)$/;
153
+
154
+ function checkNode(node) {
155
+ if (!node.source || !node.source.value) {
156
+ return;
157
+ }
158
+
159
+ const importPath = node.source.value;
160
+
161
+ // Check both relative imports and path alias imports
162
+ if (!importPath.startsWith(".") && !importPath.startsWith("@/")) {
163
+ return;
164
+ }
165
+
166
+ if (extensionsToRemove.test(importPath)) {
167
+ const match = importPath.match(extensionsToRemove);
168
+ context.report({
169
+ node: node.source,
170
+ message: `Remove "${match[0]}" extension from import`,
171
+ fix(fixer) {
172
+ const newPath = importPath.replace(extensionsToRemove, "");
173
+ return fixer.replaceText(node.source, `'${newPath}'`);
174
+ },
175
+ });
176
+ }
177
+ }
178
+
179
+ return {
180
+ ImportDeclaration: checkNode,
181
+ ExportNamedDeclaration: checkNode,
182
+ ExportAllDeclaration: checkNode,
183
+ };
184
+ },
185
+ };
186
+
187
+ export default {
188
+ meta: {
189
+ name: "codestyle",
190
+ version: "1.0.0",
191
+ },
192
+ rules: {
193
+ "arch-hexagonal": createHexagonalRule(),
194
+ "imports-with-ext": importsWithExtRule,
195
+ "imports-without-ext": importsWithoutExtRule,
196
+ },
197
+ };
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ function findProjectRoot() {
10
+ let dir = process.cwd();
11
+ if (dir.includes("node_modules")) {
12
+ dir = dir.split("node_modules")[0];
13
+ }
14
+ return dir;
15
+ }
16
+
17
+ // Rule mappings based on extended configs
18
+ const ruleMappings = [
19
+ {
20
+ pattern: "architectures/hexagonal",
21
+ rule: ["codestyle/arch-hexagonal", "error"],
22
+ },
23
+ {
24
+ pattern: "oxlint/node",
25
+ rule: ["codestyle/imports-with-ext", "error"],
26
+ },
27
+ {
28
+ pattern: "oxlint/expo",
29
+ rule: ["codestyle/imports-without-ext", "error"],
30
+ },
31
+ {
32
+ pattern: "oxlint/nextjs",
33
+ rule: ["codestyle/imports-without-ext", "error"],
34
+ },
35
+ ];
36
+
37
+ function run() {
38
+ const projectRoot = findProjectRoot();
39
+ const configPath = path.join(projectRoot, ".oxlintrc.json");
40
+
41
+ if (!fs.existsSync(configPath)) {
42
+ return;
43
+ }
44
+
45
+ let config;
46
+ try {
47
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
48
+ } catch {
49
+ console.log("[@jterrazz/codestyle] Could not parse .oxlintrc.json");
50
+ return;
51
+ }
52
+
53
+ const configStr = JSON.stringify(config);
54
+ const neededRules = new Map();
55
+ let needsPlugin = false;
56
+
57
+ for (const mapping of ruleMappings) {
58
+ if (configStr.includes(mapping.pattern)) {
59
+ neededRules.set(mapping.rule[0], mapping.rule[1]);
60
+ needsPlugin = true;
61
+ }
62
+ }
63
+
64
+ if (!needsPlugin) {
65
+ return;
66
+ }
67
+
68
+ let updated = false;
69
+ const updates = [];
70
+
71
+ // Add plugin
72
+ const pluginPath = "./node_modules/@jterrazz/codestyle/src/oxlint/plugins/codestyle.js";
73
+ config.jsPlugins = config.jsPlugins || [];
74
+ if (!config.jsPlugins.includes(pluginPath)) {
75
+ config.jsPlugins.push(pluginPath);
76
+ updated = true;
77
+ updates.push(" + jsPlugins: codestyle.js");
78
+ }
79
+
80
+ // Add rules
81
+ config.rules = config.rules || {};
82
+ for (const [ruleName, ruleLevel] of neededRules) {
83
+ if (!config.rules[ruleName]) {
84
+ config.rules[ruleName] = ruleLevel;
85
+ updated = true;
86
+ updates.push(` + rule: ${ruleName}`);
87
+ }
88
+ }
89
+
90
+ if (!updated) {
91
+ return;
92
+ }
93
+
94
+ fs.writeFileSync(
95
+ configPath,
96
+ `${JSON.stringify(config, null, 2)}
97
+ `,
98
+ );
99
+
100
+ console.log("[@jterrazz/codestyle] Updated .oxlintrc.json:");
101
+ for (const update of updates) {
102
+ console.log(update);
103
+ }
104
+ }
105
+
106
+ run();
@@ -1,86 +0,0 @@
1
- // Custom rule to enforce architecture boundaries
2
- // Prevents imports between layers (e.g., domain cannot import infra)
3
-
4
- const architectureBoundariesRule = {
5
- meta: {
6
- type: "problem",
7
- docs: {
8
- description: "Enforce architecture layer boundaries",
9
- category: "Best Practices",
10
- },
11
- schema: [
12
- {
13
- type: "object",
14
- properties: {
15
- rules: {
16
- type: "array",
17
- items: {
18
- type: "object",
19
- properties: {
20
- from: { type: "string" }, // Regex pattern for source file path
21
- disallow: {
22
- type: "array",
23
- items: { type: "string" }, // Regex patterns for disallowed imports
24
- },
25
- message: { type: "string" },
26
- },
27
- required: ["from", "disallow"],
28
- },
29
- },
30
- },
31
- },
32
- ],
33
- },
34
- create(context) {
35
- const options = context.options[0] || {};
36
- const rules = options.rules || [];
37
- const filename = context.getFilename();
38
-
39
- function checkNode(node) {
40
- if (!node.source || !node.source.value) {
41
- return;
42
- }
43
-
44
- const importPath = node.source.value;
45
-
46
- for (const rule of rules) {
47
- const fromPattern = new RegExp(rule.from);
48
-
49
- // Check if this file matches the "from" pattern
50
- if (!fromPattern.test(filename)) {
51
- continue;
52
- }
53
-
54
- // Check if the import matches any disallowed pattern
55
- for (const disallowPattern of rule.disallow) {
56
- const disallowRegex = new RegExp(disallowPattern);
57
-
58
- if (disallowRegex.test(importPath)) {
59
- context.report({
60
- node: node.source,
61
- message:
62
- rule.message || `Import from "${importPath}" violates architecture boundaries`,
63
- });
64
- break;
65
- }
66
- }
67
- }
68
- }
69
-
70
- return {
71
- ImportDeclaration: checkNode,
72
- ExportNamedDeclaration: checkNode,
73
- ExportAllDeclaration: checkNode,
74
- };
75
- },
76
- };
77
-
78
- export default {
79
- meta: {
80
- name: "architecture-boundaries",
81
- version: "1.0.0",
82
- },
83
- rules: {
84
- "architecture-boundaries": architectureBoundariesRule,
85
- },
86
- };
@@ -1,58 +0,0 @@
1
- // Custom rule to remove TS/JS extensions from imports
2
- // Compatible with both ESLint and Oxlint JS plugin API
3
-
4
- const removeTsExtensionsRule = {
5
- meta: {
6
- type: "problem",
7
- docs: {
8
- description: "Remove .js, .jsx, .ts, .tsx extensions from imports",
9
- category: "Best Practices",
10
- },
11
- fixable: "code",
12
- schema: [],
13
- },
14
- create(context) {
15
- const extensionsToRemove = /\.(js|jsx|ts|tsx)$/;
16
-
17
- function checkNode(node) {
18
- if (!node.source || !node.source.value) {
19
- return;
20
- }
21
-
22
- const importPath = node.source.value;
23
-
24
- // Check both relative imports (starting with . or ..) AND path alias imports (starting with @/)
25
- if (!importPath.startsWith(".") && !importPath.startsWith("@/")) {
26
- return;
27
- }
28
-
29
- if (extensionsToRemove.test(importPath)) {
30
- const match = importPath.match(extensionsToRemove);
31
- context.report({
32
- node: node.source,
33
- message: `Remove "${match[0]}" extension from import`,
34
- fix(fixer) {
35
- const newPath = importPath.replace(extensionsToRemove, "");
36
- return fixer.replaceText(node.source, `'${newPath}'`);
37
- },
38
- });
39
- }
40
- }
41
-
42
- return {
43
- ImportDeclaration: checkNode,
44
- ExportNamedDeclaration: checkNode,
45
- ExportAllDeclaration: checkNode,
46
- };
47
- },
48
- };
49
-
50
- export default {
51
- meta: {
52
- name: "remove-ts-extensions",
53
- version: "1.0.0",
54
- },
55
- rules: {
56
- "remove-ts-extensions": removeTsExtensionsRule,
57
- },
58
- };
@@ -1,65 +0,0 @@
1
- // Custom rule to require .js extensions in imports (for Node.js ESM)
2
- // Compatible with both ESLint and Oxlint JS plugin API
3
-
4
- const requireJsExtensionsRule = {
5
- meta: {
6
- type: "problem",
7
- docs: {
8
- description: "Require .js extension in imports for Node.js ESM compatibility",
9
- category: "Best Practices",
10
- },
11
- fixable: "code",
12
- schema: [],
13
- },
14
- create(context) {
15
- const hasExtension = /\.[a-zA-Z0-9]+$/;
16
-
17
- function checkNode(node) {
18
- if (!node.source || !node.source.value) {
19
- return;
20
- }
21
-
22
- const importPath = node.source.value;
23
-
24
- // Only check relative imports
25
- if (!importPath.startsWith(".")) {
26
- return;
27
- }
28
-
29
- // Skip if it already has an extension
30
- if (hasExtension.test(importPath)) {
31
- return;
32
- }
33
-
34
- // Skip type-only imports (they are erased at runtime)
35
- if (node.importKind === "type") {
36
- return;
37
- }
38
-
39
- context.report({
40
- node: node.source,
41
- message: "Missing .js extension in import (required for Node.js ESM)",
42
- fix(fixer) {
43
- const newPath = `${importPath}.js`;
44
- return fixer.replaceText(node.source, `'${newPath}'`);
45
- },
46
- });
47
- }
48
-
49
- return {
50
- ImportDeclaration: checkNode,
51
- ExportNamedDeclaration: checkNode,
52
- ExportAllDeclaration: checkNode,
53
- };
54
- },
55
- };
56
-
57
- export default {
58
- meta: {
59
- name: "require-js-extensions",
60
- version: "1.0.0",
61
- },
62
- rules: {
63
- "require-js-extensions": requireJsExtensionsRule,
64
- },
65
- };