Skip to content

Commit d30160e

Browse files
Abstracted example-generating utilities and made tutorial content dynamic (vuejs#1305)
1 parent 6b76c77 commit d30160e

File tree

4 files changed

+201
-173
lines changed

4 files changed

+201
-173
lines changed

src/examples/ExampleRepl.vue

Lines changed: 3 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Repl, ReplStore } from '@vue/repl'
33
import '@vue/repl/style.css'
44
import data from './data.json'
55
import { inject, watchEffect, version, Ref } from 'vue'
6+
import { resolveSFCExample, resolveNoBuildExample } from './utils'
67
78
const store = new ReplStore({
89
defaultVueRuntimeURL: `https://unpkg.com/vue@${version}/dist/vue.esm-browser.js`
@@ -28,171 +29,11 @@ function updateExample() {
2829
}
2930
store.setFiles(
3031
preferSFC.value
31-
? resolveSFCExample(data[hash])
32-
: resolveNoBuildExample(data[hash]),
32+
? resolveSFCExample(data[hash], preferComposition.value)
33+
: resolveNoBuildExample(data[hash], preferComposition.value),
3334
preferSFC.value ? 'App.vue' : 'index.html'
3435
)
3536
}
36-
37-
type ExampleData = {
38-
[key: string]: Record<string, string>
39-
} & {
40-
'import-map.json': string
41-
}
42-
43-
function resolveSFCExample(raw: ExampleData) {
44-
const files: Record<string, string> = {}
45-
forEachComponent(
46-
raw,
47-
files,
48-
(filename, { template, composition, options, style }) => {
49-
let sfcContent =
50-
filename === 'App' ? `<!--\n${raw['description.txt']}\n-->\n\n` : ``
51-
if (preferComposition.value) {
52-
sfcContent += `<script setup>\n${toScriptSetup(
53-
composition,
54-
template
55-
)}<\/script>`
56-
} else {
57-
sfcContent += `<script>\n${options}<\/script>`
58-
}
59-
sfcContent += `\n\n<template>\n${indent(template)}</template>`
60-
if (style) {
61-
sfcContent += `\n\n<style>\n${style}</style>`
62-
}
63-
files[filename + '.vue'] = sfcContent
64-
}
65-
)
66-
return files
67-
}
68-
69-
function resolveNoBuildExample(raw: ExampleData) {
70-
const files: Record<string, string> = {}
71-
72-
let html = `<!--\n${raw['description.txt']}\n-->\n\n`
73-
let css = ''
74-
75-
// set it first for ordering
76-
files['index.html'] = html
77-
forEachComponent(
78-
raw,
79-
files,
80-
(filename, { template, composition, options, style }) => {
81-
let js = preferComposition.value ? composition : options
82-
// rewrite imports to *.vue
83-
js = js.replace(/import (.*) from '(.*)\.vue'/g, "import $1 from '$2.js'")
84-
85-
const _template = indent(toKebabTags(template).trim())
86-
if (style) css += style
87-
88-
if (filename === 'App') {
89-
html += `<script type="module">\n${injectCreateApp(js)}<\/script>`
90-
html += `\n\n<div id="app">\n${_template}</div>`
91-
} else {
92-
// html += `\n\n<template id="${filename}">\n${_template}</template>`
93-
js = js.replace(
94-
/export default \{([^]*)\n\}/,
95-
`export default {$1,\n template: \`\n${_template}\n \`\n}`
96-
)
97-
files[filename + '.js'] = js
98-
}
99-
}
100-
)
101-
files['index.html'] = html
102-
if (css) {
103-
files['style.css'] = css
104-
}
105-
return files
106-
}
107-
108-
function forEachComponent(
109-
raw: ExampleData,
110-
files: Record<string, string>,
111-
cb: (filename: string, file: Record<string, string>) => void
112-
) {
113-
for (const filename in raw) {
114-
const content = raw[filename]
115-
if (filename === 'description.txt') {
116-
continue
117-
} else if (typeof content === 'string') {
118-
files[filename] = content
119-
} else {
120-
const {
121-
'template.html': template,
122-
'composition.js': composition,
123-
'options.js': options,
124-
'style.css': style
125-
} = content
126-
cb(filename, { template, composition, options, style })
127-
}
128-
}
129-
}
130-
131-
function toScriptSetup(src: string, template: string): string {
132-
const exportDefaultIndex = src.indexOf('export default')
133-
const lastReturnIndex = src.lastIndexOf('return {')
134-
135-
let setupCode =
136-
lastReturnIndex > -1
137-
? deindent(
138-
src
139-
.slice(exportDefaultIndex, lastReturnIndex)
140-
.replace(/export default[^]+?setup\([^)]*\)\s*{/, '')
141-
.trim()
142-
)
143-
: ''
144-
145-
const propsStartIndex = src.indexOf(`\n props:`)
146-
if (propsStartIndex > -1) {
147-
const propsEndIndex = src.indexOf(`\n }`, propsStartIndex) + 4
148-
const propsVar =
149-
/\bprops\b/.test(template) || /\bprops\b/.test(src)
150-
? `const props = `
151-
: ``
152-
const propsDef = deindent(
153-
src
154-
.slice(propsStartIndex, propsEndIndex)
155-
.trim()
156-
.replace(/,$/, '')
157-
.replace(/^props: /, `${propsVar}defineProps(`) + ')',
158-
1
159-
)
160-
setupCode = (propsDef + '\n\n' + setupCode).trim()
161-
}
162-
163-
return src.slice(0, exportDefaultIndex) + setupCode + '\n'
164-
}
165-
166-
function indent(str: string): string {
167-
return str
168-
.split('\n')
169-
.map((l) => (l.trim() ? ` ${l}` : l))
170-
.join('\n')
171-
}
172-
173-
function deindent(str: string, tabsize = 2): string {
174-
return str
175-
.split('\n')
176-
.map((l) => l.replace(tabsize === 1 ? /^\s{2}/ : /^\s{4}/, ''))
177-
.join('\n')
178-
}
179-
180-
function injectCreateApp(src: string): string {
181-
const importVueRE = /import {(.*?)} from 'vue'/
182-
if (importVueRE.test(src)) {
183-
src = src.replace(importVueRE, `import { createApp,$1} from 'vue'`)
184-
} else {
185-
const newline = src.startsWith(`import`) ? `\n` : `\n\n`
186-
src = `import { createApp } from 'vue'${newline}${src}`
187-
}
188-
return src.replace(/export default ({[^]*\n})/, "createApp($1).mount('#app')")
189-
}
190-
191-
function toKebabTags(str: string): string {
192-
return str.replace(/(<\/?)([A-Z]\w+)(\s|>)/g, (_, open, tagName, end) => {
193-
return open + tagName.replace(/\B([A-Z])/g, '-$1').toLowerCase() + end
194-
})
195-
}
19637
</script>
19738

19839
<template>

src/examples/utils.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
type ExampleData = {
2+
[key: string]: Record<string, string>
3+
} & {
4+
'import-map.json': string
5+
}
6+
7+
function indent(str: string): string {
8+
return str
9+
.split('\n')
10+
.map((l) => (l.trim() ? ` ${l}` : l))
11+
.join('\n')
12+
}
13+
14+
function deindent(str: string, tabsize = 2): string {
15+
return str
16+
.split('\n')
17+
.map((l) => l.replace(tabsize === 1 ? /^\s{2}/ : /^\s{4}/, ''))
18+
.join('\n')
19+
}
20+
21+
function toKebabTags(str: string): string {
22+
return str.replace(/(<\/?)([A-Z]\w+)(\s|>)/g, (_, open, tagName, end) => {
23+
return open + tagName.replace(/\B([A-Z])/g, '-$1').toLowerCase() + end
24+
})
25+
}
26+
27+
function toScriptSetup(src: string, template: string): string {
28+
const exportDefaultIndex = src.indexOf('export default')
29+
const lastReturnIndex = src.lastIndexOf('return {')
30+
31+
let setupCode =
32+
lastReturnIndex > -1
33+
? deindent(
34+
src
35+
.slice(exportDefaultIndex, lastReturnIndex)
36+
.replace(/export default[^]+?setup\([^)]*\)\s*{/, '')
37+
.trim()
38+
)
39+
: ''
40+
41+
const propsStartIndex = src.indexOf(`\n props:`)
42+
if (propsStartIndex > -1) {
43+
const propsEndIndex = src.indexOf(`\n }`, propsStartIndex) + 4
44+
const propsVar =
45+
/\bprops\b/.test(template) || /\bprops\b/.test(src)
46+
? `const props = `
47+
: ``
48+
const propsDef = deindent(
49+
src
50+
.slice(propsStartIndex, propsEndIndex)
51+
.trim()
52+
.replace(/,$/, '')
53+
.replace(/^props: /, `${propsVar}defineProps(`) + ')',
54+
1
55+
)
56+
setupCode = (propsDef + '\n\n' + setupCode).trim()
57+
}
58+
59+
return src.slice(0, exportDefaultIndex) + setupCode + '\n'
60+
}
61+
62+
function forEachComponent(
63+
raw: ExampleData,
64+
files: Record<string, string>,
65+
cb: (filename: string, file: Record<string, string>) => void
66+
) {
67+
for (const filename in raw) {
68+
const content = raw[filename]
69+
if (filename === 'description.txt') {
70+
continue
71+
} else if (typeof content === 'string') {
72+
files[filename] = content
73+
} else {
74+
const {
75+
'template.html': template,
76+
'composition.js': composition,
77+
'options.js': options,
78+
'style.css': style
79+
} = content
80+
cb(filename, { template, composition, options, style })
81+
}
82+
}
83+
}
84+
85+
function injectCreateApp(src: string): string {
86+
const importVueRE = /import {(.*?)} from 'vue'/
87+
if (importVueRE.test(src)) {
88+
src = src.replace(importVueRE, `import { createApp,$1} from 'vue'`)
89+
} else {
90+
const newline = src.startsWith(`import`) ? `\n` : `\n\n`
91+
src = `import { createApp } from 'vue'${newline}${src}`
92+
}
93+
return src.replace(/export default ({[^]*\n})/, "createApp($1).mount('#app')")
94+
}
95+
96+
export function resolveSFCExample(raw: ExampleData, preferComposition: boolean) {
97+
const files: Record<string, string> = {}
98+
forEachComponent(
99+
raw,
100+
files,
101+
(filename, { template, composition, options, style }) => {
102+
let sfcContent =
103+
filename === 'App' ? `<!--\n${raw['description.txt']}\n-->\n\n` : ``
104+
if (preferComposition) {
105+
sfcContent += `<script setup>\n${toScriptSetup(
106+
composition,
107+
template
108+
)}<\/script>`
109+
} else {
110+
sfcContent += `<script>\n${options}<\/script>`
111+
}
112+
sfcContent += `\n\n<template>\n${indent(template)}</template>`
113+
if (style) {
114+
sfcContent += `\n\n<style>\n${style}</style>`
115+
}
116+
files[filename + '.vue'] = sfcContent
117+
}
118+
)
119+
return files
120+
}
121+
122+
export function resolveNoBuildExample(raw: ExampleData, preferComposition: boolean) {
123+
const files: Record<string, string> = {}
124+
125+
let html = `<!--\n${raw['description.txt']}\n-->\n\n`
126+
let css = ''
127+
128+
// set it first for ordering
129+
files['index.html'] = html
130+
forEachComponent(
131+
raw,
132+
files,
133+
(filename, { template, composition, options, style }) => {
134+
let js = preferComposition ? composition : options
135+
// rewrite imports to *.vue
136+
js = js.replace(/import (.*) from '(.*)\.vue'/g, "import $1 from '$2.js'")
137+
138+
const _template = indent(toKebabTags(template).trim())
139+
if (style) css += style
140+
141+
if (filename === 'App') {
142+
html += `<script type="module">\n${injectCreateApp(js)}<\/script>`
143+
html += `\n\n<div id="app">\n${_template}</div>`
144+
} else {
145+
// html += `\n\n<template id="${filename}">\n${_template}</template>`
146+
js = js.replace(
147+
/export default \{([^]*)\n\}/,
148+
`export default {$1,\n template: \`\n${_template}\n \`\n}`
149+
)
150+
files[filename + '.js'] = js
151+
}
152+
}
153+
)
154+
files['index.html'] = html
155+
if (css) {
156+
files['style.css'] = css
157+
}
158+
return files
159+
}
160+
161+

src/tutorial/TutorialRepl.vue

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
1-
<script setup>
1+
<script setup lang="ts">
22
import { Repl, ReplStore } from '@vue/repl'
3+
import { inject, watchEffect, version, Ref } from 'vue'
4+
import data from './data.json'
5+
import { resolveSFCExample, resolveNoBuildExample } from '../examples/utils'
36
import '@vue/repl/style.css'
47
5-
const store = new ReplStore()
8+
const store = new ReplStore({
9+
defaultVueRuntimeURL: `https://unpkg.com/vue@${version}/dist/vue.esm-browser.js`
10+
})
611
7-
// TODO make dynamic
8-
const preference = document.documentElement.classList.contains(
9-
'prefer-composition'
10-
)
11-
? 'composition'
12-
: 'options'
12+
const preferComposition = inject('prefer-composition') as Ref<boolean>
13+
const preferSFC = inject('prefer-sfc') as Ref<boolean>
14+
15+
function updateExample() {
16+
let hash = ___location.hash.slice(1)
17+
if (!data.hasOwnProperty(hash)) {
18+
hash = 'hello-world'
19+
___location.hash = `#${hash}`
20+
}
21+
store.setFiles(
22+
preferSFC.value
23+
? resolveSFCExample(data[hash], preferComposition.value)
24+
: resolveNoBuildExample(data[hash], preferComposition.value),
25+
preferSFC.value ? 'App.vue' : 'index.html'
26+
)
27+
}
28+
29+
watchEffect(updateExample)
30+
window.addEventListener('hashchange', updateExample)
1331
</script>
1432

1533
<template>
@@ -55,9 +73,7 @@ const preference = document.documentElement.classList.contains(
5573
@media (max-width: 960px) {
5674
.vue-repl {
5775
border: none;
58-
height: calc(
59-
100vh - var(--vp-nav-height) - var(--ins-height) - 48px
60-
);
76+
height: calc(100vh - var(--vp-nav-height) - var(--ins-height) - 48px);
6177
}
6278
}
6379
</style>

0 commit comments

Comments
 (0)