diff --git a/jsconfig.site.json b/jsconfig.site.json new file mode 100644 index 000000000..565d81b33 --- /dev/null +++ b/jsconfig.site.json @@ -0,0 +1,13 @@ +{ + "extends": "./jsconfig.json", + "exclude": [ + "./src/lib/**", + "./src/_components/**", + "./src/routes/_components/**", + "./src/routes/_components_ssr/**", + "./src/routes/_examples/**", + "./src/routes/_examples_ssr/**", + "./src/_data/*", + "./src/scripts/**/*" + ] +} diff --git a/package.json b/package.json index 7edd672f7..1a121abeb 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "package": "svelte-kit sync && svelte-package -o dist && publint", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "check:site": "svelte-kit sync && node ./scripts/check-site.js", "lint": "prettier --check .", "format": "prettier --write .", "update_template": "sh ./src/scripts/update_template.sh", diff --git a/scripts/check-site.js b/scripts/check-site.js new file mode 100644 index 000000000..814e660bf --- /dev/null +++ b/scripts/check-site.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * Run svelte-check for internal site code only, ignoring: + * - src/lib (published library) + * - chart demos under src/_components and src/routes/_components* + * - example charts under src/routes/_examples* + */ +import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const svelteCheckBin = require.resolve('svelte-check/bin/svelte-check'); + +/** Paths deferred to a later pass — not part of this site-only check. */ +const ignoredPathParts = [ + 'src/lib/', + 'src/_components/', + 'src/routes/_components/', + 'src/routes/_components_ssr/', + 'src/routes/_examples/', + 'src/routes/_examples_ssr/' +]; + +/** + * @param {string} file + */ +function isIgnored(file) { + const normalized = file.replaceAll('\\', '/').replace(/^[A-Za-z]:/, ''); + return ignoredPathParts.some(part => normalized.includes(`/${part}`) || normalized.startsWith(part)); +} + +const result = spawnSync(process.execPath, [svelteCheckBin, '--tsconfig', './jsconfig.site.json'], { + encoding: 'utf8', + maxBuffer: 20 * 1024 * 1024 +}); + +const output = result.stdout || ''; +process.stdout.write(output); +if (result.stderr) process.stderr.write(result.stderr); + +const lines = output.split('\n'); +/** @type {{ file: string, line: number, col: number, msg: string }[]} */ +const siteErrors = []; +/** @type {string | null} */ +let currentFile = null; +let currentLine = 0; +let currentCol = 0; +/** @type {string | null} */ +let currentKind = null; +/** @type {string[]} */ +let currentMsg = []; + +function flush() { + if (!currentFile || currentKind !== 'Error') return; + if (isIgnored(currentFile)) return; + siteErrors.push({ + file: currentFile, + line: currentLine, + col: currentCol, + msg: currentMsg.join('\n').trim() + }); +} + +for (const line of lines) { + const locParts = line.match(/^(.+):(\d+):(\d+)$/); + if (locParts) { + flush(); + currentFile = locParts[1]; + currentLine = Number(locParts[2]); + currentCol = Number(locParts[3]); + currentKind = null; + currentMsg = []; + continue; + } + const kind = line.match(/^(Error|Warn|Hint):\s*(.*)$/); + if (kind && currentFile) { + flush(); + currentKind = kind[1]; + currentMsg = [kind[2]]; + continue; + } + if (currentKind && currentFile && line && !line.startsWith('=====')) { + currentMsg.push(line); + } +} +flush(); + +if (siteErrors.length) { + console.error( + `\ncheck:site found ${siteErrors.length} internal-site error(s) (lib/charts ignored).` + ); + process.exit(1); +} + +console.log('\ncheck:site passed (no internal-site errors; lib/charts ignored).'); +process.exit(0); diff --git a/src/_modules/arrowUtils.js b/src/_modules/arrowUtils.js index d29e7a0da..8d165fb4e 100644 --- a/src/_modules/arrowUtils.js +++ b/src/_modules/arrowUtils.js @@ -1,6 +1,6 @@ // Helper functions for creating swoopy arrows -/* -------------------------------------------- +/** * parseCssValue * * Parse various inputs and return then as a number @@ -8,6 +8,11 @@ * A percentage, which will take the percent of the appropriate dimentions * A pixel value, which will parse as a number * + * @param {string|number|null|undefined} d + * @param {number} i + * @param {number} width + * @param {number} height + * @returns {number} */ export function parseCssValue(d, i, width, height) { if (!d) return 0; @@ -20,16 +25,18 @@ export function parseCssValue(d, i, width, height) { return +d.replace('px', ''); } -/* -------------------------------------------- +/** * getElPosition * * Constract a bounding box relative in our coordinate space * that we can attach arrow starting points to * + * @param {Element} el + * @returns {{ top: number, right: number, bottom: number, left: number, width: number, height: number }} */ export function getElPosition(el) { const annotationBbox = el.getBoundingClientRect(); - const parentBbox = el.parentNode.getBoundingClientRect(); + const parentBbox = (el.parentElement ?? el).getBoundingClientRect(); const coords = { top: annotationBbox.top - parentBbox.top, right: annotationBbox.right - parentBbox.left, @@ -50,13 +57,24 @@ export function getElPosition(el) { export function swoopyArrow() { let angle = Math.PI; let clockwise = true; + /** @type {(d: any) => number} */ let xValue = d => d[0]; + /** @type {(d: any) => number} */ let yValue = d => d[1]; + /** + * @param {number} a + * @param {number} b + * @returns {number} + */ function hypotenuse(a, b) { return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); } + /** + * @param {any[]} data + * @returns {string} + */ function render(data) { data = data.map(d => { return [xValue(d), yValue(d)]; @@ -102,27 +120,31 @@ export function swoopyArrow() { return path; } + /** @param {number} [_] */ render.angle = function renderAngle(_) { if (!arguments.length) return angle; - angle = Math.min(Math.max(_, 1e-6), Math.PI - 1e-6); + angle = Math.min(Math.max(/** @type {number} */ (_), 1e-6), Math.PI - 1e-6); return render; }; + /** @param {boolean} [_] */ render.clockwise = function renderClockwise(_) { if (!arguments.length) return clockwise; clockwise = !!_; return render; }; + /** @param {(d: any) => number} [_] */ render.x = function renderX(_) { if (!arguments.length) return xValue; - xValue = _; + xValue = /** @type {(d: any) => number} */ (_); return render; }; + /** @param {(d: any) => number} [_] */ render.y = function renderY(_) { if (!arguments.length) return yValue; - yValue = _; + yValue = /** @type {(d: any) => number} */ (_); return render; }; diff --git a/src/_modules/calcThresholds.js b/src/_modules/calcThresholds.js index 7962b319f..8ddf3f19c 100644 --- a/src/_modules/calcThresholds.js +++ b/src/_modules/calcThresholds.js @@ -1,4 +1,9 @@ -export default function calcThresholds(domain = [0, 1], n) { +/** + * @param {[number, number]} [domain] + * @param {number} [n] + * @returns {number[]} + */ +export default function calcThresholds(domain = [0, 1], n = 1) { const breaks = [domain[0]]; const brk = (domain[1] - domain[0]) / n; while (breaks[breaks.length - 1] < domain[1]) { diff --git a/src/_modules/cleanTitle.js b/src/_modules/cleanTitle.js index 092e03988..b06c4b1af 100644 --- a/src/_modules/cleanTitle.js +++ b/src/_modules/cleanTitle.js @@ -1,3 +1,7 @@ +/** + * @param {string} title + * @returns {string} + */ export default function cleanTitle(title) { const parts = title.split('/'); const nameParts = parts[parts.length - 1].split('.'); diff --git a/src/_modules/constructReplLink.js b/src/_modules/constructReplLink.js index 3df2fed53..c2d2c26b8 100644 --- a/src/_modules/constructReplLink.js +++ b/src/_modules/constructReplLink.js @@ -2,6 +2,23 @@ import { csvParse } from 'd3-dsv'; import { compress_and_encode_text } from './createReplHash.js'; +/** + * @typedef {{ title: string, contents: string }} CodeFile + * @typedef {{ + * main: CodeFile, + * components: CodeFile[], + * componentModules: CodeFile[], + * modules: CodeFile[], + * componentComponents: CodeFile[], + * jsons: CodeFile[], + * csvs: CodeFile[] + * }} ExampleContent + */ + +/** + * @param {string} pageName + * @param {ExampleContent} content + */ export default async function constructReplLink(pageName, content) { // TODO, clean up import paths const pages = [content.main] @@ -17,7 +34,7 @@ export default async function constructReplLink(pageName, content) { const json = { name: pageTitle.trim(), files: pages.map(c => { - const filename = c.title.split('/').pop(); + const filename = c.title.split('/').pop() ?? ''; const name = cleanName(filename); return { type: 'file', diff --git a/src/_modules/downloadBlob.js b/src/_modules/downloadBlob.js index 1f2129eaf..3d7342525 100644 --- a/src/_modules/downloadBlob.js +++ b/src/_modules/downloadBlob.js @@ -1,9 +1,14 @@ +/** + * @param {BlobPart|Blob} blob + * @param {string} filename + * @param {boolean} [createBlob=false] + */ export default function downloadBlob(blob, filename, createBlob = false) { let myBlob; if (createBlob === true) { - myBlob = new Blob([blob], { type: 'octet/stream' }); + myBlob = new Blob([/** @type {BlobPart} */ (blob)], { type: 'octet/stream' }); } else { - myBlob = blob; + myBlob = /** @type {Blob} */ (blob); } const url = URL.createObjectURL(myBlob); const link = document.createElement('a'); diff --git a/src/_modules/getSections.js b/src/_modules/getSections.js index bc5d9b4f7..297bfb1a5 100644 --- a/src/_modules/getSections.js +++ b/src/_modules/getSections.js @@ -13,6 +13,7 @@ hljs.registerLanguage('svelte', hljsDefineSvelte); hljsDefineSvelte(hljs); +/** @type {Record} */ const escaped = { '"': '"', "'": ''', @@ -21,11 +22,13 @@ const escaped = { '>': '>' }; +/** @type {Record} */ const unescaped = Object.keys(escaped).reduce( - (unescaped, key) => ((unescaped[escaped[key]] = key), unescaped), - {} + (acc, key) => ((acc[escaped[key]] = key), acc), + /** @type {Record} */ ({}) ); +/** @param {string} str */ function unescape(str) { return String(str).replace(/&.+?;/g, match => unescaped[match] || match); } @@ -33,6 +36,10 @@ function unescape(str) { const blockTypes = 'blockquote html heading hr list listitem paragraph table tablerow tablecell'.split(' '); +/** + * @param {string} line + * @param {string} lang + */ function extractMeta(line, lang) { try { if (lang === 'html' || lang === 'svelte') { @@ -54,6 +61,7 @@ function extractMeta(line, lang) { } // https://github.com/darkskyapp/string-hash/blob/master/index.js +/** @param {string} str */ function getHash(str) { let hash = 5381; let i = str.length; @@ -64,7 +72,22 @@ function getHash(str) { const demos = new Map(); +/** + * @typedef {{ meta: Record, lang: string, source: string }} CodeBlock + * @typedef {{ id: number, blocks: CodeBlock[] }} CodeGroup + * @typedef {{ slug: string, title: string }} GuideSubsection + * @typedef {{ + * html: string | null, + * metadata: Record, + * subsections: GuideSubsection[], + * slug: string, + * file: string + * }} GuideSection + */ + +/** @param {boolean} [returnHtml=true] */ export default function (returnHtml = true) { + /** @type {Record} */ const store = {}; return fs .readdirSync(`src/content/guide`) @@ -74,7 +97,9 @@ export default function (returnHtml = true) { const { content, metadata } = processMarkdown(markdown); + /** @type {CodeGroup[]} */ const groups = []; + /** @type {CodeGroup | null} */ let group = null; let uid = 1; @@ -206,16 +231,21 @@ export default function (returnHtml = true) { let html = marked.marked(content, { async: false }); + /** @type {Record} */ const hashes = {}; groups.forEach(group => { const main = group.blocks[0]; if (main.meta.repl === false) return; - const hash = getHash(group.blocks.map(block => block.source).join('')); + const hash = getHash( + group.blocks.map(/** @param {CodeBlock} block */ block => block.source).join('') + ); hashes[group.id] = hash; - const json5 = group.blocks.find(block => block.lang === 'json'); + const json5 = group.blocks.find( + /** @param {CodeBlock} block */ block => block.lang === 'json' + ); const title = main.meta.title; // if (!title) console.error(`Missing title for demo in ${file}`); @@ -225,23 +255,30 @@ export default function (returnHtml = true) { JSON.stringify({ title: title || 'Example from guide', components: group.blocks - .filter(block => block.lang === 'html' || block.lang === 'js') - .map(block => { - const [name, type] = (block.meta.filename || '').split('.'); - return { - name: name || 'App', - type: type || 'html', - source: block.source - }; - }), + .filter( + /** @param {CodeBlock} block */ block => + block.lang === 'html' || block.lang === 'js' + ) + .map( + /** @param {CodeBlock} block */ block => { + const [name, type] = (block.meta.filename || '').split('.'); + return { + name: name || 'App', + type: type || 'html', + source: block.source + }; + } + ), json5: json5 && json5.source }) ); }); // When extracting sidebar subsections, strip ... from the title for anchors, but keep the display text for the heading itself + /** @type {{ slug: string, title: string }[]} */ const subsections = []; const pattern = /

(.+?)<\/h3>/g; + /** @type {RegExpExecArray | null} */ let match; while ((match = pattern.exec(html))) { @@ -291,7 +328,8 @@ export default function (returnHtml = true) { } return { - html: returnHtml === true ? html.replace(/@@(\d+)/g, (m, id) => hashes[id] || m) : null, + html: + returnHtml === true ? html.replace(/@@(\d+)/g, (m, id) => hashes[Number(id)] || m) : null, metadata, subsections, slug: file.replace(/^\d+-/, '').replace(/\.md$/, ''), diff --git a/src/_modules/hljsDefineSvelte.js b/src/_modules/hljsDefineSvelte.js index 8263a02f6..4d3d4442a 100644 --- a/src/_modules/hljsDefineSvelte.js +++ b/src/_modules/hljsDefineSvelte.js @@ -1,5 +1,6 @@ -/* -------------------------------------------- +/** * Adapted to work as es6 module from https://github.com/AlexxNB/highlightjs-svelte + * @param {any} hljs */ export default function hljsDefineSvelte(hljs) { return { diff --git a/src/_modules/processMarkdown.js b/src/_modules/processMarkdown.js index 99310b94a..df972f963 100644 --- a/src/_modules/processMarkdown.js +++ b/src/_modules/processMarkdown.js @@ -1,3 +1,7 @@ +/** + * @param {string} markdown + * @returns {{ metadata: Record, content: string }} + */ export default function processMarkdown(markdown) { const match = /---\n([\s\S]+?)\n---/.exec(markdown); @@ -8,6 +12,7 @@ export default function processMarkdown(markdown) { const frontMatter = match[1]; const content = markdown.slice(match[0].length); + /** @type {Record} */ const metadata = {}; frontMatter.split('\n').forEach(pair => { const colonIndex = pair.indexOf(':'); diff --git a/src/_modules/slugify.js b/src/_modules/slugify.js index 644d4fb4d..d9b5223dd 100644 --- a/src/_modules/slugify.js +++ b/src/_modules/slugify.js @@ -2,6 +2,12 @@ import emoji from 'emoji-regex'; const whitespace = /\s/g; +/** + * @param {string} string + * @param {boolean|null|undefined} maintainCase + * @param {Record} store + * @returns {string} + */ export default function slugger(string, maintainCase, store) { const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g; const replacement = '-'; diff --git a/src/routes/_site-components/DownloadBtn.svelte b/src/routes/_site-components/DownloadBtn.svelte index 49403ae24..117b9c90e 100644 --- a/src/routes/_site-components/DownloadBtn.svelte +++ b/src/routes/_site-components/DownloadBtn.svelte @@ -4,34 +4,47 @@ import downloadBlob from '../../_modules/downloadBlob.js'; + /** + * @typedef {import('../../_modules/constructReplLink.js').CodeFile} CodeFile + * @typedef {import('../../_modules/constructReplLink.js').ExampleContent} ExampleContent + */ + /** * @typedef {Object} Props - * @property {any} [data] - * @property {any} slug + * @property {ExampleContent} [data] + * @property {string} slug * @property {boolean} [ssr] */ /** @type {Props} */ - let { data = {}, slug, ssr = false } = $props(); + let { data = /** @type {ExampleContent} */ ({}), slug, ssr = false } = $props(); let downloading = $state(false); + /** @param {string} [file] */ function getImports(file = '') { const match = file.match(/from\s'(.+)'?/gm) || []; - const imports = match.map(d => d.replace(/(from |'|"|;)/g, '')).filter(d => !d.startsWith('.')); + const imports = match + .map(/** @param {string} d */ d => d.replace(/(from |'|"|;)/g, '')) + .filter(/** @param {string} d */ d => !d.startsWith('.')); return imports; } + // svelte-ignore state_referenced_locally const imports = [data.main, ...data.components, ...data.componentComponents] - .reduce((store, val) => store.concat(getImports(val.contents)), []) - .reduce((store, val) => { + .reduce( + (/** @type {string[]} */ store, /** @type {CodeFile} */ val) => + store.concat(getImports(val.contents)), + /** @type {string[]} */ ([]) + ) + .reduce((/** @type {string[]} */ store, /** @type {string} */ val) => { if (!store.includes(val)) { store.push(val); return store; } else { return store; } - }, []); + }, /** @type {string[]} */ ([])); async function download() { downloading = true; @@ -40,62 +53,81 @@ const cacheBust = new Date().getTime(); const files = await (await window.fetch(`/svelte-app.json?${cacheBust}`)).json(); + /** @type {Record} */ const depsLookup = await (await window.fetch(`/deps.json?${cacheBust}`)).json(); if (imports.length > 0) { - const idx = files.findIndex(({ path }) => path === 'package.json'); + const idx = files.findIndex( + /** @param {{ path: string }} file */ ({ path }) => path === 'package.json' + ); const pkg = JSON.parse(files[idx].data); + /** @type {Record} */ const deps = {}; + /** @type {Record} */ const devDeps = {}; - imports.forEach(mod => { - if (mod === 'svelte') { - return; - } else { - deps[mod] = depsLookup[mod]; - } - if (!depsLookup[mod]) { - window.alert(`Missing dependency, add "${mod}" to this repo's package.json`); + imports.forEach( + /** @param {string} mod */ mod => { + if (mod === 'svelte') { + return; + } else { + deps[mod] = depsLookup[mod]; + } + if (!depsLookup[mod]) { + window.alert(`Missing dependency, add "${mod}" to this repo's package.json`); + } } - }); + ); Object.assign(pkg.dependencies, deps); Object.assign(pkg.devDependencies, devDeps); files[idx].data = JSON.stringify(pkg, null, ' '); } files.push( - ...data.components.map(component => ({ - path: `src/routes/${component.title.replace('./', '')}`, - data: component.contents - })) + ...data.components.map( + /** @param {CodeFile} component */ component => ({ + path: `src/routes/${component.title.replace('./', '')}`, + data: component.contents + }) + ) ); files.push( - ...data.modules.map(mod => ({ - path: `src/routes/${mod.title.replace('./', '')}`, - data: mod.contents - })) + ...data.modules.map( + /** @param {CodeFile} mod */ mod => ({ + path: `src/routes/${mod.title.replace('./', '')}`, + data: mod.contents + }) + ) ); files.push( - ...data.componentModules.map(mod => ({ - path: `src/routes/${mod.title.replace('../', '')}`, - data: mod.contents - })) + ...data.componentModules.map( + /** @param {CodeFile} mod */ mod => ({ + path: `src/routes/${mod.title.replace('../', '')}`, + data: mod.contents + }) + ) ); files.push( - ...data.componentComponents.map(mod => ({ - path: `src/routes/${mod.title}`, - data: mod.contents - })) + ...data.componentComponents.map( + /** @param {CodeFile} mod */ mod => ({ + path: `src/routes/${mod.title}`, + data: mod.contents + }) + ) ); files.push( - ...data.csvs.map(mod => ({ - path: `src/routes/${mod.title.replace('../', '')}`, - data: mod.contents - })) + ...data.csvs.map( + /** @param {CodeFile} mod */ mod => ({ + path: `src/routes/${mod.title.replace('../', '')}`, + data: mod.contents + }) + ) ); files.push( - ...data.jsons.map(mod => ({ - path: `src/routes/${mod.title.replace('../', '')}`, - data: mod.contents - })) + ...data.jsons.map( + /** @param {CodeFile} mod */ mod => ({ + path: `src/routes/${mod.title.replace('../', '')}`, + data: mod.contents + }) + ) ); files.push({ path: `src/routes/+page.svelte`, diff --git a/src/routes/_site-components/DownloadComponentBtn.svelte b/src/routes/_site-components/DownloadComponentBtn.svelte index 438dd80b8..80eff2a11 100644 --- a/src/routes/_site-components/DownloadComponentBtn.svelte +++ b/src/routes/_site-components/DownloadComponentBtn.svelte @@ -4,7 +4,22 @@ import downloadBlob from '../../_modules/downloadBlob.js'; - let { data = {}, slug } = $props(); + /** + * @typedef {{ slug: string, contents: string }} ComponentFile + * @typedef {{ + * main: ComponentFile, + * modules: ComponentFile[] + * }} ComponentContent + */ + + /** + * @typedef {Object} Props + * @property {ComponentContent} [data] + * @property {string} slug + */ + + /** @type {Props} */ + let { data = /** @type {ComponentContent} */ ({}), slug } = $props(); let downloading = $state(false); @@ -59,7 +74,12 @@ // } // files.push(...data.components.map(component => ({ path: `src/${component.title.replace('./', '')}`, data: component.contents }))); files.push( - ...data.modules.map(mod => ({ path: mod.slug.replace('./', ''), data: mod.contents })) + ...data.modules.map( + /** @param {ComponentFile} mod */ mod => ({ + path: mod.slug.replace('./', ''), + data: mod.contents + }) + ) ); // files.push(...data.componentModules.map(mod => ({ path: `src/${mod.title.replace('../', '')}`, data: mod.contents }))); // files.push(...data.componentComponents.map(mod => ({ path: `src/${mod.title}`, data: mod.contents }))); diff --git a/src/routes/_site-components/GuideContents.svelte b/src/routes/_site-components/GuideContents.svelte index eddee3968..ed6b92972 100644 --- a/src/routes/_site-components/GuideContents.svelte +++ b/src/routes/_site-components/GuideContents.svelte @@ -1,17 +1,24 @@ diff --git a/src/routes/example-ssr/[slug].json/+server.js b/src/routes/example-ssr/[slug].json/+server.js index 104b7b47a..f4f2174b3 100644 --- a/src/routes/example-ssr/[slug].json/+server.js +++ b/src/routes/example-ssr/[slug].json/+server.js @@ -1,38 +1,44 @@ import { error, json } from '@sveltejs/kit'; import * as fs from 'fs'; +/** @param {string} example */ function getComponentJsPaths(example) { return example.match(/\.\.\/.+\.js/gm); } +/** @param {string} str */ function cleanContents(str) { return str.replace(/\t/g, ' ').trim(); } +/** @param {string} example */ function getJsonPaths(example) { const match = example.match(/\.\/.+\.json/gm); if (match) { - return match.map(d => d.replace('../../', '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '')); } return []; } +/** @param {string} example */ function getJsPaths(example) { const match = example.match(/\.\/.+\.js('|")/gm); if (match) { - return match.map(d => d.replace('../../', '').replace(/('|")/g, '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '').replace(/('|")/g, '')); } return []; } +/** @param {string} example */ function getCsvPaths(example) { const match = example.match(/\.\/.+\.csv/gm); if (match) { - return match.map(d => d.replace('../../', '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '')); } return []; } +/** @param {string} example */ function cleanMain(example) { const cleaned = example .replace(/\t/g, ' ') @@ -42,14 +48,16 @@ function cleanMain(example) { return cleaned; } +/** @param {string} example */ function getComponentPaths(example) { const match = example.match(/\.?\.\/.+svelte/gm); if (match) { - return match.map(d => d.replace('../../', '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '')); } return []; } +/** @type {import('@sveltejs/kit').RequestHandler} */ export async function GET({ params }) { // the `slug` parameter is available because // this file is called [slug].json.js @@ -73,55 +81,67 @@ export async function GET({ params }) { const dekPath = `src/content/examples-ssr/${slug}.md`; const dek = fs.existsSync(dekPath) ? fs.readFileSync(dekPath, 'utf-8') : ''; - const components = getComponentPaths(example).map(d => { - return { - title: `./${d}`, - contents: cleanContents(fs.readFileSync(`src/${d}`, 'utf-8')) - }; - }); - - const modules = getJsPaths(example).map(d => { - return { - title: d.replace('../', ''), - contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) - }; - }); - - const jsons = getJsonPaths(example).map(d => { - return { - title: d.replace('../', ''), - contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) - }; - }); - - const csvs = getCsvPaths(example).map(d => { - return { - title: d.replace('../', ''), - contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) - }; - }); + const components = getComponentPaths(example).map( + /** @param {string} d */ d => { + return { + title: `./${d}`, + contents: cleanContents(fs.readFileSync(`src/${d}`, 'utf-8')) + }; + } + ); + + const modules = getJsPaths(example).map( + /** @param {string} d */ d => { + return { + title: d.replace('../', ''), + contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) + }; + } + ); + + const jsons = getJsonPaths(example).map( + /** @param {string} d */ d => { + return { + title: d.replace('../', ''), + contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) + }; + } + ); + + const csvs = getCsvPaths(example).map( + /** @param {string} d */ d => { + return { + title: d.replace('../', ''), + contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) + }; + } + ); const componentModulesMatches = getComponentJsPaths(components.map(d => d.contents).join('')); const componentModules = componentModulesMatches === null ? [] - : componentModulesMatches.map(d => { - return { - title: d.replace('../', './'), - contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) - }; - }); + : componentModulesMatches.map( + /** @param {string} d */ d => { + return { + title: d.replace('../', './'), + contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) + }; + } + ); const componentComponentMatches = getComponentPaths(components.map(d => d.contents).join('')); const componentComponents = componentComponentMatches === null ? [] - : componentComponentMatches.map(d => { - return { - title: d.replace('./', './_components/'), - contents: cleanContents(fs.readFileSync(d.replace('./', 'src/_components/'), 'utf-8')) - }; - }); + : componentComponentMatches.map( + /** @param {string} d */ d => { + return { + title: d.replace('./', './_components/'), + contents: cleanContents(fs.readFileSync(d.replace('./', 'src/_components/'), 'utf-8')) + }; + } + ); const response = { main, diff --git a/src/routes/example-ssr/[slug]/+page.svelte b/src/routes/example-ssr/[slug]/+page.svelte index 5086b2771..f95d0b484 100644 --- a/src/routes/example-ssr/[slug]/+page.svelte +++ b/src/routes/example-ssr/[slug]/+page.svelte @@ -23,10 +23,19 @@ let active = $derived(data.active); + /** + * @param {string} text + * @returns {string} + */ function markdownToHtml(text) { return md.render(text); } + /** + * @param {string} str + * @param {string} title + * @returns {string} + */ function highlight(str, title) { const parts = title.split('.'); let ext = parts[parts.length - 1]; diff --git a/src/routes/example/[slug].json/+server.js b/src/routes/example/[slug].json/+server.js index b31a83e2d..ebc5d4af5 100644 --- a/src/routes/example/[slug].json/+server.js +++ b/src/routes/example/[slug].json/+server.js @@ -1,38 +1,44 @@ import { error, json } from '@sveltejs/kit'; import * as fs from 'fs'; +/** @param {string} example */ function getComponentJsPaths(example) { return example.match(/\.\.\/.+\.js/gm); } +/** @param {string} str */ function cleanContents(str) { return str.replace(/\t/g, ' ').trim(); } +/** @param {string} example */ function getJsonPaths(example) { const match = example.match(/\.\/.+\.json/gm); if (match) { - return match.map(d => d.replace('../../', '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '')); } return []; } +/** @param {string} example */ function getJsPaths(example) { const match = example.match(/\.\/.+\.js('|")/gm); if (match) { - return match.map(d => d.replace('../../', '').replace(/('|")/g, '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '').replace(/('|")/g, '')); } return []; } +/** @param {string} example */ function getCsvPaths(example) { const match = example.match(/\.\/.+\.csv/gm); if (match) { - return match.map(d => d.replace('../../', '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '')); } return []; } +/** @param {string} example */ function cleanMain(example) { const cleaned = example .replace(/\t/g, ' ') @@ -42,10 +48,11 @@ function cleanMain(example) { return cleaned; } +/** @param {string} example */ function getComponentPaths(example) { const match = example.match(/\.?\.\/.+svelte/gm); if (match) { - return match.map(d => d.replace('../../', '')); + return match.map(/** @param {string} d */ d => d.replace('../../', '')); } return []; } @@ -74,56 +81,68 @@ export async function GET({ params }) { const dekPath = `src/content/examples/${slug}.md`; const dek = fs.existsSync(dekPath) ? fs.readFileSync(dekPath, 'utf-8') : ''; - const components = getComponentPaths(example).map(d => { - return { - title: `./${d}`, - contents: cleanContents(fs.readFileSync(`src/${d}`, 'utf-8')) - }; - }); - - const modules = getJsPaths(example).map(d => { - return { - title: d.replace('../', ''), - contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) - }; - }); - - const jsons = getJsonPaths(example).map(d => { - return { - title: d.replace('../', ''), - contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) - }; - }); - - const csvs = getCsvPaths(example).map(d => { - return { - title: d.replace('../', ''), - contents: cleanContents(fs.readFileSync(d.replace('./../', 'src/'), 'utf-8')) - }; - }); + const components = getComponentPaths(example).map( + /** @param {string} d */ d => { + return { + title: `./${d}`, + contents: cleanContents(fs.readFileSync(`src/${d}`, 'utf-8')) + }; + } + ); + + const modules = getJsPaths(example).map( + /** @param {string} d */ d => { + return { + title: d.replace('../', ''), + contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) + }; + } + ); + + const jsons = getJsonPaths(example).map( + /** @param {string} d */ d => { + return { + title: d.replace('../', ''), + contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) + }; + } + ); + + const csvs = getCsvPaths(example).map( + /** @param {string} d */ d => { + return { + title: d.replace('../', ''), + contents: cleanContents(fs.readFileSync(d.replace('./../', 'src/'), 'utf-8')) + }; + } + ); const componentModulesMatches = getComponentJsPaths(components.map(d => d.contents).join('')); const componentModules = componentModulesMatches === null ? [] - : componentModulesMatches.map(d => { - return { - title: d.replace('../', './'), - contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) - }; - }); + : componentModulesMatches.map( + /** @param {string} d */ d => { + return { + title: d.replace('../', './'), + contents: cleanContents(fs.readFileSync(d.replace('../', 'src/'), 'utf-8')) + }; + } + ); const componentComponentMatches = getComponentPaths(components.map(d => d.contents).join('')); const componentComponents = componentComponentMatches === null ? [] - : componentComponentMatches.map(d => { - // console.log('d', d, d.replace('./', './_components/')); - return { - title: d.replace('./', './_components/'), - contents: cleanContents(fs.readFileSync(d.replace('./', 'src/_components/'), 'utf-8')) - }; - }); + : componentComponentMatches.map( + /** @param {string} d */ d => { + // console.log('d', d, d.replace('./', './_components/')); + return { + title: d.replace('./', './_components/'), + contents: cleanContents(fs.readFileSync(d.replace('./', 'src/_components/'), 'utf-8')) + }; + } + ); // console.log(componentComponents); diff --git a/src/routes/guide.json/+server.js b/src/routes/guide.json/+server.js index 5facb1a5c..0b9c2345c 100644 --- a/src/routes/guide.json/+server.js +++ b/src/routes/guide.json/+server.js @@ -1,6 +1,7 @@ import { json } from '@sveltejs/kit'; import getSections from '../../_modules/getSections.js'; +/** @type {ReturnType | undefined} */ let data; export async function GET() { diff --git a/src/routes/guide/+page.svelte b/src/routes/guide/+page.svelte index a1bc4725e..65b05bf0f 100644 --- a/src/routes/guide/+page.svelte +++ b/src/routes/guide/+page.svelte @@ -4,15 +4,18 @@ /** @type {import('./$types').PageProps} */ let { data } = $props(); + /** @type {HTMLElement | undefined} */ let container; + /** @type {number[]} */ let positions = []; let lastId = 'introduction'; let activeGuideSection = $state(); + /** @type {HTMLElement[]} */ let anchors = []; $effect(() => { - if (typeof window !== 'undefined') { - anchors = container.querySelectorAll('[id]'); + if (typeof window !== 'undefined' && container) { + anchors = /** @type {HTMLElement[]} */ ([...container.querySelectorAll('[id]')]); lastId = window.location.hash.slice(1); activeGuideSection = lastId; @@ -24,12 +27,9 @@ function onresize() { if (container) { const { top } = container.getBoundingClientRect(); - positions = [].map.call( - anchors, - /** @param {HTMLAnchorElement} anchor */ anchor => { - return anchor.getBoundingClientRect().top - top; - } - ); + positions = anchors.map(anchor => { + return anchor.getBoundingClientRect().top - top; + }); } } @@ -72,7 +72,9 @@ {#each data.sections as section}
  • - {section.slug.replace(/^\w/, d => d.toUpperCase()).replaceAll('-', ' ')}- {section.slug + .replace(/^\w/, /** @param {string} d */ d => d.toUpperCase()) + .replaceAll('-', ' ')}
  • {/each} diff --git a/src/scripts/svelte-app/.github/dependabot.yml b/src/scripts/svelte-app/.github/dependabot.yml index f71d469b2..7dea44ee5 100644 --- a/src/scripts/svelte-app/.github/dependabot.yml +++ b/src/scripts/svelte-app/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: 'npm' # See documentation for possible values + directory: '/' # Location of package manifests schedule: - interval: "monthly" + interval: 'monthly' diff --git a/src/scripts/svelte-app/src/app.css b/src/scripts/svelte-app/src/app.css index 9f5167fd7..9d3b9c4f3 100644 --- a/src/scripts/svelte-app/src/app.css +++ b/src/scripts/svelte-app/src/app.css @@ -10,7 +10,8 @@ body { margin: 0; padding: 0; box-sizing: border-box; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; }