Skip to content

Commit

Permalink
feat: support new URL(url, import.meta.url) usage
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jun 22, 2021
1 parent edc903f commit 4cbb40d
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 3 deletions.
26 changes: 26 additions & 0 deletions docs/guide/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,29 @@ Note that:

- You should always reference `public` assets using root absolute path - for example, `public/icon.png` should be referenced in source code as `/icon.png`.
- Assets in `public` cannot be imported from JavaScript.

## new URL(url, import.meta.url)

[import.meta.url](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.meta) is a native ESM feature that exposes the current module's URL. Combining it with the native [URL constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL), we can obtain the full, resolved URL of a static asset using relative path from a JavaScript module:

This comment has been minimized.

Copy link
@michaeloliverx

michaeloliverx Sep 22, 2021

Contributor

This link does not work on the deployed version as it contains <wbr/>:

<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.<wbr/>meta" target="_blank" rel="noopener noreferrer">import.<wbr>meta.url</a>

```js
const imgUrl = new URL('./img.png', import.meta.url)

document.getElementById('hero-img').src = imgUrl
```
This works natively in modern browsers - in fact, Vite doesn't need to process this code at all during development!
This pattern also supports dynamic URLs via template literals:
```js
function getImageUrl(name) {
return new URL(`./dir/${name}.png`, import.meta.url).href
}
```
During the production build, Vite will perform necessary transforms so that the URLs still point to the correct location even after bundling and asset hashing.
::: warning Note: Does not work with SSR
This pattern does not work if you are using Vite for Server-Side Rendering, because `import.meta.url` have different semantics in browsers vs. Node.js. The server bundle also cannot determine the client host URL ahead of time.
:::
13 changes: 13 additions & 0 deletions packages/playground/assets/__tests__/assets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ test('?url import', async () => {
)
})

test('new URL(..., import.meta.url)', async () => {
expect(await page.textContent('.import-meta-url')).toMatch(assetMatch)
})

test('new URL(`${dynamic}`, import.meta.url)', async () => {
expect(await page.textContent('.dynamic-import-meta-url-1')).toMatch(
isBuild ? 'data:image/png;base64' : '/foo/nested/icon.png'
)
expect(await page.textContent('.dynamic-import-meta-url-2')).toMatch(
assetMatch
)
})

if (isBuild) {
test('manifest', async () => {
const manifest = readManifest('foo')
Expand Down
27 changes: 27 additions & 0 deletions packages/playground/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ <h2>?raw import</h2>
<h2>?url import</h2>
<code class="url"></code>

<h2>new URL('...', import.meta.url)</h2>
<img class="import-meta-url-img" />
<code class="import-meta-url"></code>

<h2>new URL(`./${dynamic}`, import.meta.url)</h2>
<p>
<img class="dynamic-import-meta-url-img-1" />
<code class="dynamic-import-meta-url-1"></code>
</p>
<p>
<img class="dynamic-import-meta-url-img-2" />
<code class="dynamic-import-meta-url-2"></code>
</p>

<script type="module">
import './css/fonts.css'
import './css/css-url.css'
Expand All @@ -144,6 +158,19 @@ <h2>?url import</h2>
import fooUrl from './foo.js?url'
text('.url', fooUrl)

const metaUrl = new URL('./nested/asset.png', import.meta.url)
text('.import-meta-url', metaUrl)
document.querySelector('.import-meta-url-img').src = metaUrl

function testDynamicImportMetaUrl(name, i) {
const metaUrl = new URL(`./nested/${name}.png`, import.meta.url)
text(`.dynamic-import-meta-url-${i}`, metaUrl)
document.querySelector(`.dynamic-import-meta-url-img-${i}`).src = metaUrl
}

testDynamicImportMetaUrl('icon', 1)
testDynamicImportMetaUrl('asset', 2)

function text(el, text) {
document.querySelector(el).textContent = text
}
Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { ssrManifestPlugin } from './ssr/ssrManifestPlugin'
import { isCSSRequest } from './plugins/css'
import { DepOptimizationMetadata } from './optimizer'
import { scanImports } from './optimizer/scan'
import { assetImportMetaUrlPlugin } from './plugins/assetImportMetaUrl'

export interface BuildOptions {
/**
Expand Down Expand Up @@ -270,6 +271,7 @@ export function resolveBuildPlugins(config: ResolvedConfig): {
warnOnError: true,
exclude: [/node_modules/]
}),
assetImportMetaUrlPlugin(config),
...(options.rollupOptions.plugins || [])
],
post: [
Expand Down
8 changes: 5 additions & 3 deletions packages/vite/src/node/importGlob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export async function transformImportGlob(
base: string
}> {
const isEager = source.slice(pos, pos + 21) === 'import.meta.globEager'
const isEagerDefault =
isEager && source.slice(pos + 21, pos + 28) === 'Default'

const err = (msg: string) => {
const e = new Error(`Invalid glob import syntax: ${msg}`)
Expand Down Expand Up @@ -79,9 +81,9 @@ export async function transformImportGlob(
imports.push(importee)
const identifier = `__glob_${importIndex}_${i}`
if (isEager) {
importsString += `import * as ${identifier} from ${JSON.stringify(
importee
)};`
importsString += `import ${
isEagerDefault ? `` : `* as `
}${identifier} from ${JSON.stringify(importee)};`
entries += ` ${JSON.stringify(file)}: ${identifier},`
} else {
let imp = `import(${JSON.stringify(importee)})`
Expand Down
96 changes: 96 additions & 0 deletions packages/vite/src/node/plugins/assetImportMetaUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Plugin } from '../plugin'
import MagicString from 'magic-string'
import path from 'path'
import { fileToUrl } from './asset'
import { ResolvedConfig } from '../config'

const importMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\)/g

/**
* Convert `new URL('./foo.png', import.meta.url)` to its resolved built URL
*
* Supports tempalte string with dynamic segments:
* ```
* new URL(`./dir/${name}.png`, import.meta.url)
* // transformed to
* import.meta.globEager('./dir/**.png')[`./dir/${name}.png`].default
* ```
*/
export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
return {
name: 'asset-import-meta-url',
async transform(code, id, ssr) {
if (code.includes('new URL') && code.includes(`import.meta.url`)) {
let s: MagicString | null = null
let match: RegExpExecArray | null
while ((match = importMetaUrlRE.exec(code))) {
const { 0: exp, 1: rawUrl, index } = match

if (ssr) {
this.error(
`\`new URL(url, import.meta.url)\` is not supported in SSR.`,
index
)
}

if (!s) s = new MagicString(code)

// potential dynamic template string
if (rawUrl[0] === '`' && /\$\{/.test(rawUrl)) {
const ast = this.parse(rawUrl)
const templateLiteral = (ast as any).body[0].expression
if (templateLiteral.expressions.length) {
const pattern = buildGlobPattern(templateLiteral)
// Note: native import.meta.url is not supported in the baseline
// target so we use window.location here -
s.overwrite(
index,
index + exp.length,
`new URL(import.meta.globEagerDefault(${JSON.stringify(
pattern
)})[${rawUrl}], window.location)`
)
continue
}
}

const url = rawUrl.slice(1, -1)
const file = path.resolve(path.dirname(id), url)
const builtUrl = await fileToUrl(file, config, this)
s.overwrite(
index,
index + exp.length,
`new URL(${JSON.stringify(builtUrl)}, window.location)`
)
}
if (s) {
return {
code: s.toString(),
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null
}
}
}
return null
}
}
}

function buildGlobPattern(ast: any) {
let pattern = ''
let lastElementIndex = -1
for (const exp of ast.expressions) {
for (let i = lastElementIndex + 1; i < ast.quasis.length; i++) {
const el = ast.quasis[i]
if (el.end < exp.start) {
pattern += el.value.raw
lastElementIndex = i
}
}
pattern += '**'
}
for (let i = lastElementIndex + 1; i < ast.quasis.length; i++) {
pattern += ast.quasis[i].value.raw
}
return pattern
}

0 comments on commit 4cbb40d

Please sign in to comment.