Vue single-file components
netpack compiles Vue single-file components (.vue) natively — there is no
Node round-trip through @vue/compiler-sfc. AngleSharp (already used for HTML)
splits the file into its top-level blocks, and a native compiler turns them into
a virtual JavaScript module. Templates are precompiled to render functions at
build time, so your app does not need a compiler-included Vue build.
Nothing to configure: importing a .vue file just works.
// main.js
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
The blocks
A .vue file is parsed into its top-level <template>, <script>,
<script setup> and <style> blocks (one template and script each; any number
of styles). Every block understands a src attribute, which loads its contents
from another file (resolved relative to the .vue file):
<template src="./app.html" />
<script src="./app.js" />
<style src="./app.css" />
<script> (and <script setup>) may be written in TypeScript with lang="ts";
the types are erased the same way .ts files are.
The <script> block
A classic <script> block’s default export becomes the component. netpack
rebinds it to a local so it can attach the template and styles, keeping every
other statement (imports, helpers) verbatim:
<!-- in: -->
<script>
import { defineComponent } from 'vue';
export default defineComponent({ name: 'Hello' });
</script>
// out (simplified):
import { defineComponent } from 'vue';
const __sfc_main = defineComponent({ name: 'Hello' });
__sfc_main.render = function (_ctx, _cache) { /* … */ };
export default __sfc_main;
The <script setup> block
<script setup> is compiled to a setup() function. Imports are hoisted to
module scope, every top-level binding is returned so the template can see it,
and the compiler macros are expanded:
<!-- in: -->
<script setup>
import { ref } from 'vue';
const props = defineProps({ msg: String });
const emit = defineEmits(['change']);
const count = ref(0);
function inc() { count.value++; emit('change'); }
</script>
// out (simplified):
import { ref } from 'vue';
const __sfc_main = {
props: { msg: String },
emits: ['change'],
setup(__props, __ctx) {
const props = __props;
const emit = __ctx.emit;
const count = ref(0);
function inc() { count.value++; emit('change'); }
return { ref, props, emit, count, inc };
}
};
export default __sfc_main;
The supported macros:
defineProps(...)— hoisted to thepropsoption; the call evaluates to__props.withDefaults(defineProps(...), { … })— merged intoprops(via a tiny generated__mergeDefaultshelper).defineEmits(...)— hoisted to theemitsoption; the call evaluates to the emit function.defineExpose({ … })— becomes__ctx.expose({ … }).defineOptions({ … })— spread into the component options.
A classic <script> alongside <script setup> is allowed: its default
export contributes base options that the setup component is spread on top of
({ ...__sfc_base, setup() { … } }).
Template precompilation
The <template> is compiled to a render function built from Vue’s public
runtime helpers (h, toDisplayString, renderList, …), imported under
_vue_* aliases. Template expressions are rewritten so free identifiers resolve
against the render context — count becomes _ctx.count, while v-for items,
inline arrow parameters and JavaScript globals (Math, JSON, true, …) are
left alone.
<!-- in: -->
<template>
<p v-if="count">{{ label }}: {{ count }}</p>
<button @click="inc">+</button>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</template>
// out (simplified body of render):
_ctx.count
? _vue_h("p", null, _vue_toDisplayString(_ctx.label) + ": " + _vue_toDisplayString(_ctx.count))
: _vue_createCommentVNode("v-if", true),
_vue_h("button", { onClick: _ctx.inc }, "+"),
_vue_h(_vue_Fragment, null, _vue_renderList(_ctx.items, (item) =>
_vue_h("li", { key: item.id }, _vue_toDisplayString(item.name))))
Supported in the render compiler:
- Text interpolation
{{ … }}. v-bind/:attr(incl..prop/.attr/.camelmodifiers),v-on/@event(incl. system/key/.capture/.once/.passivemodifiers, e.g.@click.stop.prevent,@keyup.enter,@scroll.passive).v-if/v-else-if/v-else,v-for.v-show,v-html,v-text.v-model— native form controls (via thevModel*directives) and components (modelValue+onUpdate:modelValue), with.lazy/.number/.trimmodifiers.key,ref, static and dynamic attributes.- Components,
<component :is="…">(viaresolveDynamicComponent), default and named slots, and<slot>outlets.
Graceful fallback
Anything outside that subset makes netpack fall back to Vue’s runtime
compiler for that component: it attaches the raw template string as
.template instead of a precompiled .render. Your app keeps working (a
compiler-included Vue build is only needed for components that actually fall
back). Constructs that currently fall back: custom directives, v-once /
v-memo caching, and v-bind="object" spreads.
Component resolution
Component tags are resolved with resolveComponent, matching Vue’s own runtime
semantics (it looks up the components option and globally registered
components, camelizing/capitalizing as needed). For <script setup>, imported
components used by the template are auto-registered into a components
option so resolveComponent finds them.
Because the template is parsed as HTML, tag names are lowercased — so use
kebab-case for child components in templates (<my-widget>, not
<MyWidget>). resolveComponent("my-widget") still resolves a component
imported/registered as MyWidget.
The <style> blocks
Each <style> block’s CSS is injected at runtime via a <style> element.
scoped— netpack computes adata-v-*scope id and rewrites the block’s selectors to require it (.box→.box[data-v-1a2b3c]). The precompiled render is wrapped inpushScopeId/popScopeId(and the component’s__scopeIdis set) so every element it renders is stamped with the attribute.lang="scss" | "sass" | "less"— preprocessed with the same pipeline as standalone stylesheets (when the corresponding tool is enabled viapackage.json; see Styling & assets).src— loads the CSS from a file.
Requirements & limits
vuemust be available to the bundle — either bundled normally, or left external (see Import maps & externals) when you load it from a CDN.- Because templates are precompiled, a runtime-compiler Vue build is only needed for components that fall back to runtime compilation.
- Not yet supported:
defineModel, type-only props (defineProps<T>()with no runtime argument produces empty props), and<style module>.