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

DanielXMoore / Civet / 24100536734

07 Apr 2026 07:33PM UTC coverage: 95.354% (+0.6%) from 94.751%
24100536734

Pull #1921

github

web-flow
Merge 823e2138b into 059d8bfb5
Pull Request #1921: Increase CLI and unplugin test coverage

4239 of 4622 branches covered (91.71%)

Branch coverage included in aggregate %.

32 of 40 new or added lines in 2 files covered. (80.0%)

60 existing lines in 1 file now uncovered.

20310 of 21123 relevant lines covered (96.15%)

46593.96 hits per line

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

82.87
/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?, tsConfigPath: string | undefined
2✔
189

2✔
190
        if options.tsConfig
2✔
191
          config = options.tsConfig
1✔
192
          if options.tsConfig.rootDir
1✔
193
            tsConfigPath = path.join options.tsConfig.rootDir, 'tsconfig.json'
1✔
194
        else
1✔
195
          tsConfigPath = ts.findConfigFile process.cwd(), ts.sys.fileExists
1✔
196

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

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

2✔
204
        if error
2✔
205
          console.error ts.formatDiagnostic error, getFormatHost ts.sys
×
206
          throw error
×
207

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

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

2✔
225
        configContents := ts.parseJsonConfigFileContent
2✔
226
          config
2✔
227
          system
2✔
NEW
228
          if tsConfigPath then path.dirname(tsConfigPath) else process.cwd()
×
229
          undefined
2✔
230
          tsConfigPath
2✔
231

2✔
232
        configErrors = configContents.errors
2✔
233
        configFileNames = configContents.fileNames
2✔
234

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

47✔
249
    buildEnd(useConfigFileNames = false): Promise<void>
2✔
250
      if transformTS
2✔
251
        const ts = await tsPromise!
2✔
252

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

2✔
263
        system.fileExists = (filename: string): boolean =>
2✔
264
          if (!filename.endsWith('.civet.tsx')) return systemFileExists(filename)
1,026✔
265
          if (fsMap.has(filename)) return true
×
266
          return systemFileExists filename[...-4]
×
267

2✔
268
        system.readDirectory = (path: string): string[] =>
2✔
269
          systemReadDirectory(path)
×
270
          .map &.endsWith('.civet') ? & + '.tsx' : &
×
271

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

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

2✔
313
        host := createVirtualCompilerHost
2✔
314
          system
2✔
315
          compilerOptions
2✔
316
          ts
2✔
317

2✔
318
        program := ts.createProgram
2✔
319
          rootNames: useConfigFileNames ? configFileNames : [...fsMap.keys()]
2!
320
          options: compilerOptions
2✔
321
          host: host.compilerHost
2✔
322

2✔
323
        diagnostics: Diagnostic[] := ts
2✔
324
          .getPreEmitDiagnostics(program)
2✔
325
          .map (diagnostic) =>
2✔
326
            file := diagnostic.file
2✔
327
            if (!file) return diagnostic
2✔
328

1✔
329
            sourceMap := sourceMaps.get file.fileName
1✔
330
            if (!sourceMap) return diagnostic
2!
331

1✔
332
            sourcemapLines := sourceMap.lines ?? sourceMap.data.lines
2!
333
            range := remapRange(
2✔
334
              {
2✔
335
                start: diagnostic.start || 0,
2!
336
                end: (diagnostic.start || 0) + (diagnostic.length || 1),
2!
337
              },
2✔
338
              sourcemapLines
2✔
339
            )
2✔
340

2✔
341
            {
2✔
342
              ...diagnostic,
2✔
343
              messageText: flattenDiagnosticMessageText(diagnostic.messageText),
2✔
344
              length: diagnostic.length,
2✔
345
              start: range.start,
2✔
346
            }
2✔
347

2✔
348
        if configErrors?#
2✔
349
          diagnostics.unshift ...configErrors
×
350

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

1✔
374
        if options.emitDeclaration
1✔
375
          if meta.framework is 'esbuild' and not esbuildOptions.outdir
1✔
376
            throw new Error "Civet unplugin's `emitDeclaration` requires esbuild's `outdir` option to be set;"
×
377

1✔
378
          // Removed duplicate slashed (`\`) versions of the same file for emit
1✔
379
          for file of fsMap.keys()
1✔
380
            slashed := slash file
1✔
381
            unless file is slashed
1✔
382
              fsMap.delete slashed
×
383

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

1✔
400
                pathFromDistDir .= path.relative
1✔
401
                  compilerOptions.outDir ?? process.cwd()
1!
402
                  filePath
1✔
403

1✔
404
                @emitFile
1✔
405
                  source: content
1✔
406
                  fileName: pathFromDistDir
1✔
407
                  type: 'asset'
1✔
408
              undefined
1✔
409
              true // emitDtsOnly
1✔
410
              undefined
1✔
411
              // @ts-ignore @internal interface
1✔
412
              true // forceDtsEmit
47✔
413

47✔
414
    resolveId(id, importer, options)
9✔
415
      id = aliasResolver id if aliasResolver?
9✔
416
      if (/\0/.test(id)) return null
9✔
417

8✔
418
      // Remove query/hash postfix to get actual path
8✔
419
      {filename, postfix} := extractCivetFilename id, outExt
8✔
420

8✔
421
      resolved .=
8✔
422
        if path.isAbsolute filename
8✔
423
          resolveAbsolutePath rootDir, filename, implicitExtension
5✔
424
        else
3✔
425
          path.resolve path.dirname(importer ?? ''), filename
3✔
426
      if (!resolved) return null
9✔
427

6✔
428
      // Implicit .civet extension
6✔
429
      unless resolved.endsWith civetSuffix
6✔
430
        if (!implicitExtension) return null
3✔
431
        implicitId := implicitCivet resolved
2✔
432
        if (!implicitId) return null
2✔
433
        resolved = implicitId
×
434

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

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

47✔
447
    loadInclude(id)
13✔
448
      {filename, postfix} := extractCivetFilename id, outExt
13✔
449
      return false if skipWorker and workerRE.test postfix
13!
450
      filename.endsWith civetSuffix
13✔
451

47✔
452
    load(id)
16✔
453
      {filename} .= extractCivetFilename id, outExt
16✔
454
      // We're guaranteed to have .civet extension here by loadInclude
16✔
455

16✔
456
      filename = path.resolve rootDir, filename
16✔
457
      @addWatchFile filename
16✔
458

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

15✔
476
      let compiled: string
15✔
477
      let sourceMap: SourceMap | string | undefined
15✔
478
      civetOptions := {
15✔
479
        ...compileOptions
15✔
480
        filename
15✔
481
        errors: []
15✔
482
      }
15✔
483
      function checkErrors
15✔
484
        if civetOptions.errors#
16✔
485
          throw new civet.ParseErrors civetOptions.errors
×
486

15✔
487
      rawCivetSource := decode await fs.promises.readFile filename
15✔
488
      ast := await civet.compile rawCivetSource, {
15✔
489
        ...civetOptions
15✔
490
        ast: true
15✔
491
      }
15✔
492
      civetSourceMap := new SourceMap rawCivetSource,
14✔
493
        normalizePath path.relative outDir, filename
14✔
494

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

6✔
511
        switch ts
6✔
512
          when "esbuild"
6✔
513
            esbuildTransform := import("esbuild") |> await |> .transform
3✔
514
            result := await esbuildTransform compiledTS,
3✔
515
              jsx: "preserve"
3✔
516
              loader: "tsx"
3✔
517
              sourcefile: filename
3✔
518
              sourcemap: "external"
3✔
519

3✔
520
            compiled = result.code
3✔
521
            sourceMap = result.map
3✔
522
          when "tsc"
6✔
523
            tsTranspile := tsPromise! |> await |> .transpileModule
2✔
524
            result := tsTranspile compiledTS,
2✔
525
              compilerOptions: compilerOptionsWithSourceMap
2✔
526

2✔
527
            compiled = result.outputText
2✔
528
            sourceMap = result.sourceMapText
2✔
529
          when "preserve"
6✔
530
            compiled = compiledTS
1✔
531
            sourceMap = civetSourceMap
1✔
532

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

×
545
        compiledTS := await civet.generate ast, {
×
546
          ...civetOptions
×
547
          js: false
×
548
          sourceMap: civetSourceMap
×
549
        }
×
550
        checkErrors()
2✔
551

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

14✔
564
      jsonSourceMap := sourceMap and
14✔
565
        if sourceMap <? "string"
5✔
566
          JSON.parse(sourceMap)
5✔
567
        else
9✔
568
          sourceMap.json normalizePath path.relative outDir, id
9✔
569

16✔
570
      transformed: TransformResult .=
16✔
571
        code: compiled
16✔
572
        map: jsonSourceMap
16✔
573

16✔
574
      if options.transformOutput
16✔
575
        transformed = await options.transformOutput transformed.code, id
2✔
576

14✔
577
      if cached?
14✔
578
        cached.result = transformed
12✔
579
        delete cached.promise
12✔
580

14✔
581
      return transformed
16✔
582

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

2✔
600
        rootDir = path.resolve process.cwd(), config.root ?? ''
2!
601
        outDir = path.resolve rootDir, config.build?.outDir ?? 'dist'
2✔
602

2✔
603
        if implicitExtension
2✔
604
          config.resolve ??= {}
2✔
605
          config.resolve.extensions ??= DEFAULT_EXTENSIONS
2✔
606
          config.resolve.extensions.push '.civet'
2✔
607

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

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

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

1✔
668
var unplugin = createUnplugin(rawPlugin)
1✔
669
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