Skip to content

Commit 61014db

Browse files
Merge branch 'main' of github.com:FranckFreiburger/vue3-sfc-loader into main
2 parents 1e1ef7e + 98292cc commit 61014db

15 files changed

+1737
-70
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: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
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'
17

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'
211

312

413
import {
@@ -19,10 +28,6 @@ import { formatError, withCache, hash, renameDynamicImport, parseDeps, interopRe
1928

2029
import { Options, LoadModule, ModuleExport, CustomBlockCallback } from './types.ts'
2130

22-
23-
24-
25-
2631
/**
2732
* the version of the library (process.env.VERSION is set by webpack, at compile-time)
2833
*/
@@ -46,8 +51,153 @@ export async function createSFCModule(source : string, filename : string, option
4651
const component = {};
4752

4853

49-
const { delimiters, moduleCache, compiledCache, addStyle, log, additionalBabelPlugins = [], customBlockHandler } = options;
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+
}
50198

199+
if ( customBlockHandler !== undefined )
200+
await Promise.all(customBlockCallbacks.map(cb => cb?.(component)));
51201

52202
return component;
53203
}

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: 1 addition & 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 },

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 }/`);

test/vue2.html

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<body>
4+
<div id="app"></div>
5+
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
6+
<script src="vue2-sfc-loader.js"></script>
7+
<script>
8+
9+
/* <!-- */
10+
const config = {
11+
files: {
12+
'/app.vue': `
13+
<template>
14+
<span class="vue2">
15+
Hello World! {{ abc }}
16+
</span>
17+
</template>
18+
<style scoped>
19+
.vue2 {
20+
background: red
21+
}
22+
</style>
23+
<script>
24+
export default {
25+
data: {
26+
abc: "test"
27+
}
28+
}
29+
<\/script>
30+
`
31+
}
32+
};
33+
/* --> */
34+
35+
const options = {
36+
moduleCache: { vue: Vue },
37+
getFile: url => config.files[url],
38+
addStyle: (textContent) => {
39+
const style = Object.assign(document.createElement('style'), { textContent });
40+
const ref = document.head.getElementsByTagName('style')[0] || null;
41+
document.head.insertBefore(style, ref);
42+
},
43+
}
44+
45+
const asyncComponentFactory = window['vue2-sfc-loader'].loadModule('/app.vue', options)
46+
asyncComponentFactory.then((component) => {
47+
new Vue(component).$mount('#app')
48+
})
49+
50+
</script>
51+
</body>
52+
</html>

0 commit comments

Comments
 (0)