diff --git a/ts/congrats/BUILD.bazel b/ts/congrats/BUILD.bazel index 0a978b2bd..c9b4de4ca 100644 --- a/ts/congrats/BUILD.bazel +++ b/ts/congrats/BUILD.bazel @@ -1,7 +1,7 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library") load("//ts:prettier.bzl", "prettier_test") load("//ts:eslint.bzl", "eslint_test") -load("//ts/svelte:svelte.bzl", "svelte", "svelte_check") +load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check") load("//ts:esbuild.bzl", "esbuild") load("//ts:compile_sass.bzl", "compile_sass") load("//ts:typescript.bzl", "typescript") @@ -16,15 +16,14 @@ compile_sass( ], ) -svelte( - name = "CongratsPage", - entry_point = "CongratsPage.svelte", +compile_svelte( + deps = ["//ts/lib"], ) typescript( name = "index", deps = [ - "CongratsPage", + ":svelte", "//ts/lib", "@npm//@fluent", "@npm//svelte", @@ -41,9 +40,9 @@ esbuild( output_css = "congrats.css", visibility = ["//visibility:public"], deps = [ - "CongratsPage", ":base_css", ":index", + ":svelte", "//ts/lib", "@npm//protobufjs", ], diff --git a/ts/svelte/svelte.bzl b/ts/svelte/svelte.bzl index 2f45ddc66..fec7882da 100644 --- a/ts/svelte/svelte.bzl +++ b/ts/svelte/svelte.bzl @@ -2,82 +2,97 @@ load("@npm//svelte-check:index.bzl", _svelte_check = "svelte_check_test") load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "declaration_info") load("@io_bazel_rules_sass//:defs.bzl", "SassInfo") load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("@bazel_skylib//lib:paths.bzl", "paths") -def _get_dep_sources(dep): +def _get_declarations(dep): if SassInfo in dep: return dep[SassInfo].transitive_sources elif DeclarationInfo in dep: return dep[DeclarationInfo].transitive_declarations else: - return [] - -def _get_sources(deps): - return depset([], transitive = [_get_dep_sources(dep) for dep in deps]) + fail("unexpected dep", dep) def _svelte(ctx): args = ctx.actions.args() args.use_param_file("@%s", use_always = True) args.set_param_file_format("multiline") - args.add(ctx.file.entry_point.path) - args.add(ctx.outputs.mjs.path) - args.add(ctx.outputs.dts.path) - args.add(ctx.outputs.css.path) + # path to bin folder, for sass args.add(ctx.var["BINDIR"]) args.add(ctx.var["GENDIR"]) - args.add_all(ctx.files._shims) - deps = _get_sources(ctx.attr.deps).to_list() + # svelte and ts sources + outputs = [] + dts_only = [] + nondts_only = [] + for src in ctx.files.srcs: + args.add(src.path) + + if src.path.endswith(".svelte"): + # strip off external/ankidesktop if invoked from external workspace + path = src.path + if src.path.startswith("external/ankidesktop/"): + path = path[len("external/ankidesktop/"):] + + # strip off package prefix, eg ts/editor/mathjax/Foo.svelte -> mathjax/Foo.svelte + base = path[len(ctx.label.package) + 1:] + for ext in ("d.ts", "css", "mjs"): + out = ctx.actions.declare_file(base.replace(".svelte", ".svelte." + ext)) + args.add(out) + outputs.append(out) + if ext == "d.ts": + dts_only.append(out) + else: + nondts_only.append(out) + + # dependencies + deps = depset([], transitive = [_get_declarations(dep) for dep in ctx.attr.deps]) + args.add_all(deps) ctx.actions.run( execution_requirements = {"supports-workers": "1"}, executable = ctx.executable._svelte_bin, - outputs = [ctx.outputs.mjs, ctx.outputs.dts, ctx.outputs.css], - inputs = [ctx.file.entry_point] + deps + ctx.files._shims, + outputs = outputs, + inputs = ctx.files.srcs + deps.to_list(), mnemonic = "Svelte", + progress_message = "Compiling Svelte {}:{}".format(ctx.label.package, ctx.attr.name), arguments = [args], ) return [ - declaration_info(depset([ctx.outputs.dts]), deps = [ctx.attr._shims]), + declaration_info(depset(dts_only), deps = ctx.attr.deps), + DefaultInfo( + files = depset(nondts_only), + runfiles = ctx.runfiles(files = outputs, transitive_files = deps), + ), ] svelte = rule( implementation = _svelte, attrs = { - "entry_point": attr.label(allow_single_file = True), + "srcs": attr.label_list(allow_files = True, doc = ".ts and .svelte files"), "deps": attr.label_list(), "_svelte_bin": attr.label( default = Label("//ts/svelte:svelte_bin"), executable = True, cfg = "host", ), - "_shims": attr.label( - default = Label("@npm//svelte2tsx:svelte2tsx__typings"), - allow_files = True, - ), - }, - outputs = { - "mjs": "%{name}.svelte.mjs", - "dts": "%{name}.svelte.d.ts", - "css": "%{name}.svelte.css", }, ) def compile_svelte(name = "svelte", srcs = None, deps = [], visibility = ["//visibility:private"]): if not srcs: - srcs = native.glob(["*.svelte"]) - for src in srcs: - svelte( - name = src.replace(".svelte", ""), - entry_point = src, - deps = deps, - visibility = visibility, - ) + srcs = native.glob([ + "**/*.svelte", + "**/*.ts", + ]) - js_library( + svelte( name = name, - srcs = [s.replace(".svelte", "") for s in srcs], + srcs = srcs, + deps = deps + [ + "@npm//svelte2tsx", + ], visibility = visibility, ) diff --git a/ts/svelte/svelte.ts b/ts/svelte/svelte.ts index 148fabc67..aee508b89 100644 --- a/ts/svelte/svelte.ts +++ b/ts/svelte/svelte.ts @@ -10,21 +10,20 @@ import { basename } from "path"; import * as ts from "typescript"; import * as svelte from "svelte/compiler.js"; -let parsedCommandLine: ts.ParsedCommandLine = { +const parsedCommandLine: ts.ParsedCommandLine = { fileNames: [], errors: [], options: { jsx: ts.JsxEmit.Preserve, declaration: true, emitDeclarationOnly: true, - skipLibCheck: true, + // noEmitOnError: true, + paths: { + "*": ["*", "external/npm/node_modules/*"], + }, }, }; -// We avoid hitting the filesystem for ts/d.ts files after initial startup - the -// .ts file we generate can be injected directly into our cache, and Bazel -// should restart us if the Svelte or TS typings change. - interface FileContent { text: string; version: number; @@ -43,17 +42,17 @@ function getFileContent(path: string): FileContent { return content; } -function updateFileContent(path: string, text: string): void { - let content = fileContent.get(path); +function updateFileContent(input: InputFile): void { + let content = fileContent.get(input.path); if (content) { - content.text = text; + content.text = input.data; content.version += 1; } else { content = { - text, + text: input.data, version: 0, }; - fileContent.set(path, content); + fileContent.set(input.path, content); } } @@ -65,7 +64,6 @@ const languageServiceHost: ts.LanguageServiceHost = { return getFileContent(path).version.toString(); }, getScriptSnapshot: (path: string): ts.IScriptSnapshot | undefined => { - // if (!ts.sys.fileExists(fileName)) { const text = getFileContent(path).text; return { getText: (start: number, end: number) => { @@ -78,7 +76,7 @@ const languageServiceHost: ts.LanguageServiceHost = { }, getLength: () => text.length, getChangeRange: ( - oldSnapshot: ts.IScriptSnapshot + _oldSnapshot: ts.IScriptSnapshot ): ts.TextChangeRange | undefined => { return undefined; }, @@ -90,14 +88,29 @@ const languageServiceHost: ts.LanguageServiceHost = { const languageService = ts.createLanguageService(languageServiceHost); -function compile(tsPath: string, tsLibs: string[]) { - parsedCommandLine.fileNames = [tsPath, ...tsLibs]; +async function emitTypings(svelte: SvelteTsxFile[], deps: InputFile[]): Promise { + 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 = {}; - tsHost.writeFile = (fileName, contents) => (createdFiles[fileName] = contents); - program.emit(undefined /* all files */, tsHost.writeFile); - return createdFiles[parsedCommandLine.fileNames[0].replace(".tsx", ".d.ts")]; + 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 { @@ -124,26 +137,8 @@ function readFile(file) { }); } -async function writeDts(tsPath, dtsPath, tsLibs) { - const dtsSource = compile(tsPath, tsLibs); - await writeFile(dtsPath, dtsSource); -} - -function writeTs(svelteSource, sveltePath, tsPath): void { - let tsSource = svelte2tsx(svelteSource, { - filename: sveltePath, - isTsFile: true, - mode: "dts", - }); - let codeLines = tsSource.code.split("\n"); - updateFileContent(tsPath, codeLines.join("\n")); -} - -async function writeJs( - source: string, - inputFilename: string, - outputJsPath: string, - outputCssPath: string, +async function compileSingleSvelte( + input: SvelteInput, binDir: string, genDir: string ): Promise { @@ -162,14 +157,14 @@ async function writeJs( }); try { - const processed = await svelte.preprocess(source, preprocessOptions, { - filename: inputFilename, + const processed = await svelte.preprocess(input.data, preprocessOptions, { + filename: input.path, }); const result = svelte.compile(processed.toString!(), { format: "esm", css: false, generate: "dom", - filename: outputJsPath, + filename: input.mjsPath, }); // warnings are an error if (result.warnings.length > 0) { @@ -177,27 +172,123 @@ async function writeJs( } // write out the css file const outputCss = result.css.code ?? ""; - await writeFile(outputCssPath, outputCss); + 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(outputCssPath)}";` : "") + + (outputCss ? `import "./${basename(input.cssPath)}";` : "") + result.js.code; - await writeFile(outputJsPath, outputSource); + await writeFile(input.mjsPath, outputSource); } catch (err) { console.log(`compile failed: ${err}`); return; } } -async function compileSvelte(args) { - const [sveltePath, mjsPath, dtsPath, cssPath, binDir, genDir, ...tsLibs] = args; - const svelteSource = (await readFile(sveltePath)) as string; +interface Args { + binDir: string; + genDir: string; + svelteFiles: SvelteInput[]; + dependencies: InputFile[]; +} - const mockTsPath = sveltePath + ".tsx"; - writeTs(svelteSource, sveltePath, mockTsPath); - await writeDts(mockTsPath, dtsPath, tsLibs); - await writeJs(svelteSource, sveltePath, mjsPath, cssPath, binDir, genDir); +interface InputFile { + path: string; + data: string; +} + +interface SvelteInput extends InputFile { + dtsPath: string; + cssPath: string; + mjsPath: string; +} + +async function extractArgsAndData(args: string[]): Promise { + 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 { + 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 { + 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; } @@ -206,12 +297,12 @@ function main() { if (worker.runAsWorker(process.argv)) { console.log = worker.log; worker.log("Svelte running as a Bazel worker"); - worker.runWorkerLoop(compileSvelte); + 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"); - compileSvelte(commandLineArgs); + compileSvelteAndGenerateTypings(commandLineArgs); } }