@bakery-framework/plugin-vue 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +38 -0
- package/package.json +49 -0
- package/src/actions.ts +118 -0
- package/src/chunks.ts +66 -0
- package/src/compile.ts +247 -0
- package/src/handler.ts +538 -0
- package/src/index.ts +39 -0
- package/src/setup.ts +23 -0
- package/src/shell.ts +15 -0
- package/src/types.d.ts +92 -0
- package/src/utils.ts +608 -0
- package/src/vue.d.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Kyle Cyrus Santos Obille
|
|
2
|
+
|
|
3
|
+
The Software is provided subject to the standard MIT License, as detailed below, with the addition of the Commons Clause v1.0.
|
|
4
|
+
|
|
5
|
+
The Commons Clause v1.0
|
|
6
|
+
|
|
7
|
+
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
|
8
|
+
|
|
9
|
+
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
|
|
10
|
+
|
|
11
|
+
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
|
|
12
|
+
|
|
13
|
+
Standard MIT License
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software (subject to the Commons Clause condition above), and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @bakery-framework/plugin-vue
|
|
2
|
+
|
|
3
|
+
Vue single-file components for
|
|
4
|
+
[Bakery](https://github.com/obillekyle/bakery).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
bun add @bakery-framework/plugin-vue vue
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`vue` and `@vue/compiler-sfc` are peer dependencies — installing `vue` brings
|
|
11
|
+
the compiler with it.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// server.config.ts
|
|
17
|
+
import { defineConfig } from '@bakery-framework/core'
|
|
18
|
+
import vuePlugin from '@bakery-framework/plugin-vue'
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
root: 'src',
|
|
22
|
+
plugins: [vuePlugin()],
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A `.vue` file under `root` then serves as a route, alongside `.tsx` pages.
|
|
27
|
+
Options are typed as `VuePluginOptions`.
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT with the Commons Clause v1.0 — see [LICENSE](./LICENSE).
|
|
32
|
+
|
|
33
|
+
**Not an OSI-approved licence.** The Commons Clause removes the right to *sell*
|
|
34
|
+
the software — meaning to charge for a product or service whose value derives
|
|
35
|
+
substantially from it, hosting and support included. Everything else the MIT
|
|
36
|
+
licence grants is unchanged: use it, modify it, ship it inside your own product.
|
|
37
|
+
If your organisation only permits OSI-approved dependencies, this will not pass
|
|
38
|
+
that check.
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bakery-framework/plugin-vue",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bakery vue plugin.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bakery",
|
|
7
|
+
"bun",
|
|
8
|
+
"vue",
|
|
9
|
+
"sfc",
|
|
10
|
+
"plugin"
|
|
11
|
+
],
|
|
12
|
+
"author": "obillekyle",
|
|
13
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/obillekyle/bakery.git",
|
|
17
|
+
"directory": "packages/plugins/vue"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/obillekyle/bakery#readme",
|
|
20
|
+
"bugs": "https://github.com/obillekyle/bakery/issues",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./src/index.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"!src/**/*.test.ts",
|
|
33
|
+
"!src/tests"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"vue": "^3.5.0",
|
|
37
|
+
"@vue/compiler-sfc": "^3.5.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@vue/compiler-sfc": "^3.5.38",
|
|
41
|
+
"vue": "^3.5.38"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@bakery-framework/core": "^1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"bun": ">=1.3.14"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Bakery, hostKey } from '@bakery-framework/core/core/bakery'
|
|
2
|
+
import { fs, response, toHash } from '@bakery-framework/core/utils'
|
|
3
|
+
import type { VueMeta } from './types'
|
|
4
|
+
import { collectExportedFunctionNames } from './utils'
|
|
5
|
+
|
|
6
|
+
export interface ActionTarget {
|
|
7
|
+
id: string
|
|
8
|
+
lastMod: number
|
|
9
|
+
filePath: string
|
|
10
|
+
diskFile: Bun.BunFile
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The parts of a parsed component this gate reads. */
|
|
14
|
+
export interface ActionTargetInfo {
|
|
15
|
+
id: string
|
|
16
|
+
meta: VueMeta
|
|
17
|
+
serverScript: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Server actions are state-changing and run with the caller's session, so they
|
|
22
|
+
* must not be reachable from a cross-site navigation or a simple form/image
|
|
23
|
+
* request. Requiring POST + JSON puts them outside the set of CORS-simple
|
|
24
|
+
* requests a foreign page can issue without a preflight.
|
|
25
|
+
*/
|
|
26
|
+
export function validateActionRequest(req: Request, url: URL): Response | null {
|
|
27
|
+
if (req.method !== 'POST') {
|
|
28
|
+
return response.error('Server actions require POST', 405)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const contentType = req.headers.get('content-type') || ''
|
|
32
|
+
if (!contentType.includes('application/json')) {
|
|
33
|
+
return response.error('Server actions require a JSON body', 415)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const origin = req.headers.get('origin')
|
|
37
|
+
if (origin && origin !== url.origin) {
|
|
38
|
+
return response.error('Cross-origin server action rejected', 403)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fetchSite = req.headers.get('sec-fetch-site')
|
|
42
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
|
|
43
|
+
return response.error('Cross-site server action rejected', 403)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Gate the component a `__vue_action` request will actually execute.
|
|
51
|
+
*
|
|
52
|
+
* `__vue_file` replaces the whole execution target — id, path and server
|
|
53
|
+
* script — so every check that ran against the *route's* descriptor stopped
|
|
54
|
+
* describing the code that runs. The route was gated; the target was not, and
|
|
55
|
+
* `POST /any-public-page?__vue_action=x&__vue_file=admin/Panel.vue` ran
|
|
56
|
+
* `admin/Panel.vue`'s top-level server code with nothing to justify it.
|
|
57
|
+
*
|
|
58
|
+
* A guard: it returns the rejection rather than throwing, and it fails closed
|
|
59
|
+
* on anything it cannot resolve (convention 2).
|
|
60
|
+
*/
|
|
61
|
+
export function validateActionTarget(
|
|
62
|
+
target: ActionTargetInfo,
|
|
63
|
+
routeId: string,
|
|
64
|
+
actionName: string,
|
|
65
|
+
): Response | null {
|
|
66
|
+
// A `page-only` component is a page: reachable at its own route and nowhere
|
|
67
|
+
// else. Naming one in `__vue_file` from a *different* route is the bypass in
|
|
68
|
+
// its clearest form. `module-only` is deliberately not gated the same way —
|
|
69
|
+
// a module exists to be embedded in someone else's page, so its actions are
|
|
70
|
+
// invoked from that page's route by design, and the generated stub does
|
|
71
|
+
// exactly that.
|
|
72
|
+
if (target.meta.pageOnly && target.id !== routeId) {
|
|
73
|
+
return response.error('Not Found', 404)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Resolve the name against the *target's* own exports before a line of it
|
|
77
|
+
// runs. The generated wrapper checks this too, but the check reached only
|
|
78
|
+
// after the component's top-level statements had already executed.
|
|
79
|
+
const actions = collectExportedFunctionNames(target.serverScript).filter(
|
|
80
|
+
name => name !== 'middleware' && name !== 'default',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (!actions.includes(actionName)) {
|
|
84
|
+
return response.error('Not Found', 404)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the `__vue_file` parameter to a component inside the serve root.
|
|
92
|
+
* The value is attacker-controlled, so containment is checked after resolution
|
|
93
|
+
* rather than by inspecting the raw string.
|
|
94
|
+
*/
|
|
95
|
+
export async function resolveActionTarget(
|
|
96
|
+
actionFile: string,
|
|
97
|
+
): Promise<ActionTarget | null> {
|
|
98
|
+
if (!actionFile.endsWith('.vue')) return null
|
|
99
|
+
|
|
100
|
+
const root = fs.resolve(Bakery.serveRoot)
|
|
101
|
+
const resolvedPath = fs.resolve(root, actionFile)
|
|
102
|
+
|
|
103
|
+
if (resolvedPath !== root && !resolvedPath.startsWith(`${root}/`)) return null
|
|
104
|
+
if (fs.isForbidden(resolvedPath, root)) return null
|
|
105
|
+
|
|
106
|
+
const diskFile = Bun.file(resolvedPath)
|
|
107
|
+
if (!(await diskFile.exists())) return null
|
|
108
|
+
|
|
109
|
+
// Same derivation as the page path, so one file never gets two cache ids.
|
|
110
|
+
const relativePath = fs.relative(root, resolvedPath)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
id: toHash(hostKey(relativePath)),
|
|
114
|
+
lastMod: diskFile.lastModified,
|
|
115
|
+
filePath: resolvedPath,
|
|
116
|
+
diskFile,
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/chunks.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Bakery } from '@bakery-framework/core/core/bakery'
|
|
2
|
+
import { Logger } from '@bakery-framework/core/logger'
|
|
3
|
+
import { fs, response } from '@bakery-framework/core/utils'
|
|
4
|
+
import { ETag } from '@bakery-framework/core/utils/http'
|
|
5
|
+
import { VUE_VERSION } from './utils'
|
|
6
|
+
|
|
7
|
+
const logger = new Logger('vue')
|
|
8
|
+
|
|
9
|
+
export const VUE_CHUNK_PREFIX = '/_vue/'
|
|
10
|
+
|
|
11
|
+
const BUNDLE_DEFINES = {
|
|
12
|
+
__VUE_OPTIONS_API__: 'true',
|
|
13
|
+
__VUE_PROD_DEVTOOLS__: 'false',
|
|
14
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Serves the self-hosted Vue runtime that `Bakery.config.importMap` points at. */
|
|
18
|
+
export async function serveVueChunk(
|
|
19
|
+
path: string,
|
|
20
|
+
req: Request,
|
|
21
|
+
): Promise<Response> {
|
|
22
|
+
if (path !== `${VUE_CHUNK_PREFIX}${VUE_VERSION}.js`) {
|
|
23
|
+
return response.error('Not Found', 404)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const dir = fs.resolve(Bakery.cacheDir, 'vue-official', 'chunks')
|
|
27
|
+
const fileName = `${VUE_VERSION}.js`
|
|
28
|
+
|
|
29
|
+
let sourcePath = ''
|
|
30
|
+
try {
|
|
31
|
+
sourcePath = Bun.resolveSync('vue/dist/vue.esm-bundler.js', Bakery.root)
|
|
32
|
+
} catch {
|
|
33
|
+
return response.error('Vue not found', 404)
|
|
34
|
+
}
|
|
35
|
+
const sourceMtime = Bun.file(sourcePath).lastModified
|
|
36
|
+
|
|
37
|
+
const file = await fs.getOrCreateCachedFile(
|
|
38
|
+
dir,
|
|
39
|
+
fileName,
|
|
40
|
+
sourceMtime,
|
|
41
|
+
async () => {
|
|
42
|
+
const result = await Bun.build({
|
|
43
|
+
entrypoints: [sourcePath],
|
|
44
|
+
target: 'browser',
|
|
45
|
+
format: 'esm',
|
|
46
|
+
minify: import.meta.env.PROD,
|
|
47
|
+
define: BUNDLE_DEFINES,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
logger.log(
|
|
52
|
+
`Build failed: ${result.logs.map(l => l.message).join('; ')}`,
|
|
53
|
+
'error',
|
|
54
|
+
)
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
return result.outputs[0].arrayBuffer()
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (!file) return response.error('Vue chunk not built', 500)
|
|
62
|
+
|
|
63
|
+
const res = await ETag.sendFile(file, req)
|
|
64
|
+
res.headers.set('Cache-Control', 'public, max-age=31536000, immutable')
|
|
65
|
+
return res
|
|
66
|
+
}
|
package/src/compile.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { compileText } from '@bakery-framework/core/compiler'
|
|
2
|
+
import { Logger } from '@bakery-framework/core/logger'
|
|
3
|
+
import type { SFCStyleCompileResults } from '@vue/compiler-sfc'
|
|
4
|
+
|
|
5
|
+
const logger = new Logger('vue')
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AssembleComponentOptions,
|
|
9
|
+
CompileScriptOptions,
|
|
10
|
+
CompileScriptResult,
|
|
11
|
+
CompileStyleOptions,
|
|
12
|
+
CompileTemplateOptions,
|
|
13
|
+
CompileVueFileOptions,
|
|
14
|
+
CompileVueFileResult,
|
|
15
|
+
ParseVueOptions,
|
|
16
|
+
ParseVueResult,
|
|
17
|
+
VuePluginOptions,
|
|
18
|
+
} from './types'
|
|
19
|
+
|
|
20
|
+
let compiler: typeof import('@vue/compiler-sfc')
|
|
21
|
+
|
|
22
|
+
async function loadCompiler() {
|
|
23
|
+
if (!compiler) {
|
|
24
|
+
try {
|
|
25
|
+
compiler = await import('@vue/compiler-sfc')
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error('compiler-sfc not available. Run `bun add vue`.')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return compiler
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let vuePluginOptions: VuePluginOptions = {}
|
|
34
|
+
|
|
35
|
+
export function setVuePluginOptions(opts?: VuePluginOptions) {
|
|
36
|
+
if (opts) vuePluginOptions = opts
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveIsCustomElement(tag: string): boolean {
|
|
40
|
+
const ce = vuePluginOptions?.customElements
|
|
41
|
+
const userFn = vuePluginOptions?.compilerOptions?.isCustomElement
|
|
42
|
+
|
|
43
|
+
if (Array.isArray(ce)) {
|
|
44
|
+
if (ce.includes(tag)) return true
|
|
45
|
+
} else if (typeof ce === 'function') {
|
|
46
|
+
if (ce(tag)) return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof userFn === 'function') {
|
|
50
|
+
if (userFn(tag)) return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (tag === 'iconify-icon') return true
|
|
54
|
+
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function parseVue(
|
|
59
|
+
options: ParseVueOptions,
|
|
60
|
+
): Promise<ParseVueResult> {
|
|
61
|
+
const { content, filename } = options
|
|
62
|
+
const { parse } = await loadCompiler()
|
|
63
|
+
const result = parse(content, { filename })
|
|
64
|
+
return {
|
|
65
|
+
descriptor: result.descriptor,
|
|
66
|
+
errors: result.errors.map(e => e.message || String(e)),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function compileScriptBlock(
|
|
71
|
+
options: CompileScriptOptions,
|
|
72
|
+
): Promise<CompileScriptResult> {
|
|
73
|
+
const { descriptor, id } = options
|
|
74
|
+
|
|
75
|
+
if (!descriptor.script && !descriptor.scriptSetup) {
|
|
76
|
+
return {
|
|
77
|
+
code: 'export default {}',
|
|
78
|
+
bindings: {},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (descriptor.script && descriptor.scriptSetup) {
|
|
83
|
+
const scriptLang = descriptor.script.lang || descriptor.script.attrs?.lang
|
|
84
|
+
if (scriptLang === 'ts') {
|
|
85
|
+
// If the script block is TypeScript, we need to compile it first before passing it to the Vue compiler
|
|
86
|
+
const compiledScript = await compileText(descriptor.script.content)
|
|
87
|
+
descriptor.script.content = compiledScript
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { compileScript } = await loadCompiler()
|
|
92
|
+
const result = compileScript(descriptor, {
|
|
93
|
+
id,
|
|
94
|
+
isProd: !import.meta.env.DEV,
|
|
95
|
+
hoistStatic: true,
|
|
96
|
+
})
|
|
97
|
+
return {
|
|
98
|
+
code: await compileText(result.content),
|
|
99
|
+
bindings: result.bindings || {},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function compileTemplateBlock(
|
|
104
|
+
options: CompileTemplateOptions,
|
|
105
|
+
): Promise<string | null> {
|
|
106
|
+
const { descriptor, id, filename, bindings } = options
|
|
107
|
+
if (!descriptor.template) return null
|
|
108
|
+
const { compileTemplate } = await loadCompiler()
|
|
109
|
+
const userCompilerOptions = vuePluginOptions.compilerOptions || {}
|
|
110
|
+
const result = compileTemplate({
|
|
111
|
+
source: descriptor.template.content,
|
|
112
|
+
filename,
|
|
113
|
+
id,
|
|
114
|
+
scoped: descriptor.styles.some(s => s.scoped),
|
|
115
|
+
compilerOptions: {
|
|
116
|
+
isCustomElement: resolveIsCustomElement,
|
|
117
|
+
...userCompilerOptions,
|
|
118
|
+
...(bindings ? { bindingMetadata: bindings } : {}),
|
|
119
|
+
nodeTransforms: [
|
|
120
|
+
...((userCompilerOptions.nodeTransforms as any[]) || []),
|
|
121
|
+
(node: any) => {
|
|
122
|
+
if (node.type !== 5) return
|
|
123
|
+
const content = node.content
|
|
124
|
+
|
|
125
|
+
// Simple expression: `{{ value }}`
|
|
126
|
+
if (content.type === 4) {
|
|
127
|
+
const rawContent = content.content.trim()
|
|
128
|
+
if (rawContent && !rawContent.startsWith('_ctx.$fmt(')) {
|
|
129
|
+
content.content = `_ctx.$fmt(${rawContent})`
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Compound expression: `{{ a + b }}`, `{{ cond ? x : y }}`. These are
|
|
135
|
+
// rewritten into child arrays by the built-in transformExpression, so
|
|
136
|
+
// they need wrapping at the children level rather than as a string.
|
|
137
|
+
if (content.type === 8 && !content.__fmtWrapped) {
|
|
138
|
+
content.children = ['_ctx.$fmt(', ...content.children, ')']
|
|
139
|
+
content.__fmtWrapped = true
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (result.errors?.length) {
|
|
147
|
+
logger.log(`Template compile errors: ${result.errors.join(', ')}`, 'error')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let code = result.code
|
|
151
|
+
code = code.replace(/^export\s+/m, '')
|
|
152
|
+
code = await compileText(code)
|
|
153
|
+
return code
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function compileStyleBlock(
|
|
157
|
+
options: CompileStyleOptions,
|
|
158
|
+
): Promise<SFCStyleCompileResults> {
|
|
159
|
+
await loadCompiler()
|
|
160
|
+
return compiler.compileStyle({
|
|
161
|
+
source: options.style.content,
|
|
162
|
+
filename: 'style.css',
|
|
163
|
+
id: options.id,
|
|
164
|
+
scoped: !!options.style.scoped,
|
|
165
|
+
trim: true,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function assembleComponent(options: AssembleComponentOptions): string {
|
|
170
|
+
const { scriptCode, renderCode, isRoot, scopeId } = options
|
|
171
|
+
const COMPONENT_VAR = '__sfc__'
|
|
172
|
+
let output = scriptCode.replace(
|
|
173
|
+
/\bexport\s+default\s*/,
|
|
174
|
+
`const ${COMPONENT_VAR} = `,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (renderCode) {
|
|
178
|
+
const renderFn = renderCode.replace(/^export\s+/m, '')
|
|
179
|
+
output += `\n${renderFn}\n${COMPONENT_VAR}.render = render;`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (scopeId) {
|
|
183
|
+
output += `\n${COMPONENT_VAR}.__scopeId = ${JSON.stringify(scopeId)};`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
output += `\nexport default ${COMPONENT_VAR};`
|
|
187
|
+
|
|
188
|
+
if (isRoot) {
|
|
189
|
+
output +=
|
|
190
|
+
`\nimport { createApp } from 'vue';` +
|
|
191
|
+
`\nconst __app = createApp(${COMPONENT_VAR});` +
|
|
192
|
+
`\n__app.config.compilerOptions.isCustomElement = ${buildRuntimeCustomElementCheck()};` +
|
|
193
|
+
`\n__app.config.globalProperties.$fmt = (v) => globalThis.$fmt ? globalThis.$fmt(v) : v;` +
|
|
194
|
+
`\n__app.mount('#app');`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return output
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Mirror `resolveIsCustomElement` in the browser, so runtime-compiled templates
|
|
202
|
+
* agree with what the SFC compiler did. A predicate that closes over server-side
|
|
203
|
+
* state cannot be serialized — it falls back to the built-in tag.
|
|
204
|
+
*/
|
|
205
|
+
function buildRuntimeCustomElementCheck(): string {
|
|
206
|
+
const ce = vuePluginOptions.customElements
|
|
207
|
+
|
|
208
|
+
if (typeof ce === 'function') {
|
|
209
|
+
return `(tag) => { try { return (${ce.toString()})(tag) || tag === 'iconify-icon' } catch { return tag === 'iconify-icon' } }`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const tags = [...new Set([...(Array.isArray(ce) ? ce : []), 'iconify-icon'])]
|
|
213
|
+
return `(tag) => ${JSON.stringify(tags)}.includes(tag)`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function compileVueFile(
|
|
217
|
+
options: CompileVueFileOptions,
|
|
218
|
+
): Promise<CompileVueFileResult> {
|
|
219
|
+
const { content, filename, id, isRootScript } = options
|
|
220
|
+
const { descriptor, errors: parseErrors } = await parseVue({
|
|
221
|
+
content,
|
|
222
|
+
filename,
|
|
223
|
+
})
|
|
224
|
+
const hasScoped = descriptor.styles.some(s => s.scoped)
|
|
225
|
+
const { code: scriptCode, bindings } = await compileScriptBlock({
|
|
226
|
+
descriptor,
|
|
227
|
+
id,
|
|
228
|
+
})
|
|
229
|
+
const renderCode = await compileTemplateBlock({
|
|
230
|
+
descriptor,
|
|
231
|
+
id,
|
|
232
|
+
filename,
|
|
233
|
+
bindings,
|
|
234
|
+
})
|
|
235
|
+
const code = assembleComponent({
|
|
236
|
+
scriptCode,
|
|
237
|
+
renderCode,
|
|
238
|
+
isRoot: isRootScript,
|
|
239
|
+
scopeId: hasScoped ? id : undefined,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const styles = await Promise.all(
|
|
243
|
+
descriptor.styles.map(style => compileStyleBlock({ style, id })),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return { code, styles, errors: parseErrors }
|
|
247
|
+
}
|