anki/ts/svelte/svelte.ts
2021-11-12 15:02:17 +10:00

314 lines
9.3 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// languageServerHost taken from MIT sources - see below.
import * as fs from "fs";
import * as worker from "@bazel/worker";
import { svelte2tsx } from "svelte2tsx";
import preprocess from "svelte-preprocess";
import { basename } from "path";
import * as ts from "typescript";
import * as svelte from "svelte/compiler";
const parsedCommandLine: ts.ParsedCommandLine = {
fileNames: [],
errors: [],
options: {
jsx: ts.JsxEmit.Preserve,
declaration: true,
emitDeclarationOnly: true,
// noEmitOnError: true,
paths: {
"*": ["*", "external/npm/node_modules/*"],
},
},
};
interface FileContent {
text: string;
version: number;
}
const fileContent: Map<string, FileContent> = new Map();
function getFileContent(path: string): FileContent {
let content = fileContent.get(path);
if (!content) {
content = {
text: ts.sys.readFile(path)!,
version: 0,
};
fileContent.set(path, content);
}
return content;
}
function updateFileContent(input: InputFile): void {
let content = fileContent.get(input.path);
if (content && content.text !== input.data) {
content.text = input.data;
content.version += 1;
} else {
content = {
text: input.data,
version: 0,
};
fileContent.set(input.path, content);
}
}
// based on https://github.com/Asana/bazeltsc/blob/7dfa0ba2bd5eb9ee556e146df35cf793fad2d2c3/src/bazeltsc.ts (MIT)
const languageServiceHost: ts.LanguageServiceHost = {
getCompilationSettings: (): ts.CompilerOptions => parsedCommandLine.options,
getScriptFileNames: (): string[] => parsedCommandLine.fileNames,
getScriptVersion: (path: string): string => {
return getFileContent(path).version.toString();
},
getScriptSnapshot: (path: string): ts.IScriptSnapshot | undefined => {
const text = getFileContent(path).text;
return {
getText: (start: number, end: number) => {
if (start === 0 && end === text.length) {
// optimization
return text;
} else {
return text.slice(start, end);
}
},
getLength: () => text.length,
getChangeRange: (
_oldSnapshot: ts.IScriptSnapshot,
): ts.TextChangeRange | undefined => {
return undefined;
},
};
},
getCurrentDirectory: ts.sys.getCurrentDirectory,
getDefaultLibFileName: ts.getDefaultLibFilePath,
};
const languageService = ts.createLanguageService(languageServiceHost);
async function emitTypings(svelte: SvelteTsxFile[], deps: InputFile[]): Promise<void> {
const allFiles = [...svelte, ...deps];
allFiles.forEach(updateFileContent);
parsedCommandLine.fileNames = allFiles.map((i) => i.path);
const program = languageService.getProgram()!;
const tsHost = ts.createCompilerHost(parsedCommandLine.options);
const createdFiles = {};
const cwd = ts.sys.getCurrentDirectory().replace(/\\/g, "/");
tsHost.writeFile = (fileName, contents) => {
// tsc makes some paths absolute for some reason
if (fileName.startsWith(cwd)) {
fileName = fileName.substring(cwd.length + 1);
}
createdFiles[fileName] = contents;
};
const result = program.emit(undefined /* all files */, tsHost.writeFile);
// for (const diag of result.diagnostics) {
// console.log(diag.messageText);
// }
for (const file of svelte) {
await writeFile(file.realDtsPath, createdFiles[file.virtualDtsPath]);
}
}
function writeFile(file, data): Promise<void> {
return new Promise((resolve, reject) => {
fs.writeFile(file, data, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
function readFile(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, "utf8", (err, data) => {
if (err) {
reject(err);
return;
}
resolve(data);
});
});
}
async function compileSingleSvelte(
input: SvelteInput,
binDir: string,
genDir: string,
): Promise<void> {
const preprocessOptions = preprocess({
scss: {
includePaths: [
binDir,
genDir,
// a nasty hack to ensure sass/... resolves correctly
// when invoked from an external workspace
`${binDir}/external/ankidesktop`,
`${genDir}/external/ankidesktop`,
`${binDir}/external/ankidesktop/sass`,
`${binDir}/../../../external/ankidesktop`,
],
},
});
try {
const processed = await svelte.preprocess(input.data, preprocessOptions, {
filename: input.path,
});
const result = svelte.compile(processed.toString!(), {
format: "esm",
css: false,
generate: "dom",
filename: input.mjsPath,
});
// warnings are an error
if (result.warnings.length > 0) {
console.log(`warnings during compile: ${result.warnings}`);
}
// write out the css file
const outputCss = result.css.code ?? "";
await writeFile(input.cssPath, outputCss);
// if it was non-empty, prepend a reference to it in the js file, so that
// it's included in the bundled .css when bundling
const outputSource =
(outputCss ? `import "./${basename(input.cssPath)}";` : "") +
result.js.code;
await writeFile(input.mjsPath, outputSource);
} catch (err) {
console.log(`compile failed: ${err}`);
return;
}
}
interface Args {
binDir: string;
genDir: string;
svelteFiles: SvelteInput[];
dependencies: InputFile[];
}
interface InputFile {
path: string;
data: string;
}
interface SvelteInput extends InputFile {
dtsPath: string;
cssPath: string;
mjsPath: string;
}
async function extractArgsAndData(args: string[]): Promise<Args> {
const [binDir, genDir, ...rest] = args;
const [svelteFiles, dependencies] = await extractSvelteAndDeps(rest);
return {
binDir,
genDir,
svelteFiles,
dependencies,
};
}
async function extractSvelteAndDeps(
files: string[],
): Promise<[SvelteInput[], InputFile[]]> {
const svelte: SvelteInput[] = [];
const deps: InputFile[] = [];
files.reverse();
while (files.length) {
const file = files.pop()!;
const data = (await readFile(file)) as string;
if (file.endsWith(".svelte")) {
svelte.push({
path: file,
data,
dtsPath: files.pop()!,
cssPath: files.pop()!,
mjsPath: files.pop()!,
});
} else {
deps.push({ path: remapBinToSrcDir(file), data });
}
}
return [svelte, deps];
}
/// Our generated .tsx files sit in the bin dir, but .ts files
/// may be coming from the source folder, which breaks ./foo imports.
/// Adjust the path to make it appear they're all in the same folder.
function remapBinToSrcDir(file: string): string {
return file.replace(new RegExp("bazel-out/[-_a-z]+/bin/"), "");
}
/// Generate Svelte .mjs/.css files.
async function compileSvelte(
svelte: SvelteInput[],
binDir: string,
genDir: string,
): Promise<void> {
for (const file of svelte) {
await compileSingleSvelte(file, binDir, genDir);
}
}
interface SvelteTsxFile extends InputFile {
// relative to src folder
virtualDtsPath: string;
// must go to bazel-out
realDtsPath: string;
}
function generateTsxFiles(svelteFiles: SvelteInput[]): SvelteTsxFile[] {
return svelteFiles.map((file) => {
const data = svelte2tsx(file.data, {
filename: file.path,
isTsFile: true,
mode: "dts",
}).code;
const path = file.path.replace(".svelte", ".svelte.tsx");
return {
path,
data,
virtualDtsPath: path.replace(".tsx", ".d.ts"),
realDtsPath: file.dtsPath,
};
});
}
async function compileSvelteAndGenerateTypings(argsList: string[]): Promise<boolean> {
const args = await extractArgsAndData(argsList);
// mjs/css
await compileSvelte(args.svelteFiles, args.binDir, args.genDir);
// d.ts
const tsxFiles = generateTsxFiles(args.svelteFiles);
await emitTypings(tsxFiles, args.dependencies);
return true;
}
function main() {
if (worker.runAsWorker(process.argv)) {
console.log = worker.log;
worker.log("Svelte running as a Bazel worker");
worker.runWorkerLoop(compileSvelteAndGenerateTypings);
} else {
const paramFile = process.argv[2].replace(/^@/, "");
const commandLineArgs = fs.readFileSync(paramFile, "utf-8").trim().split("\n");
console.log("Svelte running as a standalone process");
compileSvelteAndGenerateTypings(commandLineArgs);
}
}
if (require.main === module) {
main();
process.exitCode = 0;
}