Typescript support

This commit is contained in:
Michal Vanko 2020-09-26 14:49:58 +02:00
parent 57ed783083
commit dcfc3eccc2
22 changed files with 3132 additions and 1264 deletions

View File

@ -8,7 +8,7 @@
<tr> <tr>
<td <td
rowspan="3" rowspan="3"
style="vertical-align: center; border-right: 1px solid #212138; width: 80px" style="vertical-align: center; width: 80px"
> >
<img <img
src="https://michalvanko.dev/images/m-logo.png" src="https://michalvanko.dev/images/m-logo.png"
@ -19,18 +19,18 @@
</td> </td>
<td <td
style="color: #212138; font-size: 12px; padding-bottom: 4px; padding-left: 8px;" style="color: #212138; font-size: 12px; padding-bottom: 4px; padding-left: 4px;"
> >
<span>Michal Vanko</span> <span>Michal Vanko</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="color: 42436a; font-size: 11px; padding-left: 8px;"> <td style="color: #42436a; font-size: 11px; padding-left: 4px;">
<span>Software Architect and Consultant</span> <span>Software Architect and Consultant</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="font-size: 11px; padding-left: 8px;"> <td style="font-size: 11px; padding-left: 4px;">
<a <a
target="_blank" target="_blank"
href="https://michalvanko.dev" href="https://michalvanko.dev"

4096
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,31 +14,41 @@
"dependencies": { "dependencies": {
"classnames": "^2.2.6", "classnames": "^2.2.6",
"compression": "^1.7.4", "compression": "^1.7.4",
"date-fns": "^2.11.1", "@rollup/plugin-typescript": "^6.0.0",
"feed": "^4.2.0", "date-fns": "^2.16.1",
"front-matter": "^3.1.0", "feed": "^4.2.1",
"marked": "^0.8.2", "front-matter": "^4.0.2",
"marked": "^1.1.1",
"polka": "^0.5.2", "polka": "^0.5.2",
"ramda": "^0.27.0", "ramda": "^0.27.1",
"rollup-plugin-svg": "^2.0.0", "sirv": "^1.0.6",
"sirv": "^0.4.2", "svelte-image": "^0.2.7"
"svelte-image": "^0.1.9"
}, },
"devDependencies": { "devDependencies": {
"npm-run-all": "^4.1.5", "@babel/core": "^7.11.6",
"sapper": "^0.27.12",
"svelte": "^3.20.1",
"@babel/core": "^7.9.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0", "@babel/plugin-transform-runtime": "^7.11.5",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.11.5",
"@babel/runtime": "^7.9.2", "@babel/runtime": "^7.11.2",
"rollup": "^2.3.2", "@rollup/plugin-typescript": "^6.0.0",
"@tsconfig/svelte": "^1.0.10",
"@types/classnames": "^2.2.10",
"@types/ramda": "^0.27.19",
"autoprefixer": "^10.0.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.0.9",
"rollup": "^2.28.2",
"rollup-plugin-babel": "^4.4.0", "rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"rollup-plugin-svelte": "^5.2.1", "rollup-plugin-svelte": "^6.0.1",
"rollup-plugin-terser": "^5.3.0" "rollup-plugin-svg": "^2.0.0",
"rollup-plugin-terser": "^7.0.2",
"sapper": "^0.28.9",
"svelte": "^3.28.0",
"svelte-check": "^1.0.51",
"svelte-preprocess": "^4.3.2",
"typescript": "^4.0.3"
} }
} }

View File

@ -7,22 +7,24 @@ import { terser } from 'rollup-plugin-terser'
import config from 'sapper/config/rollup.js' import config from 'sapper/config/rollup.js'
import pkg from './package.json' import pkg from './package.json'
import svg from 'rollup-plugin-svg' import svg from 'rollup-plugin-svg'
import image from 'svelte-image' // import image from 'svelte-image'
import sveltePreprocess from 'svelte-preprocess'
import typescript from '@rollup/plugin-typescript'
const mode = process.env.NODE_ENV const mode = process.env.NODE_ENV
const dev = mode === 'development' const dev = mode === 'development'
const legacy = !!process.env.SAPPER_LEGACY_BUILD const legacy = !!process.env.SAPPER_LEGACY_BUILD
const onwarn = (warning, onwarn) => const onwarn = (warning, onwarn) =>
(warning.code === 'CIRCULAR_DEPENDENCY' && (warning.code === 'MISSING_EXPORT' && /'preload'/.test(warning.message)) ||
/[/\\]@sapper[/\\]/.test(warning.message)) || (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) ||
onwarn(warning) onwarn(warning)
const dedupe = (importee) => const dedupe = (importee) =>
importee === 'svelte' || importee.startsWith('svelte/') importee === 'svelte' || importee.startsWith('svelte/')
export default { export default {
client: { client: {
input: config.client.input(), input: config.client.input().replace(/\.js$/, ".ts"),
output: config.client.output(), output: config.client.output(),
plugins: [ plugins: [
replace({ replace({
@ -33,9 +35,16 @@ export default {
dev, dev,
hydratable: true, hydratable: true,
emitCss: true, emitCss: true,
// Disabled automatic image compression
// preprocess: { // preprocess: {
// ...image(), // ...image(),
// }, // },
preprocess: sveltePreprocess({
sourceMap: dev,
defaults: {
script: 'typescript',
},
}),
}), }),
resolve({ resolve({
browser: true, browser: true,
@ -78,7 +87,7 @@ export default {
}, },
server: { server: {
input: config.server.input(), input: config.server.input().server.replace(/\.js$/, ".ts"),
output: config.server.output(), output: config.server.output(),
plugins: [ plugins: [
replace({ replace({
@ -88,6 +97,13 @@ export default {
svelte({ svelte({
generate: 'ssr', generate: 'ssr',
dev, dev,
hydratable: true,
preprocess: sveltePreprocess({
sourceMap: dev,
defaults: {
script: 'typescript',
}
}),
// preprocess: { // preprocess: {
// ...image(), // ...image(),
// }, // },
@ -96,6 +112,7 @@ export default {
dedupe, dedupe,
}), }),
commonjs(), commonjs(),
typescript({ sourceMap: dev}),
svg(), svg(),
], ],
external: Object.keys(pkg.dependencies).concat( external: Object.keys(pkg.dependencies).concat(

View File

@ -1,7 +1,8 @@
<script> <script lang="typescript">
import { format } from 'date-fns' import { format } from 'date-fns'
import type { PostContent } from '../../routes/blog/_content';
export let post export let post: PostContent
</script> </script>
<style> <style>

View File

@ -1,5 +1,7 @@
<script> <script lang="typescript">
export let project import type { ProjectAttributes } from "../../routes/portfolio/index.json";
export let project: ProjectAttributes
</script> </script>
<style> <style>

View File

@ -1,7 +1,7 @@
import marked from 'marked' import marked from 'marked'
export function parseField(field) { export function parseField<T>(field: string) {
return item => ({ return (item: T) => ({
...item, ...item,
[field]: marked(item[field]), [field]: marked(item[field]),
}) })

View File

@ -2,13 +2,18 @@ import { readFile } from 'fs'
import { promisify } from 'util' import { promisify } from 'util'
import fm from 'front-matter' import fm from 'front-matter'
import { parseField } from '../../markdown/parse-markdown' import { parseField } from '../../markdown/parse-markdown'
import type { PostAttributes } from './_content'
export interface SinglePost {
body: string
}
export async function get(req, res, next) { export async function get(req, res, next) {
// the `slug` parameter is available because // the `slug` parameter is available because
// this file is called [slug].json.js // this file is called [slug].json.js
const { slug } = req.params const { slug } = req.params
let postSource let postSource: string
try { try {
postSource = await promisify(readFile)(`_posts/blog/${slug}.md`, 'utf-8') postSource = await promisify(readFile)(`_posts/blog/${slug}.md`, 'utf-8')
} catch (e) { } catch (e) {
@ -22,9 +27,9 @@ export async function get(req, res, next) {
return return
} }
const parsedPost = fm(postSource) const parsedPost = fm<PostAttributes>(postSource)
const response = parseField('body')({ const response = parseField<SinglePost>('body')({
...parsedPost.attributes, ...parsedPost.attributes,
body: parsedPost.body, body: parsedPost.body,
}) })

View File

@ -64,8 +64,8 @@
<svelte:head> <svelte:head>
<title>{post.title}</title> <title>{post.title}</title>
<link rel="stylesheet" href="prism.css" /> <link rel="stylesheet" href="/prism.css" />
<script src="prism.js"> <script src="../../../static/prism.js" defer>
</script> </script>
</svelte:head> </svelte:head>

View File

@ -7,17 +7,32 @@ import marked from 'marked'
const { NODE_ENV } = process.env const { NODE_ENV } = process.env
export async function getBlogListing(tag) { export interface PostAttributes {
layout: string
title: string
published: boolean
date: string
thumbnail: string
tags: string[]
}
export interface PostContent extends PostAttributes {
preview: string
slug: string
published: boolean
}
export async function getBlogListing(tag?: string) {
const files = await promisify(readdir)(`_posts/blog/`, 'utf-8') const files = await promisify(readdir)(`_posts/blog/`, 'utf-8')
const filteredFiles = filterDevelopmentFiles(files) const filteredFiles = filterDevelopmentFiles(files)
const contents = await Promise.all( const contents = await Promise.all(
filteredFiles.map(async file => { filteredFiles.map(async (file) => {
const fileContent = await promisify(readFile)( const fileContent = await promisify(readFile)(
`_posts/blog/${file}`, `_posts/blog/${file}`,
'utf-8' 'utf-8'
) )
const parsedAttributes = fm(fileContent) const parsedAttributes = fm<PostAttributes>(fileContent)
const lineOfTextRegExp = /^(?:\w|\[).+/gm const lineOfTextRegExp = /^(?:\w|\[).+/gm
const lines = parsedAttributes.body const lines = parsedAttributes.body
@ -33,23 +48,30 @@ export async function getBlogListing(tag) {
} }
}) })
) )
const filteredContents = pipe( const filteredContents = pipe<
PostContent[],
PostContent[],
PostContent[],
PostContent[],
PostContent[]
>(
sortBy(prop('date')), sortBy(prop('date')),
reverse, reverse,
filter(article => article.published), filter<typeof contents[0]>((article) => article.published),
partial(filterByTag, [tag]) partial(filterByTag, [tag])
)(contents) )(contents)
return filteredContents return filteredContents
} }
function filterDevelopmentFiles(files) { function filterDevelopmentFiles(files: string[]) {
return NODE_ENV !== 'production' return NODE_ENV !== 'production'
? files ? files
: files.filter(file => !file.startsWith('dev-')) : files.filter((file) => !file.startsWith('dev-'))
} }
function filterByTag(tag, contents) { function filterByTag(tag: string | undefined, contents: PostContent[]) {
return tag ? contents.filter(content => content.tags.includes(tag)) : contents return tag
? contents.filter((content) => content.tags.includes(tag))
: contents
} }

View File

@ -1,4 +1,4 @@
<script context="module"> <script context="module" lang="typescript">
export function preload({ params, query }) { export function preload({ params, query }) {
const blogQuery = query const blogQuery = query
? '?' + ? '?' +
@ -14,11 +14,12 @@
} }
</script> </script>
<script> <script lang="typescript">
import ArticleFooter from '../../components/blog/article-footer.svelte' import ArticleFooter from '../../components/blog/article-footer.svelte'
import type { PostContent } from './_content';
export let posts export let posts: PostContent[]
export let query export let query
</script> </script>
<style> <style>

View File

@ -25,14 +25,16 @@ export async function getFeed() {
}) })
const blogListing = await getBlogListing() const blogListing = await getBlogListing()
blogListing.forEach(post => { blogListing.forEach((post) => {
feed.addItem({ feed.addItem({
title: post.title, title: post.title,
id: `https://michalvanko.dev/blog/${post.slug}`, id: `https://michalvanko.dev/blog/${post.slug}`,
link: `https://michalvanko.dev/blog/${post.slug}`, link: `https://michalvanko.dev/blog/${post.slug}`,
description: post.preview, description: post.preview,
date: post.date, date: new Date(post.date),
image: post.thumbnail ? `https://michalvanko.dev/${post.thumbnail}` : undefined, image: post.thumbnail
? `https://michalvanko.dev/${post.thumbnail}`
: undefined,
}) })
}) })
return feed return feed

View File

@ -1,11 +1,10 @@
import { getFeed } from './_feed' import { getFeed } from './_feed'
export async function get(req, res) { export async function get(req, res) {
const feed = await getFeed() const feed = await getFeed()
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}) })
res.end(feed.json1()) res.end(feed.json1())
} }

View File

@ -1,11 +1,10 @@
import { getFeed } from './_feed' import { getFeed } from './_feed'
export async function get(req, res) { export async function get(req, res) {
const feed = await getFeed() const feed = await getFeed()
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/xml', 'Content-Type': 'application/xml',
}) })
res.end(feed.rss2()) res.end(feed.rss2())
} }

View File

@ -4,8 +4,26 @@ import fm from 'front-matter'
import marked from 'marked' import marked from 'marked'
import { parseField } from '../../markdown/parse-markdown' import { parseField } from '../../markdown/parse-markdown'
export interface RecordAttributes {
name: string
description: string
displayed: boolean
}
export interface ProjectAttributes extends RecordAttributes {
image: string
}
export interface PortfolioAttributes {
title: string
work_history_prelude: string
work_history: string[]
projects: ProjectAttributes[]
education: RecordAttributes[]
}
export async function get(req, res, next) { export async function get(req, res, next) {
let pageSource let pageSource: string
try { try {
pageSource = await promisify(readFile)('_pages/portfolio.md', 'utf-8') pageSource = await promisify(readFile)('_pages/portfolio.md', 'utf-8')
} catch (e) { } catch (e) {
@ -14,15 +32,15 @@ export async function get(req, res, next) {
return return
} }
const parsed = fm(pageSource) const parsed = fm<PortfolioAttributes>(pageSource)
const workHistory = (parsed.attributes.work_history || []).map( const workHistory = (parsed.attributes.work_history || []).map(
parseField('description') parseField('description')
) )
const projects = (parsed.attributes.projects || []) const projects = (parsed.attributes.projects || [])
.filter(project => project.displayed) .filter((project) => project.displayed)
.map(parseField('description')) .map(parseField('description'))
const education = (parsed.attributes.education || []) const education = (parsed.attributes.education || [])
.filter(education => education.displayed) .filter((education) => education.displayed)
.map(parseField('description')) .map(parseField('description'))
const response = { const response = {

View File

@ -1,17 +0,0 @@
import sirv from 'sirv';
import polka from 'polka';
import compression from 'compression';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
polka() // You can also use Express
.use(
compression({ threshold: 0 }),
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, err => {
if (err) console.log('error', err);
});

17
src/server.ts Normal file
View File

@ -0,0 +1,17 @@
import sirv from 'sirv'
import polka from 'polka'
import compression from 'compression'
import * as sapper from '@sapper/server'
const { PORT, NODE_ENV } = process.env
const dev = NODE_ENV === 'development'
polka() // You can also use Express
.use(
compression({ threshold: 0 }),
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, (err) => {
if (err) console.log('error', err)
})

View File

@ -28,15 +28,16 @@
<!-- This contains the contents of the <svelte:head> component, if <!-- This contains the contents of the <svelte:head> component, if
the current page has one --> the current page has one -->
%sapper.head% %sapper.head%
<!-- Sapper creates a <script> tag containing `app/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
</head> </head>
<body> <body>
<!-- The application will be rendered inside this element, <!-- The application will be rendered inside this element,
because `app/client.js` references it --> because `app/client.js` references it -->
<div id="sapper">%sapper.html%</div> <div id="sapper">%sapper.html%</div>
<!-- Sapper creates a <script> tag containing `app/client.js`
and anything else it needs to hydrate the app and
initialise the router -->
%sapper.scripts%
</body> </body>
</html> </html>

10
tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*", "src/node_modules"],
"exclude": ["node_modules/*", "__sapper__/*", "static/*"],
"compilerOptions": {
"types": ["svelte", "node", "@sapper"],
"typeRoots": ["typings"],
"target": "ES2017"
}
}

41
typings/@sapper/index.d.ts vendored Normal file
View File

@ -0,0 +1,41 @@
declare module '@sapper/app' {
interface Redirect {
statusCode: number
location: string
}
function goto(
href: string,
opts: { replaceState: boolean; noscroll: boolean }
): Promise<unknown>
function prefetch(
href: string
): Promise<{ redirect?: Redirect; data?: unknown }>
function prefetchRoutes(pathnames: string[]): Promise<unknown>
function start(opts: { target: Node }): Promise<unknown>
const stores: () => unknown
export { goto, prefetch, prefetchRoutes, start, stores }
}
declare module '@sapper/server' {
import { RequestHandler } from 'express'
interface MiddlewareOptions {
session?: (req: Express.Request, res: Express.Response) => unknown
ignore?: unknown
}
function middleware(opts?: MiddlewareOptions): RequestHandler
export { middleware }
}
declare module '@sapper/service-worker' {
const timestamp: number
const files: string[]
const shell: string[]
const routes: { pattern: RegExp }[]
export { timestamp, files, files as assets, shell, routes }
}