Use "@vue/compiler-sfc" to get a root component, but the _ctx of render function doesn't have any my variable and throw a reading undefined error. #8964
-
Vue version3.3.4 Link to minimal reproductionSteps to reproduce
let __component: DefineComponent; // it is compileScript() 's output.
function render(_ctx, _cache){/*...*/} // it is compileTemplate() 's output.
__component.render = render
What is expected?Render a <input /> with v-model. What is actually happening?Cannot read properties of undefined (reading 'value'). object System InfoSystem:
OS: Windows 10 10.0.22621
CPU: (32) x64 13th Gen Intel(R) Core(TM) i9-13900HX
Memory: 14.55 GB / 31.74 GB
Binaries:
Node: 20.3.1 - D:\Program Files\nodejs\node.EXE
Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD
npm: 9.6.6 - D:\Program Files\nodejs\npm.CMD
pnpm: 8.6.7 - ~\AppData\Roaming\npm\pnpm.CMD
Browsers:
Edge: Spartan (44.22621.2134.0), Chromium (115.0.1901.203)
Internet Explorer: 11.0.22621.1 Any additional comments?Use koa, @vue/compiler-sfc and babel, but do not include the Vite. Vite was too heavy, so instead of using it, I made my own loader using KoaController, @vue/compiler-sfc and babel. Compile SFC file at run time, but all the browser receives is "text/javascrtpt".
There is a simplified version in SFC Playground SFC code: <template>
<div>
<input v-model="form.value" type="text" />
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
const form = reactive({
value: 'str',
});
</script>
<style scoped lang="scss">
input {
background-color: blue;
}
</style>
Full raw es6 javascript code in a html file: import { vModelText as _vModelText, createElementVNode as _createElementVNode, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/oAuth/lib/vue";
import { defineComponent as _defineComponent } from "/oAuth/lib/vue";
import { reactive } from "/oAuth/lib/vue";
const __component = _defineComponent({
setup(__props, {
expose: __expose
}) {
__expose();
const form = reactive({
value: 'str'
});
const __returned__ = {
form
};
Object.defineProperty(__returned__, '__isScriptSetup', {
enumerable: false,
value: true
});
return __returned__;
}
});
export function render(_ctx, _cache) {
return _openBlock(), _createElementBlock("div", null, [_withDirectives(_createElementVNode("input", {
type: "text",
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => _ctx.form.value = $event)
}, null, 512), [[_vModelText, _ctx.form.value]])]);
}
__component.__scopeId = "data-v-sfc18";
__component.render = render;
const onAppCreate = () => {};
const onBeforeAppCreate = () => {};
__component.appCreate = c => import("/oAuth/lib/vue").then(it => it.createApp(c));
;
export let app = void 0;
(async () => {
let appCreate = __component.appCreate;
if (!appCreate) appCreate = () => import("/oAuth/lib/vue").then(it => it.createApp(c));
const app1 = await appCreate(__component);
app = app1;
app1.mount('#app');
})();
export default __component;
;
(url => {
const styles = `input[data-v-sfc18] {
background-color: blue;
}
`;
let dom = document.head.querySelector(`style[data-url="${url}"]`);
dom?.remove();
dom = document.createElement('style');
dom.setAttribute('data-url', url);
dom.innerHTML = styles;
document.head.append(dom);
})("/oAuth/test.a.vue"); The source code of loader (the Midway framework, it based on koa): import { Config, Controller, Get, Inject } from '@midwayjs/decorator';
import process from 'process';
import path from 'path';
import fs from 'fs';
import mime from 'mime';
import { httpError } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { transformAsync, transformFileAsync, TransformOptions } from '@babel/core';
import sass from 'sass';
import preset from '@babel/preset-typescript';
import resolver from 'babel-plugin-module-resolver';
import { minify, MinifyOptions } from 'terser';
import { compileScript, compileStyle, compileTemplate, parse as compilerSfc } from '@vue/compiler-sfc';
import {
CallExpression,
ExportDefaultDeclaration,
ExpressionStatement,
ObjectExpression,
ObjectMethod,
ObjectProperty,
Statement,
} from '@babel/types';
@Controller('/oAuth')
export class OAuthController {
static cache = new Map<string, string>();
@Inject()
ctx: Context;
@Get('/*')
async oAuth() {
const ctx = this.ctx;
let file = ctx.url.replace(/^\/oAuth\//, '');
if (file.includes('?')) file = file.split('?')[0];
if (!file) file = 'index.html';
if (process.env.NODE_ENV === 'production' && ['lib/vue.js', 'lib/vue'].includes(file)) {
return this.fileLoader(path.join(__dirname, '../static/oAuth/lib/vue.prod.js'));
}
const rawFilePath = path.join(__dirname, '../static/oAuth/' + file);
function* tryFiles(file: string) {
yield rawFilePath;
yield path.join(__dirname, '../static/oAuth/' + file + '.html');
yield path.join(__dirname, '../static/oAuth/' + file + '.js');
yield path.join(__dirname, '../static/oAuth/' + file + '.ts');
yield path.join(__dirname, '../static/oAuth/' + file + '.d.ts');
}
for (const file1 of tryFiles(file)) {
if (fs.existsSync(file1) && fs.statSync(file1).isFile()) {
if (file1.endsWith('.js') || file1.endsWith('.ts')) return this.tsLoader(file1);
else if (file1.endsWith('.css') || file1.endsWith('.scss')) return this.cssLoader(file1);
else if (file1.endsWith('.vue')) return this.sfcLoader(file1);
else return this.fileLoader(file1);
}
}
if (fs.existsSync(rawFilePath) && fs.statSync(rawFilePath).isDirectory()) {
let file = ctx.url;
if (file.includes('?')) file = file.split('?')[0];
file += '/index';
ctx.redirect(file);
return;
}
throw new httpError.NotFoundError();
}
/**
* 脚本加载器
* @param file 文件路径
*/
async tsLoader(file: string) {
this.ctx.type = 'text/javascript';
if (OAuthController.cache.has(file)) return OAuthController.cache.get(file);
if (file.endsWith('.d.ts')) {
return '//nothing there';
}
const option: TransformOptions = {
presets: [preset],
plugins: [
[
resolver,
{
loglevel: 'silent',
alias: {
vue: '/oAuth/lib/vue',
},
},
],
],
targets: {
esmodules: true,
},
compact: false,
root: 'static/oAuth',
sourceMaps: false,
comments: false,
};
if (process.env.NODE_ENV !== 'production') {
Object.assign(option, {
sourceMaps: 'inline',
comments: true,
} as TransformOptions);
}
let rs = await transformFileAsync(file, option).then(it => it.code);
if (process.env.NODE_ENV === 'production') {
rs = await minify(rs, {
ecma: 2020,
module: true,
compress: true,
}).then(it => it.code);
}
OAuthController.cache.set(file, rs);
return rs;
}
/**
* 样式表加载器
* @param file 文件路径
*/
async cssLoader(file: string) {
if (this.ctx.request.query.type !== 'link') {
let str = CSS_LOADER_FN;
let url = this.ctx.request.url;
url += url.includes('?') ? '&type=link' : '?type=link';
str = `(${str})(\`${encodeURI(url)}\`);`;
this.ctx.type = 'text/javascript';
return str;
}
if (file.endsWith('.scss')) {
this.ctx.type = 'text/css';
if (OAuthController.cache.has(file)) return OAuthController.cache.get(file);
const css = await sass.compileAsync(file);
OAuthController.cache.set(file, css.css);
return css.css;
}
return this.fileLoader(file);
}
/**
* Vue 单文件组件加载器
* @param file 文件路径
*/
async sfcLoader(file: string) {
const isApp = file.endsWith('.a.vue');
this.ctx.type = 'text/javascript';
if (this.ctx.request.query?.type === 'script') {
return OAuthController.cache.get(file + '?type=script');
} else if (OAuthController.cache.has(file)) {
if (isApp) this.ctx.type = 'text/html';
return OAuthController.cache.get(file);
}
if (isApp) this.ctx.type = 'text/html';
let file1 = this.ctx.request.url;
if (file1.includes('?')) file1 = file1.split('?')[0];
const id = `sfc${OAuthController.cache.size + 1}`;
let beforeAppCreate: string;
/**
* 将Vue SFC源码编译为javascript
* @param source 源代码
*/
async function compile(source: string) {
async function transform(component1: string) {
const it = await transformAsync(component1, {
presets: [preset],
plugins: [
[
resolver,
{
loglevel: 'silent',
alias: {
vue: '/oAuth/lib/vue',
},
},
],
],
targets: { esmodules: true },
filename: `${filename}.ts`,
root: 'static/oAuth',
compact: false,
sourceMaps: false,
comments: false,
});
return it.code;
}
const filename = file.split('/').slice(-1)[0];
const sfc = compilerSfc(source);
const template = compileTemplate({
filename,
id,
scoped: true,
source: sfc.descriptor.template.content,
}).code;
const renderImport = template.split('\n', 2)[0];
let renderCode = template.slice(template.indexOf('\n'));
renderCode = renderCode
.split('\n')
.map(it => {
if (it.startsWith('import _imports_')) {
const group = /^import\s(?<name>\S+)\sfrom\s(?<src>.+)$/.exec(it).groups;
return `const ${group.name} = ${group.src}`;
}
return it;
})
.join('\n');
const {
content: componentRaw,
scriptAst,
scriptSetupAst,
setup: isSetup,
} = compileScript(sfc.descriptor, {
id,
genDefaultAs: '__component',
});
let component = `${renderImport}
${componentRaw}
${renderCode}
__component.__scopeId = "data-v-${id}";
__component.render = render;
const onAppCreate = () => {};
const onBeforeAppCreate = () => {};
`;
if (isApp) {
if (!isSetup) {
beforeAppCreate = getBeforeAppCreateMethod(scriptAst, sfc.descriptor.script.content);
} else {
beforeAppCreate = getBeforeAppCreateMethodSetup(scriptSetupAst, sfc.descriptor.scriptSetup.content);
const appCreate = getAppCreateMethodSetup(scriptSetupAst, sfc.descriptor.scriptSetup.content);
component += `__component.appCreate = ${appCreate};\n`;
}
beforeAppCreate = await transform(beforeAppCreate);
component += `
export let app = void 0;
(async () => {
let appCreate = __component.appCreate;
if(!appCreate) appCreate = () => import("vue").then(it => it.createApp(c));
const app1 = await appCreate(__component);
app = app1;
app1.mount('#app');
})();
`;
}
component += 'export default __component;';
let styles = '`';
for (const style of sfc.descriptor.styles) {
const rs = compileStyle({
filename,
id,
scoped: style.scoped,
source: style.content,
preprocessLang: (style.lang === 'css' ? undefined : style.lang) as any,
});
let rs1 = await sass.compileStringAsync(rs.code).then(it => it.css);
rs1 = rs1.replace('`', '\\`');
if (style.lang === 'scss') styles += `${rs1}\n`;
}
styles += '`';
const fileName1 = encodeURI(file1);
const styleScript = `
;(url =>{
const styles = ${styles};
let dom = document.head.querySelector(\`style[data-url="\${url}"]\`);
dom?.remove();
dom = document.createElement('style');
dom.setAttribute('data-url', url);
dom.innerHTML = styles;
document.head.append(dom);
})("${fileName1}");
`;
component += styleScript;
component = await transform(component);
return component;
}
const source = fs.readFileSync(file, 'utf8');
let rs = await compile(source);
if (process.env.NODE_ENV === 'production') {
const opt: MinifyOptions = {
ecma: 2020,
module: true,
compress: true,
};
rs = (await minify(rs, opt)).code;
beforeAppCreate = (await minify(beforeAppCreate, opt)).code;
}
if (isApp) {
let url = this.ctx.request.url;
url += url.includes('?') ? '&type=script' : '?type=script';
OAuthController.cache.set(`${file}?type=script`, rs);
const html = `
<!DOCTYPE html>
<html lang="zh">
<head>
<title>IGame</title>
</head>
<body>
<div id="app"></div>
<script>
(async () => {
const fn = ${beforeAppCreate};
await fn();
await import("${encodeURI(url)}")
})().catch(e => console.error(e));
</script>
</body>
</html>
`;
OAuthController.cache.set(file, html);
return html;
} else {
OAuthController.cache.set(file, rs);
return rs;
}
}
async fileLoader(file: string) {
const stream = fs.createReadStream(file);
stream.setMaxListeners(0);
this.ctx.type = mime.getType(file);
return stream;
}
}
const CSS_LOADER_FN = `s=>{
let d=document,
h=d.head,
e=h.querySelector(\`link[href="\${s}"]\`),
f=(a,b)=>e.setAttribute(a,b);
e?.remove();
e=d.createElement('link');
f('rel','stylesheet');
f('type','text/css');
f('href',s);
h.append(e);
}`.replaceAll('\n', '');
function getBeforeAppCreateMethod(node: Statement[], source: string): string {
const ex = node.find(it => it.type === 'ExportDefaultDeclaration') as ExportDefaultDeclaration;
if (!ex) throw new Error('未能找到 export default 定义.');
const d = ex.declaration;
let obj: ObjectExpression;
if (d.type === 'CallExpression' && d.callee.type === 'Identifier' && d.callee.name === 'defineComponent') {
const arg1 = d.arguments[0];
if (arg1.type === 'ObjectExpression') {
obj = arg1;
} else {
throw new Error(`defineComponent 方法中给定的第一个参数不是对象.位于: ${d.loc.start.line}:${d.loc.start.column}`);
}
} else if (d.type === 'ObjectExpression') {
obj = d;
} else {
throw new Error('export default 给定了不支持的表达式,该导出位置的源码必须为对象或者 defineComponent() 语句.');
}
let value: ObjectMethod | ObjectProperty;
for (const property of obj.properties) {
if (
property.type === 'ObjectMethod' &&
property.key.type === 'Identifier' &&
property.key.name === 'beforeAppCreate'
) {
value = property;
break;
} else if (
property.type === 'ObjectProperty' &&
property.key.type === 'Identifier' &&
property.key.name === 'beforeAppCreate'
) {
value = property;
break;
}
}
//if (!value) throw new Error('VueSFC应用程序的组件定义中必须包含 beforeAppCreate() 方法。');
if (!value) return '() => {}';
let out: string;
if (value.type === 'ObjectProperty') {
const content = value.value;
if (content.type === 'ArrowFunctionExpression') {
out = source.slice(content.start, content.end);
} else if (content.type === 'FunctionExpression') {
out = source.slice(content.start, content.end);
} else throw new Error('未知的 beforeAppCreate 属性类型,该类型的源码在此处必须为 function.');
} else {
out = source.slice(value.start, value.end);
if (out.startsWith('async ')) out = 'async function' + out.slice(5);
else out = `function ${out}`;
}
return out;
}
function getBeforeAppCreateMethodSetup(node: Statement[], source: string): string {
const d = node.find(
it =>
it.type === 'ExpressionStatement' &&
it.expression.type === 'CallExpression' &&
it.expression.callee.type === 'Identifier' &&
it.expression.callee.name === 'onBeforeAppCreate'
) as ExpressionStatement;
if (!d) return '() => {}';
let out: string;
const content = (d.expression as CallExpression).arguments[0];
if (content.type === 'ArrowFunctionExpression') {
out = source.slice(content.start, content.end);
} else if (content.type === 'FunctionExpression') {
out = source.slice(content.start, content.end);
} else throw new Error('未知的 beforeAppCreate 属性类型,该类型的源码在此处必须为 function.');
return out;
}
function getAppCreateMethodSetup(node: Statement[], source: string): string {
const d = node.find(
it =>
it.type === 'ExpressionStatement' &&
it.expression.type === 'CallExpression' &&
it.expression.callee.type === 'Identifier' &&
it.expression.callee.name === 'onAppCreate'
) as ExpressionStatement;
if (!d) return '(c) => import("vue").then(it => it.createApp(c));';
let out: string;
const content = (d.expression as CallExpression).arguments[0];
if (content.type === 'ArrowFunctionExpression') {
out = source.slice(content.start, content.end);
} else if (content.type === 'FunctionExpression') {
out = source.slice(content.start, content.end);
} else throw new Error('未知的 appCreate 属性类型,该类型的源码在此处必须为 function.');
return out;
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
I looked at the source code, and mode setup requires the bindings (compilerOptions.bindingMetadata). const filename = file.split('/').slice(-1)[0];
const sfc = compilerSfc(source);
const {
content: componentRaw,
scriptAst,
scriptSetupAst,
setup: isSetup,
bindings, // HERE
} = compileScript(sfc.descriptor, {
id,
genDefaultAs: '__component',
});
const template = compileTemplate({
filename,
id,
scoped: true,
source: sfc.descriptor.template.content,
compilerOptions: {
bindingMetadata: bindings, //HERE
},
} as SFCTemplateCompileOptions).code; It comes from line 231 of "@vue/compiler-core --> codegen.ts" and from line 2208 of "@vue/compiler-core --> dist/compiler-core.cjs.js" // enter render function
const functionName = ssr ? `ssrRender` : `render`
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
if (!__BROWSER__ && options.bindingMetadata && !options.inline) { //HERE
// binding optimization args
args.push('$props', '$setup', '$data', '$options') //HERE
} The modified source code is generated, which is basically the same as the example SFC Playground. export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => $setup.form.value = $event),
type: "text"
}, null, 512), [[_vModelText, $setup.form.value]])]);
} Previous source code, it cannot be used in setup mode: export function render(_ctx, _cache) {
return _openBlock(), _createElementBlock("div", null, [_withDirectives(_createElementVNode("input", {
type: "text",
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => _ctx.form.value = $event)
}, null, 512), [[_vModelText, _ctx.form.value]])]);
} |
Beta Was this translation helpful? Give feedback.
I looked at the source code, and mode setup requires the bindings (compilerOptions.bindingMetadata).