Merge remote-tracking branch 'origin/sites' into sites

pull/8274/head
Puru Vijay 2 years ago
commit f4c593f28f

File diff suppressed because it is too large Load Diff

@ -16,7 +16,7 @@
"test": "uvu -r ts-node/register src/lib/server/markdown"
},
"dependencies": {
"@supabase/supabase-js": "^2.7.0",
"@supabase/supabase-js": "^2.7.1",
"@sveltejs/repl": "0.0.3",
"cookie": "^0.5.0",
"devalue": "^4.2.3",
@ -25,27 +25,31 @@
"sourcemap-codec": "^1.4.8"
},
"devDependencies": {
"@resvg/resvg-js": "^2.2.0",
"@sveltejs/adapter-auto": "^1.0.2",
"@sveltejs/kit": "^1.3.10",
"@sveltejs/site-kit": "^3.2.1",
"@resvg/resvg-js": "^2.3.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"@sveltejs/site-kit": "^3.2.2",
"@sveltejs/vite-plugin-svelte": "^2.0.2",
"@types/marked": "^4.0.8",
"@types/prismjs": "^1.26.0",
"degit": "^2.8.4",
"dotenv": "^16.0.3",
"jimp": "^0.16.2",
"jimp": "^0.22.4",
"marked": "^4.2.12",
"node-fetch": "^3.3.0",
"prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"prism-svelte": "^0.5.0",
"prismjs": "^1.29.0",
"satori": "^0.1.2",
"satori": "^0.2.3",
"satori-html": "^0.3.2",
"shelljs": "^0.8.5",
"shiki": "^0.14.0",
"shiki-twoslash": "^3.1.0",
"svelte": "^3.55.1",
"svelte-check": "^3.0.3",
"typescript": "^4.9.5",
"vite": "^4.0.4",
"vite": "^4.1.1",
"vite-imagetools": "^4.0.18"
}
}

@ -0,0 +1,73 @@
import fs from 'fs';
import { extract_frontmatter } from '../markdown';
import { transform } from './marked';
/**
* @returns {import('./types').BlogPostSummary[]}
*/
export function get_index() {
return fs
.readdirSync('content/blog')
.reverse()
.map((file) => {
if (!file.endsWith('.md')) return;
const { date, slug } = get_date_and_slug(file);
const content = fs.readFileSync(`content/blog/${file}`, 'utf-8');
const { metadata } = extract_frontmatter(content);
return {
slug,
date,
title: metadata.title,
description: metadata.description,
draft: !!metadata.draft,
};
});
}
/**
* @param {string} slug
* @returns {import('./types').BlogPost}
*/
export function get_post(slug) {
for (const file of fs.readdirSync('content/blog')) {
if (!file.endsWith('.md')) continue;
if (file.slice(11, -3) !== slug) continue;
const { date, date_formatted } = get_date_and_slug(file);
const content = fs.readFileSync(`content/blog/${file}`, 'utf-8');
const { metadata, body } = extract_frontmatter(content);
return {
date,
date_formatted,
title: metadata.title,
description: metadata.description,
author: {
name: metadata.author,
url: metadata.authorURL,
},
draft: !!metadata.draft,
content: transform(body),
};
}
}
/** @param {string} filename */
function get_date_and_slug(filename) {
const match = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/.exec(filename);
if (!match) throw new Error(`Invalid filename for blog: '${filename}'`);
const [, date, slug] = match;
const [y, m, d] = date.split('-');
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
return { date, date_formatted, slug };
}
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
function format_date(date) {}

@ -0,0 +1,570 @@
import * as fs from 'fs';
import * as path from 'path';
import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash';
import PrismJS from 'prismjs';
import 'prismjs/components/prism-bash.js';
import 'prismjs/components/prism-diff.js';
import 'prismjs/components/prism-typescript.js';
import 'prism-svelte';
import { escape, extract_frontmatter, transform } from '../markdown';
// import { render, replace_placeholders } from './render.js';
// import { parse_route_id } from '../../../../../../packages/kit/src/utils/routing.js';
import ts, { ScriptTarget } from 'typescript';
import MagicString from 'magic-string';
import { fileURLToPath } from 'url';
import { createHash } from 'crypto';
const base = '../../site/content/docs/';
const languages = {
bash: 'bash',
env: 'bash',
html: 'html',
svelte: 'svelte',
sv: 'svelte',
js: 'javascript',
css: 'css',
diff: 'diff',
ts: 'typescript',
'': '',
};
/**
* @param {string} file
*/
export async function read_file(file) {
const match = /\d{2}-(.+)\.md/.exec(path.basename(file));
if (!match) return null;
// const markdown = replace_placeholders(fs.readFileSync(`${base}/${file}`, 'utf-8'));
const markdown = fs.readFileSync(`${base}/${file}`, 'utf-8');
const highlighter = await createShikiHighlighter({ theme: 'css-variables' });
const { metadata, body } = extract_frontmatter(markdown);
const { content, sections } = parse({
file,
body: generate_ts_from_js(body),
code: (source, language, current) => {
const hash = createHash('sha256');
hash.update(source + language + current);
const digest = hash.digest().toString('base64').replace(/\//g, '-');
// TODO: cache
// if (fs.existsSync(`${snippet_cache}/${digest}.html`)) {
// return fs.readFileSync(`${snippet_cache}/${digest}.html`, 'utf-8');
// }
/** @type {Record<string, string>} */
const options = {};
let html = '';
source = source
.replace(/^\/\/\/ (.+?): (.+)\n/gm, (_, key, value) => {
options[key] = value;
return '';
})
.replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => {
if (prefix && language !== 'diff') return match;
// for no good reason at all, marked replaces tabs with spaces
let tabs = '';
for (let i = 0; i < spaces.length; i += 4) {
tabs += ' ';
}
return prefix + tabs;
})
.replace(/\*\\\//g, '*/');
let version_class = '';
if (language === 'generated-ts' || language === 'generated-svelte') {
language = language.replace('generated-', '');
version_class = 'ts-version';
} else if (language === 'original-js' || language === 'original-svelte') {
language = language.replace('original-', '');
version_class = 'js-version';
}
// TODO: Replace later
html = highlighter.codeToHtml(source, { lang: languages[language] });
// if (language === 'dts') {
// // @ts-ignore
// html = renderCodeToHTML(source, 'ts', { twoslash: false }, {}, highlighter);
// } else if (language === 'js' || language === 'ts') {
// try {
// const injected = [];
// if (
// source.includes('$app/') ||
// source.includes('$service-worker') ||
// source.includes('@sveltejs/kit/')
// ) {
// injected.push(
// `// @filename: ambient-kit.d.ts`,
// `/// <reference types="@sveltejs/kit" />`
// );
// }
// if (source.includes('$env/')) {
// // TODO we're hardcoding static env vars that are used in code examples
// // in the types, which isn't... totally ideal, but will do for now
// injected.push(
// `declare module '$env/dynamic/private' { export const env: Record<string, string> }`,
// `declare module '$env/dynamic/public' { export const env: Record<string, string> }`,
// `declare module '$env/static/private' { export const API_KEY: string }`,
// `declare module '$env/static/public' { export const PUBLIC_BASE_URL: string }`
// );
// }
// if (source.includes('./$types') && !source.includes('@filename: $types.d.ts')) {
// const params = parse_route_id(options.file || `+page.${language}`)
// .params.map((param) => `${param.name}: string`)
// .join(', ');
// injected.push(
// `// @filename: $types.d.ts`,
// `import type * as Kit from '@sveltejs/kit';`,
// `export type PageLoad = Kit.Load<{${params}}>;`,
// `export type PageServerLoad = Kit.ServerLoad<{${params}}>;`,
// `export type LayoutLoad = Kit.Load<{${params}}>;`,
// `export type LayoutServerLoad = Kit.ServerLoad<{${params}}>;`,
// `export type RequestHandler = Kit.RequestHandler<{${params}}>;`,
// `export type Action = Kit.Action<{${params}}>;`,
// `export type Actions = Kit.Actions<{${params}}>;`
// );
// }
// // special case — we need to make allowances for code snippets coming
// // from e.g. ambient.d.ts
// if (file.endsWith('30-modules.md')) {
// injected.push('// @errors: 7006 7031');
// }
// // another special case
// if (source.includes('$lib/types')) {
// injected.push(`declare module '$lib/types' { export interface User {} }`);
// }
// if (injected.length) {
// const injected_str = injected.join('\n');
// if (source.includes('// @filename:')) {
// source = source.replace('// @filename:', `${injected_str}\n\n// @filename:`);
// } else {
// source = source.replace(
// /^(?!\/\/ @)/m,
// `${injected_str}\n\n// @filename: index.${language}\n// ---cut---\n`
// );
// }
// }
// const twoslash = runTwoSlash(source, language, {
// defaultCompilerOptions: {
// allowJs: true,
// checkJs: true,
// target: 'es2021',
// },
// });
// html = renderCodeToHTML(
// twoslash.code,
// 'ts',
// { twoslash: true },
// {},
// highlighter,
// twoslash
// );
// } catch (e) {
// console.error(`Error compiling snippet in ${file}`);
// console.error(e.code);
// throw e;
// }
// // we need to be able to inject the LSP attributes as HTML, not text, so we
// // turn &lt; into &amp;lt;
// html = html.replace(
// /<data-lsp lsp='([^']*)'([^>]*)>(\w+)<\/data-lsp>/g,
// (match, lsp, attrs, name) => {
// if (!lsp) return name;
// return `<data-lsp lsp='${lsp.replace(/&/g, '&amp;')}'${attrs}>${name}</data-lsp>`;
// }
// );
// // preserve blank lines in output (maybe there's a more correct way to do this?)
// html = html.replace(/<div class='line'><\/div>/g, '<div class="line"> </div>');
// } else if (language === 'diff') {
// const lines = source.split('\n').map((content) => {
// let type = null;
// if (/^[\+\-]/.test(content)) {
// type = content[0] === '+' ? 'inserted' : 'deleted';
// content = content.slice(1);
// }
// return {
// type,
// content: escape(content),
// };
// });
// html = `<pre class="language-diff"><code>${lines
// .map((line) => {
// if (line.type) return `<span class="${line.type}">${line.content}\n</span>`;
// return line.content + '\n';
// })
// .join('')}</code></pre>`;
// } else {
// const plang = languages[language];
// const highlighted = plang
// ? PrismJS.highlight(source, PrismJS.languages[plang], language)
// : source.replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
// html = `<pre class='language-${plang}'><code>${highlighted}</code></pre>`;
// }
if (options.file) {
html = `<div class="code-block"><span class="filename">${options.file}</span>${html}</div>`;
}
if (version_class) {
html = html.replace(/class=('|")/, `class=$1${version_class} `);
}
// type_regex.lastIndex = 0;
html = html
// .replace(type_regex, (match, prefix, name) => {
// if (options.link === 'false' || name === current) {
// // we don't want e.g. RequestHandler to link to RequestHandler
// return match;
// }
// const link = `<a href="${type_links.get(name)}">${name}</a>`;
// return `${prefix || ''}${link}`;
// })
.replace(
/^(\s+)<span class="token comment">([\s\S]+?)<\/span>\n/gm,
(match, intro_whitespace, content) => {
// we use some CSS trickery to make comments break onto multiple lines while preserving indentation
const lines = (intro_whitespace + content).split('\n');
return lines
.map((line) => {
const match = /^(\s*)(.*)/.exec(line);
const indent = (match[1] ?? '').replace(/\t/g, ' ').length;
return `<span class="token comment wrapped" style="--indent: ${indent}ch">${
line ?? ''
}</span>`;
})
.join('');
}
)
.replace(/\/\*…\*\//g, '…');
// fs.writeFileSync(`${snippet_cache}/${digest}.html`, html);
return html;
},
codespan: (text) => {
return (
'<code>' +
text +
// text.replace(type_regex, (match, prefix, name) => {
// const link = `<a href="${type_links.get(name)}">${name}</a>`;
// return `${prefix || ''}${link}`;
// }) +
'</code>'
);
},
});
return {
file,
slug: match[1],
title: metadata.title,
content,
sections,
};
}
/** @param {string} title */
export function slugify(title) {
return title
.toLowerCase()
.replace(/&lt;/g, '')
.replace(/&gt;/g, '')
.replace(/[^a-z0-9-$]/g, '-')
.replace(/-{2,}/g, '-')
.replace(/^-/, '')
.replace(/-$/, '');
}
/**
* @param {{
* file: string;
* body: string;
* code: (source: string, language: string, current: string) => string;
* codespan: (source: string) => string;
* }} opts
*/
function parse({ file, body, code, codespan }) {
const headings = [];
/** @type {import('./types').Section[]} */
const sections = [];
/** @type {import('./types').Section} */
let section;
// this is a bit hacky, but it allows us to prevent type declarations
// from linking to themselves
let current = '';
/** @type {string} */
const content = transform(body, {
/**
* @param {string} html
* @param {number} level
*/
heading(html, level) {
const title = html
.replace(/<\/?code>/g, '')
.replace(/&quot;/g, '"')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
current = title;
const normalized = slugify(title);
headings[level - 1] = normalized;
headings.length = level;
const slug = headings.filter(Boolean).join('-');
// TODO: All this will need to change when headings in documentation are restructured to be h2+ instead of h3+ as they are right now
if (level === 3) {
section = {
title,
slug,
sections: [],
};
sections.push(section);
} else if (level === 4 || level === 5) {
(section?.sections ?? sections).push({
title,
slug,
});
} else {
throw new Error(`Unexpected <h${level}> in ${file}`);
}
return `<h${
level - 1
} id="${slug}">${html}<a href="#${slug}" class="permalink"><span class="visually-hidden">permalink</span></a></h${level}>`;
},
code: (source, language) => code(source, language, current),
codespan,
});
return {
sections,
content,
};
}
export function generate_ts_from_js(markdown) {
return markdown
.replaceAll(/```js\n([\s\S]+?)\n```/g, (match, code) => {
if (!code.includes('/// file:')) {
// No named file -> assume that the code is not meant to be shown in two versions
return match;
}
if (code.includes('/// file: svelte.config.js')) {
// svelte.config.js has no TS equivalent
return match;
}
const ts = convert_to_ts(code);
if (!ts) {
// No changes -> don't show TS version
return match;
}
return match.replace('js', 'original-js') + '\n```generated-ts\n' + ts + '\n```';
})
.replaceAll(/```svelte\n([\s\S]+?)\n```/g, (match, code) => {
if (!code.includes('/// file:')) {
// No named file -> assume that the code is not meant to be shown in two versions
return match;
}
// Assumption: no context="module" blocks
const script = code.match(/<script>([\s\S]+?)<\/script>/);
if (!script) return match;
const [outer, inner] = script;
const ts = convert_to_ts(inner, '\t', '\n');
if (!ts) {
// No changes -> don't show TS version
return match;
}
return (
match.replace('svelte', 'original-svelte') +
'\n```generated-svelte\n' +
code.replace(outer, `<script lang="ts">${ts}</script>`) +
'\n```'
);
});
}
/**
* Transforms a JS code block into a TS code block by turning JSDoc into type annotations.
* Due to pragmatism only the cases currently used in the docs are implemented.
* @param {string} js_code
* @param {string} [indent]
* @param {string} [offset]
* */
function convert_to_ts(js_code, indent = '', offset = '') {
js_code = js_code
.replaceAll('// @filename: index.js', '// @filename: index.ts')
.replace(/(\/\/\/ .+?\.)js/, '$1ts')
// *\/ appears in some JsDoc comments in d.ts files due to the JSDoc-in-JSDoc problem
.replace(/\*\\\//g, '*/');
const ast = ts.createSourceFile(
'filename.ts',
js_code,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS
);
const code = new MagicString(js_code);
const imports = new Map();
function walk(node) {
// @ts-ignore
if (node.jsDoc) {
// @ts-ignore
for (const comment of node.jsDoc) {
let modified = false;
for (const tag of comment.tags ?? []) {
if (ts.isJSDocTypeTag(tag)) {
const [name, generics] = get_type_info(tag);
if (ts.isFunctionDeclaration(node)) {
const is_export = node.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
)
? 'export '
: '';
const is_async = node.modifiers?.some(
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword
);
code.overwrite(
node.getStart(),
node.name.getEnd(),
`${is_export ? 'export ' : ''}const ${node.name.getText()} = (${
is_async ? 'async ' : ''
}`
);
code.appendLeft(node.body.getStart(), '=> ');
const type = generics !== undefined ? `${name}${generics}` : name;
code.appendLeft(node.body.getEnd(), `) satisfies ${type};`);
modified = true;
} else if (
ts.isVariableStatement(node) &&
node.declarationList.declarations.length === 1
) {
const variable_statement = node.declarationList.declarations[0];
if (variable_statement.name.getText() === 'actions') {
code.appendLeft(variable_statement.getEnd(), ` satisfies ${name}`);
} else {
code.appendLeft(variable_statement.name.getEnd(), `: ${name}`);
}
modified = true;
} else {
throw new Error('Unhandled @type JsDoc->TS conversion: ' + js_code);
}
} else if (ts.isJSDocParameterTag(tag) && ts.isFunctionDeclaration(node)) {
if (node.parameters.length !== 1) {
throw new Error(
'Unhandled @type JsDoc->TS conversion; needs more params logic: ' + node.getText()
);
}
const [name] = get_type_info(tag);
code.appendLeft(node.parameters[0].getEnd(), `: ${name}`);
modified = true;
}
}
if (modified) {
code.overwrite(comment.getStart(), comment.getEnd(), '');
}
}
}
ts.forEachChild(node, walk);
}
walk(ast);
if (imports.size) {
const import_statements = Array.from(imports.entries())
.map(([from, names]) => {
return `${indent}import type { ${Array.from(names).join(', ')} } from '${from}';`;
})
.join('\n');
const idxOfLastImport = [...ast.statements]
.reverse()
.find((statement) => ts.isImportDeclaration(statement))
?.getEnd();
const insertion_point = Math.max(
idxOfLastImport ? idxOfLastImport + 1 : 0,
js_code.includes('---cut---')
? js_code.indexOf('\n', js_code.indexOf('---cut---')) + 1
: js_code.includes('/// file:')
? js_code.indexOf('\n', js_code.indexOf('/// file:')) + 1
: 0
);
code.appendLeft(insertion_point, offset + import_statements + '\n');
}
const transformed = code.toString();
return transformed === js_code ? undefined : transformed.replace(/\n\s*\n\s*\n/g, '\n\n');
/** @param {ts.JSDocTypeTag | ts.JSDocParameterTag} tag */
function get_type_info(tag) {
const type_text = tag.typeExpression.getText();
let name = type_text.slice(1, -1); // remove { }
const import_match = /import\('(.+?)'\)\.(\w+)(<{?[\n\* \w:;,]+}?>)?/.exec(type_text);
if (import_match) {
const [, from, _name, generics] = import_match;
name = _name;
const existing = imports.get(from);
if (existing) {
existing.add(name);
} else {
imports.set(from, new Set([name]));
}
if (generics !== undefined) {
return [
name,
generics
.replaceAll('*', '') // get rid of JSDoc asterisks
.replace(' }>', '}>'), // unindent closing brace
];
}
}
return [name];
}
}

@ -0,0 +1,13 @@
export interface Section {
title: string;
slug: string;
sections?: Section[];
}
export interface Type {
name: string;
comment: string;
snippet: string;
bullets: string[];
children: Type[];
}

@ -1,74 +1,181 @@
import fs from 'fs';
import { transform } from './marked';
import { marked } from 'marked';
const escapeTest = /[&<>"']/;
const escapeReplace = /[&<>"']/g;
const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
const escapeReplacements = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
/**
* @returns {import('./types').BlogPostSummary[]}
* @param {keyof typeof escapeReplacements} ch
*/
export function get_index() {
return fs
.readdirSync('content/blog')
.reverse()
.map((file) => {
if (!file.endsWith('.md')) return;
const { date, slug } = get_date_and_slug(file);
const content = fs.readFileSync(`content/blog/${file}`, 'utf-8');
const { metadata } = extract_frontmatter(content);
return {
slug,
date,
title: metadata.title,
description: metadata.description,
draft: !!metadata.draft
};
});
}
const getEscapeReplacement = (ch) => escapeReplacements[ch];
/**
* @param {string} slug
* @returns {import('./types').BlogPost}
* @param {string} html
* @param {boolean} encode
*/
export function get_post(slug) {
for (const file of fs.readdirSync('content/blog')) {
if (!file.endsWith('.md')) continue;
if (file.slice(11, -3) !== slug) continue;
const { date, date_formatted } = get_date_and_slug(file);
const content = fs.readFileSync(`content/blog/${file}`, 'utf-8');
const { metadata, body } = extract_frontmatter(content);
return {
date,
date_formatted,
title: metadata.title,
description: metadata.description,
author: {
name: metadata.author,
url: metadata.authorURL
},
draft: !!metadata.draft,
content: transform(body)
};
export function escape(html, encode = false) {
if (encode) {
if (escapeTest.test(html)) {
return html.replace(escapeReplace, getEscapeReplacement);
}
} else {
if (escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
}
}
return html;
}
/** @param {string} filename */
function get_date_and_slug(filename) {
const match = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/.exec(filename);
if (!match) throw new Error(`Invalid filename for blog: '${filename}'`);
/** @type {Partial<import('marked').Renderer>} */
const default_renderer = {
code(code, infostring, escaped) {
const lang = (infostring || '').match(/\S*/)[0];
code = code.replace(/\n$/, '') + '\n';
if (!lang) {
return '<pre><code>' + (escaped ? code : escape(code, true)) + '</code></pre>\n';
}
return (
'<pre><code class="language-' +
escape(lang, true) +
'">' +
(escaped ? code : escape(code, true)) +
'</code></pre>\n'
);
},
blockquote(quote) {
return '<blockquote>\n' + quote + '</blockquote>\n';
},
html(html) {
return html;
},
heading(text, level) {
return '<h' + level + '>' + text + '</h' + level + '>\n';
},
hr() {
return '<hr>\n';
},
list(body, ordered, start) {
const type = ordered ? 'ol' : 'ul',
startatt = ordered && start !== 1 ? ' start="' + start + '"' : '';
return '<' + type + startatt + '>\n' + body + '</' + type + '>\n';
},
listitem(text) {
return '<li>' + text + '</li>\n';
},
checkbox(checked) {
return '<input ' + (checked ? 'checked="" ' : '') + 'disabled="" type="checkbox"' + '' + '> ';
},
paragraph(text) {
return '<p>' + text + '</p>\n';
},
table(header, body) {
if (body) body = '<tbody>' + body + '</tbody>';
return '<table>\n' + '<thead>\n' + header + '</thead>\n' + body + '</table>\n';
},
tablerow(content) {
return '<tr>\n' + content + '</tr>\n';
},
tablecell(content, flags) {
const type = flags.header ? 'th' : 'td';
const tag = flags.align ? '<' + type + ' align="' + flags.align + '">' : '<' + type + '>';
return tag + content + '</' + type + '>\n';
},
// span level renderer
strong(text) {
return '<strong>' + text + '</strong>';
},
em(text) {
return '<em>' + text + '</em>';
},
codespan(text) {
return '<code>' + text + '</code>';
},
br() {
return '<br>';
},
del(text) {
return '<del>' + text + '</del>';
},
link(href, title, text) {
if (href === null) {
return text;
}
let out = '<a href="' + escape(href) + '"';
if (title) {
out += ' title="' + title + '"';
}
out += '>' + text + '</a>';
return out;
},
image(href, title, text) {
if (href === null) {
return text;
}
let out = '<img src="' + href + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"';
}
out += '>';
return out;
},
text(text) {
return text;
},
};
const [, date, slug] = match;
const [y, m, d] = date.split('-');
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
/**
* @param {string} markdown
* @param {Partial<import('marked').Renderer>} renderer
*/
export function transform(markdown, renderer = {}) {
marked.use({
renderer: {
// we have to jump through these hoops because of marked's API design choices —
// options are global, and merged in confusing ways. You can't do e.g.
// `new Marked(options).parse(markdown)`
...default_renderer,
...renderer,
},
});
return { date, date_formatted, slug };
return marked(markdown);
}
/** @param {string} markdown */
function extract_frontmatter(markdown) {
export function extract_frontmatter(markdown) {
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown);
const frontmatter = match[1];
const body = markdown.slice(match[0].length);
@ -77,17 +184,8 @@ function extract_frontmatter(markdown) {
const metadata = {};
frontmatter.split('\n').forEach((pair) => {
const i = pair.indexOf(':');
metadata[pair.slice(0, i).trim()] = strip_quotes(pair.slice(i + 1).trim());
metadata[pair.slice(0, i).trim()] = pair.slice(i + 1).trim();
});
return { metadata, body };
}
function strip_quotes(str) {
if (str[0] === '"' && str[str.length - 1] === '"') return str.slice(1, -1);
return str;
}
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
function format_date(date) {}

@ -1,9 +1,8 @@
<script>
import "@sveltejs/site-kit/styles/index.css";
import { page, navigating } from "$app/stores";
import { Icon, Icons, Nav, NavItem, SkipLink } from "@sveltejs/site-kit";
import PreloadingIndicator from "$lib/components/PreloadingIndicator.svelte";
import StopWar from "./stopwar.svg";
import { navigating, page } from '$app/stores';
import PreloadingIndicator from '$lib/components/PreloadingIndicator.svelte';
import { Icon, Icons, Nav, NavItem, SkipLink } from '@sveltejs/site-kit';
import '@sveltejs/site-kit/styles/index.css';
</script>
<Icons />
@ -12,9 +11,9 @@
<PreloadingIndicator />
{/if}
{#if $page.url.pathname !== "/repl/embed"}
{#if $page.url.pathname !== '/repl/embed'}
<SkipLink href="#main" />
<Nav {page} logo={StopWar}>
<Nav {page} logo="/svelte-logo.svg">
<svelte:fragment slot="nav-center">
<NavItem href="/tutorial">Tutorial</NavItem>
<NavItem href="/docs">Docs</NavItem>
@ -32,10 +31,7 @@
<span class="large"><Icon name="message-square" /></span>
</NavItem>
<NavItem
external="https://github.com/sveltejs/svelte"
title="GitHub Repo"
>
<NavItem external="https://github.com/sveltejs/svelte" title="GitHub Repo">
<span class="small">GitHub</span>
<span class="large"><Icon name="github" /></span>
</NavItem>
@ -44,16 +40,10 @@
{/if}
<svelte:head>
{#if $page.route.id !== "/blog/[slug]"}
{#if $page.route.id !== '/blog/[slug]'}
<meta name="twitter:card" content="summary" />
<meta
name="twitter:image"
content="https://svelte.dev/images/twitter-thumbnail.jpg"
/>
<meta
name="og:image"
content="https://svelte.dev/images/twitter-thumbnail.jpg"
/>
<meta name="twitter:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
<meta name="og:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
{/if}
</svelte:head>
@ -61,29 +51,7 @@
<slot />
</main>
<a target="_blank" rel="noopener noreferrer" href="https://www.stopputin.net/">
<div class="ukr">
<span class="small">
<strong>We stand with Ukraine.</strong> Donate →
</span>
<span class="large">
<strong>We stand with Ukraine.</strong> Petition your leaders. Show your support.
</span>
</div>
</a>
<style>
.ukr {
background-color: #0066cc;
color: white;
position: fixed;
bottom: 0;
width: 100vw;
text-align: center;
padding: 0.75em;
z-index: 999;
}
@media (max-width: 830px) {
:global(aside) {
z-index: 9999 !important;
@ -125,19 +93,6 @@
padding-bottom: var(--ukr-footer-height);
}
.ukr {
background-color: #0066cc;
color: white;
position: fixed;
display: flex;
align-items: center;
justify-content: center;
bottom: 0;
width: 100vw;
height: var(--ukr-footer-height);
z-index: 999;
}
:global(.examples-container, .repl-outer, .tutorial-outer) {
height: calc(100vh - var(--nav-h) - var(--ukr-footer-height)) !important;
}
@ -155,7 +110,4 @@
z-index: 9999 !important;
}
}
.ukr strong {
color: #ffcc00;
}
</style>

@ -6,7 +6,7 @@
<div class="logos">
{#each sorted as { href, filename, alt, style, invert, width, height }}
<a target="_blank" rel="noopener" {href} class:invert style={style || ''}>
<a target="_blank" rel="noreferrer" {href} class:invert style={style || ''}>
<img src="/whos-using-svelte/{filename}" {alt} {width} {height} loading="lazy" />
</a>

@ -1,9 +1,9 @@
import { get_index } from '$lib/server/markdown';
import { get_index } from '$lib/server/blog';
export const prerender = true;
export async function load() {
return {
posts: get_index()
posts: get_index(),
};
}

@ -1,4 +1,4 @@
import { get_post } from '$lib/server/markdown/index.js';
import { get_post } from '$lib/server/blog/index.js';
import { error } from '@sveltejs/kit';
export const prerender = true;
@ -11,6 +11,6 @@ export async function load({ params }) {
}
return {
post
post,
};
}

@ -2,7 +2,7 @@ import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import OverpassRegular from './Overpass-Regular.ttf';
import { html as toReactNode } from 'satori-html';
import { get_post } from '$lib/server/markdown/index.js';
import { get_post } from '$lib/server/blog/index.js';
import { error } from '@sveltejs/kit';
import Card from './Card.svelte';
@ -19,6 +19,7 @@ export const GET = async ({ params, url }) => {
throw error(404);
}
// @ts-ignore
const result = Card.render({ post });
const element = toReactNode(`${result.html}<style>${result.css.code}</style>`);
@ -28,18 +29,18 @@ export const GET = async ({ params, url }) => {
name: 'Overpass',
data: Buffer.from(OverpassRegular),
style: 'normal',
weight: 400
}
weight: 400,
},
],
height,
width
width,
});
const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: width
}
value: width,
},
});
const image = resvg.render();
@ -47,7 +48,7 @@ export const GET = async ({ params, url }) => {
return new Response(image.asPng(), {
headers: {
'content-type': 'image/png',
'cache-control': 'public, max-age=600' // cache for 10 minutes
}
'cache-control': 'public, max-age=600', // cache for 10 minutes
},
});
};

@ -1,4 +1,4 @@
import { get_index } from '$lib/server/markdown';
import { get_index } from '$lib/server/blog';
export const prerender = true;
@ -15,13 +15,13 @@ function escapeHTML(html) {
"'": '#39',
'&': 'amp',
'<': 'lt',
'>': 'gt'
'>': 'gt',
};
return html.replace(/["'&<>]/g, (c) => `&${chars[c]};`);
}
/** @type {import('$lib/server/markdown/types').BlogPostSummary[]} */
/** @param {import('$lib/server/blog/types').BlogPostSummary[]} posts */
const get_rss = (posts) =>
`
<?xml version="1.0" encoding="UTF-8" ?>
@ -63,7 +63,7 @@ export async function GET() {
return new Response(get_rss(posts), {
headers: {
'Cache-Control': `max-age=${30 * 60 * 1e3}`,
'Content-Type': 'application/rss+xml'
}
'Content-Type': 'application/rss+xml',
},
});
}

@ -0,0 +1,23 @@
import { extract_frontmatter } from '$lib/server/markdown';
import fs from 'fs';
import { base } from '$app/paths';
export const prerender = true;
const base_dir = '../../site/content/docs/';
/** @type {import('./$types').LayoutServerLoad} */
export function load() {
const sections = fs.readdirSync(base_dir).map((file) => {
const { title } = extract_frontmatter(fs.readFileSync(`${base_dir}/${file}`, 'utf-8')).metadata;
return {
title,
path: `${base}/docs/${file.slice(3, -3)}`,
};
});
return {
sections,
};
}

@ -0,0 +1,337 @@
<script>
import { page } from '$app/stores';
import Contents from './Contents.svelte';
import '@sveltejs/site-kit/styles/code.css';
/** @type {import('./$types').LayoutServerData}*/
export let data;
</script>
<div class="container">
<div class="page content">
<h1>{data.sections.find((val) => val.path === $page.url.pathname)?.title}</h1>
<slot />
</div>
<div class="toc-container">
<Contents contents={data.sections} />
</div>
</div>
<style>
.container {
--sidebar-menu-width: 20rem;
--sidebar-width: var(--sidebar-menu-width);
--ts-toggle-height: 4.2rem;
}
.page {
--on-this-page-display: none;
padding: var(--sk-page-padding-top) var(--sk-page-padding-side);
}
.page :global(hr) {
display: none;
}
.content {
width: 100%;
margin: 0;
padding: var(--sk-page-padding-top) var(--sk-page-padding-side);
tab-size: 2;
-moz-tab-size: 2;
}
@media (min-width: 832px) {
/* can't use vars in @media :( */
.content {
padding-left: calc(var(--sidebar-width) + var(--sk-page-padding-side));
}
}
.content :global(h1) {
font-size: 3.2rem;
margin: 0 0 0.5em 0;
}
.content :global(h2) {
margin-top: 8rem;
padding: 2rem 1.6rem 2rem 0.2rem;
border-bottom: 1px solid hsl(0, 0%, 87%, 0.2);
line-height: 1;
font-size: var(--sk-text-m);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.content :global(section):first-of-type > :global(h2) {
margin-top: 0;
}
.content :global(h4) {
margin: 2em 0 1em 0;
}
.content :global(.offset-anchor) {
position: relative;
display: block;
top: calc(-1 * var(--sk-page-padding-top));
width: 0;
height: 0;
}
.content :global(.anchor) {
position: absolute;
display: block;
background: url(../icons/link.svg) 0 50% no-repeat;
background-size: 1em 1em;
width: 1.4em;
height: 1em;
left: -1.3em;
bottom: 0.3rem;
opacity: 0;
transition: opacity 0.2s;
user-select: none;
}
.content :global(h2) :global(.anchor) {
bottom: 4rem;
}
.content :global(h3) :global(.anchor) {
bottom: 1rem;
}
@media (min-width: 400px) {
.content :global(h1) {
font-size: 4.2rem;
}
}
@media (min-width: 768px) {
.content :global(h1) {
font-size: 5.4rem;
}
.content :global(.anchor:focus),
.content :global(h2):hover :global(.anchor),
.content :global(h3):hover :global(.anchor),
.content :global(h4):hover :global(.anchor),
.content :global(h5):hover :global(.anchor),
.content :global(h6):hover :global(.anchor) {
opacity: 1;
}
}
.content :global(h3),
.content :global(h3 > code) {
margin: 6.4rem 0 1rem 0;
padding: 0 0 1rem 0;
color: var(--sk-text-2);
max-width: var(--sk-line-max-width);
border-bottom: 1px solid hsl(0, 0%, 87%, 0.2);
background: transparent;
line-height: 1;
}
.content :global(h3):first-child {
border: none;
margin: 0;
}
/* avoid doubled border-top */
.content :global(h3 > code) {
border-radius: 0 0 0 0;
border: none;
font-size: inherit;
}
.content :global(h4),
.content :global(h4 > code) {
font-family: inherit;
font-weight: 600;
font-size: 2.4rem;
color: var(--sk-text-2);
margin: 6.4rem 0 1.6rem 0;
padding-left: 0;
background: transparent;
line-height: 1;
padding-top: 0;
top: 0;
}
.content :global(h4::before) {
display: inline;
content: ' ';
block-size: var(--sk-nav-height);
margin-block-start: calc(-1 * var(--sk-nav-height));
}
.content :global(h4 > em) {
opacity: 0.7;
}
.content :global(h4 > .anchor) {
top: 0.05em;
}
.content :global(h5) {
font-size: 2.4rem;
margin: 2em 0 0.5em 0;
}
.content :global(code) {
padding: 0.4rem;
margin: 0 0.2rem;
top: -0.1rem;
background: var(--sk-back-4);
}
.content :global(pre) :global(code) {
padding: 0;
margin: 0;
top: 0;
background: transparent;
}
.content :global(pre) {
margin: 0 0 2rem 0;
width: 100%;
max-width: var(--sk-line-max-width);
padding: 1rem 1rem;
box-shadow: inset 1px 1px 6px hsla(205.7, 63.6%, 30.8%, 0.06);
}
.content :global(.icon) {
width: 2rem;
height: 2rem;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
.content :global(table) {
margin: 0 0 2em 0;
}
.content :global(section) :global(p) {
max-width: var(--sk-line-max-width);
margin: 1em 0;
}
.content :global(small) {
font-size: var(--sk-text-s);
float: right;
pointer-events: all;
color: var(--sk-theme-1);
cursor: pointer;
}
.content :global(blockquote) {
color: var(--sk-text-1);
background-color: rgba(255, 62, 0, 0.1);
border-left: 4px solid var(--sk-theme-1-variant);
padding: 1rem;
}
.content :global(blockquote) :global(:first-child) {
margin-top: 0;
}
.content :global(blockquote) :global(:last-child) {
margin-bottom: 0;
}
.content :global(blockquote) :global(code) {
background: var(--sk-code-bg);
}
.content :global(section) :global(a):hover {
text-decoration: underline;
}
.content :global(section) :global(a) :global(code) {
color: inherit;
background: rgba(255, 62, 0, 0.1) !important;
}
/* this replaces the offset-anchor hack, which we should remove from this CSS
once https://github.com/sveltejs/action-deploy-docs/issues/1 is closed */
.content :global(h2[id]),
.content :global(h3[id]) {
padding-top: 10rem;
margin-top: -2rem;
border-top: none;
}
/* .content :global(h2[id])::after {
content: '';
position: absolute;
width: 100%;
left: 0;
top: 8rem;
height: 2px;
background: #ddd;
} */
.toc-container {
background: var(--sk-back-3);
}
/*
.ts-toggle {
width: 100%;
border-top: 1px solid var(--sk-back-4);
background-color: var(--sk-back-3);
} */
@media (min-width: 832px) {
.toc-container {
width: var(--sidebar-width);
height: calc(100vh - var(--sk-nav-height) - var(--ts-toggle-height));
position: fixed;
left: 0;
top: var(--sk-nav-height);
overflow-x: hidden;
overflow-y: auto;
}
.toc-container::before {
content: '';
position: fixed;
width: 0;
height: 100%;
top: 0;
left: calc(var(--sidebar-width) - 1px);
border-right: 1px solid var(--sk-back-5);
}
.page {
padding-left: calc(var(--sidebar-width) + var(--sk-page-padding-side));
}
/* .ts-toggle {
position: fixed;
width: var(--sidebar-width);
bottom: 0;
z-index: 1;
margin-right: 0;
border-right: 1px solid var(--sk-back-5);
} */
}
@media (min-width: 1200px) {
.container {
--sidebar-width: max(20rem, 18vw);
}
.page {
--on-this-page-display: block;
padding: var(--sk-page-padding-top) calc(var(--sidebar-width) + var(--sk-page-padding-side));
margin: 0 auto;
max-width: var(--sk-line-max-width);
box-sizing: content-box;
}
}
</style>

@ -1,12 +1,7 @@
import { PUBLIC_API_BASE } from '$env/static/public';
import { base } from '$app/paths';
import { redirect } from '@sveltejs/kit';
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, setHeaders }) {
const sections = await (await fetch(`${PUBLIC_API_BASE}/docs/svelte/docs?content`)).json();
setHeaders({
'cache-control': 'public, max-age=60'
});
return { sections };
export async function load() {
throw redirect(307, `${base}/docs/introduction`);
}

@ -1,43 +0,0 @@
<script>
// import { Contents, Main, Section } from '@sveltejs/site-kit/docs';
/** @type {import('./$types').PageData} */
export let data;
let path;
$: contents = data.sections.map((section) => ({
path: `/docs#${section.slug}`,
title: section.title,
sections: section.sections.map((subsection) => ({
path: `/docs#${subsection.slug}`,
title: subsection.title,
sections: subsection.sections.map((subsection) => ({
path: `/docs#${subsection.slug}`,
title: subsection.title,
})),
})),
}));
</script>
<svelte:head>
<title>Docs • Svelte</title>
<meta name="twitter:title" content="Svelte docs" />
<meta name="twitter:description" content="Complete documentation for Svelte" />
<meta name="Description" content="Complete documentation for Svelte" />
</svelte:head>
<!-- <Main bind:path>
<h1>Documentation</h1>
{#each data.sections as section}
<Section
{section}
edit="https://github.com/sveltejs/svelte/edit/master/site/content/docs/{section.file}"
base="/docs"
/>
{/each}
</Main>
<Contents {contents} {path} /> -->

@ -0,0 +1,137 @@
<script>
import { page } from '$app/stores';
/** @type {{title: string, path: string}[]} */
export let contents;
</script>
<nav aria-label="Docs">
<ul class="sidebar">
{#each contents as { title, path }}
<li>
<a
data-sveltekit-preload-data
class="page"
class:active={path === $page.url.pathname}
href={path}
>
{title}
</a>
</li>
{/each}
</ul>
</nav>
<style>
nav {
top: 0;
left: 0;
color: var(--sk-text-3);
display: grid;
justify-content: center;
}
.sidebar {
padding: var(--sk-page-padding-top) 0 var(--sk-page-padding-top) 3.2rem;
font-family: var(--sk-font);
height: 100%;
bottom: auto;
width: 100%;
columns: 2;
margin: 0;
}
li {
display: block;
line-height: 1.2;
margin: 0;
margin-bottom: 1rem;
}
li:last-child {
margin-bottom: 0;
}
a {
position: relative;
transition: color 0.2s;
border-bottom: none;
padding: 0;
color: var(--sk-text-3);
user-select: none;
}
.page {
display: block;
font-size: 1.6rem;
font-family: var(--sk-font);
padding-bottom: 0.6em;
}
.active {
font-weight: 700;
color: var(--sk-text-1);
}
@media (min-width: 600px) {
.sidebar {
columns: 2;
padding-left: var(--sk-page-padding-side);
padding-right: var(--sk-page-padding-side);
}
}
@media (min-width: 700px) {
.sidebar {
/* columns: 3; */
}
}
@media (min-width: 832px) {
.sidebar {
columns: 1;
padding-left: 3.2rem;
padding-right: 0;
width: var(--sidebar-menu-width);
margin: 0 0 0 auto;
}
nav {
min-height: calc(100vh - var(--ts-toggle-height));
}
nav::after {
content: '';
position: fixed;
left: 0;
bottom: var(--ts-toggle-height);
width: calc(var(--sidebar-width) - 1px);
height: 2em;
pointer-events: none;
background: linear-gradient(
to bottom,
hsla(var(--sk-back-3-hsl), 0) 0%,
hsla(var(--sk-back-3-hsl), 0.7) 50%,
hsl(var(--sk-back-3-hsl)) 100%
);
background-repeat: no-repeat;
background-size: calc(100% - 3rem) 100%; /* cover text but not scrollbar */
}
.active::after {
--size: 1rem;
content: '';
position: absolute;
width: var(--size);
height: var(--size);
top: -0.1rem;
right: calc(-0.5 * var(--size));
background-color: var(--sk-back-1);
border-left: 1px solid var(--sk-back-5);
border-bottom: 1px solid var(--sk-back-5);
transform: translateY(0.2rem) rotate(45deg);
z-index: 2;
}
}
</style>

@ -0,0 +1,24 @@
import fs from 'fs';
// import { read_file } from '$lib/server/docs';
import { error } from '@sveltejs/kit';
import { read_file } from '$lib/server/docs';
export const prerender = true;
const base = '../../site/content/docs/';
/**
* ASSUMPTION FOR FUTURE: This assumes the directory structure of docs is flat. AKA, no nested folders
*/
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
for (const file of fs.readdirSync(`${base}`)) {
if (file.slice(3, -3) === params.slug) {
return {
page: await read_file(file),
};
}
}
throw error(404);
}

@ -0,0 +1,16 @@
<script>
import OnThisPage from './OnThisPage.svelte';
/** @type {import('./$types').PageData}*/
export let data;
</script>
<svelte:head>
<title>{data.page.title} - Svelte</title>
</svelte:head>
{@html data.page.content}
{#if data.page.sections.length !== 0}
<OnThisPage details={data.page} />
{/if}

@ -0,0 +1,141 @@
<script>
import { onMount } from 'svelte';
import { afterNavigate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
/** @type {import('./$types').PageData['page']} */
export let details;
/** @type {string} */
let hash = '';
/** @type {number} */
let height = 0;
/** @type {HTMLElement} */
let content;
/** @type {NodeListOf<HTMLElement>} */
let headings;
/** @type {number[]} */
let positions = [];
onMount(async () => {
await document.fonts.ready;
update();
highlight();
});
afterNavigate(() => {
update();
highlight();
});
function update() {
content = document.querySelector('.content');
const { top } = content.getBoundingClientRect();
headings = content.querySelectorAll('h2[id]');
positions = Array.from(headings).map((heading) => {
const style = getComputedStyle(heading);
return heading.getBoundingClientRect().top - parseFloat(style.scrollMarginTop) - top;
});
height = window.innerHeight;
}
function highlight() {
const { top, bottom } = content.getBoundingClientRect();
let i = headings.length;
while (i--) {
if (bottom - height < 50 || positions[i] + top < 100) {
const heading = headings[i];
hash = `#${heading.id}`;
return;
}
}
hash = '';
}
/** @param {URL} url */
function select(url) {
// belt...
setTimeout(() => {
hash = url.hash;
});
// ...and braces
window.addEventListener(
'scroll',
() => {
hash = url.hash;
},
{ once: true }
);
}
</script>
<svelte:window on:scroll={highlight} on:resize={update} on:hashchange={() => select($page.url)} />
<aside class="on-this-page">
<h2>On this page</h2>
<nav>
<ul>
<li><a href="{base}/docs/{details.slug}" class:active={hash === ''}>{details.title}</a></li>
{#each details.sections as { title, slug }}
<li><a href={`#${slug}`} class:active={`#${slug}` === hash}>{title}</a></li>
{/each}
</ul>
</nav>
</aside>
<style>
.on-this-page {
display: var(--on-this-page-display);
position: fixed;
padding: 0 var(--sk-page-padding-side) 0 0;
width: min(280px, calc(var(--sidebar-width) - var(--sk-page-padding-side)));
/* top: calc(var(--sk-page-padding-top) + var(--sk-nav-height)); */
top: var(--sk-nav-height);
left: calc(100vw - (var(--sidebar-width)));
}
h2 {
text-transform: uppercase;
font-size: 1.4rem;
font-weight: 400;
line-height: 0;
margin: 0 0 1rem 0 !important;
padding: 0 0 0 0.6rem;
color: var(--sk-text-3);
}
ul {
list-style: none;
}
a {
display: block;
padding: 0.3rem 0.5rem;
color: var(--sk-text-3);
border-left: 2px solid transparent;
font-size: 1.3rem;
}
a:hover {
text-decoration: none;
background: var(--sk-back-3);
}
a.active {
background: var(--sk-back-3);
border-left-color: var(--sk-theme-1);
}
</style>

@ -3,6 +3,10 @@ import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
export default {
kit: {
adapter: adapter()
}
adapter: adapter(),
prerender: {
// TODO: REMOVE
handleMissingId: 'ignore',
},
},
};

@ -1,3 +1,8 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"allowSyntheticDefaultImports": true
}
}

Loading…
Cancel
Save