Skip to content

Commit 98292cc

Browse files
Merge pull request #25 from Toilal/vue2
feat(core): add vue2 support
2 parents e981a2c + 0851103 commit 98292cc

15 files changed

+1787
-65
lines changed

build/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"module": "ESNext",
66
"target": "ESNext",
77
"lib": [
8-
"ESNext"
8+
"ESNext",
9+
"DOM"
910
],
1011
"moduleResolution": "node"
1112
}

build/webpack.config.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ module.exports = (env = {}, { mode = 'production' }) => {
146146
// doc: https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin
147147
analyzerMode: 'static',
148148
openAnalyzer: false,
149+
reportFilename: `vue${ vueVersion }-sfc-loader.report.html`
149150
})
150151
] : [],
151152

@@ -157,8 +158,16 @@ ${ pkg.name } v${ pkg.version }
157158
@license ${ pkg.license }
158159
`.trim()),
159160
],
160-
161+
externals: {
162+
'vue': {
163+
amd: 'vue',
164+
commonjs: 'vue',
165+
commonjs2: 'vue',
166+
root: 'Vue'
167+
},
168+
},
161169
resolve: {
170+
extensions: [".ts", ".js"],
162171
mainFields: ['browser', 'main', 'module'],
163172
alias: {
164173

@@ -201,6 +210,11 @@ ${ pkg.name } v${ pkg.version }
201210
'postcss-modules-values': false,
202211
'postcss-modules-scope': false,
203212

213+
// vue2
214+
'sass': false,
215+
'stylus': false,
216+
'less': false,
217+
'prettier': false,
204218

205219
...!genSourcemap ? {
206220
'source-map': false,
@@ -222,6 +236,8 @@ ${ pkg.name } v${ pkg.version }
222236
'process': false, //require.resolve('process/'),
223237
'vm': false, // or require.resolve('vm-browserify'),
224238
'fs': false,
239+
'os': false,
240+
'module': false
225241
}
226242
},
227243

package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
"scripts": {
2222
"coverage": "node test/coverageTest.mjs",
2323
"test": "cd test && yarn run start",
24+
"testVue2": "cd test && cross-env VUE_VERSION=2 yarn run start",
2425
"tests": "jest --runInBand \"tests/.*\\.test.js\"",
2526
"dev": "webpack --mode=development --config ./build/webpack.config.js --progress --watch",
26-
"build": "cross-env-shell webpack --mode=production --config ./build/webpack.config.js --progress --env targetsBrowsers=\\\"$npm_package_browserslist\\\"",
27+
"build": "cross-env-shell webpack --mode=production --config ./build/webpack.config.js --progress --env targetsBrowsers=\\\"$npm_package_browserslist\\\" && cross-env-shell webpack --mode=production --config ./build/webpack.config.js --progress --env vueVersion=\\\"2\\\" --env targetsBrowsers=\\\"$npm_package_browserslist\\\"",
2728
"docs": "cross-env-shell node build/evalHtmlComments.js README.md $npm_package_version && node build/evalHtmlComments.js docs/examples.md $npm_package_version && typedoc --plugin typedoc-plugin-markdown --mode file --tsconfig ./build/tsconfig.json --inputFiles ./src/index.ts --out ./docs/api --readme none --stripInternal --namedAnchors true",
2829
"pushDocs": "yarn run docs && git add docs/ && cross-env-shell git commit -m \\\"chore(docs): v$npm_package_version API docs & examples \\\" docs",
2930
"release": "standard-version --header \"\""
@@ -62,10 +63,10 @@
6263
"stream-browserify": "^3.0.0",
6364
"url": "^0.11.0",
6465
"util": "^0.12.3",
65-
"vm-browserify": "^1.1.2",
66-
"vue": "3.0.4"
66+
"vm-browserify": "^1.1.2"
6767
},
6868
"devDependencies": {
69+
"@vue/component-compiler-utils": "^3.2.0",
6970
"babel-loader": "^8.2.2",
7071
"caniuse-api": "^3.0.0",
7172
"compression-webpack-plugin": "^7.1.2",
@@ -79,8 +80,16 @@
7980
"typedoc": "0.19.2",
8081
"typedoc-plugin-markdown": "3.1.1",
8182
"typescript": "^4.2.2",
83+
"vue": "^3.0.7",
84+
"vue-template-compiler": "^2.6.12",
85+
"vue2": "npm:vue@^2.6.12",
8286
"webpack": "^5.24.3",
8387
"webpack-bundle-analyzer": "^4",
8488
"webpack-cli": "^4.5.0"
89+
},
90+
"peerDependencies": {
91+
"@vue/component-compiler-utils": "^3.2.0",
92+
"vue": "^2.6.12|^3.0.7",
93+
"vue-template-compiler": "^2.6.12"
8594
}
8695
}

src/createSFCModule.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { LoadModule, ModuleExport, Options } from './types'
2+
3+
declare export async function createSFCModule(source : string, filename : string, options : Options, loadModule : LoadModule) : Promise<ModuleExport>

src/createVue2SFCModule.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// compiler-sfc src: https://github.com/vuejs/vue-next/blob/master/packages/compiler-sfc/src/index.ts#L1
2+
import {
3+
compileStyleAsync as sfc_compileStyleAsync,
4+
compileTemplate as sfc_compileTemplate,
5+
parse as sfc_parse,
6+
} from '@vue/component-compiler-utils'
7+
8+
import * as vueTemplateCompiler from 'vue-template-compiler'
9+
import { VueTemplateCompiler } from '@vue/component-compiler-utils/dist/types'
10+
import { TemplateCompileOptions } from '@vue/component-compiler-utils/dist/compileTemplate'
11+
12+
13+
import {
14+
parse as babel_parse,
15+
ParserPlugin as babel_ParserPlugin
16+
} from '@babel/parser';
17+
18+
import {
19+
transformFromAstAsync as babel_transformFromAstAsync,
20+
types as t,
21+
} from '@babel/core';
22+
23+
// @ts-ignore (Could not find a declaration file for module '@babel/plugin-transform-modules-commonjs')
24+
import babelPluginTransformModulesCommonjs from '@babel/plugin-transform-modules-commonjs'
25+
26+
27+
import { formatError, withCache, hash, renameDynamicImport, parseDeps, interopRequireDefault, transformJSCode, loadDeps, createModule } from './tools.ts'
28+
29+
import { Options, LoadModule, ModuleExport, CustomBlockCallback } from './types.ts'
30+
31+
/**
32+
* the version of the library (process.env.VERSION is set by webpack, at compile-time)
33+
*/
34+
const version : string = process.env.VERSION;
35+
36+
const genSourcemap : boolean = !!process.env.GEN_SOURCEMAP;
37+
38+
/**
39+
* @internal
40+
*/
41+
const isProd : boolean = process.env.NODE_ENV === 'production';
42+
43+
44+
45+
/**
46+
* @internal
47+
*/
48+
49+
export async function createSFCModule(source : string, filename : string, options : Options, loadModule : LoadModule) : Promise<ModuleExport> {
50+
51+
const component = {};
52+
53+
54+
const { compiledCache, addStyle, log, additionalBabelPlugins = [], customBlockHandler } = options;
55+
56+
// vue-loader next: https://github.com/vuejs/vue-loader/blob/next/src/index.ts#L91
57+
const descriptor = sfc_parse({
58+
source,
59+
filename,
60+
needMap: genSourcemap,
61+
compiler: vueTemplateCompiler as VueTemplateCompiler}
62+
);
63+
64+
const customBlockCallbacks : CustomBlockCallback[] = customBlockHandler !== undefined ? await Promise.all( descriptor.customBlocks.map((block ) => customBlockHandler(block, filename, options)) ) : [];
65+
66+
const componentHash = hash(filename, version);
67+
const scopeId = `data-v-${componentHash}`;
68+
69+
// hack: asynchronously preloads the language processor before it is required by the synchronous preprocessCustomRequire() callback, see below
70+
if ( descriptor.template && descriptor.template.lang )
71+
await loadModule(descriptor.template.lang, options);
72+
73+
74+
const hasScoped = descriptor.styles.some(e => e.scoped);
75+
76+
const compileTemplateOptions : TemplateCompileOptions = descriptor.template ? {
77+
// hack, since sourceMap is not configurable an we want to get rid of source-map dependency. see genSourcemap
78+
source: descriptor.template.content,
79+
filename,
80+
compiler: vueTemplateCompiler as VueTemplateCompiler,
81+
compilerOptions: undefined,
82+
preprocessLang: descriptor.template.lang,
83+
isProduction: isProd,
84+
prettify: false
85+
} : null;
86+
87+
if ( descriptor.script ) {
88+
89+
// eg: https://github.com/vuejs/vue-loader/blob/v15.9.6/lib/index.js
90+
91+
const [ depsList, transformedScriptSource ] = await withCache(compiledCache, [ componentHash, descriptor.script.content ], async ({ preventCache }) => {
92+
93+
const babelParserPlugins : babel_ParserPlugin[] = [];
94+
95+
let ast: t.File
96+
try {
97+
ast = babel_parse(descriptor.script.content, {
98+
// doc: https://babeljs.io/docs/en/babel-parser#options
99+
// if: https://github.com/babel/babel/blob/main/packages/babel-parser/typings/babel-parser.d.ts#L24
100+
plugins: [
101+
...babelParserPlugins
102+
],
103+
sourceType: 'module',
104+
sourceFilename: filename
105+
});
106+
107+
} catch(ex) {
108+
log?.('error', 'SFC script', formatError(ex.message, filename, source, ex.loc.line, ex.loc.column + 1) );
109+
throw ex;
110+
}
111+
112+
renameDynamicImport(ast);
113+
const depsList = parseDeps(ast);
114+
115+
// doc: https://babeljs.io/docs/en/babel-core#transformfromastasync
116+
const transformedScript = await babel_transformFromAstAsync(ast, descriptor.script.content, {
117+
sourceMaps: genSourcemap, // https://babeljs.io/docs/en/options#sourcemaps
118+
plugins: [ // https://babeljs.io/docs/en/options#plugins
119+
babelPluginTransformModulesCommonjs, // https://babeljs.io/docs/en/babel-plugin-transform-modules-commonjs#options
120+
...additionalBabelPlugins,
121+
],
122+
babelrc: false,
123+
configFile: false,
124+
highlightCode: false,
125+
});
126+
127+
return [ depsList, transformedScript.code ];
128+
});
129+
130+
await loadDeps(filename, depsList, options, loadModule);
131+
Object.assign(component, interopRequireDefault(createModule(filename, transformedScriptSource, options, loadModule).exports).default);
132+
}
133+
134+
135+
if ( descriptor.template !== null ) {
136+
// compiler-sfc src: https://github.com/vuejs/vue-next/blob/15baaf14f025f6b1d46174c9713a2ec517741d0d/packages/compiler-sfc/src/compileTemplate.ts#L39
137+
// compileTemplate eg: https://github.com/vuejs/vue-loader/blob/next/src/templateLoader.ts#L33
138+
const [ templateDepsList, templateTransformedSource ] = await withCache(compiledCache, [ componentHash, descriptor.template.content ], async ({ preventCache }) => {
139+
140+
const template = sfc_compileTemplate(compileTemplateOptions);
141+
// "@vue/component-compiler-utils" does NOT assume any module system, and expose render in global scope.
142+
template.code += "\nmodule.exports = {render: render, staticRenderFns: staticRenderFns}"
143+
144+
if ( template.errors.length ) {
145+
146+
preventCache();
147+
for ( const err of template.errors ) {
148+
149+
// @ts-ignore (Property 'message' does not exist on type 'string | CompilerError')
150+
log?.('error', 'SFC template', err );
151+
}
152+
}
153+
154+
for ( const err of template.tips )
155+
log?.('info', 'SFC template', err);
156+
157+
return await transformJSCode(template.code, true, filename, options);
158+
});
159+
160+
await loadDeps(filename, templateDepsList, options, loadModule);
161+
Object.assign(component, createModule(filename, templateTransformedSource, options, loadModule).exports);
162+
}
163+
164+
165+
for ( const descStyle of descriptor.styles ) {
166+
167+
// hack: asynchronously preloads the language processor before it is required by the synchronous preprocessCustomRequire() callback, see below
168+
if ( descStyle.lang )
169+
await loadModule(descStyle.lang, options);
170+
171+
const style = await withCache(compiledCache, [ componentHash, descStyle.content ], async ({ preventCache }) => {
172+
173+
// src: https://github.com/vuejs/vue-next/blob/15baaf14f025f6b1d46174c9713a2ec517741d0d/packages/compiler-sfc/src/compileStyle.ts#L70
174+
const compiledStyle = await sfc_compileStyleAsync({
175+
source: descStyle.content,
176+
filename,
177+
id: scopeId,
178+
scoped: descStyle.scoped,
179+
trim: false, // Should we enable it, as it requires postcss trimPlugin ?
180+
preprocessLang: descStyle.lang
181+
});
182+
183+
if ( compiledStyle.errors.length ) {
184+
185+
preventCache();
186+
for ( const err of compiledStyle.errors ) {
187+
188+
// @ts-ignore (Property 'line' does not exist on type 'Error' and Property 'column' does not exist on type 'Error')
189+
log?.('error', 'SFC style', formatError(err.message, filename, source, err.line, err.column) );
190+
}
191+
}
192+
193+
return compiledStyle.code;
194+
});
195+
196+
addStyle(style, descStyle.scoped ? scopeId : undefined);
197+
}
198+
199+
if ( customBlockHandler !== undefined )
200+
await Promise.all(customBlockCallbacks.map(cb => cb?.(component)));
201+
202+
return component;
203+
}

src/createVue3SFCModule.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ import { Options, LoadModule, ModuleExport, CustomBlockCallback } from './types.
3939
*/
4040
type PreprocessLang = SFCAsyncStyleCompileOptions['preprocessLang'];
4141

42-
43-
// SFCBlock : https://github.com/vuejs/vue-next/blob/cc975c1292978dc796c581b32d4b51c32c2e6370/packages/compiler-sfc/src/parse.ts#L24-L32
44-
//export { SFCBlock } from '@vue/compiler-sfc';
45-
4642
/**
4743
* the version of the library (process.env.VERSION is set by webpack, at compile-time)
4844
*/
@@ -75,7 +71,7 @@ export async function createSFCModule(source : string, filename : string, option
7571
});
7672

7773

78-
const customBlockCallbacks : CustomBlockCallback[] = customBlockHandler !== undefined ? await Promise.all( descriptor.customBlocks.map((block : SFCBlock) => customBlockHandler(block, filename, options)) ) : [];
74+
const customBlockCallbacks : CustomBlockCallback[] = customBlockHandler !== undefined ? await Promise.all( descriptor.customBlocks.map((block) => customBlockHandler(block, filename, options)) ) : [];
7975

8076
const componentHash = hash(filename, version);
8177
const scopeId = `data-v-${componentHash}`;

src/tools.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const version : string = process.env.VERSION;
4242
/**
4343
* @internal
4444
*/
45-
export function formatError(message : string, path : string, source : string, line : number, column : number) : string {
45+
export function formatError(message : string, path : string, source : string, line? : number, column? : number) : string {
4646

4747
const location = {
4848
start: { line, column },
@@ -226,6 +226,8 @@ export function createModule(filename : string, source : string, options : Optio
226226
// see https://github.com/nodejs/node/blob/a46b21f556a83e43965897088778ddc7d46019ae/lib/internal/modules/cjs/loader.js#L1102
227227
Function('exports', 'require', 'module', '__filename', '__dirname', 'import_', source).call(module.exports, module.exports, require, module, filename, resolve(filename, '.'), import_);
228228

229+
console.log(source)
230+
console.log(module)
229231
return module;
230232
}
231233

test/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"scripts": {
3-
"start": "node ./serve.js"
3+
"start": "node ./serve.js",
4+
"startVue2": "cross-env VUE_VERSION=2 node ./serveVue2.js"
45
},
56
"dependencies": {
67
"express": "^4.17.1",

test/serve.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ const path = require('path');
22
const express = require('express');
33
const open = require('open');
44

5+
const index = process.env.VUE_VERSION === '2' ? 'vue2.html' : 'vue3.html'
6+
57
const app = express();
68
app.use(express.static(path.resolve(__dirname, '../dist')));
7-
app.use(express.static(__dirname, { 'index': ['index.html'] } ));
9+
app.use(express.static(__dirname, { 'index': [index] } ));
810

9-
const port = process.env.PORT.trim() || 80;
11+
const port = process.env.PORT ? process.env.PORT.trim() : 8080;
1012
app.listen(port)
1113

1214
console.log(`http://localhost:${ port }/`);

0 commit comments

Comments
 (0)