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

fix(dev): transform import for commonjs dependencies #837

Merged
merged 23 commits into from
Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,11 @@
"postcss-nesting": "^7.0.1",
"preact": "^10.4.1",
"prettier": "^2.0.4",
"prop-types": "^15.7.2",
"pug": "^2.0.4",
"puppeteer": "^3.0.0",
"react": "^16.13.1",
csr632 marked this conversation as resolved.
Show resolved Hide resolved
"react-dom": "^16.13.1",
"rimraf": "^3.0.2",
"sass": "^1.26.5",
"source-map-support": "^0.5.19",
Expand Down
7 changes: 5 additions & 2 deletions playground/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
<TestWasm />
</Suspense>
<TestScriptSetupStyleVars msg="Test message" />
<TestSyntax/>
<TestSyntax />
<TestCjsDepNamedExport />
</template>

<script>
Expand Down Expand Up @@ -62,6 +63,7 @@ import TestWebWorker from './worker/TestWorker.vue'
import TestWasm from './wasm/TestWasm.vue'
import TestScriptSetupStyleVars from './script-setup/TestScriptSetupStyleVars.vue'
import TestSyntax from './TestSyntax.vue'
import TestCjsDepNamedExport from './cjs-dep-named-export/TestCjsDepNamedExport.vue'

const App = {
components: {
Expand Down Expand Up @@ -92,7 +94,8 @@ const App = {
TestWebWorker,
TestWasm,
TestScriptSetupStyleVars,
TestSyntax
TestSyntax,
TestCjsDepNamedExport
}
}
export { App as default}
Expand Down
60 changes: 60 additions & 0 deletions playground/cjs-dep-named-export/TestCjsDepNamedExport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<h2>Optimize cjs dep with named export</h2>
<p class="cjs-dep-named-export-static">
static import result: {{ staticImport }}
</p>
<p class="cjs-dep-named-export-dynamic">
dynamic import result: {{ dynamicImport }}
</p>
<button class="cjs-dep-named-export-dynamic-load" @click="loadDynamic()">
load dynamic
</button>
</template>

<script>
import { ref } from 'vue'

import React, { useState, createContext } from 'react'
import { default as React2, useState as useState2 } from 'react'
import * as ReactStar from 'react'

import PropTypes, { oneOfType } from 'prop-types'

export default {
setup() {
let staticImport
if (
isFunction(React.useState) &&
isFunction(useState) &&
isFunction(createContext) &&
isFunction(React2.useState) &&
isFunction(useState2) &&
isFunction(ReactStar.useState) &&
isFunction(PropTypes.oneOfType) &&
isFunction(oneOfType)
) {
staticImport = 'success'
} else {
staticImport = 'fail'
}

const dynamicImport = ref('dynamic not loaded')
function loadDynamic() {
// dynamic import cjs dep and get named-export
import('react-dom').then(({ render }) => {
dynamicImport.value = isFunction(render) ? 'success' : 'fail'
})
}

return {
staticImport,
dynamicImport,
loadDynamic
}
}
}

function isFunction(v) {
return typeof v === 'function'
}
</script>
3 changes: 3 additions & 0 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"moment": "link:../node_modules/moment",
"@babel/runtime": "link:../node_modules/@babel/runtime",
"normalize.css": "link:../node_modules/normalize.css",
"react": "link:../node_modules/react",
"react-dom": "link:../node_modules/react-dom",
"prop-types": "link:../node_modules/prop-types",
"optimize-linked": "link:./optimize-linked",
"resolve-browser-field-test-package": "link:./resolve/browser-field",
"rewrite-optimized-test-package": "link:./resolve/rewrite-optimized/test-package",
Expand Down
51 changes: 51 additions & 0 deletions playground/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
version "0.0.0"
uid ""

"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==

"linked-dep@link:./optimize-linked/linked-dep":
version "0.0.0"
uid ""
Expand All @@ -18,6 +23,13 @@
version "0.0.0"
uid ""

loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"

"moment@link:../node_modules/moment":
version "0.0.0"
uid ""
Expand All @@ -26,10 +38,41 @@
version "0.0.0"
uid ""

object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=

"optimize-linked@link:./optimize-linked":
version "0.0.0"
uid ""

prop-types@^15.6.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"

"prop-types@link:../node_modules/prop-types":
version "0.0.0"
uid ""

"react-dom@link:../node_modules/react-dom":
version "0.0.0"
uid ""

react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==

"react@link:../node_modules/react":
version "0.0.0"
uid ""

regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
Expand All @@ -46,3 +89,11 @@ regenerator-runtime@^0.13.4:
"rewrite-unoptimized-test-package@link:./resolve/rewrite-unoptimized/test-package":
version "0.0.0"
uid ""

scheduler@^0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
31 changes: 31 additions & 0 deletions src/node/optimizer/entryAnalysisPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Plugin } from 'rollup'
import { init, parse } from 'es-module-lexer'
import * as fs from 'fs-extra'

export function entryAnalysisPlugin(): Plugin {
const analysis: { mayBeCjs: { [name: string]: true } } = { mayBeCjs: {} }
return {
name: 'vite:cjs-entry-named-export',
async generateBundle(options, bundles) {
await init
Object.values(bundles).forEach((bundle) => {
if (bundle.type === 'chunk' && bundle.isEntry) {
if (bundle.facadeModuleId) {
const [, exports] = parse(
fs.readFileSync(bundle.facadeModuleId, 'utf-8')
)
if (exports.length === 0) {
// likely commonjs
analysis.mayBeCjs[bundle.name] = true
}
}
}
})
this.emitFile({
type: 'asset',
fileName: '_analysis.json',
source: JSON.stringify(analysis)
})
}
}
}
6 changes: 6 additions & 0 deletions src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { init, parse } from 'es-module-lexer'
import chalk from 'chalk'
import { Ora } from 'ora'
import { createDepAssetPlugin, depAssetExternalPlugin } from './pluginAssets'
import { entryAnalysisPlugin } from './entryAnalysisPlugin'

const debug = require('debug')('vite:optimize')

Expand Down Expand Up @@ -187,6 +188,7 @@ export async function optimizeDeps(
...config.rollupInputOptions,
plugins: [
depAssetExternalPlugin,
entryAnalysisPlugin(),
...(await createBaseRollupPlugins(root, resolver, config)),
createDepAssetPlugin(resolver, root)
]
Expand All @@ -209,6 +211,10 @@ export async function optimizeDeps(
await fs.ensureDir(path.dirname(filePath))
await fs.writeFile(filePath, chunk.code)
}
if (chunk.type === 'asset' && chunk.fileName === '_analysis.json') {
const filePath = path.join(cacheDir, chunk.fileName)
await fs.writeFile(filePath, chunk.source)
}
}

await fs.writeFile(hashPath, depHash)
Expand Down
107 changes: 100 additions & 7 deletions src/node/server/serverPluginModuleRewrite.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { ServerPlugin } from '.'
import path from 'path'
import * as fs from 'fs-extra'
import LRUCache from 'lru-cache'
import MagicString from 'magic-string'
import {
init as initLexer,
parse as parseImports,
ImportSpecifier
} from 'es-module-lexer'
import { ImportDeclaration } from '@babel/types'
import { makeLegalIdentifier } from '@rollup/pluginutils'

import {
InternalResolver,
resolveBareModuleRequest,
Expand All @@ -32,7 +36,8 @@ import {
import chalk from 'chalk'
import { isCSSRequest } from '../utils/cssUtils'
import { envPublicPath } from './serverPluginEnv'
import fs from 'fs-extra'
import { resolveOptimizedCacheDir } from '../optimizer'
import { parse } from '../utils/babelParse'

const debug = require('debug')('vite:rewrite')

Expand Down Expand Up @@ -156,7 +161,13 @@ export function rewriteImports(
importeeMap.set(importer, currentImportees)

for (let i = 0; i < imports.length; i++) {
const { s: start, e: end, d: dynamicIndex } = imports[i]
const {
s: start,
e: end,
d: dynamicIndex,
ss: expStart,
se: expEnd
} = imports[i]
let id = source.substring(start, end)
let hasLiteralDynamicId = false
if (dynamicIndex >= 0) {
Expand All @@ -182,11 +193,27 @@ export function rewriteImports(

if (resolved !== id) {
debug(` "${id}" --> "${resolved}"`)
s.overwrite(
start,
end,
hasLiteralDynamicId ? `'${resolved}'` : resolved
)
if (isOptimizedCjs(root, id)) {
if (dynamicIndex === -1) {
const exp = source.substring(expStart, expEnd)
const replacement = transformCjsImport(exp, id, resolved, i)
s.overwrite(expStart, expEnd, replacement)
} else if (hasLiteralDynamicId) {
// es-module-lexer give us wrong expEnd for dynamic import:
// https://github.com/guybedford/es-module-lexer/issues/53
// So we can't use expEnd for now.
// For example, for import('path')
// replace the 'path' with '${resolved}').then(m=>m.default
// will give us import('${resolved}').then(m=>m.default)
s.overwrite(start, end, `'${resolved}').then(m=>m.default`)
}
} else {
s.overwrite(
start,
end,
hasLiteralDynamicId ? `'${resolved}'` : resolved
)
}
hasReplaced = true
}

Expand Down Expand Up @@ -300,3 +327,69 @@ export const resolveImport = (
}
return id
}

const analysisCache = new Map<string, { mayBeCjs: { [name: string]: true } }>()

/**
* get analysis result from optimize step:
* which optimized dependencies may be commonjs
*/
function getAnalysis(root: string): { mayBeCjs: { [name: string]: true } } {
if (analysisCache.has(root)) return analysisCache.get(root)!
const cacheDir = resolveOptimizedCacheDir(root)
if (!cacheDir) throw new Error('cacheDir not found')
const analysis = fs.readJsonSync(path.join(cacheDir, '_analysis.json'))
csr632 marked this conversation as resolved.
Show resolved Hide resolved
analysisCache.set(root, analysis)
return analysis
}

function isOptimizedCjs(root: string, id: string) {
const analysis = getAnalysis(root)
return !!analysis.mayBeCjs[id]
}

function transformCjsImport(
exp: string,
id: string,
resolvedPath: string,
importIndex: number
): string {
const ast = parse(exp)[0] as ImportDeclaration
const importNames: { importedName: string; localName: string }[] = []

ast.specifiers.forEach((obj) => {
if (obj.type === 'ImportSpecifier') {
const importedName = obj.imported.name
const localName = obj.local.name
importNames.push({ importedName, localName })
} else if (obj.type === 'ImportDefaultSpecifier') {
importNames.push({ importedName: 'default', localName: obj.local.name })
} else if (obj.type === 'ImportNamespaceSpecifier') {
importNames.push({ importedName: '*', localName: obj.local.name })
}
})

return generateCjsImport(importNames, id, resolvedPath, importIndex)
}

function generateCjsImport(
importNames: { importedName: string; localName: string }[],
id: string,
resolvedPath: string,
importIndex: number
): string {
// If there is multiple import for same id in one file,
// importIndex will prevent the cjsModuleName to be duplicate
const cjsModuleName = makeLegalIdentifier(
`$viteCjsImport${importIndex}_${id}`
)
const lines: string[] = [`import ${cjsModuleName} from "${resolvedPath}";`]
importNames.forEach(({ importedName, localName }) => {
if (importedName === '*' || importedName === 'default') {
lines.push(`const ${localName} = ${cjsModuleName};`)
} else {
lines.push(`const ${localName} = ${cjsModuleName}["${importedName}"];`)
}
})
return lines.join('\n')
}