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;
    };
...
This commit is contained in:
Damien Elmes 2021-10-18 12:44:29 +10:00
parent a29bd7c9f0
commit 1f876cfe39
3 changed files with 198 additions and 93 deletions

View file

@ -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",
],

View file

@ -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,
)

View file

@ -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<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 = {};
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<void> {
@ -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<void> {
@ -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<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;
}
@ -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);
}
}