@synaptic-simulations/mach
Version:
The last MSFS instrument bundler you'll ever need.
211 lines (192 loc) • 7.01 kB
text/typescript
/*
* SPDX-FileCopyrightText: 2022 Synaptic Simulations and its contributors
* SPDX-License-Identifier: MIT
*/
import type { BuildOptions, BuildResult, LogLevel, Plugin } from "esbuild";
import { z } from "zod";
export type BuildResultWithMeta = BuildResult<{ metafile: true }>;
interface PackageSettings {
/**
* Specifies type of instrument.
* - `React` instruments will be created with a `BaseInstrument` harness that exposes an `MSFS_REACT_MOUNT` element for mounting.
* - `BaseInstrument` instruments must specify the `instrumentId` and `mountElementId` to match the instrument configuration.
*/
type: string;
/** Final template filename. Defaults to `instrument`. */
fileName?: string;
/** Simulator packages to import in the HTML template. */
imports?: string[];
/** Paths to custom HTML template files */
htmlTemplate?: string;
}
interface ReactInstrumentPackageSettings extends PackageSettings {
type: "react";
/** Optional parameter to specify template ID. Defaults to `Instrument.name`. */
templateId?: string;
/** Whether the instrument is interactive or not. Defaults to `true`. */
isInteractive?: boolean;
/** Paths to custom JavaScript template files */
jsTemplate?: string;
}
interface BaseInstrumentPackageSettings extends PackageSettings {
type: "baseInstrument";
/**
* Required for `BaseInstrument` instruments.
* This value must match the return value from the `BaseInstrument.templateID()` function.
* */
templateId: string;
/**
* Required for `BaseInstrument` instruments.
* This value must match the ID in your call to `FSComponent.render()`..
*/
mountElementId: string;
}
export interface Instrument {
/** Instrument name, used as directory name for bundles and packages. */
name: string;
/** Entrypoint filename for instrument. */
index: string;
/** When passed a configuration object, enables a simulator package export. */
simulatorPackage?: ReactInstrumentPackageSettings | BaseInstrumentPackageSettings;
/** Instruments to import as ESM modules. */
modules?: Instrument[];
/** (Required for instruments included as `modules`) Import name to resolve to the bundled module. */
resolve?: string;
}
export interface MachConfig {
/** Name of package, used for bundling simulator packages. */
packageName: string;
/** Path to directory containing `html_ui`. */
packageDir: string;
/**
* esbuild configuration overrides (<https://esbuild.github.io/api/>)
* `entryPoints`, `outfile`, `format`, `metafile`, and `bundle` are not overridable.
*/
esbuild?: BuildOptions;
/** All instruments to be bundled by Mach. */
instruments: Instrument[];
}
export interface MachArgs {
/** Build configuration. */
config: MachConfig;
/** Path to bundle output directory. Defaults to `./bundles` */
bundles?: string;
/** Treat all ESBuild warnings as errors. */
werror?: boolean;
/** Instrument names to include. */
filter?: RegExp;
/** Minify bundled code. */
minify?: boolean;
/** Output additional build debug info. */
verbose?: boolean;
/** Output `build_meta.json` file to bundle directory. */
outputMetafile?: boolean;
/** Generate source maps. */
sourcemaps?: "linked" | "external" | "inline" | "both";
/** Skip writing simulator package files. */
skipSimulatorPackage?: boolean;
}
export const PluginSchema: z.ZodType<Plugin> = z.object({
name: z.string(),
setup: z
.function()
.args(z.any())
.returns(z.union([z.void(), z.promise(z.void())])),
});
export const InstrumentSchema: z.ZodType<Instrument> = z.lazy(() =>
z.object({
name: z.string(),
index: z.string(),
simulatorPackage: z
.union([
z.object({
type: z.literal("react"),
fileName: z.string().optional(),
templateName: z.string().optional(),
imports: z.array(z.string()).optional(),
templateId: z.string().optional(),
isInteractive: z.boolean().optional(),
htmlTemplate: z.string().optional(),
}),
z.object({
type: z.literal("baseInstrument"),
fileName: z.string().optional(),
templateName: z.string().optional(),
imports: z.array(z.string()).optional(),
templateId: z.string(),
mountElementId: z.string(),
htmlTemplate: z.string().optional(),
jsTemplate: z.string().optional(),
}),
])
.optional(),
modules: z.array(InstrumentSchema).optional(),
resolve: z.string().optional(),
}),
);
export const MachConfigSchema = z.object({
packageName: z.string(),
packageDir: z.string(),
// `passthrough` allows us to enforce an object-shaped value without specifying the fields
esbuild: z.object({}).passthrough().optional(),
instruments: z.array(InstrumentSchema),
});
export const MachArgsSchema = z.object({
config: MachConfigSchema,
bundles: z.string().optional(),
werror: z.boolean().optional(),
filter: z.instanceof(RegExp).optional(),
minify: z.boolean().optional(),
verbose: z.boolean().optional(),
outputMetafile: z.boolean().optional(),
sourcemaps: z.enum(["linked", "external", "inline", "both"]).optional(),
skipSimulatorPackage: z.boolean().optional(),
});
const ESBUILD_WARNINGS = [
"assign-to-constant",
"assign-to-import",
"call-import-namespace",
"commonjs-variable-in-esm",
"delete-super-property",
"duplicate-case",
"duplicate-object-key",
"empty-import-meta",
"equals-nan",
"equals-negative-zero",
"equals-new-object",
"html-comment-in-js",
"impossible-typeof",
"private-name-will-throw",
"semicolon-after-return",
"suspicious-boolean-not",
"this-is-undefined-in-esm",
"unsupported-dynamic-import",
"unsupported-jsx-comment",
"unsupported-regexp",
"unsupported-require-call",
"css-syntax-error",
"invalid-@charset",
"invalid-@import",
"invalid-@nest",
"invalid-@layer",
"invalid-calc",
"js-comment-in-css",
"unsupported-@charset",
"unsupported-@namespace",
"unsupported-css-property",
"ambiguous-reexport",
"different-path-case",
"ignored-bare-import",
"ignored-dynamic-import",
"import-is-undefined",
"require-resolve-not-external",
"invalid-source-mappings",
"sections-in-source-map",
"missing-source-map",
"unsupported-source-map-comment",
"package.json",
"tsconfig.json",
];
export const ESBUILD_ERRORS: Record<string, LogLevel> = Object.fromEntries(
ESBUILD_WARNINGS.map((warning) => [warning, "error"]),
);