diff --git a/.cspell.json b/.cspell.json index efa8986f..6899813c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -37,7 +37,8 @@ "apigen", "brotli", "vercel", - "preact" + "preact", + "opendir" ], "overrides": [] } diff --git a/.prettierignore b/.prettierignore index b913490c..ab70c787 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,7 +4,8 @@ dist mangle.json extractor docs/api -temp/*.api.json +temp .vercel/project.json www/public +www/public-br www/template diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 00000000..a52e1b17 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,3 @@ +www/_files +www/public +www/template diff --git a/www/.babelrc.json b/www/.babelrc.json index 286e8793..9a0e8c20 100644 --- a/www/.babelrc.json +++ b/www/.babelrc.json @@ -1,6 +1,6 @@ { "presets": [ - ["@babel/env", { "modules": false }], + ["@babel/env", { "modules": false, "loose": true }], ["@babel/preset-typescript", { "jsxPragma": "h" }] ], "plugins": [ diff --git a/www/.gitignore b/www/.gitignore index 09e3d17b..c240eb4a 100644 --- a/www/.gitignore +++ b/www/.gitignore @@ -1,2 +1,3 @@ +/_files /public /template diff --git a/www/api/index.ts b/www/api/index.ts new file mode 100644 index 00000000..f4943400 --- /dev/null +++ b/www/api/index.ts @@ -0,0 +1,142 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { encodings as parseEncodingsHeader } from '@hapi/accept'; +import { NowRequest, NowResponse } from '@vercel/node'; +import { CompressedFileMetadata } from 'script/compressPublic/types'; + +console.log(fs.readdirSync(path.join(__dirname, '..'))); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const publicFilesList: string[] = (require(path.join( + __dirname, + '..', + '_files/temp/publicFilesList.json', +)) as string[]).map((path) => `/${path}`); +const publicUrlToPublicFilePath = new Map( + publicFilesList.map((filePath) => [ + transformFilePathToUrl(filePath) || '/', + filePath, + ]), +); + +function transformFilePathToUrl(filePath: string): string { + const slashIndexDotHtml = '/index.html'; + if (filePath.endsWith(slashIndexDotHtml)) { + return filePath.slice(0, -slashIndexDotHtml.length); + } + + const dotHtml = '.html'; + if (filePath.endsWith(dotHtml)) { + return filePath.slice(0, -dotHtml.length); + } + + return filePath; +} + +const hashedCacheControl = 'public, max-age=31536000'; +const defaultCacheControl = 'public, max-age=2678400'; + +function setFileSpecificHeaders(response: NowResponse, filePath: string): void { + const extension = path.extname(filePath); + switch (extension) { + case '.js': { + response.setHeader('Content-Type', 'text/javascript'); + response.setHeader('Cache-Control', hashedCacheControl); + break; + } + case '.json': { + response.setHeader('Content-Type', 'application/json'); + response.setHeader('Cache-Control', hashedCacheControl); + break; + } + case '.html': { + response.setHeader('Content-Type', 'text/html'); + response.setHeader('Cache-Control', defaultCacheControl); + break; + } + default: { + throw new Error(`Unexpected extension ${extension}.`); + } + } +} + +function isAcceptingBrotli(request: NowRequest): boolean { + const acceptEncodingHeader = request.headers['accept-encoding']; + if (typeof acceptEncodingHeader === 'string') { + const encodings = parseEncodingsHeader(acceptEncodingHeader); + if (encodings.includes('br')) { + return true; + } + } + return false; +} + +function sendPublicFileBrotli( + response: NowResponse, + publicFilePath: string, +): void { + response.setHeader('Content-Encoding', 'br'); + + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment + const { contentLength } = require(path.join( + __dirname, + '..', + '_files', + 'public-br', + 'metadata', + `${publicFilePath}.json`, + )) as CompressedFileMetadata; + response.setHeader('Content-Length', contentLength); + + fs.createReadStream( + path.join( + __dirname, + '..', + '_files', + 'public-br', + 'binary', + `${publicFilePath}.br`, + ), + ).pipe(response); +} + +function sendPublicFile( + request: NowRequest, + response: NowResponse, + publicFilePath: string, +): void { + setFileSpecificHeaders(response, publicFilePath); + + if (isAcceptingBrotli(request)) { + sendPublicFileBrotli(response, publicFilePath); + return; + } + + fs.createReadStream( + path.join(__dirname, '..', '_files', 'public', publicFilePath), + ).pipe(response); +} + +export default (request: NowRequest, response: NowResponse) => { + const { url } = request; + + if (!url) { + throw new Error('Unexpected: no url.'); + } + + if (url.endsWith('/') && url !== '/') { + response.redirect(308, url.slice(0, -1)); + return; + } + + const publicFilePath = publicUrlToPublicFilePath.get(url); + + if (publicFilePath) { + response.status(200); + sendPublicFile(request, response, publicFilePath); + } else { + response.status(400); + sendPublicFile(request, response, '404.html'); + } +}; diff --git a/www/package.json b/www/package.json index ab03b16b..39108627 100644 --- a/www/package.json +++ b/www/package.json @@ -3,10 +3,13 @@ "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build:docs": "cd .. && npm install && npm run build && cd www && npx -s sh ts-node --project ./tsconfig.ts-node.json ./script/docs/apigen/index.ts", - "build:template": "rimraf template && ncp src template --stopOnErr && npx -s sh ts-node --project ./tsconfig.ts-node.json ./script/template/index.tsx", - "build:public": "npm run build:template && parcel build 'template/**/*.html' --dist-dir public --detailed-report --no-source-maps", - "build": "rimraf public && npm run build:docs && npm run build:public", + "build:docs": "cd .. && npm install && npm run build && cd www && npm run build:docs-analyze", + "build:docs-analyze": "npx -s sh ts-node --project ./tsconfig.ts-node.json ./script/docs/apigen/index.ts", + "build:template": "ncp src template --stopOnErr && npx -s sh ts-node --project ./tsconfig.ts-node.json ./script/template/index.tsx", + "build:public": "npm run build:template && parcel build 'template/**/*.html' --dist-dir _files/public --detailed-report --no-source-maps && rimraf template && npm run build:public-compressed", + "build:public-compressed": "rimraf _files/public-br && npx -s sh ts-node --project ./tsconfig.ts-node.json ./script/compressPublic/index.ts", + "build": "rimraf public _files/public && npm run build:docs && npm run build:public && mkdir public && echo > public/index.html && echo > api/foo.ts", + "vercel-build": "npm run build", "postinstall": "cpy .browserslistrc node_modules/unfetch" }, "dependencies": { diff --git a/www/script/compressPublic/index.ts b/www/script/compressPublic/index.ts new file mode 100644 index 00000000..971fc404 --- /dev/null +++ b/www/script/compressPublic/index.ts @@ -0,0 +1,121 @@ +import * as path from 'path'; +import { promisify } from 'util'; +import * as zlib from 'zlib'; +import * as colors from 'colors'; +import * as fs from 'fs-extra'; +import { exit } from '../exit'; +import { rootDir } from '../rootDir'; +import { CompressedFileMetadata } from './types'; + +const filesDir = path.join(rootDir, 'www', '_files'); +const publicPath = path.join(filesDir, 'public'); +const publicBrPath = path.join(filesDir, 'public-br'); +const publicBrBinaryPath = path.join(publicBrPath, 'binary'); +const publicBrMetadataPath = path.join(publicBrPath, 'metadata'); + +const brotliCompress = promisify(zlib.brotliCompress); +const compressedFiles: string[] = []; + +async function brotliCompressDirectory(subPath: string): Promise { + const dirPublicPath = path.join(publicPath, subPath); + const dirPublicBrBinaryPath = path.join(publicBrBinaryPath, subPath); + const dirPublicBrMetadataPath = path.join(publicBrMetadataPath, subPath); + + console.log(`reading dir ${colors.cyan(path.join('public', subPath))}`); + + if (subPath === '') { + await fs.ensureDir(publicBrPath); + await Promise.all([ + fs.mkdir(publicBrBinaryPath), + fs.mkdir(publicBrMetadataPath), + ]); + } else { + await fs.mkdir(dirPublicBrBinaryPath); + await fs.mkdir(dirPublicBrMetadataPath); + } + + const promises: Promise[] = []; + + for await (const thing of await fs.opendir(dirPublicPath)) { + if (thing.isDirectory()) { + promises.push( + brotliCompressDirectory(path.join(subPath, thing.name)), + ); + continue; + } + + if (!thing.isFile()) { + throw new Error(`${thing.name}: Not a file or directory.`); + } + + const filePublicPath = path.join(dirPublicPath, thing.name); + const filePublicBrBinaryPath = path.join( + dirPublicBrBinaryPath, + `${thing.name}.br`, + ); + const filePublicBrMetadataPath = path.join( + dirPublicBrMetadataPath, + `${thing.name}.json`, + ); + + const fileSubPath = path.join(subPath, thing.name); + const fileHumanPath = path.join('public', subPath, thing.name); + + if (fileSubPath === '404.html') { + console.log( + `not adding ${colors.red(fileSubPath)} to public files list`, + ); + } else { + compressedFiles.push(fileSubPath); + } + + console.log(`compressing file ${colors.red(fileHumanPath)}`); + + promises.push( + fs + .readFile(filePublicPath) + .then(brotliCompress) + .then((compressed) => { + console.log( + `writing compressed file ${colors.cyan(fileHumanPath)}`, + ); + const metadata: CompressedFileMetadata = { + contentLength: compressed.length, + }; + return Promise.all([ + fs.writeFile(filePublicBrBinaryPath, compressed), + fs.writeFile( + filePublicBrMetadataPath, + JSON.stringify(metadata), + ), + ]); + }), + ); + } + + return Promise.all(promises); +} + +const publicFilesListFileName = 'publicFilesList.json'; + +brotliCompressDirectory('') + .then(() => { + console.log( + `writing public file list ${colors.green( + path.join('temp', publicFilesListFileName), + )}`, + ); + const tempDir = path.join(filesDir, 'temp'); + return fs.ensureDir(tempDir).then(() => { + return fs.writeFile( + path.join(filesDir, 'temp', publicFilesListFileName), + JSON.stringify(compressedFiles), + 'utf-8', + ); + }); + }) + .catch((error) => { + console.error('error compressing public directory...'); + console.log(error); + exit(); + }); diff --git a/www/script/compressPublic/types.ts b/www/script/compressPublic/types.ts new file mode 100644 index 00000000..a0969da5 --- /dev/null +++ b/www/script/compressPublic/types.ts @@ -0,0 +1,3 @@ +export interface CompressedFileMetadata { + contentLength: number; +} diff --git a/www/script/docs/apigen/analyze/Context.ts b/www/script/docs/apigen/analyze/Context.ts index b3480517..a3e40e94 100644 --- a/www/script/docs/apigen/analyze/Context.ts +++ b/www/script/docs/apigen/analyze/Context.ts @@ -10,6 +10,7 @@ export interface APIPageDataItem { export interface APIPageData { pageDirectory: string; pageTitle: string; + pageUrl: string; items: APIPageDataItem[]; } diff --git a/www/script/docs/apigen/analyze/build/buildApiPage.ts b/www/script/docs/apigen/analyze/build/buildApiPage.ts index fc7fa3ec..54ce788b 100644 --- a/www/script/docs/apigen/analyze/build/buildApiPage.ts +++ b/www/script/docs/apigen/analyze/build/buildApiPage.ts @@ -137,6 +137,7 @@ export function buildApiPage( title: pageData.pageTitle, tableOfContents: tableOfContents, }, + pageUrl: pageData.pageUrl, children: Array.from(nameToApiItems, ([, apiItems]) => { return buildApiItemImplementationGroup({ apiItems, diff --git a/www/script/docs/apigen/core/nodes/Page.ts b/www/script/docs/apigen/core/nodes/Page.ts index 01ede077..8bfaa1ca 100644 --- a/www/script/docs/apigen/core/nodes/Page.ts +++ b/www/script/docs/apigen/core/nodes/Page.ts @@ -5,11 +5,13 @@ import { Node, CoreNodeType } from '.'; export interface PageParameters extends ContainerParameters { metadata: PageMetadata; + pageUrl: string; } export interface PageBase extends ContainerBase { metadata: PageMetadata; + pageUrl: string; } export function PageBase( @@ -17,6 +19,7 @@ export function PageBase( ): PageBase { return { metadata: parameters.metadata, + pageUrl: parameters.pageUrl, ...ContainerBase({ children: parameters.children }), }; } diff --git a/www/script/docs/apigen/index.ts b/www/script/docs/apigen/index.ts index a8a2fa0c..23213c2a 100644 --- a/www/script/docs/apigen/index.ts +++ b/www/script/docs/apigen/index.ts @@ -2,6 +2,8 @@ import * as crypto from 'crypto'; import * as path from 'path'; import { ApiModel } from '@microsoft/api-extractor-model'; import * as fs from 'fs-extra'; +import { exit } from '../../exit'; +import { rootDir } from '../../rootDir'; import { buildApiPageMap } from './analyze/build/buildApiPageMap'; import { buildApiPageMapToFolder } from './analyze/build/buildApiPageMapToFolder'; import { AnalyzeContext, APIPackageData } from './analyze/Context'; @@ -10,13 +12,7 @@ import { getUniqueExportIdentifierKey, } from './analyze/Identifier'; import { generateSourceMetadata } from './analyze/sourceMetadata'; -import { rootDir } from './rootDir'; -import { - PageNodeMap, - PageNodeMapWithMetadata, - PageNodeMapMetadata, - ApiDocMapPathList, -} from './types'; +import { Pages, PagesMetadata, PagesWithMetadata, PagesUrlList } from './types'; import { addFileToFolder, Folder, @@ -35,6 +31,7 @@ const packageDataList: APIPackageData[] = [ { pageDirectory: 'basics', pageTitle: 'API Reference - Basics', + pageUrl: 'api-basics', items: [ { main: 'Disposable', @@ -79,6 +76,7 @@ const packageDataList: APIPackageData[] = [ { pageDirectory: 'sources', pageTitle: 'API Reference - Sources', + pageUrl: 'api-sources', items: [ { main: 'all' }, { main: 'animationFrames' }, @@ -117,6 +115,7 @@ const packageDataList: APIPackageData[] = [ { pageDirectory: 'operators', pageTitle: 'API Reference - Operators', + pageUrl: 'api-operators', items: [ { main: 'at' }, { main: 'catchError' }, @@ -249,6 +248,7 @@ const packageDataList: APIPackageData[] = [ { pageDirectory: 'subjects', pageTitle: 'API Reference - Subjects', + pageUrl: 'api-subjects', items: [ { main: 'CurrentValueSubject' }, { main: 'FinalValueSubject' }, @@ -262,6 +262,7 @@ const packageDataList: APIPackageData[] = [ { pageDirectory: 'schedule-functions', pageTitle: 'API Reference - Schedule Functions', + pageUrl: 'api-schedule', items: [ { main: 'ScheduleAnimationFrameQueued' }, { main: 'ScheduleInterval' }, @@ -277,6 +278,7 @@ const packageDataList: APIPackageData[] = [ { pageDirectory: 'util', pageTitle: 'API Reference - Utils', + pageUrl: 'api-utils', items: [ { main: 'setTimeout' }, { main: 'setInterval' }, @@ -294,6 +296,7 @@ const packageDataList: APIPackageData[] = [ { pageDirectory: '_index', pageTitle: 'API Reference', + pageUrl: 'testing/api-reference', items: [ { main: 'TestSource' }, { main: 'SharedTestSource' }, @@ -396,8 +399,8 @@ const outFolder = Folder(); // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -const pageNodeMap: PageNodeMap = Object.fromEntries(pageMap.entries()); -const pageNodeMapMetadata: PageNodeMapMetadata = { +const pages: Pages = [...pageMap.values()]; +const pagesMetadata: PagesMetadata = { github: process.env.VERCEL_GITHUB_DEPLOYMENT === '1' ? { @@ -410,35 +413,29 @@ const pageNodeMapMetadata: PageNodeMapMetadata = { } : null, }; -const pageNodeMapWithMetadata: PageNodeMapWithMetadata = { - metadata: pageNodeMapMetadata, - pageNodeMap, +const pagesWithMetadata: PagesWithMetadata = { + metadata: pagesMetadata, + pages, }; -const pageNodeMapWithMetadataStringified = JSON.stringify( - pageNodeMapWithMetadata, -); +const pagesWithMetadataStringified = JSON.stringify(pagesWithMetadata); const hash = crypto .createHash('md5') - .update(pageNodeMapWithMetadataStringified) + .update(pagesWithMetadataStringified) .digest('hex') .slice(0, 8); -const pathList: ApiDocMapPathList = [...pageMap.keys()]; +const pagesUrlList: PagesUrlList = pages.map((page) => page.pageUrl); addFileToFolder( outFolder, - `www/public/apiDocMap.${hash}.json`, - pageNodeMapWithMetadataStringified, -); -addFileToFolder( - outFolder, - `www/temp/apiDocMap.json`, - pageNodeMapWithMetadataStringified, + `www/_files/public/pages.${hash}.json`, + pagesWithMetadataStringified, ); -addFileToFolder(outFolder, 'www/temp/apiDocMapHash', hash); +addFileToFolder(outFolder, `www/temp/pages.json`, pagesWithMetadataStringified); +addFileToFolder(outFolder, 'www/temp/pagesHash', hash); addFileToFolder( outFolder, - 'www/temp/apiDocMapPathList.json', - JSON.stringify(pathList), + 'www/temp/pagesUrlList.json', + JSON.stringify(pagesUrlList), ); const outApiFolder = getNestedFolderAtPath(outFolder, context.outDir); @@ -448,5 +445,6 @@ for (const [path, fileOrFolder] of renderedApiFolder) { writeFolderToDirectoryPath(outFolder, rootDir).catch((error) => { console.error('error writing pages to out directory...'); - throw error; + console.log(error); + exit(); }); diff --git a/www/script/docs/apigen/rootDir.ts b/www/script/docs/apigen/rootDir.ts deleted file mode 100644 index 93e05b31..00000000 --- a/www/script/docs/apigen/rootDir.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as path from 'path'; - -export const rootDir = path.join(__dirname, '..', '..', '..', '..'); diff --git a/www/script/docs/apigen/types.ts b/www/script/docs/apigen/types.ts index bf3d7c44..7b54d049 100644 --- a/www/script/docs/apigen/types.ts +++ b/www/script/docs/apigen/types.ts @@ -1,8 +1,9 @@ import { DeepCoreNode } from './core/nodes'; +import { PageNode } from './core/nodes/Page'; -export type PageNodeMap = Record; +export type Pages = PageNode[]; -export interface PageNodeMapMetadata { +export interface PagesMetadata { github: null | { org: string; repo: string; @@ -11,12 +12,12 @@ export interface PageNodeMapMetadata { }; } -export interface PageNodeMapWithMetadata { - metadata: PageNodeMapMetadata; - pageNodeMap: PageNodeMap; +export interface PagesWithMetadata { + metadata: PagesMetadata; + pages: Pages; } -export type ApiDocMapPathList = string[]; +export type PagesUrlList = string[]; export interface TableOfContentsInlineReference { text: string; diff --git a/www/script/docs/apigen/util/glob.ts b/www/script/docs/apigen/util/glob.ts index a4c9e02b..b5fd69e0 100644 --- a/www/script/docs/apigen/util/glob.ts +++ b/www/script/docs/apigen/util/glob.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as glob from 'glob'; -import { rootDir } from './../rootDir'; +import { rootDir } from '../../../rootDir'; export function globAbsolute(pattern: string): string[] { return glob.sync(path.join(rootDir, pattern)); diff --git a/www/script/docs/apigen/util/ts.ts b/www/script/docs/apigen/util/ts.ts index 557481d2..f248cfe5 100644 --- a/www/script/docs/apigen/util/ts.ts +++ b/www/script/docs/apigen/util/ts.ts @@ -2,10 +2,7 @@ import * as os from 'os'; import * as colors from 'colors'; import * as ts from 'typescript'; import * as configJson from '../../../../../tsconfig.json'; - -export function exit(): never { - throw new Error('exiting...'); -} +import { exit } from '../../../exit'; export function logDiagnostic(diagnostic: ts.Diagnostic): void { const message = ts.flattenDiagnosticMessageText( diff --git a/www/script/exit.ts b/www/script/exit.ts new file mode 100644 index 00000000..119407d4 --- /dev/null +++ b/www/script/exit.ts @@ -0,0 +1,4 @@ +export function exit(): never { + console.error('exiting...'); + process.exit(1); +} diff --git a/www/script/rootDir.ts b/www/script/rootDir.ts new file mode 100644 index 00000000..f97a4ae9 --- /dev/null +++ b/www/script/rootDir.ts @@ -0,0 +1,3 @@ +import * as path from 'path'; + +export const rootDir = path.join(__dirname, '..', '..'); diff --git a/www/script/template/index.html b/www/script/template/index.html index d5cc46d6..4e3f213e 100644 --- a/www/script/template/index.html +++ b/www/script/template/index.html @@ -3,7 +3,7 @@ - + @@ -11,9 +11,9 @@
::ssr::
- + diff --git a/www/script/template/index.tsx b/www/script/template/index.tsx index 7e682e41..809f6940 100644 --- a/www/script/template/index.tsx +++ b/www/script/template/index.tsx @@ -2,22 +2,23 @@ import * as fs from 'fs'; import * as path from 'path'; import { h } from 'preact'; import { render } from 'preact-render-to-string'; -import { convertApiDocMapPathToUrlPathName } from '../../src/apiDocMapPathList'; -import { ApiDocMapResponseContextProvider } from '../../src/ApiDocMapResponseContext'; -import { ApiDocPage, IndexPage, NotFoundPage } from '../../src/App'; -import { ResponseDoneType, ResponseState } from '../../src/loadApiDocMap'; -import { PageNodeMapWithMetadata } from '../docs/apigen/types'; +import { IndexPage, NotFoundPage, DocPage } from '../../src/App'; +import { DocPagesResponseContextProvider } from '../../src/DocPagesResponseContext'; +import { convertDocPageUrlToUrlPathName } from '../../src/docPageUrls'; +import { ResponseDoneType, ResponseState } from '../../src/loadDocPages'; +import { PagesWithMetadata } from '../docs/apigen/types'; import { addFileToFolder, Folder, writeFolderToDirectoryPath, } from '../docs/apigen/util/Folder'; import { getRelativePath } from '../docs/apigen/util/getRelativePath'; +import { exit } from '../exit'; const template = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8'); // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires -const apiDocMap: PageNodeMapWithMetadata = require('../../temp/apiDocMap.json'); +const pagesWithMetadata: PagesWithMetadata = require('../../temp/pages.json'); function shouldNotBeCalled(): never { throw new Error('This should not be called.'); @@ -36,69 +37,66 @@ function addRenderedHtmlToFolder(html: string, filePath: string): void { const contents = template .replace('::ssr::', html) .replace( - /::index\.tsx::/g, - ensureRelative(getRelativePath(filePath, 'index.tsx')), + /::script\.tsx::/g, + ensureRelative(getRelativePath(filePath, 'script.tsx')), ) .replace( /::index\.css::/g, ensureRelative(getRelativePath(filePath, 'index.css')), ) .replace( - /::loadApiDocMap\.ts::/g, - ensureRelative(getRelativePath(filePath, 'loadApiDocMap.ts')), + /::loadDocPages\.ts::/g, + ensureRelative(getRelativePath(filePath, 'loadDocPages.ts')), ); addFileToFolder(outFolder, filePath, contents); } addRenderedHtmlToFolder( render( - - , + , ), 'index.html', ); addRenderedHtmlToFolder( render( - - , + , ), '404.html', ); const responseState: ResponseState = { type: ResponseDoneType, - data: apiDocMap, + data: pagesWithMetadata, }; -for (const apiDocMapPath of Object.keys(apiDocMap.pageNodeMap)) { +for (const { pageUrl } of pagesWithMetadata.pages) { addRenderedHtmlToFolder( render( - responseState, onResponseStateChange: shouldNotBeCalled, }} > - - , + + , ), - `${convertApiDocMapPathToUrlPathName(apiDocMapPath).replace( - /^\//, - '', - )}.html`, + `${convertDocPageUrlToUrlPathName(pageUrl).replace(/^\//, '')}.html`, ); } @@ -106,5 +104,6 @@ export const rootDir = path.join(__dirname, '..', '..', 'template'); writeFolderToDirectoryPath(outFolder, rootDir).catch((error) => { console.error('error writing pages to out directory...'); - throw error; + console.log(error); + exit(); }); diff --git a/www/src/App.tsx b/www/src/App.tsx index 32743290..28e0eee6 100644 --- a/www/src/App.tsx +++ b/www/src/App.tsx @@ -1,33 +1,32 @@ import { h, Fragment, VNode } from 'preact'; import { Router, Route, RouterOnChangeArgs } from 'preact-router'; import { useMemo } from 'preact/hooks'; -import { - apiDocMapPathList, - convertApiDocMapPathToUrlPathName, -} from './apiDocMapPathList'; -import { useApiDocMapResponseState } from './ApiDocMapResponseContext'; +import { useDocPagesResponseState } from './DocPagesResponseContext'; +import { docPageUrls, convertDocPageUrlToUrlPathName } from './docPageUrls'; import { ResponseDoneType, ResponseHttpStatusErrorType, ResponseJSONParsingErrorType, ResponseLoadingType, -} from './loadApiDocMap'; +} from './loadDocPages'; export function IndexPage(): VNode { return
index
; } -interface ApiDocPageProps { - pagePath: string; +interface DocPageProps { + pageUrl: string; } -export function ApiDocPage({ pagePath }: ApiDocPageProps): VNode { - const responseState = useApiDocMapResponseState(); +export function DocPage({ pageUrl }: DocPageProps): VNode { + const responseState = useDocPagesResponseState(); const stringifiedNodeMap = useMemo( () => responseState.type === ResponseDoneType ? JSON.stringify( - responseState.data.pageNodeMap[pagePath], + responseState.data.pages.filter( + (page) => page.pageUrl === pageUrl, + )[0], null, 4, ) @@ -80,70 +79,39 @@ function endsWithTrailingSlash(url: string): boolean { return url[url.length - 1] === '/' && url !== '/'; } -function cleanUrlTrailingSlash(url: string): string | undefined { +function cleanUrlTrailingSlash({ url }: RouterOnChangeArgs): void { if (endsWithTrailingSlash(url)) { do { url = url.slice(0, -1); } while (endsWithTrailingSlash(url)); - return url; - } - return; -} - -function cleanUrlTrailingString(url: string, str: string): string | undefined { - if (url.slice(-str.length) === str) { - return url.slice(0, -str.length); - } - return; -} - -function cleanUrlTrailingIndexHtml(url: string): string | undefined { - return cleanUrlTrailingString(url, '/index.html'); -} - -function cleanUrlTrailingHtmlExtension(url: string): string | undefined { - return cleanUrlTrailingString(url, '.html'); -} - -function cleanUrl({ url }: RouterOnChangeArgs) { - const withoutTrailingSlashOrUndefined = cleanUrlTrailingSlash(url); - const withoutTrailingSlash = withoutTrailingSlashOrUndefined || url; - const newUrl = - cleanUrlTrailingIndexHtml(withoutTrailingSlash) || - cleanUrlTrailingHtmlExtension(withoutTrailingSlash) || - withoutTrailingSlashOrUndefined; - - if (newUrl) { - history.pushState(null, (null as unknown) as string, newUrl); + history.pushState(null, (null as unknown) as string, url); } } export function App(): VNode { return ( - + {Array.prototype.concat.apply( [], - apiDocMapPathList.map((apiDocMapPath) => { - const path = convertApiDocMapPathToUrlPathName( - apiDocMapPath, - ); + docPageUrls.map((docPageUrl) => { + const path = convertDocPageUrlToUrlPathName(docPageUrl); return [ , , , ]; }), diff --git a/www/src/ApiDocMapResponseContext.ts b/www/src/DocPagesResponseContext.ts similarity index 63% rename from www/src/ApiDocMapResponseContext.ts rename to www/src/DocPagesResponseContext.ts index eb73e5cf..e4e67101 100644 --- a/www/src/ApiDocMapResponseContext.ts +++ b/www/src/DocPagesResponseContext.ts @@ -1,24 +1,23 @@ import { createContext } from 'preact'; import { useState, useContext, useEffect } from 'preact/hooks'; -import { ResponseState, NonLoadingResponseState } from './loadApiDocMap'; +import { ResponseState, NonLoadingResponseState } from './loadDocPages'; -export interface ApiDocMapResponseContextValue { +export interface DocPagesResponseContextValue { getCurrentResponseState: () => ResponseState; onResponseStateChange: ( setResponseState: (responseState: NonLoadingResponseState) => void, ) => () => void; } -const apiDocMapResponseContext = createContext( - (null as unknown) as ApiDocMapResponseContextValue, +const docPagesResponseContext = createContext( + (null as unknown) as DocPagesResponseContextValue, ); -export const ApiDocMapResponseContextProvider = - apiDocMapResponseContext.Provider; +export const DocPagesResponseContextProvider = docPagesResponseContext.Provider; -export function useApiDocMapResponseState(): ResponseState { +export function useDocPagesResponseState(): ResponseState { const { getCurrentResponseState, onResponseStateChange } = useContext( - apiDocMapResponseContext, + docPagesResponseContext, ); const { 0: responseState, 1: setResponseState } = useState( getCurrentResponseState(), diff --git a/www/src/apiDocMapPathList.ts b/www/src/apiDocMapPathList.ts deleted file mode 100644 index 6b4d49d9..00000000 --- a/www/src/apiDocMapPathList.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiDocMapPathList } from './../script/docs/apigen/types'; - -// eslint-disable-next-line max-len -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires -export const apiDocMapPathList: ApiDocMapPathList = require('../temp/apiDocMapPathList.json'); - -export function convertApiDocMapPathToUrlPathName( - apiDocMapPath: string, -): string { - const split = apiDocMapPath.split('/'); - const last = split[split.length - 1]; - const ending = - last === '_index' - ? split.slice(0, split.length - 1).join('/') - : apiDocMapPath; - return `/docs/api/${ending}`; -} diff --git a/www/src/docPageUrls.ts b/www/src/docPageUrls.ts new file mode 100644 index 00000000..52caeb5c --- /dev/null +++ b/www/src/docPageUrls.ts @@ -0,0 +1,9 @@ +import { PagesUrlList } from '../script/docs/apigen/types'; + +// eslint-disable-next-line max-len +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires +export const docPageUrls: PagesUrlList = require('../temp/pagesUrlList.json'); + +export function convertDocPageUrlToUrlPathName(docPageUrl: string): string { + return `/docs/${docPageUrl}`; +} diff --git a/www/src/loadApiDocMap.ts b/www/src/loadDocPages.ts similarity index 91% rename from www/src/loadApiDocMap.ts rename to www/src/loadDocPages.ts index ebd1e7cd..a2e572e6 100644 --- a/www/src/loadApiDocMap.ts +++ b/www/src/loadDocPages.ts @@ -1,5 +1,5 @@ import * as fs from 'fs'; -import { PageNodeMapWithMetadata } from '../script/docs/apigen/types'; +import { PagesWithMetadata } from '../script/docs/apigen/types'; export const ResponseLoadingType = 0; export const ResponseHttpStatusErrorType = 1; @@ -13,7 +13,7 @@ export type NonLoadingResponseState = statusText: string; } | { type: typeof ResponseJSONParsingErrorType; error: unknown } - | { type: typeof ResponseDoneType; data: PageNodeMapWithMetadata }; + | { type: typeof ResponseDoneType; data: PagesWithMetadata }; export type ResponseState = | { type: typeof ResponseLoadingType } | NonLoadingResponseState; @@ -84,7 +84,7 @@ export function makeRequest(): void { return; } - let data: PageNodeMapWithMetadata; + let data: PagesWithMetadata; try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -105,7 +105,7 @@ export function makeRequest(): void { }); }; - const hash = fs.readFileSync('temp/apiDocMapHash', 'utf-8'); - request.open('GET', `/apiDocMap.${hash}.json`); + const hash = fs.readFileSync('temp/pagesHash', 'utf-8'); + request.open('GET', `/pages.${hash}.json`); request.send(); } diff --git a/www/src/index.tsx b/www/src/script.tsx similarity index 69% rename from www/src/index.tsx rename to www/src/script.tsx index 3800fd4c..fd1fb3ff 100644 --- a/www/src/index.tsx +++ b/www/src/script.tsx @@ -1,13 +1,10 @@ import { h, render } from 'preact'; -import { - apiDocMapPathList, - convertApiDocMapPathToUrlPathName, -} from './apiDocMapPathList'; -import { - ApiDocMapResponseContextProvider, - ApiDocMapResponseContextValue, -} from './ApiDocMapResponseContext'; import { App } from './App'; +import { + DocPagesResponseContextProvider, + DocPagesResponseContextValue, +} from './DocPagesResponseContext'; +import { docPageUrls, convertDocPageUrlToUrlPathName } from './docPageUrls'; import { NonLoadingResponseState, getGlobalState, @@ -16,36 +13,36 @@ import { ResponseHttpStatusErrorType, ResponseJSONParsingErrorType, ResponseLoadingType, -} from './loadApiDocMap'; +} from './loadDocPages'; -const apiDocMapResponseContextValue: ApiDocMapResponseContextValue = { +const docPagesResponseContextValue: DocPagesResponseContextValue = { getCurrentResponseState: getGlobalState, onResponseStateChange: onGlobalStateChange, }; function renderApp(): void { render( - + - , + , // eslint-disable-next-line @typescript-eslint/no-non-null-assertion document.getElementById('root')!, ); } -function isApiDocMapPath(path: string): boolean { - return apiDocMapPathList.some((apiDocMapPath) => { - const otherPath = convertApiDocMapPathToUrlPathName(apiDocMapPath); +function isDocPageUrl(pageUrl: string): boolean { + return docPageUrls.some((docPageUrl) => { + const otherPath = convertDocPageUrlToUrlPathName(docPageUrl); return ( - path === otherPath || - path === otherPath + '/' || - path === otherPath + 'index.html' || - path === otherPath + 'index.html/' + pageUrl === otherPath || + pageUrl === otherPath + '/' || + pageUrl === otherPath + 'index.html' || + pageUrl === otherPath + 'index.html/' ); }); } -if (isApiDocMapPath(window.location.pathname)) { +if (isDocPageUrl(window.location.pathname)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const responseState = getGlobalState()!; diff --git a/www/vercel.json b/www/vercel.json index eab01831..6b23ff23 100644 --- a/www/vercel.json +++ b/www/vercel.json @@ -1,22 +1,10 @@ { "version": 2, "scope": "ahrota", - "trailingSlash": false, - "cleanUrls": true, - "redirects": [ - { "source": "/(.*)/index.html(/*)", "destination": "/$1" }, - { "source": "/(.*).html(/*)", "destination": "/$1" }, - { "source": "/(.*)(/+)", "destination": "/$1" } - ], - "headers": [ - { - "source": "/(.*).(js|json)", - "headers": [ - { - "key": "Cache-Control", - "value": "public, max-age=31536000" - } - ] + "routes": [{ "src": "/(.*)", "dest": "api/index" }], + "functions": { + "api/index.ts": { + "includeFiles": "_files/**" } - ] + } }