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

DanielXMoore / Civet / 14698364670

28 Apr 2025 01:32AM UTC coverage: 91.652% (+0.01%) from 91.64%
14698364670

Pull #1734

github

web-flow
Merge 947987742 into 0039cdd9c
Pull Request #1734: ESM loader supports civetconfig by default, and overriding configuration

3644 of 3963 branches covered (91.95%)

Branch coverage included in aggregate %.

49 of 54 new or added lines in 3 files covered. (90.74%)

1 existing line in 1 file now uncovered.

18809 of 20535 relevant lines covered (91.59%)

16287.38 hits per line

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

12.16
/source/unplugin/unplugin.civet
1
import { type TransformResult, createUnplugin } from 'unplugin'
1✔
2
import civet, { 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 * as tsvfs 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
import { DEFAULT_EXTENSIONS } from './constants.mjs'
1✔
19

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

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

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

1✔
55
civetExtension := /\.civet$/
1✔
56
// Normally .jsx/.tsx extension should be present, but sometimes
1✔
57
// (e.g. esbuild's alias feature) loads directly without resolve
1✔
58
isCivetTranspiled := /(\.civet)(\.[jt]sx)?([?#].*)?$/
1✔
59
postfixRE := /[?#].*$/s
1✔
60
isWindows := os.platform() is 'win32'
1✔
61
windowsSlashRE := /\\/g
1✔
62

1✔
63
// Extracts query string and hash, and removes tsx/jsx extension
1✔
64
function cleanCivetId(id: string): {id: string, postfix: string}
×
65
  let postfix = ''
×
66
  id = id
×
67
  .replace postfixRE, (match) =>
×
68
    postfix = match
×
69
    ''
×
70
  .replace /\.[jt]sx$/, ''
×
71
  {id, postfix}
×
72

1✔
73
function tryStatSync(file: string): fs.Stats?
×
74
  try
×
75
    // The "throwIfNoEntry" is a performance optimization for cases where the file does not exist
×
76
    return fs.statSync(file, { throwIfNoEntry: false });
×
77

1✔
78
export function slash(p: string): string
1✔
79
  p.replace windowsSlashRE, '/'
×
80

1✔
81
function normalizePath(id: string): string
×
82
  path.posix.normalize isWindows ? slash(id) : id
×
83

1✔
84
function tryFsResolve(file: string): string?
×
85
  fileStat := tryStatSync file
×
86
  if fileStat?.isFile()
×
87
    normalizePath file
×
88

1✔
89
function resolveAbsolutePath(rootDir: string, id: string, implicitExtension: boolean)
×
90
  file := path.join rootDir, id
×
91
  // Check for existence of resolved file and unresolved id,
×
92
  // without and with implicit .civet extension, and return first existing
×
93
  (or)
×
94
    tryFsResolve(file)
×
95
    implicitExtension and implicitCivet file
×
96
    tryFsResolve id
×
97
    implicitExtension and implicitCivet id
×
98

1✔
99
function implicitCivet(file: string): string?
×
100
  return if tryFsResolve file
×
101
  civet := file + '.civet'
×
102
  return civet if tryFsResolve civet
×
103

1✔
104
export const rawPlugin: Parameters<typeof createUnplugin<PluginOptions>>[0] =
1✔
105
(options: PluginOptions = {}, meta) =>
×
106
  if (options.dts) options.emitDeclaration = options.dts
×
107
  compileOptions: CompileOptions .= {}
×
108

×
109
  ts .= options.ts
×
110
  if (options.js) ts = 'civet'
×
111
  unless ts?
×
112
    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.'
×
113
    ts = "civet"
×
114
  unless ts is in ["civet", "esbuild", "tsc", "preserve"]
×
115
    console.log `WARNING: Invalid option ts: ${JSON.stringify ts}; switching to "civet"`
×
116
    ts = "civet"
×
117

×
118
  transformTS := options.emitDeclaration or options.typecheck
×
119
  outExt :=
×
120
    options.outputExtension ?? (ts is "preserve" ? ".tsx" : ".jsx")
×
121
  implicitExtension := options.implicitExtension ?? true
×
122
  let aliasResolver: (id: string) => string
×
123

×
124
  fsMap: Map<string, string> .= new Map
×
125
  sourceMaps := new Map<string, SourceMap>
×
126
  let compilerOptions: any, compilerOptionsWithSourceMap: any
×
127
  rootDir .= process.cwd()
×
128
  let esbuildOptions: BuildOptions
×
129
  let configErrors: Diagnostic[]?
×
130
  let configFileNames: string[]
×
131

×
132
  tsPromise := if transformTS or ts is "tsc"
×
133
    import('typescript').then .default
×
134
  getFormatHost := (sys: System): FormatDiagnosticsHost =>
×
135
    return {
×
136
      getCurrentDirectory: => sys.getCurrentDirectory()
×
137
      getNewLine: => sys.newLine
×
138
      getCanonicalFileName: sys.useCaseSensitiveFileNames
×
139
        ? (f) => f
×
140
        : (f) => f.toLowerCase()
×
141
    }
×
142

×
143
  cache := new Map<string, CacheEntry> unless options.cache is false
×
144

×
145
  plugin: ReturnType<typeof rawPlugin> := {
×
146
    name: 'unplugin-civet'
×
147
    enforce: 'pre'
×
148

×
149
    async buildStart(): Promise<void>
×
NEW
150
      civetConfigPath .= options.config
×
NEW
151
      if civetConfigPath is undefined
×
NEW
152
        civetConfigPath = await findInDir process.cwd()
×
153
      if civetConfigPath
×
154
        compileOptions = await loadConfig civetConfigPath
×
155
      // Merge parseOptions, with plugin options taking priority
×
156
      compileOptions.parseOptions = {
×
157
        ...compileOptions.parseOptions
×
158
        ...options.parseOptions
×
159
      }
×
160
      compileOptions.threads = options.threads if options.threads?
×
161

×
162
      if transformTS or ts is "tsc"
×
163
        ts := await tsPromise!
×
164

×
165
        tsConfigPath := ts.findConfigFile process.cwd(), ts.sys.fileExists
×
166

×
167
        unless tsConfigPath
×
168
          throw new Error "Could not find 'tsconfig.json'"
×
169

×
170
        { config, error } := ts.readConfigFile
×
171
          tsConfigPath
×
172
          ts.sys.readFile
×
173

×
174
        if error
×
175
          console.error ts.formatDiagnostic error, getFormatHost ts.sys
×
176
          throw error
×
177

×
178
        // Mogrify tsconfig.json "files" field to use .civet.tsx
×
179
        function mogrify(key: string)
×
180
          if key in config and Array.isArray config[key]
×
181
            config[key] = config[key].map (item: unknown) =>
×
182
              return item unless item <? "string"
×
183
              return item.replace(/\.civet\b(?!\.)/g, '.civet.tsx')
×
184
        mogrify "files"
×
185

×
186
        // Override readDirectory (used for include/exclude matching)
×
187
        // to include .civet files, as .civet.tsx files
×
188
        system := {...ts.sys}
×
189
        {readDirectory: systemReadDirectory} := system
×
190
        system.readDirectory = (path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] =>
×
191
          extensions = [ ...(extensions ?? []), ".civet" ]
×
192
          systemReadDirectory(path, extensions, excludes, includes, depth)
×
193
          .map &.endsWith(".civet") ? & + ".tsx" : &
×
194

×
195
        configContents := ts.parseJsonConfigFileContent
×
196
          config
×
197
          system
×
198
          process.cwd()
×
199
        configErrors = configContents.errors
×
200
        configFileNames = configContents.fileNames
×
201

×
202
        compilerOptions = {
×
203
          ...configContents.options
×
204
          target: ts.ScriptTarget.ESNext
×
205
          composite: false
×
206
        }
×
207
        // We use .tsx extensions when type checking, so need to enable
×
208
        // JSX mode even if the user doesn't request/use it.
×
209
        compilerOptions.jsx ??= ts.JsxEmit.Preserve
×
210
        compilerOptionsWithSourceMap = {
×
211
          ...compilerOptions
×
212
          sourceMap: true
×
213
        }
×
214
        fsMap = new Map()
×
215

×
216
    async buildEnd(useConfigFileNames = false): Promise<void>
×
217
      if transformTS
×
218
        const ts = await tsPromise!
×
219

×
220
        // Create a virtual file system with all source files processed so far,
×
221
        // but which further resolves any Civet dependencies that are needed
×
222
        // just for typechecking (e.g. `import type` which get removed in JS).
×
223
        system := tsvfs.createFSBackedSystem fsMap, process.cwd(), ts
×
224
        {
×
225
          fileExists: systemFileExists
×
226
          readFile: systemReadFile
×
227
          readDirectory: systemReadDirectory
×
228
        } := system
×
229

×
230
        system.fileExists = (filename: string): boolean =>
×
231
          if (!filename.endsWith('.civet.tsx')) return systemFileExists(filename)
×
232
          if (fsMap.has(filename)) return true
×
233
          return systemFileExists filename[...-4]
×
234

×
235
        system.readDirectory = (path: string): string[] =>
×
236
          systemReadDirectory(path)
×
237
          .map &.endsWith('.civet') ? & + '.tsx' : &
×
238

×
239
        tsCompileOptions := {
×
240
          ...compileOptions
×
241
          rewriteCivetImports: false
×
242
          rewriteTsImports: true
×
243
        }
×
244
        system.readFile = (filename: string, encoding = 'utf-8'): string? =>
×
245
          // Mogrify package.json imports field to use .civet.tsx
×
246
          if path.basename(filename) is "package.json"
×
247
            json := systemReadFile filename, encoding
×
248
            return json unless json
×
249
            parsed: Record<string, unknown> := JSON.parse(json)
×
250
            modified .= false
×
251
            function recurse(node: unknown): void
×
252
              if node? <? "object"
×
253
                for key in node
×
254
                  value := (node as Record<string, unknown>)[key]
×
255
                  if value <? "string"
×
256
                    if value.endsWith ".civet"
×
257
                      (node as Record<string, unknown>)[key] = value + '.tsx'
×
258
                      modified = true
×
259
                  else if value
×
260
                    recurse value
×
261
            recurse parsed.imports
×
262
            return modified ? JSON.stringify(parsed) : json
×
263

×
264
          // Generate .civet.tsx files on the fly
×
265
          if (!filename.endsWith('.civet.tsx')) return systemReadFile(filename, encoding)
×
266
          if (fsMap.has(filename)) return fsMap.get(filename)
×
267
          civetFilename := filename[...-4]
×
268
          rawCivetSource := fs.readFileSync civetFilename,
×
269
            encoding: encoding as BufferEncoding
×
270
          { code: compiledTS, sourceMap } := civet.compile rawCivetSource, {
×
271
            ...tsCompileOptions
×
272
            filename
×
273
            js: false
×
274
            sourceMap: true
×
275
            sync: true // TS readFile API seems to need to be synchronous
×
276
          }
×
277
          fsMap.set filename, compiledTS
×
278
          sourceMaps.set filename, sourceMap
×
279
          return compiledTS
×
280

×
281
        host := tsvfs.createVirtualCompilerHost
×
282
          system
×
283
          compilerOptions
×
284
          ts
×
285

×
286
        program := ts.createProgram
×
287
          rootNames: useConfigFileNames ? configFileNames : [...fsMap.keys()]
×
288
          options: compilerOptions
×
289
          host: host.compilerHost
×
290

×
291
        diagnostics: Diagnostic[] := ts
×
292
          .getPreEmitDiagnostics(program)
×
293
          .map (diagnostic) =>
×
294
            file := diagnostic.file
×
295
            if (!file) return diagnostic
×
296

×
297
            sourceMap := sourceMaps.get file.fileName
×
298
            if (!sourceMap) return diagnostic
×
299

×
300
            sourcemapLines := sourceMap.lines ?? sourceMap.data.lines
×
301
            range := remapRange(
×
302
              {
×
303
                start: diagnostic.start || 0,
×
304
                end: (diagnostic.start || 0) + (diagnostic.length || 1),
×
305
              },
×
306
              sourcemapLines
×
307
            )
×
308

×
309
            {
×
310
              ...diagnostic,
×
311
              messageText: flattenDiagnosticMessageText(diagnostic.messageText),
×
312
              length: diagnostic.length,
×
313
              start: range.start,
×
314
            }
×
315

×
316
        if configErrors?#
×
317
          diagnostics.unshift ...configErrors
×
318

×
319
        if diagnostics# > 0
×
320
          console.error
×
321
            ts.formatDiagnosticsWithColorAndContext
×
322
              diagnostics
×
323
              getFormatHost ts.sys
×
324
          if options.typecheck
×
325
            failures: DiagnosticCategory[] .= []
×
326
            if options.typecheck <? "string"
×
327
              if (options.typecheck.includes('error')) failures.push(DiagnosticCategory.Error)
×
328
              if (options.typecheck.includes('warning')) failures.push(DiagnosticCategory.Warning)
×
329
              if (options.typecheck.includes('suggestion')) failures.push(DiagnosticCategory.Suggestion)
×
330
              if (options.typecheck.includes('message')) failures.push(DiagnosticCategory.Message)
×
331
              if (options.typecheck.includes('all'))
×
332
                failures = { includes: () => true } as any as DiagnosticCategory[]
×
333
            else
×
334
              // Default behavior: fail on errors
×
335
              failures.push(DiagnosticCategory.Error)
×
336
            count := diagnostics.filter((d) => failures.includes(d.category)).length
×
337
            if count
×
338
              reason :=
×
339
                (count is diagnostics# ? count : `${count} out of ${diagnostics#}`)
×
340
              throw new Error `Aborting build because of ${reason} TypeScript diagnostic${diagnostics.length > 1 ? 's' : ''} above`
×
341

×
342
        if options.emitDeclaration
×
343
          if meta.framework is 'esbuild' and not esbuildOptions.outdir
×
344
            console.log "WARNING: Civet unplugin's `emitDeclaration` requires esbuild's `outdir` option to be set;"
×
345

×
346
          // Removed duplicate slashed (`\`) versions of the same file for emit
×
347
          for file of fsMap.keys()
×
348
            slashed := slash file
×
349
            unless file is slashed
×
350
              fsMap.delete slashed
×
351

×
352
          for file of fsMap.keys()
×
353
            sourceFile := program.getSourceFile(file)!
×
354
            program.emit
×
355
              sourceFile
×
356
              (filePath, content) =>
×
357
                if options.declarationExtension?
×
358
                  if filePath.endsWith '.d.ts'
×
359
                    filePath = filePath[0...-5]
×
360
                  else
×
361
                    console.log `WARNING: No .d.ts extension in ${filePath}`
×
362
                  if match := filePath.match civetExtension
×
363
                    filePath = filePath[0...-match[0]#]
×
364
                  else
×
365
                    console.log `WARNING: No .civet extension in ${filePath}`
×
366
                  filePath += options.declarationExtension
×
367

×
368
                pathFromDistDir .= path.relative
×
369
                  compilerOptions.outDir ?? process.cwd()
×
370
                  filePath
×
371

×
372
                this.emitFile
×
373
                  source: content
×
374
                  fileName: pathFromDistDir
×
375
                  type: 'asset'
×
376
              undefined
×
377
              true // emitDtsOnly
×
378
              undefined
×
379
              // @ts-ignore @internal interface
×
380
              true // forceDtsEmit
×
381

×
382
    resolveId(id, importer, options)
×
383
      id = aliasResolver id if aliasResolver?
×
384
      if (/\0/.test(id)) return null
×
385

×
386
      let postfix: string
×
387
      {id, postfix} = cleanCivetId(id)
×
388
      resolvedId .=
×
389
        if path.isAbsolute id
×
390
          resolveAbsolutePath rootDir, id, implicitExtension
×
391
        else
×
392
          path.resolve path.dirname(importer ?? ''), id
×
393
      if (!resolvedId) return null
×
394

×
395
      // Implicit .civet extension
×
396
      unless resolvedId.endsWith '.civet'
×
397
        if (!implicitExtension) return null
×
398
        implicitId := implicitCivet resolvedId
×
399
        if (!implicitId) return null
×
400
        resolvedId = implicitId
×
401

×
402
      // Tell Vite that this is a virtual module during dependency scanning
×
403
      if (options as! {scan?: boolean}).scan and meta.framework is 'vite'
×
404
        resolvedId = `\0${resolvedId}`
×
405

×
406
      return resolvedId + outExt + postfix
×
407

×
408
    loadInclude(id)
×
409
      isCivetTranspiled.test id
×
410

×
411
    async load(id)
×
412
      match := isCivetTranspiled.exec id
×
413
      return null unless match
×
414
      basename := id[...match.index + match[1]#]
×
415

×
416
      filename := path.resolve rootDir, basename
×
417
      @addWatchFile filename
×
418

×
419
      let mtime: number?, cached: CacheEntry?, resolve: =>?
×
420
      if cache?
×
421
        try
×
422
          mtime = fs.promises.stat(filename) |> await |> .mtimeMs
×
423
        // If we fail to stat file, ignore cache
×
424
        if mtime?
×
425
          cached = cache.get filename
×
426
          if cached and cached.mtime is mtime
×
427
            // If the file is currently being compiled, wait for it to finish
×
428
            await cached.promise if cached.promise
×
429
            if result? := cached.result
×
430
              return result
×
431
          // We're the first to compile this file with this mtime
×
432
          promise := new Promise<void> (r): void => resolve = r
×
433
          cache.set filename, cached = {mtime, promise}
×
434
      finally resolve?()
×
435

×
436
      let compiled: string
×
437
      let sourceMap: SourceMap | string | undefined
×
438
      civetOptions := {
×
439
        ...compileOptions
×
440
        filename: id
×
441
        errors: []
×
442
      }
×
443
      function checkErrors
×
444
        if civetOptions.errors#
×
445
          throw new civet.ParseErrors civetOptions.errors
×
446

×
447
      rawCivetSource := await fs.promises.readFile filename, 'utf-8'
×
448
      ast := await civet.compile rawCivetSource, {
×
449
        ...civetOptions
×
450
        ast: true
×
451
      }
×
452
      civetSourceMap := new SourceMap rawCivetSource
×
453

×
454
      if ts is "civet"
×
455
        compiled = await civet.generate ast, {
×
456
          ...civetOptions
×
457
          js: true
×
458
          sourceMap: civetSourceMap
×
459
        }
×
460
        sourceMap = civetSourceMap
×
461
        checkErrors()
×
462
      else
×
463
        compiledTS := await civet.generate ast, {
×
464
          ...civetOptions
×
465
          js: false
×
466
          sourceMap: civetSourceMap
×
467
        }
×
468
        checkErrors()
×
469

×
470
        switch ts
×
471
          when "esbuild"
×
472
            esbuildTransform := import("esbuild") |> await |> .transform
×
473
            result := await esbuildTransform compiledTS,
×
474
              jsx: "preserve"
×
475
              loader: "tsx"
×
476
              sourcefile: id
×
477
              sourcemap: "external"
×
478

×
479
            compiled = result.code
×
480
            sourceMap = result.map
×
481
          when "tsc"
×
482
            tsTranspile := tsPromise! |> await |> .transpileModule
×
483
            result := tsTranspile compiledTS,
×
484
              compilerOptions: compilerOptionsWithSourceMap
×
485

×
486
            compiled = result.outputText
×
487
            sourceMap = result.sourceMapText
×
488
          when "preserve"
×
489
            compiled = compiledTS
×
490
            sourceMap = civetSourceMap
×
491

×
492
      if transformTS
×
493
        // When working with TypeScript, disable rewriteCivetImports and
×
494
        // force rewriteTsImports by rewriting imports again.
×
495
        // See `ModuleSpecifier` in parser.hera
×
496
        for each _spec of lib.gatherRecursive ast, (
×
497
          ($) => ($ as {type: string}).type is "ModuleSpecifier"
×
498
        )
×
499
          spec := _spec as { module?: { token: string, input?: string } }
×
500
          if spec.module?.input
×
501
            spec.module.token = spec.module.input
×
502
            .replace /\.([mc])?ts(['"])$/, ".$1js$2"
×
503

×
504
        compiledTS := await civet.generate ast, {
×
505
          ...civetOptions
×
506
          js: false
×
507
          sourceMap: civetSourceMap
×
508
        }
×
509
        checkErrors()
×
510

×
511
        // Force .tsx extension for type checking purposes.
×
512
        // Otherwise, TypeScript complains about types in .jsx files.
×
513
        tsx := filename + '.tsx'
×
514
        fsMap.set tsx, compiledTS
×
515
        sourceMaps.set tsx, civetSourceMap
×
516
        // Vite and Rollup normalize filenames to use `/` instead of `\`.
×
517
        // We give the TypeScript VFS both versions just in case.
×
518
        slashed := slash tsx
×
519
        unless tsx is slashed
×
520
          fsMap.set slashed, compiledTS
×
521
          sourceMaps.set slashed, civetSourceMap
×
522

×
523
      jsonSourceMap := sourceMap and
×
524
        if sourceMap <? "string"
×
525
          JSON.parse(sourceMap)
×
526
        else
×
527
          sourceMap.json
×
528
            path.relative rootDir, id.replace /\.[jt]sx$/, ''
×
529
            path.relative rootDir, id
×
530

×
531
      transformed: TransformResult .=
×
532
        code: compiled
×
533
        map: jsonSourceMap
×
534

×
535
      if options.transformOutput
×
536
        transformed = await options.transformOutput transformed.code, id
×
537

×
538
      if cached?
×
539
        cached.result = transformed
×
540
        delete cached.promise
×
541

×
542
      return transformed
×
543

×
544
    esbuild: {
×
545
      config(options: BuildOptions): void
×
546
        esbuildOptions = options
×
547
    }
×
548
    vite: {
×
549
      config(config: UserConfig): void
×
550
        rootDir = path.resolve process.cwd(), config.root ?? ''
×
551

×
552
        if implicitExtension
×
553
          config.resolve ??= {}
×
554
          config.resolve.extensions ??= DEFAULT_EXTENSIONS
×
555
          config.resolve.extensions.push '.civet'
×
556
      async transformIndexHtml(html)
×
557
        html.replace /<!--[^]*?-->|<[^<>]*>/g, (tag) =>
×
558
          tag.replace /<\s*script\b[^<>]*>/gi, (script) =>
×
559
            // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
×
560
            script.replace
×
561
              /([:_\p{ID_Start}][:\p{ID_Continue}]*)(\s*=\s*("[^"]*"|'[^']*'|[^\s"'=<>`]*))?/gu
×
562
              (attr, name, value) =>
×
563
                name.toLowerCase() === 'src' && value
×
564
                  ? attr.replace(
×
565
                      /(\.civet)(['"]?)$/,
×
566
                      (_, extension, endQuote) =>
×
567
                        `${extension}${outExt}?transform${endQuote}`
×
568
                    )
×
569
                  : attr
×
570
      handleHotUpdate({ file, server, modules })
×
571
        // `file` is an absolute path to the changed file on disk,
×
572
        // so for our case it should end with .civet extension
×
573
        return unless file.endsWith '.civet'
×
574
        // Convert into path as would be output by `resolveId`
×
575
        resolvedId := slash path.resolve(file) + outExt
×
576
        // Check for module with this name
×
577
        module := server.moduleGraph.getModuleById resolvedId
×
578
        if module
×
579
          // Invalidate modules depending on this one
×
580
          server.moduleGraph.onFileChange resolvedId
×
581
          // Hot reload this module
×
582
          return [ ...modules, module ]
×
583
        modules
×
584
    }
×
585

×
586
    rspack(compiler)
×
587
      if implicitExtension
×
588
        compiler.options ?= {}
×
589
        compiler.options.resolve ?= {}
×
590
        // Default from https://rspack.dev/config/resolve#resolveextensions
×
591
        compiler.options.resolve.extensions ?= ['', '.js', '.json', '.wasm']
×
592
        compiler.options.resolve.extensions.unshift ".civet"
×
593
    webpack(compiler)
×
594
      if implicitExtension
×
595
        compiler.options ?= {}
×
596
        compiler.options.resolve ?= {}
×
597
        // Default from https://webpack.js.org/configuration/resolve/#resolveextensions
×
598
        compiler.options.resolve.extensions ?= ['', '.js', '.json', '.wasm']
×
599
        compiler.options.resolve.extensions.unshift ".civet"
×
600
      aliasResolver = (id) =>
×
601
        // Based on normalizeAlias from
×
602
        // https://github.com/webpack/enhanced-resolve/blob/72999caf002f6f7bb4624e65fdeb7ba980b11e24/lib/ResolverFactory.js#L158
×
603
        // and AliasPlugin from
×
604
        // https://github.com/webpack/enhanced-resolve/blob/72999caf002f6f7bb4624e65fdeb7ba980b11e24/lib/AliasPlugin.js
×
605
        for key, value in compiler.options.resolve.alias
×
606
          if key.endsWith '$'
×
607
            if id is key[...-1]
×
608
              return value <? 'string' ? value : '\0'
×
609
          else
×
610
            if id is key or id.startsWith key + '/'
×
611
              return '\0' unless value <? 'string'
×
612
              return value + id[key.length..]
×
613
        id
×
614
  }
×
615

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