feat: allow snippets to be exported from module scripts

pull/14315/head
Dominic Gannaway 2 years ago
parent efc65d4e0c
commit 61a2943454

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow snippets to be exported from module scripts

@ -10,17 +10,43 @@ const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
/**
* @param {string} source
* @param {boolean} typescript
* @param {boolean} is_script
*/
export function parse(source, typescript) {
export function parse(source, typescript, is_script) {
const parser = typescript ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(source);
const ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
locations: true
});
// @ts-ignore
const parse_statement = parser.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
// component instead, so we need to ensure that Acorn doesn't throw
// an error in these cases
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
return v;
};
}
let ast;
try {
ast = parser.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 13,
locations: true
});
} finally {
if (is_script) {
// @ts-ignore
parser.prototype.parseStatement = parse_statement;
}
}
if (typescript) amend(source, ast);
add_comments(ast);

@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) {
let ast;
try {
ast = acorn.parse(source, parser.ts);
ast = acorn.parse(source, parser.ts, true);
} catch (err) {
parser.acorn_error(err);
}

@ -425,6 +425,7 @@ export function analyze_component(root, source, options) {
binding_groups: new Map(),
slot_names: new Map(),
top_level_snippets: [],
module_level_snippets: [],
css: {
ast: root.css,
hash: root.css

@ -483,7 +483,7 @@ export function client_component(analysis, options) {
}
}
body = [...imports, ...body];
body = [...imports, ...analysis.module_level_snippets, ...body];
const component = b.function_declaration(
b.id(analysis.name),

@ -4,6 +4,7 @@
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { can_hoist_snippet } from '../../utils.js';
import { get_value } from './shared/declarations.js';
/**
@ -80,10 +81,16 @@ export function SnippetBlock(node, context) {
}
const declaration = b.const(node.expression, snippet);
const local_scope = context.state.scope;
const can_hoist = can_hoist_snippet(node, local_scope);
// Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
context.state.analysis.top_level_snippets.push(declaration);
if (can_hoist) {
context.state.analysis.module_level_snippets.push(declaration);
} else {
context.state.analysis.top_level_snippets.push(declaration);
}
} else {
context.state.init.push(declaration);
}

@ -1,7 +1,8 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
/** @import { ComponentContext, } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { can_hoist_snippet } from '../../utils.js';
/**
* @param {AST.SnippetBlock} node
@ -17,6 +18,11 @@ export function SnippetBlock(node, context) {
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
// TODO hoist where possible
context.state.init.push(fn);
const can_hoist = can_hoist_snippet(node, context.state.scope);
if (context.path.length === 1 && context.path[0].type === 'Fragment' && can_hoist) {
context.state.hoisted.push(fn);
} else {
context.state.init.push(fn);
}
}

@ -1,5 +1,6 @@
/** @import { Context } from 'zimmerframe' */
/** @import { TransformState } from './types.js' */
/** @import { Scope } from '../scope.js' */
/** @import { AST, Binding, Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler' */
/** @import { Node, Expression, CallExpression } from 'estree' */
import {
@ -452,3 +453,34 @@ export function transform_inspect_rune(node, context) {
return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg));
}
}
/**
* @param {AST.SnippetBlock} node
* @param {Scope} scope
*/
export function can_hoist_snippet(node, scope) {
let can_hoist = true;
ref_loop: for (const [reference] of scope.references) {
const local_binding = scope.get(reference);
if (local_binding) {
if (local_binding.node === node.expression) {
continue;
}
/** @type {Scope | null} */
let current_scope = local_binding.scope;
while (current_scope !== null) {
if (current_scope === scope) {
continue ref_loop;
}
current_scope = current_scope.parent;
}
can_hoist = false;
break;
}
}
return can_hoist;
}

@ -63,6 +63,7 @@ export interface ComponentAnalysis extends Analysis {
inject_styles: boolean;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
top_level_snippets: VariableDeclaration[];
module_level_snippets: VariableDeclaration[];
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Map<string, AST.SlotElement>;

@ -82,4 +82,4 @@
<svelte:fragment let:should_stay slot="cool stuff">
cool
</svelte:fragment>
</Comp>
</Comp>

@ -0,0 +1,9 @@
<script module>
export {
foo
}
</script>
{#snippet foo(a, b)}
Hello world {a + b}
{/snippet}

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true // Render in dev mode to check that the validation error is not thrown
},
html: `Hello world 3`
});

@ -0,0 +1,5 @@
<script>
import { foo } from './Child.svelte';
</script>
{@render foo(1, 2)}

@ -2,17 +2,17 @@ import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
import TextInput from './Child.svelte';
var root = $.template(`<!> `, 1);
const snippet = ($$anchor) => {
$.next();
export default function Bind_component_snippet($$anchor) {
const snippet = ($$anchor) => {
$.next();
var text = $.text("Something");
var text = $.text("Something");
$.append($$anchor, text);
};
$.append($$anchor, text);
};
var root = $.template(`<!> `, 1);
export default function Bind_component_snippet($$anchor) {
let value = $.state('');
const _snippet = snippet;
var fragment = root();

@ -1,14 +1,13 @@
import * as $ from "svelte/internal/server";
import TextInput from './Child.svelte';
function snippet($$payload) {
$$payload.out += `<!---->Something`;
}
export default function Bind_component_snippet($$payload) {
let value = '';
const _snippet = snippet;
function snippet($$payload) {
$$payload.out += `<!---->Something`;
}
let $$settled = true;
let $$inner_payload;

Loading…
Cancel
Save