Skip to content

Commit c348afe

Browse files
committed
fix: use builtin utils
Fixes #17 Fixes #9
1 parent 46bef59 commit c348afe

File tree

5 files changed

+282
-2
lines changed

5 files changed

+282
-2
lines changed

scripts/postbuild.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async function run() {
1515
console.log(chalk.cyan.inverse(' POST '), `Fix ${basename(file)}`)
1616
let code = await fs.readFile(file, 'utf8')
1717
code = code.replace('exports.default =', 'module.exports =')
18-
code += 'exports.default = module.exports;'
18+
code += '\nexports.default = module.exports;'
1919
await fs.writeFile(file, code)
2020
}
2121
}

src/core/context/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import type { UserOptions } from '../../types'
33
import type { Generate, Lex, ObjectDirective, Parse, Transform } from '../types'
44
import process from 'node:process'
55
import MagicString from 'magic-string'
6-
import { createFilter, createLogger, loadEnv } from 'vite'
6+
import { loadEnv } from '../utils/env'
7+
import { createFilter } from '../utils/filter'
8+
import { createLogger } from '../utils/logger'
79
import { Generator } from './generator'
810
import { Lexer } from './lexer'
911
import { Parser } from './parser'

src/core/utils/env.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// copied and adapted from Vite (MIT Licensed)
2+
3+
import fs from 'node:fs'
4+
import path from 'node:path'
5+
import { parse } from 'dotenv'
6+
import { type DotenvPopulateInput, expand } from 'dotenv-expand'
7+
8+
export function arraify<T>(target: T | T[]): T[] {
9+
return Array.isArray(target) ? target : [target]
10+
}
11+
12+
const windowsSlashRE = /\\/g
13+
export function slash(p: string): string {
14+
return p.replace(windowsSlashRE, '/')
15+
}
16+
export const isWindows = process.platform === 'win32'
17+
export function normalizePath(id: string): string {
18+
return path.posix.normalize(isWindows ? slash(id) : id)
19+
}
20+
21+
export function tryStatSync(file: string): fs.Stats | undefined {
22+
try {
23+
// The "throwIfNoEntry" is a performance optimization for cases where the file does not exist
24+
return fs.statSync(file, { throwIfNoEntry: false })
25+
} catch {
26+
// Ignore errors
27+
}
28+
}
29+
30+
export function getEnvFilesForMode(
31+
mode: string,
32+
envDir: string | false,
33+
): string[] {
34+
if (envDir !== false) {
35+
return [
36+
/** default file */ `.env`,
37+
/** local file */ `.env.local`,
38+
/** mode file */ `.env.${mode}`,
39+
/** mode local file */ `.env.${mode}.local`,
40+
].map((file) => normalizePath(path.join(envDir, file)))
41+
}
42+
43+
return []
44+
}
45+
46+
export function loadEnv(
47+
mode: string,
48+
envDir: string | false,
49+
prefixes: string | string[] = 'VITE_',
50+
): Record<string, string> {
51+
const start = performance.now()
52+
const getTime = () => `${(performance.now() - start).toFixed(2)}ms`
53+
54+
if (mode === 'local') {
55+
throw new Error(
56+
`"local" cannot be used as a mode name because it conflicts with ` +
57+
`the .local postfix for .env files.`,
58+
)
59+
}
60+
prefixes = arraify(prefixes)
61+
const env: Record<string, string> = {}
62+
const envFiles = getEnvFilesForMode(mode, envDir)
63+
64+
65+
const parsed = Object.fromEntries(
66+
envFiles.flatMap((filePath) => {
67+
if (!tryStatSync(filePath)?.isFile()) return []
68+
69+
return Object.entries(parse(fs.readFileSync(filePath)))
70+
}),
71+
)
72+
73+
// test NODE_ENV override before expand as otherwise process.env.NODE_ENV would override this
74+
if (parsed.NODE_ENV && process.env.VITE_USER_NODE_ENV === undefined) {
75+
process.env.VITE_USER_NODE_ENV = parsed.NODE_ENV
76+
}
77+
// support BROWSER and BROWSER_ARGS env variables
78+
if (parsed.BROWSER && process.env.BROWSER === undefined) {
79+
process.env.BROWSER = parsed.BROWSER
80+
}
81+
if (parsed.BROWSER_ARGS && process.env.BROWSER_ARGS === undefined) {
82+
process.env.BROWSER_ARGS = parsed.BROWSER_ARGS
83+
}
84+
85+
// let environment variables use each other. make a copy of `process.env` so that `dotenv-expand`
86+
// doesn't re-assign the expanded values to the global `process.env`.
87+
const processEnv = { ...process.env } as DotenvPopulateInput
88+
expand({ parsed, processEnv })
89+
90+
// only keys that start with prefix are exposed to client
91+
for (const [key, value] of Object.entries(parsed)) {
92+
if (prefixes.some((prefix) => key.startsWith(prefix))) {
93+
env[key] = value
94+
}
95+
}
96+
97+
// check if there are actual env variables starting with VITE_*
98+
// these are typically provided inline and should be prioritized
99+
for (const key in process.env) {
100+
if (prefixes.some((prefix) => key.startsWith(prefix))) {
101+
env[key] = process.env[key] as string
102+
}
103+
}
104+
105+
return env
106+
}

src/core/utils/filter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createFilter as _createFilter, FilterPattern } from '@rollup/pluginutils'
2+
3+
export const createFilter = _createFilter as (
4+
include?: FilterPattern,
5+
exclude?: FilterPattern,
6+
options?: { resolve?: string | false | null },
7+
) => (id: string | unknown) => boolean

src/core/utils/logger.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/* eslint no-console: 0 */
2+
// copied and adapted from Vite (MIT Licensed)
3+
4+
import readline from 'node:readline'
5+
import colors from 'picocolors'
6+
7+
export type LogType = 'error' | 'warn' | 'info'
8+
export type LogLevel = LogType | 'silent'
9+
export interface Logger {
10+
info(msg: string, options?: LogOptions): void
11+
warn(msg: string, options?: LogOptions): void
12+
warnOnce(msg: string, options?: LogOptions): void
13+
error(msg: string, options?: LogErrorOptions): void
14+
clearScreen(type: LogType): void
15+
hasErrorLogged(error: Error): boolean
16+
hasWarned: boolean
17+
}
18+
19+
export interface LogOptions {
20+
clear?: boolean
21+
timestamp?: boolean
22+
environment?: string
23+
}
24+
25+
export interface LogErrorOptions extends LogOptions {
26+
error?: Error | null
27+
}
28+
29+
export const LogLevels: Record<LogLevel, number> = {
30+
silent: 0,
31+
error: 1,
32+
warn: 2,
33+
info: 3,
34+
}
35+
36+
let lastType: LogType | undefined
37+
let lastMsg: string | undefined
38+
let sameCount = 0
39+
40+
function clearScreen() {
41+
const repeatCount = process.stdout.rows - 2
42+
const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : ''
43+
console.log(blank)
44+
readline.cursorTo(process.stdout, 0, 0)
45+
readline.clearScreenDown(process.stdout)
46+
}
47+
48+
export interface LoggerOptions {
49+
prefix?: string
50+
allowClearScreen?: boolean
51+
customLogger?: Logger
52+
console?: Console
53+
}
54+
55+
// Only initialize the timeFormatter when the timestamp option is used, and
56+
// reuse it across all loggers
57+
let timeFormatter: Intl.DateTimeFormat
58+
function getTimeFormatter() {
59+
timeFormatter ??= new Intl.DateTimeFormat(undefined, {
60+
hour: 'numeric',
61+
minute: 'numeric',
62+
second: 'numeric',
63+
})
64+
return timeFormatter
65+
}
66+
67+
export function createLogger(
68+
level: LogLevel = 'info',
69+
options: LoggerOptions = {},
70+
): Logger {
71+
if (options.customLogger) {
72+
return options.customLogger
73+
}
74+
75+
const loggedErrors = new WeakSet<Error>()
76+
const {
77+
prefix = '[vite]',
78+
allowClearScreen = true,
79+
console = globalThis.console,
80+
} = options
81+
const thresh = LogLevels[level]
82+
const canClearScreen =
83+
allowClearScreen && process.stdout.isTTY && !process.env.CI
84+
const clear = canClearScreen ? clearScreen : () => { }
85+
86+
function format(type: LogType, msg: string, options: LogErrorOptions = {}) {
87+
if (options.timestamp) {
88+
let tag = ''
89+
if (type === 'info') {
90+
tag = colors.cyan(colors.bold(prefix))
91+
} else if (type === 'warn') {
92+
tag = colors.yellow(colors.bold(prefix))
93+
} else {
94+
tag = colors.red(colors.bold(prefix))
95+
}
96+
const environment = options.environment ? options.environment + ' ' : ''
97+
return `${colors.dim(getTimeFormatter().format(new Date()))} ${tag} ${environment}${msg}`
98+
} else {
99+
return msg
100+
}
101+
}
102+
103+
function output(type: LogType, msg: string, options: LogErrorOptions = {}) {
104+
if (thresh >= LogLevels[type]) {
105+
const method = type === 'info' ? 'log' : type
106+
107+
if (options.error) {
108+
loggedErrors.add(options.error)
109+
}
110+
if (canClearScreen) {
111+
if (type === lastType && msg === lastMsg) {
112+
sameCount++
113+
clear()
114+
console[method](
115+
format(type, msg, options),
116+
colors.yellow(`(x${sameCount + 1})`),
117+
)
118+
} else {
119+
sameCount = 0
120+
lastMsg = msg
121+
lastType = type
122+
if (options.clear) {
123+
clear()
124+
}
125+
console[method](format(type, msg, options))
126+
}
127+
} else {
128+
console[method](format(type, msg, options))
129+
}
130+
}
131+
}
132+
133+
const warnedMessages = new Set<string>()
134+
135+
const logger: Logger = {
136+
hasWarned: false,
137+
info(msg, opts) {
138+
output('info', msg, opts)
139+
},
140+
warn(msg, opts) {
141+
logger.hasWarned = true
142+
output('warn', msg, opts)
143+
},
144+
warnOnce(msg, opts) {
145+
if (warnedMessages.has(msg)) return
146+
logger.hasWarned = true
147+
output('warn', msg, opts)
148+
warnedMessages.add(msg)
149+
},
150+
error(msg, opts) {
151+
logger.hasWarned = true
152+
output('error', msg, opts)
153+
},
154+
clearScreen(type) {
155+
if (thresh >= LogLevels[type]) {
156+
clear()
157+
}
158+
},
159+
hasErrorLogged(error) {
160+
return loggedErrors.has(error)
161+
},
162+
}
163+
164+
return logger
165+
}

0 commit comments

Comments
 (0)