Source locations
CORA’s build plugin writes each host element’s place in the project’s source into the page, as its data-cora-source attribute. A host element is one the page renders as itself, such as a <button>, an <img> or a custom element like <share-widget>, rather than a component such as <Header>. Which tags are host elements follows how your framework compiles them, as each framework’s section below says.
cora scan and cora-mcp’s scans read the attribute, so a finding on a stamped element names the file and line to fix. In an Angular app, Angular’s own compiler writes each element’s place instead, as its data-ng-source-location attribute, which the scans read too.
Choose your bundler
Section titled “Choose your bundler”The plugin comes with @vispero/cora, from the subpath of your bundler, in a project that has installed cora. Each takes no options: the project’s CORA config says when it stamps.
| Bundler | Subpath | Stamps | Left unset, stamps | Where to list it | Registered by cora init |
|---|---|---|---|---|---|
| Vite, and Nuxt, Astro and SvelteKit | @vispero/cora/vite |
JSX, in .js files too, Lit templates, Vue, Svelte and Astro components |
under the dev server | anywhere | yes |
| webpack, Rspack, Rsbuild | @vispero/cora/webpack, /rspack, /rsbuild |
JSX, in .js files too, Lit templates, Vue templates and Svelte components |
in development mode |
anywhere | yes |
| Rollup | @vispero/cora/rollup |
JSX, in .js files too, Lit templates, Vue templates and Svelte components |
in watch mode | anywhere | yes |
| Rolldown | @vispero/cora/rolldown |
JSX, in .js files too, Lit templates, Vue templates and Svelte components |
in watch mode | anywhere | yes |
| esbuild | @vispero/cora/esbuild |
JSX, in .js files too, and Lit templates |
never: set sourceStamps |
after the plugins that load your JSX or Lit files | no: a build is a script |
| Angular CLI | none: Angular’s compiler option | Angular templates, in .html or .svg files and inline |
not by sourceStamps: in the configurations whose tsconfig turns it on, development as cora init sets it up |
a tsconfig the configuration names | yes |
cora init registers the plugin in the config the bundler loads, where its default export, or a CommonJS config’s module.exports, is an object literal or defineConfig of one, imported from the bundler’s own package, a function that returns one, or a name for one that the file declares once, as webpack’s own scaffold writes, or, for webpack, Rspack, Rollup and Rolldown, a list of them: a CommonJS config requires the plugin’s .default, such as require('@vispero/cora/webpack').default. It leaves any other config to you, and names this page, such as one whose function returns its config from a branch, one it imports, a list with a config it cannot edit, one with a getter or a setter, or one whose plugins, or an item of them, a later line could change. In a project that lists one of these bundlers or frameworks but holds none of its configs in the working directory, it points you here instead, unless a tool that brings the bundler along is set up there: Vite, Nuxt or Astro for Rollup and Rolldown, and Rsbuild for Rspack.
Adding the plugin
Section titled “Adding the plugin”In a Vite project, register @vispero/cora/vite in the Vite config, as cora init does. In a Nuxt or Astro project, it goes in the framework’s own config, as Nuxt and Astro show. In a project that lists one of these without a config of it in the working directory, such as one a build names with --config, cora init says it did not set the plugin up, and points you here.
import react from '@vitejs/plugin-react';import sourceStamps from '@vispero/cora/vite';import { defineConfig } from 'vite';
export default defineConfig({ plugins: [react(), sourceStamps()],});It takes no options: the project’s CORA config says when it stamps. It runs ahead of Vite’s own JSX transform, and of plugins that set only enforce: 'pre', wherever it is listed. So it sees each file as it is on disk.
It stamps JSX in .jsx and .tsx files, and in .js files where your Vite config compiles JSX there: under the dev server, through an oxc option whose include and exclude take in .js files and whose lang is jsx, and in a build, through build.rolldownOptions.moduleTypes, set to read .js as jsx; before Vite 8, through an esbuild option whose include and exclude take in .js files and whose loader is jsx, in both. A .js file with no JSX passes through as it is.
It supports Vite 5.0.0 or later, the first that tells a plugin whether Vite is previewing a build. Each framework’s section below gives the oldest release of its compiler that the plugin reads.
Register it beside your framework’s own Vite plugin, as react() is above. It works with these frameworks and their plugins:
| Framework | Vite plugin |
|---|---|
| React | @vitejs/plugin-react |
| Preact | @preact/preset-vite |
| Solid | vite-plugin-solid |
| React Router | reactRouter() from @react-router/dev/vite, in framework mode, server-rendered pages included |
| Vue | @vitejs/plugin-vue, or unplugin-vue’s Vite plugin |
| Svelte | @sveltejs/vite-plugin-svelte, or SvelteKit’s sveltekit() |
| Nuxt | Nuxt’s own Vite setup, through vite.plugins in nuxt.config |
| Astro | Astro’s own Vite setup, through vite.plugins in astro.config |
| Lit | none: Vite builds Lit’s modules itself, so the plugin is the only one listed |
In a Vue project, the plugin stamps the elements of each single-file component’s template. It parses them with the compiler and the template options of the project’s Vue plugin, @vitejs/plugin-vue or unplugin-vue, so it reads each template as the project’s build does. It needs Vue 3.4.3 or later, the first whose parse takes isCustomElement and parses a template again when the options change, with @vitejs/plugin-vue 4.5.0 or later, or unplugin-vue 4.5.0 or later, each the first for Vite 5. A Vite config that lists neither, or a project on an older Vue, gets no stamps in its .vue files, and Vite prints a warning once.
- A tag Vue takes as a component is never stamped, such as
<SaveButton>, nor are<slot>,<component>and a<template>that holds a directive, such asv-if. - A tag written in kebab-case, such as
<sl-button>, is a component to Vue unless the Vue plugin’sisCustomElementnames it, as Vue needs it named to render it as itself. Once named, it is stamped:
import vue from '@vitejs/plugin-vue';import sourceStamps from '@vispero/cora/vite';import { defineConfig } from 'vite';
export default defineConfig({ plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('sl-') } } }), sourceStamps(), ],});- A template in another language, such as Pug, or in a file of its own through
src, gets no stamps.
Vue with JSX
Section titled “Vue with JSX”In the JSX that Vue 3’s JSX compiler, @vue/babel-plugin-jsx, or its SWC port, swc-plugin-vue-jsx, builds, in .jsx and .tsx files, and in .js files where the bundler stamps JSX in them, the plugin stamps the tags Vue’s JSX compiles as HTML or SVG elements, such as <button> or <path>:
- Any other name, such as
<router-link>or a component registered assave-button, is a component to Vue, and gets no stamp. The element the component renders keeps its own stamp when it is a host element in your project’s source, or otherwise takes its nearest stamped ancestor’s location, as an element of a component fromnode_modulesdoes. - A custom element gets no stamp in Vue’s JSX, even one that the Vue JSX compiler’s
isCustomElementnames: no Vue JSX plugin publishes that option, and CORA reads no Babel config. A finding on it takes its nearest stamped ancestor’s location. In a.vuetemplate, the same element is stamped where CORA reads theisCustomElementof the Vue plugin or loader that builds it. - The plugin knows a build compiles Vue’s JSX from its Vue JSX plugin:
@vitejs/plugin-vue-jsxunder Vite,unplugin-vue-jsxunder Vite, Rollup and Rolldown, and@rsbuild/plugin-vue-jsxunder Rsbuild, for every environment or the one it is registered for. In a build that lists none of them, under any bundler but esbuild, it knows from your package.json listing@vue/babel-plugin-jsx,unplugin-vue-jsx,@vue/cli-plugin-babel,@vue/babel-preset-apporswc-plugin-vue-jsx: the nearest package.json from the bundler’s root, such as webpack’scontext, upward. That covers webpack and Rspack, whose plugins it cannot see, and a build whose JSX Babel compiles with no Vue JSX plugin, such as through@rollup/plugin-babel. A change to which of them it lists takes effect when the bundler starts again. Withswc-plugin-vue-jsx, which keeps its own lists of HTML and SVG elements, a tag that those lists and Vue’s do not share may get no stamp, and then takes its nearest stamped ancestor’s location.contextdefaults to the working directory, so in a workspace whose build runs from its root, such aswebpack --config packages/app/webpack.config.js, setcontextto the app’s directory, or list the package in the package.json at the root. In a Vue JSX setup it can see neither way, such as a preset of your own that brings the compiler, or another compiler of Vue’s JSX, a component written as a lowercase or hyphenated tag is stamped, as in a React project, and a finding on the element it renders names the line that uses the component. - This then applies to all of the build’s JSX, even a file that the Vue JSX plugin’s
includeorexcludeleaves to another plugin, such as React’s beside it: there a React file’s tags that are no HTML or SVG element, its custom elements among them, get no stamp. - Under esbuild, a build with
unplugin-vue-jsxgets no JSX stamps, since esbuild loads each.jsxand.tsxfile through one plugin, Vue’s. The plugin leaves them to it, wherever it is listed, so a Lit template in one of them gets no stamps either, and esbuild prints a warning as the build starts. - When a build lists a Vue JSX plugin but neither the plugin nor your project’s
vueresolves from the bundler’s root, so the names of Vue’s elements cannot be loaded, the bundler prints a warning once and none of the build’s JSX is stamped. Install them in the project to fix it. Where only your package.json says the build compiles Vue’s JSX, and no@vue/sharedcan be loaded, the plugin stamps every lowercase or hyphenated tag instead, as in a React project, with no warning.
It needs @vue/babel-plugin-jsx 1.3.0 or later, the first that takes a tag as an element by Vue’s own lists of elements, which the plugin reads. An older one’s lists differ from Vue’s on a few rare tags: <slot>, <math>, <search>, <menuitem> and SVG’s font elements, among others, get no stamp, and SVG 2’s <feDropShadow>, <discard> and mesh elements are stamped though the older compiler takes them as components. It supports @vitejs/plugin-vue-jsx 3.1.0 or later, the first for Vite 5, unplugin-vue-jsx 0.6.0 or later, the first with an entry for Rolldown, and @rsbuild/plugin-vue-jsx 1.0.1 or later, the first for Rsbuild 1.
Nuxt configures Vite in nuxt.config, and reads no Vite config, so register the plugin in its vite.plugins, as cora init does:
import sourceStamps from '@vispero/cora/vite';
export default defineNuxtConfig({ vite: { plugins: [sourceStamps()] },});The plugin then stamps the project’s .vue files as in any Vue project, server-rendered pages and the client’s alike, and takes custom elements from vue.compilerOptions.isCustomElement, which Nuxt hands to its Vue plugin.
Svelte
Section titled “Svelte”In a Svelte or SvelteKit project, the plugin stamps the elements of each component. It parses them with the project’s own Svelte, as Vite’s root finds it, before any preprocessor, such as vitePreprocess, has changed the file. It needs Svelte 5.0.0 or later, the first with the syntax tree it reads. A project with no such Svelte that Vite’s root can load gets no stamps in its .svelte files, and Vite prints a warning once.
- A custom element, such as
<sl-button>, is stamped, as Svelte renders it as itself. - An element Svelte’s parse does not take as a regular element is never stamped: a component, a
<slot>that takes a parent’s content, thesvelte:elements,<svelte:element>among them, and the<title>that<svelte:head>holds. - A file whose name holds
{or}is named as it is written, as in every other file. - A component whose markup Svelte’s parse cannot read, written for a preprocessor, gets no stamps. Pug in a
<template lang="pug">parses: the<template>gets a stamp, and its Pug lines get none.
Astro configures Vite in astro.config, so register the plugin in its vite.plugins, as cora init does:
import sourceStamps from '@vispero/cora/vite';import { defineConfig } from 'astro/config';
export default defineConfig({ vite: { plugins: [sourceStamps()] },});The plugin parses each .astro file’s template with Astro 7’s compiler, @astrojs/compiler-rs, which Astro 7 installs. An Astro 6 project adds it as a development dependency, such as with npm install -D @astrojs/compiler-rs. It needs @astrojs/compiler-rs 0.1.0 or later. A project with neither, or with an older compiler, gets no stamps in its .astro files, and Vite prints a warning once.
- An element written with a lowercase name, a custom element included, is stamped, as Astro renders it as itself.
- A component,
Fragment,<slot>,<script>and<style>are not stamped: Astro fills a slot, bundles a script, which an attribute of the plugin’s would make it leave as written, and lifts a style’s rules out of the page’s markup, so it renders no element of its own. - A framework component an Astro page renders, such as a React or Vue island, is stamped in its own file, by the plugin’s transform for that framework’s files, where the project’s Astro integration for it builds them.
- Pages written as
.md,.mdxor.htmlfiles get no stamps: the plugin reads only.astrofiles of Astro’s own syntax. - Astro 7’s compiler refuses some markup that Astro 6’s own compiler takes, such as a list item that only the next one closes. On Astro 6, a file of that kind gets no stamps, and no warning.
In a Lit project, the plugin stamps the elements of each template tagged with the html, svg or mathml a file imports from lit, lit/html.js, lit-html, lit-element or lit-element/lit-element.js, or from lit/static-html.js or lit-html/static.js, in the project’s own .js, .mjs, .ts, .mts, .jsx and .tsx files, under each bundler plugin in the table above. It reads each template’s static parts as markup, as the browser reads a Lit template, so an element in a component’s shadow root and one in the light DOM are each located at their tag in the template. Vite needs no plugin of its own for Lit, so its config lists CORA’s alone:
import sourceStamps from '@vispero/cora/vite';import { defineConfig } from 'vite';
export default defineConfig({ plugins: [sourceStamps()],});- A custom element, such as
<sl-button>, is stamped, as Lit renders it as itself. - The stamp is a plain attribute in a template’s static text, just before the tag’s
>, after its attributes and bindings. Expressions, such as${this.label}or@click=${this.save}, are never changed, since their values are what your app renders. - An element whose tag name is an expression gets no stamp, such as
<${tag}>or a tag built withliteralfromlit/static-html.js, since its tag is not in your file. The rest of its template is stamped. - An
svgtemplate’s elements are read as SVG, and amathmltemplate’s as MathML, as Lit reads them. Acsstemplate, a template tagged with any other function, and a plain template literal are left as they are. - In an
htmltemplate, nothing inside a<textarea>,<title>,<style>or<script>, or inside a comment, is stamped, since the browser reads it as text. - A file that declares another
html, such as a function parameter of that name, gets no stamps in the templates tagged with that name, since the plugin cannot tell which of the two each one means. - A class with decorators is read whether it uses the standard decorators or TypeScript’s
experimentalDecorators. - Markup that is not in a template’s text gets no stamp, such as HTML rendered with
unsafeHTMLor written byunsafeStatic; a finding there takes its nearest stamped ancestor’s location. - A template tagged with an
htmlthat a module of your own re-exports from Lit, such as a design system’s, gets no stamps, since the plugin reads each file alone. Import the tag from Lit’s own modules in the files that hold your templates. - A template whose static text holds an escape other than
\\,\`,\$,\{or\}, such as\",\nor\x3c, gets no stamps: such an escape can move a tag’s end, or open a tag, in what Lit renders, where a stamp would change your page, and reading every escape as JavaScript does is not worth its cost for so rare a template. - A Lit template in a
.vue,.svelteor.astrofile’s script gets no stamps, since the plugin reads only that file’s own template there.
webpack, Rspack and Rsbuild
Section titled “webpack, Rspack and Rsbuild”In a webpack project, register @vispero/cora/webpack among the config’s plugins:
import sourceStamps from '@vispero/cora/webpack';
export default { mode: 'development', // ...your loaders, such as babel-loader or swc-loader for JSX plugins: [sourceStamps()],};In an Rspack project, register @vispero/cora/rspack the same way, among the config’s plugins:
import sourceStamps from '@vispero/cora/rspack';
export default { mode: 'development', // ...your loaders, such as Rspack's builtin:swc-loader for JSX plugins: [sourceStamps()],};In an Rsbuild project, register @vispero/cora/rsbuild among the Rsbuild config’s plugins:
import { defineConfig } from '@rsbuild/core';import { pluginReact } from '@rsbuild/plugin-react';import sourceStamps from '@vispero/cora/rsbuild';
export default defineConfig({ plugins: [pluginReact(), sourceStamps()],});Each adds CORA’s loader to the config’s rules once every plugin has applied, in Rsbuild to the Rspack config of each environment Rsbuild builds, after tools.rspack, where it runs before every loader the config’s rules list, enforce: 'pre' ones that other plugins add included, wherever CORA’s plugin is listed, so it sees each file as it is on disk, before your JSX compiler does. A request whose inline loaders turn the config’s off, such as one starting !!, gets no stamps. cora init edits a config that exports an object literal, a function that returns one, a name for one or a list of them, each through its bundler’s defineConfig too.
It supports webpack 5.61.0 or later, the first that builds on the Node releases CORA supports, Rspack 1.0.0 or later, and Rsbuild 1.0.1 or later, the first Rsbuild 1 on Rspack 1.
- It stamps JSX in
.jsx,.tsxand.jsfiles, since webpack projects often write JSX in.jsfiles, those written with Flow’s types included, as a Flow preset compiles them, and Lit templates in the script files that section names,.tsfiles included, before your TypeScript loader reads them. A.jsfile with neither JSX nor a Lit template passes through as it is, and so does one that parses neither as JavaScript with JSX nor with Flow’s types, with no stamps and no warning, since a.jsfile need not be JSX at all. JSX that Vue’s JSX compiler builds is stamped as Vue with JSX says. A persistent cache is kept apart by which Vue JSX package your package.json lists. - It stamps the templates of
.vuefiles thatvue-loaderbuilds, under webpack and Rspack, and that Rsbuild’s Vue plugin builds withrspack-vue-loader, or withvue-loaderbefore the plugin’s 1.2.0. It parses each with the compiler and thecompilerOptionsof the Vue loader that builds the file, so a custom element itsisCustomElementnames is stamped, as under Vite; under Rsbuild, give it through the Vue plugin’svueLoaderOptions. It needs Vue 3.4.3 or later, withvue-loader16.8.2 or later, the first that builds under Rspack too, orrspack-vue-loader17.4.4 or later; a project on an older Vue, or whose Vue loader’s compiler CORA cannot load, gets no stamps in its.vuefiles, and the bundler prints a warning once. The limits under Vue on a Pug template and a<template src>hold here too. - It stamps the elements of
.sveltefiles thatsvelte-loaderbuilds, with your project’s own Svelte, 5.0.0 or later, as under Vite. It stamps no.astrofile, since Astro builds with Vite. - A module you import as its text or its address is left as the file holds it, so a page that shows it shows no stamp: one whose query is
raw,urlorinline, or whose module type is not JavaScript, such as a.jsxfile imported withwith { type: 'text' }, which webpack and Rspack load asasset/source. - Left unset, the setting stamps in the
developmentmode, and not inproductionornone. A webpack or Rspack config with nomodebuilds asproduction, so it carries no stamps unless the setting istrue. A dev server or--watchstamps as its mode says; by default,rsbuild devbuilds indevelopmentandrsbuild buildinproduction, unlessmodeorNODE_ENVsays otherwise. - A config that runs several compilers, such as a client and a server build, or Rsbuild’s environments, stamps each by its own mode.
- It reads the setting for each
contextand bundler a process builds with, in development and not, from the project root it finds from thatcontext, and reads it again once the CORA config orCORA_SOURCE_STAMPSchanges, appears or goes, so a watch’s next build and a restarted dev server take the edit. Each module depends on each file a CORA config would be, the one there and, where the bundler offers it, the ones that are not, so a watch and a persistent cache build it again when the config changes, appears or is fixed, and the plugin addsCORA_SOURCE_STAMPSto a persistent cache’sversion, so a build with it set and one without never share their modules. A persistent cache tracks nothing else the setting may come from: if yourcora.config.tstakessourceStampsfrom another environment variable or from a module it imports, clear the cache when that value changes, or setCORA_SOURCE_STAMPSinstead. It takes a change to the Vue loader’scompilerOptionsonly where it builds again when the file that sets them changes, as for the loader’s own output: list that file, such as your bundler’s config, in the cache’s build dependencies.
Rollup, Rolldown and esbuild
Section titled “Rollup, Rolldown and esbuild”In a Rollup project, register @vispero/cora/rollup among the config’s plugins:
import sourceStamps from '@vispero/cora/rollup';
export default { input: 'src/main.jsx', // ...and the plugin that compiles your JSX, before or after it plugins: [sourceStamps()],};In a Rolldown project, register @vispero/cora/rolldown the same way:
import sourceStamps from '@vispero/cora/rolldown';
export default { input: 'src/main.jsx', // Rolldown compiles JSX itself, after its plugins plugins: [sourceStamps()],};cora init appends the plugin to the config’s plugins, as it does for every bundler.
It supports Rollup 2.78.0 or later, the first that takes a plugin’s hook written as an object, and Rolldown 1.0.0 or later, its first full release.
- Rollup and Rolldown stamp JSX in
.jsxand.tsxfiles, and in.jsfiles, which Rollup’sjsxoption, from 4.24.0, or a JSX plugin compiles in any module, and Rolldown where itsmoduleTypesoption reads.jsasjsx. A.jsfile with neither JSX nor a Lit template passes through as it is. Both stamp Lit templates in the script files that section names, and the templates of.vuefiles, on Vue 3.4.3 or later, and the elements of.sveltefiles, with your project’s own Svelte, 5.0.0 or later. Neither stamps.astrofiles, since Astro builds with Vite. Both stamp JSX that Vue’s JSX compiler builds as Vue with JSX says. - With
unplugin-vuein the config, CORA parses a.vuefile with its compiler and template options, as under Vite, so a custom element itsisCustomElementnames is stamped. With another Vue plugin, CORA parses with your project’s own Vue, where a custom element counts as a component and gets no stamp. As under webpack, a project on an older Vue, or with none CORA can find, gets no stamps in its.vuefiles, and the bundler prints a warning once. The limits under Vue on a Pug template and a<template src>hold either way. - Under Rolldown, a module its
moduleTypesoption loads as anything butjs,jsx,tsortsx, such astext, is left as the file holds it. - CORA’s plugin asks Rollup and Rolldown to run its transform first, with
order: 'pre', so it reads each file as it is on disk, before the plugins that compile your JSX, TypeScript, Vue or Svelte, wherever it is listed. Rolldown compiles JSX itself, after its plugins. A plugin listed before CORA’s whose own transform also asks to run first, or one that changes a file as it loads it, runs before it: the files it changes carry no stamps, and the bundler prints a warning naming each, but a.jsfile it leaves holding neither a<nor an import from Lit, or a.mjs,.tsor.mtsfile holding no import from Lit, which gets none. @rollup/plugin-typescriptcompiles each.tsand.tsxfile as it loads it, before any plugin’s transform runs, so under it those files carry no stamps, and Rollup prints the warning that a plugin before CORA’s changed them, wherever CORA’s is listed. CORA’s plugin cannot load them first instead, since Rollup takes a file’s load from one plugin, and the TypeScript plugin would then compile nothing. Compile TypeScript with a plugin that transforms each file instead, such asrollup-plugin-esbuild, listed after CORA’s.- Left unset, the setting stamps in watch mode,
--watch, and not in a build. Rollup reads it from the project root it finds from the working directory, and Rolldown from itscwd, as each build starts, a watch’s rebuilds included, and a watch builds again when the CORA config changes. Concurrent builds that share one plugin instance share one reading of the setting and of the build’s plugins: call the plugin’s function once for each build when their settings, working directories, or Vue or Vue JSX plugins differ.
esbuild
Section titled “esbuild”In an esbuild project, which builds through a script, register @vispero/cora/esbuild among the build’s plugins:
import sourceStamps from '@vispero/cora/esbuild';import * as esbuild from 'esbuild';
await esbuild.build({ entryPoints: ['src/main.jsx'], bundle: true, outdir: 'dist', plugins: [sourceStamps()],});cora init does not register it, since an esbuild build is a script. It supports esbuild 0.19.7 or later, the first that gives a plugin an import’s attributes.
- It stamps JSX in
.jsxand.tsxfiles, and in.jsfiles where your build’sloaderoption reads JSX there, such asloader: { '.js': 'jsx' }, Lit templates in the script files that section names, and no.vue,.svelteor.astrofile: esbuild loads a.vueor.sveltefile through one plugin, the framework’s own. A.jsfile with no JSX is left as it is. esbuild calls the plugin for every script file it loads, your dependencies’ included, and the plugin answers each it would not stamp from the file’s path alone, without reading it. A script file that the build’sloaderoption, for the longest extension it names that the file’s name ends with, such as.raw.jsxbefore.jsx, or an import’stypeattribute, loads as anything butjs,jsx,tsortsx, such astext, is left to esbuild as it is. A file it stamps, it loads with the loader the option names, or the one its extension takes. - esbuild loads each file through the first plugin that gives its contents. CORA’s gives a file’s contents only when it stamps the file, and leaves every other file to the plugins after it. List it after any plugin of yours that loads a
.jsxor.tsxfile, a.jsfile holding JSX, or a script file holding Lit templates, so that plugin still runs; the files it loads carry no stamps, with no warning. Withunplugin-vue-jsx, it leaves your.jsxand.tsxfiles to that plugin wherever it is listed, and warns once that it stamps none (Vue with JSX). - esbuild tells a plugin nothing of whether a build is for development, so the setting left unset stamps nothing. Set
sourceStampstotrue, orCORA_SOURCE_STAMPS=1in the script that serves or builds for development. It reads the setting from the project root it finds fromabsWorkingDir, or the working directory, as each build starts, a rebuild included; each build reads its own, so builds that share apluginsarray each take their own setting.
Angular
Section titled “Angular”The Angular CLI’s builders take no plugin of a project’s own that reaches its templates: Angular’s compiler reads each template itself. Angular 22.1.4 or later writes each element’s place on its own, when the compiler option enableTemplateSourceLocations is on, and CORA’s scans read it. Turn it on for the builds you scan, the development configuration’s, as cora init does: a tsconfig beside the application’s own, which extends it,
{ "extends": "./tsconfig.app.json", "compilerOptions": { "rootDir": "." }, "angularCompilerOptions": { "enableTemplateSourceLocations": true }}and the development configuration of the application’s build target pointed at it:
"configurations": { "development": { "optimization": false, "tsConfig": "tsconfig.cora.json" }}rootDir names the repository root from the tsconfig’s directory, such as "../.." for a workspace in apps/web, since Angular names each template’s file from it. Use rootDir, not rootDirs: under rootDirs, a template you edit under ng serve in a workspace below the repository root never reloads. TypeScript then reports TS6059 for a compiled source outside the repository. cora init writes both files for each application of the workspace, the tsconfig under a comment that marks it as CORA’s, so a later run rewrites it when a release changes what it writes, and leaves it once you edit it. A later cora init takes a tsconfig.cora.json you wrote by hand as set up only while it holds exactly the settings that release writes, however laid out; otherwise it leaves the workspace as it is and says so. Commit tsconfig.cora.json with angular.json: a checkout without it fails ng serve.
enableTemplateSourceLocations is an internal option of Angular’s: angular.dev does not list it, and a minor release of Angular may change or drop it. CORA’s own Angular fixture builds with the Angular its lockfile installs, so a change shows there first.
ng serve, andng build --configuration development, then writedata-ng-source-locationon each element of the application’s templates, in a template’s own.htmlor.svgfile and in an inlinetemplatewritten as one string alike: a component’s own element, such as<app-header>, included, and<ng-container>and<ng-template>, which render no element, left out. An element of an inline template built from an expression, such as a template literal with${}, a constant or a concatenation, gets no location, since Angular names the component’s.tsfile with offsets into the template’s evaluated text, which the file does not hold there. It gets no call site either: the scan cannot tell its location from one an edit made stale, and a stale location never falls back to an ancestor’s, which would name another component’s line. A scan reads a location only from a.html,.svgor.tsfile in the repository, by its name and by the file a link leads it to, so a page cannot make a scan read a secret file, such as.env. An element whose location names a file of another kind is taken as one with no location, so it takes its nearest ancestor’s location as its call site, unless that one too names a file of another kind. A page can still name any.html,.svgor.tsfile there, and a finding then shows the text from a<at the offset it names to the next>, wherever that file holds a tag-like run of text at that place. A production build keeps the application’s own tsconfig, and carries none.sourceStampsandCORA_SOURCE_STAMPSdo not switch Angular’s locations: the tsconfig a configuration names does. A production build keeps them off, since its configuration names the application’s own tsconfig, and pointing it at CORA’s would ship each element’s place to your users. To scan a production build at its source in CI, add a configuration that names CORA’s tsconfig alone, and build with it afterproduction, which it overrides only there; the same caveat as the option applies:
"configurations": { "cora": { "tsConfig": "tsconfig.cora.json" }}ng build --configuration production,coraScan that build, and deploy one built without cora, which carries no locations.
cora initsets nothing up in a project whose Angular is older than 22.1.4, which has no such option, in one whose packages are not installed yet, where it cannot tell the version, in one installed through Yarn’s Plug’n’Play, since it reads the version fromnode_modules, which such an install does not have, or in one that has noangular.jsonin the working directory, such as an Nx workspace. On Plug’n’Play, set it up by hand as this section shows. It leaves the whole workspace to you, and names this page, when an application’sbuildtarget names notsConfigor has nodevelopmentconfiguration, when its tsconfig, or one it extends, setsrootDirsalready, when its directory holds atsconfig.cora.jsonof another’s, or when two applications with different tsconfigs share a directory; its notice names the application. Change what the notice names and runcora initagain, which sets up the workspace once no application stops it.- Angular names a file in lower case when it builds on a file system that ignores case, as on macOS and Windows. A scan finds the file by its real name wherever it runs, unless two files of a directory differ only by case: then it reads the one whose name matches Angular’s exactly, and none where neither does. Give one of the two files another name.
- Angular’s own compiler reads the templates, so a template that does not parse fails the build with Angular’s error, as it would without the option.
When it stamps
Section titled “When it stamps”The project’s config says when the plugin stamps, through sourceStamps. Left unset, it stamps in development, as Choose your bundler gives for each bundler, and not in a build. true stamps every build, and false none. To stamp the builds a CI job scans, for example:
import { defineConfig } from '@vispero/cora/config';
export default defineConfig({ baseUrl: 'http://localhost:4173', sourceStamps: true,});CORA_SOURCE_STAMPSreplaces the config’s value for one build:CORA_SOURCE_STAMPS=1 npx vite build, or the same before any bundler’s build, builds a stamped app whatever the config says.npx cora infolists whether it is set.- Under Vite’s dev server, when it stamps, it stamps every page the server sends, the client’s and the server-rendered alike.
- Under Vitest, which runs a project’s Vite plugins in mode
test, it never stamps. So the markup a test renders, and its snapshots, carry no stamps.
The plugin reads the setting from the config in the project root, which it finds from the bundler’s root, such as Vite’s root or webpack’s context, as cora finds it from where it runs (Configuration). So it loads cora.config.ts in the bundler’s process. Under Vite, an edit to the setting takes effect when the dev server or the build starts again; under webpack’s family, at the next build once the config file or CORA_SOURCE_STAMPS has changed, though not once only a module the config imports has; under Rollup, Rolldown and esbuild, at the next build.
A config it cannot read never fails the build, and nor does a CORA_SOURCE_STAMPS that is not a switch. The bundler prints one warning that names the error’s code, once per build under Rollup, Rolldown and esbuild, which read the setting as each build starts, and the plugin stamps as if the setting were left out. The same config or variable fails cora scan until it is fixed.
What it stamps
Section titled “What it stamps”- It stamps the files Choose your bundler gives for each bundler, in the project’s repository, and none in
node_modulesor outside the repository. - A file that another plugin or loader has already changed gets no stamps, since their positions would not be the file’s. The bundler prints a warning that names the file, once. Under esbuild, which loads a file through one plugin, a file another plugin loads never reaches CORA’s, and no warning names it, but for the one warning of a build with
unplugin-vue-jsx.
The attribute
Section titled “The attribute”Each stamped element carries one value:
<main class="app" data-cora-source="1:src/App.tsx:12:5:12:27:YgYYvutAeZ2zM47gyndF"></main>Its fields, separated by colons, are:
- the version of the value’s format,
1; - the file, from the repository root, with forward slashes on every platform;
- the line and column where the element’s opening tag starts, at its
<; - the line and column just after the opening tag’s closing
>; - a digest of the file’s text, 20 characters, which tells whether the file has changed since the stamp was written.
- Lines and columns count from 1. A column counts characters as JavaScript does, so a tab is one column and an emoji is two.
- The repository root is the nearest directory, from the bundler’s root up, whose
.gitis a file, as in a worktree or a submodule, or a directory that holdsHEAD. Outside a git repository, the repository root is the project root. When there is neither, the bundler prints a warning, and nothing is stamped. - The attribute is markup like any other
data-attribute: it changes nothing the page does.
Findings at their source
Section titled “Findings at their source”cora scan and cora-mcp’s scans read the stamp of each element a finding names. A finding whose element has a stamp that holds carries its place in the source, as location.source in the results file:
{ "file": "src/App.tsx", "line": 12, "column": 5, "endLine": 12, "endColumn": 27, "openingTag": "<main className=\"app\">", "resolution": "element"}The file and positions are the stamp’s, or for Angular’s attribute those of the opening tag it leads to in the file now. openingTag is the tag’s source text, with each run of whitespace written as one space. resolution says whose tag it is:
element: the element’s own, when it has a stamp of its own.call-site: its nearest stamped ancestor’s, when it has none, such as an element that a component fromnode_modulesrenders, or one inside a shadow root. That is the closest element in the project’s source that encloses the element: the custom element that holds the shadow root, or the host element around your use of a third-party component, or on an Angular app, where a component’s own element carries a location, that component’s tag. The elements such a component renders share that location. A custom element in Vue’s JSX has no stamp, so the call site of an element in its shadow root is the closest stamped element around the custom element.
A stamp holds only while its file has the text the stamp was written from, which its digest tells. Angular’s attribute carries no digest: it holds while its file still has an opening tag of the element’s name where Angular found it, at the same offset, line and column. So an edit below an element leaves its location in place, and an edit above it that moves it, or one that renames its tag, drops it until the app is rebuilt; an edit within its tag that moves nothing keeps it, with the tag’s text as the file holds it now. Any edit that leaves an opening tag of the element’s name at that offset, line and column is taken for the element, such as two rows of equal length swapped, a tag of the same shape moved there, the element commented out by a <!-- written over its indentation, or the element commented out with an identical one written where it was. A scan finds the repository root as the plugin does, but from the project’s root, and reads each file as it is on disk now. So it runs in a checkout of the files the app was built from.
A finding carries no location:
- when its element’s file has been edited since the page was built, until the app is rebuilt, or under the dev server until the page loads again after the server sent the edited file; for Angular’s attribute, when the edit moved or renamed the element’s tag;
- when the file is missing;
- when neither the element nor an ancestor in its document has a stamp;
- when another release’s plugin built the page with stamps this release does not read, until the app is built again;
- in cora-mcp started outside a project, which reads no files.
Issues and fingerprints
Section titled “Issues and fingerprints”A finding with a source location also carries a source fingerprint, built from its rule, its file and its opening tag. Lines are not part of it, so an edit elsewhere in the file keeps it. A change to the opening tag, or a move of the file, gives a new one.
An issue is then the findings that share that fingerprint, rather than one element on one page:
- An element of a shared component is one issue on every page it appears on. The issue’s ID in cora-mcp’s results is that fingerprint.
- Two places where your code uses a third-party component, under host elements whose opening tags read the same in one file, are one issue too.
- cora-mcp’s results give each issue its source location and
callSites: the number of distinctcall-sitelocations its findings are at. So an agent knows how many places a fix at the call site must reach.
When an app starts being built with stamps, its issues regroup, so their IDs change once. A baseline written from a build without stamps still accepts the same violations. See How a violation is matched.
Code scanning
Section titled “Code scanning”In the SARIF log of the report directory, an issue with a source location is located at its file and lines, rather than at the repository root. So GitHub code scanning shows its alert on the line to fix.
Code scanning tracks an alert by the issue’s fingerprint. So the first upload from a stamped build closes each regrouped issue’s alert at the repository root, and opens one at its source line.