@round-core/lint 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/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@round-core/lint",
3
+ "version": "0.1.0",
4
+ "description": "ESLint processor for RoundJS files",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "peerDependencies": {
11
+ "eslint": "^8.0.0 || ^9.0.0"
12
+ },
13
+ "dependencies": {
14
+ "@round-core/shared": "latest"
15
+ }
16
+ }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import processor from './processor.js';
2
+
3
+ export default {
4
+ processors: {
5
+ '.round': processor
6
+ }
7
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Computes line start offsets for a given source string.
3
+ * @param {string} source
4
+ * @returns {number[]} Array of offsets where each line starts.
5
+ */
6
+ function computeLineStarts(source) {
7
+ const lineStarts = [0];
8
+ for (let i = 0; i < source.length; i++) {
9
+ if (source[i] === '\n') {
10
+ lineStarts.push(i + 1);
11
+ }
12
+ }
13
+ return lineStarts;
14
+ }
15
+
16
+ /**
17
+ * Converts a 0-based offset to line and column (1-based).
18
+ * @param {number} offset
19
+ * @param {number[]} lineStarts
20
+ * @returns {{line: number, column: number}}
21
+ */
22
+ function offsetToPos(offset, lineStarts) {
23
+ let low = 0;
24
+ let high = lineStarts.length - 1;
25
+
26
+ while (low <= high) {
27
+ const mid = (low + high) >>> 1;
28
+ if (lineStarts[mid] <= offset) {
29
+ low = mid + 1;
30
+ } else {
31
+ high = mid - 1;
32
+ }
33
+ }
34
+
35
+ const lineIndex = high; // 0-based
36
+ const lineStart = lineStarts[lineIndex];
37
+
38
+ return {
39
+ line: lineIndex + 1,
40
+ column: offset - lineStart + 1
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Remaps ESLint messages to original source positions.
46
+ * @param {any[]} messages
47
+ * @param {{source: string, mapper: any}} state
48
+ * @returns {any[]}
49
+ */
50
+ export function remapMessages(messages, state) {
51
+ const { source, mapper } = state;
52
+ const lineStarts = computeLineStarts(source);
53
+
54
+ return messages.map(msg => {
55
+ if (!msg.range && !msg.line) return msg;
56
+
57
+ let startOffset, endOffset;
58
+
59
+ if (msg.range) {
60
+ startOffset = msg.range[0];
61
+ endOffset = msg.range[1];
62
+ } else {
63
+ return msg;
64
+ }
65
+
66
+ // Remap offsets using SourceMapper
67
+ const originalStart = mapper.remap(startOffset);
68
+ const originalEnd = mapper.remap(endOffset);
69
+
70
+ // Calculate new line/column
71
+ const startPos = offsetToPos(originalStart, lineStarts);
72
+ const endPos = offsetToPos(originalEnd, lineStarts);
73
+
74
+ // Clone and update message
75
+ return {
76
+ ...msg,
77
+ line: startPos.line,
78
+ column: startPos.column,
79
+ endLine: endPos.line,
80
+ endColumn: endPos.column,
81
+ range: [originalStart, originalEnd],
82
+ fix: undefined
83
+ };
84
+ });
85
+ }
@@ -0,0 +1,65 @@
1
+ import { SourceMapper, runPreprocess } from '@round-core/shared';
2
+ import { fileState } from './state.js';
3
+ import { remapMessages } from './offset-map.js';
4
+
5
+ export default {
6
+ preprocess(code, filename) {
7
+ const mapper = new SourceMapper();
8
+ runPreprocess(code, mapper, 0);
9
+
10
+ // MAGIC: Suppress false positives for RoundJS syntax without react-plugin
11
+
12
+ // 1. Identify top-level names (imports and declarations)
13
+ const topLevelNames = new Set();
14
+ const declMatches = mapper.code.matchAll(/^\s*(?:export\s+)?(?:function|const|let|var|class|import)\s+({[\s\S]*?}|[a-zA-Z0-9_$]+)/gm);
15
+ for (const m of declMatches) {
16
+ const inner = m[1];
17
+ if (inner.startsWith('{')) {
18
+ const innerNames = inner.match(/([a-zA-Z0-9_$]+)/g);
19
+ if (innerNames) innerNames.forEach(n => topLevelNames.add(n));
20
+ } else {
21
+ topLevelNames.add(inner);
22
+ }
23
+ }
24
+
25
+ // 2. Mark components used in JSX as "used"
26
+ const cleanForComponents = mapper.code.replace(/`[\s\S]*?`/g, '');
27
+ const componentMatches = cleanForComponents.matchAll(/<([A-Z][a-zA-Z0-9_$]*)/g);
28
+ const components = new Set([...componentMatches].map(m => m[1]));
29
+
30
+ let magic = '\n\n/* eslint-disable */\n/* globals RoundControlFlow, e, item */\n;';
31
+
32
+ // Usage for top-level components only
33
+ for (const comp of components) {
34
+ if (topLevelNames.has(comp)) {
35
+ magic += `${comp};`;
36
+ }
37
+ }
38
+
39
+ mapper.add(magic, code.length);
40
+
41
+ fileState.set(filename, {
42
+ source: code,
43
+ mapper: mapper
44
+ });
45
+
46
+ return [mapper.code];
47
+ },
48
+
49
+ postprocess(messages, filename) {
50
+ const state = fileState.get(filename);
51
+
52
+ // Clean up state (optional, but good for memory if ESLint is long running)
53
+ // However, ESLint might run multiple fixes?
54
+ // For now we keep it simple. If we delete here, we might lose it if
55
+ // specific flows access it again. But usually postprocess is the end.
56
+ fileState.delete(filename);
57
+
58
+ if (!state) return messages[0];
59
+
60
+ // Remap messages from the first (and only) part
61
+ return remapMessages(messages[0], state);
62
+ },
63
+
64
+ supportsAutofix: false
65
+ };
package/src/state.js ADDED
@@ -0,0 +1,14 @@
1
+
2
+ const map = new Map();
3
+
4
+ export const fileState = {
5
+ set(file, state) {
6
+ map.set(file, state);
7
+ },
8
+ get(file) {
9
+ return map.get(file);
10
+ },
11
+ delete(file) {
12
+ map.delete(file);
13
+ }
14
+ };