diff --git a/.changeset/stale-steaks-brush.md b/.changeset/stale-steaks-brush.md
new file mode 100644
index 000000000..e1dd493dd
--- /dev/null
+++ b/.changeset/stale-steaks-brush.md
@@ -0,0 +1,21 @@
+---
+'@astrojs/compiler': minor
+---
+
+Add a new `annotateSourceFile` option. This option makes it so the compiler will annotate every element with its source file location. This is notably useful for dev tools to be able to provide features like a "Open in editor" button. This option is disabled by default.
+
+```html
+
+ hello world
+
+```
+
+Results in:
+
+```html
+
+ hello world
+
+```
+
+In Astro, this option is enabled only in development mode.
diff --git a/cmd/astro-wasm/astro-wasm.go b/cmd/astro-wasm/astro-wasm.go
index b9d6288d8..dda3bdd00 100644
--- a/cmd/astro-wasm/astro-wasm.go
+++ b/cmd/astro-wasm/astro-wasm.go
@@ -95,11 +95,17 @@ func makeTransformOptions(options js.Value) transform.TransformOptions {
if jsBool(options.Get("resultScopedSlot")) {
scopedSlot = true
}
+
transitionsAnimationURL := jsString(options.Get("transitionsAnimationURL"))
if transitionsAnimationURL == "" {
transitionsAnimationURL = "astro/components/viewtransitions.css"
}
+ annotateSourceFile := false
+ if jsBool(options.Get("annotateSourceFile")) {
+ annotateSourceFile = true
+ }
+
var resolvePath any = options.Get("resolvePath")
var resolvePathFn func(string) string
if resolvePath.(js.Value).Type() == js.TypeFunction {
@@ -132,6 +138,7 @@ func makeTransformOptions(options js.Value) transform.TransformOptions {
ResultScopedSlot: scopedSlot,
ScopedStyleStrategy: scopedStyleStrategy,
TransitionsAnimationURL: transitionsAnimationURL,
+ AnnotateSourceFile: annotateSourceFile,
}
}
diff --git a/internal/printer/print-to-js.go b/internal/printer/print-to-js.go
index fcf644104..ef15ad866 100644
--- a/internal/printer/print-to-js.go
+++ b/internal/printer/print-to-js.go
@@ -9,6 +9,7 @@ import (
"fmt"
"sort"
"strings"
+ "unicode"
. "github.com/withastro/compiler/internal"
"github.com/withastro/compiler/internal/handler"
@@ -448,6 +449,25 @@ func render1(p *printer, n *Node, opts RenderOptions) {
// Note: if we encounter "slot" NOT inside a component, that's fine
// These should be preserved in the output
p.printAttribute(a, n)
+ } else if a.Key == "data-astro-source-file" {
+ p.printAttribute(a, n)
+ var l []int
+ if n.FirstChild != nil && len(n.FirstChild.Loc) > 0 {
+ start := n.FirstChild.Loc[0].Start
+ if n.FirstChild.Type == TextNode {
+ start += len(n.Data) - len(strings.TrimLeftFunc(n.Data, unicode.IsSpace))
+ }
+ l = p.builder.GetLineAndColumnForLocation(loc.Loc{Start: start})
+ } else if len(n.Loc) > 0 {
+ l = p.builder.GetLineAndColumnForLocation(n.Loc[0])
+ }
+ if len(l) > 0 {
+ p.printAttribute(Attribute{
+ Key: "data-astro-source-loc",
+ Type: QuotedAttribute,
+ Val: fmt.Sprintf("%d:%d", l[0], l[1]),
+ }, n)
+ }
p.addSourceMapping(n.Loc[0])
} else {
p.printAttribute(a, n)
diff --git a/internal/transform/scope-html.go b/internal/transform/scope-html.go
index 15f0d1dfe..7094cbceb 100644
--- a/internal/transform/scope-html.go
+++ b/internal/transform/scope-html.go
@@ -5,6 +5,7 @@ import (
"strings"
astro "github.com/withastro/compiler/internal"
+ "golang.org/x/net/html/atom"
)
func ScopeElement(n *astro.Node, opts TransformOptions) {
@@ -27,6 +28,14 @@ func AddDefineVars(n *astro.Node, values []string) bool {
return false
}
+func AnnotateElement(n *astro.Node, opts TransformOptions) {
+ if n.Type == astro.ElementNode && !n.Component && !n.Fragment {
+ if _, noScope := NeverScopedElements[n.Data]; !noScope {
+ annotateElement(n, opts)
+ }
+ }
+}
+
var NeverScopedElements map[string]bool = map[string]bool{
"Fragment": true,
"base": true,
@@ -48,6 +57,17 @@ var NeverScopedSelectors map[string]bool = map[string]bool{
":root": true,
}
+func annotateElement(n *astro.Node, opts TransformOptions) {
+ if n.DataAtom == atom.Html {
+ return
+ }
+ n.Attr = append(n.Attr, astro.Attribute{
+ Key: "data-astro-source-file",
+ Type: astro.QuotedAttribute,
+ Val: opts.Filename,
+ })
+}
+
func injectDefineVars(n *astro.Node, values []string) {
definedVars := "$$definedVars"
for i, attr := range n.Attr {
diff --git a/internal/transform/transform.go b/internal/transform/transform.go
index 9cf788d5c..f5388344d 100644
--- a/internal/transform/transform.go
+++ b/internal/transform/transform.go
@@ -30,6 +30,7 @@ type TransformOptions struct {
TransitionsAnimationURL string
ResolvePath func(string) string
PreprocessStyle interface{}
+ AnnotateSourceFile bool
}
func Transform(doc *astro.Node, opts TransformOptions, h *handler.Handler) *astro.Node {
@@ -59,6 +60,9 @@ func Transform(doc *astro.Node, opts TransformOptions, h *handler.Handler) *astr
if n.DataAtom == a.Head && !IsImplicitNode(n) {
doc.ContainsHead = true
}
+ if opts.AnnotateSourceFile {
+ AnnotateElement(n, opts)
+ }
})
if len(definedVars) > 0 && !didAddDefinedVars {
for _, style := range doc.Styles {
diff --git a/internal/transform/transform_test.go b/internal/transform/transform_test.go
index 78ecf9718..e85ee351c 100644
--- a/internal/transform/transform_test.go
+++ b/internal/transform/transform_test.go
@@ -471,3 +471,48 @@ func TestCompactTransform(t *testing.T) {
})
}
}
+
+func TestAnnotation(t *testing.T) {
+ tests := []struct {
+ name string
+ source string
+ want string
+ }{
+ {
+ name: "basic",
+ source: `Hello world!
`,
+ want: `Hello world!
`,
+ },
+ {
+ name: "no components",
+ source: `Hello world!`,
+ want: `Hello world!`,
+ },
+ {
+ name: "injects root",
+ source: ``,
+ want: ``,
+ },
+ }
+ var b strings.Builder
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ b.Reset()
+ doc, err := astro.Parse(strings.NewReader(tt.source))
+ if err != nil {
+ t.Error(err)
+ }
+ h := handler.NewHandler(tt.source, "/src/pages/index.astro")
+ Transform(doc, TransformOptions{
+ AnnotateSourceFile: true,
+ Filename: "/src/pages/index.astro",
+ NormalizedFilename: "/src/pages/index.astro",
+ }, h)
+ astro.PrintToSource(&b, doc)
+ got := strings.TrimSpace(b.String())
+ if tt.want != got {
+ t.Errorf("\nFAIL: %s\n want: %s\n got: %s", tt.name, tt.want, got)
+ }
+ })
+ }
+}
diff --git a/packages/compiler/src/shared/types.ts b/packages/compiler/src/shared/types.ts
index 360d10240..75af6f0a5 100644
--- a/packages/compiler/src/shared/types.ts
+++ b/packages/compiler/src/shared/types.ts
@@ -56,6 +56,7 @@ export interface TransformOptions {
transitionsAnimationURL?: string;
resolvePath?: (specifier: string) => Promise;
preprocessStyle?: (content: string, attrs: Record) => null | Promise;
+ annotateSourceFile?: boolean;
}
export type ConvertToTSXOptions = Pick;