Repository: sveltejs/kit (js)

Issue:
Reorder "Library skeleton project" to 3rd in the list for `create-svelte`
### Describe the problem

I've been seeing reports of users mistakenly choosing "Library skeleton project" because it's 2nd on the list in `create-svelte`, resulting in confusion.  Moving it to be 3rd should help people avoid this mistake.

### Describe the proposed solution

Changing the template name from `libskeleton` to `skeletonlib` _sorts_ it out 😉

### Alternatives considered

_No response_

### Importance

nice to have

### Additional Information

Happy to submit the PR from my fork where I tested it.  If so- would the PR warrant a changelog?

Pull request description:
reorder create-svelte templates such that library is last  * `libskeleton` was renamed to `skeletonlib`
* `libskeleton` was renamed to `skeletonlib`

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs

closes #6470

- [x] This message body should clearly illustrate what problems it solves.

I've been seeing reports of users mistakenly choosing "Library skeleton project" because it's 2nd on the list in create-svelte, resulting in confusion. Moving it to be 3rd should help people avoid this mistake.

- [ ] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/cold-cycles-report.md b/.changeset/cold-cycles-report.md
new file mode 100644
index 000000000..d6acd2c45
--- /dev/null
+++ b/.changeset/cold-cycles-report.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Prevent validation_errors from being serialized twice
diff --git a/.changeset/three-garlics-brush.md b/.changeset/three-garlics-brush.md
new file mode 100644
index 000000000..fe4139bfc
--- /dev/null
+++ b/.changeset/three-garlics-brush.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+reorder create-svelte templates such that library is last
diff --git a/packages/create-svelte/index.js b/packages/create-svelte/index.js
index b002120a8..737318815 100755
--- a/packages/create-svelte/index.js
+++ b/packages/create-svelte/index.js
@@ -83,7 +83,7 @@ function write_common_files(cwd, options, name) {
  * @returns {boolean}
  */
 function matches_condition(condition, options) {
-	if (condition === 'default' || condition === 'skeleton' || condition === 'libskeleton') {
+	if (condition === 'default' || condition === 'skeleton' || condition === 'skeletonlib') {
 		return options.template === condition;
 	}
 	if (condition === 'typescript' || condition === 'checkjs') {
diff --git a/packages/create-svelte/templates/libskeleton/.gitignore b/packages/create-svelte/templates/skeletonlib/.gitignore
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/.gitignore
rename to packages/create-svelte/templates/skeletonlib/.gitignore
diff --git a/packages/create-svelte/templates/libskeleton/.ignore b/packages/create-svelte/templates/skeletonlib/.ignore
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/.ignore
rename to packages/create-svelte/templates/skeletonlib/.ignore
diff --git a/packages/create-svelte/templates/libskeleton/.meta.json b/packages/create-svelte/templates/skeletonlib/.meta.json
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/.meta.json
rename to packages/create-svelte/templates/skeletonlib/.meta.json
diff --git a/packages/create-svelte/templates/libskeleton/.npmrc b/packages/create-svelte/templates/skeletonlib/.npmrc
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/.npmrc
rename to packages/create-svelte/templates/skeletonlib/.npmrc
diff --git a/packages/create-svelte/templates/libskeleton/package.template.json b/packages/create-svelte/templates/skeletonlib/package.template.json
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/package.template.json
rename to packages/create-svelte/templates/skeletonlib/package.template.json
diff --git a/packages/create-svelte/templates/libskeleton/src/app.d.ts b/packages/create-svelte/templates/skeletonlib/src/app.d.ts
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/src/app.d.ts
rename to packages/create-svelte/templates/skeletonlib/src/app.d.ts
diff --git a/packages/create-svelte/templates/libskeleton/src/app.html b/packages/create-svelte/templates/skeletonlib/src/app.html
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/src/app.html
rename to packages/create-svelte/templates/skeletonlib/src/app.html
diff --git a/packages/create-svelte/templates/libskeleton/src/lib/index.js b/packages/create-svelte/templates/skeletonlib/src/lib/index.js
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/src/lib/index.js
rename to packages/create-svelte/templates/skeletonlib/src/lib/index.js
diff --git a/packages/create-svelte/templates/libskeleton/src/routes/+page.svelte b/packages/create-svelte/templates/skeletonlib/src/routes/+page.svelte
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/src/routes/+page.svelte
rename to packages/create-svelte/templates/skeletonlib/src/routes/+page.svelte
diff --git a/packages/create-svelte/templates/libskeleton/static/favicon.png b/packages/create-svelte/templates/skeletonlib/static/favicon.png
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/static/favicon.png
rename to packages/create-svelte/templates/skeletonlib/static/favicon.png
diff --git a/packages/create-svelte/templates/libskeleton/svelte.config.js b/packages/create-svelte/templates/skeletonlib/svelte.config.js
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/svelte.config.js
rename to packages/create-svelte/templates/skeletonlib/svelte.config.js
diff --git a/packages/create-svelte/templates/libskeleton/vite.config.js b/packages/create-svelte/templates/skeletonlib/vite.config.js
similarity index 100%
rename from packages/create-svelte/templates/libskeleton/vite.config.js
rename to packages/create-svelte/templates/skeletonlib/vite.config.js
diff --git a/packages/create-svelte/types/internal.d.ts b/packages/create-svelte/types/internal.d.ts
index ad1fa0b2d..857180765 100644
--- a/packages/create-svelte/types/internal.d.ts
+++ b/packages/create-svelte/types/internal.d.ts
@@ -1,6 +1,6 @@
 export type Options = {
 	name: string;
-	template: 'default' | 'skeleton' | 'libskeleton';
+	template: 'default' | 'skeleton' | 'skeletonlib';
 	types: 'typescript' | 'checkjs' | null;
 	prettier: boolean;
 	eslint: boolean;
@@ -20,7 +20,7 @@ export type Condition =
 	| 'playwright'
 	| 'skeleton'
 	| 'default'
-	| 'libskeleton';
+	| 'skeletonlib';
 
 export type Common = {
 	files: Array<{
diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js
index e806f9c0a..97ee337dc 100644
--- a/packages/kit/src/runtime/client/fetcher.js
+++ b/packages/kit/src/runtime/client/fetcher.js
@@ -44,10 +44,10 @@ if (import.meta.env.DEV) {
 export function initial_fetch(resource, opts) {
 	const url = JSON.stringify(typeof resource === 'string' ? resource : resource.url);
 
-	let selector = `script[sveltekit\\:data-type="data"][sveltekit\\:data-url=${url}]`;
+	let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
 
 	if (opts && typeof opts.body === 'string') {
-		selector += `[sveltekit\\:data-body="${hash(opts.body)}"]`;
+		selector += `[data-hash="${hash(opts.body)}"]`;
 	}
 
 	const script = document.querySelector(selector);
diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js
index efef5f7b3..adde5f994 100644
--- a/packages/kit/src/runtime/server/page/fetch.js
+++ b/packages/kit/src/runtime/server/page/fetch.js
@@ -192,6 +192,7 @@ export function create_fetch({ event, options, state, route, prerender_default }
 				async function text() {
 					const body = await response.text();
 
+					// TODO just pass `response.headers`, for processing inside `serialize_data`
 					/** @type {import('types').ResponseHeaders} */
 					const headers = {};
 					for (const [key, value] of response.headers) {
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 4ca30dc50..9e69c3dcd 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -2,7 +2,7 @@ import { devalue } from 'devalue';
 import { readable, writable } from 'svelte/store';
 import * as cookie from 'cookie';
 import { hash } from '../../hash.js';
-import { render_json_payload_script } from '../../../utils/escape.js';
+import { serialize_data } from './serialize_data.js';
 import { s } from '../../../utils/misc.js';
 import { Csp } from './csp.js';
 import { serialize_error } from '../utils.js';
@@ -284,25 +284,7 @@ export async function render_response({
 	}
 
 	if (page_config.ssr && page_config.csr) {
-		/** @type {string[]} */
-		const serialized_data = [];
-
-		for (const { url, body, response } of fetched) {
-			serialized_data.push(
-				render_json_payload_script(
-					{ type: 'data', url, body: typeof body === 'string' ? hash(body) : undefined },
-					response
-				)
-			);
-		}
-
-		if (validation_errors) {
-			serialized_data.push(
-				render_json_payload_script({ type: 'validation_errors' }, validation_errors)
-			);
-		}
-
-		body += `\n\t${serialized_data.join('\n\t')}`;
+		body += `\n\t${fetched.map(serialize_data).join('\n\t')}`;
 	}
 
 	if (options.service_worker) {
diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js
new file mode 100644
index 000000000..f32922bb5
--- /dev/null
+++ b/packages/kit/src/runtime/server/page/serialize_data.js
@@ -0,0 +1,58 @@
+import { escape_html_attr } from '../../../utils/escape.js';
+import { hash } from '../../hash.js';
+
+/**
+ * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
+ *
+ * The first closes the script element, so everything after is treated as raw HTML.
+ * The second disables further parsing until `-->`, so the script element might be unexpectedly
+ * kept open until until an unrelated HTML comment in the page.
+ *
+ * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
+ * browsers.
+ *
+ * @see tests for unsafe parsing examples.
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
+ * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
+ * @see https://github.com/tc39/proposal-json-superset
+ * @type {Record<string, string>}
+ */
+const replacements = {
+	'<': '\\u003C',
+	'\u2028': '\\u2028',
+	'\u2029': '\\u2029'
+};
+
+const pattern = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g');
+
+/**
+ * Generates a raw HTML string containing a safe script element carrying data and associated attributes.
+ *
+ * It escapes all the special characters needed to guarantee the element is unbroken, but care must
+ * be taken to ensure it is inserted in the document at an acceptable position for a script element,
+ * and that the resulting string isn't further modified.
+ *
+ * @param {import('./types.js').Fetched} fetched
+ * @returns {string} The raw HTML of a script element carrying the JSON payload.
+ * @example const html = serialize_data('/data.json', null, { foo: 'bar' });
+ */
+export function serialize_data(fetched) {
+	const safe_payload = JSON.stringify(fetched.response).replace(
+		pattern,
+		(match) => replacements[match]
+	);
+
+	const attrs = [
+		'type="application/json"',
+		'data-sveltekit-fetched',
+		`data-url=${escape_html_attr(fetched.url)}`
+	];
+
+	if (fetched.body) {
+		attrs.push(`data-hash=${escape_html_attr(hash(fetched.body))}`);
+	}
+
+	return `<script ${attrs.join(' ')}>${safe_payload}</script>`;
+}
diff --git a/packages/kit/src/runtime/server/page/serialize_data.spec.js b/packages/kit/src/runtime/server/page/serialize_data.spec.js
new file mode 100644
index 000000000..9906ba429
--- /dev/null
+++ b/packages/kit/src/runtime/server/page/serialize_data.spec.js
@@ -0,0 +1,59 @@
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import { serialize_data } from './serialize_data.js';
+
+test('escapes slashes', () => {
+	assert.equal(
+		serialize_data({
+			url: 'foo',
+			body: null,
+			response: {
+				status: 200,
+				statusText: 'OK',
+				headers: {},
+				body: '</script><script>alert("xss")'
+			}
+		}),
+		'<script type="application/json" data-sveltekit-fetched data-url="foo">' +
+			'{"status":200,"statusText":"OK","headers":{},"body":"\\u003C/script>\\u003Cscript>alert(\\"xss\\")"}' +
+			'</script>'
+	);
+});
+
+test('escapes exclamation marks', () => {
+	assert.equal(
+		serialize_data({
+			url: 'foo',
+			body: null,
+			response: {
+				status: 200,
+				statusText: 'OK',
+				headers: {},
+				body: '<!--</script>...-->alert("xss")'
+			}
+		}),
+		'<script type="application/json" data-sveltekit-fetched data-url="foo">' +
+			'{"status":200,"statusText":"OK","headers":{},"body":"\\u003C!--\\u003C/script>...-->alert(\\"xss\\")"}' +
+			'</script>'
+	);
+});
+
+test('escapes the attribute values', () => {
+	const raw = 'an "attr" & a \ud800';
+	const escaped = 'an &quot;attr&quot; &amp; a &#55296;';
+	assert.equal(
+		serialize_data({
+			url: raw,
+			body: null,
+			response: {
+				status: 200,
+				statusText: 'OK',
+				headers: {},
+				body: ''
+			}
+		}),
+		`<script type="application/json" data-sveltekit-fetched data-url="${escaped}">{"status":200,"statusText":"OK","headers":{},"body":\"\"}</script>`
+	);
+});
+
+test.run();
diff --git a/packages/kit/src/utils/escape.js b/packages/kit/src/utils/escape.js
index 959a16067..543e1a13c 100644
--- a/packages/kit/src/utils/escape.js
+++ b/packages/kit/src/utils/escape.js
@@ -1,61 +1,3 @@
-/**
- * Inside a script element, only `</script` and `<!--` hold special meaning to the HTML parser.
- *
- * The first closes the script element, so everything after is treated as raw HTML.
- * The second disables further parsing until `-->`, so the script element might be unexpectedly
- * kept open until until an unrelated HTML comment in the page.
- *
- * U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are escaped for the sake of pre-2018
- * browsers.
- *
- * @see tests for unsafe parsing examples.
- * @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
- * @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
- * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
- * @see https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
- * @see https://github.com/tc39/proposal-json-superset
- * @type {Record<string, string>}
- */
-const render_json_payload_script_dict = {
-	'<': '\\u003C',
-	'\u2028': '\\u2028',
-	'\u2029': '\\u2029'
-};
-
-const render_json_payload_script_regex = new RegExp(
-	`[${Object.keys(render_json_payload_script_dict).join('')}]`,
-	'g'
-);
-
-/**
- * Generates a raw HTML string containing a safe script element carrying JSON data and associated attributes.
- *
- * It escapes all the special characters needed to guarantee the element is unbroken, but care must
- * be taken to ensure it is inserted in the document at an acceptable position for a script element,
- * and that the resulting string isn't further modified.
- *
- * Attribute names must be type-checked so we don't need to escape them.
- *
- * @param {import('types').PayloadScriptAttributes} attrs A list of attributes to be added to the element.
- * @param {any} payload The data to be carried by the element. Must be serializable to JSON.
- * @returns {string} The raw HTML of a script element carrying the JSON payload.
- * @example const html = render_json_payload_script({ type: 'data', url: '/data.json' }, { foo: 'bar' });
- */
-export function render_json_payload_script(attrs, payload) {
-	const safe_payload = JSON.stringify(payload).replace(
-		render_json_payload_script_regex,
-		(match) => render_json_payload_script_dict[match]
-	);
-
-	let safe_attrs = '';
-	for (const [key, value] of Object.entries(attrs)) {
-		if (value === undefined) continue;
-		safe_attrs += ` sveltekit:data-${key}=${escape_html_attr(value)}`;
-	}
-
-	return `<script type="application/json"${safe_attrs}>${safe_payload}</script>`;
-}
-
 /**
  * When inside a double-quoted attribute value, only `&` and `"` hold special meaning.
  * @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
diff --git a/packages/kit/src/utils/escape.spec.js b/packages/kit/src/utils/escape.spec.js
index 57eef069c..4c6585280 100644
--- a/packages/kit/src/utils/escape.spec.js
+++ b/packages/kit/src/utils/escape.spec.js
@@ -1,52 +1,15 @@
-import { suite } from 'uvu';
+import { test } from 'uvu';
 import * as assert from 'uvu/assert';
-import { render_json_payload_script, escape_html_attr } from './escape.js';
+import { escape_html_attr } from './escape.js';
 
-const json = suite('render_json_payload_script');
-
-json('escapes slashes', () => {
-	// The type here doesn't really matter for the purposes of escaping,
-	// but we want to avoid upsetting TypeScript.
-	assert.equal(
-		render_json_payload_script({ type: 'validation_errors' }, [
-			{ unsafe: '</script><script>alert("xss")' }
-		]),
-		'<script type="application/json" sveltekit:data-type="validation_errors">' +
-			'[{"unsafe":"\\u003C/script>\\u003Cscript>alert(\\"xss\\")"}]' +
-			'</script>'
-	);
-});
-
-json('escapes exclamation marks', () => {
-	assert.equal(
-		render_json_payload_script({ type: 'validation_errors' }, [
-			{ '<!--</script>...-->alert("xss")': 'unsafe' }
-		]),
-		'<script type="application/json" sveltekit:data-type="validation_errors">' +
-			'[{"\\u003C!--\\u003C/script>...-->alert(\\"xss\\")":"unsafe"}]' +
-			'</script>'
-	);
-});
-
-json('escapes the attribute values', () => {
-	const raw = 'an "attr" & a \ud800';
-	const escaped = 'an &quot;attr&quot; &amp; a &#55296;';
-	assert.equal(
-		render_json_payload_script({ type: 'data', url: raw }, {}),
-		`<script type="application/json" sveltekit:data-type="data" sveltekit:data-url="${escaped}">{}</script>`
-	);
-});
-
-const attr = suite('escape_html_attr');
-
-attr('escapes special attribute characters', () => {
+test('escape_html_attr escapes special attribute characters', () => {
 	assert.equal(
 		escape_html_attr('some "values" are &special here, <others> aren\'t.'),
 		'"some &quot;values&quot; are &amp;special here, <others> aren\'t."'
 	);
 });
 
-attr('escapes invalid surrogates', () => {
+test('escape_html_attr escapes invalid surrogates', () => {
 	assert.equal(escape_html_attr('\ud800\udc00'), '"\ud800\udc00"');
 	assert.equal(escape_html_attr('\ud800'), '"&#55296;"');
 	assert.equal(escape_html_attr('\udc00'), '"&#56320;"');
@@ -56,5 +19,4 @@ attr('escapes invalid surrogates', () => {
 	assert.equal(escape_html_attr('\ud800\ud800\udc00\udc00'), '"&#55296;\ud800\udc00&#56320;"');
 });
 
-json.run();
-attr.run();
+test.run();
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index c5669e786..234af54f8 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -135,10 +135,6 @@ export interface PageNode {
 	child_pages?: PageNode[];
 }
 
-export type PayloadScriptAttributes =
-	| { type: 'data'; url: string; body?: string }
-	| { type: 'validation_errors' };
-
 export interface PrerenderDependency {
 	response: Response;
 	body: null | string | Uint8Array;


Test patch:
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index be319a1f9..6e9eb5652 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -663,7 +663,7 @@ test.describe('Load', () => {
 
 		if (!javaScriptEnabled) {
 			// by the time JS has run, hydration will have nuked these scripts
-			const script_contents = await page.innerHTML('script[sveltekit\\:data-type="data"]');
+			const script_contents = await page.innerHTML('script[data-sveltekit-fetched]');
 
 			const payload =
 				'{"status":200,"statusText":"","headers":{"content-type":"application/json"},"body":"{\\"answer\\":42}"}';
@@ -693,11 +693,11 @@ test.describe('Load', () => {
 		if (!javaScriptEnabled) {
 			// by the time JS has run, hydration will have nuked these scripts
 			const script_contents_a = await page.innerHTML(
-				'script[sveltekit\\:data-type="data"][sveltekit\\:data-url="/load/serialization-post.json"][sveltekit\\:data-body="3t25"]'
+				'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="3t25"]'
 			);
 
 			const script_contents_b = await page.innerHTML(
-				'script[sveltekit\\:data-type="data"][sveltekit\\:data-url="/load/serialization-post.json"][sveltekit\\:data-body="3t24"]'
+				'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="3t24"]'
 			);
 
 			expect(script_contents_a).toBe(payload_a);