Skip to content

Commit

Permalink
feat(www): refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonyrota committed Sep 19, 2020
1 parent 0c9d7a7 commit d601ace
Show file tree
Hide file tree
Showing 28 changed files with 414 additions and 196 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"apigen",
"brotli",
"vercel",
"preact"
"preact",
"opendir"
],
"overrides": []
}
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ dist
mangle.json
extractor
docs/api
temp/*.api.json
temp
.vercel/project.json
www/public
www/public-br
www/template
3 changes: 3 additions & 0 deletions .vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
www/_files
www/public
www/template
2 changes: 1 addition & 1 deletion www/.babelrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"presets": [
["@babel/env", { "modules": false }],
["@babel/env", { "modules": false, "loose": true }],
["@babel/preset-typescript", { "jsxPragma": "h" }]
],
"plugins": [
Expand Down
1 change: 1 addition & 0 deletions www/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/_files
/public
/template
138 changes: 138 additions & 0 deletions www/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const publicFilesList: string[] = (require('../_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');
}
};
10 changes: 6 additions & 4 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
"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",
"postinstall": "cpy .browserslistrc node_modules/unfetch"
},
"dependencies": {
Expand Down
121 changes: 121 additions & 0 deletions www/script/compressPublic/index.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> {
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<unknown>[] = [];

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();
});
3 changes: 3 additions & 0 deletions www/script/compressPublic/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface CompressedFileMetadata {
contentLength: number;
}
1 change: 1 addition & 0 deletions www/script/docs/apigen/analyze/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface APIPageDataItem {
export interface APIPageData {
pageDirectory: string;
pageTitle: string;
pageUrl: string;
items: APIPageDataItem[];
}

Expand Down
1 change: 1 addition & 0 deletions www/script/docs/apigen/analyze/build/buildApiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function buildApiPage(
title: pageData.pageTitle,
tableOfContents: tableOfContents,
},
pageUrl: pageData.pageUrl,
children: Array.from(nameToApiItems, ([, apiItems]) => {
return buildApiItemImplementationGroup({
apiItems,
Expand Down
3 changes: 3 additions & 0 deletions www/script/docs/apigen/core/nodes/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import { Node, CoreNodeType } from '.';
export interface PageParameters<ChildNode extends Node>
extends ContainerParameters<ChildNode> {
metadata: PageMetadata;
pageUrl: string;
}

export interface PageBase<ChildNode extends Node>
extends ContainerBase<ChildNode> {
metadata: PageMetadata;
pageUrl: string;
}

export function PageBase<ChildNode extends Node>(
parameters: PageParameters<ChildNode>,
): PageBase<ChildNode> {
return {
metadata: parameters.metadata,
pageUrl: parameters.pageUrl,
...ContainerBase<ChildNode>({ children: parameters.children }),
};
}
Expand Down
Loading

1 comment on commit d601ace

@vercel
Copy link

@vercel vercel bot commented on d601ace Sep 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.