提交 0ae45ea3 编写于 作者: fxy060608's avatar fxy060608

wip(uts): compiler

上级 fb5c25c1
......@@ -48,7 +48,7 @@ module.exports = {
'jest.config.js',
'rollup.config.mjs',
'scripts/**',
'packages/{uni-cli-shared,uni-app-vite,uni-h5-vite,uni-mp-vite,uni-mp-compiler,vite-plugin-uni,uts,uni-uts-v1}/**',
'packages/{uni-cli-shared,uni-app-vite,uni-h5-vite,uni-mp-vite,uni-mp-compiler,vite-plugin-uni,uts,uni-uts-v1,uni-app-uts}/**',
'packages/*/vite.config.ts',
],
rules: {
......
......@@ -9,6 +9,7 @@ packages/uni-nvue-styler/dist
packages/vite-plugin-uni/dist
packages/uts/dist
packages/uni-uts-v1/dist
packages/uni-app-uts/dist
.DS_Store
......
......@@ -9,7 +9,7 @@
"clean": "rm -rf node_modules **/*/node_modules && pnpm install",
"build": "node scripts/build.js",
"build:h5": "node scripts/build.js uni-app uts uni-uts-v1 uni-cli-shared uni-h5 uni-i18n uni-stat 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-app-uvue",
"build:mp": "node scripts/build.js uni-mp-vue uni-mp-vite uni-mp-compiler uni-mp-alipay uni-mp-baidu uni-mp-kuaishou uni-mp-lark uni-mp-qq uni-mp-toutiao uni-mp-weixin uni-quickapp-webview",
"size": "npm run build size-check",
"lint": "eslint packages/*/src/**/*.ts",
......@@ -98,4 +98,4 @@
},
"packageManager": "pnpm@8.2.0",
"name": "uni-app-next"
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@
},
"devDependencies": {
"@dcloudio/vite-plugin-uni": "../../vite-plugin-uni",
"vite": "^4.0.0"
"vite": "^4.2.1"
},
"resolutions": {
"@dcloudio/uni-app-vite": "../../uni-app-vite"
......
......@@ -34,6 +34,8 @@ declare namespace NodeJS {
UNI_CUSTOM_DEFINE?: string
UNI_CUSTOM_CONTEXT?: string
UNI_MINIMIZE?: 'true'
UNI_UVUE?: 'true'
UNI_UVUE_TARGET_LANGUAGE?: 'kotlin' | 'swift' | 'javascript'
UNI_COMPILER: 'vue' | 'nvue'
UNI_RENDERER_NATIVE: 'appService' | 'pages'
UNI_NVUE_APP_STYLES: string
......
......@@ -18,6 +18,7 @@
"format": "cjs"
},
"external": [
"@dcloudio/uni-app-uts",
"@dcloudio/uni-app-vite"
]
},
......
'use strict';
var appVite = require('@dcloudio/uni-app-vite');
var appUVue = require('@dcloudio/uni-app-uts');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var appVite__default = /*#__PURE__*/_interopDefault(appVite);
var appUVue__default = /*#__PURE__*/_interopDefault(appUVue);
var index = [appVite__default.default];
var index = [process.env.UNI_UVUE === 'true' ? appUVue__default.default : appVite__default.default];
module.exports = index;
......@@ -25,9 +25,11 @@
"apply": [
"app"
],
"uvue": true,
"main": "dist/uni.compiler.js"
},
"dependencies": {
"@dcloudio/uni-app-uts": "3.0.0-alpha-3071220230324001",
"@dcloudio/uni-app-vite": "3.0.0-alpha-3071220230324001",
"@dcloudio/uni-app-vue": "3.0.0-alpha-3071220230324001",
"debug": "^4.3.3",
......@@ -48,4 +50,4 @@
"postcss": "^8.4.21",
"vue": "3.2.47"
}
}
}
\ No newline at end of file
import appVite from '@dcloudio/uni-app-vite'
export default [appVite]
import appUVue from '@dcloudio/uni-app-uts'
export default [process.env.UNI_UVUE === 'true' ? appUVue : appVite]
import { assert } from './testUtils'
describe('compiler:codegen', () => {
test('default', () => {
assert(`<view/>`, `createElementVNode("view")`)
assert(
`<view style="width:100px;height:100px;"/>`,
`createElementVNode("view", new Map<string,any>([["style", "width:100px;height:100px;"]]))`
)
})
test(`function:kotlin`, () => {
assert(
`<view/>`,
`@Suppress("UNUSED_PARAMETER") function PagesIndexIndexRender(ctx: PagesIndexIndex): VNode | null {\n return createElementVNode("view")\n}`,
{
targetLanguage: 'kotlin',
mode: 'function',
}
)
})
})
import { assert } from './testUtils'
console.log(
assert(`<view style="width:100px;height:100px;"><text>aaa</text></view>`, ``)
)
import { compile } from '../src/plugins/uvue/code/template/compiler/index'
import { CompilerOptions } from '../src/plugins/uvue/code/template/compiler/options'
export function assert(
template: string,
templateCode: string,
options: CompilerOptions = { targetLanguage: 'kotlin' }
) {
const compilerOptions: CompilerOptions = {
filename: 'PagesIndexIndex',
prefixIdentifiers: true,
...options,
}
const res = compile(template, compilerOptions)
if (typeof expect !== 'undefined') {
expect(res.code).toBe(templateCode)
}
return res
}
declare const _default: never[];
declare const _default: () => import("vite").Plugin[];
export default _default;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = [];
const plugins_1 = require("./plugins");
const mainUTS_1 = require("./plugins/mainUTS");
const manifestJson_1 = require("./plugins/manifestJson");
const pagesJson_1 = require("./plugins/pagesJson");
const pre_1 = require("./plugins/pre");
const uvue_1 = require("./plugins/uvue");
exports.default = () => {
return [
(0, pre_1.uniPrePlugin)(),
(0, plugins_1.uniAppUTSPlugin)(),
(0, uvue_1.uniAppUVuePlugin)(),
(0, mainUTS_1.uniAppMainPlugin)(),
(0, manifestJson_1.uniAppManifestPlugin)(),
(0, pagesJson_1.uniAppPagesPlugin)(),
];
};
......@@ -19,5 +19,18 @@
},
"license": "Apache-2.0",
"dependencies": {
"@dcloudio/uni-cli-shared": "3.0.0-alpha-3071220230324001",
"@rollup/pluginutils": "^4.2.0",
"@vue/compiler-core": "3.2.47",
"@vue/compiler-sfc": "3.2.47",
"@vue/shared": "3.2.47",
"debug": "^4.3.3",
"es-module-lexer": "^1.2.1",
"fs-extra": "^10.0.0",
"source-map": "^0.6.1"
},
"devDependencies": {
"@types/debug": "^4.1.7",
"@types/fs-extra": "^9.0.13"
}
}
}
\ No newline at end of file
export default []
import { uniAppUTSPlugin } from './plugins'
import { uniAppMainPlugin } from './plugins/mainUTS'
import { uniAppManifestPlugin } from './plugins/manifestJson'
import { uniAppPagesPlugin } from './plugins/pagesJson'
import { uniPrePlugin } from './plugins/pre'
import { uniAppUVuePlugin } from './plugins/uvue'
export default () => {
return [
uniPrePlugin(),
uniAppUTSPlugin(),
uniAppUVuePlugin(),
uniAppMainPlugin(),
uniAppManifestPlugin(),
uniAppPagesPlugin(),
]
}
import path from 'path'
import fs from 'fs-extra'
import {
emptyDir,
normalizePath,
parseManifestJsonOnce,
parseVueRequest,
resolveMainPathOnce,
resolveUTSCompiler,
} from '@dcloudio/uni-cli-shared'
import type { Plugin } from 'vite'
import { parseImports, uvueOutDir } from './utils'
const REMOVED_PLUGINS = [
'vite:build-metadata',
'vite:modulepreload-polyfill',
'vite:css',
'vite:esbuild',
'vite:json',
'vite:wasm-helper',
'vite:worker',
'vite:asset',
'vite:wasm-fallback',
'vite:define',
'vite:css-post',
'vite:build-html',
'vite:html-inline-proxy',
'vite:worker-import-meta-url',
'vite:asset-import-meta-url',
'vite:force-systemjs-wrap-complete',
'vite:watch-package-data',
'commonjs',
'vite:data-uri',
'vite:dynamic-import-vars',
'vite:import-glob',
'vite:build-import-analysis',
'vite:esbuild-transpile',
'vite:terser',
'vite:reporter',
]
export function uniAppUTSPlugin(): Plugin {
const inputDir = process.env.UNI_INPUT_DIR
const outputDir = process.env.UNI_OUTPUT_DIR
const mainUTS = resolveMainPathOnce(inputDir)
const tempOutputDir = uvueOutDir()
const manifestJson = parseManifestJsonOnce(inputDir)
// 开始编译时,清空输出目录
function emptyOutDir() {
if (fs.existsSync(outputDir)) {
emptyDir(outputDir)
}
}
emptyOutDir()
return {
name: 'uni:app-uts',
apply: 'build',
config() {
return {
base: '/', // 强制 base
build: {
outDir: tempOutputDir,
lib: {
// 必须使用 lib 模式
fileName: 'output',
entry: resolveMainPathOnce(inputDir),
formats: ['cjs'],
},
rollupOptions: {
external: ['vue', 'vuex', 'pinia'],
},
},
}
},
configResolved(config) {
const len = config.plugins.length
for (let i = len - 1; i >= 0; i--) {
const plugin = config.plugins[i]
if (REMOVED_PLUGINS.includes(plugin.name)) {
;(config.plugins as any).splice(i, 1)
}
}
},
async transform(code, id) {
const { filename } = parseVueRequest(id)
if (!filename.endsWith('.uts')) {
return
}
const isMainUTS = normalizePath(id) === mainUTS
const fileName = path.relative(inputDir, id)
this.emitFile({
type: 'asset',
fileName: normalizeFilename(fileName, isMainUTS),
source: normalizeCode(code, isMainUTS),
})
code = await parseImports(code)
return code
},
async writeBundle() {
await resolveUTSCompiler().compileApp(
path.join(tempOutputDir, 'index.uts'),
{
inputDir: tempOutputDir,
outputDir: outputDir,
package: 'uni.' + (manifestJson.appid || '').replace(/_/g, ''),
}
)
},
}
}
function normalizeFilename(filename: string, isMain = false) {
if (isMain) {
return 'index.uts'
}
return filename
}
function normalizeCode(code: string, isMain = false) {
if (!isMain) {
return code
}
return `
export function main() {
createPage(__uniRoutes[0])
}
`
}
import { normalizePath, resolveMainPathOnce } from '@dcloudio/uni-cli-shared'
import type { Plugin } from 'vite'
import { parseImports } from './utils'
export const MANIFEST_JSON_UTS = 'manifest-json-uts'
export const PAGES_JSON_UTS = 'pages-json-uts'
export function uniAppMainPlugin(): Plugin {
const mainUTS = resolveMainPathOnce(process.env.UNI_INPUT_DIR)
return {
name: 'uni:app-main',
apply: 'build',
async transform(code, id) {
if (normalizePath(id) === mainUTS) {
code = await parseImports(code)
return `
import './${MANIFEST_JSON_UTS}'
import './${PAGES_JSON_UTS}'
export default 'main.uts'
`
}
},
}
}
import path from 'path'
import fs from 'fs-extra'
import type { OutputAsset } from 'rollup'
import type { Plugin } from 'vite'
import { parseJson } from '@dcloudio/uni-cli-shared'
import { MANIFEST_JSON_UTS } from './mainUTS'
import { ENTRY_FILENAME } from './utils'
function isManifest(id: string) {
return id.endsWith(MANIFEST_JSON_UTS)
}
export function uniAppManifestPlugin(): Plugin {
const manifestJsonPath = path.resolve(
process.env.UNI_INPUT_DIR,
'manifest.json'
)
const manifestJsonUTSPath = path.resolve(
process.env.UNI_INPUT_DIR,
MANIFEST_JSON_UTS
)
let manifestJson: Record<string, any> = {}
return {
name: 'uni:app-manifest',
apply: 'build',
resolveId(id) {
if (isManifest(id)) {
return manifestJsonUTSPath
}
},
load(id) {
if (isManifest(id)) {
return fs.readFileSync(manifestJsonPath, 'utf8')
}
},
transform(code, id) {
if (isManifest(id)) {
manifestJson = parseJson(code)
return `export default 'manifest.json'`
}
},
generateBundle(_, bundle) {
if (bundle[ENTRY_FILENAME]) {
const asset = bundle[ENTRY_FILENAME] as OutputAsset
asset.source =
asset.source +
`
import "io.dcloud.uniapp.appframe.IAppConfig"
export class UniAppConfig extends IAppConfig {
override name: string = "${manifestJson.name || ''}"
override appid: string = "${manifestJson.appid || ''}"
override versionName: string = "${manifestJson.versionName || ''}"
override versionCode: string = "${manifestJson.versionCode || ''}"
// 以下要移除
override pageMode: string = "${manifestJson.pageMode || ''}"
override launchPath: string = "${manifestJson.launchPath || ''}"
}
`
}
},
}
}
import path from 'path'
import fs from 'fs-extra'
import { normalizePagesJson } from '@dcloudio/uni-cli-shared'
import type { OutputAsset } from 'rollup'
import type { Plugin } from 'vite'
import { PAGES_JSON_UTS } from './mainUTS'
import { ENTRY_FILENAME, genClassName } from './utils'
function isPages(id: string) {
return id.endsWith(PAGES_JSON_UTS)
}
export function uniAppPagesPlugin(): Plugin {
const pagesJsonPath = path.resolve(process.env.UNI_INPUT_DIR, 'pages.json')
const pagesJsonUTSPath = path.resolve(
process.env.UNI_INPUT_DIR,
PAGES_JSON_UTS
)
let imports: string[] = []
let routes: string[] = []
return {
name: 'uni:app-pages',
apply: 'build',
resolveId(id) {
if (isPages(id)) {
return pagesJsonUTSPath
}
},
load(id) {
if (isPages(id)) {
return fs.readFileSync(pagesJsonPath, 'utf8')
}
},
transform(code, id) {
if (isPages(id)) {
const pagesJson = normalizePagesJson(code, process.env.UNI_PLATFORM)
imports = []
routes = []
pagesJson.pages.forEach((page) => {
const className = genClassName(page.path)
imports.push(page.path)
routes.push(
`{ path: "${page.path}", component: ${className}Class, meta: { isQuit: true, navigationBar: { titleText: "uni-app" } as PageNavigationBar } as PageMeta } as PageRoute`
)
})
return `${imports.map((p) => `import './${p}.uvue'`).join('\n')}
export default 'pages.json'`
}
},
generateBundle(_, bundle) {
if (bundle[ENTRY_FILENAME]) {
const asset = bundle[ENTRY_FILENAME] as OutputAsset
asset.source =
asset.source +
`
${imports
.map((p) => {
const className = genClassName(p)
return `import ${className}Class from './${p}.uvue'`
})
.join('\n')}
const __uniRoutes = [${routes.join(',\n')}]
`
}
},
}
}
import path from 'path'
import debug from 'debug'
import { Plugin } from 'vite'
import { FilterPattern, createFilter } from '@rollup/pluginutils'
import {
parseVueRequest,
preUVueJs,
preUVueHtml,
} from '@dcloudio/uni-cli-shared'
export interface UniPrePluginOptions {
include?: FilterPattern
exclude?: FilterPattern
}
const debugPreJs = debug('uni:pre-js')
const debugPreHtml = debug('uni:pre-html')
const debugPreJsTry = debug('uni:pre-js-try')
const PRE_HTML_EXTNAME = ['.vue', '.uvue']
const PRE_JS_EXTNAME = ['.json', '.css', '.uts'].concat(PRE_HTML_EXTNAME)
export function uniPrePlugin(options: UniPrePluginOptions = {}): Plugin {
const filter = createFilter(options.include, options.exclude)
const preJsFile = preUVueJs
const preHtmlFile = preUVueHtml
return {
name: 'uni:pre',
transform(code, id) {
if (!filter(id)) {
return
}
const { filename } = parseVueRequest(id)
const extname = path.extname(filename)
const isHtml = PRE_HTML_EXTNAME.includes(extname)
const isJs = PRE_JS_EXTNAME.includes(extname)
const isPre = isHtml || isJs
if (isPre) {
debugPreJsTry(id)
}
const hasEndif = isPre && code.includes('#endif')
if (!hasEndif) {
return
}
if (isHtml) {
code = preHtmlFile(code)
debugPreHtml(id)
}
if (isJs) {
code = preJsFile(code)
debugPreJs(id)
}
return {
code,
}
},
}
}
import path from 'path'
import { init, parse } from 'es-module-lexer'
import { normalizePath, removeExt } from '@dcloudio/uni-cli-shared'
import { camelize, capitalize } from '@vue/shared'
export const ENTRY_FILENAME = 'index.uts'
export async function parseImports(code: string) {
await init
const [imports] = parse(code)
return imports
.map(({ s, e }) => {
return `import "${code.slice(s, e)}"`
})
.join('\n')
}
export function uvueOutDir() {
return path.join(process.env.UNI_OUTPUT_DIR, '../.uvue')
}
export function genClassName(fileName: string) {
return capitalize(
camelize(removeExt(normalizePath(fileName).replace(/\//g, '-')))
)
}
import { SFCScriptBlock } from '@vue/compiler-sfc'
export function genScript(
script: SFCScriptBlock | null,
{ filename }: { filename: string }
) {
const parentClass = filename === 'App' ? 'BaseApp' : 'BasePage'
if (!script) {
return `
class ${filename} extends ${parentClass} {
constructor() {}
render(ctx: ${filename}): VNode | null {
return ${filename}Render(ctx)
}
}
export default UTSAndroid.getKotlinClass(${filename})
`
}
return (
'\n'.repeat(script.loc.start.line - 1) +
`
class ${filename} extends ${parentClass} {
constructor() {}
render(ctx: ${filename}): VNode | null {
return ${filename}Render(ctx)
}
}
export default UTSAndroid.getKotlinClass(${filename})
`
)
}
import { SFCStyleBlock } from '@vue/compiler-sfc'
export function genStyle(
styles: SFCStyleBlock[],
{ filename }: { filename: string }
) {
return `export const ${filename}Styles = []`
}
import { SourceMapGenerator } from 'source-map'
import {
ArrayExpression,
CREATE_COMMENT,
CacheExpression,
CallExpression,
CommentNode,
CompoundExpressionNode,
ConditionalExpression,
ExpressionNode,
FunctionExpression,
InterpolationNode,
JSChildNode,
NodeTypes,
OPEN_BLOCK,
ObjectExpression,
Position,
RootNode,
SET_BLOCK_TRACKING,
SSRCodegenNode,
SimpleExpressionNode,
TO_DISPLAY_STRING,
TemplateChildNode,
TextNode,
VNodeCall,
WITH_CTX,
WITH_DIRECTIVES,
advancePositionWithMutation,
getVNodeBlockHelper,
getVNodeHelper,
helperNameMap,
isSimpleIdentifier,
locStub,
} from '@vue/compiler-core'
import { CodegenOptions, CodegenResult } from './options'
import { isArray, isString, isSymbol } from '@vue/shared'
import { genRenderFunctionDecl } from '../utils'
type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode
export interface CodegenContext extends Required<CodegenOptions> {
source: string
code: string
line: number
column: number
offset: number
indentLevel: number
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
}
function createCodegenContext(
ast: RootNode,
{
targetLanguage,
mode = 'default',
prefixIdentifiers = false,
sourceMap = false,
filename = '',
}: CodegenOptions
): CodegenContext {
const context: CodegenContext = {
targetLanguage,
mode,
prefixIdentifiers,
sourceMap,
filename,
source: ast.loc.source,
code: ``,
column: 1,
line: 1,
offset: 0,
indentLevel: 0,
map: undefined,
helper(key) {
return `${helperNameMap[key]}`
},
push(code, node) {
context.code += code
if (context.map) {
if (node) {
let name
if (node.type === NodeTypes.SIMPLE_EXPRESSION && !node.isStatic) {
const content = node.content.replace(/^_ctx\./, '')
if (content !== node.content && isSimpleIdentifier(content)) {
name = content
}
}
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
}
},
indent() {
newline(++context.indentLevel)
},
deindent(withoutNewLine = false) {
if (withoutNewLine) {
--context.indentLevel
} else {
newline(--context.indentLevel)
}
},
newline() {
newline(context.indentLevel)
},
}
function newline(n: number) {
context.push('\n' + ` `.repeat(n))
}
function addMapping(loc: Position, name?: string) {
context.map!.addMapping({
name,
source: context.filename,
original: {
line: loc.line,
column: loc.column - 1, // source-map column is 0 based
},
generated: {
line: context.line,
column: context.column - 1,
},
})
}
if (sourceMap) {
// lazy require source-map implementation, only in non-browser builds
context.map = new SourceMapGenerator()
context.map!.setSourceContent(filename, context.source)
}
return context
}
export function generate(
ast: RootNode,
options: CodegenOptions
): CodegenResult {
const context = createCodegenContext(ast, options)
const { mode, deindent, indent, push } = context
if (mode === 'function') {
push(genRenderFunctionDecl(options) + ` {`)
indent()
push(`return `)
}
if (ast.codegenNode) {
genNode(ast.codegenNode as CodegenNode, context)
} else {
push(`null`)
}
if (mode === 'function') {
deindent()
push(`}`)
}
return {
code: context.code,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined,
}
}
function isText(n: string | CodegenNode) {
return (
isString(n) ||
n.type === NodeTypes.SIMPLE_EXPRESSION ||
n.type === NodeTypes.TEXT ||
n.type === NodeTypes.INTERPOLATION ||
n.type === NodeTypes.COMPOUND_EXPRESSION
)
}
function genNodeListAsArray(
nodes: (string | CodegenNode | TemplateChildNode[])[],
context: CodegenContext
) {
const multilines =
nodes.length > 3 || nodes.some((n) => isArray(n) || !isText(n))
context.push(`[`)
multilines && context.indent()
genNodeList(nodes, context, multilines)
multilines && context.deindent()
context.push(`]`)
}
function genNodeList(
nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
context: CodegenContext,
multilines: boolean = false,
comma: boolean = true
) {
const { push, newline } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
push(node)
} else if (isArray(node)) {
genNodeListAsArray(node, context)
} else {
genNode(node, context)
}
if (i < nodes.length - 1) {
if (multilines) {
comma && push(',')
newline()
} else {
comma && push(', ')
}
}
}
}
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
return
}
if (isSymbol(node)) {
return
}
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT:
genText(node, context)
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
case NodeTypes.COMMENT:
genComment(node, context)
break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION:
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
break
case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
/* istanbul ignore next */
case NodeTypes.IF_BRANCH:
// noop
break
default:
}
}
function genText(
node: TextNode | SimpleExpressionNode,
context: CodegenContext
) {
context.push(JSON.stringify(node.content), node)
}
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
}
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
const { push, helper } = context
push(`${helper(TO_DISPLAY_STRING)}(`)
genNode(node.content, context)
push(`)`)
}
function genCompoundExpression(
node: CompoundExpressionNode,
context: CodegenContext
) {
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (isString(child)) {
context.push(child)
} else {
genNode(child, context)
}
}
}
function genExpressionAsPropertyKey(
node: ExpressionNode,
context: CodegenContext
) {
const { push } = context
if (node.type === NodeTypes.COMPOUND_EXPRESSION) {
push(`[`)
genCompoundExpression(node, context)
push(`]`)
} else if (node.isStatic) {
// only quote keys if necessary
const text = JSON.stringify(node.content)
push(text, node)
} else {
push(`[${node.content}]`, node)
}
}
function genComment(node: CommentNode, context: CodegenContext) {
const { push, helper } = context
push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
}
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
// isBlock,
disableTracking,
isComponent,
} = node
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
const isBlock = false
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `)
}
const callHelper: symbol = isBlock
? getVNodeBlockHelper(false, isComponent)
: getVNodeHelper(false, isComponent)
push(helper(callHelper) + `(`, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
)
push(`)`)
if (isBlock) {
push(`)`)
}
if (directives) {
push(`, `)
genNode(directives, context)
push(`)`)
}
}
function genNullableArgs(args: any[]): CallExpression['arguments'] {
let i = args.length
while (i--) {
if (args[i] != null) break
}
return args.slice(0, i + 1).map((arg) => arg || `null`)
}
// JavaScript
function genCallExpression(node: CallExpression, context: CodegenContext) {
const { push, helper } = context
const callee = isString(node.callee) ? node.callee : helper(node.callee)
push(callee + `(`, node)
genNodeList(node.arguments, context)
push(`)`)
}
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
const { push, indent, deindent, newline } = context
const { properties } = node
if (!properties.length) {
push(`new Map<string,any>()`, node)
return
}
const multilines =
properties.length > 1 ||
properties.some((p) => p.value.type !== NodeTypes.SIMPLE_EXPRESSION)
push(`new Map<string,any>([`)
multilines && indent()
for (let i = 0; i < properties.length; i++) {
const { key, value } = properties[i]
push(`[`)
// key
genExpressionAsPropertyKey(key, context)
push(`, `)
// value
genNode(value, context)
push(`]`)
if (i < properties.length - 1) {
// will only reach this if it's multilines
push(`,`)
newline()
}
}
multilines && deindent()
push(`])`)
}
function genArrayExpression(node: ArrayExpression, context: CodegenContext) {
genNodeListAsArray(node.elements as CodegenNode[], context)
}
function genFunctionExpression(
node: FunctionExpression,
context: CodegenContext
) {
const { push, indent, deindent } = context
const { params, returns, body, newline, isSlot } = node
if (isSlot) {
// wrap slot functions with owner context
push(`_${helperNameMap[WITH_CTX]}(`)
}
push(`(`, node)
if (isArray(params)) {
genNodeList(params, context)
} else if (params) {
genNode(params, context)
}
push(`) => `)
if (newline || body) {
push(`{`)
indent()
}
if (returns) {
if (newline) {
push(`return `)
}
if (isArray(returns)) {
genNodeListAsArray(returns, context)
} else {
genNode(returns, context)
}
} else if (body) {
genNode(body, context)
}
if (newline || body) {
deindent()
push(`}`)
}
if (isSlot) {
push(`)`)
}
}
function genConditionalExpression(
node: ConditionalExpression,
context: CodegenContext
) {
const { test, consequent, alternate, newline: needNewline } = node
const { push, indent, deindent, newline } = context
if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
const needsParens = !isSimpleIdentifier(test.content)
needsParens && push(`(`)
genExpression(test, context)
needsParens && push(`)`)
} else {
push(`(`)
genNode(test, context)
push(`)`)
}
needNewline && indent()
context.indentLevel++
needNewline || push(` `)
push(`? `)
genNode(consequent, context)
context.indentLevel--
needNewline && newline()
needNewline || push(` `)
push(`: `)
const isNested = alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
if (!isNested) {
context.indentLevel++
}
genNode(alternate, context)
if (!isNested) {
context.indentLevel--
}
needNewline && deindent(true /* without newline */)
}
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
const { push, helper, indent, deindent, newline } = context
push(`_cache[${node.index}] || (`)
if (node.isVNode) {
indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
newline()
}
push(`_cache[${node.index}] = `)
genNode(node.value, context)
if (node.isVNode) {
push(`,`)
newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
newline()
push(`_cache[${node.index}]`)
deindent()
}
push(`)`)
}
import { SourceLocation } from '@vue/compiler-core'
export interface CompilerError extends SyntaxError {
code: number | string
loc?: SourceLocation
}
export interface CoreCompilerError extends CompilerError {
code: ErrorCodes
}
export function defaultOnError(error: CompilerError) {
throw error
}
export function defaultOnWarn(msg: CompilerError) {
console.warn(`[Vue warn] ${msg.message}`)
}
type InferCompilerError<T> = T extends ErrorCodes
? CoreCompilerError
: CompilerError
export function createCompilerError<T extends number>(
code: T,
loc?: SourceLocation,
messages?: { [code: number]: string },
additionalMessage?: string
): InferCompilerError<T> {
const msg = (messages || errorMessages)[code] + (additionalMessage || ``)
const error = new SyntaxError(String(msg)) as InferCompilerError<T>
error.code = code
error.loc = loc
return error
}
export const enum ErrorCodes {
// parse errors
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
CDATA_IN_HTML_CONTENT,
DUPLICATE_ATTRIBUTE,
END_TAG_WITH_ATTRIBUTES,
END_TAG_WITH_TRAILING_SOLIDUS,
EOF_BEFORE_TAG_NAME,
EOF_IN_CDATA,
EOF_IN_COMMENT,
EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT,
EOF_IN_TAG,
INCORRECTLY_CLOSED_COMMENT,
INCORRECTLY_OPENED_COMMENT,
INVALID_FIRST_CHARACTER_OF_TAG_NAME,
MISSING_ATTRIBUTE_VALUE,
MISSING_END_TAG_NAME,
MISSING_WHITESPACE_BETWEEN_ATTRIBUTES,
NESTED_COMMENT,
UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
UNEXPECTED_NULL_CHARACTER,
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
UNEXPECTED_SOLIDUS_IN_TAG,
// Vue-specific parse errors
X_INVALID_END_TAG,
X_MISSING_END_TAG,
X_MISSING_INTERPOLATION_END,
X_MISSING_DIRECTIVE_NAME,
X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END,
// transform errors
X_V_IF_NO_EXPRESSION,
X_V_IF_SAME_KEY,
X_V_ELSE_NO_ADJACENT_IF,
X_V_FOR_NO_EXPRESSION,
X_V_FOR_MALFORMED_EXPRESSION,
X_V_FOR_TEMPLATE_KEY_PLACEMENT,
X_V_BIND_NO_EXPRESSION,
X_V_ON_NO_EXPRESSION,
X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
X_V_SLOT_MIXED_SLOT_USAGE,
X_V_SLOT_DUPLICATE_SLOT_NAMES,
X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
X_V_SLOT_MISPLACED,
X_V_MODEL_NO_EXPRESSION,
X_V_MODEL_MALFORMED_EXPRESSION,
X_V_MODEL_ON_SCOPE_VARIABLE,
X_V_MODEL_ON_PROPS,
X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
X_MODULE_MODE_NOT_SUPPORTED,
X_CACHE_HANDLER_NOT_SUPPORTED,
X_SCOPE_ID_NOT_SUPPORTED,
// Special value for higher-order compilers to pick up the last code
// to avoid collision of error codes. This should always be kept as the last
// item.
__EXTEND_POINT__,
}
export const errorMessages: Record<ErrorCodes, string> = {
// parse errors
[ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT]: 'Illegal comment.',
[ErrorCodes.CDATA_IN_HTML_CONTENT]:
'CDATA section is allowed only in XML context.',
[ErrorCodes.DUPLICATE_ATTRIBUTE]: 'Duplicate attribute.',
[ErrorCodes.END_TAG_WITH_ATTRIBUTES]: 'End tag cannot have attributes.',
[ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS]: "Illegal '/' in tags.",
[ErrorCodes.EOF_BEFORE_TAG_NAME]: 'Unexpected EOF in tag.',
[ErrorCodes.EOF_IN_CDATA]: 'Unexpected EOF in CDATA section.',
[ErrorCodes.EOF_IN_COMMENT]: 'Unexpected EOF in comment.',
[ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT]:
'Unexpected EOF in script.',
[ErrorCodes.EOF_IN_TAG]: 'Unexpected EOF in tag.',
[ErrorCodes.INCORRECTLY_CLOSED_COMMENT]: 'Incorrectly closed comment.',
[ErrorCodes.INCORRECTLY_OPENED_COMMENT]: 'Incorrectly opened comment.',
[ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME]:
"Illegal tag name. Use '&lt;' to print '<'.",
[ErrorCodes.MISSING_ATTRIBUTE_VALUE]: 'Attribute value was expected.',
[ErrorCodes.MISSING_END_TAG_NAME]: 'End tag name was expected.',
[ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES]:
'Whitespace was expected.',
[ErrorCodes.NESTED_COMMENT]: "Unexpected '<!--' in comment.",
[ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME]:
'Attribute name cannot contain U+0022 ("), U+0027 (\'), and U+003C (<).',
[ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE]:
'Unquoted attribute value cannot contain U+0022 ("), U+0027 (\'), U+003C (<), U+003D (=), and U+0060 (`).',
[ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME]:
"Attribute name cannot start with '='.",
[ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]:
"'<?' is allowed only in XML context.",
[ErrorCodes.UNEXPECTED_NULL_CHARACTER]: `Unexpected null character.`,
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
// Vue-specific parse errors
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
[ErrorCodes.X_MISSING_END_TAG]: 'Element is missing end tag.',
[ErrorCodes.X_MISSING_INTERPOLATION_END]:
'Interpolation end sign was not found.',
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
'End bracket for dynamic directive argument was not found. ' +
'Note that dynamic directive argument cannot contain spaces.',
[ErrorCodes.X_MISSING_DIRECTIVE_NAME]: 'Legal directive name was expected.',
// transform errors
[ErrorCodes.X_V_IF_NO_EXPRESSION]: `v-if/v-else-if is missing expression.`,
[ErrorCodes.X_V_IF_SAME_KEY]: `v-if/else branches must use unique keys.`,
[ErrorCodes.X_V_ELSE_NO_ADJACENT_IF]: `v-else/v-else-if has no adjacent v-if or v-else-if.`,
[ErrorCodes.X_V_FOR_NO_EXPRESSION]: `v-for is missing expression.`,
[ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression.`,
[ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT]: `<template v-for> key should be placed on the <template> tag.`,
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
[ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
[ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]:
`Mixed v-slot usage on both the component and nested <template>. ` +
`When there are multiple named slots, all slots should use <template> ` +
`syntax to avoid scope ambiguity.`,
[ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
[ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
`Extraneous children found when component already has explicitly named ` +
`default slot. These children will be ignored.`,
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
[ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
[ErrorCodes.X_V_MODEL_ON_PROPS]: `v-model cannot be used on a prop, because local prop bindings are not writable.\nUse a v-bind binding combined with a v-on listener that emits update:x event instead.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
[ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED]: `"cacheHandlers" option is only supported when the "prefixIdentifiers" option is enabled.`,
[ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED]: `"scopeId" option is only supported in module mode.`,
// just to fulfill types
[ErrorCodes.__EXTEND_POINT__]: ``,
}
import { extend } from '@vue/shared'
import {
baseParse,
trackSlotScopes,
trackVForSlotScopes,
transformBind,
transformElement,
transformExpression,
transformModel,
transformOn,
} from '@vue/compiler-core'
import { CodegenResult, CompilerOptions } from './options'
import { generate } from './codegen'
import { DirectiveTransform, NodeTransform, transform } from './transform'
export type TransformPreset = [
NodeTransform[],
Record<string, DirectiveTransform>
]
export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
[
// order is important
trackVForSlotScopes,
transformExpression,
transformElement,
trackSlotScopes,
] as any,
{
on: transformOn,
bind: transformBind,
model: transformModel,
} as any,
]
}
export function compile(
template: string,
options: CompilerOptions
): CodegenResult {
const ast = baseParse(template, {
isCustomElement(tag) {
return true
},
})
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
options.prefixIdentifiers
)
transform(
ast,
extend({}, options, {
prefixIdentifiers: options.prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
),
})
)
return generate(ast, options)
}
import { CompilerError } from '@vue/compiler-core'
import { RawSourceMap } from 'source-map'
import { DirectiveTransform, NodeTransform } from './transform'
interface SharedTransformCodegenOptions {
targetLanguage: 'kotlin' | 'swift'
/**
* Transform expressions like {{ foo }} to `_ctx.foo`.
* @default false
*/
prefixIdentifiers?: boolean
/**
* Filename for source map generation.
* Also used for self-recursive reference in templates
* @default ''
*/
filename?: string
}
export interface CodegenOptions extends SharedTransformCodegenOptions {
/**
* function
* @default 'default'
*/
mode?: 'default' | 'function'
/**
* Generate source map?
* @default false
*/
sourceMap?: boolean
}
export interface ErrorHandlingOptions {
onWarn?: (warning: CompilerError) => void
onError?: (error: CompilerError) => void
}
export interface TransformOptions
extends SharedTransformCodegenOptions,
ErrorHandlingOptions {
/**
* An array of node transforms to be applied to every AST node.
*/
nodeTransforms?: NodeTransform[]
/**
* An object of { name: transform } to be applied to every directive attribute
* node found on element nodes.
*/
directiveTransforms?: Record<string, DirectiveTransform | undefined>
/**
* If the pairing runtime provides additional built-in elements, use this to
* mark them as built-in so the compiler will generate component vnodes
* for them.
*/
isBuiltInComponent?: (tag: string) => symbol | void
/**
* Used by some transforms that expects only native elements
*/
isCustomElement?: (tag: string) => boolean | void
}
export type CompilerOptions = TransformOptions & CodegenOptions
export interface CodegenResult {
code: string
map?: RawSourceMap
}
import {
CacheExpression,
ComponentNode,
ConstantTypes,
DirectiveNode,
ElementNode,
ExpressionNode,
JSChildNode,
NodeTypes,
ParentNode,
PlainElementNode,
Property,
RootNode,
TemplateChildNode,
TemplateLiteral,
TemplateNode,
createCacheExpression,
helperNameMap,
isSlotOutlet,
makeBlock,
} from '@vue/compiler-core'
import { NOOP, camelize, capitalize, isArray, isString } from '@vue/shared'
import { defaultOnError, defaultOnWarn } from './errors'
import { TransformOptions } from './options'
// There are two types of transforms:
//
// - NodeTransform:
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
// replace or remove the node being processed.
export type NodeTransform = (
node: RootNode | TemplateChildNode,
context: TransformContext
) => void | (() => void) | (() => void)[]
// - DirectiveTransform:
// Transforms that handles a single directive attribute on an element.
// It translates the raw directive into actual props for the VNode.
export type DirectiveTransform = (
dir: DirectiveNode,
node: ElementNode,
context: TransformContext,
// a platform specific compiler can import the base transform and augment
// it by passing in this optional argument.
augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult
) => DirectiveTransformResult
export interface DirectiveTransformResult {
props: Property[]
needRuntime?: boolean | symbol
ssrTagParts?: TemplateLiteral['elements']
}
// A structural directive transform is technically also a NodeTransform;
// Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = (
node: ElementNode,
dir: DirectiveNode,
context: TransformContext
) => void | (() => void)
export interface ImportItem {
exp: string | ExpressionNode
path: string
}
export interface TransformContext
extends Required<Omit<TransformOptions, 'filename'>> {
selfName: string | null
root: RootNode
helpers: Map<symbol, number>
components: Set<string>
directives: Set<string>
imports: ImportItem[]
temps: number
cached: number
identifiers: { [name: string]: number | undefined }
scopes: {
vFor: number
vSlot: number
vPre: number
vOnce: number
}
parent: ParentNode | null
childIndex: number
currentNode: RootNode | TemplateChildNode | null
inVOnce: boolean
helper<T extends symbol>(name: T): T
removeHelper<T extends symbol>(name: T): void
helperString(name: symbol): string
replaceNode(node: TemplateChildNode): void
removeNode(node?: TemplateChildNode): void
onNodeRemoved(): void
addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
constantCache: Map<TemplateChildNode, ConstantTypes>
}
export function createTransformContext(
root: RootNode,
{
targetLanguage,
filename = '',
prefixIdentifiers = false,
nodeTransforms = [],
directiveTransforms = {},
isBuiltInComponent = NOOP,
isCustomElement = NOOP,
onError = defaultOnError,
onWarn = defaultOnWarn,
}: TransformOptions
): TransformContext {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
const context: TransformContext = {
// options
targetLanguage,
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
prefixIdentifiers,
nodeTransforms,
directiveTransforms,
isBuiltInComponent,
isCustomElement,
onError,
onWarn,
// state
root,
helpers: new Map(),
components: new Set(),
directives: new Set(),
imports: [],
constantCache: new Map(),
temps: 0,
cached: 0,
identifiers: Object.create(null),
scopes: {
vFor: 0,
vSlot: 0,
vPre: 0,
vOnce: 0,
},
parent: null,
currentNode: root,
childIndex: 0,
inVOnce: false,
// methods
helper(name) {
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
},
removeHelper(name) {
const count = context.helpers.get(name)
if (count) {
const currentCount = count - 1
if (!currentCount) {
context.helpers.delete(name)
} else {
context.helpers.set(name, currentCount)
}
}
},
helperString(name) {
return `_${helperNameMap[context.helper(name)]}`
},
replaceNode(node) {
if (!context.currentNode) {
throw new Error(`Node being replaced is already removed.`)
}
if (!context.parent) {
throw new Error(`Cannot replace root node.`)
}
context.parent!.children[context.childIndex] = context.currentNode = node
},
removeNode(node) {
if (!context.parent) {
throw new Error(`Cannot remove root node.`)
}
const list = context.parent!.children
const removalIndex = node
? list.indexOf(node)
: context.currentNode
? context.childIndex
: -1
/* istanbul ignore if */
if (removalIndex < 0) {
throw new Error(`node being removed is not a child of current parent`)
}
if (!node || node === context.currentNode) {
// current node removed
context.currentNode = null
context.onNodeRemoved()
} else {
// sibling node removed
if (context.childIndex > removalIndex) {
context.childIndex--
context.onNodeRemoved()
}
}
context.parent!.children.splice(removalIndex, 1)
},
onNodeRemoved: () => {},
addIdentifiers(exp) {
// identifier tracking only happens in non-browser builds.
if (isString(exp)) {
addId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(addId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
addId(exp.content)
}
},
removeIdentifiers(exp) {
if (isString(exp)) {
removeId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(removeId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
removeId(exp.content)
}
},
cache(exp, isVNode = false) {
return createCacheExpression(context.cached++, exp, isVNode)
},
}
function addId(id: string) {
const { identifiers } = context
if (identifiers[id] === undefined) {
identifiers[id] = 0
}
identifiers[id]!++
}
function removeId(id: string) {
context.identifiers[id]!--
}
return context
}
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
createRootCodegen(root, context)
}
export function isSingleElementRoot(
root: RootNode,
child: TemplateChildNode
): child is PlainElementNode | ComponentNode | TemplateNode {
const { children } = root
return (
children.length === 1 &&
child.type === NodeTypes.ELEMENT &&
!isSlotOutlet(child)
)
}
function createRootCodegen(root: RootNode, context: TransformContext) {
const { children } = root
if (children.length === 1) {
const child = children[0]
// if the single child is an element, turn it into a block.
if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
makeBlock(codegenNode, context as any)
}
root.codegenNode = codegenNode
} else {
// - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.
// root codegen falls through via genNode()
root.codegenNode = child
}
} else if (children.length > 1) {
// root has multiple nodes - return a fragment block.
// let patchFlag = PatchFlags.STABLE_FRAGMENT
// let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// // check if the fragment actually contains a single valid child with
// // the rest being comments
// if (
// children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
// ) {
// patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
// patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
// }
// root.codegenNode = createVNodeCall(
// context,
// helper(FRAGMENT),
// undefined,
// root.children,
// patchFlag + ` /* ${patchFlagText} */`,
// undefined,
// undefined,
// true,
// undefined,
// false /* isComponent */
// )
} else {
// no children = noop. codegen will return null.
}
}
export function traverseChildren(
parent: ParentNode,
context: TransformContext
) {
let i = 0
const nodeRemoved = () => {
i--
}
for (; i < parent.children.length; i++) {
const child = parent.children[i]
if (isString(child)) continue
context.parent = parent
context.childIndex = i
context.onNodeRemoved = nodeRemoved
traverseNode(child, context)
}
}
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
context.currentNode = node
// apply transform plugins
const { nodeTransforms } = context
const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
if (!context.currentNode) {
// node was removed
return
} else {
// node may have been replaced
node = context.currentNode
}
}
switch (node.type) {
case NodeTypes.COMMENT:
break
case NodeTypes.INTERPOLATION:
break
// for container types, further traverse downwards
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context)
}
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
}
// exit transforms
context.currentNode = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
import { SFCTemplateBlock } from '@vue/compiler-sfc'
import { compile } from './compiler'
import { CompilerOptions } from './compiler/options'
import { genRenderFunctionDecl } from './utils'
export function genTemplate(
template: SFCTemplateBlock | null,
options: CompilerOptions
) {
if (!template) {
return genRenderFunctionDecl(options) + ` { return null }`
}
return compile(template.content, options).code
}
import { CompilerOptions } from './compiler/options'
export function genRenderFunctionDecl({
targetLanguage,
filename,
}: CompilerOptions): string {
return `${
targetLanguage === 'kotlin' ? '@Suppress("UNUSED_PARAMETER") ' : ''
}function ${filename}Render(ctx: ${filename}): VNode | null`
}
import fs from 'fs'
import path from 'path'
import { createHash } from 'crypto'
import type * as _compiler from '@vue/compiler-sfc'
import type { CompilerError, SFCDescriptor } from '@vue/compiler-sfc'
import { normalizePath } from '@dcloudio/uni-cli-shared'
export interface ResolvedOptions {
compiler: typeof _compiler
root: string
sourceMap: boolean
}
// compiler-sfc should be exported so it can be re-used
export interface SFCParseResult {
descriptor: SFCDescriptor
errors: Array<CompilerError | SyntaxError>
}
const cache = new Map<string, SFCDescriptor>()
const prevCache = new Map<string, SFCDescriptor | undefined>()
declare module '@vue/compiler-sfc' {
interface SFCDescriptor {
id: string
}
}
export function createDescriptor(
filename: string,
source: string,
{ root, sourceMap, compiler }: ResolvedOptions
): SFCParseResult {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap,
})
// ensure the path is normalized in a way that is consistent inside
// project (relative to root) and on different systems.
const normalizedPath = normalizePath(
path.normalize(path.relative(root, filename))
)
descriptor.id = getHash(normalizedPath)
cache.set(filename, descriptor)
return { descriptor, errors }
}
export function getPrevDescriptor(filename: string): SFCDescriptor | undefined {
return prevCache.get(filename)
}
export function setPrevDescriptor(
filename: string,
entry: SFCDescriptor
): void {
prevCache.set(filename, entry)
}
export function getDescriptor(
filename: string,
options: ResolvedOptions,
createIfNotFound = true
): SFCDescriptor | undefined {
if (cache.has(filename)) {
return cache.get(filename)!
}
if (createIfNotFound) {
const { descriptor, errors } = createDescriptor(
filename,
fs.readFileSync(filename, 'utf-8'),
options
)
if (errors.length) {
throw errors[0]
}
return descriptor
}
}
export function getSrcDescriptor(filename: string): SFCDescriptor {
return cache.get(filename)!
}
export function setSrcDescriptor(filename: string, entry: SFCDescriptor): void {
cache.set(filename, entry)
}
function getHash(text: string): string {
return createHash('sha256').update(text).digest('hex').substring(0, 8)
}
import type { CompilerError } from '@vue/compiler-sfc'
import type { RollupError } from 'rollup'
export function createRollupError(
id: string,
error: CompilerError | SyntaxError
): RollupError {
const { message, name, stack } = error
const rollupError: RollupError = {
id,
plugin: 'vue',
message,
name,
stack,
}
if ('code' in error && error.loc) {
rollupError.loc = {
file: id,
line: error.loc.start.line,
column: error.loc.start.column,
}
}
return rollupError
}
import path from 'path'
import fs from 'fs-extra'
import { normalizePath, parseVueRequest } from '@dcloudio/uni-cli-shared'
import type { Plugin } from 'vite'
import { ResolvedOptions, createDescriptor } from './descriptorCache'
import { createRollupError } from './error'
import { genClassName, parseImports } from '../utils'
import { genScript } from './code/script'
import { genTemplate } from './code/template'
import { genStyle } from './code/style'
function resolveAppVue(inputDir: string) {
const appUVue = path.resolve(inputDir, 'app.uvue')
if (fs.existsSync(appUVue)) {
return normalizePath(appUVue)
}
return normalizePath(path.resolve(inputDir, 'App.vue'))
}
export function uniAppUVuePlugin(): Plugin {
const options: ResolvedOptions = {
root: process.env.UNI_INPUT_DIR,
sourceMap: false,
// eslint-disable-next-line no-restricted-globals
compiler: require('@vue/compiler-sfc'),
}
const appVue = resolveAppVue(process.env.UNI_INPUT_DIR)
function isAppVue(id: string) {
return normalizePath(id) === appVue
}
return {
name: 'uni:app-uvue',
apply: 'build',
transform(code, id) {
const { filename } = parseVueRequest(id)
const isVue = filename.endsWith('.vue') || filename.endsWith('.uvue')
if (!isVue) {
return
}
// prev descriptor is only set and used for hmr
const { descriptor, errors } = createDescriptor(filename, code, options)
if (errors.length) {
errors.forEach((error) =>
this.error(createRollupError(filename, error))
)
return null
}
const isApp = isAppVue(id)
const fileName = path.relative(process.env.UNI_INPUT_DIR, id)
const className = genClassName(fileName)
// 生成 script 文件
this.emitFile({
type: 'asset',
fileName,
source:
genScript(descriptor.script, { filename: className }) +
'\n' +
genStyle(descriptor.styles, { filename: className }) +
'\n' +
(!isApp
? genTemplate(descriptor.template, {
targetLanguage: process.env.UNI_UVUE_TARGET_LANGUAGE as
| 'kotlin'
| 'swift',
mode: 'function',
filename: className,
})
: ''),
})
const content = descriptor.script?.content
if (content) {
return parseImports(content)
}
return {
code: 'export default {}',
}
},
}
}
......@@ -47,4 +47,5 @@ export default {
'Quick App Alliance Devtools | Huawei Quick App Devtools',
'prompt.run.devtools.quickapp-webview-huawei': 'Huawei Quick App Devtools',
'prompt.run.devtools.quickapp-webview-union': 'Quick App Alliance Devtools',
'uvue.unsupported': 'uvue does not support {platform} platform',
} as const
......@@ -47,4 +47,5 @@ export default {
'快应用联盟开发者工具 | 华为快应用开发者工具',
'prompt.run.devtools.quickapp-webview-huawei': '华为快应用开发者工具',
'prompt.run.devtools.quickapp-webview-union': '快应用联盟开发者工具',
'uvue.unsupported': 'uvue 暂不支持 {platform} 平台',
} as const
......@@ -29,6 +29,7 @@ const DEFAULT_KEYS = [
const preVueContext = Object.create(null)
const preNVueContext = Object.create(null)
const preUVueContext = Object.create(null)
export function getPreVueContext() {
return preVueContext
......@@ -38,12 +39,18 @@ export function getPreNVueContext() {
return preNVueContext
}
export function getPreUVueContext() {
return preUVueContext
}
export function initPreContext(
platform: UniApp.PLATFORM,
userPreContext?: Record<string, boolean> | string
userPreContext?: Record<string, boolean> | string,
utsPlatform?: typeof process.env.UNI_UTS_PLATFORM
) {
const vueContext = Object.create(null)
const nvueContext = Object.create(null)
const uvueContext = Object.create(null)
const defaultContext = Object.create(null)
DEFAULT_KEYS.forEach((key) => {
......@@ -54,6 +61,7 @@ export function initPreContext(
vueContext.VUE3 = true
nvueContext.VUE3 = true
uvueContext.VUE3 = true
if (platform === 'app' || platform === 'app-plus') {
defaultContext.APP = true
......@@ -63,6 +71,14 @@ export function initPreContext(
nvueContext.APP_NVUE = true
nvueContext.APP_PLUS_NVUE = true
uvueContext.APP_UVUE = true
if (utsPlatform === 'app-android') {
uvueContext.APP_ANDROID = true
} else if (utsPlatform === 'app-ios') {
uvueContext.APP_IOS = true
}
} else if (platform.startsWith('mp-')) {
defaultContext.MP = true
} else if (platform.startsWith('quickapp-webview')) {
......@@ -87,6 +103,7 @@ export function initPreContext(
}
extend(preVueContext, defaultContext, vueContext)
extend(preNVueContext, defaultContext, nvueContext)
extend(preUVueContext, defaultContext, uvueContext)
}
function normalizeKey(name: string) {
......
import { getPreNVueContext, getPreVueContext } from './context'
import {
getPreNVueContext,
getPreUVueContext,
getPreVueContext,
} from './context'
/* eslint-disable no-restricted-globals */
const { preprocess } = require('../../lib/preprocess')
......@@ -25,3 +29,14 @@ export function preNVueHtml(htmlCode: string) {
export const preNVueCss = preNVueJs
export const preNVueJson = preNVueJs
export function preUVueJs(jsCode: string) {
return preprocess(jsCode, getPreUVueContext(), { type: 'js' })
}
export function preUVueHtml(htmlCode: string) {
return preprocess(htmlCode, getPreUVueContext(), { type: 'html' })
}
export const preUVueCss = preUVueJs
export const preUVueJson = preUVueJs
......@@ -28,6 +28,10 @@ export function relativeFile(from: string, to: string) {
}
export const resolveMainPathOnce = once((inputDir: string) => {
const mainUTSPath = path.resolve(inputDir, 'main.uts')
if (fs.existsSync(mainUTSPath)) {
return normalizePath(mainUTSPath)
}
const mainTsPath = path.resolve(inputDir, 'main.ts')
if (fs.existsSync(mainTsPath)) {
return normalizePath(mainTsPath)
......
......@@ -52,6 +52,8 @@ export const sourcemap = {
generateCodeFrameWithSwiftStacktrace,
}
export { compileApp } from './uvue/index'
export * from './sourceMap'
export { compile as toKotlin } from './kotlin'
......
......@@ -25,9 +25,9 @@ import {
import { Module } from '../types/types'
import { parseUTSSyntaxError } from './stacktrace'
interface KotlinCompilerServer extends CompilerServer {
export interface KotlinCompilerServer extends CompilerServer {
getKotlincHome(): string
getDefaultJar(): string[]
getDefaultJar(arg?: any): string[]
compile(
options: { kotlinc: string[]; d8: string[] },
projectPath: string
......@@ -365,7 +365,11 @@ export async function compile(
return result
}
function resolveKotlincArgs(filename: string, kotlinc: string, jars: string[]) {
export function resolveKotlincArgs(
filename: string,
kotlinc: string,
jars: string[]
) {
return [
filename,
'-cp',
......@@ -377,7 +381,7 @@ function resolveKotlincArgs(filename: string, kotlinc: string, jars: string[]) {
]
}
function resolveD8Args(filename: string) {
export function resolveD8Args(filename: string) {
return [
filename,
'--no-desugaring',
......@@ -419,7 +423,7 @@ function resolveAndroidArchiveOutputPath(aar?: string) {
aar ? aar.replace('.aar', '') : ''
)
}
function resolveDexFile(jarFile: string) {
export function resolveDexFile(jarFile: string) {
return normalizePath(path.resolve(path.dirname(jarFile), 'classes.dex'))
}
......@@ -427,7 +431,7 @@ function resolveDexPath(filename: string) {
return path.dirname(filename)
}
function resolveJarPath(filename: string) {
export function resolveJarPath(filename: string) {
return filename.replace(path.extname(filename), '.jar')
}
......
import path from 'path'
import fs from 'fs-extra'
import {
KotlinCompilerServer,
resolveD8Args,
resolveDexFile,
resolveJarPath,
resolveKotlincArgs,
} from '../kotlin'
import { parseUTSSyntaxError } from '../stacktrace'
import { getCompilerServer, getUTSCompiler } from '../utils'
const DEFAULT_IMPORTS = [
'kotlinx.coroutines.async',
'kotlinx.coroutines.CoroutineScope',
'kotlinx.coroutines.Deferred',
'kotlinx.coroutines.Dispatchers',
'io.dcloud.uts.Map',
'io.dcloud.uts.*',
'io.dcloud.uts.framework.*',
'io.dcloud.uts.vue.*',
'io.dcloud.uts.vue.reactivity.*',
]
export interface CompileAppOptions {
inputDir: string
outputDir: string
package: string
}
export async function compileApp(entry: string, options: CompileAppOptions) {
const { bundle, UTSTarget } = getUTSCompiler()
const imports = [...DEFAULT_IMPORTS]
const { package: pkg, inputDir, outputDir } = options
const input: Parameters<typeof bundle>[1]['input'] = {
root: inputDir,
filename: entry,
paths: {
vue: 'io.dcloud.uts.vue',
},
}
const result = await bundle(UTSTarget.KOTLIN, {
input,
output: {
isPlugin: false,
outDir: outputDir,
package: pkg,
sourceMap: false,
extname: 'kt',
imports,
logFilename: true,
noColor: true,
transform: {
uniExtApiPackage: 'io.dcloud.uts.extapi',
},
},
})
if (!result) {
return
}
if (result.error) {
throw parseUTSSyntaxError(result.error, inputDir)
}
const kotlinFile = path.resolve(outputDir, result.filename!)
// 开发模式下,需要生成 dex
if (fs.existsSync(kotlinFile)) {
const compilerServer = getCompilerServer<KotlinCompilerServer>(
'uniapp-runextension'
)
if (!compilerServer) {
throw `项目使用了uts插件,正在安装 uts Android 运行扩展...`
}
const {
getDefaultJar,
getKotlincHome,
compile: compileDex,
} = compilerServer
// time = Date.now()
const jarFile = resolveJarPath(kotlinFile)
const options = {
kotlinc: resolveKotlincArgs(
kotlinFile,
getKotlincHome(),
getDefaultJar(2)
),
d8: resolveD8Args(jarFile),
sourceRoot: inputDir,
// sourceMapPath: resolveSourceMapFile(outputDir, kotlinFile),
}
const res = await compileDex(options, inputDir)
// console.log('dex compile time: ' + (Date.now() - time) + 'ms')
if (res) {
try {
fs.unlinkSync(jarFile)
// 短期内先不删除,方便排查问题
// fs.unlinkSync(kotlinFile)
} catch (e) {}
const dexFile = resolveDexFile(jarFile)
if (fs.existsSync(dexFile)) {
// result.changed = [normalizePath(path.relative(outputDir, dexFile))]
}
}
}
}
......@@ -21,7 +21,7 @@ export type UTSParseOptions = UTSParserConfig & {
export type UTSInputOptions = UTSParseOptions & {
root: string
pluginId: string
pluginId?: string
filename: string
fileContent?: string
fileAppendContent?: string
......
......@@ -17,6 +17,7 @@ import {
showRunPrompt,
} from './utils'
import { initEasycom } from '../utils/easycom'
import { runUVueDev } from './uvue'
export async function runDev(options: CliOptions & ServerOptions) {
extend(options, {
......@@ -26,6 +27,9 @@ export async function runDev(options: CliOptions & ServerOptions) {
;(options as BuildOptions).minify = true
}
initEnv('dev', options)
if (process.env.UNI_UVUE === 'true') {
return runUVueDev(options)
}
const createLogger = await import('vite').then(
({ createLogger }) => createLogger
)
......
......@@ -12,7 +12,7 @@ import { CliOptions } from '.'
import { addConfigFile, cleanOptions } from './utils'
import { RollupWatcher, RollupWatcherEvent } from 'rollup'
async function buildByVite(inlineConfig: InlineConfig) {
export async function buildByVite(inlineConfig: InlineConfig) {
return import('vite').then(({ build }) => build(inlineConfig))
}
......@@ -63,7 +63,7 @@ export async function buildSSR(options: CliOptions) {
})
}
function initBuildOptions(
export function initBuildOptions(
options: CliOptions,
build: BuildOptions
): InlineConfig {
......
......@@ -16,6 +16,7 @@ import {
import { CliOptions } from '.'
import { initNVueEnv } from './nvue'
import { initUVueEnv } from './uvue'
export const PLATFORMS = [
'app',
......@@ -181,6 +182,8 @@ export function initEnv(
initModulePaths()
initUVueEnv()
console.log(M['compiling'])
}
......
import { BuildOptions, ServerOptions, createLogger } from 'vite'
import { M, output, parseManifestJsonOnce } from '@dcloudio/uni-cli-shared'
import { RollupWatcher } from 'rollup'
import { CliOptions } from '.'
import { buildByVite, initBuildOptions } from './build'
import { addConfigFile, cleanOptions, printStartupDuration } from './utils'
import { extend } from '@vue/shared'
export function initUVueEnv() {
const manifestJson = parseManifestJsonOnce(process.env.UNI_INPUT_DIR)
const isNVueEnabled = manifestJson?.uvue?.enable === true
if (!isNVueEnabled) {
return
}
process.env.UNI_UVUE = 'true'
process.env.UNI_UVUE_TARGET_LANGUAGE = 'javascript'
if (process.env.UNI_UTS_PLATFORM === 'app-android') {
process.env.UNI_UVUE_TARGET_LANGUAGE = 'kotlin'
} else if (process.env.UNI_UTS_PLATFORM === 'app-ios') {
process.env.UNI_UVUE_TARGET_LANGUAGE = 'swift'
}
}
export async function runUVueDev(options: CliOptions & ServerOptions) {
if (options.platform !== 'app') {
output(
'error',
M['uvue.unsupported'].replace('{platform}', options.platform!)
)
return process.exit(0)
}
const watcher = (await buildUVue(options)) as RollupWatcher
let isFirstStart = true
let isFirstEnd = true
watcher.on('event', (event) => {
if (event.code === 'BUNDLE_START') {
if (isFirstStart) {
isFirstStart = false
return
}
output('log', M['dev.watching.start'])
} else if (event.code === 'BUNDLE_END') {
event.result.close()
if (isFirstEnd) {
// 首次全量同步
isFirstEnd = false
output('log', M['dev.watching.end'])
printStartupDuration(createLogger(options.logLevel), false)
return
}
return output(
'log',
M['dev.watching.end.files'].replace(
'{files}',
JSON.stringify(['classes.dex'])
)
)
return output('log', M['dev.watching.end'])
}
})
}
/**
* 目前的简易实现逻辑
* node层:
* 1. 监听项目,生成资源到临时目录 .uts/android, .uts/ios
* 2. uvue 文件,做解析,拆分生成 render.kt, css.kt, uts.uvue
* 3. static 文件,copy 到最终目录
* 4. uvue、vue、uts 文件发生变化,调用 uts 编译器
* @param options
*/
export async function buildUVue(
options: CliOptions
): Promise<RollupWatcher | void> {
return buildByVite(
addConfigFile(
extend(
{ nvueAppService: true, nvue: true },
initBuildOptions(options, cleanOptions(options) as BuildOptions)
)
)
) as Promise<RollupWatcher | void>
}
......@@ -50,7 +50,10 @@ function initCheckUpdate() {
})
}
function initLogger({ logger, nvue }: ResolvedConfig & { nvue?: boolean }) {
export function initLogger({
logger,
nvue,
}: ResolvedConfig & { nvue?: boolean }) {
const { info, warn, error } = logger
logger.info = (msg, opts) => {
msg = formatInfoMsg(msg, extend(opts || {}, { nvue }))
......
......@@ -36,6 +36,7 @@ import {
initPluginVueOptions,
} from './vue'
import { initEnv } from './cli/utils'
import { uniUVuePlugin } from './uvue/plugins'
export type ViteLegacyOptions = Parameters<typeof ViteLegacyPlugin>[0]
......@@ -51,6 +52,7 @@ process.env.UNI_COMPILER_VERSION_TYPE = pkg.version.includes('alpha')
: 'r'
export interface VitePluginUniOptions {
uvue?: boolean
vueOptions?: VueOptions
vueJsxOptions?: (VueJSXPluginOptions & { babelPlugins?: any[] }) | boolean
viteLegacyOptions?: ViteLegacyOptions | false
......@@ -91,6 +93,12 @@ export default function uniPlugin(
initPreContext(options.platform, process.env.UNI_CUSTOM_CONTEXT)
return process.env.UNI_UVUE === 'true'
? createUVuePlugins(options)
: createPlugins(options)
}
function createPlugins(options: VitePluginUniResolvedOptions) {
const plugins: Plugin[] = []
const injects = parseUniExtApis()
......@@ -207,3 +215,46 @@ export default function uniPlugin(
return plugins
}
function createUVuePlugins(options: VitePluginUniResolvedOptions) {
const plugins: Plugin[] = []
options.uvue = true
const uniPlugins = initExtraPlugins(
process.env.UNI_CLI_CONTEXT || process.cwd(),
(process.env.UNI_PLATFORM as UniApp.PLATFORM) || 'h5',
options
)
debugUni(uniPlugins)
const uniPluginOptions = initPluginUniOptions(uniPlugins)
options.copyOptions = uniPluginOptions.copyOptions
plugins.push(uniUVuePlugin(options))
plugins.push(...uniPlugins)
plugins.push(...initFixedEsbuildInitTSConfck(process.env.UNI_INPUT_DIR))
// 执行 build 命令时,vite 强制了 NODE_ENV
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/build.ts#L405
// const config = await resolveConfig(inlineConfig, 'build', 'production')
// 在 @vitejs/plugin-vue 之前校正回来
if (
process.env.UNI_NODE_ENV &&
process.env.UNI_NODE_ENV !== process.env.NODE_ENV
) {
process.env.NODE_ENV = process.env.UNI_NODE_ENV
}
plugins.push(
uniCopyPlugin({
outputDir: process.env.UNI_OUTPUT_DIR,
copyOptions: options.copyOptions,
})
)
return plugins
}
......@@ -14,6 +14,7 @@ interface PluginConfig {
id: string
name: string
apply?: UniApp.PLATFORM | UniApp.PLATFORM[]
uvue?: boolean
config: {
name: string
main?: string
......@@ -89,7 +90,11 @@ export function initExtraPlugins(
platform: UniApp.PLATFORM,
options: VitePluginUniResolvedOptions
) {
return initPlugins(cliRoot, resolvePlugins(cliRoot, platform), options)
return initPlugins(
cliRoot,
resolvePlugins(cliRoot, platform, options.uvue),
options
)
}
function initPlugin(
......@@ -126,7 +131,11 @@ function initPlugins(
.flat()
}
function resolvePlugins(cliRoot: string, platform: UniApp.PLATFORM) {
function resolvePlugins(
cliRoot: string,
platform: UniApp.PLATFORM,
uvue: boolean = false
) {
const pkg = require(path.join(cliRoot, 'package.json'))
return Object.keys(pkg.devDependencies || {})
.concat(Object.keys(pkg.dependencies || {}))
......@@ -151,6 +160,10 @@ function resolvePlugins(cliRoot: string, platform: UniApp.PLATFORM) {
return
}
}
// 插件必须支持 uvue
if (uvue && !config.uvue) {
return
}
return {
id,
name: config.name,
......
import type { Plugin } from 'vite'
import { parseManifestJsonOnce } from '@dcloudio/uni-cli-shared'
import { VitePluginUniResolvedOptions } from '../..'
import { createDefine } from '../../config/define'
import { createResolve } from '../../config/resolve'
import { createCss } from '../../config/css'
import { initLogger } from '../../configResolved'
export function uniUVuePlugin(options: VitePluginUniResolvedOptions): Plugin {
return {
name: 'uni:uvue',
config(config, env) {
options.command = env.command
let base = config.base
if (!base) {
const { h5 } = parseManifestJsonOnce(options.inputDir)
base = (h5 && h5.router && h5.router.base) || ''
}
if (!base) {
base = '/'
}
options.base = base!
return {
base: process.env.UNI_H5_BASE || base,
root: process.env.VITE_ROOT_DIR,
// TODO 临时设置为__static__,屏蔽警告:https://github.com/vitejs/vite/blob/824d042535033a5c3d7006978c0d05c201cd1c25/packages/vite/src/node/server/middlewares/transform.ts#L125
publicDir: config.publicDir || '__static__',
define: createDefine(options),
resolve: createResolve(options, config),
logLevel: config.logLevel || 'warn', // 默认使用 warn 等级,因为 info 等级vite:report 会输出文件列表等信息
optimizeDeps: {
disabled: true,
},
css: createCss(options, config),
}
},
configResolved(config) {
initLogger(config)
},
}
}
......@@ -189,7 +189,7 @@ importers:
specifier: ../../vite-plugin-uni
version: link:../../vite-plugin-uni
vite:
specifier: ^4.0.0
specifier: ^4.2.1
version: 4.2.1(@types/node@18.15.5)(terser@5.16.6)
packages/playground/ssr:
......@@ -296,6 +296,9 @@ importers:
packages/uni-app-plus:
dependencies:
'@dcloudio/uni-app-uts':
specifier: 3.0.0-alpha-3071220230324001
version: link:../uni-app-uts
'@dcloudio/uni-app-vite':
specifier: 3.0.0-alpha-3071220230324001
version: link:../uni-app-vite
......@@ -349,7 +352,42 @@ importers:
specifier: 3.2.47
version: 3.2.47
packages/uni-app-uts: {}
packages/uni-app-uts:
dependencies:
'@dcloudio/uni-cli-shared':
specifier: 3.0.0-alpha-3071220230324001
version: link:../uni-cli-shared
'@rollup/pluginutils':
specifier: ^4.2.0
version: 4.2.1
'@vue/compiler-core':
specifier: 3.2.47
version: 3.2.47
'@vue/compiler-sfc':
specifier: 3.2.47
version: 3.2.47
'@vue/shared':
specifier: 3.2.47
version: 3.2.47
debug:
specifier: ^4.3.3
version: 4.3.4(supports-color@8.1.1)
es-module-lexer:
specifier: ^1.2.1
version: 1.2.1
fs-extra:
specifier: ^10.0.0
version: 10.1.0
source-map:
specifier: ^0.6.1
version: 0.6.1
devDependencies:
'@types/debug':
specifier: ^4.1.7
version: 4.1.7
'@types/fs-extra':
specifier: ^9.0.13
version: 9.0.13
packages/uni-app-vite:
dependencies:
......@@ -3151,7 +3189,7 @@ packages:
dependencies:
'@jridgewell/trace-mapping': 0.3.17
callsites: 3.1.0
graceful-fs: 4.2.10
graceful-fs: 4.2.11
dev: true
/@jest/test-result@27.5.1:
......@@ -3191,7 +3229,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/test-result': 29.5.0
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jest-haste-map: 29.5.0
slash: 3.0.0
dev: true
......@@ -4716,7 +4754,7 @@ packages:
babel-plugin-istanbul: 6.1.1
babel-preset-jest: 29.5.0(@babel/core@7.21.3)
chalk: 4.1.2
graceful-fs: 4.2.10
graceful-fs: 4.2.11
slash: 3.0.0
transitivePeerDependencies:
- supports-color
......@@ -5794,6 +5832,10 @@ packages:
resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==}
dev: false
/es-module-lexer@1.2.1:
resolution: {integrity: sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==}
dev: false
/esbuild@0.17.12:
resolution: {integrity: sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==}
engines: {node: '>=12'}
......@@ -6286,7 +6328,7 @@ packages:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
dependencies:
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
dev: true
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册