Skip to content

Commit

Permalink
feat: warn and ignore custom index.html with doctype declaration (#1793)
Browse files Browse the repository at this point in the history
  • Loading branch information
KermanX committed Aug 11, 2024
1 parent 0436ca6 commit d72f0a0
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 70 deletions.
4 changes: 2 additions & 2 deletions packages/slidev/node/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { build as viteBuild } from 'vite'
import connect from 'connect'
import sirv from 'sirv'
import type { BuildArgs, ResolvedSlidevOptions } from '@slidev/types'
import { getIndexHtml, resolveViteConfigs } from './shared'
import { resolveViteConfigs } from './shared'

export async function build(
options: ResolvedSlidevOptions,
Expand All @@ -19,7 +19,7 @@ export async function build(
if (fs.existsSync(indexPath))
originalIndexHTML = await fs.readFile(indexPath, 'utf-8')

await fs.writeFile(indexPath, await getIndexHtml(options), 'utf-8')
await fs.writeFile(indexPath, options.utils.indexHtml, 'utf-8')
let config: ResolvedConfig = undefined!

try {
Expand Down
63 changes: 4 additions & 59 deletions packages/slidev/node/commands/shared.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { existsSync, promises as fs } from 'node:fs'
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { loadConfigFromFile, mergeConfig } from 'vite'
import type { ConfigEnv, InlineConfig } from 'vite'
import type { ResolvedSlidevOptions, SlidevData, SlidevServerOptions } from '@slidev/types'
import MarkdownIt from 'markdown-it'
import { slash } from '@antfu/utils'
import type { ConfigEnv, InlineConfig } from 'vite'
import { loadConfigFromFile, mergeConfig } from 'vite'
import markdownItLink from '../syntax/markdown-it/markdown-it-link'
import { generateGoogleFontsUrl, stringifyMarkdownTokens } from '../utils'
import { toAtFS } from '../resolver'
import { version } from '../../package.json'
import { stringifyMarkdownTokens } from '../utils'
import { ViteSlidevPlugin } from '../vite'

export const sharedMd = MarkdownIt({ html: true })
Expand All @@ -21,58 +18,6 @@ export function getSlideTitle(data: SlidevData) {
return slideTitle === 'Slidev - Slidev' ? 'Slidev' : slideTitle
}

function escapeHtml(unsafe: unknown) {
return JSON.stringify(
String(unsafe)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;'),
)
}

export async function getIndexHtml({ mode, entry, clientRoot, roots, data }: ResolvedSlidevOptions): Promise<string> {
let main = await fs.readFile(join(clientRoot, 'index.html'), 'utf-8')
let head = ''
let body = ''

const { info, author, keywords } = data.headmatter
head += [
`<meta name="slidev:version" content="${version}">`,
mode === 'dev' && `<meta charset="slidev:entry" content="${slash(entry)}">`,
`<link rel="icon" href="${data.config.favicon}">`,
`<title>${getSlideTitle(data)}</title>`,
info && `<meta name="description" content=${escapeHtml(info)}>`,
author && `<meta name="author" content=${escapeHtml(author)}>`,
keywords && `<meta name="keywords" content=${escapeHtml(Array.isArray(keywords) ? keywords.join(', ') : keywords)}>`,
].filter(Boolean).join('\n')

for (const root of roots) {
const path = join(root, 'index.html')
if (!existsSync(path))
continue

const index = await fs.readFile(path, 'utf-8')

head += `\n${(index.match(/<head>([\s\S]*?)<\/head>/i)?.[1] || '').trim()}`
body += `\n${(index.match(/<body>([\s\S]*?)<\/body>/i)?.[1] || '').trim()}`
}

if (data.features.tweet)
body += '\n<script async src="https://platform.twitter.com/widgets.js"></script>'

if (data.config.fonts.webfonts.length && data.config.fonts.provider !== 'none')
head += `\n<link rel="stylesheet" href="${generateGoogleFontsUrl(data.config.fonts)}" type="text/css">`

main = main
.replace('__ENTRY__', toAtFS(join(clientRoot, 'main.ts')))
.replace('<!-- head -->', head)
.replace('<!-- body -->', body)

return main
}

export async function resolveViteConfigs(
options: ResolvedSlidevOptions,
baseConfig: InlineConfig,
Expand Down
18 changes: 11 additions & 7 deletions packages/slidev/node/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getThemeMeta, resolveTheme } from './integrations/themes'
import { resolveAddons } from './integrations/addons'
import { getRoots, resolveEntry } from './resolver'
import setupShiki from './setups/shiki'
import setupIndexHtml from './setups/indexHtml'

const debug = Debug('slidev:options')

Expand Down Expand Up @@ -50,7 +51,7 @@ export async function resolveOptions(
themeMeta,
}

const resolved: ResolvedSlidevOptions = {
const resolved: Omit<ResolvedSlidevOptions, 'utils'> = {
...rootsInfo,
...entryOptions,
data,
Expand All @@ -61,21 +62,24 @@ export async function resolveOptions(
themeRoots,
addonRoots,
roots,
utils: await createDataUtils(data, rootsInfo.clientRoot, roots),
}

return resolved
return {
...resolved,
utils: await createDataUtils(resolved),
}
}

export async function createDataUtils(data: SlidevData, clientRoot: string, roots: string[]): Promise<ResolvedSlidevUtils> {
const monacoTypesIgnorePackagesMatches = (data.config.monacoTypesIgnorePackages || [])
export async function createDataUtils(resolved: Omit<ResolvedSlidevOptions, 'utils'>): Promise<ResolvedSlidevUtils> {
const monacoTypesIgnorePackagesMatches = (resolved.data.config.monacoTypesIgnorePackages || [])
.map(i => mm.matcher(i))

let _layouts_cache_time = 0
let _layouts_cache: Record<string, string> = {}

return {
...await setupShiki(roots),
...await setupShiki(resolved.roots),
indexHtml: setupIndexHtml(resolved),
isMonacoTypesIgnored: pkg => monacoTypesIgnorePackagesMatches.some(i => i(pkg)),
getLayouts: () => {
const now = Date.now()
Expand All @@ -84,7 +88,7 @@ export async function createDataUtils(data: SlidevData, clientRoot: string, root

const layouts: Record<string, string> = {}

for (const root of [clientRoot, ...roots]) {
for (const root of [resolved.clientRoot, ...resolved.roots]) {
const layoutPaths = fg.sync('layouts/**/*.{vue,ts}', {
cwd: root,
absolute: true,
Expand Down
61 changes: 61 additions & 0 deletions packages/slidev/node/setups/indexHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { slash } from '@antfu/utils'
import type { ResolvedSlidevOptions } from '@slidev/types'
import { white, yellow } from 'kolorist'
import { escapeHtml } from 'markdown-it/lib/common/utils.mjs'
import { version } from '../../package.json'
import { getSlideTitle } from '../commands/shared'
import { toAtFS } from '../resolver'
import { generateGoogleFontsUrl } from '../utils'

function toAttrValue(unsafe: unknown) {
return JSON.stringify(escapeHtml(String(unsafe)))
}

export default function setupIndexHtml({ mode, entry, clientRoot, userRoot, roots, data }: Omit<ResolvedSlidevOptions, 'utils'>): string {
let main = readFileSync(join(clientRoot, 'index.html'), 'utf-8')
let head = ''
let body = ''

const { info, author, keywords } = data.headmatter
head += [
`<meta name="slidev:version" content="${version}">`,
mode === 'dev' && `<meta charset="slidev:entry" content="${slash(entry)}">`,
`<link rel="icon" href="${data.config.favicon}">`,
`<title>${getSlideTitle(data)}</title>`,
info && `<meta name="description" content=${toAttrValue(info)}>`,
author && `<meta name="author" content=${toAttrValue(author)}>`,
keywords && `<meta name="keywords" content=${toAttrValue(Array.isArray(keywords) ? keywords.join(', ') : keywords)}>`,
].filter(Boolean).join('\n')

for (const root of roots) {
const path = join(root, 'index.html')
if (!existsSync(path))
continue

const index = readFileSync(path, 'utf-8')

if (root === userRoot && index.includes('<!DOCTYPE')) {
console.error(yellow(`[Slidev] Ignored provided index.html with doctype declaration. (${white(path)})`))
console.error(yellow('This file may be generated by Slidev, please remove it from your project.'))
continue
}

head += `\n${(index.match(/<head>([\s\S]*?)<\/head>/i)?.[1] || '').trim()}`
body += `\n${(index.match(/<body>([\s\S]*?)<\/body>/i)?.[1] || '').trim()}`
}

if (data.features.tweet)
body += '\n<script async src="https://platform.twitter.com/widgets.js"></script>'

if (data.config.fonts.webfonts.length && data.config.fonts.provider !== 'none')
head += `\n<link rel="stylesheet" href="${generateGoogleFontsUrl(data.config.fonts)}" type="text/css">`

main = main
.replace('__ENTRY__', toAtFS(join(clientRoot, 'main.ts')))
.replace('<!-- head -->', head)
.replace('<!-- body -->', body)

return main
}
3 changes: 1 addition & 2 deletions packages/slidev/node/vite/extendConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { mergeConfig } from 'vite'
import { slash, uniq } from '@antfu/utils'
import type { ResolvedSlidevOptions } from '@slidev/types'
import { createResolve } from 'mlly'
import { getIndexHtml } from '../commands/shared'
import { isInstalledGlobally, resolveImportPath, toAtFS } from '../resolver'

const INCLUDE_GLOBAL = [
Expand Down Expand Up @@ -197,7 +196,7 @@ export function createConfigPlugin(options: ResolvedSlidevOptions): Plugin {
if (req.url!.endsWith('.html')) {
res.setHeader('Content-Type', 'text/html')
res.statusCode = 200
res.end(await getIndexHtml(options))
res.end(options.utils.indexHtml)
return
}
next()
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface ResolvedSlidevOptions extends RootsInfo, SlidevEntryOptions {
export interface ResolvedSlidevUtils {
shiki: HighlighterGeneric<any, any>
shikiOptions: MarkdownItShikiOptions
indexHtml: string
isMonacoTypesIgnored: (pkg: string) => boolean
getLayouts: () => Record<string, string>
}
Expand Down

0 comments on commit d72f0a0

Please sign in to comment.