提交 087bc446 编写于 作者: fxy060608's avatar fxy060608

wip(app): nvue

上级 30a7fb1d
......@@ -7,7 +7,8 @@
"scripts": {
"build": "node scripts/build.js",
"build:h5": "node scripts/build.js uni-app uni-cli-shared uni-h5 uni-i18n uni-shared uni-h5-vite vite-plugin-uni",
"build:app": "node scripts/build.js uni-app-plus uni-app-vite uni-app-vue",
"build:app": "node scripts/build.js uni-app-plus uni-app-vite uni-app-vue uni-cli-nvue",
"build:mp": "node scripts/build.js uni-mp-alipay uni-mp-baidu uni-mp-qq uni-mp-toutiao uni-mp-weixin uni-quickapp-webview",
"size": "npm run build size-check",
"lint": "eslint packages/*/src/**/*.ts",
"format": "prettier --write --parser typescript \"packages/**/*.ts?(x)\"",
......
......@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.createConfig = void 0;
const optimization_1 = require("./optimization");
const output_1 = require("./output");
const module_1 = require("./module");
const plugins_1 = require("./plugins");
function createConfig(mode) {
return {
target: 'node',
......@@ -17,6 +19,8 @@ function createConfig(mode) {
},
optimization: optimization_1.optimization,
output: output_1.output,
module: module_1.module,
plugins: plugins_1.plugins,
};
}
exports.createConfig = createConfig;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.module = void 0;
const rules_1 = require("./rules");
exports.module = {
rules: rules_1.rules,
};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = void 0;
exports.rules = [
{
test: [/\.nvue(\?[^?]+)?$/, /\.vue(\?[^?]+)?$/],
loader: 'vue-loader',
},
];
......@@ -4,4 +4,10 @@ exports.plugins = void 0;
const define_1 = require("./define");
const banner_1 = require("./banner");
const provide_1 = require("./provide");
exports.plugins = [define_1.define, banner_1.banner, provide_1.provide];
const vueLoader_1 = require("./vueLoader");
exports.plugins = [
define_1.define,
banner_1.banner,
provide_1.provide,
vueLoader_1.vueLoaderPlugin,
];
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.vueLoaderPlugin = void 0;
const { VueLoaderPlugin } = require('../../../../lib/vue-loader/dist');
exports.vueLoaderPlugin = new VueLoaderPlugin();
export declare function genCSSModulesCode(id: string, index: number, request: string, moduleName: string | boolean, needsHotReload: boolean): string;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.genCSSModulesCode = void 0;
function genCSSModulesCode(id, index, request, moduleName, needsHotReload) {
const styleVar = `style${index}`;
let code = `\nimport ${styleVar} from ${request}`;
// inject variable
const name = typeof moduleName === 'string' ? moduleName : '$style';
code += `\ncssModules["${name}"] = ${styleVar}`;
if (needsHotReload) {
code += `
if (module.hot) {
module.hot.accept(${request}, () => {
cssModules["${name}"] = ${styleVar}
__VUE_HMR_RUNTIME__.rerender("${id}")
})
}`;
}
return code;
}
exports.genCSSModulesCode = genCSSModulesCode;
import { SFCDescriptor } from '@vue/compiler-sfc';
export declare function setDescriptor(filename: string, entry: SFCDescriptor): void;
export declare function getDescriptor(filename: string): SFCDescriptor;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDescriptor = exports.setDescriptor = void 0;
const fs = require("fs");
const compiler_sfc_1 = require("@vue/compiler-sfc");
const cache = new Map();
function setDescriptor(filename, entry) {
cache.set(cleanQuery(filename), entry);
}
exports.setDescriptor = setDescriptor;
function getDescriptor(filename) {
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_sfc_1.parse(source, {
filename,
sourceMap: true,
});
cache.set(filename, descriptor);
return descriptor;
}
exports.getDescriptor = getDescriptor;
function cleanQuery(str) {
const i = str.indexOf('?');
return i > 0 ? str.slice(0, i) : str;
}
import { CompilerError } from '@vue/compiler-sfc';
export declare function formatError(err: SyntaxError | CompilerError, source: string, file: string): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatError = void 0;
const compiler_sfc_1 = require("@vue/compiler-sfc");
const chalk = require("chalk");
function formatError(err, source, file) {
const loc = err.loc;
if (!loc) {
return;
}
const locString = `:${loc.start.line}:${loc.start.column}`;
const filePath = chalk.gray(`at ${file}${locString}`);
const codeframe = compiler_sfc_1.generateCodeFrame(source, loc.start.offset, loc.end.offset);
err.message = `\n${chalk.red(`VueCompilerError: ${err.message}`)}\n${filePath}\n${chalk.yellow(codeframe)}\n`;
}
exports.formatError = formatError;
export declare function genHotReloadCode(id: string, templateRequest: string | undefined): string;
"use strict";
// __VUE_HMR_RUNTIME__ is injected to global scope by @vue/runtime-core
Object.defineProperty(exports, "__esModule", { value: true });
exports.genHotReloadCode = void 0;
function genHotReloadCode(id, templateRequest) {
return `
/* hot reload */
if (module.hot) {
script.__hmrId = "${id}"
const api = __VUE_HMR_RUNTIME__
module.hot.accept()
if (!api.createRecord('${id}', script)) {
api.reload('${id}', script)
}
${templateRequest ? genTemplateHotReloadCode(id, templateRequest) : ''}
}
`;
}
exports.genHotReloadCode = genHotReloadCode;
function genTemplateHotReloadCode(id, request) {
return `
module.hot.accept(${request}, () => {
api.rerender('${id}', render)
})
`;
}
import webpack = require('webpack');
import { TemplateCompiler, CompilerOptions, SFCTemplateCompileOptions, SFCScriptCompileOptions } from '@vue/compiler-sfc';
import VueLoaderPlugin from './plugin';
export { VueLoaderPlugin };
export interface VueLoaderOptions {
babelParserPlugins?: SFCScriptCompileOptions['babelParserPlugins'];
transformAssetUrls?: SFCTemplateCompileOptions['transformAssetUrls'];
compiler?: TemplateCompiler | string;
compilerOptions?: CompilerOptions;
refSugar?: boolean;
customElement?: boolean | RegExp;
hotReload?: boolean;
exposeFilename?: boolean;
appendExtension?: boolean;
isServerBuild?: boolean;
}
export default function loader(this: webpack.loader.LoaderContext, source: string): string | void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VueLoaderPlugin = void 0;
try {
require.resolve('@vue/compiler-sfc');
}
catch (e) {
throw new Error('vue-loader requires @vue/compiler-sfc to be present in the dependency ' +
'tree.');
}
const path = require("path");
const qs = require("querystring");
const loaderUtils = require("loader-utils");
const hash = require("hash-sum");
const compiler_sfc_1 = require("@vue/compiler-sfc");
const select_1 = require("./select");
const hotReload_1 = require("./hotReload");
const cssModules_1 = require("./cssModules");
const formatError_1 = require("./formatError");
const plugin_1 = require("./plugin");
exports.VueLoaderPlugin = plugin_1.default;
const resolveScript_1 = require("./resolveScript");
const descriptorCache_1 = require("./descriptorCache");
let errorEmitted = false;
function loader(source) {
var _a;
const loaderContext = this;
// check if plugin is installed
if (!errorEmitted &&
!loaderContext['thread-loader'] &&
!loaderContext[plugin_1.default.NS]) {
loaderContext.emitError(new Error(`vue-loader was used without the corresponding plugin. ` +
`Make sure to include VueLoaderPlugin in your webpack config.`));
errorEmitted = true;
}
const stringifyRequest = (r) => loaderUtils.stringifyRequest(loaderContext, r);
const { mode, target, sourceMap, rootContext, resourcePath, resourceQuery = '', } = loaderContext;
const rawQuery = resourceQuery.slice(1);
const incomingQuery = qs.parse(rawQuery);
const options = (loaderUtils.getOptions(loaderContext) ||
{});
const isServer = (_a = options.isServerBuild) !== null && _a !== void 0 ? _a : target === 'node';
const isProduction = mode === 'production' || process.env.NODE_ENV === 'production';
const filename = resourcePath.replace(/\?.*$/, '');
const { descriptor, errors } = compiler_sfc_1.parse(source, {
filename,
sourceMap,
});
const asCustomElement = typeof options.customElement === 'boolean'
? options.customElement
: (options.customElement || /\.ce\.vue$/).test(filename);
// cache descriptor
descriptorCache_1.setDescriptor(filename, descriptor);
if (errors.length) {
errors.forEach((err) => {
formatError_1.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 select_1.selectBlock(descriptor, id, options, loaderContext, incomingQuery, !!options.appendExtension);
}
// feature information
const hasScoped = descriptor.styles.some((s) => s.scoped);
const needsHotReload = !isServer &&
!isProduction &&
!!(descriptor.script || descriptor.scriptSetup || descriptor.template) &&
options.hotReload !== false;
// script
let scriptImport = `const script = {}`;
const { script, scriptSetup } = descriptor;
if (script || scriptSetup) {
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;
const renderFnName = isServer ? `ssrRender` : `render`;
const useInlineTemplate = resolveScript_1.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 varsQuery = descriptor.cssVars
// ? `&vars=${qs.escape(generateCssVars(descriptor, id, isProduction))}`
// : ``
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}`;
templateRequest = stringifyRequest(src + query);
templateImport = `import { ${renderFnName} } from ${templateRequest}`;
}
// styles
let stylesCode = ``;
let hasCSSModules = false;
const nonWhitespaceRE = /\S+/;
if (descriptor.styles.length) {
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(`<style module> is not supported in custom element mode.`);
}
if (!hasCSSModules) {
stylesCode += `\nconst cssModules = script.__cssModules = {}`;
hasCSSModules = true;
}
stylesCode += cssModules_1.genCSSModulesCode(id, i, styleRequest, style.module, needsHotReload);
}
else {
if (asCustomElement) {
stylesCode += `\nimport _style_${i} from ${styleRequest}`;
}
else {
stylesCode += `\nimport ${styleRequest}`;
}
}
// TODO SSR critical CSS collection
});
if (asCustomElement) {
stylesCode += `\nscript.styles = [${descriptor.styles.map((_, i) => `_style_${i}`)}]`;
}
}
let code = [
templateImport,
scriptImport,
stylesCode,
templateImport ? `script.${renderFnName} = ${renderFnName}` : ``,
]
.filter(Boolean)
.join('\n');
// attach scope Id for runtime use
if (hasScoped) {
code += `\nscript.__scopeId = "data-v-${id}"`;
}
if (needsHotReload) {
code += hotReload_1.genHotReloadCode(id, templateRequest);
}
// Expose filename. This is used by the devtools and Vue runtime warnings.
if (!isProduction) {
// Expose the file's full path in development, so that it can be opened
// from the devtools.
code += `\nscript.__file = ${JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))}`;
}
else if (options.exposeFilename) {
// Libraries can opt-in to expose their components' filenames in production builds.
// For security reasons, only expose the file's basename in production.
code += `\nscript.__file = ${JSON.stringify(path.basename(resourcePath))}`;
}
// custom blocks
if (descriptor.customBlocks && descriptor.customBlocks.length) {
code += `\n/* custom blocks */\n`;
code +=
descriptor.customBlocks
.map((block, i) => {
const src = block.attrs.src || resourcePath;
const attrsQuery = attrsToQuery(block.attrs);
const blockTypeQuery = `&blockType=${qs.escape(block.type)}`;
const issuerQuery = block.attrs.src
? `&issuerPath=${qs.escape(resourcePath)}`
: '';
const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}`;
return (`import block${i} from ${stringifyRequest(src + query)}\n` +
`if (typeof block${i} === 'function') block${i}(script)`);
})
.join(`\n`) + `\n`;
}
// finalize
code += asCustomElement
? `\n\nimport { defineCustomElement as __ce } from 'vue';` +
`export default __ce(script)`
: `\n\nexport default script`;
return code;
}
exports.default = loader;
// these are built-in query parameters so should be ignored
// if the user happen to add them as attrs
const ignoreList = ['id', 'index', 'src', 'type'];
function attrsToQuery(attrs, langFallback) {
let query = ``;
for (const name in attrs) {
const value = attrs[name];
if (!ignoreList.includes(name)) {
query += `&${qs.escape(name)}=${value ? qs.escape(String(value)) : ``}`;
}
}
if (langFallback && !(`lang` in attrs)) {
query += `&lang=${langFallback}`;
}
return query;
}
import webpack = require('webpack');
declare const pitcher: webpack.loader.Loader;
export declare const pitch: () => string | undefined;
export default pitcher;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.pitch = void 0;
const qs = require("querystring");
const loaderUtils = require("loader-utils");
const selfPath = require.resolve('./index');
// const templateLoaderPath = require.resolve('./templateLoader')
const stylePostLoaderPath = require.resolve('./stylePostLoader');
const styleInlineLoaderPath = require.resolve('./styleInlineLoader');
const isESLintLoader = (l) => /(\/|\\|@)eslint-loader/.test(l.path);
const isNullLoader = (l) => /(\/|\\|@)null-loader/.test(l.path);
const isCSSLoader = (l) => /(\/|\\|@)css-loader/.test(l.path);
const isCacheLoader = (l) => /(\/|\\|@)cache-loader/.test(l.path);
const isNotPitcher = (l) => l.path !== __filename;
const pitcher = (code) => code;
// This pitching loader is responsible for intercepting all vue block requests
// and transform it into appropriate requests.
exports.pitch = function () {
const context = this;
const rawLoaders = context.loaders.filter(isNotPitcher);
let loaders = rawLoaders;
// do not inject if user uses null-loader to void the type (#1239)
if (loaders.some(isNullLoader)) {
return;
}
const query = qs.parse(context.resourceQuery.slice(1));
const isInlineBlock = /\.vue$/.test(context.resourcePath);
// eslint-loader may get matched multiple times
// if this is an inline block, since the whole file itself is being linted,
// remove eslint-loader to avoid duplicate linting.
if (isInlineBlock) {
loaders = loaders.filter((l) => !isESLintLoader(l));
}
// Important: dedupe loaders since both the original rule
// and the cloned rule would match a source import request or a
// resourceQuery-only rule that intends to target a custom block with no lang
const seen = new Map();
loaders = loaders.filter((loader) => {
const identifier = typeof loader === 'string'
? loader
: // Dedupe based on both path and query if available. This is important
// in Vue CLI so that postcss-loaders with different options can co-exist
loader.path + loader.query;
if (!seen.has(identifier)) {
seen.set(identifier, true);
return true;
}
});
// Inject style-post-loader before css-loader for scoped CSS and trimming
if (query.type === `style`) {
const cssLoaderIndex = loaders.findIndex(isCSSLoader);
if (cssLoaderIndex > -1) {
// if inlined, ignore any loaders after css-loader and replace w/ inline
// loader
const afterLoaders = query.inline != null
? [styleInlineLoaderPath]
: loaders.slice(0, cssLoaderIndex + 1);
const beforeLoaders = loaders.slice(cssLoaderIndex + 1);
return genProxyModule([...afterLoaders, stylePostLoaderPath, ...beforeLoaders], context, !!query.module || query.inline != null);
}
}
// if a custom block has no other matching loader other than vue-loader itself
// or cache-loader, we should ignore it
if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
return ``;
}
// Rewrite request. Technically this should only be done when we have deduped
// loaders. But somehow this is required for block source maps to work.
return genProxyModule(loaders, context, query.type !== 'template');
};
function genProxyModule(loaders, context, exportDefault = true) {
const request = genRequest(loaders, context);
// return a proxy module which simply re-exports everything from the
// actual request. Note for template blocks the compiled module has no
// default export.
return ((exportDefault ? `export { default } from ${request}; ` : ``) +
`export * from ${request}`);
}
function genRequest(loaders, context) {
const loaderStrings = loaders.map((loader) => {
return typeof loader === 'string' ? loader : loader.request;
});
const resource = context.resourcePath + context.resourceQuery;
return loaderUtils.stringifyRequest(context, '-!' + [...loaderStrings, resource].join('!'));
}
function shouldIgnoreCustomBlock(loaders) {
const actualLoaders = loaders.filter((loader) => {
// vue-loader
if (loader.path === selfPath) {
return false;
}
// cache-loader
if (isCacheLoader(loader)) {
return false;
}
return true;
});
return actualLoaders.length === 0;
}
exports.default = pitcher;
import webpack = require('webpack');
declare class VueLoaderPlugin implements webpack.Plugin {
static NS: string;
apply(compiler: webpack.Compiler): void;
}
declare let Plugin: typeof VueLoaderPlugin;
export default Plugin;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const webpack = require("webpack");
let Plugin;
if (webpack.version && webpack.version[0] > '4') {
// webpack5 and upper
Plugin = require('./pluginWebpack5').default;
}
else {
// webpack4 and lower
Plugin = require('./pluginWebpack4').default;
}
exports.default = Plugin;
import webpack = require('webpack');
declare class VueLoaderPlugin implements webpack.Plugin {
static NS: string;
apply(compiler: webpack.Compiler): void;
}
export default VueLoaderPlugin;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const qs = require("querystring");
const RuleSet = require('webpack/lib/RuleSet');
const id = 'vue-loader-plugin';
const NS = 'vue-loader';
class VueLoaderPlugin {
apply(compiler) {
// inject NS for plugin installation check in the main loader
compiler.hooks.compilation.tap(id, (compilation) => {
compilation.hooks.normalModuleLoader.tap(id, (loaderContext) => {
loaderContext[NS] = true;
});
});
const rawRules = compiler.options.module.rules;
// use webpack's RuleSet utility to normalize user rules
const rules = new RuleSet(rawRules).rules;
// find the rule that applies to vue files
let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`));
if (vueRuleIndex < 0) {
vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`));
}
const vueRule = rules[vueRuleIndex];
if (!vueRule) {
throw new Error(`[VueLoaderPlugin Error] No matching rule for .vue files found.\n` +
`Make sure there is at least one root-level rule that matches .vue or .vue.html files.`);
}
if (vueRule.oneOf) {
throw new Error(`[VueLoaderPlugin Error] vue-loader currently does not support vue rules with oneOf.`);
}
// get the normlized "use" for vue files
const vueUse = vueRule.use;
// get vue-loader options
const vueLoaderUseIndex = vueUse.findIndex((u) => {
// FIXME: this code logic is incorrect when project paths starts with `vue-loader-something`
return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader || '');
});
if (vueLoaderUseIndex < 0) {
throw new Error(`[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` +
`Make sure the rule matching .vue files include vue-loader in its use.`);
}
const vueLoaderUse = vueUse[vueLoaderUseIndex];
const vueLoaderOptions = (vueLoaderUse.options =
vueLoaderUse.options || {});
// for each user rule (expect the vue rule), create a cloned rule
// that targets the corresponding language blocks in *.vue files.
const clonedRules = rules.filter((r) => r !== vueRule).map(cloneRule);
// rule for template compiler
const templateCompilerRule = {
loader: require.resolve('./templateLoader'),
resourceQuery: (query) => {
const parsed = qs.parse(query.slice(1));
return parsed.vue != null && parsed.type === 'template';
},
options: vueLoaderOptions,
};
// for each rule that matches plain .js files, also create a clone and
// match it against the compiled template code inside *.vue files, so that
// compiled vue render functions receive the same treatment as user code
// (mostly babel)
const matchesJS = createMatcher(`test.js`);
const jsRulesForRenderFn = rules
.filter((r) => r !== vueRule && matchesJS(r))
.map(cloneRuleForRenderFn);
// pitcher for block requests (for injecting stylePostLoader and deduping
// loaders matched for src imports)
const pitcher = {
loader: require.resolve('./pitcher'),
resourceQuery: (query) => {
const parsed = qs.parse(query.slice(1));
return parsed.vue != null;
},
};
// replace original rules
compiler.options.module.rules = [
pitcher,
...jsRulesForRenderFn,
templateCompilerRule,
...clonedRules,
...rules,
];
}
}
VueLoaderPlugin.NS = NS;
function createMatcher(fakeFile) {
return (rule) => {
// #1201 we need to skip the `include` check when locating the vue rule
const clone = Object.assign({}, rule);
delete clone.include;
const normalized = RuleSet.normalizeRule(clone, {}, '');
return !rule.enforce && normalized.resource && normalized.resource(fakeFile);
};
}
function cloneRule(rule) {
const resource = rule.resource;
const resourceQuery = rule.resourceQuery;
// Assuming `test` and `resourceQuery` tests are executed in series and
// synchronously (which is true based on RuleSet's implementation), we can
// save the current resource being matched from `test` so that we can access
// it in `resourceQuery`. This ensures when we use the normalized rule's
// resource check, include/exclude are matched correctly.
let currentResource;
const res = Object.assign(Object.assign({}, rule), { resource: (resource) => {
currentResource = resource;
return true;
}, resourceQuery: (query) => {
const parsed = qs.parse(query.slice(1));
if (parsed.vue == null) {
return false;
}
if (resource && parsed.lang == null) {
return false;
}
const fakeResourcePath = `${currentResource}.${parsed.lang}`;
if (resource && !resource(fakeResourcePath)) {
return false;
}
if (resourceQuery && !resourceQuery(query)) {
return false;
}
return true;
} });
if (rule.rules) {
res.rules = rule.rules.map(cloneRule);
}
if (rule.oneOf) {
res.oneOf = rule.oneOf.map(cloneRule);
}
return res;
}
function cloneRuleForRenderFn(rule) {
const resource = rule.resource;
const resourceQuery = rule.resourceQuery;
let currentResource;
const res = Object.assign(Object.assign({}, rule), { resource: (resource) => {
currentResource = resource;
return true;
}, resourceQuery: (query) => {
const parsed = qs.parse(query.slice(1));
if (parsed.vue == null || parsed.type !== 'template') {
return false;
}
const fakeResourcePath = `${currentResource}.js`;
if (resource && !resource(fakeResourcePath)) {
return false;
}
if (resourceQuery && !resourceQuery(query)) {
return false;
}
return true;
} });
if (rule.rules) {
res.rules = rule.rules.map(cloneRuleForRenderFn);
}
if (rule.oneOf) {
res.oneOf = rule.oneOf.map(cloneRuleForRenderFn);
}
return res;
}
exports.default = VueLoaderPlugin;
import { Compiler, Plugin } from 'webpack';
declare class VueLoaderPlugin implements Plugin {
static NS: string;
apply(compiler: Compiler): void;
}
export default VueLoaderPlugin;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const qs = require("querystring");
const id = 'vue-loader-plugin';
const NS = 'vue-loader';
const NormalModule = require('webpack/lib/NormalModule');
const BasicEffectRulePlugin = require('webpack/lib/rules/BasicEffectRulePlugin');
const BasicMatcherRulePlugin = require('webpack/lib/rules/BasicMatcherRulePlugin');
const DescriptionDataMatcherRulePlugin = require('webpack/lib/rules/DescriptionDataMatcherRulePlugin');
const UseEffectRulePlugin = require('webpack/lib/rules/UseEffectRulePlugin');
const RuleSetCompiler = require('webpack/lib/rules/RuleSetCompiler');
const ruleSetCompiler = new RuleSetCompiler([
new BasicMatcherRulePlugin('test', 'resource'),
new BasicMatcherRulePlugin('mimetype'),
new BasicMatcherRulePlugin('dependency'),
new BasicMatcherRulePlugin('include', 'resource'),
new BasicMatcherRulePlugin('exclude', 'resource', true),
new BasicMatcherRulePlugin('conditions'),
new BasicMatcherRulePlugin('resource'),
new BasicMatcherRulePlugin('resourceQuery'),
new BasicMatcherRulePlugin('resourceFragment'),
new BasicMatcherRulePlugin('realResource'),
new BasicMatcherRulePlugin('issuer'),
new BasicMatcherRulePlugin('compiler'),
new DescriptionDataMatcherRulePlugin(),
new BasicEffectRulePlugin('type'),
new BasicEffectRulePlugin('sideEffects'),
new BasicEffectRulePlugin('parser'),
new BasicEffectRulePlugin('resolve'),
new BasicEffectRulePlugin('generator'),
new UseEffectRulePlugin(),
]);
class VueLoaderPlugin {
apply(compiler) {
// add NS marker so that the loader can detect and report missing plugin
compiler.hooks.compilation.tap(id, (compilation) => {
NormalModule.getCompilationHooks(compilation).loader.tap(id, (loaderContext) => {
loaderContext[NS] = true;
});
});
const rules = compiler.options.module.rules;
let rawVueRule;
let vueRules = [];
for (const rawRule of rules) {
// skip rules with 'enforce'. eg. rule for eslint-loader
if (rawRule.enforce) {
continue;
}
vueRules = match(rawRule, 'foo.vue');
if (!vueRules.length) {
vueRules = match(rawRule, 'foo.vue.html');
}
if (vueRules.length > 0) {
if (rawRule.oneOf) {
throw new Error(`[VueLoaderPlugin Error] vue-loader currently does not support vue rules with oneOf.`);
}
rawVueRule = rawRule;
break;
}
}
if (!vueRules.length) {
throw new Error(`[VueLoaderPlugin Error] No matching rule for .vue files found.\n` +
`Make sure there is at least one root-level rule that matches .vue or .vue.html files.`);
}
// get the normlized "use" for vue files
const vueUse = vueRules
.filter((rule) => rule.type === 'use')
.map((rule) => rule.value);
// get vue-loader options
const vueLoaderUseIndex = vueUse.findIndex((u) => {
// FIXME: this code logic is incorrect when project paths starts with `vue-loader-something`
return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader);
});
if (vueLoaderUseIndex < 0) {
throw new Error(`[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` +
`Make sure the rule matching .vue files include vue-loader in its use.`);
}
// make sure vue-loader options has a known ident so that we can share
// options by reference in the template-loader by using a ref query like
// template-loader??vue-loader-options
const vueLoaderUse = vueUse[vueLoaderUseIndex];
const vueLoaderOptions = (vueLoaderUse.options =
vueLoaderUse.options || {});
// for each user rule (expect the vue rule), create a cloned rule
// that targets the corresponding language blocks in *.vue files.
const refs = new Map();
const clonedRules = rules
.filter((r) => r !== rawVueRule)
.map((rawRule) => cloneRule(rawRule, refs, langBlockRuleCheck, langBlockRuleResource));
// fix conflict with config.loader and config.options when using config.use
delete rawVueRule.loader;
delete rawVueRule.options;
rawVueRule.use = vueUse;
// rule for template compiler
const templateCompilerRule = {
loader: require.resolve('./templateLoader'),
resourceQuery: (query) => {
if (!query) {
return false;
}
const parsed = qs.parse(query.slice(1));
return parsed.vue != null && parsed.type === 'template';
},
options: vueLoaderOptions,
};
// for each rule that matches plain .js files, also create a clone and
// match it against the compiled template code inside *.vue files, so that
// compiled vue render functions receive the same treatment as user code
// (mostly babel)
const jsRulesForRenderFn = rules
.filter((r) => r !== rawVueRule && match(r, 'test.js').length > 0)
.map((rawRule) => cloneRule(rawRule, refs, jsRuleCheck, jsRuleResource));
// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
const pitcher = {
loader: require.resolve('./pitcher'),
resourceQuery: (query) => {
if (!query) {
return false;
}
const parsed = qs.parse(query.slice(1));
return parsed.vue != null;
},
};
// replace original rules
compiler.options.module.rules = [
pitcher,
...jsRulesForRenderFn,
templateCompilerRule,
...clonedRules,
...rules,
];
}
}
VueLoaderPlugin.NS = NS;
const matcherCache = new WeakMap();
function match(rule, fakeFile) {
let ruleSet = matcherCache.get(rule);
if (!ruleSet) {
// skip the `include` check when locating the vue rule
const clonedRawRule = Object.assign({}, rule);
delete clonedRawRule.include;
ruleSet = ruleSetCompiler.compile([clonedRawRule]);
matcherCache.set(rule, ruleSet);
}
return ruleSet.exec({
resource: fakeFile,
});
}
const langBlockRuleCheck = (query, rule) => {
return (query.type === 'custom' || !rule.conditions.length || query.lang != null);
};
const langBlockRuleResource = (query, resource) => `${resource}.${query.lang}`;
const jsRuleCheck = (query) => {
return query.type === 'template';
};
const jsRuleResource = (query, resource) => `${resource}.js`;
let uid = 0;
function cloneRule(rawRule, refs, ruleCheck, ruleResource) {
const compiledRule = ruleSetCompiler.compileRule(`clonedRuleSet-${++uid}`, rawRule, refs);
// do not process rule with enforce
if (!rawRule.enforce) {
const ruleUse = compiledRule.effects
.filter((effect) => effect.type === 'use')
.map((effect) => effect.value);
// fix conflict with config.loader and config.options when using config.use
delete rawRule.loader;
delete rawRule.options;
rawRule.use = ruleUse;
}
let currentResource;
const res = Object.assign(Object.assign({}, rawRule), { resource: (resources) => {
currentResource = resources;
return true;
}, resourceQuery: (query) => {
if (!query) {
return false;
}
const parsed = qs.parse(query.slice(1));
if (parsed.vue == null) {
return false;
}
if (!ruleCheck(parsed, compiledRule)) {
return false;
}
const fakeResourcePath = ruleResource(parsed, currentResource);
for (const condition of compiledRule.conditions) {
// add support for resourceQuery
const request = condition.property === 'resourceQuery' ? query : fakeResourcePath;
if (condition && !condition.fn(request)) {
return false;
}
}
return true;
} });
delete res.test;
if (rawRule.rules) {
res.rules = rawRule.rules.map((rule) => cloneRule(rule, refs, ruleCheck, ruleResource));
}
if (rawRule.oneOf) {
res.oneOf = rawRule.oneOf.map((rule) => cloneRule(rule, refs, ruleCheck, ruleResource));
}
return res;
}
exports.default = VueLoaderPlugin;
import webpack = require('webpack');
import { SFCDescriptor, SFCScriptBlock } from '@vue/compiler-sfc';
import { VueLoaderOptions } from 'src';
/**
* inline template mode can only be enabled if:
* - is production (separate compilation needed for HMR during dev)
* - template has no pre-processor (separate loader chain required)
* - template is not using src
*/
export declare function canInlineTemplate(descriptor: SFCDescriptor, isProd: boolean): boolean;
export declare function resolveScript(descriptor: SFCDescriptor, scopeId: string, options: VueLoaderOptions, loaderContext: webpack.loader.LoaderContext): SFCScriptBlock | null;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveScript = exports.canInlineTemplate = void 0;
const compiler_sfc_1 = require("@vue/compiler-sfc");
const clientCache = new WeakMap();
const serverCache = new WeakMap();
/**
* inline template mode can only be enabled if:
* - is production (separate compilation needed for HMR during dev)
* - template has no pre-processor (separate loader chain required)
* - template is not using src
*/
function canInlineTemplate(descriptor, isProd) {
const templateLang = descriptor.template && descriptor.template.lang;
const templateSrc = descriptor.template && descriptor.template.src;
return isProd && !!descriptor.scriptSetup && !templateLang && !templateSrc;
}
exports.canInlineTemplate = canInlineTemplate;
function resolveScript(descriptor, scopeId, options, loaderContext) {
var _a;
if (!descriptor.script && !descriptor.scriptSetup) {
return null;
}
const isProd = loaderContext.mode === 'production' || process.env.NODE_ENV === 'production';
const isServer = (_a = options.isServerBuild) !== null && _a !== void 0 ? _a : loaderContext.target === 'node';
const enableInline = canInlineTemplate(descriptor, isProd);
const cacheToUse = isServer ? serverCache : clientCache;
const cached = cacheToUse.get(descriptor);
if (cached) {
return cached;
}
let resolved = null;
let compiler;
if (typeof options.compiler === 'string') {
compiler = require(options.compiler);
}
else {
compiler = options.compiler;
}
if (compiler_sfc_1.compileScript) {
try {
resolved = compiler_sfc_1.compileScript(descriptor, {
id: scopeId,
isProd,
inlineTemplate: enableInline,
refSugar: options.refSugar,
babelParserPlugins: options.babelParserPlugins,
templateOptions: {
ssr: isServer,
compiler,
compilerOptions: options.compilerOptions,
transformAssetUrls: options.transformAssetUrls || true,
},
});
}
catch (e) {
loaderContext.emitError(e);
}
}
else if (descriptor.scriptSetup) {
loaderContext.emitError(`<script setup> is not supported by the installed version of ` +
`@vue/compiler-sfc - please upgrade.`);
}
else {
resolved = descriptor.script;
}
cacheToUse.set(descriptor, resolved);
return resolved;
}
exports.resolveScript = resolveScript;
/// <reference types="node" />
import webpack = require('webpack');
import { SFCDescriptor } from '@vue/compiler-sfc';
import { ParsedUrlQuery } from 'querystring';
import { VueLoaderOptions } from 'src';
export declare function selectBlock(descriptor: SFCDescriptor, scopeId: string, options: VueLoaderOptions, loaderContext: webpack.loader.LoaderContext, query: ParsedUrlQuery, appendExtension: boolean): void;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.selectBlock = void 0;
const resolveScript_1 = require("./resolveScript");
function selectBlock(descriptor, scopeId, options, loaderContext, query, appendExtension) {
// template
if (query.type === `template`) {
// if we are receiving a query with type it can only come from a *.vue file
// that contains that block, so the block is guaranteed to exist.
const template = descriptor.template;
if (appendExtension) {
loaderContext.resourcePath += '.' + (template.lang || 'html');
}
loaderContext.callback(null, template.content, template.map);
return;
}
// script
if (query.type === `script`) {
const script = resolveScript_1.resolveScript(descriptor, scopeId, options, loaderContext);
if (appendExtension) {
loaderContext.resourcePath += '.' + (script.lang || 'js');
}
loaderContext.callback(null, script.content, script.map);
return;
}
// styles
if (query.type === `style` && query.index != null) {
const style = descriptor.styles[Number(query.index)];
if (appendExtension) {
loaderContext.resourcePath += '.' + (style.lang || 'css');
}
loaderContext.callback(null, style.content, style.map);
return;
}
// custom
if (query.type === 'custom' && query.index != null) {
const block = descriptor.customBlocks[Number(query.index)];
loaderContext.callback(null, block.content, block.map);
}
}
exports.selectBlock = selectBlock;
import webpack = require('webpack');
declare const StyleInineLoader: webpack.loader.Loader;
export default StyleInineLoader;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const StyleInineLoader = function (source) {
// TODO minify this?
return `export default ${JSON.stringify(source)}`;
};
exports.default = StyleInineLoader;
import webpack = require('webpack');
declare const StylePostLoader: webpack.loader.Loader;
export default StylePostLoader;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const qs = require("querystring");
const compiler_sfc_1 = require("@vue/compiler-sfc");
// This is a post loader that handles scoped CSS transforms.
// Injected right before css-loader by the global pitcher (../pitch.js)
// for any <style scoped> selection requests initiated from within vue files.
const StylePostLoader = function (source, inMap) {
const query = qs.parse(this.resourceQuery.slice(1));
const { code, map, errors } = compiler_sfc_1.compileStyle({
source: source,
filename: this.resourcePath,
id: `data-v-${query.id}`,
map: inMap,
scoped: !!query.scoped,
trim: true,
isProd: this.mode === 'production' || process.env.NODE_ENV === 'production',
});
if (errors.length) {
this.callback(errors[0]);
}
else {
this.callback(null, code, map);
}
};
exports.default = StylePostLoader;
import webpack = require('webpack');
declare const TemplateLoader: webpack.loader.Loader;
export default TemplateLoader;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const qs = require("querystring");
const loaderUtils = require("loader-utils");
const formatError_1 = require("./formatError");
const compiler_sfc_1 = require("@vue/compiler-sfc");
const descriptorCache_1 = require("./descriptorCache");
const resolveScript_1 = require("./resolveScript");
// Loader that compiles raw template into JavaScript functions.
// This is injected by the global pitcher (../pitch) for template
// selection requests initiated from vue files.
const TemplateLoader = function (source, inMap) {
var _a;
source = String(source);
const loaderContext = this;
// although this is not the main vue-loader, we can get access to the same
// vue-loader options because we've set an ident in the plugin and used that
// ident to create the request for this loader in the pitcher.
const options = (loaderUtils.getOptions(loaderContext) ||
{});
const isServer = (_a = options.isServerBuild) !== null && _a !== void 0 ? _a : loaderContext.target === 'node';
const isProd = loaderContext.mode === 'production' || process.env.NODE_ENV === 'production';
const query = qs.parse(loaderContext.resourceQuery.slice(1));
const scopeId = query.id;
const descriptor = descriptorCache_1.getDescriptor(loaderContext.resourcePath);
const script = resolveScript_1.resolveScript(descriptor, query.id, options, loaderContext);
let compiler;
if (typeof options.compiler === 'string') {
compiler = require(options.compiler);
}
else {
compiler = options.compiler;
}
const compiled = compiler_sfc_1.compileTemplate({
source,
filename: loaderContext.resourcePath,
inMap,
id: scopeId,
scoped: !!query.scoped,
slotted: descriptor.slotted,
isProd,
ssr: isServer,
ssrCssVars: descriptor.cssVars,
compiler,
compilerOptions: Object.assign(Object.assign({}, options.compilerOptions), { scopeId: query.scoped ? `data-v-${scopeId}` : undefined, bindingMetadata: script ? script.bindings : undefined }),
transformAssetUrls: options.transformAssetUrls || true,
});
// tips
if (compiled.tips.length) {
compiled.tips.forEach((tip) => {
loaderContext.emitWarning(tip);
});
}
// errors
if (compiled.errors && compiled.errors.length) {
compiled.errors.forEach((err) => {
if (typeof err === 'string') {
loaderContext.emitError(err);
}
else {
formatError_1.formatError(err, inMap ? inMap.sourcesContent[0] : source, loaderContext.resourcePath);
loaderContext.emitError(err);
}
});
}
const { code, map } = compiled;
loaderContext.callback(null, code, map);
};
exports.default = TemplateLoader;
{
"name": "vue-loader",
"version": "16.3.3",
"license": "MIT",
"author": "Evan You",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"dev": "tsc --watch",
"build": "tsc",
"pretest": "tsc",
"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",
"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"
],
"*.ts": [
"prettier --parser=typescript --write"
]
},
"dependencies": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"peerDependencies": {
"@vue/compiler-sfc": "^3.0.8",
"webpack": "^4.1.0 || ^5.0.0-0"
},
"peerDependenciesMeta": {
"@vue/compiler-sfc": {
"optional": true
}
},
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/preset-env": "^7.11.5",
"@types/estree": "^0.0.45",
"@types/hash-sum": "^1.0.0",
"@types/jest": "^26.0.13",
"@types/jsdom": "^16.2.4",
"@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.8",
"babel-loader": "^8.1.0",
"cache-loader": "^4.1.0",
"conventional-changelog-cli": "^2.1.1",
"css-loader": "^4.3.0",
"file-loader": "^6.1.0",
"html-webpack-plugin": "^4.5.0",
"jest": "^26.4.1",
"jsdom": "^16.4.0",
"lint-staged": "^10.3.0",
"markdown-loader": "^6.0.0",
"memfs": "^3.1.2",
"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": "^4.1.1",
"sugarss": "^3.0.1",
"ts-jest": "^26.2.0",
"ts-loader": "^8.0.6",
"typescript": "^4.0.2",
"url-loader": "^4.1.0",
"vue": "^3.0.8",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"webpack-merge": "^5.1.4",
"webpack5": "npm:webpack@5",
"yorkie": "^2.0.0"
}
}
......@@ -21,6 +21,7 @@
"gitHead": "3b83cc9dc4c9a214747c626f79693559c338044f",
"dependencies": {
"terser-webpack-plugin": "^5.1.4",
"vue-loader": "^16.3.3",
"webpack": "^5.45.1"
},
"devDependencies": {
......
import { Configuration } from 'webpack'
import { optimization } from './optimization'
import { output } from './output'
import { module } from './module'
import { plugins } from './plugins'
export function createConfig(
mode: 'production' | 'development'
): Configuration {
......@@ -17,5 +19,7 @@ export function createConfig(
},
optimization,
output,
module,
plugins,
}
}
import { Configuration } from 'webpack'
import { rules } from './rules'
export const module: Configuration['module'] = {
rules,
}
import { RuleSetRule } from 'webpack'
export const rules: RuleSetRule[] = [
{
test: [/\.nvue(\?[^?]+)?$/, /\.vue(\?[^?]+)?$/],
loader: 'vue-loader',
},
]
......@@ -2,4 +2,11 @@ import { Configuration } from 'webpack'
import { define } from './define'
import { banner } from './banner'
import { provide } from './provide'
export const plugins: Configuration['plugins'] = [define, banner, provide]
import { vueLoaderPlugin } from './vueLoader'
export const plugins: Configuration['plugins'] = [
define,
banner,
provide,
vueLoaderPlugin,
]
const { VueLoaderPlugin } = require('../../../../lib/vue-loader/dist')
export const vueLoaderPlugin = new VueLoaderPlugin()
......@@ -6656,6 +6656,15 @@ loader-utils@^1.1.0:
emojis-list "^3.0.0"
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^2.1.2"
localstorage-polyfill@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/localstorage-polyfill/-/localstorage-polyfill-1.0.1.tgz#4b3083d4bc51d23b4158537e66816137413fd31a"
......@@ -9708,6 +9717,15 @@ vlq@^0.2.2:
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
vue-loader@^16.3.3:
version "16.3.3"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.3.3.tgz#e440e4eb12786e16138b5d98b612208f29ddf532"
integrity sha512-/1GzCuQ6MRORbC+leKTKoTGtpQt60bYe0gDGEextSteA2OM+v201FPha5jzmjQzVhRcwieZeUvezAtG5a/e5cw==
dependencies:
chalk "^4.1.0"
hash-sum "^2.0.0"
loader-utils "^2.0.0"
vue-router@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.10.tgz#ec8fda032949b2a31d3273170f8f376e86eb52ac"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册