Skip to content

Commit 6949a3d

Browse files
Barthelemy Ledouxelevatebart
authored andcommitted
feat: use dynamic components to build preview
In order ot make the devtools work we need to avoid nested instances of Vue. WIth this new way, we can keep the tree together while still being able to inspect components.
1 parent 90a1ae1 commit 6949a3d

File tree

5 files changed

+106
-151
lines changed

5 files changed

+106
-151
lines changed

demo/assets/Button.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template>
22
<div class="hello">
3+
<h1>Colored Text</h1>
34
<button>{{ msg }}</button>
45
</div>
56
</template>
@@ -14,8 +15,9 @@ export default {
1415
};
1516
</script>
1617

17-
<style scoped>
18+
<style>
1819
.hello {
1920
text-align: center;
21+
color: #900;
2022
}
2123
</style>

src/Preview.vue

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
<template>
22
<div>
33
<div style="color:red" v-if="error">{{this.error}}</div>
4-
<div :id="scope"/>
4+
<VuePreview :id="scope"/>
55
</div>
66
</template>
77

88
<script>
9-
import Vue from "vue";
109
import { transform } from "buble";
1110
import compileCode, { isCodeVueSfc } from "./utils/compileCode";
1211
import getVars from "./utils/getVars";
1312
import getVueConfigObject from "./utils/getVueConfigObject";
14-
import styleScoper from "./utils/styleScoper";
13+
import addScopedStyle from "./utils/addScopedStyle";
1514
1615
export default {
1716
name: "VueLivePreviewComponent",
17+
components: {},
1818
props: {
1919
code: {
2020
type: String,
2121
required: true
2222
},
23-
scoped: {
24-
type: Boolean,
25-
default: true
26-
},
2723
components: {
2824
type: Object,
2925
default: () => {}
@@ -35,7 +31,7 @@ export default {
3531
error: false
3632
};
3733
},
38-
mounted() {
34+
beforeMount() {
3935
this.renderComponent(this.code.trim());
4036
},
4137
methods: {
@@ -50,11 +46,11 @@ export default {
5046
this.error = e.message;
5147
},
5248
renderComponent(code) {
53-
let data = {},
54-
script,
55-
style,
56-
template;
49+
let data = {};
5750
let listVars = [];
51+
let script;
52+
let style;
53+
let template;
5854
try {
5955
const renderedComponent = compileCode(code);
6056
style = renderedComponent.style;
@@ -90,21 +86,8 @@ export default {
9086
9187
data.components = this.components;
9288
93-
// eslint-disable-next-line no-new
94-
const vueInstance = new Vue({
95-
el: `#${this.scope}`,
96-
render: createElement => createElement(data)
97-
});
98-
99-
// Add the scoped style if there is any
100-
if (style) {
101-
vueInstance.$el.setAttribute(`data-${this.scope}`, true);
102-
const styleContainer = document.createElement("div");
103-
styleContainer.innerHTML = style;
104-
styleContainer.firstChild.id = `data-${this.scope}`;
105-
vueInstance.$el.appendChild(styleContainer.firstChild);
106-
}
107-
styleScoper();
89+
this.$options.components.VuePreview = data;
90+
addScopedStyle(style, this.scope);
10891
}
10992
}
11093
};

src/utils/addScopedStyle.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { scoper } from "./styleScoper";
2+
3+
export default function addScopedStyle(css, suffix) {
4+
const head = document.head || document.getElementsByTagName("head")[0];
5+
const newstyle = document.createElement("style");
6+
newstyle.dataset.cssscoper = "true";
7+
const csses = scoper(css, `[data-${suffix}]`);
8+
if (newstyle.styleSheet) {
9+
newstyle.styleSheet.cssText = csses;
10+
} else {
11+
newstyle.appendChild(document.createTextNode(csses));
12+
}
13+
head.appendChild(newstyle);
14+
}

src/utils/normalizeSfcComponent.js

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,55 @@
1-
import { parseComponent } from 'vue-template-compiler'
2-
import walkes from 'walkes'
3-
import transformOneImport from './transformOneImport'
4-
import getAst from './getAst'
1+
import { parseComponent } from "vue-template-compiler";
2+
import walkes from "walkes";
3+
import transformOneImport from "./transformOneImport";
4+
import getAst from "./getAst";
55

66
const buildStyles = function(styles) {
7-
let _styles = ''
7+
let _styles = "";
88
if (styles) {
99
styles.forEach(it => {
1010
if (it.content) {
11-
_styles += it.content
11+
_styles += it.content;
1212
}
13-
})
13+
});
1414
}
15-
if (_styles !== '') {
16-
return `<style scoped>${_styles.trim()}</style>`
15+
if (_styles !== "") {
16+
return _styles.trim();
1717
}
18-
return undefined
19-
}
18+
return undefined;
19+
};
2020

2121
function getSingleFileComponentParts(code) {
22-
const parts = parseComponent(code)
22+
const parts = parseComponent(code);
2323
if (parts.script)
2424
parts.script.content = parts.script.content.replace(
2525
/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm,
26-
'$1'
27-
)
28-
return parts
26+
"$1"
27+
);
28+
return parts;
2929
}
3030

3131
function injectTemplateAndParseExport(parts) {
32-
const templateString = parts.template.content.replace(/`/g, '\\`')
32+
const templateString = parts.template.content.replace(/`/g, "\\`");
3333

34-
if (!parts.script) return `{\ntemplate: \`${templateString}\` }`
34+
if (!parts.script) return `{\ntemplate: \`${templateString}\` }`;
3535

36-
let code = parts.script.content
37-
let preprocessing = ''
38-
let startIndex = -1
39-
let endIndex = -1
40-
let offset = 0
36+
let code = parts.script.content;
37+
let preprocessing = "";
38+
let startIndex = -1;
39+
let endIndex = -1;
40+
let offset = 0;
4141
walkes(getAst(code), {
4242
// export const MyComponent = {}
4343
ExportNamedDeclaration(node) {
44-
preprocessing = code.slice(0, node.start + offset)
45-
startIndex = node.declaration.declarations[0].init.start + offset
46-
endIndex = node.declaration.declarations[0].init.end + offset
44+
preprocessing = code.slice(0, node.start + offset);
45+
startIndex = node.declaration.declarations[0].init.start + offset;
46+
endIndex = node.declaration.declarations[0].init.end + offset;
4747
},
4848
// export default {}
4949
ExportDefaultDeclaration(node) {
50-
preprocessing = code.slice(0, node.start + offset)
51-
startIndex = node.declaration.start + offset
52-
endIndex = node.declaration.end + offset
50+
preprocessing = code.slice(0, node.start + offset);
51+
startIndex = node.declaration.start + offset;
52+
endIndex = node.declaration.end + offset;
5353
},
5454
// module.exports = {}
5555
AssignmentExpression(node) {
@@ -59,26 +59,26 @@ function injectTemplateAndParseExport(parts) {
5959
/module/.test(node.left.object.name) &&
6060
/exports/.test(node.left.property.name))
6161
) {
62-
preprocessing = code.slice(0, node.start + offset)
63-
startIndex = node.right.start + offset
64-
endIndex = node.right.end + offset
62+
preprocessing = code.slice(0, node.start + offset);
63+
startIndex = node.right.start + offset;
64+
endIndex = node.right.end + offset;
6565
}
6666
},
6767
ImportDeclaration(node) {
68-
const ret = transformOneImport(node, code, offset)
69-
offset = ret.offset
70-
code = ret.code
68+
const ret = transformOneImport(node, code, offset);
69+
offset = ret.offset;
70+
code = ret.code;
7171
}
72-
})
72+
});
7373
if (startIndex === -1) {
74-
throw new Error('Failed to parse single file component: ' + code)
74+
throw new Error("Failed to parse single file component: " + code);
7575
}
76-
let right = code.slice(startIndex + 1, endIndex - 1)
76+
let right = code.slice(startIndex + 1, endIndex - 1);
7777
return {
7878
preprocessing,
7979
component: `{\n template: \`${templateString}\`,\n ${right}}`,
8080
postprocessing: code.slice(endIndex)
81-
}
81+
};
8282
}
8383

8484
/**
@@ -87,14 +87,14 @@ function injectTemplateAndParseExport(parts) {
8787
* transformed into requires
8888
*/
8989
export default function normalizeSfcComponent(code) {
90-
const parts = getSingleFileComponentParts(code)
91-
const extractedComponent = injectTemplateAndParseExport(parts)
90+
const parts = getSingleFileComponentParts(code);
91+
const extractedComponent = injectTemplateAndParseExport(parts);
9292
return {
9393
component: [
9494
extractedComponent.preprocessing,
9595
`new Vue(${extractedComponent.component});`,
9696
extractedComponent.postprocessing
97-
].join('\n'),
97+
].join("\n"),
9898
style: buildStyles(parts.styles)
99-
}
99+
};
100100
}

src/utils/styleScoper.js

Lines changed: 37 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,43 @@
11
/* eslint-disable no-control-regex */
22

33
// used to make CSS selectors remain scoped properly
4-
function init() {
5-
const style = document.createElement('style')
6-
style.appendChild(document.createTextNode(''))
7-
document.head.appendChild(style)
8-
style.sheet.insertRule('body { visibility: hidden; }', 0)
9-
}
104

115
export function scoper(css, suffix) {
12-
const re = /([^\r\n,{}]+)(,(?=[^}]*{)|s*{)/g
13-
14-
// `after` is going to contain eithe a comma or an opening curly bracket
15-
css = css.replace(re, function(full, selector, after) {
16-
// if non-rule delimiter
17-
if (selector.match(/^\s*(@media|@keyframes|to|from|@font-face)/)) {
18-
return selector + after
19-
}
20-
21-
// deal with :scope pseudo selectors
22-
if (selector.match(/:scope/)) {
23-
selector = selector.replace(/([^\s]*):scope/, function(full, cutSelector) {
24-
if (cutSelector === '') {
25-
return '> *'
26-
}
27-
return '> ' + cutSelector
28-
})
29-
}
30-
31-
// deal with other pseudo selectors
32-
let pseudo = ''
33-
if (selector.match(/:/)) {
34-
const parts = selector.split(/:/)
35-
selector = parts[0]
36-
pseudo = ':' + parts[1]
37-
}
38-
39-
selector = selector.trim() + ' '
40-
selector = selector.replace(/ /g, suffix + pseudo + ' ')
41-
42-
return selector + after
43-
})
44-
45-
return css
46-
}
47-
48-
function process() {
49-
const styles = document.body.querySelectorAll('style[scoped]')
50-
51-
if (styles.length === 0) {
52-
document.getElementsByTagName('body')[0].style.visibility = 'visible'
53-
return
54-
}
55-
56-
const head = document.head || document.getElementsByTagName('head')[0]
57-
const newstyle = document.createElement('style')
58-
newstyle.dataset.cssscoper = 'true'
59-
let csses = ''
60-
61-
let idx
62-
for (idx = 0; idx < styles.length; idx++) {
63-
const style = styles[idx]
64-
const moduleId = style.id
65-
const css = style.innerHTML
66-
67-
if (css && style.parentElement.nodeName !== 'BODY') {
68-
const suffix = '[' + moduleId + ']'
69-
style.parentNode.removeChild(style)
70-
71-
csses = csses + scoper(css, suffix)
72-
}
73-
}
74-
75-
if (newstyle.styleSheet) {
76-
newstyle.styleSheet.cssText = csses
77-
} else {
78-
newstyle.appendChild(document.createTextNode(csses))
79-
}
80-
head.appendChild(newstyle)
81-
82-
document.getElementsByTagName('body')[0].style.visibility = 'visible'
6+
const re = /([^\r\n,{}]+)(,(?=[^}]*{)|s*{)/g;
7+
8+
// `after` is going to contain eithe a comma or an opening curly bracket
9+
css = css.replace(re, function(full, selector, after) {
10+
// if non-rule delimiter
11+
if (selector.match(/^\s*(@media|@keyframes|to|from|@font-face)/)) {
12+
return selector + after;
13+
}
14+
15+
// deal with :scope pseudo selectors
16+
if (selector.match(/:scope/)) {
17+
selector = selector.replace(/([^\s]*):scope/, function(
18+
full,
19+
cutSelector
20+
) {
21+
if (cutSelector === "") {
22+
return "> *";
23+
}
24+
return "> " + cutSelector;
25+
});
26+
}
27+
28+
// deal with other pseudo selectors
29+
let pseudo = "";
30+
if (selector.match(/:/)) {
31+
const parts = selector.split(/:/);
32+
selector = parts[0];
33+
pseudo = ":" + parts[1];
34+
}
35+
36+
selector = selector.trim() + " ";
37+
selector = selector.replace(/ /g, suffix + pseudo + " ");
38+
39+
return selector + after;
40+
});
41+
42+
return css;
8343
}
84-
85-
init()
86-
87-
export default process

0 commit comments

Comments
 (0)