web-framework-vue-composition-api-c33faa — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-framework-vue-composition-api-c33faa (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide:<script setup>for every component.ref()for primitives and anything reassigned,reactive()for an object mutated in place. Reusable stateful logic goes into ause*composable that returns an object of refs. Everything opened inonMountedis closed inonUnmounted. The 3.4+ and 3.5+ macros —defineModel(),useTemplateRef(),useId(),onWatcherCleanup()— replace whole patterns that preceded them, and destructured props need a getter wrapper inwatch().
Detailed Resources:
ref, reactive, computed and their anti-patternswatch/watchEffect or an async composable, as Patterns 3 and 5 describe.<critical_requirements>
Write components as `<script setup>`. Bindings reach the template with no return statement, and the compiler macros — defineProps, defineEmits, defineModel — only exist inside it.
Pair every `onMounted` setup with an `onUnmounted` teardown. Timers, listeners, observers and sockets outlive the component otherwise, and nothing reports it.
Pick `ref()` for primitives and anything you will reassign, `reactive()` for an object you mutate in place. A reassigned reactive variable leaves the old proxy behind, still wired to the template.
Prefix a composable with `use`. The convention is what tells a reader — and the linter — that the function may call lifecycle hooks and must run during setup.
Wrap a destructured prop in a getter for `watch()` — `watch(() => count, …)`. Passing the value itself hands watch a number, which it can never see change.
</critical_requirements>
Auto-detection: Vue 3 Composition API, script setup, ref, reactive, computed, watch, watchEffect, composables, onMounted, onUnmounted, defineProps, defineEmits, defineExpose, defineModel, useTemplateRef, useId, onWatcherCleanup, provide, inject, InjectionKey, Suspense, toRefs, toValue, MaybeRefOrGetter
Applies to:
ref, reactive, computed, watch and watchEffectHandled elsewhere:
<style scoped> block is Vue's, and which CSS approach fills it is not settled here<philosophy>
The Composition API groups code by the concern it serves rather than by the kind of thing it is. One feature's state, its derived values, its watcher and its cleanup sit together and can be lifted out whole into a composable — where the Options API scattered the same feature across data, computed, methods and mounted, and offered no way to move it.
That is what makes a composable the unit of reuse: it is a plain function that happens to call reactive primitives, so it composes, takes arguments and returns whatever shape suits — no mixin merge order, no name collisions.
</philosophy>
<patterns>
defineProps and defineEmits take type arguments, so the contract is declared once and checked at both ends.
<script setup lang="ts">
const props = defineProps<{
userId: string;
initialCount?: number;
}>();
const emit = defineEmits<{
update: [value: number];
submit: [];
}>();
const count = ref(props.initialCount ?? 0);
const doubleCount = computed(() => count.value * 2);
</script>The named-tuple emit syntax (3.3+) documents each payload in the type itself.
Full code: examples/core.md
const count = ref(0);
count.value++;
const state = reactive({
user: null as User | null,
settings: { theme: "light" },
});
state.settings.theme = "dark";.value in script, unwrapped in template. Destructuring a reactive object copies its values out of the proxy — toRefs(state) is what keeps them connected.
Full code: examples/reactivity.md
watch names its source and gives you the previous value; watchEffect tracks whatever it reads and runs immediately. onWatcherCleanup() (3.5+) cancels work the next run supersedes.
watch(searchQuery, async (newQuery, oldQuery) => {
/* … */
});
watchEffect(async () => {
if (userId.value) userData.value = await fetchUser(userId.value);
});
watch(searchQuery, async (query) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
await fetch(`/api/search?q=${query}`, { signal: controller.signal });
});A property of a reactive object is watched through a getter — watch(() => state.count, …).
Full code: examples/vue-3-5-features.md
const POLL_INTERVAL_MS = 5000;
let intervalId: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
intervalId = setInterval(fetchData, POLL_INTERVAL_MS);
});
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
});Full code: examples/lifecycle.md
A composable returns an object of refs, so a caller can destructure it without flattening the reactivity.
export function useCounter(options: UseCounterOptions = {}) {
const { initialValue = 0, max = Infinity } = options;
const count = ref(initialValue);
const isAtMax = computed(() => count.value >= max);
function increment() {
if (count.value < max) count.value++;
}
return { count, isAtMax, increment };
}An async composable takes MaybeRefOrGetter<T> inputs, normalises them with toValue(), and returns { data, error, isLoading } — so a caller can pass a ref, a getter or a plain value interchangeably.
Full code: examples/composables.md
Returns a ref that reads the prop and emits the update, replacing the defineProps + defineEmits pair the parent used to need.
<script setup lang="ts">
const model = defineModel<string>();
const firstName = defineModel<string>("firstName");
const [value, modifiers] = defineModel<string>({
set(v) {
return modifiers.capitalize ? v.charAt(0).toUpperCase() + v.slice(1) : v;
},
});
</script>Full code: examples/vue-3-5-features.md
useTemplateRef() looks up a ref by its string name, which is what makes it work with a dynamic name and inside a composable. A plain ref() matching the attribute still works for a static one.
<script setup lang="ts">
const inputRef = useTemplateRef<HTMLInputElement>("myInput");
onMounted(() => inputRef.value?.focus());
</script>
<template>
<input ref="myInput" type="text" />
</template>For a child component, defineExpose() declares its public surface, and the parent types the ref InstanceType<typeof Child>.
Full code: examples/define-expose.md, examples/vue-3-5-features.md
Generates an id that matches between server and client render, which is what a hand-rolled counter or a random string cannot do.
<script setup lang="ts">
const id = useId();
</script>
<template>
<label :for="id">Email</label>
<input :id="id" type="email" />
</template>Each call returns a different id, so call it once per field in setup — never inside a computed.
Full code: examples/vue-3-5-features.md
Destructured props stay reactive, and JavaScript default syntax replaces withDefaults().
<script setup lang="ts">
const {
title,
count = 0,
items = () => [],
} = defineProps<{
title: string;
count?: number;
items?: string[];
}>();
watch(
() => count,
(newCount) => {
/* … */
},
);
</script>The compiler rewrites each reference into a prop access, so a destructured name passed as a value — to watch, or into a function — is just the value at that instant. Hence the getter.
Full code: examples/vue-3-5-features.md
An InjectionKey<T> symbol carries the value's type from provider to consumer, so neither side casts.
// injection-keys.ts
export const THEME_KEY: InjectionKey<ThemeContext> = Symbol("theme");
// provider
provide(THEME_KEY, { theme, toggleTheme });
// consumer
const ctx = inject(THEME_KEY);
if (!ctx) throw new Error("Must be used within ThemeProvider");Full code: examples/provide-inject.md
defineAsyncComponent code-splits at the component boundary. A top-level await in <script setup> makes the component async, which requires a <Suspense> above it.
const LOADING_DELAY_MS = 200;
const LOAD_TIMEOUT_MS = 10000;
const HeavyChart = defineAsyncComponent({
loader: () => import("./components/heavy-chart.vue"),
loadingComponent: LoadingSpinner,
delay: LOADING_DELAY_MS,
timeout: LOAD_TIMEOUT_MS,
});delay is what stops the spinner flashing on a fast load. Errors from the boundary are caught with onErrorCaptured.
Full code: examples/async.md
</patterns>
<red_flags>
Breaks at runtime:
props.count++ warns and changes nothing, because props are read-only; emit an update and let the parent own the valuewatch(count, …) receives a number and never fires; wrap it in a getterreactive object destructured without toRefs() — the copies leave the proxy and stop updatingreactive variable reassigned — the template still holds the old proxyonUnmounted — the timer, listener or socket survives the componentwatch running an async request with no cleanup — a slow earlier response overwrites a fast later one; onWatcherCleanup() (3.5+) or the cleanup callback settles the orderuseId() called inside a computed — it mints a new id per evaluation, so the label and the input drift apartprovide() with a string key — the consumer gets unknown, and two features can collide on the same keySurprising behaviour:
.value written in a template is wrong; templates unwrap refs alreadyreactive object unwrap at the root, but not inside an array, a Map or a SetwatchEffect runs immediately, watch does not until immediate: truecomputed is read-only unless declared with a getter and a setterref() or reactive() if consumers should see changesonUnmounted never runs for a component that threw during setupdefineModel returns a ref, so it takes .value in script and none in the template</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.