Skip to content

Commit

Permalink
fix: handle nested script tags (#10416)
Browse files Browse the repository at this point in the history
fixes #9484
  • Loading branch information
dummdidumm authored Feb 6, 2024
1 parent 90f8b63 commit 9aa0ed3
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-ravens-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

fix: handle nested script tags
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export function client_component(source, analysis, options) {
},
legacy_reactive_statements: new Map(),
metadata: {
template_needs_import_node: false,
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace: options.namespace,
bound_contenteditable: false
},
Expand Down
17 changes: 15 additions & 2 deletions packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,22 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly template: string[];
readonly metadata: {
namespace: Namespace;
/** `true` if the HTML template needs to be instantiated with `importNode` */
template_needs_import_node: boolean;
bound_contenteditable: boolean;
/**
* Stuff that is set within the children of one `create_block` that is relevant
* to said `create_block`. Shouldn't be destructured or otherwise spread unless
* inside `create_block` to keep the object reference intact (it's also nested
* within `metadata` for this reason).
*/
context: {
/** `true` if the HTML template needs to be instantiated with `importNode` */
template_needs_import_node: boolean;
/**
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
*/
template_contains_script_tag: boolean;
};
};
readonly preserve_whitespace: boolean;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,10 @@ function create_block(parent, name, nodes, context) {
after_update: [],
template: [],
metadata: {
template_needs_import_node: false,
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
Expand All @@ -1085,10 +1088,14 @@ function create_block(parent, name, nodes, context) {
node: id
});

const callee = namespace === 'svg' ? '$.svg_template' : '$.template';

context.state.hoisted.push(
b.var(template_name, b.call(callee, b.template([b.quasi(state.template.join(''), true)], [])))
b.var(
template_name,
b.call(
get_template_function(namespace, state),
b.template([b.quasi(state.template.join(''), true)], [])
)
)
);

body.push(
Expand All @@ -1097,7 +1104,7 @@ function create_block(parent, name, nodes, context) {
b.call(
'$.open',
b.id('$$anchor'),
b.literal(!state.metadata.template_needs_import_node),
b.literal(!state.metadata.context.template_needs_import_node),
template_name
)
),
Expand Down Expand Up @@ -1138,12 +1145,14 @@ function create_block(parent, name, nodes, context) {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment', b.id('$$anchor'))));
} else {
const callee = namespace === 'svg' ? '$.svg_template' : '$.template';

state.hoisted.push(
b.var(
template_name,
b.call(callee, b.template([b.quasi(state.template.join(''), true)], []), b.true)
b.call(
get_template_function(namespace, state),
b.template([b.quasi(state.template.join(''), true)], []),
b.true
)
)
);

Expand All @@ -1153,7 +1162,7 @@ function create_block(parent, name, nodes, context) {
b.call(
'$.open_frag',
b.id('$$anchor'),
b.literal(!state.metadata.template_needs_import_node),
b.literal(!state.metadata.context.template_needs_import_node),
template_name
)
)
Expand Down Expand Up @@ -1217,6 +1226,23 @@ function create_block(parent, name, nodes, context) {
return body;
}

/**
*
* @param {import('#compiler').Namespace} namespace
* @param {import('../types.js').ComponentClientTransformState} state
* @returns
*/
function get_template_function(namespace, state) {
const contains_script_tag = state.metadata.context.template_contains_script_tag;
return namespace === 'svg'
? contains_script_tag
? '$.svg_template_with_script'
: '$.svg_template'
: contains_script_tag
? '$.template_with_script'
: '$.template';
}

/**
*
* @param {import('../types.js').ComponentClientTransformState} state
Expand Down Expand Up @@ -1847,6 +1873,9 @@ export const template_visitors = {
context.state.template.push('<!>');
return;
}
if (node.name === 'script') {
context.state.metadata.context.template_contains_script_tag = true;
}

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

for (const attribute of node.attributes) {
Expand Down
30 changes: 25 additions & 5 deletions packages/svelte/src/internal/client/reconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ export function create_fragment_from_html(html) {
return elem.content;
}

/**
* Creating a document fragment from HTML that contains script tags will not execute
* the scripts. We need to replace the script tags with new ones so that they are executed.
* @param {string} html
*/
export function create_fragment_with_script_from_html(html) {
var content = create_fragment_from_html(html);
var scripts = content.querySelectorAll('script');
for (const script of scripts) {
var new_script = document.createElement('script');
for (var i = 0; i < script.attributes.length; i++) {
new_script.setAttribute(script.attributes[i].name, script.attributes[i].value);
}
new_script.textContent = script.textContent;
/** @type {Node} */ (script.parentNode).replaceChild(new_script, script);
}
return content;
}

/**
* @param {Array<import('./types.js').TemplateNode> | import('./types.js').TemplateNode} current
* @param {null | Element} parent_element
Expand Down Expand Up @@ -63,27 +82,28 @@ export function remove(current) {
}

/**
* Creates the content for a `@html` tag from its string value,
* inserts it before the target anchor and returns the new nodes.
* @template V
* @param {Element | Text | Comment} dom
* @param {Element | Text | Comment} target
* @param {V} value
* @param {boolean} svg
* @returns {Element | Comment | (Element | Comment | Text)[]}
*/
export function reconcile_html(dom, value, svg) {
hydrate_block_anchor(dom);
export function reconcile_html(target, value, svg) {
hydrate_block_anchor(target);
if (current_hydration_fragment !== null) {
return current_hydration_fragment;
}
var html = value + '';
// Even if html is the empty string we need to continue to insert something or
// else the element ordering gets out of sync, resulting in subsequent values
// not getting inserted anymore.
var target = dom;
var frag_nodes;
if (svg) {
html = `<svg>${html}</svg>`;
}
var content = create_fragment_from_html(html);
var content = create_fragment_with_script_from_html(html);
if (svg) {
content = /** @type {DocumentFragment} */ (/** @type {unknown} */ (content.firstChild));
}
Expand Down
60 changes: 50 additions & 10 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ import {
PassiveDelegatedEvents,
DelegatedEvents,
AttributeAliases,
namespace_svg,
namespace_html
namespace_svg
} from '../../constants.js';
import { create_fragment_from_html, insert, reconcile_html, remove } from './reconciler.js';
import {
create_fragment_from_html,
create_fragment_with_script_from_html,
insert,
reconcile_html,
remove
} from './reconciler.js';
import {
render_effect,
destroy_signal,
get,
is_signal,
push_destroy_fn,
execute_effect,
Expand Down Expand Up @@ -78,35 +82,71 @@ export function empty() {

/**
* @param {string} html
* @param {boolean} is_fragment
* @param {boolean} return_fragment
* @returns {() => Node}
*/
/*#__NO_SIDE_EFFECTS__*/
export function template(html, is_fragment) {
export function template(html, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
return () => {
if (cached_content === undefined) {
const content = create_fragment_from_html(html);
cached_content = is_fragment ? content : /** @type {Node} */ (child(content));
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
}
return cached_content;
};
}

/**
* @param {string} html
* @param {boolean} return_fragment
* @returns {() => Node}
*/
/*#__NO_SIDE_EFFECTS__*/
export function template_with_script(html, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
return () => {
if (cached_content === undefined) {
const content = create_fragment_with_script_from_html(html);
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
}
return cached_content;
};
}

/**
* @param {string} svg
* @param {boolean} is_fragment
* @param {boolean} return_fragment
* @returns {() => Node}
*/
/*#__NO_SIDE_EFFECTS__*/
export function svg_template(svg, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
return () => {
if (cached_content === undefined) {
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
}
return cached_content;
};
}

/**
* @param {string} svg
* @param {boolean} return_fragment
* @returns {() => Node}
*/
/*#__NO_SIDE_EFFECTS__*/
export function svg_template(svg, is_fragment) {
export function svg_template_with_script(svg, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
return () => {
if (cached_content === undefined) {
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
cached_content = is_fragment ? content : /** @type {Node} */ (child(content));
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
}
return cached_content;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { test } from '../../test';

/**
* @type {any[]}
*/
let log;
/**
* @type {typeof console.log}}
*/
let original_log;

export default test({
skip_if_ssr: 'permanent',
skip_if_hydrate: 'permanent', // log patching will be too late
before_test() {
log = [];
original_log = console.log;
console.log = (...v) => {
log.push(...v);
};
},
after_test() {
console.log = original_log;
},
async test({ assert }) {
assert.deepEqual(log, ['init']);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div>
<script>
console.log('init');
</script>
</div>

0 comments on commit 9aa0ed3

Please sign in to comment.