Skip to content

Commit 9aa0ed3

Browse files
authored
fix: handle nested script tags (#10416)
fixes #9484
1 parent 90f8b63 commit 9aa0ed3

File tree

8 files changed

+171
-28
lines changed

8 files changed

+171
-28
lines changed

.changeset/loud-ravens-drop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: handle nested script tags

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ export function client_component(source, analysis, options) {
8484
},
8585
legacy_reactive_statements: new Map(),
8686
metadata: {
87-
template_needs_import_node: false,
87+
context: {
88+
template_needs_import_node: false,
89+
template_contains_script_tag: false
90+
},
8891
namespace: options.namespace,
8992
bound_contenteditable: false
9093
},

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,22 @@ export interface ComponentClientTransformState extends ClientTransformState {
4747
readonly template: string[];
4848
readonly metadata: {
4949
namespace: Namespace;
50-
/** `true` if the HTML template needs to be instantiated with `importNode` */
51-
template_needs_import_node: boolean;
5250
bound_contenteditable: boolean;
51+
/**
52+
* Stuff that is set within the children of one `create_block` that is relevant
53+
* to said `create_block`. Shouldn't be destructured or otherwise spread unless
54+
* inside `create_block` to keep the object reference intact (it's also nested
55+
* within `metadata` for this reason).
56+
*/
57+
context: {
58+
/** `true` if the HTML template needs to be instantiated with `importNode` */
59+
template_needs_import_node: boolean;
60+
/**
61+
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
62+
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
63+
*/
64+
template_contains_script_tag: boolean;
65+
};
5366
};
5467
readonly preserve_whitespace: boolean;
5568

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,10 @@ function create_block(parent, name, nodes, context) {
10651065
after_update: [],
10661066
template: [],
10671067
metadata: {
1068-
template_needs_import_node: false,
1068+
context: {
1069+
template_needs_import_node: false,
1070+
template_contains_script_tag: false
1071+
},
10691072
namespace,
10701073
bound_contenteditable: context.state.metadata.bound_contenteditable
10711074
}
@@ -1085,10 +1088,14 @@ function create_block(parent, name, nodes, context) {
10851088
node: id
10861089
});
10871090

1088-
const callee = namespace === 'svg' ? '$.svg_template' : '$.template';
1089-
10901091
context.state.hoisted.push(
1091-
b.var(template_name, b.call(callee, b.template([b.quasi(state.template.join(''), true)], [])))
1092+
b.var(
1093+
template_name,
1094+
b.call(
1095+
get_template_function(namespace, state),
1096+
b.template([b.quasi(state.template.join(''), true)], [])
1097+
)
1098+
)
10921099
);
10931100

10941101
body.push(
@@ -1097,7 +1104,7 @@ function create_block(parent, name, nodes, context) {
10971104
b.call(
10981105
'$.open',
10991106
b.id('$$anchor'),
1100-
b.literal(!state.metadata.template_needs_import_node),
1107+
b.literal(!state.metadata.context.template_needs_import_node),
11011108
template_name
11021109
)
11031110
),
@@ -1138,12 +1145,14 @@ function create_block(parent, name, nodes, context) {
11381145
// special case — we can use `$.comment` instead of creating a unique template
11391146
body.push(b.var(id, b.call('$.comment', b.id('$$anchor'))));
11401147
} else {
1141-
const callee = namespace === 'svg' ? '$.svg_template' : '$.template';
1142-
11431148
state.hoisted.push(
11441149
b.var(
11451150
template_name,
1146-
b.call(callee, b.template([b.quasi(state.template.join(''), true)], []), b.true)
1151+
b.call(
1152+
get_template_function(namespace, state),
1153+
b.template([b.quasi(state.template.join(''), true)], []),
1154+
b.true
1155+
)
11471156
)
11481157
);
11491158

@@ -1153,7 +1162,7 @@ function create_block(parent, name, nodes, context) {
11531162
b.call(
11541163
'$.open_frag',
11551164
b.id('$$anchor'),
1156-
b.literal(!state.metadata.template_needs_import_node),
1165+
b.literal(!state.metadata.context.template_needs_import_node),
11571166
template_name
11581167
)
11591168
)
@@ -1217,6 +1226,23 @@ function create_block(parent, name, nodes, context) {
12171226
return body;
12181227
}
12191228

1229+
/**
1230+
*
1231+
* @param {import('#compiler').Namespace} namespace
1232+
* @param {import('../types.js').ComponentClientTransformState} state
1233+
* @returns
1234+
*/
1235+
function get_template_function(namespace, state) {
1236+
const contains_script_tag = state.metadata.context.template_contains_script_tag;
1237+
return namespace === 'svg'
1238+
? contains_script_tag
1239+
? '$.svg_template_with_script'
1240+
: '$.svg_template'
1241+
: contains_script_tag
1242+
? '$.template_with_script'
1243+
: '$.template';
1244+
}
1245+
12201246
/**
12211247
*
12221248
* @param {import('../types.js').ComponentClientTransformState} state
@@ -1847,6 +1873,9 @@ export const template_visitors = {
18471873
context.state.template.push('<!>');
18481874
return;
18491875
}
1876+
if (node.name === 'script') {
1877+
context.state.metadata.context.template_contains_script_tag = true;
1878+
}
18501879

18511880
const metadata = context.state.metadata;
18521881
const child_metadata = {
@@ -1885,7 +1914,7 @@ export const template_visitors = {
18851914
// custom element until the template is connected to the dom, which would
18861915
// cause problems when setting properties on the custom element.
18871916
// Therefore we need to use importNode instead, which doesn't have this caveat.
1888-
metadata.template_needs_import_node = true;
1917+
metadata.context.template_needs_import_node = true;
18891918
}
18901919

18911920
for (const attribute of node.attributes) {

packages/svelte/src/internal/client/reconciler.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ export function create_fragment_from_html(html) {
99
return elem.content;
1010
}
1111

12+
/**
13+
* Creating a document fragment from HTML that contains script tags will not execute
14+
* the scripts. We need to replace the script tags with new ones so that they are executed.
15+
* @param {string} html
16+
*/
17+
export function create_fragment_with_script_from_html(html) {
18+
var content = create_fragment_from_html(html);
19+
var scripts = content.querySelectorAll('script');
20+
for (const script of scripts) {
21+
var new_script = document.createElement('script');
22+
for (var i = 0; i < script.attributes.length; i++) {
23+
new_script.setAttribute(script.attributes[i].name, script.attributes[i].value);
24+
}
25+
new_script.textContent = script.textContent;
26+
/** @type {Node} */ (script.parentNode).replaceChild(new_script, script);
27+
}
28+
return content;
29+
}
30+
1231
/**
1332
* @param {Array<import('./types.js').TemplateNode> | import('./types.js').TemplateNode} current
1433
* @param {null | Element} parent_element
@@ -63,27 +82,28 @@ export function remove(current) {
6382
}
6483

6584
/**
85+
* Creates the content for a `@html` tag from its string value,
86+
* inserts it before the target anchor and returns the new nodes.
6687
* @template V
67-
* @param {Element | Text | Comment} dom
88+
* @param {Element | Text | Comment} target
6889
* @param {V} value
6990
* @param {boolean} svg
7091
* @returns {Element | Comment | (Element | Comment | Text)[]}
7192
*/
72-
export function reconcile_html(dom, value, svg) {
73-
hydrate_block_anchor(dom);
93+
export function reconcile_html(target, value, svg) {
94+
hydrate_block_anchor(target);
7495
if (current_hydration_fragment !== null) {
7596
return current_hydration_fragment;
7697
}
7798
var html = value + '';
7899
// Even if html is the empty string we need to continue to insert something or
79100
// else the element ordering gets out of sync, resulting in subsequent values
80101
// not getting inserted anymore.
81-
var target = dom;
82102
var frag_nodes;
83103
if (svg) {
84104
html = `<svg>${html}</svg>`;
85105
}
86-
var content = create_fragment_from_html(html);
106+
var content = create_fragment_with_script_from_html(html);
87107
if (svg) {
88108
content = /** @type {DocumentFragment} */ (/** @type {unknown} */ (content.firstChild));
89109
}

packages/svelte/src/internal/client/render.js

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ import {
2323
PassiveDelegatedEvents,
2424
DelegatedEvents,
2525
AttributeAliases,
26-
namespace_svg,
27-
namespace_html
26+
namespace_svg
2827
} from '../../constants.js';
29-
import { create_fragment_from_html, insert, reconcile_html, remove } from './reconciler.js';
28+
import {
29+
create_fragment_from_html,
30+
create_fragment_with_script_from_html,
31+
insert,
32+
reconcile_html,
33+
remove
34+
} from './reconciler.js';
3035
import {
3136
render_effect,
3237
destroy_signal,
33-
get,
3438
is_signal,
3539
push_destroy_fn,
3640
execute_effect,
@@ -78,35 +82,71 @@ export function empty() {
7882

7983
/**
8084
* @param {string} html
81-
* @param {boolean} is_fragment
85+
* @param {boolean} return_fragment
8286
* @returns {() => Node}
8387
*/
8488
/*#__NO_SIDE_EFFECTS__*/
85-
export function template(html, is_fragment) {
89+
export function template(html, return_fragment) {
8690
/** @type {undefined | Node} */
8791
let cached_content;
8892
return () => {
8993
if (cached_content === undefined) {
9094
const content = create_fragment_from_html(html);
91-
cached_content = is_fragment ? content : /** @type {Node} */ (child(content));
95+
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
96+
}
97+
return cached_content;
98+
};
99+
}
100+
101+
/**
102+
* @param {string} html
103+
* @param {boolean} return_fragment
104+
* @returns {() => Node}
105+
*/
106+
/*#__NO_SIDE_EFFECTS__*/
107+
export function template_with_script(html, return_fragment) {
108+
/** @type {undefined | Node} */
109+
let cached_content;
110+
return () => {
111+
if (cached_content === undefined) {
112+
const content = create_fragment_with_script_from_html(html);
113+
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
92114
}
93115
return cached_content;
94116
};
95117
}
96118

97119
/**
98120
* @param {string} svg
99-
* @param {boolean} is_fragment
121+
* @param {boolean} return_fragment
122+
* @returns {() => Node}
123+
*/
124+
/*#__NO_SIDE_EFFECTS__*/
125+
export function svg_template(svg, return_fragment) {
126+
/** @type {undefined | Node} */
127+
let cached_content;
128+
return () => {
129+
if (cached_content === undefined) {
130+
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
131+
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
132+
}
133+
return cached_content;
134+
};
135+
}
136+
137+
/**
138+
* @param {string} svg
139+
* @param {boolean} return_fragment
100140
* @returns {() => Node}
101141
*/
102142
/*#__NO_SIDE_EFFECTS__*/
103-
export function svg_template(svg, is_fragment) {
143+
export function svg_template_with_script(svg, return_fragment) {
104144
/** @type {undefined | Node} */
105145
let cached_content;
106146
return () => {
107147
if (cached_content === undefined) {
108148
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
109-
cached_content = is_fragment ? content : /** @type {Node} */ (child(content));
149+
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
110150
}
111151
return cached_content;
112152
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { test } from '../../test';
2+
3+
/**
4+
* @type {any[]}
5+
*/
6+
let log;
7+
/**
8+
* @type {typeof console.log}}
9+
*/
10+
let original_log;
11+
12+
export default test({
13+
skip_if_ssr: 'permanent',
14+
skip_if_hydrate: 'permanent', // log patching will be too late
15+
before_test() {
16+
log = [];
17+
original_log = console.log;
18+
console.log = (...v) => {
19+
log.push(...v);
20+
};
21+
},
22+
after_test() {
23+
console.log = original_log;
24+
},
25+
async test({ assert }) {
26+
assert.deepEqual(log, ['init']);
27+
}
28+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
<script>
3+
console.log('init');
4+
</script>
5+
</div>

0 commit comments

Comments
 (0)