@react-pug/check-types 0.1.4

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,56 @@
1
+ # @react-pug/check-types
2
+
3
+ Type-check React-Pug projects through the TypeScript language-service plugin, from the CLI or as a library.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -D @react-pug/check-types
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ ```bash
14
+ npx @react-pug/check-types
15
+ npx @react-pug/check-types .
16
+ npx @react-pug/check-types src/App.tsx src/Button.tsx
17
+ npx @react-pug/check-types --project tsconfig.json
18
+ ```
19
+
20
+ Supported options:
21
+
22
+ - `-p, --project <path>`: explicit `tsconfig.json` file or directory
23
+ - positional file paths: check only selected files while still using the full project context
24
+ - `--tagFunction <name>`: tag function name, default `pug`
25
+ - `--injectCssxjsTypes <never|auto|force>`: cssxjs/startupjs React prop injection mode
26
+
27
+ Default behavior mirrors `tsc` closely:
28
+
29
+ - if `--project` is omitted, the checker searches upward from the target directory for the nearest `tsconfig.json`
30
+ - diagnostics are printed against original source locations, including Pug regions
31
+ - process exits with code `1` when errors are found
32
+
33
+ ## Library
34
+
35
+ ```js
36
+ import { checkTypes } from '@react-pug/check-types'
37
+
38
+ const result = await checkTypes({ cwd: process.cwd() })
39
+ if (!result.ok) {
40
+ for (const line of result.formattedErrors) console.error(line)
41
+ }
42
+ ```
43
+
44
+ Useful exports:
45
+
46
+ - `checkTypes(options)`
47
+ - `runCli(argv, io?)`
48
+ - `parseArgs(argv)`
49
+
50
+ Published binary:
51
+
52
+ - `check-pug-types`
53
+
54
+ ## Notes
55
+
56
+ The checker tries to use the target project's local `typescript` first and falls back to the package dependency if none is available.
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@react-pug/check-types",
3
+ "version": "0.1.4",
4
+ "type": "module",
5
+ "main": "./src/index.js",
6
+ "bin": {
7
+ "check-pug-types": "./src/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./cli": "./src/cli.js"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "peerDependencies": {
17
+ "typescript": "*"
18
+ },
19
+ "dependencies": {
20
+ "@react-pug/typescript-plugin-react-pug": "^0.1.3"
21
+ },
22
+ "gitHead": "38ab50d75b83fbab27b7261349cba94db7707840"
23
+ }
package/src/cli.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './index.js'
3
+
4
+ const exitCode = await runCli(process.argv.slice(2))
5
+ process.exit(exitCode)
package/src/index.js ADDED
@@ -0,0 +1,407 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { createRequire } from 'node:module'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const require = createRequire(import.meta.url)
7
+ const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
8
+
9
+ export function printUsage (stdout = console.log) {
10
+ stdout('Usage: npx @react-pug/check-types [files...] [--project <tsconfig-path>]')
11
+ stdout(' [--tagFunction <name>] [--injectCssxjsTypes <never|auto|force>] [--pretty [true|false]]')
12
+ stdout('')
13
+ stdout('Examples:')
14
+ stdout(' npx @react-pug/check-types')
15
+ stdout(' npx @react-pug/check-types src/App.tsx src/Button.tsx')
16
+ stdout(' npx @react-pug/check-types --project example example/src/App.tsx')
17
+ stdout(' npx @react-pug/check-types example')
18
+ }
19
+
20
+ export function parseArgs (argv) {
21
+ const positionals = []
22
+ let projectPath
23
+ let tagFunction = 'pug'
24
+ let injectCssxjsTypes = 'auto'
25
+ let pretty = 'auto'
26
+
27
+ for (let i = 0; i < argv.length; i += 1) {
28
+ const arg = argv[i]
29
+ if (arg === '--help' || arg === '-h') return { help: true }
30
+ if (arg === '--project' || arg === '-p') {
31
+ projectPath = argv[i + 1]
32
+ i += 1
33
+ continue
34
+ }
35
+ if (arg === '--tagFunction') {
36
+ tagFunction = argv[i + 1] ?? tagFunction
37
+ i += 1
38
+ continue
39
+ }
40
+ if (arg === '--injectCssxjsTypes') {
41
+ injectCssxjsTypes = argv[i + 1] ?? injectCssxjsTypes
42
+ i += 1
43
+ continue
44
+ }
45
+ if (arg === '--pretty') {
46
+ const next = argv[i + 1]
47
+ if (next === 'true' || next === 'false') {
48
+ pretty = next === 'true'
49
+ i += 1
50
+ } else {
51
+ pretty = true
52
+ }
53
+ continue
54
+ }
55
+ if (!arg.startsWith('-')) {
56
+ positionals.push(arg)
57
+ continue
58
+ }
59
+ throw new Error(`Unknown argument: ${arg}`)
60
+ }
61
+
62
+ if (!['never', 'auto', 'force'].includes(injectCssxjsTypes)) {
63
+ throw new Error(`Invalid --injectCssxjsTypes value: ${injectCssxjsTypes}`)
64
+ }
65
+
66
+ return { help: false, positionals, projectPath, tagFunction, injectCssxjsTypes, pretty }
67
+ }
68
+
69
+ function formatWithPretty (text, code) {
70
+ return `\u001b[${code}m${text}\u001b[0m`
71
+ }
72
+
73
+ export function resolvePrettyOption (pretty, isTTY = false) {
74
+ if (pretty === 'auto') return Boolean(isTTY)
75
+ return Boolean(pretty)
76
+ }
77
+
78
+ export function formatSummary (errorCount, fileCount) {
79
+ if (fileCount > 0) {
80
+ return `Found ${errorCount} TypeScript error${errorCount === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}.`
81
+ }
82
+ return `Found ${errorCount} TypeScript error${errorCount === 1 ? '' : 's'}.`
83
+ }
84
+
85
+ export function resolveCliTargets (cwd, positionals, explicitProjectPath) {
86
+ if (explicitProjectPath) {
87
+ return { projectDir: '.', filePaths: positionals }
88
+ }
89
+
90
+ if (positionals.length === 1) {
91
+ const candidate = path.resolve(cwd, positionals[0])
92
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
93
+ return { projectDir: positionals[0], filePaths: [] }
94
+ }
95
+ }
96
+
97
+ return { projectDir: '.', filePaths: positionals }
98
+ }
99
+
100
+ function loadModuleFromLocations (specifier, locations) {
101
+ let lastError
102
+ for (const location of locations) {
103
+ try {
104
+ const resolved = require.resolve(specifier, { paths: [location] })
105
+ const mod = require(resolved)
106
+ return mod.default ?? mod
107
+ } catch (err) {
108
+ lastError = err
109
+ }
110
+ }
111
+ throw new Error(`Cannot load ${specifier}.\n${String(lastError)}`)
112
+ }
113
+
114
+ export function loadTypeScript (projectDir, cwd = process.cwd()) {
115
+ return loadModuleFromLocations('typescript', [projectDir, cwd, packageDir])
116
+ }
117
+
118
+ export function loadPlugin (projectDir, cwd = process.cwd()) {
119
+ return loadModuleFromLocations('@react-pug/typescript-plugin-react-pug', [projectDir, cwd, packageDir])
120
+ }
121
+
122
+ export function resolveTsconfigPath (ts, cwd, projectDirArg = '.', explicitProjectPath) {
123
+ if (explicitProjectPath) {
124
+ const resolved = path.resolve(cwd, explicitProjectPath)
125
+ if (ts.sys.directoryExists?.(resolved)) {
126
+ const found = ts.findConfigFile(resolved, ts.sys.fileExists, 'tsconfig.json')
127
+ if (!found) throw new Error(`Cannot find tsconfig.json inside ${resolved}`)
128
+ return found
129
+ }
130
+ return resolved
131
+ }
132
+
133
+ const searchDir = path.resolve(cwd, projectDirArg)
134
+ const found = ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json')
135
+ if (!found) {
136
+ throw new Error(`Cannot find tsconfig.json from ${searchDir}`)
137
+ }
138
+ return found
139
+ }
140
+
141
+ export function createLanguageServiceFromTsconfig ({ ts, tsconfigPath, pluginInit, pluginConfig }) {
142
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile)
143
+ if (configFile.error) {
144
+ return { configErrors: [configFile.error], parsedConfig: undefined, ls: undefined }
145
+ }
146
+
147
+ const configDir = path.dirname(tsconfigPath)
148
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, configDir)
149
+
150
+ const host = {
151
+ getScriptFileNames: () => parsedConfig.fileNames,
152
+ getScriptVersion: fileName => String(ts.sys.getModifiedTime?.(fileName)?.valueOf() ?? 0),
153
+ getScriptSnapshot: fileName => {
154
+ const text = ts.sys.readFile(fileName)
155
+ return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text)
156
+ },
157
+ getCurrentDirectory: () => configDir,
158
+ getCompilationSettings: () => parsedConfig.options,
159
+ getDefaultLibFileName: ts.getDefaultLibFilePath,
160
+ fileExists: ts.sys.fileExists,
161
+ readFile: ts.sys.readFile,
162
+ readDirectory: ts.sys.readDirectory,
163
+ directoryExists: ts.sys.directoryExists,
164
+ getDirectories: ts.sys.getDirectories,
165
+ useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
166
+ getNewLine: () => ts.sys.newLine
167
+ }
168
+
169
+ const baseLs = ts.createLanguageService(host, ts.createDocumentRegistry())
170
+ const pluginModule = pluginInit({ typescript: ts })
171
+ const proxiedLs = pluginModule.create({
172
+ languageServiceHost: host,
173
+ languageService: baseLs,
174
+ project: {
175
+ getCurrentDirectory: () => configDir,
176
+ projectService: {
177
+ logger: {
178
+ info: () => {}
179
+ }
180
+ }
181
+ },
182
+ serverHost: ts.sys,
183
+ config: pluginConfig
184
+ })
185
+
186
+ return { configErrors: parsedConfig.errors, parsedConfig, ls: proxiedLs }
187
+ }
188
+
189
+ export function collectDiagnostics (ts, ls, parsedConfig, selectedFiles) {
190
+ const diagnostics = []
191
+ const program = ls.getProgram?.()
192
+ if (program) {
193
+ diagnostics.push(...program.getOptionsDiagnostics())
194
+ diagnostics.push(...program.getGlobalDiagnostics())
195
+ }
196
+ const filesToCheck = selectedFiles?.length ? selectedFiles : parsedConfig.fileNames
197
+ for (const fileName of filesToCheck) {
198
+ diagnostics.push(...ls.getSyntacticDiagnostics(fileName))
199
+ diagnostics.push(...ls.getSemanticDiagnostics(fileName))
200
+ }
201
+ return diagnostics
202
+ }
203
+
204
+ export function uniqDiagnostics (ts, diagnostics) {
205
+ const out = []
206
+ const seen = new Set()
207
+ for (const diag of diagnostics) {
208
+ const key = [
209
+ diag.file?.fileName ?? '',
210
+ diag.start ?? '',
211
+ diag.length ?? '',
212
+ diag.code ?? '',
213
+ ts.flattenDiagnosticMessageText(diag.messageText, '\n')
214
+ ].join('|')
215
+ if (seen.has(key)) continue
216
+ seen.add(key)
217
+ out.push(diag)
218
+ }
219
+ return out
220
+ }
221
+
222
+ function computeLineAndColumn (text, offset) {
223
+ const safeOffset = Math.max(0, Math.min(offset, text.length))
224
+ let line = 0
225
+ let lineStart = 0
226
+ for (let i = 0; i < safeOffset; i += 1) {
227
+ if (text.charCodeAt(i) === 10) {
228
+ line += 1
229
+ lineStart = i + 1
230
+ }
231
+ }
232
+ return { line: line + 1, column: safeOffset - lineStart + 1 }
233
+ }
234
+
235
+ export function formatDiagnostic (ts, diag, cwd, fileTextCache) {
236
+ const category = ts.DiagnosticCategory[diag.category]?.toLowerCase() ?? 'unknown'
237
+ const code = `TS${diag.code}`
238
+ const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n')
239
+
240
+ if (!diag.file || diag.start === undefined) {
241
+ return `${category} ${code}: ${message}`
242
+ }
243
+
244
+ const filePath = diag.file.fileName
245
+ const file = path.relative(cwd, filePath) || filePath
246
+
247
+ if (!fileTextCache.has(filePath)) {
248
+ fileTextCache.set(filePath, ts.sys.readFile(filePath) ?? null)
249
+ }
250
+ const text = fileTextCache.get(filePath)
251
+ if (typeof text === 'string') {
252
+ const pos = computeLineAndColumn(text, diag.start)
253
+ return `${file}:${pos.line}:${pos.column} - ${category} ${code}: ${message}`
254
+ }
255
+
256
+ const pos = diag.file.getLineAndCharacterOfPosition(diag.start)
257
+ return `${file}:${pos.line + 1}:${pos.character + 1} - ${category} ${code}: ${message}`
258
+ }
259
+
260
+ export function formatDiagnosticOutput (line, pretty) {
261
+ if (!pretty) return line
262
+ return line
263
+ .replace(/(^|\s)(error)(\s+TS\d+:)/, (_, prefix, word, suffix) => `${prefix}${formatWithPretty(word, '31;1')}${formatWithPretty(suffix.trimStart(), '36;1')}`)
264
+ .replace(/^(.*?:\d+:\d+)/, match => formatWithPretty(match, '90'))
265
+ }
266
+
267
+ function normalizeFiles (cwd, configDir, filePaths) {
268
+ return filePaths.map(filePath => {
269
+ const resolved = path.resolve(cwd, filePath)
270
+ const normalized = path.normalize(resolved)
271
+ return path.isAbsolute(normalized) ? normalized : path.resolve(configDir, normalized)
272
+ })
273
+ }
274
+
275
+ export async function checkTypes ({
276
+ cwd = process.cwd(),
277
+ projectDir = '.',
278
+ projectPath,
279
+ filePaths = [],
280
+ tagFunction = 'pug',
281
+ injectCssxjsTypes = 'auto',
282
+ loadPluginModule,
283
+ loadTypeScriptModule
284
+ } = {}) {
285
+ const resolvedProjectDir = path.resolve(cwd, projectDir)
286
+ const ts = loadTypeScriptModule ? await loadTypeScriptModule(resolvedProjectDir, cwd) : loadTypeScript(resolvedProjectDir, cwd)
287
+ const pluginInit = loadPluginModule ? await loadPluginModule(resolvedProjectDir, cwd) : loadPlugin(resolvedProjectDir, cwd)
288
+ const tsconfigPath = resolveTsconfigPath(ts, cwd, projectDir, projectPath)
289
+
290
+ const { configErrors, parsedConfig, ls } = createLanguageServiceFromTsconfig({
291
+ ts,
292
+ tsconfigPath,
293
+ pluginInit,
294
+ pluginConfig: {
295
+ enabled: true,
296
+ diagnostics: { enabled: true },
297
+ tagFunction,
298
+ injectCssxjsTypes
299
+ }
300
+ })
301
+
302
+ if (!parsedConfig || !ls) {
303
+ const diagnostics = uniqDiagnostics(ts, configErrors)
304
+ const fileTextCache = new Map()
305
+ const formattedErrors = diagnostics.map(diag => formatDiagnostic(ts, diag, cwd, fileTextCache))
306
+ return {
307
+ ok: false,
308
+ exitCode: 1,
309
+ ts,
310
+ parsedConfig: null,
311
+ diagnostics,
312
+ errors: diagnostics,
313
+ formattedErrors,
314
+ fileCount: 0,
315
+ selectedFiles: [],
316
+ errorFileCount: 0,
317
+ tsconfigPath
318
+ }
319
+ }
320
+
321
+ const configDir = path.dirname(tsconfigPath)
322
+ const selectedFiles = filePaths.length ? normalizeFiles(cwd, configDir, filePaths) : []
323
+ const projectFiles = new Set(parsedConfig.fileNames.map(fileName => path.normalize(fileName)))
324
+ const missingFiles = selectedFiles.filter(fileName => !projectFiles.has(path.normalize(fileName)))
325
+
326
+ const diagnostics = uniqDiagnostics(ts, [
327
+ ...configErrors,
328
+ ...collectDiagnostics(ts, ls, parsedConfig, selectedFiles)
329
+ ]).sort((a, b) => {
330
+ const af = a.file?.fileName ?? ''
331
+ const bf = b.file?.fileName ?? ''
332
+ if (af !== bf) return af.localeCompare(bf)
333
+ const as = a.start ?? -1
334
+ const bs = b.start ?? -1
335
+ if (as !== bs) return as - bs
336
+ return a.code - b.code
337
+ })
338
+
339
+ const errors = diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error)
340
+ const fileTextCache = new Map()
341
+ const formattedErrors = [
342
+ ...errors.map(diag => formatDiagnostic(ts, diag, cwd, fileTextCache)),
343
+ ...missingFiles.map(fileName => `error CHECK0001: File is not part of the resolved TypeScript project: ${path.relative(cwd, fileName) || fileName}`)
344
+ ]
345
+ const errorFileCount = new Set(errors.map(diag => diag.file?.fileName).filter(Boolean)).size
346
+
347
+ return {
348
+ ok: errors.length === 0 && missingFiles.length === 0,
349
+ exitCode: errors.length === 0 && missingFiles.length === 0 ? 0 : 1,
350
+ ts,
351
+ parsedConfig,
352
+ diagnostics,
353
+ errors,
354
+ formattedErrors,
355
+ fileCount: selectedFiles.length || parsedConfig.fileNames.length,
356
+ selectedFiles,
357
+ missingFiles,
358
+ errorFileCount,
359
+ tsconfigPath
360
+ }
361
+ }
362
+
363
+ export async function runCli (argv = process.argv.slice(2), io = {}) {
364
+ const stdout = io.stdout ?? console.log
365
+ const stderr = io.stderr ?? console.error
366
+ const cwd = io.cwd ?? process.cwd()
367
+ const prettyIsTTY = io.stderrIsTTY ?? process.stderr.isTTY
368
+
369
+ let parsed
370
+ try {
371
+ parsed = parseArgs(argv)
372
+ } catch (err) {
373
+ stderr(String(err))
374
+ printUsage(stdout)
375
+ return 1
376
+ }
377
+
378
+ if (parsed.help) {
379
+ printUsage(stdout)
380
+ return 0
381
+ }
382
+
383
+ try {
384
+ const { projectDir, filePaths } = resolveCliTargets(cwd, parsed.positionals, parsed.projectPath)
385
+ const result = await checkTypes({
386
+ cwd,
387
+ projectDir,
388
+ filePaths,
389
+ projectPath: parsed.projectPath,
390
+ tagFunction: parsed.tagFunction,
391
+ injectCssxjsTypes: parsed.injectCssxjsTypes
392
+ })
393
+ const pretty = resolvePrettyOption(parsed.pretty, prettyIsTTY)
394
+ if (result.ok) {
395
+ const scope = result.selectedFiles.length ? 'selected ' : ''
396
+ stdout(`No TypeScript errors (with pug plugin) in ${result.fileCount} ${scope}file${result.fileCount === 1 ? '' : 's'}.`)
397
+ return 0
398
+ }
399
+ for (const line of result.formattedErrors) stderr(formatDiagnosticOutput(line, pretty))
400
+ const totalErrorCount = result.errors.length + result.missingFiles.length
401
+ stderr(`\n${formatSummary(totalErrorCount, result.errorFileCount)}`)
402
+ return 1
403
+ } catch (err) {
404
+ stderr(err instanceof Error ? err.message : String(err))
405
+ return 1
406
+ }
407
+ }