vite-plugin-lint-overlay 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 +55 -0
- package/package.json +30 -0
- package/src/client-overlay.js +137 -0
- package/src/lint-overlay.js +116 -0
- package/src/lint-worker.js +108 -0
- package/src/ts-worker.js +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# vite-plugin-lint-overlay
|
|
2
|
+
|
|
3
|
+
A Vite dev-server overlay that shows **TypeScript** + **ESLint** problems in a single UI. 😊
|
|
4
|
+
|
|
5
|
+
- ✅ TypeScript diagnostics (optional, watch mode)
|
|
6
|
+
- ✅ ESLint diagnostics (incremental, cached)
|
|
7
|
+
- ✅ Runs on **dev start**, **file events**, and **browser reload**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -D vite-plugin-lint-overlay
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Peer dependencies:** `vite`, `eslint`, `typescript`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
`vite.config.js` / `vite.config.ts`
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { defineConfig } from 'vite';
|
|
27
|
+
import react from '@vitejs/plugin-react';
|
|
28
|
+
import lintOverlay from 'vite-plugin-lint-overlay';
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [
|
|
32
|
+
react(),
|
|
33
|
+
lintOverlay({
|
|
34
|
+
rootDir: 'src',
|
|
35
|
+
ts: true // Enable TypeScript checks
|
|
36
|
+
})
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
| Option | Type | Default | Description |
|
|
46
|
+
| :--- | :--- | :--- | :--- |
|
|
47
|
+
| **rootDir** | `string` | `'src'` | Root directory to watch and lint for ESLint issues. |
|
|
48
|
+
| **ts** | `boolean` | `false` | Set to `true` to enable the TypeScript compiler check. |
|
|
49
|
+
| **tsconfigPath** | `string` | `''` | Path to `tsconfig`. Defaults to `tsconfig.app.json` or `tsconfig.json`. |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-lint-overlay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Smart dev-server overlay for ESLint and TypeScript errors.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Max Matinpalo",
|
|
7
|
+
"homepage": "https://github.com/max-matinpalo/vite-lint-overlay#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/max-matinpalo/vite-lint-overlay.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/max-matinpalo/vite-lint-overlay/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./src/lint-overlay.js",
|
|
17
|
+
"module": "./src/lint-overlay.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/lint-overlay.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"eslint": "^8.0.0 || ^9.0.0",
|
|
27
|
+
"typescript": "^5.0.0",
|
|
28
|
+
"vite": "^4.0.0 || ^5.0.0 || ^6.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const STYLES = `
|
|
2
|
+
:host {
|
|
3
|
+
position: fixed; top: 0; left: 0; z-index: 99999;
|
|
4
|
+
width: 100%; height: 100%;
|
|
5
|
+
pointer-events: none; display: none;
|
|
6
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
|
7
|
+
}
|
|
8
|
+
:host([visible]) {
|
|
9
|
+
display: flex; pointer-events: auto;
|
|
10
|
+
background: #00000099; backdrop-filter: blur(4px);
|
|
11
|
+
justify-content: center; align-items: center; padding: 20px;
|
|
12
|
+
}
|
|
13
|
+
.container {
|
|
14
|
+
background: #1e1e1e; color: #d4d4d4;
|
|
15
|
+
width: 100%; max-width: 900px; max-height: 800px;
|
|
16
|
+
border-radius: 8px; border: 1px solid #333333;
|
|
17
|
+
display: flex; flex-direction: column;
|
|
18
|
+
box-shadow: 0 20px 50px #00000080; overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
.header {
|
|
21
|
+
background: #252526; padding: 12px 16px;
|
|
22
|
+
border-bottom: 1px solid #333333;
|
|
23
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
24
|
+
flex-shrink: 0;
|
|
25
|
+
}
|
|
26
|
+
.title { font-weight: bold; font-size: 14px; color: #ffffff; }
|
|
27
|
+
.close {
|
|
28
|
+
cursor: pointer; background: transparent; border: none; color: #999999;
|
|
29
|
+
font-size: 20px; line-height: 1; padding: 0;
|
|
30
|
+
}
|
|
31
|
+
.close:hover { color: #ffffff; }
|
|
32
|
+
.body { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
|
33
|
+
h2 {
|
|
34
|
+
margin: 0; padding: 10px 16px; font-size: 13px; font-weight: bold;
|
|
35
|
+
background: #252526; border-bottom: 1px solid #333333;
|
|
36
|
+
position: sticky; top: 0; z-index: 10;
|
|
37
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
38
|
+
}
|
|
39
|
+
h2.red { color: #f44336; }
|
|
40
|
+
h2.orange { color: #ff9800; border-top: 1px solid #333333; }
|
|
41
|
+
.list { list-style: none; margin: 0; padding: 0; }
|
|
42
|
+
.item {
|
|
43
|
+
padding: 12px 16px; border-bottom: 1px solid #2d2d2d;
|
|
44
|
+
border-left: 3px solid transparent;
|
|
45
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
46
|
+
}
|
|
47
|
+
.item.error { border-left-color: #f44336; background: #f443360d; }
|
|
48
|
+
.item.warning { border-left-color: #ff9800; background: #ff98000d; }
|
|
49
|
+
.meta {
|
|
50
|
+
font-size: 12px; font-weight: bold; color: #888888; display: flex; align-items: center;
|
|
51
|
+
gap: 8px; font-family: Menlo, monospace;
|
|
52
|
+
}
|
|
53
|
+
.file { color: #aaaaaa; word-break: break-all; }
|
|
54
|
+
.badge {
|
|
55
|
+
padding: 3px 6px; border-radius: 4px; font-size: 10px; font-weight: 700;
|
|
56
|
+
text-transform: uppercase; letter-spacing: 0.5px; color: #ffffff; background: #444444;
|
|
57
|
+
}
|
|
58
|
+
.badge.ts { background: #3178c6; color: #ffffff; }
|
|
59
|
+
.badge.eslint { background: #f7b93e; color: #000000; }
|
|
60
|
+
.msg {
|
|
61
|
+
white-space: pre-wrap; word-break: break-word; font-size: 13px;
|
|
62
|
+
line-height: 1.5; color: #cccccc; font-family: Menlo, monospace;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SmartErrorOverlay extends HTMLElement {
|
|
68
|
+
constructor() {
|
|
69
|
+
super();
|
|
70
|
+
this.attachShadow({ mode: 'open' });
|
|
71
|
+
this.root = this.shadowRoot;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
connectedCallback() {
|
|
75
|
+
this.root.innerHTML = `<style>${STYLES}</style><div id="mount"></div>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setErrors(errors) {
|
|
79
|
+
if (!this.root.getElementById('mount')) this.connectedCallback();
|
|
80
|
+
if (!errors || !errors.length) return this.removeAttribute('visible');
|
|
81
|
+
this.setAttribute('visible', '');
|
|
82
|
+
const errs = errors.filter(e => e.severity === 'error');
|
|
83
|
+
const warns = errors.filter(e => e.severity === 'warning');
|
|
84
|
+
const renderList = (list) => list.map(e => {
|
|
85
|
+
const type = (e.source || 'unk').toLowerCase();
|
|
86
|
+
const cleanFile = (e.file || 'Global').split(/\/|\\/).slice(-3).join('/');
|
|
87
|
+
const hasPath = /[\\/]/.test(e.file || '');
|
|
88
|
+
const fileDisplay = hasPath && cleanFile !== e.file
|
|
89
|
+
? `.../${cleanFile}` : (e.file || 'Global');
|
|
90
|
+
return `
|
|
91
|
+
<li class="item ${e.severity || 'error'}">
|
|
92
|
+
<div class="meta">
|
|
93
|
+
<span class="badge ${type}">${e.source || 'UNK'}</span>
|
|
94
|
+
<span class="file" title="${this.escape(e.file)}">
|
|
95
|
+
${this.escape(fileDisplay)}${e.line ? ':' + e.line : ''}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="msg">${this.escape(e.message)}</div>
|
|
99
|
+
</li>`;
|
|
100
|
+
}).join('');
|
|
101
|
+
this.root.getElementById('mount').innerHTML = `
|
|
102
|
+
<div class="container">
|
|
103
|
+
<div class="header"><span class="title">Dev Server Issues</span>
|
|
104
|
+
<button id="close" class="close">×</button></div>
|
|
105
|
+
<div class="body">
|
|
106
|
+
${errs.length ? `<h2 class="red">Errors (${errs.length})</h2>
|
|
107
|
+
<ul class="list">${renderList(errs)}</ul>` : ''}
|
|
108
|
+
${warns.length ? `<h2 class="orange">Warnings (${warns.length})</h2>
|
|
109
|
+
<ul class="list">${renderList(warns)}</ul>` : ''}
|
|
110
|
+
</div>
|
|
111
|
+
</div>`;
|
|
112
|
+
this.root.getElementById('close').onclick = () => this.removeAttribute('visible');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
escape(str) {
|
|
116
|
+
return String(str || '').replace(/[&<>"']/g, (m) => ({
|
|
117
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
118
|
+
})[m]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if (!customElements.get('smart-error-overlay')) {
|
|
124
|
+
customElements.define('smart-error-overlay', SmartErrorOverlay);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if (import.meta.hot) {
|
|
129
|
+
let overlay = document.querySelector('smart-error-overlay');
|
|
130
|
+
if (!overlay) {
|
|
131
|
+
overlay = document.createElement('smart-error-overlay');
|
|
132
|
+
document.body.appendChild(overlay);
|
|
133
|
+
}
|
|
134
|
+
import.meta.hot.on('smart-overlay:update', (data) => {
|
|
135
|
+
if (overlay) overlay.setErrors(data?.errors || []);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export default function lintOverlay(props = {}) {
|
|
8
|
+
const { rootDir: rawRoot = 'src' } = props;
|
|
9
|
+
const { tsconfigPath = '' } = props;
|
|
10
|
+
const { ts = false } = props;
|
|
11
|
+
const rootDir = String(rawRoot).trim().replace(/\/+$/, '') || 'src';
|
|
12
|
+
const virtualId = 'virtual:lint-overlay-client.js';
|
|
13
|
+
const resolvedVirtualId = '\0' + virtualId;
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
let tsErrors = [];
|
|
17
|
+
let lintErrors = [];
|
|
18
|
+
let serverWs = null;
|
|
19
|
+
|
|
20
|
+
const updateOverlay = () => {
|
|
21
|
+
if (!serverWs) return;
|
|
22
|
+
serverWs.send({
|
|
23
|
+
type: 'custom',
|
|
24
|
+
event: 'smart-overlay:update',
|
|
25
|
+
data: { errors: [...tsErrors, ...lintErrors] }
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: 'vite-plugin-lint-overlay',
|
|
31
|
+
apply: 'serve',
|
|
32
|
+
|
|
33
|
+
resolveId(id) {
|
|
34
|
+
if (id === virtualId || id === '/' + virtualId) return resolvedVirtualId;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
load(id) {
|
|
38
|
+
if (id === resolvedVirtualId) {
|
|
39
|
+
return fs.readFileSync(path.join(__dirname, 'client-overlay.js'), 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
configureServer(server) {
|
|
44
|
+
serverWs = server.ws;
|
|
45
|
+
const lintWorker = new Worker(
|
|
46
|
+
new URL('./lint-worker.js', import.meta.url),
|
|
47
|
+
{ type: 'module', workerData: { rootDir } }
|
|
48
|
+
);
|
|
49
|
+
const handleCrash = (source, msg) => {
|
|
50
|
+
const err = { file: 'Global', message: msg, source, severity: 'error' };
|
|
51
|
+
if (source === 'TS') tsErrors = [err];
|
|
52
|
+
if (source === 'ESLint') lintErrors = [err];
|
|
53
|
+
updateOverlay();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (ts) {
|
|
57
|
+
const tsWorker = new Worker(
|
|
58
|
+
new URL('./ts-worker.js', import.meta.url),
|
|
59
|
+
{ type: 'module', workerData: { tsconfigPath } }
|
|
60
|
+
);
|
|
61
|
+
tsWorker.on('error', (e) => handleCrash('TS', `TS Worker error: ${e.message}`));
|
|
62
|
+
tsWorker.on('exit', (c) => c !== 0 && handleCrash('TS', `TS Worker died (code ${c})`));
|
|
63
|
+
tsWorker.on('message', (msg) => {
|
|
64
|
+
if (msg.type !== 'SNAPSHOT') return;
|
|
65
|
+
tsErrors = msg.errors;
|
|
66
|
+
updateOverlay();
|
|
67
|
+
});
|
|
68
|
+
server.httpServer?.on('close', () => tsWorker.terminate());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lintWorker.on('error', (e) => handleCrash('ESLint', `Lint Worker error: ${e.message}`));
|
|
72
|
+
lintWorker.on('exit', (c) => c !== 0 && handleCrash('ESLint', `Lint Worker died (code ${c})`));
|
|
73
|
+
lintWorker.on('message', (msg) => {
|
|
74
|
+
if (msg.type !== 'SNAPSHOT') return;
|
|
75
|
+
lintErrors = msg.errors;
|
|
76
|
+
updateOverlay();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
server.httpServer?.on('close', () => lintWorker.terminate());
|
|
80
|
+
const isTarget = (f) => /\.(js|jsx|ts|tsx)$/.test(f);
|
|
81
|
+
const isInRoot = (f) => {
|
|
82
|
+
const rel = path
|
|
83
|
+
.relative(server.config.root || process.cwd(), f)
|
|
84
|
+
.replace(/\\/g, '/');
|
|
85
|
+
return rel === rootDir || rel.startsWith(rootDir + '/');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const lintFile = (f) => {
|
|
89
|
+
if (!isTarget(f)) return;
|
|
90
|
+
if (!isInRoot(f)) return;
|
|
91
|
+
lintWorker.postMessage({ type: 'LINT', files: [f] });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
lintWorker.postMessage({ type: 'LINT_ALL' });
|
|
95
|
+
server.ws.on('connection', () => {
|
|
96
|
+
updateOverlay();
|
|
97
|
+
lintWorker.postMessage({ type: 'LINT_ALL' });
|
|
98
|
+
});
|
|
99
|
+
server.watcher.on('add', lintFile);
|
|
100
|
+
server.watcher.on('change', lintFile);
|
|
101
|
+
server.watcher.on('unlink', (f) => {
|
|
102
|
+
if (!isTarget(f)) return;
|
|
103
|
+
if (!isInRoot(f)) return;
|
|
104
|
+
lintWorker.postMessage({ type: 'UNLINK', f });
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
transformIndexHtml() {
|
|
109
|
+
return [{
|
|
110
|
+
tag: 'script',
|
|
111
|
+
attrs: { type: 'module', src: `/${virtualId}` },
|
|
112
|
+
injectTo: 'body-prepend'
|
|
113
|
+
}];
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { ESLint } from 'eslint';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
if (!parentPort) throw new Error('No parentPort');
|
|
6
|
+
|
|
7
|
+
let rootDir = workerData?.rootDir || 'src';
|
|
8
|
+
rootDir = String(rootDir).trim().replace(/\/+$/, '');
|
|
9
|
+
if (!rootDir) rootDir = 'src';
|
|
10
|
+
|
|
11
|
+
const ALL = [`${rootDir}/**/*.{js,jsx,ts,tsx}`];
|
|
12
|
+
const cache = new Map();
|
|
13
|
+
const CWD = process.cwd();
|
|
14
|
+
const rel = (p) => path.relative(CWD, p).replace(/\\/g, '/');
|
|
15
|
+
|
|
16
|
+
let eslint = null;
|
|
17
|
+
let running = false;
|
|
18
|
+
let pending = null;
|
|
19
|
+
|
|
20
|
+
const snap = (errors) => parentPort.postMessage({ type: 'SNAPSHOT', errors });
|
|
21
|
+
|
|
22
|
+
function errText(e) {
|
|
23
|
+
if (!e) return 'Unknown error';
|
|
24
|
+
if (typeof e === 'string') return e;
|
|
25
|
+
const msg = e.message ? String(e.message) : String(e);
|
|
26
|
+
const stack = e.stack
|
|
27
|
+
? String(e.stack).split('\n').slice(0, 2).join('\n')
|
|
28
|
+
: '';
|
|
29
|
+
return stack ? `${msg}\n${stack}` : msg;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function globalError(message) {
|
|
33
|
+
return [{
|
|
34
|
+
file: 'Global',
|
|
35
|
+
line: 0,
|
|
36
|
+
source: 'ESLint',
|
|
37
|
+
severity: 'error',
|
|
38
|
+
message
|
|
39
|
+
}];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function unlinkFile(f) {
|
|
43
|
+
if (!f) return;
|
|
44
|
+
cache.delete(rel(f));
|
|
45
|
+
snap([...cache.values()].flat());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function run() {
|
|
49
|
+
while (pending) {
|
|
50
|
+
const job = pending;
|
|
51
|
+
pending = null;
|
|
52
|
+
|
|
53
|
+
if (!eslint) {
|
|
54
|
+
try {
|
|
55
|
+
eslint = new ESLint();
|
|
56
|
+
} catch (e) {
|
|
57
|
+
snap(globalError(`Init failed: ${errText(e)}`));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (job.reset) cache.clear();
|
|
64
|
+
|
|
65
|
+
const res = await eslint.lintFiles(job.files);
|
|
66
|
+
|
|
67
|
+
res.forEach((r) => {
|
|
68
|
+
const msgs = r.messages.filter((m) => m.severity > 0);
|
|
69
|
+
const key = rel(r.filePath);
|
|
70
|
+
if (!msgs.length) return cache.delete(key);
|
|
71
|
+
|
|
72
|
+
cache.set(key, msgs.map((m) => ({
|
|
73
|
+
file: key,
|
|
74
|
+
line: m.line || 0,
|
|
75
|
+
message: m.message,
|
|
76
|
+
source: 'ESLint',
|
|
77
|
+
severity: m.severity === 2 ? 'error' : 'warning'
|
|
78
|
+
})));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
snap([...cache.values()].flat());
|
|
82
|
+
} catch (e) {
|
|
83
|
+
snap(globalError(`Lint crashed: ${errText(e)}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
running = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
parentPort.on('message', (msg) => {
|
|
91
|
+
if (msg?.type === 'UNLINK') {
|
|
92
|
+
unlinkFile(msg.f);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const global = msg?.type === 'LINT_ALL';
|
|
97
|
+
if (pending?.reset && !global) return;
|
|
98
|
+
|
|
99
|
+
pending = {
|
|
100
|
+
files: global ? ALL : (Array.isArray(msg.files) ? msg.files : []),
|
|
101
|
+
reset: global
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (!running) {
|
|
105
|
+
running = true;
|
|
106
|
+
run();
|
|
107
|
+
}
|
|
108
|
+
});
|
package/src/ts-worker.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
if (!parentPort) throw new Error('No parentPort');
|
|
7
|
+
|
|
8
|
+
const ROOT = process.cwd();
|
|
9
|
+
|
|
10
|
+
let tsconfigPath = workerData?.tsconfigPath || '';
|
|
11
|
+
tsconfigPath = String(tsconfigPath).trim();
|
|
12
|
+
|
|
13
|
+
function postGlobalError(message) {
|
|
14
|
+
parentPort.postMessage({
|
|
15
|
+
type: 'SNAPSHOT',
|
|
16
|
+
errors: [{
|
|
17
|
+
file: 'Global',
|
|
18
|
+
line: 0,
|
|
19
|
+
source: 'TS',
|
|
20
|
+
severity: 'error',
|
|
21
|
+
message
|
|
22
|
+
}]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveConfigPath() {
|
|
27
|
+
if (tsconfigPath) {
|
|
28
|
+
const abs = path.isAbsolute(tsconfigPath)
|
|
29
|
+
? tsconfigPath
|
|
30
|
+
: path.join(ROOT, tsconfigPath);
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(abs)) return abs;
|
|
33
|
+
|
|
34
|
+
const found = ts.findConfigFile(ROOT, ts.sys.fileExists, tsconfigPath);
|
|
35
|
+
if (found) return found;
|
|
36
|
+
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fallback = fs.existsSync(path.join(ROOT, 'tsconfig.app.json'))
|
|
41
|
+
? 'tsconfig.app.json'
|
|
42
|
+
: 'tsconfig.json';
|
|
43
|
+
|
|
44
|
+
return ts.findConfigFile(ROOT, ts.sys.fileExists, fallback) || '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const configPath = resolveConfigPath();
|
|
48
|
+
|
|
49
|
+
if (!configPath) {
|
|
50
|
+
postGlobalError(
|
|
51
|
+
tsconfigPath
|
|
52
|
+
? `tsconfig not found: ${tsconfigPath}`
|
|
53
|
+
: 'tsconfig not found: tsconfig.app.json or tsconfig.json'
|
|
54
|
+
);
|
|
55
|
+
parentPort.close();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const host = ts.createWatchCompilerHost(
|
|
60
|
+
configPath,
|
|
61
|
+
{ noEmit: true },
|
|
62
|
+
ts.sys,
|
|
63
|
+
ts.createSemanticDiagnosticsBuilderProgram,
|
|
64
|
+
() => { },
|
|
65
|
+
() => { }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
host.afterProgramCreate = (builder) => {
|
|
69
|
+
const prog = builder.getProgram();
|
|
70
|
+
|
|
71
|
+
const errors = ts.getPreEmitDiagnostics(prog)
|
|
72
|
+
.filter((d) =>
|
|
73
|
+
d.category === ts.DiagnosticCategory.Error ||
|
|
74
|
+
d.category === ts.DiagnosticCategory.Warning
|
|
75
|
+
)
|
|
76
|
+
.map((d) => {
|
|
77
|
+
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
|
|
78
|
+
const severity = d.category === ts.DiagnosticCategory.Error
|
|
79
|
+
? 'error'
|
|
80
|
+
: 'warning';
|
|
81
|
+
|
|
82
|
+
if (!d.file) {
|
|
83
|
+
return { file: 'Global', line: 0, message, source: 'TS', severity };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { line } = d.file.getLineAndCharacterOfPosition(d.start ?? 0);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
file: path.relative(ROOT, d.file.fileName).replace(/\\/g, '/'),
|
|
90
|
+
line: line + 1,
|
|
91
|
+
message,
|
|
92
|
+
source: 'TS',
|
|
93
|
+
severity
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
parentPort.postMessage({ type: 'SNAPSHOT', errors });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
ts.createWatchProgram(host);
|