)/, `$1${html}`)
+ )
+})
diff --git a/example/tsconfig.json b/example/tsconfig.json
new file mode 100644
index 000000000..f27872932
--- /dev/null
+++ b/example/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "sourceMap": true
+ }
+}
\ No newline at end of file
diff --git a/example/webpack.config.js b/example/webpack.config.js
index 0e2d73065..59aa31026 100644
--- a/example/webpack.config.js
+++ b/example/webpack.config.js
@@ -7,104 +7,162 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = (env = {}) => {
const isProd = env.prod
- const minimize = isProd && !env.noMinimize
- const babel = isProd && !env.noBabel
+ const isSSR = env.ssr
- return {
- mode: isProd ? 'production' : 'development',
- entry: path.resolve(__dirname, './main.js'),
- devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
- output: {
- path: path.resolve(__dirname, 'dist'),
- filename: '[name].js',
- publicPath: '/dist/'
- },
- module: {
- rules: [
- {
- test: /\.vue$/,
- loader: 'vue-loader'
- },
- {
- test: /\.png$/,
- use: [
- {
- loader: 'url-loader',
- options: {
- limit: 8192
- }
- }
- ]
- },
- {
- test: /\.css$/,
- use: [
- {
- loader: MiniCssExtractPlugin.loader,
- options: {
- hmr: !isProd
+ /**
+ * Some notes regarding config for the server build of an SSR app:
+ * 1. target: 'node'
+ * 2. output.libraryTarget: 'commonjs' (so the exported app can be required)
+ * 3. externals: this is mostly for faster builds.
+ * - externalize @vue/* deps via commonjs require()
+ * - externalize client side deps that are never used on the server, e.g.
+ * ones that are only used in onMounted() to empty modules
+ * 4. If using cache-loader or any other forms of cache, make sure the cache
+ * key takes client vs. server builds into account!
+ */
+ const genConfig = (isServerBuild = false) => {
+ const minimize = isProd && !isServerBuild && !env.noMinimize
+ const useBabel = isProd && !isServerBuild && !env.noBabel
+
+ return {
+ mode: isProd ? 'production' : 'development',
+ entry: path.resolve(__dirname, './main.js'),
+ target: isServerBuild ? 'node' : 'web',
+ devtool: 'source-map',
+ output: {
+ path: path.resolve(
+ __dirname,
+ isSSR ? (isServerBuild ? 'dist-ssr/server' : 'dist-ssr/dist') : 'dist'
+ ),
+ filename: '[name].js',
+ publicPath: '/dist/',
+ libraryTarget: isServerBuild ? 'commonjs' : undefined,
+ },
+ externals: isServerBuild
+ ? [
+ (ctx, request, cb) => {
+ if (/^@vue/.test(request)) {
+ return cb(null, 'commonjs ' + request)
}
+ cb()
},
- 'css-loader'
]
- },
- {
- test: /\.js$/,
- use: [
- {
- loader: 'cache-loader',
- options: {
- cacheIdentifier: hash(
- fs.readFileSync(path.resolve(__dirname, '../package.json')) +
- JSON.stringify(env)
- ),
- cacheDirectory: path.resolve(__dirname, '../.cache')
- }
+ : undefined,
+ module: {
+ rules: [
+ {
+ test: /\.vue$/,
+ loader: 'vue-loader',
+ options: {
+ refSugar: true,
+ // enableTsInTemplate: false,
},
- ...(babel
- ? [
- {
- loader: 'babel-loader',
- options: {
- // use yarn build-example --env.noMinimize to verify that
- // babel is properly applied to all js code, including the
- // render function compiled from SFC templates.
- presets: ['@babel/preset-env']
- }
- }
- ]
- : [])
- ]
+ },
+ {
+ test: /\.png$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 8192,
+ },
+ },
+ ],
+ },
+ {
+ test: /\.css$/,
+ use: [
+ {
+ loader: MiniCssExtractPlugin.loader,
+ options: {
+ hmr: !isProd,
+ },
+ },
+ 'css-loader',
+ ],
+ },
+ // {
+ // test: /\.js$/,
+ // use: [
+ // {
+ // loader: 'cache-loader',
+ // options: {
+ // cacheIdentifier: hash(
+ // // deps
+ // fs.readFileSync(
+ // path.resolve(__dirname, '../package.json')
+ // ) +
+ // // env
+ // JSON.stringify(env) +
+ // // client vs. server build
+ // isServerBuild
+ // ),
+ // cacheDirectory: path.resolve(__dirname, '../.cache'),
+ // },
+ // },
+ // ...(useBabel
+ // ? [
+ // {
+ // loader: 'babel-loader',
+ // options: {
+ // // use yarn build-example --env.noMinimize to verify that
+ // // babel is properly applied to all js code, including the
+ // // render function compiled from SFC templates.
+ // presets: ['@babel/preset-env'],
+ // },
+ // },
+ // ]
+ // : []),
+ // ],
+ // },
+ {
+ test: /\.ts$/,
+ use: [
+ {
+ loader: 'ts-loader',
+ options: {
+ appendTsSuffixTo: [/\.vue$/],
+ },
+ },
+ ],
+ },
+ // target
custom blocks
+ {
+ resourceQuery: /blockType=docs/,
+ loader: require.resolve('./docs-loader'),
+ },
+ ],
+ },
+ plugins: [
+ new VueLoaderPlugin(),
+ new MiniCssExtractPlugin({
+ filename: '[name].css',
+ }),
+ new webpack.DefinePlugin({
+ __IS_SSR__: !!isSSR,
+ __VUE_OPTIONS_API__: true,
+ __VUE_PROD_DEVTOOLS__: false,
+ }),
+ ],
+ optimization: {
+ minimize,
+ },
+ devServer: {
+ stats: 'minimal',
+ contentBase: __dirname,
+ overlay: true,
+ },
+ resolveLoader: {
+ alias: {
+ 'vue-loader': require.resolve('../'),
},
- // target custom blocks
- {
- resourceQuery: /blockType=docs/,
- loader: require.resolve('./docs-loader')
- }
- ]
- },
- plugins: [
- new VueLoaderPlugin(),
- new MiniCssExtractPlugin({
- filename: '[name].css'
- }),
- new webpack.DefinePlugin({
- __VUE_OPTIONS_API__: true,
- __VUE_PROD_DEVTOOLS__: false
- })
- ],
- optimization: {
- minimize
- },
- devServer: {
- stats: 'minimal',
- contentBase: __dirname,
- overlay: true
- },
- resolveLoader: {
- alias: {
- 'vue-loader': require.resolve('../')
- }
+ },
}
}
+
+ if (!isSSR) {
+ return genConfig()
+ } else {
+ return [genConfig(), genConfig(true)]
+ }
}
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 000000000..3a81a10f3
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,24 @@
+const config = {
+ preset: 'ts-jest',
+ testTimeout: 60000,
+ testEnvironment: 'node',
+ testPathIgnorePatterns: ['/dist/', '/node_modules/'],
+}
+
+if (process.env.WEBPACK5) {
+ module.exports = {
+ ...config,
+
+ globals: {
+ 'ts-jest': {
+ diagnostics: false,
+ },
+ },
+ moduleNameMapper: {
+ '^webpack$': 'webpack5',
+ '^webpack/(.*)': 'webpack5/$1',
+ },
+ }
+} else {
+ module.exports = config
+}
diff --git a/package.json b/package.json
index cc2ace8e9..9e0977d88 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "vue-loader",
- "version": "16.0.0-beta.7",
+ "version": "16.8.3",
"license": "MIT",
"author": "Evan You",
"main": "dist/index.js",
@@ -12,72 +12,82 @@
"dev": "tsc --watch",
"build": "tsc",
"pretest": "tsc",
- "test": "jest",
+ "test": "jest --coverage",
+ "test:webpack5": "WEBPACK5=true jest -c --coverage",
"dev-example": "webpack-dev-server --config example/webpack.config.js --inline --hot",
"build-example": "rm -rf example/dist && webpack --config example/webpack.config.js --env.prod",
- "lint": "prettier --write --parser typescript \"{src,test}/**/*.ts\"",
- "prepublishOnly": "tsc"
+ "build-example-ssr": "rm -rf example/dist-ssr && webpack --config example/webpack.config.js --env.prod --env.ssr && node example/ssr.js",
+ "lint": "prettier --write --parser typescript \"{src,test}/**/*.{j,t}s\"",
+ "prepublishOnly": "tsc && conventional-changelog -p angular -i CHANGELOG.md -s -r 2"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.js": [
- "prettier --write",
- "git add"
+ "prettier --write"
],
"*.ts": [
- "prettier --parser=typescript --write",
- "git add"
+ "prettier --parser=typescript --write"
]
},
"dependencies": {
- "@types/mini-css-extract-plugin": "^0.9.1",
- "chalk": "^3.0.0",
+ "chalk": "^4.1.0",
"hash-sum": "^2.0.0",
- "loader-utils": "^1.2.3",
- "merge-source-map": "^1.1.0",
- "source-map": "^0.6.1"
+ "loader-utils": "^2.0.0"
+ },
+ "peerDependencies": {
+ "webpack": "^4.1.0 || ^5.0.0-0"
},
"devDependencies": {
"@babel/core": "^7.7.7",
- "@babel/preset-env": "^7.7.7",
- "@types/estree": "^0.0.42",
+ "@babel/preset-env": "^7.11.5",
+ "@intlify/vue-i18n-loader": "^3.0.0",
+ "@types/estree": "^0.0.45",
"@types/hash-sum": "^1.0.0",
- "@types/jest": "^25.2.1",
- "@types/loader-utils": "^1.1.3",
+ "@types/jest": "^26.0.13",
+ "@types/jsdom": "^16.2.13",
+ "@types/loader-utils": "^2.0.1",
+ "@types/mini-css-extract-plugin": "^0.9.1",
"@types/webpack": "^4.41.0",
"@types/webpack-merge": "^4.1.5",
- "@vue/compiler-sfc": "^3.0.0-rc.10",
- "babel-loader": "^8.0.6",
+ "@vue/compiler-sfc": "^3.2.12",
+ "babel-loader": "^8.1.0",
"cache-loader": "^4.1.0",
- "css-loader": "^3.3.2",
- "file-loader": "^5.0.2",
+ "conventional-changelog-cli": "^2.1.1",
+ "css-loader": "^4.3.0",
+ "file-loader": "^6.1.0",
+ "html-webpack-plugin": "^4.5.0",
+ "html-webpack-plugin-v5": "npm:html-webpack-plugin@^5.3.2",
"jest": "^26.4.1",
- "lint-staged": "^9.5.0",
+ "jsdom": "^16.4.0",
+ "lint-staged": "^10.3.0",
+ "markdown-loader": "^6.0.0",
"memfs": "^3.1.2",
- "mini-css-extract-plugin": "^0.8.0",
- "prettier": "^1.19.1",
- "pug": "^2.0.4",
+ "mini-css-extract-plugin": "^0.11.2",
+ "normalize-newline": "^3.0.0",
+ "null-loader": "^4.0.1",
+ "postcss-loader": "^4.0.4",
+ "prettier": "^2.1.1",
+ "pug": "^2.0.0",
"pug-plain-loader": "^1.0.0",
+ "source-map": "^0.6.1",
+ "style-loader": "^2.0.0",
"stylus": "^0.54.7",
- "stylus-loader": "^3.0.2",
+ "stylus-loader": "^4.1.1",
+ "sugarss": "^3.0.1",
"ts-jest": "^26.2.0",
- "typescript": "^4.0.2",
- "url-loader": "^3.0.0",
- "vue": "^3.0.0-rc.10",
+ "ts-loader": "^8.0.6",
+ "ts-loader-v9": "npm:ts-loader@^9.2.4",
+ "typescript": "^4.4.3",
+ "url-loader": "^4.1.0",
+ "vue": "^3.2.13",
+ "vue-i18n": "^9.1.7",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
- "webpack-merge": "^4.2.2",
+ "webpack-merge": "^5.1.4",
+ "webpack5": "npm:webpack@5",
"yorkie": "^2.0.0"
- },
- "jest": {
- "preset": "ts-jest",
- "testEnvironment": "node",
- "testPathIgnorePatterns": [
- "/dist/",
- "/node_modules/"
- ]
}
}
diff --git a/src/compiler.ts b/src/compiler.ts
new file mode 100644
index 000000000..e7ece1dfc
--- /dev/null
+++ b/src/compiler.ts
@@ -0,0 +1,25 @@
+// extend the descriptor so we can store the scopeId on it
+declare module '@vue/compiler-sfc' {
+ interface SFCDescriptor {
+ id: string
+ }
+}
+
+import * as _compiler from '@vue/compiler-sfc'
+
+export let compiler: typeof _compiler
+
+try {
+ // Vue 3.2.13+ ships the SFC compiler directly under the `vue` package
+ // making it no longer necessary to have @vue/compiler-sfc separately installed.
+ compiler = require('vue/compiler-sfc')
+} catch (e) {
+ try {
+ compiler = require('@vue/compiler-sfc')
+ } catch (e) {
+ throw new Error(
+ `@vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc ` +
+ `to be present in the dependency tree.`
+ )
+ }
+}
diff --git a/src/descriptorCache.ts b/src/descriptorCache.ts
new file mode 100644
index 000000000..4c592a0d1
--- /dev/null
+++ b/src/descriptorCache.ts
@@ -0,0 +1,34 @@
+import * as fs from 'fs'
+import type { SFCDescriptor } from '@vue/compiler-sfc'
+import { compiler } from './compiler'
+
+const cache = new Map()
+
+export function setDescriptor(filename: string, entry: SFCDescriptor) {
+ cache.set(cleanQuery(filename), entry)
+}
+
+export function getDescriptor(filename: string): SFCDescriptor {
+ filename = cleanQuery(filename)
+ if (cache.has(filename)) {
+ return cache.get(filename)!
+ }
+
+ // This function should only be called after the descriptor has been
+ // cached by the main loader.
+ // If this is somehow called without a cache hit, it's probably due to sub
+ // loaders being run in separate threads. The only way to deal with this is to
+ // read from disk directly...
+ const source = fs.readFileSync(filename, 'utf-8')
+ const { descriptor } = compiler.parse(source, {
+ filename,
+ sourceMap: true,
+ })
+ cache.set(filename, descriptor)
+ return descriptor
+}
+
+function cleanQuery(str: string) {
+ const i = str.indexOf('?')
+ return i > 0 ? str.slice(0, i) : str
+}
diff --git a/src/exportHelper.ts b/src/exportHelper.ts
new file mode 100644
index 000000000..9f98b41e5
--- /dev/null
+++ b/src/exportHelper.ts
@@ -0,0 +1,9 @@
+// runtime helper for setting properties on components
+// in a tree-shakable way
+export default (sfc: any, props: [string, string][]) => {
+ const target = sfc.__vccOpts || sfc
+ for (const [key, val] of props) {
+ target[key] = val
+ }
+ return target
+}
diff --git a/src/formatError.ts b/src/formatError.ts
index 5b73bc73b..44ea88b66 100644
--- a/src/formatError.ts
+++ b/src/formatError.ts
@@ -1,5 +1,6 @@
-import { generateCodeFrame, CompilerError } from '@vue/compiler-sfc'
-import chalk from 'chalk'
+import type { CompilerError } from '@vue/compiler-sfc'
+import { compiler } from './compiler'
+import chalk = require('chalk')
export function formatError(
err: SyntaxError | CompilerError,
@@ -12,7 +13,11 @@ export function formatError(
}
const locString = `:${loc.start.line}:${loc.start.column}`
const filePath = chalk.gray(`at ${file}${locString}`)
- const codeframe = generateCodeFrame(source, loc.start.offset, loc.end.offset)
+ const codeframe = compiler.generateCodeFrame(
+ source,
+ loc.start.offset,
+ loc.end.offset
+ )
err.message = `\n${chalk.red(
`VueCompilerError: ${err.message}`
)}\n${filePath}\n${chalk.yellow(codeframe)}\n`
diff --git a/src/hotReload.ts b/src/hotReload.ts
index d29883b20..a445b650a 100644
--- a/src/hotReload.ts
+++ b/src/hotReload.ts
@@ -7,11 +7,12 @@ export function genHotReloadCode(
return `
/* hot reload */
if (module.hot) {
- script.__hmrId = "${id}"
+ __exports__.__hmrId = "${id}"
const api = __VUE_HMR_RUNTIME__
module.hot.accept()
- if (!api.createRecord('${id}', script)) {
- api.reload('${id}', script)
+ if (!api.createRecord('${id}', __exports__)) {
+ console.log('reload')
+ api.reload('${id}', __exports__)
}
${templateRequest ? genTemplateHotReloadCode(id, templateRequest) : ''}
}
@@ -21,6 +22,7 @@ if (module.hot) {
function genTemplateHotReloadCode(id: string, request: string) {
return `
module.hot.accept(${request}, () => {
+ console.log('re-render')
api.rerender('${id}', render)
})
`
diff --git a/src/index.ts b/src/index.ts
index 67c9e2e3c..d74373ef5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,27 +1,17 @@
-try {
- require.resolve('@vue/compiler-sfc')
-} catch (e) {
- throw new Error(
- 'vue-loader requires @vue/compiler-sfc to be present in the dependency ' +
- 'tree.'
- )
-}
+import webpack = require('webpack')
+import * as path from 'path'
+import * as qs from 'querystring'
+import * as loaderUtils from 'loader-utils'
+
+import hash = require('hash-sum')
-import webpack from 'webpack'
-import path from 'path'
-import qs from 'querystring'
-import hash from 'hash-sum'
-import loaderUtils from 'loader-utils'
-import {
- parse,
- compileScript,
+import { compiler } from './compiler'
+import type {
TemplateCompiler,
CompilerOptions,
SFCBlock,
SFCTemplateCompileOptions,
SFCScriptCompileOptions,
- SFCStyleBlock,
- SFCScriptBlock
} from '@vue/compiler-sfc'
import { selectBlock } from './select'
import { genHotReloadCode } from './hotReload'
@@ -29,6 +19,8 @@ import { genCSSModulesCode } from './cssModules'
import { formatError } from './formatError'
import VueLoaderPlugin from './plugin'
+import { canInlineTemplate } from './resolveScript'
+import { setDescriptor } from './descriptorCache'
export { VueLoaderPlugin }
@@ -38,13 +30,21 @@ export interface VueLoaderOptions {
transformAssetUrls?: SFCTemplateCompileOptions['transformAssetUrls']
compiler?: TemplateCompiler | string
compilerOptions?: CompilerOptions
+ refSugar?: boolean
+ customElement?: boolean | RegExp
+
hotReload?: boolean
exposeFilename?: boolean
appendExtension?: boolean
+ enableTsInTemplate?: boolean
+
+ isServerBuild?: boolean
}
let errorEmitted = false
+const exportHelperPath = JSON.stringify(require.resolve('./exportHelper'))
+
export default function loader(
this: webpack.loader.LoaderContext,
source: string
@@ -75,7 +75,7 @@ export default function loader(
sourceMap,
rootContext,
resourcePath,
- resourceQuery
+ resourceQuery = '',
} = loaderContext
const rawQuery = resourceQuery.slice(1)
@@ -83,148 +83,176 @@ export default function loader(
const options = (loaderUtils.getOptions(loaderContext) ||
{}) as VueLoaderOptions
- const isServer = target === 'node'
- const isProduction = mode === 'production'
+ const isServer = options.isServerBuild ?? target === 'node'
+ const isProduction =
+ mode === 'production' || process.env.NODE_ENV === 'production'
- const { descriptor, errors } = parse(source, {
- filename: resourcePath,
- sourceMap
+ const filename = resourcePath.replace(/\?.*$/, '')
+ const { descriptor, errors } = compiler.parse(source, {
+ filename,
+ sourceMap,
})
+ const asCustomElement =
+ typeof options.customElement === 'boolean'
+ ? options.customElement
+ : (options.customElement || /\.ce\.vue$/).test(filename)
+
+ // cache descriptor
+ setDescriptor(filename, descriptor)
+
if (errors.length) {
- errors.forEach(err => {
+ errors.forEach((err) => {
formatError(err, source, resourcePath)
loaderContext.emitError(err)
})
return ``
}
+ // module id for scoped CSS & hot-reload
+ const rawShortFilePath = path
+ .relative(rootContext || process.cwd(), filename)
+ .replace(/^(\.\.[\/\\])+/, '')
+ const shortFilePath = rawShortFilePath.replace(/\\/g, '/')
+ const id = hash(
+ isProduction
+ ? shortFilePath + '\n' + source.replace(/\r\n/g, '\n')
+ : shortFilePath
+ )
+
// if the query has a type field, this is a language block request
// e.g. foo.vue?type=template&id=xxxxx
// and we will return early
if (incomingQuery.type) {
return selectBlock(
descriptor,
+ id,
+ options,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
- // module id for scoped CSS & hot-reload
- const rawShortFilePath = path
- .relative(rootContext || process.cwd(), resourcePath)
- .replace(/^(\.\.[\/\\])+/, '')
- const shortFilePath = rawShortFilePath.replace(/\\/g, '/') + resourceQuery
- const id = hash(isProduction ? shortFilePath + '\n' + source : shortFilePath)
-
// feature information
- const hasScoped = descriptor.styles.some(s => s.scoped)
+ const hasScoped = descriptor.styles.some((s) => s.scoped)
const needsHotReload =
!isServer &&
!isProduction &&
- !!(descriptor.script || descriptor.template) &&
+ !!(descriptor.script || descriptor.scriptSetup || descriptor.template) &&
options.hotReload !== false
+ // extra properties to attach to the script object
+ // we need to do this in a tree-shaking friendly manner
+ const propsToAttach: [string, string][] = []
+
// script
- let script: SFCScriptBlock | undefined
let scriptImport = `const script = {}`
- if (descriptor.script || descriptor.scriptSetup) {
- try {
- script = (descriptor as any).scriptCompiled = compileScript(descriptor, {
- babelParserPlugins: options.babelParserPlugins
- })
- } catch (e) {
- loaderContext.emitError(e)
- }
- if (script) {
- const src = script.src || resourcePath
- const attrsQuery = attrsToQuery(script.attrs, 'js')
- const query = `?vue&type=script${attrsQuery}${resourceQuery}`
- const scriptRequest = stringifyRequest(src + query)
- scriptImport =
- `import script from ${scriptRequest}\n` +
- // support named exports
- `export * from ${scriptRequest}`
- }
+ let isTS = false
+ const { script, scriptSetup } = descriptor
+ if (script || scriptSetup) {
+ const lang = script?.lang || scriptSetup?.lang
+ isTS = !!(lang && /tsx?/.test(lang))
+ const src = (script && !scriptSetup && script.src) || resourcePath
+ const attrsQuery = attrsToQuery((scriptSetup || script)!.attrs, 'js')
+ const query = `?vue&type=script${attrsQuery}${resourceQuery}`
+ const scriptRequest = stringifyRequest(src + query)
+ scriptImport =
+ `import script from ${scriptRequest}\n` +
+ // support named exports
+ `export * from ${scriptRequest}`
}
// template
let templateImport = ``
let templateRequest
- if (descriptor.template) {
+ const renderFnName = isServer ? `ssrRender` : `render`
+ const useInlineTemplate = canInlineTemplate(descriptor, isProduction)
+ if (descriptor.template && !useInlineTemplate) {
const src = descriptor.template.src || resourcePath
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``
const attrsQuery = attrsToQuery(descriptor.template.attrs)
- const bindingsQuery = script
- ? `&bindings=${JSON.stringify(script.bindings)}`
- : ``
- const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${bindingsQuery}${resourceQuery}`
+ const tsQuery =
+ options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``
+ const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}`
templateRequest = stringifyRequest(src + query)
- templateImport = `import { render } from ${templateRequest}`
+ templateImport = `import { ${renderFnName} } from ${templateRequest}`
+ propsToAttach.push([renderFnName, renderFnName])
}
// styles
let stylesCode = ``
let hasCSSModules = false
+ const nonWhitespaceRE = /\S+/
if (descriptor.styles.length) {
- descriptor.styles.forEach((style: SFCStyleBlock, i: number) => {
- const src = style.src || resourcePath
- const attrsQuery = attrsToQuery(style.attrs, 'css')
- // make sure to only pass id when necessary so that we don't inject
- // duplicate tags when multiple components import the same css file
- const idQuery = style.scoped ? `&id=${id}` : ``
- const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${resourceQuery}`
- const styleRequest = stringifyRequest(src + query)
- if (style.module) {
- if (!hasCSSModules) {
- stylesCode += `\nconst cssModules = script.__cssModules = {}`
- hasCSSModules = true
+ descriptor.styles
+ .filter((style) => style.src || nonWhitespaceRE.test(style.content))
+ .forEach((style, i) => {
+ const src = style.src || resourcePath
+ const attrsQuery = attrsToQuery(style.attrs, 'css')
+ // make sure to only pass id when necessary so that we don't inject
+ // duplicate tags when multiple components import the same css file
+ const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
+ const inlineQuery = asCustomElement ? `&inline` : ``
+ const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}`
+ const styleRequest = stringifyRequest(src + query)
+ if (style.module) {
+ if (asCustomElement) {
+ loaderContext.emitError(
+ `
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/css-modules-simple.vue b/test/fixtures/css-modules-simple.vue
new file mode 100644
index 000000000..f677aa29c
--- /dev/null
+++ b/test/fixtures/css-modules-simple.vue
@@ -0,0 +1,9 @@
+
+
+
diff --git a/test/fixtures/css-modules.vue b/test/fixtures/css-modules.vue
new file mode 100644
index 000000000..473f90ce9
--- /dev/null
+++ b/test/fixtures/css-modules.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/test/fixtures/custom-import.vue b/test/fixtures/custom-import.vue
new file mode 100644
index 000000000..34eed79df
--- /dev/null
+++ b/test/fixtures/custom-import.vue
@@ -0,0 +1,22 @@
+
+
+
+
+ {{msg}}
+
+
+
+
+
diff --git a/test/fixtures/custom-language.vue b/test/fixtures/custom-language.vue
new file mode 100644
index 000000000..f026cd7f1
--- /dev/null
+++ b/test/fixtures/custom-language.vue
@@ -0,0 +1,31 @@
+
+ describe('example', () => {
+ it('basic', done => {
+ done();
+ })
+ })
+
+
+
+ This is example documentation for a component.
+
+
+
+ {{msg}}
+
+
+
+
+
diff --git a/test/fixtures/duplicate-cssm.css b/test/fixtures/duplicate-cssm.css
new file mode 100644
index 000000000..611d25565
--- /dev/null
+++ b/test/fixtures/duplicate-cssm.css
@@ -0,0 +1 @@
+@value color: red;
diff --git a/test/fixtures/duplicate-cssm.js b/test/fixtures/duplicate-cssm.js
new file mode 100644
index 000000000..20c5905e3
--- /dev/null
+++ b/test/fixtures/duplicate-cssm.js
@@ -0,0 +1,7 @@
+import values from './duplicate-cssm.css'
+import Comp from './duplicate-cssm.vue'
+
+export { values }
+export default Comp
+
+window.exports = values
diff --git a/test/fixtures/duplicate-cssm.vue b/test/fixtures/duplicate-cssm.vue
new file mode 100644
index 000000000..0d37d0bd0
--- /dev/null
+++ b/test/fixtures/duplicate-cssm.vue
@@ -0,0 +1,11 @@
+
+ hi
+
+
+
diff --git a/test/fixtures/empty-style.vue b/test/fixtures/empty-style.vue
new file mode 100644
index 000000000..12b50f3a7
--- /dev/null
+++ b/test/fixtures/empty-style.vue
@@ -0,0 +1,6 @@
+
+ empty style
+
+
+
diff --git a/test/fixtures/entry.js b/test/fixtures/entry.js
index 431e1ed4e..ac3e4ea2a 100644
--- a/test/fixtures/entry.js
+++ b/test/fixtures/entry.js
@@ -1,9 +1,15 @@
+import { createApp } from 'vue'
+
import Component from '~target'
import * as exports from '~target'
if (typeof window !== 'undefined') {
- window.module = Component
+ window.componentModule = Component
window.exports = exports
+
+ const app = createApp(Component)
+ const container = window.document.createElement('div')
+ window.instance = app.mount(container)
}
export default Component
diff --git a/test/fixtures/extract-css-chunks.vue b/test/fixtures/extract-css-chunks.vue
new file mode 100644
index 000000000..80c5e182f
--- /dev/null
+++ b/test/fixtures/extract-css-chunks.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/extract-css.vue b/test/fixtures/extract-css.vue
new file mode 100644
index 000000000..7fed1096e
--- /dev/null
+++ b/test/fixtures/extract-css.vue
@@ -0,0 +1,10 @@
+
+
+
diff --git a/test/fixtures/i18n-entry.js b/test/fixtures/i18n-entry.js
new file mode 100644
index 000000000..6c1ffb43c
--- /dev/null
+++ b/test/fixtures/i18n-entry.js
@@ -0,0 +1,23 @@
+import { createApp } from 'vue'
+import { createI18n } from 'vue-i18n'
+
+import Component from './i18n.vue'
+import * as exports from './i18n.vue'
+
+const i18n = createI18n({
+ locale: 'de',
+ silentFallbackWarn: true,
+ silentTranslationWarn: true,
+})
+
+if (typeof window !== 'undefined') {
+ window.componentModule = Component
+ window.exports = exports
+
+ const app = createApp(Component).use(i18n)
+
+ const container = window.document.createElement('div')
+ window.instance = app.mount(container)
+}
+
+export default Component
diff --git a/test/fixtures/i18n.vue b/test/fixtures/i18n.vue
new file mode 100644
index 000000000..1d9d6bc75
--- /dev/null
+++ b/test/fixtures/i18n.vue
@@ -0,0 +1,7 @@
+
+ {{ $t('test') }}
+
+
+
+test: Example Text
+
diff --git a/test/fixtures/index.html b/test/fixtures/index.html
new file mode 100644
index 000000000..7e5123ef1
--- /dev/null
+++ b/test/fixtures/index.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/logo.png b/test/fixtures/logo.png
new file mode 100644
index 000000000..1b3356a74
Binary files /dev/null and b/test/fixtures/logo.png differ
diff --git a/test/fixtures/markdown.vue b/test/fixtures/markdown.vue
new file mode 100644
index 000000000..113f4118d
--- /dev/null
+++ b/test/fixtures/markdown.vue
@@ -0,0 +1,11 @@
+## {{msg}}
+
+
diff --git a/test/fixtures/named-exports.vue b/test/fixtures/named-exports.vue
new file mode 100644
index 000000000..d9637d3ef
--- /dev/null
+++ b/test/fixtures/named-exports.vue
@@ -0,0 +1,9 @@
+
+Terms
diff --git a/test/fixtures/optional-chaining.vue b/test/fixtures/optional-chaining.vue
new file mode 100644
index 000000000..7e5139ce5
--- /dev/null
+++ b/test/fixtures/optional-chaining.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/test/fixtures/postcss.vue b/test/fixtures/postcss.vue
new file mode 100644
index 000000000..0d4dd195b
--- /dev/null
+++ b/test/fixtures/postcss.vue
@@ -0,0 +1,5 @@
+
diff --git a/test/fixtures/pre.vue b/test/fixtures/pre.vue
new file mode 100644
index 000000000..c9303cb6e
--- /dev/null
+++ b/test/fixtures/pre.vue
@@ -0,0 +1,22 @@
+
+
+
+div
+ h1 This is the app
+ comp-a
+ comp-b
+
+
+
diff --git a/test/fixtures/process-custom-file/custom-file.svg b/test/fixtures/process-custom-file/custom-file.svg
new file mode 100644
index 000000000..7edfe0d23
--- /dev/null
+++ b/test/fixtures/process-custom-file/custom-file.svg
@@ -0,0 +1,3 @@
+
+ ProcessedCustomFile
+
\ No newline at end of file
diff --git a/test/fixtures/process-custom-file/process-custom-file.vue b/test/fixtures/process-custom-file/process-custom-file.vue
new file mode 100644
index 000000000..842e42d2d
--- /dev/null
+++ b/test/fixtures/process-custom-file/process-custom-file.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/resolve.vue b/test/fixtures/resolve.vue
new file mode 100644
index 000000000..86d58e7dc
--- /dev/null
+++ b/test/fixtures/resolve.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/test/fixtures/scoped-css.vue b/test/fixtures/scoped-css.vue
new file mode 100644
index 000000000..789853b50
--- /dev/null
+++ b/test/fixtures/scoped-css.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
diff --git a/test/fixtures/script-import.js b/test/fixtures/script-import.js
new file mode 100644
index 000000000..ce0b6a81c
--- /dev/null
+++ b/test/fixtures/script-import.js
@@ -0,0 +1,7 @@
+export default {
+ data() {
+ return {
+ msg: 'Hello from Component A!',
+ }
+ },
+}
diff --git a/test/fixtures/script-import.vue b/test/fixtures/script-import.vue
new file mode 100644
index 000000000..08f329768
--- /dev/null
+++ b/test/fixtures/script-import.vue
@@ -0,0 +1 @@
+
diff --git a/test/fixtures/style-import-scoped.css b/test/fixtures/style-import-scoped.css
new file mode 100644
index 000000000..954b5d865
--- /dev/null
+++ b/test/fixtures/style-import-scoped.css
@@ -0,0 +1 @@
+h1 { color: green; }
diff --git a/test/fixtures/style-import-twice-sub.vue b/test/fixtures/style-import-twice-sub.vue
new file mode 100644
index 000000000..6ae3d7173
--- /dev/null
+++ b/test/fixtures/style-import-twice-sub.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/test/fixtures/style-import-twice.vue b/test/fixtures/style-import-twice.vue
new file mode 100644
index 000000000..1459b47e0
--- /dev/null
+++ b/test/fixtures/style-import-twice.vue
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/test/fixtures/style-import.css b/test/fixtures/style-import.css
new file mode 100644
index 000000000..5ce768ca5
--- /dev/null
+++ b/test/fixtures/style-import.css
@@ -0,0 +1 @@
+h1 { color: red; }
diff --git a/test/fixtures/style-import.vue b/test/fixtures/style-import.vue
new file mode 100644
index 000000000..54b83bf89
--- /dev/null
+++ b/test/fixtures/style-import.vue
@@ -0,0 +1,2 @@
+
+
diff --git a/test/fixtures/template-import-pre.vue b/test/fixtures/template-import-pre.vue
new file mode 100644
index 000000000..461624104
--- /dev/null
+++ b/test/fixtures/template-import-pre.vue
@@ -0,0 +1 @@
+
diff --git a/test/fixtures/template-import.html b/test/fixtures/template-import.html
new file mode 100644
index 000000000..c9dc2eef3
--- /dev/null
+++ b/test/fixtures/template-import.html
@@ -0,0 +1,3 @@
+
+
hello
+
diff --git a/test/fixtures/template-import.pug b/test/fixtures/template-import.pug
new file mode 100644
index 000000000..bc60abffa
--- /dev/null
+++ b/test/fixtures/template-import.pug
@@ -0,0 +1,2 @@
+div
+ h1 hello
diff --git a/test/fixtures/template-import.vue b/test/fixtures/template-import.vue
new file mode 100644
index 000000000..00de859d2
--- /dev/null
+++ b/test/fixtures/template-import.vue
@@ -0,0 +1 @@
+
diff --git a/test/fixtures/ts.vue b/test/fixtures/ts.vue
new file mode 100644
index 000000000..95031a7f2
--- /dev/null
+++ b/test/fixtures/ts.vue
@@ -0,0 +1,11 @@
+
+ {{ msg }}
+
+
+
+
+
diff --git a/test/fixtures/unit-test.js b/test/fixtures/unit-test.js
new file mode 100644
index 000000000..70d411983
--- /dev/null
+++ b/test/fixtures/unit-test.js
@@ -0,0 +1,5 @@
+describe('example', () => {
+ it('basic', (done) => {
+ done()
+ })
+})
diff --git a/test/mock-loaders/docs.js b/test/mock-loaders/docs.js
new file mode 100644
index 000000000..c79cd45c0
--- /dev/null
+++ b/test/mock-loaders/docs.js
@@ -0,0 +1,9 @@
+module.exports = function (source, map) {
+ this.callback(
+ null,
+ `export default Component => {
+ Component.__docs = ${JSON.stringify(source)}
+ }`,
+ map
+ )
+}
diff --git a/test/mock-loaders/html.js b/test/mock-loaders/html.js
new file mode 100644
index 000000000..182d66e49
--- /dev/null
+++ b/test/mock-loaders/html.js
@@ -0,0 +1,3 @@
+module.exports = function (content) {
+ return content.replace(/red/, 'green')
+}
diff --git a/test/mock-loaders/js.js b/test/mock-loaders/js.js
new file mode 100644
index 000000000..a0c719bb1
--- /dev/null
+++ b/test/mock-loaders/js.js
@@ -0,0 +1,3 @@
+module.exports = function (content) {
+ return content.replace(/Hello from Component A!/, 'Changed!')
+}
diff --git a/test/mock-loaders/query.js b/test/mock-loaders/query.js
new file mode 100644
index 000000000..0a2ff8727
--- /dev/null
+++ b/test/mock-loaders/query.js
@@ -0,0 +1,22 @@
+module.exports = function (content) {
+ const query = this.resourceQuery.slice(1)
+
+ if (/change/.test(query)) {
+ return `
+
+ Changed!
+
+
+ `
+ }
+
+ return content
+}
diff --git a/test/script.spec.ts b/test/script.spec.ts
new file mode 100644
index 000000000..f2405c66b
--- /dev/null
+++ b/test/script.spec.ts
@@ -0,0 +1,13 @@
+import { mockBundleAndRun } from './utils'
+
+test('named exports', async () => {
+ const { exports } = await mockBundleAndRun({
+ entry: 'named-exports.vue',
+ })
+ expect(exports.default.name).toBe('named-exports')
+ expect(exports.foo()).toBe(1)
+})
+
+test('experimental