@kitajs/ts-html-plugin 4.1.4 → 5.0.0-next.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitajs/ts-html-plugin",
3
- "version": "4.1.4",
3
+ "version": "5.0.0-next.0",
4
4
  "homepage": "https://github.com/kitajs/html/tree/master/packages/ts-html-plugin#readme",
5
5
  "bugs": "https://github.com/kitajs/html/issues",
6
6
  "repository": {
@@ -16,30 +16,38 @@
16
16
  "main": "dist/index.js",
17
17
  "types": "dist/index.d.ts",
18
18
  "bin": {
19
- "ts-html-plugin": "dist/cli.js",
20
- "xss-scan": "dist/cli.js"
19
+ "ts-html-plugin": "bin/index.js",
20
+ "xss-scan": "bin/index.js"
21
21
  },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "!dist/*.tsbuildinfo"
26
+ ],
22
27
  "dependencies": {
23
28
  "chalk": "^5.6.2",
24
29
  "tslib": "^2.8.1",
25
30
  "yargs": "^18.0.0"
26
31
  },
27
32
  "devDependencies": {
28
- "@swc-node/register": "^1.11.1",
29
- "@swc/helpers": "^0.5.17",
30
- "@types/node": "^24.5.0",
31
- "@types/yargs": "^17.0.33",
33
+ "@types/node": "^24.10.13",
34
+ "@types/yargs": "^17.0.35",
35
+ "@typescript/native-preview": "^7.0.0-dev.20260122.2",
36
+ "@vitest/coverage-v8": "^4.0.18",
32
37
  "fast-defer": "^1.1.9",
33
- "self": "file:"
38
+ "self-ts-plugin": "link:",
39
+ "typescript": "^5.9.3",
40
+ "vitest": "^4.0.18",
41
+ "@kitajs/html": "^5.0.0-next.0"
34
42
  },
35
43
  "peerDependencies": {
36
- "@kitajs/html": "^4.2.10",
37
- "typescript": "^5.9.3"
44
+ "typescript": "^5.9.3",
45
+ "@kitajs/html": "^5.0.0-next.0"
38
46
  },
39
47
  "scripts": {
40
- "build": "tsc -p tsconfig.build.json",
41
- "dev": "tsc -p tsconfig.build.json --watch",
42
- "test": "pnpm build && pnpm install && pnpm test-exec",
43
- "test-exec": "node --require @swc-node/register --test test/**/*.test.ts"
48
+ "build": "tsgo -p tsconfig.build.json",
49
+ "pretest": "pnpm run build",
50
+ "test": "vitest --coverage --typecheck --run",
51
+ "test-types": "tsgo --noEmit"
44
52
  }
45
53
  }
package/src/cli.ts ADDED
@@ -0,0 +1,229 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import ts from 'typescript';
5
+ import yargs from 'yargs';
6
+ import { hideBin } from 'yargs/helpers';
7
+ import { recursiveDiagnoseJsxElements } from './util';
8
+
9
+ const { version } = require('../package.json');
10
+
11
+ const help = `
12
+
13
+ @kitajs/ts-html-plugin v${version} - A CLI tool & TypeScript LSP for finding XSS vulnerabilities in your TypeScript code.
14
+
15
+ Usage: xss-scan [options] <file> <file>...
16
+ ts-html-plugin [options] <file> <file>...
17
+
18
+ Options:
19
+ --cwd <path> The current working directory to use (defaults to process.cwd())
20
+ -p, --project <path> The path to the tsconfig.json file to use (defaults to 'tsconfig.json')
21
+ -s, --simplified Use simplified diagnostics
22
+ -h, --help Show this help message
23
+ --version Show the version number
24
+ <file> <file>... The files to check (defaults to all files in tsconfig.json)
25
+
26
+ Examples:
27
+ $ xss-scan
28
+ $ xss-scan --cwd src
29
+ $ xss-scan --project tsconfig.build.json
30
+ $ xss-scan src/index.tsx src/App.tsx
31
+
32
+ Exit codes:
33
+ 0 - No XSS vulnerabilities were found
34
+ 1 - XSS vulnerabilities were found
35
+ 2 - Only warnings were found
36
+
37
+ `.trim();
38
+
39
+ function readCompilerOptions(tsconfigPath: string) {
40
+ const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
41
+
42
+ if (error) {
43
+ return { errors: [error] };
44
+ }
45
+
46
+ const { options, errors, fileNames } = ts.parseJsonConfigFileContent(
47
+ config,
48
+ ts.sys,
49
+ path.dirname(tsconfigPath),
50
+ undefined,
51
+ tsconfigPath
52
+ );
53
+
54
+ if (errors.length) {
55
+ return { errors };
56
+ }
57
+
58
+ return { options, fileNames, errors: undefined };
59
+ }
60
+
61
+ function prettyPrintErrorCount(diagnostics: ts.Diagnostic[], root: string) {
62
+ const files = new Map<string, number>();
63
+
64
+ // Counts the amount of errors per file
65
+ for (const diagnostic of diagnostics) {
66
+ if (!diagnostic.file) {
67
+ continue;
68
+ }
69
+
70
+ const file = files.get(diagnostic.file.fileName);
71
+
72
+ if (file !== undefined) {
73
+ files.set(diagnostic.file.fileName, file + 1);
74
+ continue;
75
+ }
76
+
77
+ files.set(diagnostic.file.fileName, 1);
78
+ }
79
+
80
+ if (files.size > 1) {
81
+ console.error(
82
+ chalk.red(`Found a total of ${diagnostics.length} errors in ${files.size} files\n`)
83
+ );
84
+ }
85
+
86
+ for (const [file, amount] of files.entries()) {
87
+ console.error(
88
+ chalk.red(
89
+ `Found ${amount} error${amount === 1 ? '' : 's'} in ${path.relative(root, file)}`
90
+ )
91
+ );
92
+ }
93
+ }
94
+
95
+ function fileExists(p: string) {
96
+ try {
97
+ fs.statSync(p);
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ export async function main() {
105
+ const args = await yargs(hideBin(process.argv)).help(false).version(version).argv;
106
+
107
+ if (args.help || args.h) {
108
+ console.log(help);
109
+ return process.exit(0);
110
+ }
111
+
112
+ // Detects unknown arguments
113
+ for (const key in args) {
114
+ if (key === '_' || key === '$0') {
115
+ continue;
116
+ }
117
+
118
+ switch (key) {
119
+ case 'cwd':
120
+ case 'project':
121
+ case 'p':
122
+ case 's':
123
+ case 'simplified':
124
+ continue;
125
+ default:
126
+ console.error(`Unknown argument: ${key}. Run --help for more information.`);
127
+ return process.exit(1);
128
+ }
129
+ }
130
+
131
+ const root = args.cwd ? String(args.cwd) : process.cwd();
132
+
133
+ const tsconfigPath = String(args.project || args.p || 'tsconfig.json');
134
+
135
+ const simplified = !!(args.simplified || args.s);
136
+
137
+ const diagnosticFormatter =
138
+ !process.stdout.isTTY || simplified
139
+ ? ts.formatDiagnostics
140
+ : ts.formatDiagnosticsWithColorAndContext;
141
+
142
+ if (!fileExists(tsconfigPath)) {
143
+ console.error((!simplified ? chalk.red : String)(`Could not find ${tsconfigPath}`));
144
+ return process.exit(1);
145
+ }
146
+
147
+ const tsconfig = readCompilerOptions(tsconfigPath);
148
+
149
+ const diagnosticHost: ts.FormatDiagnosticsHost = {
150
+ getCurrentDirectory: ts.sys.getCurrentDirectory,
151
+ getCanonicalFileName: (fileName) => fileName,
152
+ getNewLine: () => ts.sys.newLine
153
+ };
154
+
155
+ if (tsconfig.errors) {
156
+ console.error(diagnosticFormatter(tsconfig.errors, diagnosticHost));
157
+ return process.exit(1);
158
+ }
159
+
160
+ let files = tsconfig.fileNames;
161
+
162
+ if (args._.length) {
163
+ // Prefer the files passed as arguments, otherwise use the files in tsconfig.json
164
+ files = [];
165
+
166
+ for (let i = 0; i < args._.length; i++) {
167
+ const file = String(args._[i]);
168
+
169
+ if (!fileExists(file)) {
170
+ console.error(
171
+ (!simplified ? chalk.red : String)(`Could not find provided '${file}' file.`)
172
+ );
173
+ return process.exit(1);
174
+ }
175
+
176
+ if (!file.match(/(t|j)sx$/)) {
177
+ console.warn(
178
+ (!simplified ? chalk.yellow : String)(
179
+ `Provided '${file}' file is not a TSX/JSX file.`
180
+ )
181
+ );
182
+ continue;
183
+ }
184
+
185
+ files.push(file);
186
+ }
187
+ }
188
+
189
+ if (!files.length) {
190
+ console.error((!simplified ? chalk.red : String)('No files were found to check.'));
191
+ return process.exit(1);
192
+ }
193
+
194
+ const program = ts.createProgram(files, tsconfig.options);
195
+ const typeChecker = program.getTypeChecker();
196
+ const sources = program.getSourceFiles();
197
+
198
+ const diagnostics: ts.Diagnostic[] = [];
199
+
200
+ for (const source of sources) {
201
+ const filename = source.fileName;
202
+
203
+ // Not a tsx file, so don't do anything
204
+ if (!filename.match(/(t|j)sx$/)) {
205
+ continue;
206
+ }
207
+
208
+ ts.forEachChild(source, function loopSourceNodes(node) {
209
+ recursiveDiagnoseJsxElements(ts, node, typeChecker, diagnostics);
210
+ });
211
+ }
212
+
213
+ if (diagnostics.length) {
214
+ const hasError = diagnostics.some(
215
+ (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error
216
+ );
217
+
218
+ console.error(diagnosticFormatter(diagnostics, diagnosticHost));
219
+
220
+ if (!simplified) {
221
+ prettyPrintErrorCount(diagnostics, root);
222
+ }
223
+
224
+ process.exit(hasError ? 1 : 2);
225
+ }
226
+
227
+ console.log(chalk.green(`No XSS vulnerabilities found in ${files.length} files!`));
228
+ process.exit(0);
229
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,34 @@
1
+ function createError(code: number, message: string) {
2
+ return {
3
+ code,
4
+ message:
5
+ process.env.INTERNAL_DISABLE_URL_DOCS !== 'true'
6
+ ? `${message}\nSee https://html.kitajs.org/TS${code}`
7
+ : message
8
+ };
9
+ }
10
+
11
+ // 88600 is the base code for all errors in this plugin.
12
+ // KITA in ASCII
13
+ // 75 * 105 * 116 * 97 = 88609500
14
+ // Simplify to 88600
15
+
16
+ export const Xss = createError(
17
+ 88601,
18
+ `Content may introduce an XSS vulnerability and must be marked with the \`safe\` attribute.`
19
+ );
20
+
21
+ export const DoubleEscape = createError(
22
+ 88602,
23
+ `The \`safe\` attribute causes this content to be escaped more than once.`
24
+ );
25
+
26
+ export const ComponentXss = createError(
27
+ 88603,
28
+ `Content inside a Component must be escaped using escapeHtml().`
29
+ );
30
+
31
+ export const UnusedSafe = createError(
32
+ 88604,
33
+ `The \`safe\` attribute is unused in this context.`
34
+ );
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { default as TS, server } from 'typescript/lib/tsserverlibrary';
2
+ import { proxyObject, recursiveDiagnoseJsxElements } from './util';
3
+
4
+ export = function (modules: { typescript: typeof TS }) {
5
+ const ts = modules.typescript;
6
+
7
+ return {
8
+ create(info: Pick<server.PluginCreateInfo, 'languageService'>) {
9
+ const proxy = proxyObject(info.languageService);
10
+
11
+ proxy.getSemanticDiagnostics = function clonedSemanticDiagnostics(filename) {
12
+ const diagnostics = info.languageService.getSemanticDiagnostics(filename);
13
+
14
+ // Not a tsx file, so don't do anything
15
+ if (!filename.endsWith('.tsx') && !filename.endsWith('.jsx')) {
16
+ return diagnostics;
17
+ }
18
+
19
+ const program = info.languageService.getProgram();
20
+
21
+ if (!program) {
22
+ return diagnostics;
23
+ }
24
+
25
+ const source = program.getSourceFile(filename);
26
+
27
+ if (!source) {
28
+ return diagnostics;
29
+ }
30
+
31
+ const typeChecker = program.getTypeChecker();
32
+
33
+ ts.forEachChild(source, function loopSourceNodes(node) {
34
+ recursiveDiagnoseJsxElements(ts, node, typeChecker, diagnostics);
35
+ });
36
+
37
+ return diagnostics;
38
+ };
39
+
40
+ return proxy;
41
+ }
42
+ };
43
+ };