Scenario generator for vore roleplay and story ideas. https://scenario-generator.deliciousreya.net/responses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

208 lines
7.4 KiB

import {
createPrinter,
parseJsonText,
factory,
NewLineKind,
NodeFlags,
type PropertyAssignment,
SyntaxKind,
type VariableDeclaration,
type VariableStatement
} from 'typescript';
import typescriptModule from 'typescript';
import { readFile, writeFile, readdir } from 'node:fs/promises';
import { basename, dirname, join, normalize } from 'node:path';
import {createHash} from 'node:crypto';
import {camelCase} from 'change-case';
import { render as renderLess } from 'less';
import CleanCSS from 'clean-css';
import type {
HashedBundled,
SourceMappedHashedBundled,
SourceMappedBundled,
Bundled,
MaybeSourceMappedHashedBundled, SourceMap
} from '../common/bundle';
import { rollup, type RollupCache } from 'rollup';
import typescript from 'rollup-plugin-ts';
import terser from '@rollup/plugin-terser';
import nodeResolve from '@rollup/plugin-node-resolve';
import commonJs from '@rollup/plugin-commonjs';
function* assignProperties(pairs: Iterable<[string, MaybeSourceMappedHashedBundled]>, includeSourceMap: boolean): Generator<PropertyAssignment> {
for (const [identifier, { bundled, hash, sourceMap }] of pairs) {
yield factory.createPropertyAssignment(
factory.createIdentifier(identifier),
factory.createObjectLiteralExpression([
factory.createPropertyAssignment(
factory.createIdentifier("bundled"),
factory.createNoSubstitutionTemplateLiteral(bundled)
),
factory.createPropertyAssignment(
factory.createIdentifier("hash"),
factory.createStringLiteral(hash)
),
...(includeSourceMap && sourceMap ? [factory.createPropertyAssignment(
factory.createIdentifier("sourceMap"),
parseJsonText(hash + ".map", JSON.stringify(sourceMap)).statements[0].expression,
)] : [])
], true));
}
}
function declareObjectLiteral(identifier: string, pairs: Iterable<[string, MaybeSourceMappedHashedBundled]>, includeSourceMap: boolean): VariableDeclaration {
return factory.createVariableDeclaration(
factory.createIdentifier(identifier),
undefined,
undefined,
factory.createSatisfiesExpression(
factory.createAsExpression(
factory.createObjectLiteralExpression(Array.from(assignProperties(pairs, includeSourceMap)), true),
factory.createTypeReferenceNode(factory.createIdentifier('const'))),
factory.createTypeReferenceNode(
factory.createIdentifier("Record"),
[
factory.createTypeReferenceNode(factory.createIdentifier("string")),
factory.createTypeReferenceNode(factory.createIdentifier("MaybeSourceMappedHashedBundled"))])));
}
function exportObjectLiteral(identifier: string, pairs: Iterable<[string, MaybeSourceMappedHashedBundled]>, includeSourceMap: boolean): VariableStatement {
return factory.createVariableStatement(
[factory.createToken(SyntaxKind.ExportKeyword)],
factory.createVariableDeclarationList([declareObjectLiteral(identifier, pairs, includeSourceMap)], NodeFlags.Const)
);
}
async function processLess(atPath: string): Promise<SourceMappedBundled> {
const fileBase = basename(atPath.substring(0, atPath.length - LESS_SUFFIX.length));
const { css: lessCss, map: lessMap } = await renderLess(await readFile(atPath, { encoding: 'utf-8' }), {
paths: [dirname(atPath)],
math: 'strict',
strictUnits: true,
filename: fileBase + '.less',
strictImports: true,
sourceMap: {
outputSourceFiles: true,
}
});
const { styles, sourceMap } = await new CleanCSS({
sourceMap: true,
sourceMapInlineSources: true,
returnPromise: true,
level: 2,
format: false,
inline: ['all'],
rebase: false,
compatibility: '*',
fetch(uri): never {
throw Error(`external files are unexpected after less compilation, but found ${uri}`)
},
}).minify({
[fileBase + '.css']: {
styles: lessCss,
sourceMap: lessMap
}
})
return { bundled: styles, sourceMap: {...JSON.parse(sourceMap!.toString()), file: fileBase + ".css"} as SourceMap };
}
async function processTypescript(atPath: string, inDir: string, cache?: RollupCache): Promise<{cache: RollupCache, bundle: SourceMappedBundled}> {
const build = await rollup({
cache: cache ?? true,
input: atPath,
plugins: [
nodeResolve({
}),
commonJs({
}),
typescript({
transpiler: "babel",
typescript: typescriptModule,
tsconfig: join(inDir, 'tsconfig.json')
}),
terser({})
]
})
const {output: [chunk]} = await build.generate({
name: camelCase(basename(atPath.substring(0, atPath.length - TS_SUFFIX.length))),
sourcemap: 'hidden',
sourcemapFile: join(inDir, 'sourcemap.map'),
format: 'iife',
compact: true,
})
return {
cache: build.cache!,
bundle: {
bundled: chunk.code,
sourceMap: chunk.map!
}
}
}
const LESS_SUFFIX = '-entrypoint.less';
const TS_SUFFIX = '-entrypoint.ts';
function hashBundled<T extends Bundled>(value: T & {readonly hash?: never}): T & HashedBundled {
const hash = createHash('sha256').update(value.bundled).digest('hex')
return {
...value,
hash,
}
}
export async function getBundle(inDir: string): Promise<{ css: Map<string, SourceMappedHashedBundled>, js: Map<string, SourceMappedHashedBundled> }> {
const css = new Map<string, SourceMappedHashedBundled>();
const js = new Map<string, SourceMappedHashedBundled>();
const dir = await readdir(inDir, { withFileTypes: true });
let cache: RollupCache|undefined = undefined
for (const ent of dir) {
if (!ent.isFile()) {
continue;
}
if (ent.name.endsWith(LESS_SUFFIX)) {
css.set(camelCase(ent.name.substring(0, ent.name.length - LESS_SUFFIX.length)), hashBundled(await processLess(join(inDir, ent.name))));
} else if (ent.name.endsWith(TS_SUFFIX)) {
const {cache: newCache, bundle} = await processTypescript(join(inDir, ent.name), inDir, cache)
cache = newCache
js.set(camelCase(ent.name.substring(0, ent.name.length - TS_SUFFIX.length)), hashBundled(bundle));
} else {
// continue;
}
}
return { css, js };
}
export const DEFAULT_IN_PATH = normalize(join(__dirname, '../../src/client/'))
export const DEFAULT_OUT_PATH = normalize(join(__dirname, '../../src/server/web/bundles/client.generated.ts'))
export async function writeBundle({ css, js }: {css: Map<string, SourceMappedHashedBundled>, js: Map<string, SourceMappedHashedBundled>}, outFile: string, includeSourceMap: true): Promise<void>
export async function writeBundle({ css, js }: {css: Map<string, HashedBundled>, js: Map<string, HashedBundled>}, outFile: string, includeSourceMap: false): Promise<void>
export async function writeBundle({ css, js }: {css: Map<string, MaybeSourceMappedHashedBundled>, js: Map<string, MaybeSourceMappedHashedBundled>}, outFile: string, includeSourceMap: boolean): Promise<void>
export async function writeBundle({ css, js }: {css: Map<string, MaybeSourceMappedHashedBundled>, js: Map<string, MaybeSourceMappedHashedBundled>}, outFile: string, includeSourceMap: boolean): Promise<void> {
const printer = createPrinter({
newLine: NewLineKind.LineFeed,
omitTrailingSemicolon: true
});
await writeFile(outFile, printer.printFile(factory.createSourceFile([
factory.createImportDeclaration(
undefined,
factory.createImportClause(
false,
undefined,
factory.createNamedImports([
factory.createImportSpecifier(
true,
undefined,
factory.createIdentifier( "MaybeSourceMappedHashedBundled")),
])
),
factory.createStringLiteral("../../common/bundle.js")),
exportObjectLiteral('CSS', css, includeSourceMap),
exportObjectLiteral('JS', js, includeSourceMap)
], factory.createToken(SyntaxKind.EndOfFileToken), NodeFlags.None)), {
encoding: 'utf-8',
mode: 0o644
});
}