Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/long-swans-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/compiler": patch
---

fix: strip dead imports for client:only components
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import Component from '../components';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import {
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import Component from '../components';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import { Component } from '../components';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import * as components from '../components';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import defaultImport from '../components/ui-1';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components/ui-1'], hydrationDirectives: new Set(['only']), hoisted: [] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import { namedImport } from '../components/ui-2';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components/ui-2'], hydrationDirectives: new Set(['only']), hoisted: [] });

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

[TestPrinter/client:only_preserves_import_when_binding_used_in_frontmatter - 1]
## Input

```
/-/-/-/
import Component from '../components';
console.log(Component.name);
/-/-/-/
<Component client:only />
```

## Output

```js
import {
Fragment,
render as $$render,
createAstro as $$createAstro,
createComponent as $$createComponent,
renderComponent as $$renderComponent,
renderHead as $$renderHead,
maybeRenderHead as $$maybeRenderHead,
unescapeHTML as $$unescapeHTML,
renderSlot as $$renderSlot,
mergeSlots as $$mergeSlots,
addAttribute as $$addAttribute,
spreadAttributes as $$spreadAttributes,
defineStyleVars as $$defineStyleVars,
defineScriptVars as $$defineScriptVars,
renderTransition as $$renderTransition,
createTransitionScope as $$createTransitionScope,
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import Component from '../components';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });

const $$Component = $$createComponent(($$result, $$props, $$slots) => {

console.log(Component.name);

return $$render`${$$renderComponent($$result,'Component',null,{"client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})}`;
}, undefined, undefined);
export default $$Component;
```
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

[TestPrinter/client:only_preserves_mixed_binding_import - 1]
## Input

```
/-/-/-/
import Component, { helper } from '../components';
const data = helper();
/-/-/-/
<Component client:only />
<div>{data}</div>
```

## Output

```js
import {
Fragment,
render as $$render,
createAstro as $$createAstro,
createComponent as $$createComponent,
renderComponent as $$renderComponent,
renderHead as $$renderHead,
maybeRenderHead as $$maybeRenderHead,
unescapeHTML as $$unescapeHTML,
renderSlot as $$renderSlot,
mergeSlots as $$mergeSlots,
addAttribute as $$addAttribute,
spreadAttributes as $$spreadAttributes,
defineStyleVars as $$defineStyleVars,
defineScriptVars as $$defineScriptVars,
renderTransition as $$renderTransition,
createTransitionScope as $$createTransitionScope,
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import Component, { helper } from '../components';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });

const $$Component = $$createComponent(($$result, $$props, $$slots) => {

const data = helper();

return $$render`${$$renderComponent($$result,'Component',null,{"client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})}
${$$maybeRenderHead($$result)}<div>${data}</div>`;
}, undefined, undefined);
export default $$Component;
```
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

[TestPrinter/client:only_preserves_mixed_import_from_same_specifier - 1]
## Input

```
/-/-/-/
import Component from '../components';
import { helper } from '../components';
const data = helper();
/-/-/-/
<Component client:only />
<div>{data}</div>
```

## Output

```js
import {
Fragment,
render as $$render,
createAstro as $$createAstro,
createComponent as $$createComponent,
renderComponent as $$renderComponent,
renderHead as $$renderHead,
maybeRenderHead as $$maybeRenderHead,
unescapeHTML as $$unescapeHTML,
renderSlot as $$renderSlot,
mergeSlots as $$mergeSlots,
addAttribute as $$addAttribute,
spreadAttributes as $$spreadAttributes,
defineStyleVars as $$defineStyleVars,
defineScriptVars as $$defineScriptVars,
renderTransition as $$renderTransition,
createTransitionScope as $$createTransitionScope,
renderScript as $$renderScript,
createMetadata as $$createMetadata
} from "http://localhost:3000/";
import { helper } from '../components';

import * as $$module1 from '../components';

export const $$metadata = $$createMetadata(import.meta.url, { modules: [{ module: $$module1, specifier: '../components', assert: {} }], hydratedComponents: [], clientOnlyComponents: ['../components'], hydrationDirectives: new Set(['only']), hoisted: [] });

const $$Component = $$createComponent(($$result, $$props, $$slots) => {

const data = helper();

return $$render`${$$renderComponent($$result,'Component',null,{"client:only":true,"client:component-hydration":"only","client:component-path":($$metadata.resolvePath("../components")),"client:component-export":"default"})}
${$$maybeRenderHead($$result)}<div>${data}</div>`;
}, undefined, undefined);
export default $$Component;
```
---
63 changes: 63 additions & 0 deletions internal/printer/print-to-js.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package printer
import (
"bytes"
"fmt"
"regexp"
"sort"
"strings"
"unicode"
Expand Down Expand Up @@ -168,10 +169,72 @@ func render1(p *printer, n *Node, opts RenderOptions) {
}
render := js_scanner.HoistImports([]byte(c.Data))
if len(render.Hoisted) > 0 {
// Build set of local binding names used exclusively by
// client:only components. The compiler passes null to
// $$renderComponent for these, so the import is dead code
// and can be stripped to keep Rollup from traversing the
// component's dependency tree during prerender.
clientOnlyBindings := make(map[string]bool)
for _, node := range n.Parent.ClientOnlyComponentNodes {
name := node.Data
if idx := strings.Index(name, "."); idx != -1 {
name = name[:idx]
}
clientOnlyBindings[name] = true
}
for _, node := range n.Parent.HydratedComponentNodes {
name := node.Data
if idx := strings.Index(name, "."); idx != -1 {
name = name[:idx]
}
delete(clientOnlyBindings, name)
}
// Also exclude server-rendered components (no client: directive)
for _, comp := range n.Parent.ServerComponents {
name := comp.LocalName
if name == "" {
name = comp.Specifier
}
if idx := strings.Index(name, "."); idx != -1 {
name = name[:idx]
}
delete(clientOnlyBindings, name)
}

// Collect frontmatter body text to check for binding usage
var bodyText []byte
if len(clientOnlyBindings) > 0 {
for _, b := range render.Body {
bodyText = append(bodyText, b...)
}
}

for i, hoisted := range render.Hoisted {
if len(bytes.TrimSpace(hoisted)) == 0 {
continue
}
// Strip an import only when every binding it introduces
// is a client-only-exclusive component identifier that
// does not appear in the frontmatter body.
if len(clientOnlyBindings) > 0 {
_, stmt := js_scanner.NextImportStatement(hoisted, 0)
if len(stmt.Imports) > 0 && !stmt.IsType {
allDead := true
for _, imp := range stmt.Imports {
if !clientOnlyBindings[imp.LocalName] {
allDead = false
break
}
if regexp.MustCompile(`\b` + regexp.QuoteMeta(imp.LocalName) + `\b`).Match(bodyText) {
allDead = false
break
}
}
if allDead {
continue
}
}
}
hoistedLoc := render.HoistedLocs[i]
p.printTextWithSourcemap(string(hoisted)+"\n", loc.Loc{Start: start + hoistedLoc.Start})
}
Expand Down
27 changes: 27 additions & 0 deletions internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,33 @@ import Component from '../components';
<Component test="c" client:only />
</body>
</html>`,
},
{
name: "client:only preserves mixed import from same specifier",
source: `---
import Component from '../components';
import { helper } from '../components';
const data = helper();
---
<Component client:only />
<div>{data}</div>`,
},
{
name: "client:only preserves import when binding used in frontmatter",
source: `---
import Component from '../components';
console.log(Component.name);
---
<Component client:only />`,
},
{
name: "client:only preserves mixed binding import",
source: `---
import Component, { helper } from '../components';
const data = helper();
---
<Component client:only />
<div>{data}</div>`,
},
{
name: "iframe",
Expand Down