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

DanielXMoore / Civet / 24004194159

05 Apr 2026 03:06PM UTC coverage: 94.459% (+3.0%) from 91.452%
24004194159

Pull #1885

github

web-flow
Merge 1e29fc641 into 45ccf153c
Pull Request #1885: Fix computed index side effects evaluated twice in compound assignment expressions

6089 of 6456 branches covered (94.32%)

Branch coverage included in aggregate %.

29 of 29 new or added lines in 1 file covered. (100.0%)

271 existing lines in 6 files now uncovered.

25365 of 26843 relevant lines covered (94.49%)

37112.74 hits per line

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

30.82
/source/unplugin/unplugin.civet
1
import { type TransformResult, createUnplugin } 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

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

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

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

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

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

1✔
103
function normalizePath(id: string): string
×
UNCOV
104
  path.posix.normalize isWindows ? slash(id) : id
×
105

1✔
106
function tryFsResolve(file: string): string?
2✔
107
  fileStat := tryStatSync file
2✔
108
  if fileStat?.isFile()
2!
UNCOV
109
    normalizePath file
×
110

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

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

1✔
126
export const rawPlugin: Parameters<typeof createUnplugin<PluginOptions>>[0] =
1✔
127
(options: PluginOptions = {}, meta) =>
28✔
128
  if (options.dts) options.emitDeclaration = options.dts
28✔
129
  compileOptions: CompileOptions .= {}
28✔
130

28✔
131
  ts .= options.ts
28✔
132
  if (options.js) ts = 'civet'
28✔
133
  unless ts?
28✔
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"]
28✔
137
    console.log `WARNING: Invalid option ts: ${JSON.stringify ts}; switching to "civet"`
1✔
138
    ts = "civet"
1✔
139

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

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

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

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

28✔
169
  plugin: ReturnType<typeof rawPlugin> := {
28✔
170
    name: 'unplugin-civet'
28✔
171
    enforce: 'pre'
28✔
172

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

×
186
      if transformTS or ts is "tsc"
×
187
        ts := await tsPromise!
×
188

×
189
        tsConfigPath := ts.findConfigFile process.cwd(), ts.sys.fileExists
×
190

×
191
        unless tsConfigPath
×
192
          throw new Error "Could not find 'tsconfig.json'"
×
193

×
194
        { config, error } := ts.readConfigFile
×
195
          tsConfigPath
×
196
          ts.sys.readFile
×
197

×
198
        if error
×
199
          console.error ts.formatDiagnostic error, getFormatHost ts.sys
×
200
          throw error
×
201

×
202
        // Mogrify tsconfig.json "files" field to use .civet.tsx
×
203
        function mogrify(key: string)
×
204
          if key in config and Array.isArray config[key]
×
205
            config[key] = config[key].map (item: unknown) =>
×
206
              return item unless item <? "string"
×
207
              return item.replace(/\.civet\b(?!\.)/g, '.civet.tsx')
×
208
        mogrify "files"
×
209

×
210
        // Override readDirectory (used for include/exclude matching)
×
211
        // to include .civet files, as .civet.tsx files
×
212
        system := {...ts.sys}
×
213
        {readDirectory: systemReadDirectory} := system
×
214
        system.readDirectory = (path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] =>
×
215
          extensions = [ ...(extensions ?? []), ".civet" ]
×
216
          systemReadDirectory(path, extensions, excludes, includes, depth)
×
217
          .map &.endsWith(".civet") ? & + ".tsx" : &
×
218

×
219
        configContents := ts.parseJsonConfigFileContent
×
220
          config
×
221
          system
×
222
          process.cwd()
×
223
        configErrors = configContents.errors
×
224
        configFileNames = configContents.fileNames
×
225

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

28✔
240
    async buildEnd(useConfigFileNames = false): Promise<void>
28✔
241
      if transformTS
×
242
        const ts = await tsPromise!
×
243

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

×
254
        system.fileExists = (filename: string): boolean =>
×
255
          if (!filename.endsWith('.civet.tsx')) return systemFileExists(filename)
×
256
          if (fsMap.has(filename)) return true
×
257
          return systemFileExists filename[...-4]
×
258

×
259
        system.readDirectory = (path: string): string[] =>
×
260
          systemReadDirectory(path)
×
261
          .map &.endsWith('.civet') ? & + '.tsx' : &
×
262

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

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

×
304
        host := createVirtualCompilerHost
×
305
          system
×
306
          compilerOptions
×
307
          ts
×
308

×
309
        program := ts.createProgram
×
310
          rootNames: useConfigFileNames ? configFileNames : [...fsMap.keys()]
×
311
          options: compilerOptions
×
312
          host: host.compilerHost
×
313

×
314
        diagnostics: Diagnostic[] := ts
×
315
          .getPreEmitDiagnostics(program)
×
316
          .map (diagnostic) =>
×
317
            file := diagnostic.file
×
318
            if (!file) return diagnostic
×
319

×
320
            sourceMap := sourceMaps.get file.fileName
×
321
            if (!sourceMap) return diagnostic
×
322

×
323
            sourcemapLines := sourceMap.lines ?? sourceMap.data.lines
×
324
            range := remapRange(
×
325
              {
×
326
                start: diagnostic.start || 0,
×
327
                end: (diagnostic.start || 0) + (diagnostic.length || 1),
×
328
              },
×
329
              sourcemapLines
×
330
            )
×
331

×
332
            {
×
333
              ...diagnostic,
×
334
              messageText: flattenDiagnosticMessageText(diagnostic.messageText),
×
335
              length: diagnostic.length,
×
336
              start: range.start,
×
337
            }
×
338

×
339
        if configErrors?#
×
340
          diagnostics.unshift ...configErrors
×
341

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

×
365
        if options.emitDeclaration
×
366
          if meta.framework is 'esbuild' and not esbuildOptions.outdir
×
367
            throw new Error "Civet unplugin's `emitDeclaration` requires esbuild's `outdir` option to be set;"
×
368

×
369
          // Removed duplicate slashed (`\`) versions of the same file for emit
×
370
          for file of fsMap.keys()
×
371
            slashed := slash file
×
372
            unless file is slashed
×
373
              fsMap.delete slashed
×
374

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

×
391
                pathFromDistDir .= path.relative
×
392
                  compilerOptions.outDir ?? process.cwd()
×
393
                  filePath
×
394

×
395
                this.emitFile
×
396
                  source: content
×
397
                  fileName: pathFromDistDir
×
398
                  type: 'asset'
×
399
              undefined
×
400
              true // emitDtsOnly
×
401
              undefined
×
402
              // @ts-ignore @internal interface
×
403
              true // forceDtsEmit
28✔
404

28✔
405
    resolveId(id, importer, options)
3✔
406
      id = aliasResolver id if aliasResolver?
3!
407
      if (/\0/.test(id)) return null
3✔
408

2✔
409
      // Remove query/hash postfix to get actual path
2✔
410
      {filename, postfix} := extractCivetFilename id, outExt
2✔
411

2✔
412
      resolved .=
2✔
413
        if path.isAbsolute filename
2✔
414
          resolveAbsolutePath rootDir, filename, implicitExtension
×
415
        else
2✔
416
          path.resolve path.dirname(importer ?? ''), filename
2✔
417
      if (!resolved) return null
3!
418

2✔
419
      // Implicit .civet extension
2✔
420
      unless resolved.endsWith civetSuffix
2✔
421
        if (!implicitExtension) return null
2✔
422
        implicitId := implicitCivet resolved
1✔
423
        if (!implicitId) return null
1✔
424
        resolved = implicitId
×
425

×
426
      // Tell Vite that this is a virtual module during dependency scanning
×
427
      if (options as! {scan?: boolean}).scan and meta.framework is 'vite'
×
428
        resolved = `\0${resolved}`
×
429

×
430
      // Add back the original postfix at the end
×
431
      return resolved + outExt + postfix
×
432

28✔
433
    loadInclude(id)
12✔
434
      {filename, postfix} := extractCivetFilename id, outExt
12✔
435
      return false if skipWorker and workerRE.test postfix
12!
436
      filename.endsWith civetSuffix
12✔
437

28✔
438
    async load(id)
28✔
439
      {filename} .= extractCivetFilename id, outExt
×
440
      // We're guaranteed to have .civet extension here by loadInclude
×
441

×
442
      filename = path.resolve rootDir, filename
×
443
      @addWatchFile filename
×
444

×
445
      let mtime: number?, cached: CacheEntry?, resolve: =>?
×
446
      if cache?
×
447
        try
×
448
          mtime = fs.promises.stat(filename) |> await |> .mtimeMs
×
449
        // If we fail to stat file, ignore cache
×
450
        if mtime?
×
451
          cached = cache.get filename
×
452
          if cached and cached.mtime is mtime
×
453
            // If the file is currently being compiled, wait for it to finish
×
454
            await cached.promise if cached.promise
×
455
            if result? := cached.result
×
456
              return result
×
457
          // We're the first to compile this file with this mtime
×
458
          promise := new Promise<void> (r): void => resolve = r
×
459
          cache.set filename, cached = {mtime, promise}
×
460
      finally resolve?()
×
461

×
462
      let compiled: string
×
463
      let sourceMap: SourceMap | string | undefined
×
464
      civetOptions := {
×
465
        ...compileOptions
×
466
        filename: id
×
467
        errors: []
×
468
      }
×
469
      function checkErrors
×
470
        if civetOptions.errors#
×
471
          throw new civet.ParseErrors civetOptions.errors
×
472

×
473
      rawCivetSource := decode await fs.promises.readFile filename
×
474
      ast := await civet.compile rawCivetSource, {
×
475
        ...civetOptions
×
476
        ast: true
×
477
      }
×
478
      civetSourceMap := new SourceMap rawCivetSource,
×
479
        normalizePath path.relative outDir, filename
×
480

×
481
      if ts is "civet"
×
482
        compiled = await civet.generate ast, {
×
483
          ...civetOptions
×
484
          js: true
×
485
          sourceMap: civetSourceMap
×
486
        }
×
487
        sourceMap = civetSourceMap
×
488
        checkErrors()
×
489
      else
×
490
        compiledTS := await civet.generate ast, {
×
491
          ...civetOptions
×
492
          js: false
×
493
          sourceMap: civetSourceMap
×
494
        }
×
495
        checkErrors()
×
496

×
497
        switch ts
×
498
          when "esbuild"
×
499
            esbuildTransform := import("esbuild") |> await |> .transform
×
500
            result := await esbuildTransform compiledTS,
×
501
              jsx: "preserve"
×
502
              loader: "tsx"
×
503
              sourcefile: id
×
504
              sourcemap: "external"
×
505

×
506
            compiled = result.code
×
507
            sourceMap = result.map
×
508
          when "tsc"
×
509
            tsTranspile := tsPromise! |> await |> .transpileModule
×
510
            result := tsTranspile compiledTS,
×
511
              compilerOptions: compilerOptionsWithSourceMap
×
512

×
513
            compiled = result.outputText
×
514
            sourceMap = result.sourceMapText
×
515
          when "preserve"
×
516
            compiled = compiledTS
×
517
            sourceMap = civetSourceMap
×
518

×
519
      if transformTS
×
520
        // When working with TypeScript, disable rewriteCivetImports and
×
521
        // force rewriteTsImports by rewriting imports again.
×
522
        // See `ModuleSpecifier` in parser.hera
×
523
        for each _spec of lib.gatherRecursive ast, (
×
524
          ($) => ($ as {type: string}).type is "ModuleSpecifier"
×
525
        )
×
526
          spec := _spec as { module?: { token: string, input?: string } }
×
527
          if spec.module?.input
×
528
            spec.module.token = spec.module.input
×
529
            .replace /\.([mc])?ts(['"])$/, ".$1js$2"
×
530

×
531
        compiledTS := await civet.generate ast, {
×
532
          ...civetOptions
×
533
          js: false
×
534
          sourceMap: civetSourceMap
×
535
        }
×
536
        checkErrors()
×
537

×
538
        // Force .tsx extension for type checking purposes.
×
539
        // Otherwise, TypeScript complains about types in .jsx files.
×
540
        tsx := filename + '.tsx'
×
541
        fsMap.set tsx, compiledTS
×
542
        sourceMaps.set tsx, civetSourceMap
×
543
        // Vite and Rollup normalize filenames to use `/` instead of `\`.
×
544
        // We give the TypeScript VFS both versions just in case.
×
545
        slashed := slash tsx
×
546
        unless tsx is slashed
×
547
          fsMap.set slashed, compiledTS
×
548
          sourceMaps.set slashed, civetSourceMap
×
549

×
550
      jsonSourceMap := sourceMap and
×
551
        if sourceMap <? "string"
×
552
          JSON.parse(sourceMap)
×
553
        else
×
554
          sourceMap.json normalizePath path.relative outDir, id
×
555

×
556
      transformed: TransformResult .=
×
557
        code: compiled
×
558
        map: jsonSourceMap
×
559

×
560
      if options.transformOutput
×
561
        transformed = await options.transformOutput transformed.code, id
×
562

×
563
      if cached?
×
564
        cached.result = transformed
×
565
        delete cached.promise
×
566

×
567
      return transformed
×
568

28✔
569
    esbuild: {
28✔
570
      config(options: BuildOptions): void
×
571
        esbuildOptions = options
×
572
        outDir = options.outdir or path.dirname options.outfile!
×
573
    }
28✔
574
    vite: {
28✔
575
      config(config: UserConfig): void
×
576
        // Vite 7 requires us to skip processing ?worker
×
577
        // and instead process a followup request for ?worker_file
×
578
        // Luckily, Vite 7 introduced @meta.viteVersion for detection
×
579
        //@ts-ignore lacking type for this
×
580
        if @?meta?viteVersion and 7 <= Number @meta.viteVersion.split('.')[0]
×
581
          skipWorker = true
×
582

×
583
        rootDir = path.resolve process.cwd(), config.root ?? ''
×
584
        outDir = path.resolve rootDir, config.build?.outDir ?? 'dist'
×
585

×
586
        if implicitExtension
×
587
          config.resolve ??= {}
×
588
          config.resolve.extensions ??= DEFAULT_EXTENSIONS
×
589
          config.resolve.extensions.push '.civet'
×
590
      async transformIndexHtml(html)
28✔
591
        html.replace /<!--[^]*?-->|<[^<>]*>/g, (tag) =>
×
592
          tag.replace /<\s*script\b[^<>]*>/gi, (script) =>
×
593
            // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
×
594
            script.replace
×
595
              /([:_\p{ID_Start}][:\p{ID_Continue}]*)(\s*=\s*("[^"]*"|'[^']*'|[^\s"'=<>`]*))?/gu
×
596
              (attr, name, value) =>
×
597
                name.toLowerCase() === 'src' && value
×
598
                  ? attr.replace(
×
599
                      /(\.civet)(['"]?)$/,
×
600
                      (_, extension, endQuote) =>
×
601
                        `${extension}${outExt}?transform${endQuote}`
×
602
                    )
×
603
                  : attr
×
604
      handleHotUpdate({ file, server, modules })
×
605
        // `file` is an absolute path to the changed file on disk,
×
606
        // so for our case it should end with .civet extension
×
607
        return unless file.endsWith '.civet'
×
608
        // Convert into path as would be output by `resolveId`
×
609
        resolvedId := slash path.resolve(file) + outExt
×
610
        // Check for modules for this file
×
611
        if fileModules := server.moduleGraph.getModulesByFile resolvedId
×
612
          // Invalidate modules depending on this one
×
613
          server.moduleGraph.onFileChange resolvedId
×
614
          // Hot reload this module
×
615
          return [ ...modules, ...fileModules ]
×
616
        modules
×
617
    }
28✔
618

28✔
619
    rspack(compiler)
×
620
      if implicitExtension
×
621
        compiler.options ?= {}
×
622
        compiler.options.resolve ?= {}
×
623
        // Default from https://rspack.dev/config/resolve#resolveextensions
×
624
        compiler.options.resolve.extensions ?= ['', '.js', '.json', '.wasm']
×
625
        compiler.options.resolve.extensions.unshift ".civet"
×
626
    webpack(compiler)
×
627
      if implicitExtension
×
628
        compiler.options.resolve ?= {}
×
629
        // Default from https://webpack.js.org/configuration/resolve/#resolveextensions
×
630
        compiler.options.resolve.extensions ?= ['', '.js', '.json', '.wasm']
×
631
        compiler.options.resolve.extensions.unshift ".civet"
×
632
      aliasResolver = (id) =>
×
633
        // Based on normalizeAlias from
×
634
        // https://github.com/webpack/enhanced-resolve/blob/72999caf002f6f7bb4624e65fdeb7ba980b11e24/lib/ResolverFactory.js#L158
×
635
        // and AliasPlugin from
×
636
        // https://github.com/webpack/enhanced-resolve/blob/72999caf002f6f7bb4624e65fdeb7ba980b11e24/lib/AliasPlugin.js
×
637
        // @ts-expect-error TODO
×
638
        for key, value in compiler.options.resolve.alias
×
639
          if key.endsWith '$'
×
640
            if id is key[...-1]
×
641
              return value <? 'string' ? value : '\0'
×
642
          else
×
643
            if id is key or id.startsWith key + '/'
×
644
              return '\0' unless value <? 'string'
×
645
              return value + id[key.length..]
×
646
        id
×
647
  }
28✔
648

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