anki/ts/svelte/svelte.ts
Henrik Giesel a981e56008
Improved add-on extension API (#1626)
* Add componentHook functionality

* Register package NoteEditor

* Rename OldEditorAdapter to NoteEditor

* Expose instances in component-hook as well

* Rename NoteTypeButtons to NotetypeButtons

* Move PreviewButton initialization to BrowserEditor.svelte

* Remove focusInRichText

- Same thing can be done by inspecting activeInput

* Satisfy formatter

* Fix remaining rebase issues

* Add .bazel to .prettierignore

* Rename currentField and activeInput to focused{Field,Input}

* Move identifier to lib and registration to sveltelib

* Fix Dynamic component insertion

* Simplify editingInputIsRichText

* Give extra warning in svelte/svelte.ts

- This was caused by doing a rename of a files, that only differed in
  case: NoteTypeButtons.svelte to NotetypeButtons.svelte
- It was quite tough to figure out, and this console.log might make it
  easier if it ever happens again

* Change signature of contextProperty

* Add ts/typings for add-on definition files

* Add Anki types in typings/common/index.d.ts

* Export without .svelte suffix

It conflicts with how Svelte types its packages

* Fix left over .svelte import from editor.py

* Rename NoteTypeButtons to unrelated to ensure case-only rename

* Rename back to NotetypeButtons.svelte

* Remove unused component-hook.ts, Fix typing in lifecycle-hooks

* Merge runtime-require and register-package into one file

+ Give some preliminary types to require

* Rename uiDidLoad to loaded

* Fix eslint / svelte-check

* Rename context imports to noteEditorContext

* Fix import name mismatch

- I wonder why these issues are not caught by svelte-check?

* Rename two missed usages of uiDidLoad

* Fix ButtonDropdown from having wrong border-radius

* Uniformly rename libraries to packages

- I don't have a strong opinion on whether to name them libraries or
  packages, I just think we should have a uniform name.
- JS/TS only uses the terms "module" and "namespace", however `package`
  is a reserved keyword for future use, whereas `library` is not.

* Refactor registration.ts into dynamic-slotting

- This is part of an effort to refactor the dynamic slotting (extending
  buttons) functionality out of components like ButtonGroup.

* Remove dynamically-slottable logic from ButtonToolbar

* Use DynamicallySlottable in editor-toolbar

* Fix no border radius on indentation button dropdown

* Fix AddonButtons

* Remove Item/ButtonGroupItem in deck-options, where it's not necessary

* Remove unnecessary uses of Item and ButtonGroupItem

* Fix remaining tests

* Fix relative imports

* Revert change return value of remapBinToSrcDir to ./bazel/out...

* Remove typings directory

* Adjust comments for dynamic-slottings
2022-02-03 14:52:11 +10:00

310 lines
9.4 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: string, contents: string): void => {
// 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) {
if (!(file.virtualDtsPath in createdFiles)) {
/**
* This can happen if you do a case-only rename
* e.g. NoteTypeButtons.svelte -> NotetypeButtons.svelte
*/
console.log(
"file not among created files: ",
file.virtualDtsPath,
Object.keys(createdFiles),
);
} else {
await writeFile(file.realDtsPath, createdFiles[file.virtualDtsPath]);
}
}
}
async function writeFile(file: string, data: string): Promise<void> {
await fs.promises.writeFile(file, data);
}
function readFile(file: string): Promise<string> {
return fs.promises.readFile(file, "utf-8");
}
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;
}