From 1f876cfe397a9dd29acc487d252677a29b151216 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 18 Oct 2021 12:44:29 +1000 Subject: [PATCH] Svelte build improvements 1. All Svelte files in a package are compiled in one step now, which ensures that properties that use types from a different Svelte file in the same package are typed correctly. The single-file svelte() has been removed, and compile_svelte() may be renamed to svelte() in the future. 2. The .ts files in the same package are included as part of the Svelte compilation, so that types imported imported from .ts files in the same package work. 3. Dependencies passed into the rule are now loaded into the TypeScript compiler, so that properties referencing types from different packages work. We'll need to update our compile_svelte() lines to list the dependencies. For example, before this change: % cat bazel-bin/ts/congrats/CongratsPage.svelte.d.ts import { SvelteComponentTyped } from "svelte"; declare const __propDef: { props: { info: any; }; ... After adding //ts/lib to the deps of compile_svelte() in ts/congrats: % cat bazel-bin/ts/congrats/CongratsPage.svelte.d.ts import { SvelteComponentTyped } from "svelte"; import type { Scheduler } from "../lib/proto"; declare const __propDef: { props: { info: Scheduler.CongratsInfoResponse; }; ... --- ts/congrats/BUILD.bazel | 11 +-- ts/svelte/svelte.bzl | 83 ++++++++++------- ts/svelte/svelte.ts | 197 +++++++++++++++++++++++++++++----------- 3 files changed, 198 insertions(+), 93 deletions(-) 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); } }