• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DanielXMoore / Civet / 24060715537

07 Apr 2026 02:06AM UTC coverage: 95.904% (+1.2%) from 94.751%
24060715537

Pull #1921

github

web-flow
Merge 6299a9be5 into c526803cf
Pull Request #1921: Increase CLI and unplugin test coverage

6234 of 6635 branches covered (93.96%)

Branch coverage included in aggregate %.

27 of 34 new or added lines in 2 files covered. (79.41%)

3 existing lines in 1 file now uncovered.

25870 of 26840 relevant lines covered (96.39%)

37110.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.13
/source/unplugin/unplugin.civet
1
import { type TransformResult, createUnplugin, type UnpluginContextMeta, type UnpluginOptions } from 'unplugin'
1✔
2
import civet, { decode, lib, SourceMap, type CompileOptions, type ParseOptions } from '@danielx/civet'
1✔
3
import { findInDir, loadConfig } from '@danielx/civet/config'
1✔
4
import {
1✔
5
  remapRange,
1✔
6
  flattenDiagnosticMessageText,
1✔
7
  // @ts-ignore
1✔
8
  // using ts-ignore because the version of @danielx/civet typescript is checking against
1✔
9
  // is the one published to npm, not the one in the repo
1✔
10
} from '@danielx/civet/ts-diagnostic'
1✔
11
import * as fs from 'fs'
1✔
12
import path from 'path'
1✔
13
import type { FormatDiagnosticsHost, Diagnostic, System } from 'typescript'
1✔
14
import { createFSBackedSystem, createVirtualCompilerHost } from '@typescript/vfs'
1✔
15
import type { UserConfig } from 'vite'
1✔
16
import type { BuildOptions } from 'esbuild'
1✔
17
import os from 'os'
1✔
18
// From vite/src/node/constants.ts (no stable export path in vite 8)
1✔
19
DEFAULT_EXTENSIONS := [".mjs",".js",".mts",".ts",".jsx",".tsx",".json"]
1✔
20

1✔
21
// Copied from typescript to avoid importing the whole package
1✔
22
enum DiagnosticCategory
1✔
23
  Warning = 0
1✔
24
  Error = 1
1✔
25
  Suggestion = 2
1✔
26
  Message = 3
1✔
27

1✔
28
export type PluginOptions
1✔
29
  implicitExtension?: boolean
1✔
30
  outputExtension?: string
1✔
31
  transformOutput?: (
1✔
32
    code: string
1✔
33
    id: string
1✔
34
  ) => TransformResult | Promise<TransformResult>
1✔
35
  emitDeclaration?: boolean
1✔
36
  declarationExtension?: string
1✔
37
  typecheck?: boolean | string
1✔
38
  ts?: 'civet' | 'esbuild' | 'tsc' | 'preserve'
1✔
39
  /** @deprecated Use "ts" option instead */
1✔
40
  js?: boolean
1✔
41
  /** @deprecated Use "emitDeclaration" instead */
1✔
42
  dts?: boolean
1✔
43
  /** Number of parallel threads to compile with (Node only) */
1✔
44
  threads?: number
1✔
45
  /** Cache compilation results based on file mtime (useful for serve or watch mode) */
1✔
46
  cache?: boolean
1✔
47
  /** config filename, or false/null to not look for default config file */
1✔
48
  config?: string | false | null
1✔
49
  parseOptions?: ParseOptions
1✔
50
  tsConfig?: any
1✔
51

1✔
52
type CacheEntry
1✔
53
  mtime: number
1✔
54
  result?: TransformResult
1✔
55
  promise?: Promise<void>
1✔
56

1✔
57
queryPostfixRE := /\?.*$/s
1✔
58
escapedHashRE := /\0#/g
1✔
59
isWindows := os.platform() is 'win32'
1✔
60
windowsSlashRE := /\\/g
1✔
61
civetSuffix := '.civet'
1✔
62
workerRE := /(?:\?|&)(?:worker|sharedworker)(?:&|$|#)/
1✔
63

1✔
64
/**
1✔
65
Extract a possible Civet filename from an id, after removing a possible
1✔
66
outputExtension and/or a query/hash (?/#) postfix.
1✔
67
Returns {filename, postfix} in case you need to add the postfix back.
1✔
68
You should check whether the filename ends in .civet extension,
1✔
69
or needs an implicit .civet extension, or isn't Civet-related at all.
1✔
70
*/
1✔
71
function extractCivetFilename(id: string, outputExtension: string): {filename: string, postfix: string}
37✔
72
  postfix .= ''
37✔
73
  // Webpack escapes literal `#` in `loaderContext.resource` as `\0#` so it
37✔
74
  // can distinguish path characters from fragment suffixes. Undo that first.
37✔
75
  id = id.replace escapedHashRE, '#'
37✔
76
  // `?` is always treated as a suffix. Raw `?` is impossible in Windows paths,
37✔
77
  // and we rely on query suffixes across bundlers.
37✔
78
  filename .= id.replace queryPostfixRE, (match) =>
37✔
79
    postfix = match
1✔
80
    ''
1✔
81
  // Normally the outputExtension (.jsx/.tsx by default) should be present,
37✔
82
  // but sometimes (e.g. esbuild's alias feature) load directly without resolve
37✔
83
  if filename.endsWith outputExtension
37✔
84
    filename = filename[< -outputExtension#]
28✔
85
  // `#` may be either a literal path character or a fragment-like suffix.
37✔
86
  // Keep it when the file exists as written; otherwise strip one rightmost
37✔
87
  // `#...` suffix and let later resolution check whether file exists.
37✔
88
  hashIndex := filename.lastIndexOf '#'
37✔
89
  if hashIndex >= 0 and not tryStatSync filename
37✔
90
    postfix = filename[hashIndex..] + postfix
1✔
91
    filename = filename[..<hashIndex]
1✔
92
    if filename.endsWith outputExtension
1✔
93
      filename = filename[< -outputExtension#]
1✔
94
  {filename, postfix}
37✔
95

1✔
96
function tryStatSync(file: string): fs.Stats?
25✔
97
  try
25✔
98
    // The "throwIfNoEntry" is a performance optimization for cases where the file does not exist
25✔
99
    return fs.statSync file, throwIfNoEntry: false
25✔
100

1✔
101
export function slash(p: string): string
1✔
102
  p.replace windowsSlashRE, '/'
8✔
103

1✔
104
function normalizePath(id: string): string
26✔
105
  path.posix.normalize isWindows ? slash(id) : id
26!
106

1✔
107
function tryFsResolve(file: string): string?
24✔
108
  fileStat := tryStatSync file
24✔
109
  if fileStat?.isFile()
24✔
110
    normalizePath file
3✔
111

1✔
112
function resolveAbsolutePath(rootDir: string, id: string, implicitExtension: boolean)
5✔
113
  file := path.join rootDir, id
5✔
114
  // Check for existence of resolved file and unresolved id,
5✔
115
  // without and with implicit .civet extension, and return first existing
5✔
116
  (or)
5✔
117
    tryFsResolve(file)
5✔
118
    implicitExtension and implicitCivet file
5✔
119
    tryFsResolve id
5✔
120
    implicitExtension and implicitCivet id
2✔
121

1✔
122
function implicitCivet(file: string): string?
7✔
123
  return if tryFsResolve file
7!
124
  civet := file + '.civet'
7✔
125
  return civet if tryFsResolve civet
7✔
126

1✔
127
export rawPlugin := (options: PluginOptions = {}, meta: UnpluginContextMeta) =>
1✔
128
  if (options.dts) options.emitDeclaration = options.dts
47✔
129
  compileOptions: CompileOptions .= {}
47✔
130

47✔
131
  ts .= options.ts
47✔
132
  if (options.js) ts = 'civet'
47✔
133
  unless ts?
47✔
134
    console.log 'WARNING: You are using the default mode for `options.ts` which is `"civet"`. This mode does not support all TS features. If this is intentional, you should explicitly set `options.ts` to `"civet"`, or choose a different mode.'
×
135
    ts = "civet"
×
136
  unless ts is in ["civet", "esbuild", "tsc", "preserve"]
47✔
137
    console.log `WARNING: Invalid option ts: ${JSON.stringify ts}; switching to "civet"`
1✔
138
    ts = "civet"
1✔
139

47✔
140
  transformTS := options.emitDeclaration or options.typecheck
47✔
141
  outExt :=
47✔
142
    options.outputExtension ?? (ts is "preserve" ? ".tsx" : ".jsx")
47✔
143
  implicitExtension := options.implicitExtension ?? true
47✔
144
  let aliasResolver: (id: string) => string
47✔
145

47✔
146
  fsMap: Map<string, string> .= new Map
47✔
147
  sourceMaps := new Map<string, SourceMap>
47✔
148
  let compilerOptions: any, compilerOptionsWithSourceMap: any
47✔
149
  rootDir .= process.cwd()
47✔
150
  outDir .= path.join rootDir, 'dist'
47✔
151
  let esbuildOptions: BuildOptions
47✔
152
  let configErrors: Diagnostic[]?
47✔
153
  let configFileNames: string[]
47✔
154
  skipWorker .= false
47✔
155

47✔
156
  tsPromise := if transformTS or ts is "tsc"
47✔
157
    import('typescript').then .default
4✔
158
  getFormatHost := (sys: System): FormatDiagnosticsHost =>
47✔
159
    return {
2✔
160
      getCurrentDirectory: => sys.getCurrentDirectory()
2✔
161
      getNewLine: => sys.newLine
2✔
162
      getCanonicalFileName: sys.useCaseSensitiveFileNames
2✔
163
        ? (f) => f
2✔
164
        : (f) => f.toLowerCase()
2!
165
    }
2✔
166

47✔
167
  cache := new Map<string, CacheEntry> unless options.cache is false
1✔
168

47✔
169
  plugin := {
47✔
170
    name: 'unplugin-civet'
47✔
171
    enforce: 'pre'
47✔
172

47✔
173
    buildStart(): Promise<void>
26✔
174
      civetConfigPath .= options.config
26✔
175
      if civetConfigPath is undefined
26✔
176
        civetConfigPath = await findInDir process.cwd()
×
177
      if civetConfigPath
26✔
178
        compileOptions = await loadConfig civetConfigPath
×
179
      // Merge parseOptions, with plugin options taking priority
26✔
180
      compileOptions.parseOptions = {
26✔
181
        ...compileOptions.parseOptions
26✔
182
        ...options.parseOptions
26✔
183
      }
26✔
184
      compileOptions.threads = options.threads if options.threads?
26✔
185

26✔
186
      if transformTS or ts is "tsc"
26✔
187
        ts := await tsPromise!
2✔
188
        let config: any, error: Diagnostic?
2✔
189

2✔
190
        if options.tsConfig
2✔
191
          config = options.tsConfig
1✔
192
        else
1✔
193
          tsConfigPath := ts.findConfigFile process.cwd(), ts.sys.fileExists
1✔
194

1✔
195
          unless tsConfigPath
1✔
NEW
196
            throw new Error "Could not find 'tsconfig.json'"
×
197

1✔
198
          { config, error } = ts.readConfigFile
1✔
199
            tsConfigPath
1✔
200
            ts.sys.readFile
1✔
201

2✔
202
        if error
2✔
203
          console.error ts.formatDiagnostic error, getFormatHost ts.sys
×
204
          throw error
×
205

2✔
206
        // Mogrify tsconfig.json "files" field to use .civet.tsx
2✔
207
        function mogrify(key: string)
2✔
208
          if key in config and Array.isArray config[key]
2✔
209
            config[key] = config[key].map (item: unknown) =>
1✔
210
              return item unless item <? "string"
1!
211
              return item.replace(/\.civet\b(?!\.)/g, '.civet.tsx')
1✔
212
        mogrify "files"
2✔
213

2✔
214
        // Override readDirectory (used for include/exclude matching)
2✔
215
        // to include .civet files, as .civet.tsx files
2✔
216
        system := {...ts.sys}
2✔
217
        {readDirectory: systemReadDirectory} := system
2✔
218
        system.readDirectory = (path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] =>
2✔
219
          extensions = [ ...(extensions ?? []), ".civet" ]
1!
220
          systemReadDirectory(path, extensions, excludes, includes, depth)
1✔
221
          .map &.endsWith(".civet") ? & + ".tsx" : &
1✔
222

2✔
223
        configContents := ts.parseJsonConfigFileContent
2✔
224
          config
2✔
225
          system
2✔
226
          process.cwd()
2✔
227
        configErrors = configContents.errors
2✔
228
        configFileNames = configContents.fileNames
2✔
229

2✔
230
        compilerOptions = {
2✔
231
          ...configContents.options
2✔
232
          target: ts.ScriptTarget.ESNext
2✔
233
          composite: false
2✔
234
        }
2✔
235
        // We use .tsx extensions when type checking, so need to enable
2✔
236
        // JSX mode even if the user doesn't request/use it.
2✔
237
        compilerOptions.jsx ??= ts.JsxEmit.Preserve
2✔
238
        compilerOptionsWithSourceMap = {
2✔
239
          ...compilerOptions
2✔
240
          sourceMap: true
2✔
241
        }
2✔
242
        fsMap = new Map()
2✔
243

47✔
244
    buildEnd(useConfigFileNames = false): Promise<void>
2✔
245
      if transformTS
2✔
246
        const ts = await tsPromise!
2✔
247

2✔
248
        // Create a virtual file system with all source files processed so far,
2✔
249
        // but which further resolves any Civet dependencies that are needed
2✔
250
        // just for typechecking (e.g. `import type` which get removed in JS).
2✔
251
        system := createFSBackedSystem fsMap, process.cwd(), ts
2✔
252
        {
2✔
253
          fileExists: systemFileExists
2✔
254
          readFile: systemReadFile
2✔
255
          readDirectory: systemReadDirectory
2✔
256
        } := system
2✔
257

2✔
258
        system.fileExists = (filename: string): boolean =>
2✔
259
          if (!filename.endsWith('.civet.tsx')) return systemFileExists(filename)
4,097✔
260
          if (fsMap.has(filename)) return true
×
261
          return systemFileExists filename[...-4]
×
262

2✔
263
        system.readDirectory = (path: string): string[] =>
2✔
264
          systemReadDirectory(path)
×
265
          .map &.endsWith('.civet') ? & + '.tsx' : &
×
266

2✔
267
        tsCompileOptions := {
2✔
268
          ...compileOptions
2✔
269
          rewriteCivetImports: false
2✔
270
          rewriteTsImports: true
2✔
271
        }
2✔
272
        system.readFile = (filename: string, encoding: BufferEncoding = 'utf-8'): string? =>
2✔
273
          // Mogrify package.json imports field to use .civet.tsx
333✔
274
          if path.basename(filename) is "package.json"
333✔
275
            json := systemReadFile filename, encoding
25✔
276
            return json unless json
25!
277
            parsed: Record<string, unknown> := JSON.parse(json)
25✔
278
            modified .= false
25✔
279
            function recurse(node: unknown): void
25✔
280
              if node? <? "object"
×
281
                for key in node
×
282
                  value := (node as Record<string, unknown>)[key]
×
283
                  if value <? "string"
×
284
                    if value.endsWith ".civet"
×
285
                      (node as Record<string, unknown>)[key] = value + '.tsx'
×
286
                      modified = true
×
287
                  else if value
×
288
                    recurse value
×
289
            recurse parsed.imports
25✔
290
            return modified ? JSON.stringify(parsed) : json
25!
291

308✔
292
          // Generate .civet.tsx files on the fly
308✔
293
          if (!filename.endsWith('.civet.tsx')) return systemReadFile(filename, encoding)
333✔
294
          if (fsMap.has(filename)) return fsMap.get(filename)
2✔
295
          civetFilename := filename[...-4]
×
296
          rawCivetSource := fs.readFileSync civetFilename, {encoding}
×
297
          { code: compiledTS, sourceMap } := civet.compile rawCivetSource, {
×
298
            ...tsCompileOptions
×
299
            filename
×
300
            js: false
×
301
            sourceMap: true
×
302
            sync: true // TS readFile API seems to need to be synchronous
×
303
          }
×
304
          fsMap.set filename, compiledTS
×
305
          sourceMaps.set filename, sourceMap
×
306
          return compiledTS
×
307

2✔
308
        host := createVirtualCompilerHost
2✔
309
          system
2✔
310
          compilerOptions
2✔
311
          ts
2✔
312

2✔
313
        program := ts.createProgram
2✔
314
          rootNames: useConfigFileNames ? configFileNames : [...fsMap.keys()]
2!
315
          options: compilerOptions
2✔
316
          host: host.compilerHost
2✔
317

2✔
318
        diagnostics: Diagnostic[] := ts
2✔
319
          .getPreEmitDiagnostics(program)
2✔
320
          .map (diagnostic) =>
2✔
321
            file := diagnostic.file
7✔
322
            if (!file) return diagnostic
7✔
323

6✔
324
            sourceMap := sourceMaps.get file.fileName
6✔
325
            if (!sourceMap) return diagnostic
7✔
326

1✔
327
            sourcemapLines := sourceMap.lines ?? sourceMap.data.lines
7!
328
            range := remapRange(
7✔
329
              {
7✔
330
                start: diagnostic.start || 0,
7!
331
                end: (diagnostic.start || 0) + (diagnostic.length || 1),
7!
332
              },
7✔
333
              sourcemapLines
7✔
334
            )
7✔
335

7✔
336
            {
7✔
337
              ...diagnostic,
7✔
338
              messageText: flattenDiagnosticMessageText(diagnostic.messageText),
7✔
339
              length: diagnostic.length,
7✔
340
              start: range.start,
7✔
341
            }
7✔
342

2✔
343
        if configErrors?#
2✔
344
          diagnostics.unshift ...configErrors
×
345

2✔
346
        if diagnostics# > 0
2✔
347
          console.error
2✔
348
            ts.formatDiagnosticsWithColorAndContext
2✔
349
              diagnostics
2✔
350
              getFormatHost ts.sys
2✔
351
          if options.typecheck
2✔
352
            failures: DiagnosticCategory[] .= []
1✔
353
            if options.typecheck <? "string"
1✔
354
              if (options.typecheck.includes('error')) failures.push(DiagnosticCategory.Error)
×
355
              if (options.typecheck.includes('warning')) failures.push(DiagnosticCategory.Warning)
×
356
              if (options.typecheck.includes('suggestion')) failures.push(DiagnosticCategory.Suggestion)
×
357
              if (options.typecheck.includes('message')) failures.push(DiagnosticCategory.Message)
×
358
              if (options.typecheck.includes('all'))
×
359
                failures = { includes: () => true } as any as DiagnosticCategory[]
×
360
            else
1✔
361
              // Default behavior: fail on errors
1✔
362
              failures.push(DiagnosticCategory.Error)
1✔
363
            count := diagnostics.filter((d) => failures.includes(d.category)).length
1✔
364
            if count
1✔
365
              reason :=
1✔
366
                (count is diagnostics# ? count : `${count} out of ${diagnostics#}`)
1!
367
              throw new Error `Aborting build because of ${reason} TypeScript diagnostic${diagnostics.length > 1 ? 's' : ''} above`
1!
368

1✔
369
        if options.emitDeclaration
1✔
370
          if meta.framework is 'esbuild' and not esbuildOptions.outdir
1✔
371
            throw new Error "Civet unplugin's `emitDeclaration` requires esbuild's `outdir` option to be set;"
×
372

1✔
373
          // Removed duplicate slashed (`\`) versions of the same file for emit
1✔
374
          for file of fsMap.keys()
1✔
375
            slashed := slash file
1✔
376
            unless file is slashed
1✔
377
              fsMap.delete slashed
×
378

1✔
379
          for file of fsMap.keys()
1✔
380
            sourceFile := program.getSourceFile(file)!
1✔
381
            program.emit
1✔
382
              sourceFile
1✔
383
              (filePath, content) =>
1✔
384
                if options.declarationExtension?
1✔
385
                  if filePath.endsWith '.d.ts'
×
386
                    filePath = filePath[< -5]
×
387
                  else
×
388
                    console.log `WARNING: No .d.ts extension in ${filePath}`
×
389
                  if filePath.endsWith civetSuffix
×
390
                    filePath = filePath[< -civetSuffix#]
×
391
                  else
×
392
                    console.log `WARNING: No .civet extension in ${filePath}`
×
393
                  filePath += options.declarationExtension
×
394

1✔
395
                pathFromDistDir .= path.relative
1✔
396
                  compilerOptions.outDir ?? process.cwd()
1!
397
                  filePath
1✔
398

1✔
399
                @emitFile
1✔
400
                  source: content
1✔
401
                  fileName: pathFromDistDir
1✔
402
                  type: 'asset'
1✔
403
              undefined
1✔
404
              true // emitDtsOnly
1✔
405
              undefined
1✔
406
              // @ts-ignore @internal interface
1✔
407
              true // forceDtsEmit
47✔
408

47✔
409
    resolveId(id, importer, options)
9✔
410
      id = aliasResolver id if aliasResolver?
9✔
411
      if (/\0/.test(id)) return null
9✔
412

8✔
413
      // Remove query/hash postfix to get actual path
8✔
414
      {filename, postfix} := extractCivetFilename id, outExt
8✔
415

8✔
416
      resolved .=
8✔
417
        if path.isAbsolute filename
8✔
418
          resolveAbsolutePath rootDir, filename, implicitExtension
5✔
419
        else
3✔
420
          path.resolve path.dirname(importer ?? ''), filename
3✔
421
      if (!resolved) return null
9✔
422

6✔
423
      // Implicit .civet extension
6✔
424
      unless resolved.endsWith civetSuffix
6✔
425
        if (!implicitExtension) return null
3✔
426
        implicitId := implicitCivet resolved
2✔
427
        if (!implicitId) return null
2✔
UNCOV
428
        resolved = implicitId
×
429

3✔
430
      // Tell Vite that this is a virtual module during dependency scanning
3✔
431
      if (options as! {scan?: boolean}).scan and meta.framework is 'vite'
9!
UNCOV
432
        resolved = `\0${resolved}`
×
433

3✔
434
      // Add back the original postfix at the end.
3✔
435
      // For esbuild, the virtual module lives in the plugin's namespace so the
3✔
436
      // output extension is not needed to signal the file type to the bundler
3✔
437
      // (we set the loader explicitly instead).  Keeping the .jsx/.tsx suffix
3✔
438
      // would cause esbuild to use it as the filename in diagnostics, producing
3✔
439
      // confusing messages like "main.civet.jsx" instead of "main.civet".
3✔
440
      return resolved + (meta.framework is 'esbuild' ? '' : outExt) + postfix
9✔
441

47✔
442
    loadInclude(id)
13✔
443
      {filename, postfix} := extractCivetFilename id, outExt
13✔
444
      return false if skipWorker and workerRE.test postfix
13!
445
      filename.endsWith civetSuffix
13✔
446

47✔
447
    load(id)
16✔
448
      {filename} .= extractCivetFilename id, outExt
16✔
449
      // We're guaranteed to have .civet extension here by loadInclude
16✔
450

16✔
451
      filename = path.resolve rootDir, filename
16✔
452
      @addWatchFile filename
16✔
453

16✔
454
      let mtime: number?, cached: CacheEntry?, resolve: =>?
16✔
455
      if cache?
16✔
456
        try
14✔
457
          mtime = fs.promises.stat(filename) |> await |> .mtimeMs
14✔
458
        // If we fail to stat file, ignore cache
14✔
459
        if mtime?
14✔
460
          cached = cache.get filename
14✔
461
          if cached and cached.mtime is mtime
14✔
462
            // If the file is currently being compiled, wait for it to finish
1✔
463
            await cached.promise if cached.promise
1!
464
            if result? := cached.result
1✔
465
              return result
1✔
466
          // We're the first to compile this file with this mtime
13✔
467
          promise := new Promise<void> (r): void => resolve = r
13✔
468
          cache.set filename, cached = {mtime, promise}
13✔
469
      finally resolve?()
15✔
470

15✔
471
      let compiled: string
15✔
472
      let sourceMap: SourceMap | string | undefined
15✔
473
      civetOptions := {
15✔
474
        ...compileOptions
15✔
475
        filename
15✔
476
        errors: []
15✔
477
      }
15✔
478
      function checkErrors
15✔
479
        if civetOptions.errors#
16✔
UNCOV
480
          throw new civet.ParseErrors civetOptions.errors
×
481

15✔
482
      rawCivetSource := decode await fs.promises.readFile filename
15✔
483
      ast := await civet.compile rawCivetSource, {
15✔
484
        ...civetOptions
15✔
485
        ast: true
15✔
486
      }
15✔
487
      civetSourceMap := new SourceMap rawCivetSource,
14✔
488
        normalizePath path.relative outDir, filename
14✔
489

14✔
490
      if ts is "civet"
14✔
491
        compiled = await civet.generate ast, {
8✔
492
          ...civetOptions
8✔
493
          js: true
8✔
494
          sourceMap: civetSourceMap
8✔
495
        }
8✔
496
        sourceMap = civetSourceMap
8✔
497
        checkErrors()
8✔
498
      else
6✔
499
        compiledTS := await civet.generate ast, {
6✔
500
          ...civetOptions
6✔
501
          js: false
6✔
502
          sourceMap: civetSourceMap
6✔
503
        }
6✔
504
        checkErrors()
6✔
505

6✔
506
        switch ts
6✔
507
          when "esbuild"
6✔
508
            esbuildTransform := import("esbuild") |> await |> .transform
3✔
509
            result := await esbuildTransform compiledTS,
3✔
510
              jsx: "preserve"
3✔
511
              loader: "tsx"
3✔
512
              sourcefile: filename
3✔
513
              sourcemap: "external"
3✔
514

3✔
515
            compiled = result.code
3✔
516
            sourceMap = result.map
3✔
517
          when "tsc"
6✔
518
            tsTranspile := tsPromise! |> await |> .transpileModule
2✔
519
            result := tsTranspile compiledTS,
2✔
520
              compilerOptions: compilerOptionsWithSourceMap
2✔
521

2✔
522
            compiled = result.outputText
2✔
523
            sourceMap = result.sourceMapText
2✔
524
          when "preserve"
6✔
525
            compiled = compiledTS
1✔
526
            sourceMap = civetSourceMap
1✔
527

14✔
528
      if transformTS
14✔
529
        // When working with TypeScript, disable rewriteCivetImports and
2✔
530
        // force rewriteTsImports by rewriting imports again.
2✔
531
        // See `ModuleSpecifier` in parser.hera
2✔
532
        for each _spec of lib.gatherRecursive ast, (
2✔
533
          ($) => ($ as {type: string}).type is "ModuleSpecifier"
2✔
534
        )
2✔
535
          spec := _spec as { module?: { token: string, input?: string } }
×
536
          if spec.module?.input
×
537
            spec.module.token = spec.module.input
×
538
            .replace /\.([mc])?ts(['"])$/, ".$1js$2"
×
539

×
540
        compiledTS := await civet.generate ast, {
×
541
          ...civetOptions
×
542
          js: false
×
543
          sourceMap: civetSourceMap
×
544
        }
×
545
        checkErrors()
2✔
546

2✔
547
        // Force .tsx extension for type checking purposes.
2✔
548
        // Otherwise, TypeScript complains about types in .jsx files.
2✔
549
        tsx := filename + '.tsx'
2✔
550
        fsMap.set tsx, compiledTS
2✔
551
        sourceMaps.set tsx, civetSourceMap
2✔
552
        // Vite and Rollup normalize filenames to use `/` instead of `\`.
2✔
553
        // We give the TypeScript VFS both versions just in case.
2✔
554
        slashed := slash tsx
2✔
555
        unless tsx is slashed
2✔
556
          fsMap.set slashed, compiledTS
×
557
          sourceMaps.set slashed, civetSourceMap
×
558

14✔
559
      jsonSourceMap := sourceMap and
14✔
560
        if sourceMap <? "string"
5✔
561
          JSON.parse(sourceMap)
5✔
562
        else
9✔
563
          sourceMap.json normalizePath path.relative outDir, id
9✔
564

16✔
565
      transformed: TransformResult .=
16✔
566
        code: compiled
16✔
567
        map: jsonSourceMap
16✔
568

16✔
569
      if options.transformOutput
16✔
570
        transformed = await options.transformOutput transformed.code, id
2✔
571

14✔
572
      if cached?
14✔
573
        cached.result = transformed
12✔
574
        delete cached.promise
12✔
575

14✔
576
      return transformed
16✔
577

47✔
578
    esbuild: {
47✔
579
      // Explicitly set the loader so esbuild handles the compiled output
47✔
580
      // correctly even though the virtual module id no longer ends in .jsx/.tsx.
47✔
581
      loader: ts is 'preserve' ? 'tsx' : 'jsx'
47✔
582
      config(options: BuildOptions): void
4✔
583
        esbuildOptions = options
4✔
584
        outDir = options.outdir or path.dirname options.outfile!
4✔
585
    }
47✔
586
    vite: {
47✔
587
      config(config: UserConfig): void
2✔
588
        // Vite 7 requires us to skip processing ?worker
2✔
589
        // and instead process a followup request for ?worker_file
2✔
590
        // Luckily, Vite 7 introduced @meta.viteVersion for detection
2✔
591
        //@ts-ignore lacking type for this
2✔
592
        if @?meta?viteVersion and 7 <= Number @meta.viteVersion.split('.')[0]
2!
593
          skipWorker = true
×
594

2✔
595
        rootDir = path.resolve process.cwd(), config.root ?? ''
2!
596
        outDir = path.resolve rootDir, config.build?.outDir ?? 'dist'
2✔
597

2✔
598
        if implicitExtension
2✔
599
          config.resolve ??= {}
2✔
600
          config.resolve.extensions ??= DEFAULT_EXTENSIONS
2✔
601
          config.resolve.extensions.push '.civet'
2✔
602

47✔
NEW
603
      transformIndexHtml(html)
×
604
        html.replace /<!--[^]*?-->|<[^<>]*>/g, (tag) =>
×
605
          tag.replace /<\s*script\b[^<>]*>/gi, (script) =>
×
606
            // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
×
607
            script.replace
×
608
              /([:_\p{ID_Start}][:\p{ID_Continue}]*)(\s*=\s*("[^"]*"|'[^']*'|[^\s"'=<>`]*))?/gu
×
609
              (attr, name, value) =>
×
610
                name.toLowerCase() === 'src' && value
×
611
                  ? attr.replace(
×
612
                      /(\.civet)(['"]?)$/,
×
613
                      (_, extension, endQuote) =>
×
614
                        `${extension}${outExt}?transform${endQuote}`
×
615
                    )
×
616
                  : attr
×
617

47✔
618
      handleHotUpdate({ file, server, modules })
×
619
        // `file` is an absolute path to the changed file on disk,
×
620
        // so for our case it should end with .civet extension
×
621
        return unless file.endsWith '.civet'
×
622
        // Convert into path as would be output by `resolveId`
×
623
        resolvedId := slash path.resolve(file) + outExt
×
624
        // Check for modules for this file
×
625
        if fileModules := server.moduleGraph.getModulesByFile resolvedId
×
626
          // Invalidate modules depending on this one
×
627
          server.moduleGraph.onFileChange resolvedId
×
628
          // Hot reload this module
×
629
          return [ ...modules, ...fileModules ]
×
630
        modules
×
631
    }
47✔
632

47✔
633
    rspack(compiler)
1✔
634
      if implicitExtension
1✔
635
        compiler.options ?= {}
1✔
636
        compiler.options.resolve ?= {}
1✔
637
        // Default from https://rspack.dev/config/resolve#resolveextensions
1✔
638
        compiler.options.resolve.extensions ?= ['', '.js', '.json', '.wasm']
1✔
639
        compiler.options.resolve.extensions.unshift ".civet"
1✔
640
    webpack(compiler)
3✔
641
      if implicitExtension
3✔
642
        compiler.options.resolve ?= {}
3✔
643
        // Default from https://webpack.js.org/configuration/resolve/#resolveextensions
3✔
644
        compiler.options.resolve.extensions ?= ['', '.js', '.json', '.wasm']
3✔
645
        compiler.options.resolve.extensions.unshift ".civet"
3✔
646
      aliasResolver = (id) =>
3✔
647
        // Based on normalizeAlias from
2✔
648
        // https://github.com/webpack/enhanced-resolve/blob/72999caf002f6f7bb4624e65fdeb7ba980b11e24/lib/ResolverFactory.js#L158
2✔
649
        // and AliasPlugin from
2✔
650
        // https://github.com/webpack/enhanced-resolve/blob/72999caf002f6f7bb4624e65fdeb7ba980b11e24/lib/AliasPlugin.js
2✔
651
        // @ts-expect-error TODO
2✔
652
        for key, value in compiler.options.resolve.alias
2✔
653
          if key.endsWith '$'
2✔
654
            if id is key[...-1]
1✔
655
              return value <? 'string' ? value : '\0'
1!
656
          else
1✔
657
            if id is key or id.startsWith key + '/'
1✔
658
              return '\0' unless value <? 'string'
×
659
              return value + id[key.length..]
×
660
        id
1✔
661
  } satisfies UnpluginOptions
47✔
662

1✔
663
var unplugin = createUnplugin(rawPlugin)
1✔
664
export default unplugin
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc