From 243030cb9ebe8984bf0da1f9d66d0b90a943b023 Mon Sep 17 00:00:00 2001 From: Mark Pearce Date: Wed, 15 Jul 2026 15:51:22 -0300 Subject: [PATCH] Add `compilerOptions` group to bsconfig.json Introduces a dedicated `compilerOptions` object (similar to TypeScript's) for options that control how BrighterScript interprets, validates, and compiles code, as opposed to top-level options that control project structure, file discovery, and tooling. - 17 options move into `compilerOptions`: allowBrighterScriptInBrightScript, autoImportComponentScript, bslibDestinationDir, emitDefinitions, emitFullPaths, legacyCallfuncHandling, manifest, minFirmwareVersion, pruneEmptyCodeFiles, relativeSourceMaps, removeParameterTypes, resolveSourceRoot, sourceMap, sourceRoot, strict, strictCallFunc, strictNodeMembers. - Legacy top-level locations still work; `compilerOptions` wins if both are set. - Using a deprecated top-level option loaded from an actual bsconfig.json file (including via `extends`) emits a `deprecated-bsconfig-option` warning diagnostic. Options passed programmatically to Program/ProgramBuilder are not flagged, since that isn't bsconfig.json authoring. - `compilerOptions` deep-merges across an `extends` chain, unlike other bsconfig keys. Closes #1734 Co-Authored-By: Claude Sonnet 5 --- README.md | 8 +- bsconfig.schema.json | 136 +++++++++++++++++++++++ docs/bsconfig.md | 67 ++++++++++++ src/BsConfig.ts | 214 +++++++++++++++++++++++++++++-------- src/DiagnosticMessages.ts | 6 ++ src/Program.ts | 9 +- src/ProgramBuilder.spec.ts | 38 ++++++- src/ProgramBuilder.ts | 7 ++ src/util.spec.ts | 100 +++++++++++++++++ src/util.ts | 62 ++++++++++- 10 files changed, 594 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d5d422da7..b41996b77 100644 --- a/README.md +++ b/README.md @@ -186,9 +186,11 @@ The presence of a `bsconfig.json` file in a directory indicates that the directo "**/*" ], "outDir": "out", - //this flag tells BrighterScript that for every xml file, try to import a .bs file with the same name and location - "autoImportComponentScript": true, - "sourceMap": true + "compilerOptions": { + //this flag tells BrighterScript that for every xml file, try to import a .bs file with the same name and location + "autoImportComponentScript": true, + "sourceMap": true + } } ``` diff --git a/bsconfig.schema.json b/bsconfig.schema.json index b2168da82..bdd05d56b 100644 --- a/bsconfig.schema.json +++ b/bsconfig.schema.json @@ -70,6 +70,8 @@ }, "manifest": { "description": "A entry to modify manifest values", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.manifest` instead.", "type": "object", "properties": { "bs_const": { @@ -147,11 +149,15 @@ }, "sourceMap": { "description": "Enables generating sourcemap files, which allow debugging tools to show the original source code while running the emitted files.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.sourceMap` instead.", "type": "boolean", "default": false }, "relativeSourceMaps": { "description": "If true, source file paths in generated sourcemaps will be relative to the map file location instead of absolute. This makes sourcemaps portable across different machines and environments.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.relativeSourceMaps` instead.", "type": "boolean", "default": false }, @@ -258,16 +264,22 @@ }, "emitFullPaths": { "description": "Emit full paths to files when printing diagnostics to the console.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.emitFullPaths` instead.", "type": "boolean", "default": false }, "emitDefinitions": { "description": "Emit type definition files (`d.bs`) during transpile", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.emitDefinitions` instead.", "type": "boolean", "default": false }, "removeParameterTypes": { "description": "Removes the explicit type to function's parameters and return (i.e. the `as type` syntax) ", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.removeParameterTypes` instead.", "type": "boolean", "default": false }, @@ -343,6 +355,8 @@ }, "autoImportComponentScript": { "description": "When enabled, every xml component will search for a .bs or .brs file with the same name in the same folder, and add it as a script import if found. Disabled by default", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.autoImportComponentScript` instead.", "type": "boolean", "default": false }, @@ -361,10 +375,14 @@ }, "sourceRoot": { "description": "Override the root directory path where debugger should locate the source files. The location will be embedded in the source map to help debuggers locate the original source files. This only applies to files found within rootDir. This is useful when you want to preprocess files before passing them to BrighterScript, and want a debugger to open the original files. This option also affects the `SOURCE_FILE_PATH` and `SOURCE_LOCATION` source literals.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.sourceRoot` instead.", "type": "string" }, "resolveSourceRoot": { "description": "Should the `sourceRoot` property be resolve to an absolute path (relative to the bsconfig it's defined in)", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.resolveSourceRoot` instead.", "type": "boolean", "default": false }, @@ -396,16 +414,22 @@ }, "allowBrighterScriptInBrightScript": { "description": "Allow brighterscript features (classes, interfaces, etc...) to be included in BrightScript (`.brs`) files, and force those files to be transpiled.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.allowBrighterScriptInBrightScript` instead.", "type": "boolean", "default": false }, "legacyCallfuncHandling": { "description": "Legacy RokuOS versions required at least one argument in callfunc() invocations. Previous brighterscript versions handled this by inserting invalid as an argument when no other args are present. This is not necessary in modern RokuOS versions.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.legacyCallfuncHandling` instead.", "type": "boolean", "default": false }, "minFirmwareVersion": { "description": "The minimum Roku firmware version required to run this project. When set, BrightScript (.brs) files will be validated to ensure they only use language features available in that firmware version or earlier. BrighterScript (.bs) files are always allowed to use all features since they are transpiled. Should be a semver-compatible string (e.g. \"11.0.0\").", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.minFirmwareVersion` instead.", "type": "string", "examples": [ "11.0.0", @@ -414,18 +438,130 @@ }, "strict": { "description": "Enables a set of stricter type-checking rules and language features. When set, this acts as a default value for other `strict`-related options. Defaults to false.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.strict` instead.", "type": "boolean", "default": false }, "strictCallFunc": { "description": "Enables stricter type-checking for callfunc() invocations. When true, callfunc() invocations will not be allowed on generic Node types. Defaults to false.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.strictCallFunc` instead.", "type": "boolean", "default": false }, "strictNodeMembers": { "description": "Enables stricter type-checking for Node members. When true, unknown members on Node types will be treated as errors instead of dynamic values. Defaults to false.", + "deprecated": true, + "deprecationMessage": "Deprecated starting in v1. Use `compilerOptions.strictNodeMembers` instead.", "type": "boolean", "default": false + }, + "compilerOptions": { + "description": "Options that control how BrighterScript interprets, validates, and compiles your code (as opposed to the top-level options, which control project structure, file discovery, and tooling/reporting behavior). Similar in spirit to TypeScript's `compilerOptions`. If an option is set both here and at the top level (deprecated location), the value here wins.", + "type": "object", + "properties": { + "allowBrighterScriptInBrightScript": { + "description": "Allow brighterscript features (classes, interfaces, etc...) to be included in BrightScript (`.brs`) files, and force those files to be transpiled.", + "type": "boolean", + "default": false + }, + "autoImportComponentScript": { + "description": "When enabled, every xml component will search for a .bs or .brs file with the same name in the same folder, and add it as a script import if found. Disabled by default", + "type": "boolean", + "default": false + }, + "bslibDestinationDir": { + "description": "Override the destination directory for the bslib.brs file. Use this if you want to customize where the bslib.brs file is located in the staging directory. Note that using a location outside of `source` will break scripts inside `source` that depend on bslib.brs. Defaults to `source`.", + "type": "string", + "default": "source" + }, + "emitDefinitions": { + "description": "Emit type definition files (`d.bs`) during transpile", + "type": "boolean", + "default": false + }, + "emitFullPaths": { + "description": "Emit full paths to files when printing diagnostics to the console.", + "type": "boolean", + "default": false + }, + "legacyCallfuncHandling": { + "description": "Legacy RokuOS versions required at least one argument in callfunc() invocations. Previous brighterscript versions handled this by inserting invalid as an argument when no other args are present. This is not necessary in modern RokuOS versions.", + "type": "boolean", + "default": false + }, + "manifest": { + "description": "A entry to modify manifest values", + "type": "object", + "properties": { + "bs_const": { + "description": "A dictionary of bs_consts to change or add to the manifest. Each entry ", + "type": "object", + "patternProperties": { + "^.+$": { + "type": [ + "boolean", + "null" + ] + } + } + } + } + }, + "minFirmwareVersion": { + "description": "The minimum Roku firmware version required to run this project. When set, BrightScript (.brs) files will be validated to ensure they only use language features available in that firmware version or earlier. BrighterScript (.bs) files are always allowed to use all features since they are transpiled. Should be a semver-compatible string (e.g. \"11.0.0\").", + "type": "string", + "examples": [ + "11.0.0", + "12.0.0" + ] + }, + "pruneEmptyCodeFiles": { + "description": "Excludes empty files from being included in the output. Some Brighterscript files are left empty or with only comments after transpilation to Brightscript. The default behavior is to write these to disk after transpilation. Setting this flag to `true` will prevent empty files being written and will remove associated script tags from XML.", + "type": "boolean", + "default": false + }, + "relativeSourceMaps": { + "description": "If true, source file paths in generated sourcemaps will be relative to the map file location instead of absolute. This makes sourcemaps portable across different machines and environments.", + "type": "boolean", + "default": false + }, + "removeParameterTypes": { + "description": "Removes the explicit type to function's parameters and return (i.e. the `as type` syntax) ", + "type": "boolean", + "default": false + }, + "resolveSourceRoot": { + "description": "Should the `sourceRoot` property be resolve to an absolute path (relative to the bsconfig it's defined in)", + "type": "boolean", + "default": false + }, + "sourceMap": { + "description": "Enables generating sourcemap files, which allow debugging tools to show the original source code while running the emitted files.", + "type": "boolean", + "default": false + }, + "sourceRoot": { + "description": "Override the root directory path where debugger should locate the source files. The location will be embedded in the source map to help debuggers locate the original source files. This only applies to files found within rootDir. This is useful when you want to preprocess files before passing them to BrighterScript, and want a debugger to open the original files. This option also affects the `SOURCE_FILE_PATH` and `SOURCE_LOCATION` source literals.", + "type": "string" + }, + "strict": { + "description": "Enables a set of stricter type-checking rules and language features. When set, this acts as a default value for other `strict`-related options. Defaults to false.", + "type": "boolean", + "default": false + }, + "strictCallFunc": { + "description": "Enables stricter type-checking for callfunc() invocations. When true, callfunc() invocations will not be allowed on generic Node types. Defaults to false.", + "type": "boolean", + "default": false + }, + "strictNodeMembers": { + "description": "Enables stricter type-checking for Node members. When true, unknown members on Node types will be treated as errors instead of dynamic values. Defaults to false.", + "type": "boolean", + "default": false + } + } } } } \ No newline at end of file diff --git a/docs/bsconfig.md b/docs/bsconfig.md index 61b5e5769..c55648a35 100644 --- a/docs/bsconfig.md +++ b/docs/bsconfig.md @@ -3,6 +3,7 @@ While a minimal `bsconfig.json` file is sufficient for getting started, `bsc` supports a range of helpful options. - [bsconfig.json options](#bsconfigjson-options) + - [`compilerOptions`](#compileroptions) - [`allowBrighterScriptInBrightScript`](#allowbrighterscriptinbrightscript) - [`autoImportComponentScript`](#autoimportcomponentscript) - [`bslibDestinationDir`](#bslibdestinationdir) @@ -46,20 +47,68 @@ While a minimal `bsconfig.json` file is sufficient for getting started, `bsc` su - [`username`](#username) - [`watch`](#watch) +## `compilerOptions` + +Type: `object` + +A dedicated group for options that control how BrighterScript interprets, validates, and compiles your code — as opposed to the other top-level `bsconfig.json` options, which mostly control project structure, file discovery, and tooling/reporting behavior. This mirrors how TypeScript's `tsconfig.json` separates `compilerOptions` from the rest of the config. For example: + +```jsonc +{ + "rootDir": "src", + "outDir": "out", + "compilerOptions": { + "strict": true, + "sourceMap": true, + "autoImportComponentScript": true + } +} +``` + +The following options live inside `compilerOptions`: + +- [`allowBrighterScriptInBrightScript`](#allowbrighterscriptinbrightscript) +- [`autoImportComponentScript`](#autoimportcomponentscript) +- [`bslibDestinationDir`](#bslibdestinationdir) +- [`emitDefinitions`](#emitdefinitions) +- [`emitFullPaths`](#emitfullpaths) +- `legacyCallfuncHandling` +- `manifest` +- [`minFirmwareVersion`](#minfirmwareversion) +- [`pruneEmptyCodeFiles`](#pruneemptycodefiles) +- [`relativeSourceMaps`](#relativesourcemaps) +- [`removeParameterTypes`](#removeparametertypes) +- [`resolveSourceRoot`](#sourceroot) +- [`sourceMap`](#sourcemap) +- [`sourceRoot`](#sourceroot) +- [`strict`](#strict) +- [`strictCallFunc`](#strictCallFunc) +- [`strictNodeMembers`](#strictNodeMembers) + +Each of these options used to live at the top level of `bsconfig.json`. Those top-level locations still work for backwards compatibility, but are **deprecated** — using one emits a `deprecated-bsconfig-option` warning diagnostic pointing you at the `compilerOptions` equivalent. If an option is set in both places, the value in `compilerOptions` wins. + +`extends` deep-merges `compilerOptions` across the config chain (a child config that only sets one option under `compilerOptions` does not wipe out the other options set by a parent config it extends) — this is different from every other `bsconfig.json` option, where the child's value completely replaces the parent's. + ## `allowBrighterScriptInBrightScript` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.allowBrighterScriptInBrightScript`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` Allow BrighterScript features (classes, interfaces, etc...) to be included in BrightScript (`.brs`) files, and force those files to be transpiled. ## `autoImportComponentScript` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.autoImportComponentScript`). The top-level location still works but is deprecated and emits a warning. + Type: `bool` BrighterScript only: will automatically import a script at transpile-time for a component with the same name if it exists. Defaults to `false`. ## `bslibDestinationDir` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.bslibDestinationDir`). The top-level location still works but is deprecated and emits a warning. + Type: `string` Override the destination directory for the bslib.brs file. Use this if you want to customize where the bslib.brs file is located in the staging directory. Note that using a location outside of `source` will break scripts inside `source` that depend on bslib.brs. @@ -215,12 +264,16 @@ A map of error codes and severity levels that will override diagnostics' severit ## `emitDefinitions` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.emitDefinitions`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` Emit type definition files (`d.bs`) during transpile. Defaults to `false`. ## `emitFullPaths` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.emitFullPaths`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` Emit full paths to files when printing diagnostics to the console. Defaults to `false`. @@ -435,6 +488,8 @@ The host of the Roku that this project will deploy to when the [`deploy`](#deplo ## `minFirmwareVersion` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.minFirmwareVersion`). The top-level location still works but is deprecated and emits a warning. + Type: `string` The minimum Roku firmware version required to run this project. When set, files are validated to ensure they only use language features available in that firmware version or earlier. BrightScript (`.brs`) files are always validated against the version restriction. BrighterScript (`.bs`) files are only validated for features that BrighterScript does not transpile — for example, optional chaining is emitted as-is rather than transpiled down, so it is subject to the version restriction. BrighterScript features that are fully transpiled (such as classes) are not restricted, since the transpiled output is compatible with older firmware. @@ -509,12 +564,16 @@ A path to a project file. This is really only passed in from the command line or ## `pruneEmptyCodeFiles` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.pruneEmptyCodeFiles`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` Remove files from the final package which would be empty or consist entirely of comments after compilation. Also removes imports of any such empty scripts from XML files. This can speed up sideloading packages during development. Defaults to `false`. ## `removeParameterTypes` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.removeParameterTypes`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` If true, removes the explicit type to function's parameters and return (i.e. the `as type` syntax); otherwise keep this information. @@ -535,12 +594,16 @@ The root directory of your roku project. Defaults to `process.cwd()`. ## `sourceMap` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.sourceMap`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` Enables generating sourcemap files (`.map`), which allow debugging tools to show the original source code while running the emitted files. Defaults to `false`. ## `relativeSourceMaps` +> **Deprecated:** move this option into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.relativeSourceMaps`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` If `true`, file paths in the sourcemap `sources` array will be written as relative paths instead of absolute paths, and the behavior of [`sourceRoot`](#sourceroot) changes. Defaults to `false`. @@ -577,6 +640,8 @@ If [`sourceRoot`](#sourceroot) is also set, the map file's `sourceRoot` field is ## `sourceRoot` +> **Deprecated:** move this option (and `resolveSourceRoot`) into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.sourceRoot` / `compilerOptions.resolveSourceRoot`). The top-level location still works but is deprecated and emits a warning. + Type: `string` Overrides where source files appear to live, both in sourcemaps and in the `SOURCE_FILE_PATH` / `SOURCE_LOCATION` runtime literals. Only applies to files within [`rootDir`](#rootdir) — files outside `rootDir` are unaffected. @@ -592,6 +657,8 @@ In both cases, `SOURCE_FILE_PATH` and `SOURCE_LOCATION` source literals embedded ## `strict` +> **Deprecated:** move this option (and `strictCallFunc`/`strictNodeMembers`) into [`compilerOptions`](#compileroptions) (i.e. `compilerOptions.strict`). The top-level location still works but is deprecated and emits a warning. + Type: `boolean` Enables a set of stricter type-checking rules and language features. When set, this acts as a default value for other `strict`-related options. Defaults to false. diff --git a/src/BsConfig.ts b/src/BsConfig.ts index 801888cb6..8e004187d 100644 --- a/src/BsConfig.ts +++ b/src/BsConfig.ts @@ -12,6 +12,9 @@ export interface BsConfig { */ project?: string; + /** + * @deprecated Moved to `compilerOptions.manifest`. If both are present, `compilerOptions.manifest` wins. + */ manifest?: { bs_const?: Record; }; @@ -98,19 +101,17 @@ export interface BsConfig { diagnosticSeverityOverrides?: Record; /** - * Emit full paths to files when printing diagnostics to the console. Defaults to false + * @deprecated Moved to `compilerOptions.emitFullPaths`. If both are present, `compilerOptions.emitFullPaths` wins. */ emitFullPaths?: boolean; /** - * Emit type definition files (`d.bs`) - * @default false + * @deprecated Moved to `compilerOptions.emitDefinitions`. If both are present, `compilerOptions.emitDefinitions` wins. */ emitDefinitions?: boolean; /** - * If true, removes the explicit type to function's parameters and return (i.e. the `as type` syntax); otherwise keep this information. - * @default false + * @deprecated Moved to `compilerOptions.removeParameterTypes`. If both are present, `compilerOptions.removeParameterTypes` wins. */ removeParameterTypes?: boolean; @@ -162,8 +163,7 @@ export interface BsConfig { require?: Array; /** - * When enabled, every xml component will search for a .bs or .brs file with the same name - * in the same folder, and add it as a script import if found. Disabled by default" + * @deprecated Moved to `compilerOptions.autoImportComponentScript`. If both are present, `compilerOptions.autoImportComponentScript` wins. */ autoImportComponentScript?: boolean; /** @@ -177,58 +177,96 @@ export interface BsConfig { * @default LogLevel.log */ logLevel?: LogLevel | 'error' | 'warn' | 'log' | 'info' | 'debug' | 'trace' | 'off'; + /** - * Overrides where source files appear to live, in both sourcemaps and the `SOURCE_FILE_PATH` / - * `SOURCE_LOCATION` runtime literals. Only applies to files within `rootDir`. - * - * When `relativeSourceMaps` is false (default): the `rootDir` portion of each source path is - * replaced with `sourceRoot` directly in `sources[]`. The map's `sourceRoot` field is not written. - * - * When `relativeSourceMaps` is true: the map's `sourceRoot` field is set to this value, and - * `sources[]` entries are relative to `sourceRoot` (per the sourcemap spec). - * - * In both modes, `SOURCE_FILE_PATH` and `SOURCE_LOCATION` reflect the `sourceRoot`-substituted path. + * @deprecated Moved to `compilerOptions.sourceRoot`. If both are present, `compilerOptions.sourceRoot` wins. */ sourceRoot?: string; /** - * Should the `sourceRoot` property be resolve to an absolute path (relative to the bsconfig it's defined in) - * @default false + * @deprecated Moved to `compilerOptions.resolveSourceRoot`. If both are present, `compilerOptions.resolveSourceRoot` wins. */ resolveSourceRoot?: boolean; /** - * Enables generating sourcemap files, which allow debugging tools to show the original source code while running the emitted files. - * @default true + * @deprecated Moved to `compilerOptions.sourceMap`. If both are present, `compilerOptions.sourceMap` wins. */ sourceMap?: boolean; /** - * If true, file paths in sourcemap `sources[]` will be written as relative paths instead of absolute. - * Only has an effect when `sourceMap` is true. - * - * When false (default): `sources[]` contains absolute paths. If `sourceRoot` is set, the `rootDir` - * portion is replaced with `sourceRoot` in-place; the map's `sourceRoot` field is never written. - * - * When true: `sources[]` entries are relative to the map file's directory, making sourcemaps - * portable across machines. If `sourceRoot` is also set, the map's `sourceRoot` field is written - * and `sources[]` entries are instead relative to `sourceRoot` (per the sourcemap spec — consumers - * reconstruct the full path as `path.resolve(sourceRoot, sources[0])`). - * @default false + * @deprecated Moved to `compilerOptions.relativeSourceMaps`. If both are present, `compilerOptions.relativeSourceMaps` wins. */ relativeSourceMaps?: boolean; /** - * Excludes empty files from being included in the output. Some Brighterscript files - * are left empty or with only comments after transpilation to Brightscript. - * The default behavior is to write these to disk after transpilation. - * Setting this flag to `true` will prevent empty files being written and will - * remove associated script tags from XML - * @default false + * @deprecated Moved to `compilerOptions.pruneEmptyCodeFiles`. If both are present, `compilerOptions.pruneEmptyCodeFiles` wins. */ pruneEmptyCodeFiles?: boolean; + /** + * @deprecated Moved to `compilerOptions.allowBrighterScriptInBrightScript`. If both are present, `compilerOptions.allowBrighterScriptInBrightScript` wins. + */ + allowBrighterScriptInBrightScript?: boolean; + + /** + * @deprecated Moved to `compilerOptions.bslibDestinationDir`. If both are present, `compilerOptions.bslibDestinationDir` wins. + */ + bslibDestinationDir?: string; + + /** + * @deprecated Moved to `compilerOptions.legacyCallfuncHandling`. If both are present, `compilerOptions.legacyCallfuncHandling` wins. + */ + legacyCallfuncHandling?: boolean; + + /** + * @deprecated Moved to `compilerOptions.minFirmwareVersion`. If both are present, `compilerOptions.minFirmwareVersion` wins. + */ + minFirmwareVersion?: string; + + /** + * When set to false, validation is skipped entirely. This can speed up builds when diagnostics + * are not needed (e.g. when using the VSCode extension which already surfaces diagnostics in the + * editor). Note that skipping validation may cause transpilation to fail or produce incorrect + * output if the project contains errors that would normally be caught during validation. + * @default true + */ + validate?: boolean; + + /** + * @deprecated Moved to `compilerOptions.strict`. If both are present, `compilerOptions.strict` wins. + */ + strict?: boolean; + + /** + * @deprecated Moved to `compilerOptions.strictCallFunc`. If both are present, `compilerOptions.strictCallFunc` wins. + */ + strictCallFunc?: boolean; + + /** + * @deprecated Moved to `compilerOptions.strictNodeMembers`. If both are present, `compilerOptions.strictNodeMembers` wins. + */ + strictNodeMembers?: boolean; + + /** + * Options that control how BrighterScript interprets, validates, and compiles your code + * (as opposed to top-level options, which control project structure, file discovery, and + * tooling/reporting behavior). Similar in spirit to TypeScript's `compilerOptions`. + * + * Several of these options used to live at the top level of `bsconfig.json`. Those top-level + * options still work, but are deprecated: if an option is set in both places, the value in + * `compilerOptions` wins, and a deprecation warning is emitted for the top-level usage. + */ + compilerOptions?: BsConfigCompilerOptions; +} + +export interface BsConfigCompilerOptions { /** * Allow brighterscript features (classes, interfaces, etc...) to be included in BrightScript (`.brs`) files, and force those files to be transpiled. * @default false */ allowBrighterScriptInBrightScript?: boolean; + /** + * When enabled, every xml component will search for a .bs or .brs file with the same name + * in the same folder, and add it as a script import if found. Disabled by default" + */ + autoImportComponentScript?: boolean; + /** * Override the destination directory for the bslib.brs file. Use this if * you want to customize where the bslib.brs file is located in the staging @@ -237,12 +275,27 @@ export interface BsConfig { */ bslibDestinationDir?: string; + /** + * Emit type definition files (`d.bs`) + * @default false + */ + emitDefinitions?: boolean; + + /** + * Emit full paths to files when printing diagnostics to the console. Defaults to false + */ + emitFullPaths?: boolean; + /* Legacy RokuOS versions required at least one argument in callfunc() invocations. * Previous brighterscript versions handled this by inserting invalid as an argument when no other args are present. * This is not necessary in modern RokuOS versions. */ legacyCallfuncHandling?: boolean; + manifest?: { + bs_const?: Record; + }; + /** * The minimum Roku firmware version required to run this project. * When set, BrightScript (.brs) files are always validated against the version restriction. @@ -255,13 +308,61 @@ export interface BsConfig { minFirmwareVersion?: string; /** - * When set to false, validation is skipped entirely. This can speed up builds when diagnostics - * are not needed (e.g. when using the VSCode extension which already surfaces diagnostics in the - * editor). Note that skipping validation may cause transpilation to fail or produce incorrect - * output if the project contains errors that would normally be caught during validation. + * Excludes empty files from being included in the output. Some Brighterscript files + * are left empty or with only comments after transpilation to Brightscript. + * The default behavior is to write these to disk after transpilation. + * Setting this flag to `true` will prevent empty files being written and will + * remove associated script tags from XML + * @default false + */ + pruneEmptyCodeFiles?: boolean; + + /** + * If true, file paths in sourcemap `sources[]` will be written as relative paths instead of absolute. + * Only has an effect when `sourceMap` is true. + * + * When false (default): `sources[]` contains absolute paths. If `sourceRoot` is set, the `rootDir` + * portion is replaced with `sourceRoot` in-place; the map's `sourceRoot` field is never written. + * + * When true: `sources[]` entries are relative to the map file's directory, making sourcemaps + * portable across machines. If `sourceRoot` is also set, the map's `sourceRoot` field is written + * and `sources[]` entries are instead relative to `sourceRoot` (per the sourcemap spec — consumers + * reconstruct the full path as `path.resolve(sourceRoot, sources[0])`). + * @default false + */ + relativeSourceMaps?: boolean; + + /** + * If true, removes the explicit type to function's parameters and return (i.e. the `as type` syntax); otherwise keep this information. + * @default false + */ + removeParameterTypes?: boolean; + + /** + * Should the `sourceRoot` property be resolve to an absolute path (relative to the bsconfig it's defined in) + * @default false + */ + resolveSourceRoot?: boolean; + + /** + * Enables generating sourcemap files, which allow debugging tools to show the original source code while running the emitted files. * @default true */ - validate?: boolean; + sourceMap?: boolean; + + /** + * Overrides where source files appear to live, in both sourcemaps and the `SOURCE_FILE_PATH` / + * `SOURCE_LOCATION` runtime literals. Only applies to files within `rootDir`. + * + * When `relativeSourceMaps` is false (default): the `rootDir` portion of each source path is + * replaced with `sourceRoot` directly in `sources[]`. The map's `sourceRoot` field is not written. + * + * When `relativeSourceMaps` is true: the map's `sourceRoot` field is set to this value, and + * `sources[]` entries are relative to `sourceRoot` (per the sourcemap spec). + * + * In both modes, `SOURCE_FILE_PATH` and `SOURCE_LOCATION` reflect the `sourceRoot`-substituted path. + */ + sourceRoot?: string; /** * When true, enables all strict mode options (strictCallFunc, strictNodeMembers, and any future options added that are considered "strict"). This is a convenient way to enable strict mode without having to set each option individually. @@ -282,6 +383,30 @@ export interface BsConfig { strictNodeMembers?: boolean; } +/** + * The list of `BsConfig` keys that were moved into `compilerOptions`. Kept in one place so the + * legacy-option shimming/deprecation logic in `util.ts` and the JSON schema stay in sync. + */ +export const deprecatedCompilerOptionKeys: ReadonlyArray = [ + 'allowBrighterScriptInBrightScript', + 'autoImportComponentScript', + 'bslibDestinationDir', + 'emitDefinitions', + 'emitFullPaths', + 'legacyCallfuncHandling', + 'manifest', + 'minFirmwareVersion', + 'pruneEmptyCodeFiles', + 'relativeSourceMaps', + 'removeParameterTypes', + 'resolveSourceRoot', + 'sourceMap', + 'sourceRoot', + 'strict', + 'strictCallFunc', + 'strictNodeMembers' +]; + /** * Discriminated union describing how diagnostics are rendered to the console. * - String shorthand: a preset name ('detailed' | 'github-actions') or a template string @@ -322,7 +447,8 @@ type OptionalBsConfigFields = | 'stagingDir' | 'stagingFolderPath' | 'minFirmwareVersion' - | 'diagnosticReporters'; + | 'diagnosticReporters' + | 'compilerOptions'; export type FinalizedBsConfig = Omit, OptionalBsConfigFields> diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index 3c45ba579..a96a61692 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -1165,6 +1165,12 @@ export let DiagnosticMessages = { legacyCode: 1154, severity: DiagnosticSeverity.Error, code: 'rsg-version-removed' + }), + deprecatedBsConfigOption: (optionName: string) => ({ + message: `'${optionName}' is deprecated at the top level of bsconfig.json. Move it into 'compilerOptions.${optionName}' instead`, + legacyCode: 1155, + severity: DiagnosticSeverity.Warning, + code: 'deprecated-bsconfig-option' }) }; export const defaultMaximumTruncationLength = 160; diff --git a/src/Program.ts b/src/Program.ts index 089fe4f1a..222af7036 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -9,7 +9,7 @@ import { Scope } from './Scope'; import type { NamespaceContainer, NamespaceFileContribution } from './Scope'; import { SymbolTable } from './SymbolTable'; import { DiagnosticMessages } from './DiagnosticMessages'; -import type { FileObj, SemanticToken, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, BeforeAddFileEvent, BeforeRemoveFileEvent, PrepareFileEvent, PrepareProgramEvent, ProvideFileEvent, SerializedFile, TranspileObj, SerializeFileEvent, ScopeValidationOptions, ExtraSymbolData, ProvideSelectionRangesEvent, ProvideInlayHintsEvent, OnGetSourceFixAllCodeActionsEvent } from './interfaces'; +import type { BsDiagnostic, FileObj, SemanticToken, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, BeforeAddFileEvent, BeforeRemoveFileEvent, PrepareFileEvent, PrepareProgramEvent, ProvideFileEvent, SerializedFile, TranspileObj, SerializeFileEvent, ScopeValidationOptions, ExtraSymbolData, ProvideSelectionRangesEvent, ProvideInlayHintsEvent, OnGetSourceFixAllCodeActionsEvent } from './interfaces'; import type { SourceFixAllCodeAction } from './CodeActionUtil'; import { codeActionUtil } from './CodeActionUtil'; import { standardizePath as s, util } from './util'; @@ -92,6 +92,13 @@ export class Program { this.plugins = plugins || new PluginInterface([], { logger: this.logger }); this.diagnostics = diagnosticsManager || new DiagnosticManager(); + //surface warnings for any deprecated bsconfig options that were used to build `this.options` + const deprecationDiagnostics = (this.options as any)._deprecationDiagnostics as BsDiagnostic[]; + if (deprecationDiagnostics?.length > 0) { + this.diagnostics.register(deprecationDiagnostics); + } + delete (this.options as any)._deprecationDiagnostics; + //try to find a location for the diagnostic if it doesn't have one this.diagnostics.locationResolver = (args) => { diff --git a/src/ProgramBuilder.spec.ts b/src/ProgramBuilder.spec.ts index 754f1af26..68b39882d 100644 --- a/src/ProgramBuilder.spec.ts +++ b/src/ProgramBuilder.spec.ts @@ -9,7 +9,7 @@ import { LogLevel, createLogger } from './logging'; import * as diagnosticUtils from './diagnosticUtils'; import { DiagnosticSeverity } from 'vscode-languageserver'; import { BrsFile } from './files/BrsFile'; -import { expectZeroDiagnostics } from './testHelpers.spec'; +import { expectZeroDiagnostics, expectDiagnosticsIncludes } from './testHelpers.spec'; import type { BsConfig } from './BsConfig'; import type { BscFile } from './files/BscFile'; import { tempDir, rootDir, outDir } from './testHelpers.spec'; @@ -348,6 +348,42 @@ describe('ProgramBuilder', () => { }); }); + describe('compilerOptions', () => { + it('warns when a deprecated top-level option is loaded from bsconfig.json', async () => { + fsExtra.outputFileSync(`${rootDir}/bsconfig.json`, JSON.stringify({ sourceMap: true })); + + await builder.run({ + project: `${rootDir}/bsconfig.json`, + rootDir: rootDir, + files: [] + }); + + expectDiagnosticsIncludes(builder.getDiagnostics(), ['deprecated-bsconfig-option']); + }); + + it('does not warn when the option is only set via compilerOptions', async () => { + fsExtra.outputFileSync(`${rootDir}/bsconfig.json`, JSON.stringify({ compilerOptions: { sourceMap: true } })); + + await builder.run({ + project: `${rootDir}/bsconfig.json`, + rootDir: rootDir, + files: [] + }); + + expectZeroDiagnostics(builder.getDiagnostics()); + }); + + it('does not warn when the deprecated option is passed programmatically', async () => { + await builder.run({ + rootDir: rootDir, + sourceMap: true, + files: [] + }); + + expectZeroDiagnostics(builder.getDiagnostics()); + }); + }); + it('uses a unique logger for each builder', async () => { let builder1 = new ProgramBuilder(); sinon.stub(builder1 as any, 'runOnce').returns(Promise.resolve()); diff --git a/src/ProgramBuilder.ts b/src/ProgramBuilder.ts index a6a270a22..798f67ef9 100644 --- a/src/ProgramBuilder.ts +++ b/src/ProgramBuilder.ts @@ -104,6 +104,13 @@ export class ProgramBuilder { this.logger.logLevel = this.options?.logLevel; } + //surface warnings for any deprecated bsconfig options that were used to build `this.options` + const deprecationDiagnostics = (this.options as any)._deprecationDiagnostics as BsDiagnostic[]; + if (deprecationDiagnostics?.length > 0) { + this.diagnostics.register(deprecationDiagnostics); + } + delete (this.options as any)._deprecationDiagnostics; + if (this.options.noProject) { this.logger.log(`'noProject' flag is set so bsconfig.json loading is disabled'`); } else if (this.options.project) { diff --git a/src/util.spec.ts b/src/util.spec.ts index 774292c08..db93691cd 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -159,6 +159,7 @@ describe('util', () => { '_ancestors': [ s`${rootDir}/child.json` ], + '_deprecationDiagnostics': [], 'cwd': s`${rootDir}/cwd`, 'rootDir': s`${rootDir}/rootDir` }); @@ -490,6 +491,105 @@ describe('util', () => { util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }).noEmit ).to.be.true; }); + + describe('compilerOptions', () => { + it('honors a deprecated top-level option from a loaded project file', () => { + fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, { + sourceMap: true + }); + const result = util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }); + expect(result.sourceMap).to.be.true; + expect(result.compilerOptions.sourceMap).to.be.true; + }); + + it('flags a deprecated top-level option from a loaded project file with a warning diagnostic', () => { + fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, { + sourceMap: true + }); + const result = util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }); + const diagnostics = (result as any)._deprecationDiagnostics as Array<{ code: string }>; + expect(diagnostics.map(d => d.code)).to.include('deprecated-bsconfig-option'); + }); + + it('does not flag options set only in compilerOptions', () => { + fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, { + compilerOptions: { + sourceMap: true + } + }); + const result = util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }); + expect(result.sourceMap).to.be.true; + const diagnostics = (result as any)._deprecationDiagnostics as Array<{ code: string }>; + expect(diagnostics).to.be.empty; + }); + + it('prefers compilerOptions over a deprecated top-level option in the same file', () => { + fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, { + strict: false, + compilerOptions: { + strict: true + } + }); + const result = util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }); + expect(result.strict).to.be.true; + expect(result.compilerOptions.strict).to.be.true; + }); + + it('does not flag options passed programmatically (not from a bsconfig.json file)', () => { + const result = util.normalizeAndResolveConfig({ + sourceMap: true + }); + const diagnostics = (result as any)._deprecationDiagnostics as Array<{ code: string }> | undefined; + expect(diagnostics ?? []).to.be.empty; + }); + + it('deep-merges compilerOptions across an extends chain', () => { + fsExtra.outputJsonSync(s`${rootDir}/parent.json`, { + compilerOptions: { + sourceMap: true, + strict: true + } + }); + fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, { + extends: 'parent.json', + compilerOptions: { + strictCallFunc: true + } + }); + const result = util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }); + expect(result.compilerOptions.sourceMap).to.be.true; + expect(result.compilerOptions.strict).to.be.true; + expect(result.compilerOptions.strictCallFunc).to.be.true; + }); + + it('lets a child compilerOptions value override the parent', () => { + fsExtra.outputJsonSync(s`${rootDir}/parent.json`, { + compilerOptions: { + strict: true + } + }); + fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, { + extends: 'parent.json', + compilerOptions: { + strict: false + } + }); + const result = util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }); + expect(result.compilerOptions.strict).to.be.false; + expect(result.strict).to.be.false; + }); + + it('resolves a compilerOptions.sourceRoot relative to the bsconfig file that declared it', () => { + fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, { + compilerOptions: { + sourceRoot: './src', + resolveSourceRoot: true + } + }); + const result = util.normalizeAndResolveConfig({ project: s`${rootDir}/bsconfig.json` }); + expect(result.sourceRoot).to.eql(s`${rootDir}/src`); + }); + }); }); describe('normalizeConfig', () => { diff --git a/src/util.ts b/src/util.ts index 2e7e48136..2a6e35e0d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -7,7 +7,8 @@ import { rokuDeploy, DefaultFiles } from 'roku-deploy'; import type { Diagnostic, Position, DiagnosticRelatedInformation } from 'vscode-languageserver'; import { Range, Location } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -import type { BsConfig, FinalizedBsConfig } from './BsConfig'; +import type { BsConfig, BsConfigCompilerOptions, FinalizedBsConfig } from './BsConfig'; +import { deprecatedCompilerOptionKeys } from './BsConfig'; import { DiagnosticMessages } from './DiagnosticMessages'; import type { CallableContainer, BsDiagnostic, FileReference, CallableContainerMap, Plugin, ExpressionInfo, TranspileResult, MaybePromise, DisposableLike, ExtraSymbolData, GetTypeOptions, TypeChainProcessResult, PluginFactory, TypeCircularReferenceInfo } from './interfaces'; import { TypeChainEntry } from './interfaces'; @@ -228,11 +229,21 @@ export class Util { util.resolvePathsRelativeTo(projectConfig, 'require', projectFileCwd); let result: BsConfig; + let baseProjectConfigDeprecationDiagnostics: BsDiagnostic[] | undefined; //if the project has a base file, load it if (projectConfig && typeof projectConfig.extends === 'string') { let baseProjectConfig = this.loadConfigFile(projectConfig.extends, [...parentProjectPaths, configFilePath], projectFileCwd); //extend the base config with the current project settings result = { ...baseProjectConfig, ...projectConfig }; + //`compilerOptions` is deep-merged (like TypeScript does), so a child config only + //overriding one option doesn't wipe out the rest of the options set by its parent(s) + if (baseProjectConfig?.compilerOptions || projectConfig.compilerOptions) { + result.compilerOptions = { + ...baseProjectConfig?.compilerOptions, + ...projectConfig.compilerOptions + }; + } + baseProjectConfigDeprecationDiagnostics = (baseProjectConfig as any)?._deprecationDiagnostics; } else { result = projectConfig; let ancestors = parentProjectPaths ? parentProjectPaths : []; @@ -259,8 +270,35 @@ export class Util { if (result.cwd) { result.cwd = path.resolve(projectFileCwd, result.cwd); } - if (result.sourceRoot && result.resolveSourceRoot) { - result.sourceRoot = path.resolve(projectFileCwd, result.sourceRoot); + + //flag any deprecated top-level `compilerOptions` options used directly in THIS config file + //(this is deliberately scoped to options loaded from an actual bsconfig.json file; options + //passed programmatically to `Program`/`ProgramBuilder` are not flagged) + const deprecationDiagnostics: BsDiagnostic[] = [...((baseProjectConfigDeprecationDiagnostics) ?? [])]; + for (const key of deprecatedCompilerOptionKeys) { + if (key in projectConfig) { + deprecationDiagnostics.push({ + ...DiagnosticMessages.deprecatedBsConfigOption(key), + location: { + uri: this.pathToUri(configFilePath), + range: this.createRange(0, 0, 0, 0) + } + }); + } + } + (result as any)._deprecationDiagnostics = deprecationDiagnostics; + + //`sourceRoot`/`resolveSourceRoot` may live at the top level (deprecated) or in `compilerOptions`. + //resolve whichever location actually holds the value, relative to THIS config file + const sourceRootValue = result.compilerOptions?.sourceRoot ?? result.sourceRoot; + const resolveSourceRootValue = result.compilerOptions?.resolveSourceRoot ?? result.resolveSourceRoot; + if (sourceRootValue && resolveSourceRootValue) { + const resolvedSourceRoot = path.resolve(projectFileCwd, sourceRootValue); + if (result.compilerOptions?.sourceRoot !== undefined) { + result.compilerOptions.sourceRoot = resolvedSourceRoot; + } else { + result.sourceRoot = resolvedSourceRoot; + } } return result; } @@ -331,6 +369,21 @@ export class Util { const cwd = config.cwd ?? process.cwd(); + //Resolve `compilerOptions`: values set there win over their deprecated top-level counterparts. + //(deprecation diagnostics for options loaded directly from a bsconfig.json file are collected + //separately, in `loadConfigFile`, and simply carried through on `config._deprecationDiagnostics`) + const compilerOptions: BsConfigCompilerOptions = { ...(config.compilerOptions ?? {}) }; + for (const key of deprecatedCompilerOptionKeys) { + if (!(key in compilerOptions) && key in config) { + (compilerOptions as any)[key] = (config as any)[key]; + } + //push the resolved (winning) value back onto the flat config so the rest of this function + //(and any code that still reads the deprecated flat option) sees the correct, resolved value + if (key in compilerOptions) { + (config as any)[key] = compilerOptions[key]; + } + } + let logLevel: LogLevel = LogLevel.log; if (typeof config.logLevel === 'string') { @@ -396,7 +449,8 @@ export class Util { validate: config.validate === false ? false : true, strict: strictValue, strictCallFunc: (typeof config.strictCallFunc === 'boolean' ? config.strictCallFunc : strictValue), - strictNodeMembers: (typeof config.strictNodeMembers === 'boolean' ? config.strictNodeMembers : strictValue) + strictNodeMembers: (typeof config.strictNodeMembers === 'boolean' ? config.strictNodeMembers : strictValue), + compilerOptions: compilerOptions }; //mutate `config` in case anyone is holding a reference to the incomplete one