Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(define): don't modify string literals #9791

Closed
wants to merge 44 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
29da61c
fix(define): don't modify string literals
tony19 Aug 23, 2022
0781d42
test(define): verify define ignores literals
tony19 Aug 23, 2022
cf93c33
test(define): expand tests out of loop to easily identify failures
tony19 Aug 23, 2022
ed16db2
test(define): verify define ignores nested string of all quote types
tony19 Aug 23, 2022
589d30b
fix(define): don't modify string literals in ssr
tony19 Aug 23, 2022
603073c
test(define): verify define ignores literals in ssr mode
tony19 Aug 23, 2022
2eb1aea
test(define): include tests from #9621
tony19 Aug 23, 2022
91c47d4
fix(define): only strip literals if special constant found
tony19 Aug 23, 2022
2031d94
fix(define): remove unnecessary string replace for ssr
tony19 Aug 23, 2022
8974e65
fix(define): only start matching if replacement needed
tony19 Aug 23, 2022
46153b7
fix(define): return null early if no replacements needed
tony19 Aug 24, 2022
609df10
wip: test(define): more tests
tony19 Aug 24, 2022
f5277f0
wip: test(define): skip meta.env and process.env tests for now
tony19 Aug 24, 2022
6a06d5f
wip: test(define): refactor
tony19 Aug 24, 2022
d83ab82
wip: test(define): fix tests for for import.meta.env
tony19 Aug 24, 2022
7319275
fix(define): prevent replacement of import.meta.env during tests
tony19 Aug 24, 2022
d875495
test(define): fix possibly-undefined warning
tony19 Aug 24, 2022
89d8c29
test(define): tweaks
tony19 Aug 24, 2022
188fbdd
chore: escape import.meta.env in vue playground
tony19 Aug 24, 2022
e71a22c
fix(define): use object hook
tony19 Aug 24, 2022
22a9010
fix(define): simplify workaround for import.meta.env replacement
tony19 Aug 24, 2022
bb34dd1
chore(define): typo
tony19 Aug 25, 2022
cbff5c6
test(define): fix missing backticks in multiline tests
tony19 Aug 25, 2022
2b5bf2b
test(define): remove invalid test
tony19 Aug 25, 2022
75ff486
refactor(define): move string replacement code into utils
tony19 Sep 1, 2022
c5736ed
test(define): add playground tests
tony19 Aug 26, 2022
ae211b5
docs(define): describe new string replacement behavior
tony19 Sep 1, 2022
e41792a
chore: link to vitest issue in fixme comments
tony19 Sep 1, 2022
46a291b
test(define): add playground tests
tony19 Aug 26, 2022
bce6fe2
test(define): skip tests for 'process.env.NODE_ENV' until #9829 fix
tony19 Sep 1, 2022
b40fd91
perf(define): don't compare id if no replacements needed
tony19 Sep 1, 2022
2e9460d
chore: fix docs comment for replaceInCode
tony19 Sep 1, 2022
0383606
fix(client-inject): don't modify string literals
tony19 Sep 1, 2022
1303424
test(client-inject): add unit tests
tony19 Aug 26, 2022
59c2432
test: update snapshots for client-inject tests
tony19 Aug 26, 2022
60c8d4b
test(client-inject): verify replaces template literal expression
tony19 Aug 26, 2022
e3dd331
revert: "test(define): skip tests for 'process.env.NODE_ENV' until #9…
tony19 Sep 1, 2022
e8a3f24
Merge branch 'main' into fix/define-must-ignore-strings
tony19 Sep 1, 2022
87b7b16
Merge branch 'main' into fix/define-must-ignore-strings
tony19 Sep 6, 2022
43d21c2
chore(define): remove workaround for vitest#1941
tony19 Sep 6, 2022
39bd627
docs(define): update expected version
tony19 Sep 6, 2022
1df6fa4
test(util): verify replaceInCode ignores string literals/comments
tony19 Sep 6, 2022
60c4aee
Merge branch 'main' into fix/define-must-ignore-strings
tony19 Oct 23, 2022
308d640
Merge branch 'main' into fix/define-must-ignore-strings
tony19 Nov 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/config/shared-options.md
Expand Up @@ -39,12 +39,14 @@ Define global constant replacements. Entries will be defined as globals during d

- Starting from `2.0.0-beta.70`, string values will be used as raw expressions, so if defining a string constant, it needs to be explicitly quoted (e.g. with `JSON.stringify`).

- Starting from `3.2.0`, defined constants within string literals and comments are ignored. For example, `process.env.NODE_ENV` is not replaced in `console.log('mode is process.env.NODE_ENV') /* process.env.NODE_ENV */`.

- To be consistent with [esbuild behavior](https://esbuild.github.io/api/#define), expressions must either be a JSON object (null, boolean, number, string, array, or object) or a single identifier.

- Replacements are performed only when the match isn't surrounded by other letters, numbers, `_` or `$`.

::: warning
Because it's implemented as straightforward text replacements without any syntax analysis, we recommend using `define` for CONSTANTS only.
Because it's implemented as straightforward text replacements without any syntax analysis (pre Vite 3.2.0), we recommend using `define` for CONSTANTS only.

For example, `process.env.FOO` and `__APP_VERSION__` are good fits. But `process` or `global` should not be put into this option. Variables can be shimmed or polyfilled instead.
:::
Expand Down
6 changes: 4 additions & 2 deletions docs/guide/env-and-mode.md
Expand Up @@ -18,9 +18,11 @@ Vite exposes env variables on the special **`import.meta.env`** object. Some bui

During production, these env variables are **statically replaced**. It is therefore necessary to always reference them using the full static string. For example, dynamic key access like `import.meta.env[key]` will not work.

It will also replace these strings appearing in JavaScript strings and Vue templates. This should be a rare case, but it can be unintended. You may see errors like `Missing Semicolon` or `Unexpected token` in this case, for example when `"process.env.`<wbr>`NODE_ENV"` is transformed to `""development": "`. There are ways to work around this behavior:
Before version 3.2.0, Vite also replaces environment variables (and other [defined constants](/config/shared-options.html#define)) within string literals and comments. This could result in unintentional changes to your string literals that result in invalid JavaScript syntax, causing errors like `Missing ) after argument list`, `Missing semicolon`, or `Unexpected token`. For example, you'd see an error when `console.log("This is process.env.`<wbr>`NODE_ENV")` is transformed to `console.log("This is "development"")`.

- For JavaScript strings, you can break the string up with a Unicode zero-width space, e.g. `'import.meta\u200b.env.MODE'`.
There are ways to work around this string replacement behavior:

- For JavaScript strings, you can break the string up with a Unicode zero-width space, e.g. `'import.meta\0.env.MODE'` or use string concatenation, e.g. `'import' + '.meta.env.MODE'`.

- For Vue templates or other HTML that gets compiled into JavaScript strings, you can use the [`<wbr>` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr), e.g. `import.meta.<wbr>env.MODE`.

Expand Down
98 changes: 98 additions & 0 deletions packages/vite/src/node/__tests__/plugins/clientInjections.spec.ts
@@ -0,0 +1,98 @@
import { describe, expect, test } from 'vitest'
import {
clientInjectionsPlugin,
normalizedClientEntry,
normalizedEnvEntry,
resolveInjections
} from '../../plugins/clientInjections'
import { resolveConfig } from '../../config'

async function createClientInjectionPluginTransform({
build = true,
ssr = false,
id = 'foo.ts'
} = {}) {
const config = await resolveConfig({}, build ? 'build' : 'serve')
const instance = clientInjectionsPlugin(config)
const transform = async (code: string) => {
const transform =
instance.transform && 'handler' in instance.transform
? instance.transform.handler
: instance.transform
const result = await transform?.call({}, code, id, { ssr })
return result?.code || result
}
return {
transform,
injections: resolveInjections(config)
}
}

describe('clientInjectionsPlugin', () => {
test('replaces process.env.NODE_ENV for non-SSR', async () => {
const { transform } = await createClientInjectionPluginTransform()
expect(await transform('let x = process.env.NODE_ENV;')).toBe(
'let x = "test";'
)
})

test('replaces process.env.NODE_ENV inside template literal expressions for non-SSR', async () => {
const { transform } = await createClientInjectionPluginTransform()
expect(await transform('let x = `${process.env.NODE_ENV}`;')).toBe(
'let x = `${"test"}`;'
)
})

test('ignores process.env.NODE_ENV for SSR', async () => {
const { transform: ssrTransform } =
await createClientInjectionPluginTransform({
ssr: true
})
expect(await ssrTransform('let x = process.env.NODE_ENV;')).toBe(null)
})

test('ignores process.env.NODE_ENV inside string literal for non-SSR', async () => {
const { transform } = await createClientInjectionPluginTransform()
expect(await transform(`let x = 'process.env.NODE_ENV';`)).toBe(null)
expect(await transform('let x = "process.env.NODE_ENV";')).toBe(null)
expect(await transform('let x = `process.env.NODE_ENV`;')).toBe(null)
})

test('ignores process.env.NODE_ENV inside string literal for SSR', async () => {
const { transform: ssrTransform } =
await createClientInjectionPluginTransform({
ssr: true
})
expect(await ssrTransform(`let x = 'process.env.NODE_ENV';`)).toBe(null)
expect(await ssrTransform('let x = "process.env.NODE_ENV";')).toBe(null)
expect(await ssrTransform('let x = `process.env.NODE_ENV`;')).toBe(null)
})

test('replaces code injections for client entry', async () => {
const { injections, transform } =
await createClientInjectionPluginTransform({
id: normalizedClientEntry
})
test.each(Object.keys(injections))('replaces %s', async (key) => {
expect(await transform(key)).toBe(injections[key])
})
})

test('replaces code injections for env entry', async () => {
const { injections, transform } =
await createClientInjectionPluginTransform({
id: normalizedEnvEntry
})
test.each(Object.keys(injections))('replaces %s', async (key) => {
expect(await transform(key)).toBe(injections[key])
})
})

test('ignores code injections for non entry files', async () => {
const { injections, transform } =
await createClientInjectionPluginTransform()
test.each(Object.keys(injections))('replaces %s', async (key) => {
expect(await transform(key)).toBe(null)
})
})
})
116 changes: 115 additions & 1 deletion packages/vite/src/node/__tests__/plugins/define.spec.ts
Expand Up @@ -10,7 +10,11 @@ async function createDefinePluginTransform(
const config = await resolveConfig({ define }, build ? 'build' : 'serve')
const instance = definePlugin(config)
return async (code: string) => {
const result = await instance.transform.call({}, code, 'foo.ts', { ssr })
const transform =
instance.transform && 'handler' in instance.transform
? instance.transform.handler
: instance.transform
const result = await transform?.call({}, code, 'foo.ts', { ssr })
return result?.code || result
}
}
Expand All @@ -37,4 +41,114 @@ describe('definePlugin', () => {
'const isSSR = false;'
)
})

// Specially defined constants found in packages/vite/src/node/plugins/define.ts
const specialDefines = {
'process.env.': '({}).',
'global.process.env.': '({}).',
'globalThis.process.env.': '({}).',
'process.env.NODE_ENV': '"test"',
'global.process.env.NODE_ENV': '"test"',
'globalThis.process.env.NODE_ENV': '"test"',
__vite_process_env_NODE_ENV: '"test"',
'import.meta.env.': '({}).',
'import.meta.env':
'{"BASE_URL":"/","MODE":"development","DEV":true,"PROD":false}',
'import.meta.hot': 'false'
}
const specialDefineKeys = Object.keys(specialDefines)

const specialDefinesSSR = {
...specialDefines,
// process.env is not replaced in SSR
'process.env.': null,
'global.process.env.': null,
'globalThis.process.env.': null,
'process.env.NODE_ENV': null,
'global.process.env.NODE_ENV': null,
'globalThis.process.env.NODE_ENV': null,
// __vite_process_env_NODE_ENV is a special variable that resolves to process.env.NODE_ENV, which is not replaced in SSR
__vite_process_env_NODE_ENV: 'process.env.NODE_ENV'
}

describe('ignores defined constants in string literals', async () => {
const singleQuotedDefines = specialDefineKeys
.map((define) => `let x = '${define}'`)
.join(';\n')
const doubleQuotedDefines = specialDefineKeys
.map((define) => `let x = "${define}"`)
.join(';\n')
const backtickedDefines = specialDefineKeys
.map((define) => `let x = \`${define}\``)
.join(';\n')
const singleQuotedDefinesMultilineNested = specialDefineKeys
.map((define) => `let x = \`\n'${define}'\n\``)
.join(';\n')
const doubleQuotedDefinesMultilineNested = specialDefineKeys
.map((define) => `let x = \`\n"${define}"\n\``)
.join(';\n')

const inputs = [
['double-quoted', doubleQuotedDefines],
['single-quoted', singleQuotedDefines],
['backticked', backtickedDefines],
['multiline nested double-quoted', doubleQuotedDefinesMultilineNested],
['multiline nested single-quoted', singleQuotedDefinesMultilineNested]
]

describe('non-SSR', async () => {
const transform = await createDefinePluginTransform()
test.each(inputs)('%s', async (label, input) => {
// transform() returns null when no replacement is made
expect(await transform(input)).toBe(null)
})
})

describe('SSR', async () => {
const ssrTransform = await createDefinePluginTransform({}, true, true)
test.each(inputs)('%s', async (label, input) => {
// transform() returns null when no replacement is made
expect(await ssrTransform(input)).toBe(null)
})
})
})

describe('replaces defined constants in template literal expressions', async () => {
describe('non-SSR', async () => {
const transform = await createDefinePluginTransform()

test.each(specialDefineKeys)('%s', async (key) => {
const result = await transform('let x = `${' + key + '}`')
expect(result).toBe('let x = `${' + specialDefines[key] + '}`')
})

// multiline tests
test.each(specialDefineKeys)('%s', async (key) => {
const result = await transform('let x = `\n${' + key + '}\n`')
expect(result).toBe('let x = `\n${' + specialDefines[key] + '}\n`')
})
})
describe('SSR', async () => {
const ssrTransform = await createDefinePluginTransform({}, true, true)

test.each(specialDefineKeys)('%s', async (key) => {
const result = await ssrTransform('let x = `${' + key + '}`')
expect(result).toBe(
specialDefinesSSR[key]
? 'let x = `${' + specialDefinesSSR[key] + '}`'
: null
)
})

// multiline tests
test.each(specialDefineKeys)('%s', async (key) => {
const result = await ssrTransform('let x = `\n${' + key + '}\n`')
expect(result).toBe(
specialDefinesSSR[key]
? 'let x = `\n${' + specialDefinesSSR[key] + '}\n`'
: null
)
})
})
})
})
Expand Up @@ -13,7 +13,7 @@ export const excludeSelf = /* #__PURE__ */ Object.assign({\\"./sibling.ts\\": ()
export const customQueryString = /* #__PURE__ */ Object.assign({\\"./sibling.ts\\": () => import(\\"./sibling.ts?custom\\")});
export const customQueryObject = /* #__PURE__ */ Object.assign({\\"./sibling.ts\\": () => import(\\"./sibling.ts?foo=bar&raw=true\\")});
export const parent = /* #__PURE__ */ Object.assign({});
export const rootMixedRelative = /* #__PURE__ */ Object.assign({\\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/esbuild.spec.ts\\": () => import(\\"../../esbuild.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/index.ts\\": () => import(\\"../fixture-b/index.ts?url\\").then(m => m[\\"default\\"])});
export const rootMixedRelative = /* #__PURE__ */ Object.assign({\\"/clientInjections.spec.ts\\": () => import(\\"../../clientInjections.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/esbuild.spec.ts\\": () => import(\\"../../esbuild.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/index.ts\\": () => import(\\"../fixture-b/index.ts?url\\").then(m => m[\\"default\\"])});
export const cleverCwd1 = /* #__PURE__ */ Object.assign({\\"./node_modules/framework/pages/hello.page.js\\": () => import(\\"./node_modules/framework/pages/hello.page.js\\")});
export const cleverCwd2 = /* #__PURE__ */ Object.assign({\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"),\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\"),\\"../fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts\\"),\\"../fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts\\")});
"
Expand All @@ -32,7 +32,7 @@ export const excludeSelf = /* #__PURE__ */ Object.assign({\\"./sibling.ts\\": ()
export const customQueryString = /* #__PURE__ */ Object.assign({\\"./sibling.ts\\": () => import(\\"./sibling.ts?custom&lang.ts\\")});
export const customQueryObject = /* #__PURE__ */ Object.assign({\\"./sibling.ts\\": () => import(\\"./sibling.ts?foo=bar&raw=true&lang.ts\\")});
export const parent = /* #__PURE__ */ Object.assign({});
export const rootMixedRelative = /* #__PURE__ */ Object.assign({\\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/esbuild.spec.ts\\": () => import(\\"../../esbuild.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/index.ts\\": () => import(\\"../fixture-b/index.ts?url&lang.ts\\").then(m => m[\\"default\\"])});
export const rootMixedRelative = /* #__PURE__ */ Object.assign({\\"/clientInjections.spec.ts\\": () => import(\\"../../clientInjections.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/esbuild.spec.ts\\": () => import(\\"../../esbuild.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url&lang.ts\\").then(m => m[\\"default\\"]),\\"/importGlob/fixture-b/index.ts\\": () => import(\\"../fixture-b/index.ts?url&lang.ts\\").then(m => m[\\"default\\"])});
export const cleverCwd1 = /* #__PURE__ */ Object.assign({\\"./node_modules/framework/pages/hello.page.js\\": () => import(\\"./node_modules/framework/pages/hello.page.js\\")});
export const cleverCwd2 = /* #__PURE__ */ Object.assign({\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"),\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\"),\\"../fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts\\"),\\"../fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts\\")});
"
Expand Down
34 changes: 34 additions & 0 deletions packages/vite/src/node/__tests__/utils.spec.ts
Expand Up @@ -7,6 +7,7 @@ import {
injectQuery,
isWindows,
posToNumber,
replaceInCode,
resolveHostname
} from '../utils'

Expand Down Expand Up @@ -236,3 +237,36 @@ describe('asyncFlatten', () => {
expect(arr).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9])
})
})

describe('replaceInCode', () => {
test('replaces pattern with string', () => {
expect(replaceInCode('let a1 = 1;', /\d/g, 'b')?.toString()).toBe(
'let ab = b;'
)
})

test('replaces pattern with replacement object', () => {
expect(
replaceInCode('let a1 = 2;', /\d/g, {
'1': 'b',
'2': 'c'
})?.toString()
).toBe('let ab = c;')
})

test('returns null if no replacement', () => {
expect(replaceInCode('let a = b;', /\d/g, {})).toBe(null)
})

test('ignores string literals', () => {
expect(replaceInCode('let a1 = "1";', /\d/g, 'b')?.toString()).toBe(
'let ab = "1";'
)
})

test('ignores comments', () => {
expect(
replaceInCode('let a /* 1 */ = 1; // 1', /\d/g, 'b')?.toString()
).toBe('let a /* 1 */ = b; // 1')
})
})