This commit is contained in:
Morten Olsen
2023-03-26 22:15:07 +02:00
commit 9b1a067d56
80 changed files with 7889 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import { createFile } from "../file";
import ejs from "ejs";
const createEjs = (path: string) => {
const file = createFile({ path });
const template = file.pipe(async (tmpl) => {
const compiled = ejs.compile(tmpl.toString());
return compiled;
});
return template;
};
export { createEjs };

View File

@@ -0,0 +1,19 @@
import { readFile } from "fs/promises";
import { Observable } from "../../observable";
import { watch } from "fs";
type FileOptions = {
path: string;
};
const createFile = ({ path }: FileOptions) => {
const file = new Observable(async () => readFile(path, "utf-8"));
watch(path, () => {
file.recreate();
});
return file;
};
export { createFile };

View File

@@ -0,0 +1,33 @@
import fastGlob from "fast-glob";
import watchGlob from "glob-watcher";
import { Observable } from "../../observable";
type GlobOptions<T> = {
cwd?: string;
pattern: string;
create?: (path: string) => T;
};
const defaultCreate = (a: any) => a;
const createGlob = <T = string>({
cwd,
pattern,
create = defaultCreate,
}: GlobOptions<T>) => {
const glob = new Observable(async () => {
const files = await fastGlob(pattern, { cwd });
return files.map(create);
});
const watcher = watchGlob(pattern, { cwd });
watcher.on("add", (path) => {
glob.set((current) => Promise.resolve([...(current || []), create(path)]));
return glob;
});
return glob;
};
export { createGlob };

View File

@@ -0,0 +1,40 @@
import { createHash } from "crypto";
import { Asset, Bundler } from "../../bundler";
import { Observable } from "../../observable";
import sharp, { FormatEnum } from "sharp";
type ImageOptions = {
format: keyof FormatEnum;
name?: string;
image: string;
width?: number;
height?: number;
bundler: Bundler;
};
const createImage = (options: ImageOptions) => {
let path =
options.name || createHash("sha256").update(options.image).digest("hex");
if (options.width) {
path += `-w${options.width}`;
}
if (options.height) {
path += `-h${options.height}`;
}
path += `.${options.format}`;
const loader = async () => {
const item = sharp(options.image);
if (options.width || options.height) {
item.resize(options.width, options.height);
}
item.toFormat(options.format);
const content = await item.toBuffer();
return {
content,
};
};
const observable = new Observable<Asset>(loader);
return options.bundler.register(path, observable);
};
export { createImage };

View File

@@ -0,0 +1,29 @@
import { Asset, Bundler } from "../../bundler";
import { Observable } from "../../observable";
import { createEjs } from "../ejs";
import { latexToPdf } from "./utils";
type LatexOptions = {
path: string;
bundler: Bundler;
template: ReturnType<typeof createEjs>;
data: Observable<any>;
};
const createLatex = ({ template, data, path, bundler }: LatexOptions) => {
const pdf = Observable.combine({
template,
data,
})
.pipe(async ({ template, data }) => template(data))
.pipe(async (latex) => {
const pdf = await latexToPdf(latex);
const asset: Asset = {
content: pdf,
};
return asset;
});
return bundler.register(`${path}.pdf`, pdf);
};
export { createLatex };

View File

@@ -0,0 +1,23 @@
import latex from "node-latex";
import { Readable } from "stream";
const latexToPdf = (doc: string) =>
new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
const input = new Readable();
input.push(doc);
input.push(null);
const latexStream = latex(input);
latexStream.on("data", (chunk) => {
chunks.push(Buffer.from(chunk));
});
latexStream.on("finish", () => {
const result = Buffer.concat(chunks);
resolve(result);
});
latexStream.on("error", (err) => {
reject(err);
});
});
export { latexToPdf };

View File

View File

@@ -0,0 +1,62 @@
import React, { ComponentType } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { HelmetProvider, FilledContext } from "react-helmet-async";
import { Asset, Bundler } from "../../bundler";
import { Observable } from "../../observable";
import { ServerStyleSheet } from "styled-components";
import { resolve } from "path";
type PageOptions = {
path: string;
template: Observable<ComponentType<any>>;
props: Observable<any>;
bundler: Bundler;
};
const createPage = (options: PageOptions) => {
const data = Observable.combine({
template: options.template,
props: options.props,
});
const page = data.pipe(async ({ template, props }) => {
const sheet = new ServerStyleSheet();
const helmetContext: FilledContext = {} as any;
const body = sheet.collectStyles(
React.createElement(
HelmetProvider,
{ context: helmetContext },
React.createElement(template, props)
)
);
const bodyHtml = renderToStaticMarkup(body);
const { helmet } = helmetContext;
const css = sheet.getStyleTags();
const headHtml = [
css,
helmet.title?.toString(),
helmet.priority?.toString(),
helmet.meta?.toString(),
helmet.link?.toString(),
helmet.script?.toString(),
]
.filter(Boolean)
.join("");
const html = `<!DOCTYPE html>
<html lang="en">
<head>
${headHtml}
</head>
<body>
${bodyHtml}
</body>
</html>
`;
const asset: Asset = { content: html };
return asset;
});
const path = resolve("/", options.path, "index.html");
return options.bundler.register(path, page);
};
export { createPage };

View File

@@ -0,0 +1,82 @@
import vm from "vm";
import React, { ComponentType } from "react";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import replace from "@rollup/plugin-replace";
import sucrase from "@rollup/plugin-sucrase";
import alias from "@rollup/plugin-alias";
import externalGlobals from "rollup-plugin-external-globals";
import { createScript } from "../script";
import orgStyled from "styled-components";
import * as styledExports from "styled-components";
import ReactHelmetAsync from "react-helmet-async";
import { resolve } from "path";
const styled = orgStyled.bind(null);
for (let key of Object.keys(orgStyled)) {
if (key === "default") {
continue;
}
(styled as any)[key] = (orgStyled as any)[key];
}
for (let key of Object.keys(styledExports)) {
if (key === "default") {
continue;
}
(styled as any)[key] = (styledExports as any)[key];
}
const createReact = <TProps = any>(path: string) => {
const script = createScript({
path,
format: "cjs",
plugins: [
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify("production"),
}),
alias({
entries: [
{ find: "@", replacement: resolve("content/templates/react") },
],
}),
sucrase({
exclude: ["node_modules/**"],
transforms: ["jsx", "typescript"],
}),
nodeResolve({
browser: true,
preferBuiltins: false,
extensions: [".js", ".ts", ".tsx"],
}),
json(),
commonjs({
include: /node_modules/,
}),
externalGlobals({
react: "React",
"styled-components": "StyledComponents",
"react-helmet-async": "ReactHelmetAsync",
}),
],
});
const template = script.pipe<ComponentType<TProps>>(async () => {
const scriptContent = await script.data;
const exports: any = {};
const module = { exports };
const globals = {
module,
exports,
React,
StyledComponents: styled,
ReactHelmetAsync,
};
vm.runInNewContext(scriptContent, globals);
return module.exports;
});
return template;
};
export { createReact };

View File

@@ -0,0 +1,52 @@
import { Observable } from "../../observable";
import { InputPluginOption, ModuleFormat, watch } from "rollup";
type ScriptOptions = {
path: string;
format: ModuleFormat;
plugins?: InputPluginOption;
};
const build = (options: ScriptOptions, update: (code: string) => void) =>
new Promise<string>((resolve, reject) => {
let compiled = false;
const watcher = watch({
input: options.path,
plugins: options.plugins,
onwarn: () => { },
output: {
format: options.format,
},
watch: {
skipWrite: true,
},
});
watcher.on("event", async (event) => {
if (event.code === "BUNDLE_END") {
const { output } = await event.result.generate({
format: options.format,
});
const { code } = output[0];
if (!compiled) {
resolve(code);
compiled = true;
} else {
update(code);
}
}
if (event.code === "ERROR") {
reject(event.error);
}
});
});
const createScript = (options: ScriptOptions) => {
const script: Observable<string> = new Observable(() =>
build(options, (code) => script.set(() => Promise.resolve(code)))
);
return script;
};
export { createScript };