@typed/vite-plugin 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,31 +1,19 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'fs'
2
- import { readFile } from 'fs/promises'
1
+ import { existsSync, writeFileSync } from 'fs'
3
2
  import { EOL } from 'os'
4
- import { basename, dirname, join, relative, resolve } from 'path'
5
-
6
- import effectTransformer from '@effect/language-service/transformer'
7
- import {
8
- setupTsProject,
9
- makeHtmlModule,
10
- makeRuntimeModule,
11
- readDirectory,
12
- readModules,
13
- readApiModules,
14
- makeApiModule,
15
- type ApiModuleTreeJson,
16
- type ModuleTreeJsonWithFallback,
17
- moduleTreeToJson,
18
- apiModuleTreeToJson,
19
- addOrUpdateBase,
20
- } from '@typed/compiler'
3
+ import { basename, join, resolve } from 'path'
4
+
5
+ import { pipe } from '@fp-ts/core/Function'
6
+ import * as Option from '@fp-ts/core/Option'
7
+ import { Compiler, getRelativePath, type ResolvedOptions } from '@typed/compiler'
21
8
  import glob from 'fast-glob'
22
- import { Project, SourceFile, ts, type CompilerOptions } from 'ts-morph'
23
9
  // @ts-expect-error Unable to resolve types w/ NodeNext
24
10
  import vavite from 'vavite'
25
- import type { ConfigEnv, Logger, Plugin, PluginOption, UserConfig, ViteDevServer } from 'vite'
11
+ import type { ConfigEnv, Plugin, PluginOption, UserConfig, ViteDevServer } from 'vite'
26
12
  import compression from 'vite-plugin-compression'
27
13
  import tsconfigPaths from 'vite-tsconfig-paths'
28
14
 
15
+ import { PLUGIN_NAME } from './constants.js'
16
+
29
17
  /**
30
18
  * The Configuration for the Typed Plugin. All file paths can be relative to sourceDirectory or
31
19
  * can be absolute, path.resolve is used to stitch things together.
@@ -81,433 +69,75 @@ export interface PluginOptions {
81
69
 
82
70
  const cwd = process.cwd()
83
71
 
84
- export const PLUGIN_NAME = '@typed/vite-plugin'
85
-
86
72
  export interface TypedVitePlugin extends Plugin {
87
73
  readonly name: typeof PLUGIN_NAME
88
74
  readonly resolvedOptions: ResolvedOptions
89
75
  }
90
76
 
91
- const RUNTIME_VIRTUAL_ENTRYPOINT_PREFIX = 'runtime'
92
- const BROWSER_VIRTUAL_ENTRYPOINT_PREFIX = 'browser'
93
- const HTML_VIRTUAL_ENTRYPOINT_PREFIX = 'html'
94
- const API_VIRTUAL_ENTRYPOINT_PREFIX = 'api'
95
- const EXPRESS_VIRTUAL_ENTRYPOINT_PREFIX = 'express'
96
- const TYPED_CONFIG_IMPORT = 'typed:config'
97
-
98
- const PREFIXES = [
99
- RUNTIME_VIRTUAL_ENTRYPOINT_PREFIX,
100
- BROWSER_VIRTUAL_ENTRYPOINT_PREFIX,
101
- HTML_VIRTUAL_ENTRYPOINT_PREFIX,
102
- API_VIRTUAL_ENTRYPOINT_PREFIX,
103
- EXPRESS_VIRTUAL_ENTRYPOINT_PREFIX,
104
- ]
105
-
106
- const VIRTUAL_ID_PREFIX = '\0'
107
-
108
- export interface Manifest {
109
- readonly entryFiles: EntryFile[]
110
-
111
- readonly modules: {
112
- [importer: string]: Record<string, ManifestEntry>
113
- }
114
- }
115
-
116
- export interface ClientManifest extends Manifest {
117
- readonly entryFiles: HtmlEntryFile[]
118
-
119
- readonly modules: {
120
- [importer: string]: Record<string, ManifestEntry>
121
- }
122
- }
123
-
124
- export type EntryFile = HtmlEntryFile | TsEntryFile
125
-
126
- export interface HtmlEntryFile {
127
- readonly type: 'html'
128
- readonly filePath: string
129
- readonly imports: string[]
130
- readonly basePath: string
131
- }
132
-
133
- export interface TsEntryFile {
134
- readonly type: 'ts'
135
- readonly filePath: string
136
- }
137
-
138
- export type ManifestEntry =
139
- | ApiManifestEntry
140
- | ExpressManifestEntry
141
- | HtmlManifestEntry
142
- | RuntimeManifestEntry
143
- | BrowserManifestEntry
144
-
145
- export interface ApiManifestEntry extends ApiModuleTreeJson {
146
- readonly type: 'api'
147
- }
148
-
149
- export interface ExpressManifestEntry extends ApiModuleTreeJson {
150
- readonly type: 'express'
151
- }
152
-
153
- export interface HtmlManifestEntry {
154
- readonly type: 'html'
155
- readonly filePath: string
156
- }
157
-
158
- export interface BrowserManifestEntry extends ModuleTreeJsonWithFallback {
159
- readonly type: 'browser'
160
- }
161
-
162
- export interface RuntimeManifestEntry extends ModuleTreeJsonWithFallback {
163
- readonly type: 'runtime'
164
- }
165
-
166
- export interface ResolvedOptions {
167
- readonly sourceDirectory: string
168
- readonly tsConfig: string
169
- readonly serverFilePath: string
170
- readonly clientOutputDirectory: string
171
- readonly serverOutputDirectory: string
172
- readonly htmlFiles: readonly string[]
173
- readonly debug: boolean
174
- readonly saveGeneratedModules: boolean
175
- readonly isStaticBuild: boolean
176
- readonly base: string
177
- }
178
-
179
- export default function makePlugin({
180
- sourceDirectory: directory,
181
- tsConfig,
182
- serverFilePath,
183
- clientOutputDirectory,
184
- serverOutputDirectory,
185
- htmlFileGlobs,
186
- debug = false,
187
- saveGeneratedModules = false,
188
- isStaticBuild = process.env.STATIC_BUILD === 'true',
189
- }: PluginOptions): PluginOption[] {
190
- // Resolved options
191
- const sourceDirectory = resolve(cwd, directory)
192
- const tsConfigFilePath = resolve(sourceDirectory, tsConfig ?? 'tsconfig.json')
193
- const resolvedServerFilePath = resolve(sourceDirectory, serverFilePath ?? 'server.ts')
194
- const resolvedServerOutputDirectory = resolve(
195
- sourceDirectory,
196
- serverOutputDirectory ?? 'dist/server',
197
- )
198
- const resolvedClientOutputDirectory = resolve(
199
- sourceDirectory,
200
- clientOutputDirectory ?? 'dist/client',
201
- )
202
- const defaultIncludeExcludeTs = {
203
- include: ['**/*.ts', '**/*.tsx'],
204
- exclude: ['dist/**/*'],
205
- }
206
- const resolvedEffectTsOptions = {
207
- trace: defaultIncludeExcludeTs,
208
- optimize: defaultIncludeExcludeTs,
209
- debug: debug ? defaultIncludeExcludeTs : {},
210
- }
211
-
212
- const resolvedOptions: ResolvedOptions = {
213
- sourceDirectory,
214
- tsConfig: tsConfigFilePath,
215
- serverFilePath: resolvedServerFilePath,
216
- serverOutputDirectory: resolvedServerOutputDirectory,
217
- clientOutputDirectory: resolvedClientOutputDirectory,
218
- htmlFiles: findHtmlFiles(sourceDirectory, htmlFileGlobs).map((p) =>
219
- resolve(sourceDirectory, p),
220
- ),
221
- debug,
222
- saveGeneratedModules,
223
- isStaticBuild,
224
- base: '/',
225
- }
226
-
227
- const dependentsMap = new Map<string, Set<string>>()
228
- const filePathToModule = new Map<string, SourceFile>()
229
- const manifest: Manifest = {
230
- entryFiles: [],
231
- modules: {},
232
- }
233
-
234
- const addManifestEntry = (entry: ManifestEntry, importer: string, id: string) => {
235
- if (!manifest.modules[importer]) {
236
- manifest.modules[importer] = {}
237
- }
238
-
239
- manifest.modules[importer][id] = entry
240
- }
77
+ export default function makePlugin(pluginOptions: PluginOptions): PluginOption[] {
78
+ const options: ResolvedOptions = resolveOptions(pluginOptions)
241
79
 
80
+ let compiler: Compiler
242
81
  let devServer: ViteDevServer
243
- let logger: Logger
244
82
  let isSsr = false
245
- let project: Project
246
- let transformers: ts.CustomTransformers
247
-
248
- const serverExists = existsSync(resolvedServerFilePath)
249
83
 
250
84
  const plugins: PluginOption[] = [
251
85
  tsconfigPaths({
252
- projects: [tsConfigFilePath],
86
+ projects: [options.tsConfig],
253
87
  }),
254
- serverExists &&
255
- !isStaticBuild &&
256
- vavite({
257
- serverEntry: resolvedServerFilePath,
258
- serveClientAssetsInDev: true,
259
- }),
88
+ pipe(
89
+ options.serverFilePath,
90
+ Option.filter(() => !options.isStaticBuild),
91
+ Option.map((serverEntry) =>
92
+ vavite({
93
+ serverEntry,
94
+ serveClientAssetsInDev: true,
95
+ }),
96
+ ),
97
+ Option.getOrNull,
98
+ ),
260
99
  ]
261
100
 
262
- const setupProject = () => {
263
- if (project) {
264
- return
265
- }
266
-
267
- info(`Setting up TypeScript project...`, logger)
268
- project = setupTsProject(tsConfigFilePath)
269
- info(`Setup TypeScript project.`, logger)
270
-
271
- // Setup transformer for virtual modules.
272
- transformers = {
273
- before: [
274
- // Types are weird for some reason
275
- (effectTransformer as any as typeof effectTransformer.default)(
276
- project.getProgram().compilerObject,
277
- resolvedEffectTsOptions,
278
- ).before,
279
- ],
280
- }
281
- }
282
-
283
- const transpilerCompilerOptions = (): CompilerOptions => {
284
- setupProject()
285
-
286
- return {
287
- ...project.getCompilerOptions(),
288
- inlineSourceMap: false,
289
- inlineSources: saveGeneratedModules,
290
- sourceMap: true,
291
- }
292
- }
293
-
294
- const handleFileChange = async (path: string, event: 'create' | 'update' | 'delete') => {
295
- if (/\.tsx?$/.test(path)) {
296
- switch (event) {
297
- case 'create': {
298
- project.addSourceFileAtPath(path)
299
- break
300
- }
301
- case 'update': {
302
- const sourceFile = project.getSourceFile(path)
303
-
304
- if (sourceFile) {
305
- sourceFile.refreshFromFileSystemSync()
306
- } else {
307
- project.addSourceFileAtPath(path)
308
- }
309
-
310
- if (devServer) {
311
- const dependents = dependentsMap.get(path.replace(/.ts(x)?/, '.js$1'))
312
-
313
- for (const dependent of dependents ?? []) {
314
- const mod = devServer.moduleGraph.getModuleById(dependent)
315
-
316
- if (mod) {
317
- info(`reloading ${dependent}`, logger)
318
-
319
- await devServer.reloadModule(mod)
320
- }
321
- }
322
- }
323
-
324
- break
325
- }
326
- case 'delete': {
327
- await project.getSourceFile(path)?.deleteImmediately()
328
- break
329
- }
330
- }
331
- }
332
- }
333
-
334
- const buildRuntimeModule = async (importer: string, id: string) => {
335
- // Setup the TypeScript project if it hasn't been already
336
- setupProject()
337
-
338
- const moduleDirectory = resolve(dirname(importer), parseModulesFromId(id, importer))
339
- const relativeDirectory = relative(sourceDirectory, moduleDirectory)
340
- const isBrowser = id.startsWith(BROWSER_VIRTUAL_ENTRYPOINT_PREFIX)
341
- const moduleType = isBrowser ? 'browser' : 'runtime'
342
- const filePath = `${moduleDirectory}.${moduleType}.__generated__.ts`
343
- const directory = await readDirectory(moduleDirectory)
344
- const moduleTree = readModules(project, directory)
345
-
346
- addManifestEntry(
347
- {
348
- type: moduleType,
349
- ...moduleTreeToJson(sourceDirectory, moduleTree),
350
- },
351
- relative(sourceDirectory, importer),
352
- id,
353
- )
354
-
355
- const sourceFile = makeRuntimeModule(project, moduleTree, importer, filePath, isBrowser)
356
-
357
- addDependents(sourceFile)
358
-
359
- info(`Built ${moduleType} module for ${relativeDirectory}.`, logger)
360
-
361
- filePathToModule.set(filePath, sourceFile)
362
-
363
- if (saveGeneratedModules) {
364
- await sourceFile.save()
365
- }
366
-
367
- return filePath
368
- }
369
-
370
- const buildHtmlModule = async (importer: string, id: string) => {
371
- // Setup the TypeScript project if it hasn't been already
372
- setupProject()
373
-
374
- const htmlFileName = parseModulesFromId(id, importer)
375
- const htmlFilePath = resolve(dirname(importer), htmlFileName + '.html')
376
- const relativeHtmlFilePath = relative(sourceDirectory, htmlFilePath)
377
- let html = ''
378
-
379
- // If there's a dev server, use it to transform the HTML for development
380
- if (!isStaticBuild && devServer) {
381
- html = (await readFile(htmlFilePath, 'utf-8')).toString()
382
- html = await devServer.transformIndexHtml(
383
- getRelativePath(sourceDirectory, htmlFilePath),
384
- html,
385
- )
386
- } else {
387
- // Otherwise, read the already transformed file from the output directory.
388
- html = (
389
- await readFile(resolve(resolvedClientOutputDirectory, relativeHtmlFilePath), 'utf-8')
390
- ).toString()
391
- }
392
-
393
- const sourceFile = await makeHtmlModule({
394
- project,
395
- base: parseBasePath(html),
396
- filePath: htmlFilePath,
397
- html,
398
- importer,
399
- serverOutputDirectory: resolvedServerOutputDirectory,
400
- clientOutputDirectory: resolvedClientOutputDirectory,
401
- build: isStaticBuild ? 'static' : devServer ? 'development' : 'production',
402
- })
403
-
404
- addManifestEntry(
405
- {
406
- type: 'html',
407
- filePath: relativeHtmlFilePath,
408
- },
409
- relative(sourceDirectory, importer),
410
- id,
411
- )
412
-
413
- addDependents(sourceFile)
414
-
415
- info(`Built html module for ${relativeHtmlFilePath}.`, logger)
416
-
417
- const filePath = sourceFile.getFilePath()
418
-
419
- filePathToModule.set(filePath, sourceFile)
420
-
421
- if (saveGeneratedModules) {
422
- await sourceFile.save()
423
- }
424
-
425
- return filePath
426
- }
427
-
428
- const buildApiModule = async (importer: string, id: string) => {
429
- // Setup the TypeScript project if it hasn't been already
430
- setupProject()
431
-
432
- const importDirectory = dirname(importer)
433
- const moduleName = parseModulesFromId(id, importer)
434
- const moduleDirectory = resolve(importDirectory, moduleName)
435
- const relativeDirectory = relative(sourceDirectory, moduleDirectory)
436
- const moduleType = id.startsWith(EXPRESS_VIRTUAL_ENTRYPOINT_PREFIX) ? 'express' : 'api'
437
- const directory = await readDirectory(moduleDirectory)
438
- const moduleTree = readApiModules(project, directory)
439
- const filePath = `${importDirectory}/${basename(
440
- moduleName,
441
- )}.${moduleType.toLowerCase()}.__generated__.ts`
442
-
443
- const sourceFile = makeApiModule(
444
- project,
445
- moduleTree,
446
- filePath,
447
- importer,
448
- id.startsWith(EXPRESS_VIRTUAL_ENTRYPOINT_PREFIX),
449
- )
450
-
451
- addManifestEntry(
452
- {
453
- type: moduleType,
454
- ...apiModuleTreeToJson(sourceDirectory, moduleTree),
455
- },
456
- relative(sourceDirectory, importer),
457
- id,
458
- )
459
-
460
- addDependents(sourceFile)
461
-
462
- info(`Built ${moduleType} module for ${relativeDirectory}.`, logger)
463
-
464
- filePathToModule.set(filePath, sourceFile)
465
-
466
- if (saveGeneratedModules) {
467
- await sourceFile.save()
101
+ const getCompiler = () => {
102
+ if (compiler) {
103
+ return compiler
468
104
  }
469
105
 
470
- return filePath
471
- }
472
-
473
- const addDependents = (sourceFile: SourceFile) => {
474
- const importer = sourceFile.getFilePath()
475
- const imports = sourceFile
476
- .getLiteralsReferencingOtherSourceFiles()
477
- .map((i) => i.getLiteralValue())
478
-
479
- for (const i of imports) {
480
- const dependents = dependentsMap.get(i) ?? new Set()
481
-
482
- dependents.add(importer)
483
- dependentsMap.set(i, dependents)
484
- }
106
+ return (compiler = new Compiler(PLUGIN_NAME, options))
485
107
  }
486
108
 
487
109
  const virtualModulePlugin: TypedVitePlugin = {
488
110
  name: PLUGIN_NAME,
489
- resolvedOptions,
111
+ resolvedOptions: options,
112
+
113
+ /**
114
+ * Configures our production build using vavite
115
+ */
490
116
  config(config: UserConfig, env: ConfigEnv) {
491
117
  isSsr = env.ssrBuild ?? false
492
118
 
493
119
  // Configure Build steps when running with vavite
494
120
  if (env.mode === 'multibuild') {
495
121
  const clientBuild: UserConfig['build'] = {
496
- outDir: resolvedClientOutputDirectory,
122
+ outDir: options.clientOutputDirectory,
497
123
  rollupOptions: {
498
- input: buildClientInput(resolvedOptions.htmlFiles),
124
+ input: buildClientInput(options.htmlFiles),
499
125
  },
500
126
  }
501
127
 
502
- const serverBuild: UserConfig['build'] = {
503
- ssr: true,
504
- outDir: resolvedServerOutputDirectory,
505
- rollupOptions: {
506
- input: {
507
- index: resolvedServerFilePath,
128
+ const serverBuild = pipe(
129
+ options.serverFilePath,
130
+ Option.map((index): UserConfig['build'] => ({
131
+ ssr: true,
132
+ outDir: options.serverOutputDirectory,
133
+ rollupOptions: {
134
+ input: {
135
+ index,
136
+ },
508
137
  },
509
- },
510
- }
138
+ })),
139
+ Option.getOrNull,
140
+ )
511
141
 
512
142
  ;(config as any).buildSteps = [
513
143
  {
@@ -515,63 +145,63 @@ export default function makePlugin({
515
145
  // @ts-expect-error Unable to resolve types w/ NodeNext
516
146
  config: { build: clientBuild, plugins: [compression()] },
517
147
  },
518
- ...(serverExists
519
- ? [
520
- {
521
- name: 'server',
522
- config: { build: serverBuild },
523
- },
524
- ]
525
- : []),
148
+ serverBuild,
526
149
  ]
527
150
 
528
151
  return
529
152
  }
530
153
  },
531
154
 
155
+ /**
156
+ * Updates our resolved options with the correct base path
157
+ * and parses our input files for our manifest
158
+ */
532
159
  configResolved(resolvedConfig) {
533
- logger = resolvedConfig.logger
534
-
535
- const input = resolvedConfig.build.rollupOptions.input
536
-
537
- Object.assign(resolvedOptions, { base: resolvedConfig.base })
160
+ // Ensure options have the correct base path
161
+ Object.assign(options, { base: resolvedConfig.base })
538
162
 
539
- if (!input) return
540
-
541
- if (typeof input === 'string') {
542
- manifest.entryFiles.push(parseEntryFile(sourceDirectory, input))
543
- } else if (Array.isArray(input)) {
544
- manifest.entryFiles.push(...input.map((i) => parseEntryFile(sourceDirectory, i)))
545
- } else {
546
- manifest.entryFiles.push(
547
- ...Object.values(input).map((i) => parseEntryFile(sourceDirectory, i)),
548
- )
549
- }
163
+ getCompiler().parseInput(resolvedConfig.build.rollupOptions.input)
550
164
  },
551
165
 
166
+ /**
167
+ * Configures our dev server to watch for changes to our input files
168
+ * and exposes the dev server to our compiler methods
169
+ */
552
170
  configureServer(server) {
553
171
  devServer = server
554
172
 
555
173
  server.watcher.on('all', (event, path) => {
556
174
  if (event === 'change') {
557
- handleFileChange(path, 'update')
175
+ getCompiler().handleFileChange(path, 'update', server)
558
176
  } else if (event === 'add') {
559
- handleFileChange(path, 'create')
177
+ getCompiler().handleFileChange(path, 'create', server)
560
178
  } else if (event === 'unlink') {
561
- handleFileChange(path, 'delete')
179
+ getCompiler().handleFileChange(path, 'delete', server)
562
180
  }
563
181
  })
564
182
  },
565
183
 
184
+ /**
185
+ * Handles file changes
186
+ */
566
187
  async watchChange(path, { event }) {
567
- handleFileChange(path, event)
188
+ getCompiler().handleFileChange(path, event, devServer)
568
189
  },
569
190
 
191
+ /**
192
+ * Type-check our project and fail the build if there are any errors.
193
+ * If successful, save our manifest to disk.
194
+ */
570
195
  async closeBundle() {
571
- if (Object.keys(manifest).length > 0 && !isStaticBuild) {
196
+ const { manifest, throwDiagnostics } = getCompiler()
197
+
198
+ // Throw any diagnostics that were collected during the build
199
+ throwDiagnostics()
200
+
201
+ if (Object.keys(manifest).length > 0 && !options.isStaticBuild) {
572
202
  writeFileSync(
573
203
  resolve(
574
- isSsr ? resolvedServerOutputDirectory : resolvedClientOutputDirectory,
204
+ isSsr ? options.serverOutputDirectory : options.clientOutputDirectory,
575
205
  'typed-manifest.json',
576
206
  ),
577
207
  JSON.stringify(manifest, null, 2) + EOL,
@@ -579,82 +209,32 @@ export default function makePlugin({
579
209
  }
580
210
  },
581
211
 
212
+ /**
213
+ * Resolve and build our virtual modules
214
+ */
582
215
  async resolveId(id: string, importer?: string) {
583
- if (!importer) {
584
- return
585
- }
586
-
587
- if (
588
- id.startsWith(RUNTIME_VIRTUAL_ENTRYPOINT_PREFIX) ||
589
- id.startsWith(BROWSER_VIRTUAL_ENTRYPOINT_PREFIX)
590
- ) {
591
- return VIRTUAL_ID_PREFIX + (await buildRuntimeModule(importer, id))
592
- }
593
-
594
- if (id.startsWith(HTML_VIRTUAL_ENTRYPOINT_PREFIX)) {
595
- return VIRTUAL_ID_PREFIX + (await buildHtmlModule(importer, id))
596
- }
597
-
598
- if (
599
- id.startsWith(API_VIRTUAL_ENTRYPOINT_PREFIX) ||
600
- id.startsWith(EXPRESS_VIRTUAL_ENTRYPOINT_PREFIX)
601
- ) {
602
- return VIRTUAL_ID_PREFIX + (await buildApiModule(importer, id))
603
- }
604
-
605
- if (id === TYPED_CONFIG_IMPORT) {
606
- return VIRTUAL_ID_PREFIX + TYPED_CONFIG_IMPORT
607
- }
608
-
609
- importer = importer.replace(VIRTUAL_ID_PREFIX, '')
610
-
611
- // Virtual modules have problems with resolving relative paths due to not
612
- // having a real directory to work with thus the need to resolve them manually.
613
- if (filePathToModule.has(importer) && id.startsWith('.')) {
614
- return findRelativeFile(importer, id)
615
- }
216
+ return await getCompiler().resolveId(id, importer, devServer)
616
217
  },
617
218
 
219
+ /**
220
+ * Load our virtual modules
221
+ */
618
222
  async load(id: string) {
619
- id = id.replace(VIRTUAL_ID_PREFIX, '')
620
-
621
- const sourceFile = filePathToModule.get(id) ?? project?.getSourceFile(id)
622
-
623
- if (sourceFile) {
624
- logDiagnostics(project, sourceFile, sourceDirectory, id, logger)
625
-
626
- return {
627
- code: sourceFile.getFullText(),
628
- }
629
- }
630
-
631
- if (id === TYPED_CONFIG_IMPORT) {
632
- return {
633
- code: Object.entries(resolvedOptions)
634
- .map(([key, value]) => `export const ${key} = ${JSON.stringify(value)}`)
635
- .join(EOL),
636
- }
637
- }
223
+ return await getCompiler().load(id)
638
224
  },
639
225
 
226
+ /**
227
+ * Transorm TypeScript modules
228
+ */
640
229
  transform(text: string, id: string) {
641
- if (/.[c|m]?tsx?$/.test(id)) {
642
- const output = ts.transpileModule(text, {
643
- fileName: id,
644
- compilerOptions: transpilerCompilerOptions(),
645
- transformers,
646
- })
647
-
648
- return {
649
- code: output.outputText,
650
- map: output.sourceMapText,
651
- }
652
- }
230
+ return getCompiler().transpileTsModule(text, id, devServer)
653
231
  },
654
232
 
233
+ /**
234
+ * Transform HTML files
235
+ */
655
236
  transformIndexHtml(html: string) {
656
- // Add vite's base path to all HTML files
657
- return addOrUpdateBase(html, resolvedOptions.base)
237
+ return getCompiler().transformHtml(html)
658
238
  },
659
239
  }
660
240
 
@@ -663,60 +243,68 @@ export default function makePlugin({
663
243
  return plugins
664
244
  }
665
245
 
666
- function logDiagnostics(
667
- project: Project,
668
- sourceFile: SourceFile,
669
- sourceDirectory: string,
670
- filePath: string,
671
- logger: Logger | undefined,
672
- ) {
673
- const diagnostics = sourceFile.getPreEmitDiagnostics()
674
- const relativeFilePath = relative(sourceDirectory, filePath)
675
-
676
- if (diagnostics.length > 0) {
677
- info(`Type-checking errors found at ${relativeFilePath}`, logger)
678
- info(`Source:` + EOL + sourceFile.getFullText(), logger)
679
- info(project.formatDiagnosticsWithColorAndContext(diagnostics), logger)
680
- }
681
- }
682
-
683
- function findRelativeFile(importer: string, id: string) {
684
- const dir = dirname(importer)
685
- const tsPath = resolve(dir, id.replace(/.([c|m])?js(x)?$/, '.$1ts$2'))
686
-
687
- if (existsSync(tsPath)) {
688
- return tsPath
689
- }
690
-
691
- const jsPath = resolve(dir, id)
692
-
693
- if (existsSync(jsPath)) {
694
- return tsPath
695
- }
696
- }
697
-
698
- function parseModulesFromId(id: string, importer: string | undefined): string {
699
- let pages = id
246
+ function resolveOptions({
247
+ sourceDirectory: directory,
248
+ tsConfig,
249
+ serverFilePath,
250
+ clientOutputDirectory,
251
+ serverOutputDirectory,
252
+ htmlFileGlobs,
253
+ debug = false,
254
+ saveGeneratedModules = false,
255
+ isStaticBuild = process.env.STATIC_BUILD === 'true',
256
+ }: PluginOptions): ResolvedOptions {
257
+ // Resolved options
258
+ const sourceDirectory = resolve(cwd, directory)
259
+ const tsConfigFilePath = resolve(sourceDirectory, tsConfig ?? 'tsconfig.json')
260
+ const resolvedServerFilePath = resolve(sourceDirectory, serverFilePath ?? 'server.ts')
261
+ const serverExists = existsSync(resolvedServerFilePath)
262
+ const resolvedServerOutputDirectory = resolve(
263
+ sourceDirectory,
264
+ serverOutputDirectory ?? 'dist/server',
265
+ )
266
+ const resolvedClientOutputDirectory = resolve(
267
+ sourceDirectory,
268
+ clientOutputDirectory ?? 'dist/client',
269
+ )
700
270
 
701
- for (const prefix of PREFIXES) {
702
- pages = pages.replace(prefix + ':', '')
703
- }
271
+ const exclusions = [
272
+ getRelativePath(sourceDirectory, join(resolvedServerOutputDirectory, '/**/*')),
273
+ getRelativePath(sourceDirectory, join(resolvedClientOutputDirectory, '/**/*')),
274
+ '**/node_modules/**',
275
+ ]
704
276
 
705
- if (pages === '') {
706
- throw new Error(`[${PLUGIN_NAME}]: No pages were specified from ${importer}`)
277
+ const resolvedOptions: ResolvedOptions = {
278
+ base: '/',
279
+ clientOutputDirectory: resolvedClientOutputDirectory,
280
+ debug,
281
+ exclusions,
282
+ htmlFiles: findHtmlFiles(sourceDirectory, htmlFileGlobs, exclusions).map((p) =>
283
+ resolve(sourceDirectory, p),
284
+ ),
285
+ isStaticBuild,
286
+ saveGeneratedModules,
287
+ serverFilePath: serverExists ? Option.some(resolvedServerFilePath) : Option.none(),
288
+ serverOutputDirectory: resolvedServerOutputDirectory,
289
+ sourceDirectory,
290
+ tsConfig: tsConfigFilePath,
707
291
  }
708
292
 
709
- return pages
293
+ return resolvedOptions
710
294
  }
711
295
 
712
- function findHtmlFiles(directory: string, htmlFileGlobs?: readonly string[]): readonly string[] {
296
+ function findHtmlFiles(
297
+ directory: string,
298
+ htmlFileGlobs: readonly string[] | undefined,
299
+ exclusions: readonly string[],
300
+ ): readonly string[] {
713
301
  if (htmlFileGlobs) {
714
302
  // eslint-disable-next-line import/no-named-as-default-member
715
- return glob.sync([...htmlFileGlobs], { cwd: directory })
303
+ return glob.sync([...htmlFileGlobs, ...exclusions.map((x) => '!' + x)], { cwd: directory })
716
304
  }
717
305
 
718
306
  // eslint-disable-next-line import/no-named-as-default-member
719
- return glob.sync(['**/*.html', '!' + '**/node_modules/**', '!' + '**/dist/**'], {
307
+ return glob.sync(['**/*.html', ...exclusions.map((x) => '!' + x)], {
720
308
  cwd: directory,
721
309
  })
722
310
  }
@@ -727,87 +315,3 @@ function buildClientInput(htmlFilePaths: readonly string[]) {
727
315
  {},
728
316
  )
729
317
  }
730
-
731
- function getRelativePath(from: string, to: string) {
732
- const path = relative(from, to)
733
-
734
- if (!path.startsWith('.')) {
735
- return './' + path
736
- }
737
-
738
- return path
739
- }
740
-
741
- function info(message: string, logger: Logger | undefined) {
742
- if (logger) {
743
- logger.info(`[${PLUGIN_NAME}]: ${message}`)
744
- } else {
745
- console.info(`[${PLUGIN_NAME}]:`, `${message}`)
746
- }
747
- }
748
-
749
- function parseEntryFile(sourceDirectory: string, filePath: string): EntryFile {
750
- if (filePath.endsWith('.html')) {
751
- return parseHtmlEntryFile(sourceDirectory, filePath)
752
- }
753
-
754
- return parseTsEntryFile(sourceDirectory, filePath)
755
- }
756
-
757
- function parseHtmlEntryFile(sourceDirectory: string, filePath: string): EntryFile {
758
- const content = readFileSync(filePath, 'utf-8').toString()
759
-
760
- return {
761
- type: 'html',
762
- filePath: relative(sourceDirectory, filePath),
763
- imports: parseHtmlImports(sourceDirectory, content),
764
- basePath: parseBasePath(content),
765
- }
766
- }
767
-
768
- function parseHtmlImports(sourceDirectory: string, content: string) {
769
- const imports: string[] = []
770
-
771
- const matches = content.match(/<script[^>]*src="([^"]*)"[^>]*>/g)
772
-
773
- if (matches) {
774
- for (const match of matches) {
775
- // If script is not type=module then skip
776
- if (!match.includes('type="module"')) {
777
- continue
778
- }
779
-
780
- const src = match.match(/src="([^"]*)"/)?.[1]
781
-
782
- if (src) {
783
- const fullPath = join(sourceDirectory, src)
784
- const relativePath = relative(sourceDirectory, fullPath)
785
-
786
- imports.push(relativePath)
787
- }
788
- }
789
- }
790
-
791
- return imports
792
- }
793
-
794
- function parseBasePath(content: string) {
795
- const baseTag = content.match(/<base[^>]*>/)?.[0]
796
-
797
- if (baseTag) {
798
- const href = baseTag.match(/href="([^"]*)"/)?.[1]
799
-
800
- if (href) {
801
- return href
802
- }
803
- }
804
-
805
- return '/'
806
- }
807
-
808
- function parseTsEntryFile(sourceDirectory: string, filePath: string): EntryFile {
809
- return {
810
- type: 'ts',
811
- filePath: relative(sourceDirectory, filePath),
812
- }
813
- }