diff --git a/README.md b/README.md index 1c1399f6c13..85ba79f0b12 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/heft-plugins/heft-dev-cert-plugin](./heft-plugins/heft-dev-cert-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-dev-cert-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-dev-cert-plugin) | [changelog](./heft-plugins/heft-dev-cert-plugin/CHANGELOG.md) | [@rushstack/heft-dev-cert-plugin](https://www.npmjs.com/package/@rushstack/heft-dev-cert-plugin) | | [/heft-plugins/heft-jest-plugin](./heft-plugins/heft-jest-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-jest-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-jest-plugin) | [changelog](./heft-plugins/heft-jest-plugin/CHANGELOG.md) | [@rushstack/heft-jest-plugin](https://www.npmjs.com/package/@rushstack/heft-jest-plugin) | | [/heft-plugins/heft-lint-plugin](./heft-plugins/heft-lint-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-lint-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-lint-plugin) | [changelog](./heft-plugins/heft-lint-plugin/CHANGELOG.md) | [@rushstack/heft-lint-plugin](https://www.npmjs.com/package/@rushstack/heft-lint-plugin) | +| [/heft-plugins/heft-localization-typings-plugin](./heft-plugins/heft-localization-typings-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-localization-typings-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-localization-typings-plugin) | [changelog](./heft-plugins/heft-localization-typings-plugin/CHANGELOG.md) | [@rushstack/heft-localization-typings-plugin](https://www.npmjs.com/package/@rushstack/heft-localization-typings-plugin) | | [/heft-plugins/heft-sass-plugin](./heft-plugins/heft-sass-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-sass-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-sass-plugin) | [changelog](./heft-plugins/heft-sass-plugin/CHANGELOG.md) | [@rushstack/heft-sass-plugin](https://www.npmjs.com/package/@rushstack/heft-sass-plugin) | | [/heft-plugins/heft-serverless-stack-plugin](./heft-plugins/heft-serverless-stack-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-serverless-stack-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-serverless-stack-plugin) | [changelog](./heft-plugins/heft-serverless-stack-plugin/CHANGELOG.md) | [@rushstack/heft-serverless-stack-plugin](https://www.npmjs.com/package/@rushstack/heft-serverless-stack-plugin) | | [/heft-plugins/heft-storybook-plugin](./heft-plugins/heft-storybook-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-storybook-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-storybook-plugin) | [changelog](./heft-plugins/heft-storybook-plugin/CHANGELOG.md) | [@rushstack/heft-storybook-plugin](https://www.npmjs.com/package/@rushstack/heft-storybook-plugin) | diff --git a/apps/api-extractor/src/analyzer/Span.ts b/apps/api-extractor/src/analyzer/Span.ts index 06afaf7b1a3..96841d800a0 100644 --- a/apps/api-extractor/src/analyzer/Span.ts +++ b/apps/api-extractor/src/analyzer/Span.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; -import { InternalError, Sort } from '@rushstack/node-core-library'; +import { InternalError, Sort, Text } from '@rushstack/node-core-library'; import { IndentedWriter } from '../generators/IndentedWriter'; @@ -637,12 +637,7 @@ export class Span { } private _getTrimmed(text: string): string { - const trimmed: string = text.replace(/\r?\n/g, '\\n'); - - if (trimmed.length > 100) { - return trimmed.substr(0, 97) + '...'; - } - return trimmed; + return Text.truncateWithEllipsis(Text.convertToLf(text), 100); } private _getSubstring(startIndex: number, endIndex: number): string { diff --git a/apps/heft/src/configuration/HeftConfiguration.ts b/apps/heft/src/configuration/HeftConfiguration.ts index 4e19d5d435b..7d88d982e54 100644 --- a/apps/heft/src/configuration/HeftConfiguration.ts +++ b/apps/heft/src/configuration/HeftConfiguration.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import * as path from 'path'; -import { type IPackageJson, PackageJsonLookup, InternalError } from '@rushstack/node-core-library'; +import { type IPackageJson, PackageJsonLookup, InternalError, Path } from '@rushstack/node-core-library'; import { Terminal, type ITerminalProvider, type ITerminal } from '@rushstack/terminal'; import { trueCasePathSync } from 'true-case-path'; import { type IRigConfig, RigConfig } from '@rushstack/rig-package'; @@ -30,8 +30,8 @@ export interface IHeftConfigurationInitializationOptions { */ export class HeftConfiguration { private _buildFolderPath!: string; + private _slashNormalizedBuildFolderPath: string | undefined; private _projectConfigFolderPath: string | undefined; - private _cacheFolderPath: string | undefined; private _tempFolderPath: string | undefined; private _rigConfig: IRigConfig | undefined; private _globalTerminal!: Terminal; @@ -45,6 +45,17 @@ export class HeftConfiguration { return this._buildFolderPath; } + /** + * {@link HeftConfiguration.buildFolderPath} with all path separators converted to forward slashes. + */ + public get slashNormalizedBuildFolderPath(): string { + if (!this._slashNormalizedBuildFolderPath) { + this._slashNormalizedBuildFolderPath = Path.convertToSlashes(this.buildFolderPath); + } + + return this._slashNormalizedBuildFolderPath; + } + /** * The path to the project's "config" folder. */ diff --git a/apps/heft/src/utilities/GitUtilities.ts b/apps/heft/src/utilities/GitUtilities.ts index dbc7fb0fef6..937beddd5db 100644 --- a/apps/heft/src/utilities/GitUtilities.ts +++ b/apps/heft/src/utilities/GitUtilities.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import type { ChildProcess, SpawnSyncReturns } from 'child_process'; import { default as getGitRepoInfo, type GitRepoInfo as IGitRepoInfo } from 'git-repo-info'; -import { Executable, FileSystem, InternalError, Path } from '@rushstack/node-core-library'; +import { Executable, FileSystem, InternalError, Path, Text } from '@rushstack/node-core-library'; import { default as ignore, type Ignore as IIgnoreMatcher } from 'ignore'; // Matches lines starting with "#" and whitepace lines @@ -282,7 +282,7 @@ export class GitUtilities { const foundIgnorePatterns: string[] = []; if (gitIgnoreContent) { - const gitIgnorePatterns: string[] = gitIgnoreContent.split(/\r?\n/g); + const gitIgnorePatterns: string[] = Text.splitByNewLines(gitIgnoreContent); for (const gitIgnorePattern of gitIgnorePatterns) { // Ignore whitespace-only lines and comments if (gitIgnorePattern.length === 0 || GITIGNORE_IGNORABLE_LINE_REGEX.test(gitIgnorePattern)) { diff --git a/build-tests/localization-plugin-test-02/build.js b/build-tests/localization-plugin-test-02/build.js deleted file mode 100644 index eef1c23c1a7..00000000000 --- a/build-tests/localization-plugin-test-02/build.js +++ /dev/null @@ -1,21 +0,0 @@ -const { FileSystem } = require('@rushstack/node-core-library'); -const child_process = require('child_process'); -const path = require('path'); -const process = require('process'); - -function executeCommand(command) { - console.log('---> ' + command); - child_process.execSync(command, { stdio: 'inherit' }); -} - -// Clean the old build outputs -console.log(`==> Starting build.js for ${path.basename(process.cwd())}`); -FileSystem.ensureEmptyFolder('dist-dev'); -FileSystem.ensureEmptyFolder('dist-prod'); -FileSystem.ensureEmptyFolder('lib'); -FileSystem.ensureEmptyFolder('temp'); - -// Run Webpack -executeCommand('node node_modules/webpack-cli/bin/cli'); - -console.log(`==> Finished build.js for ${path.basename(process.cwd())}`); diff --git a/build-tests/localization-plugin-test-02/config/heft.json b/build-tests/localization-plugin-test-02/config/heft.json new file mode 100644 index 00000000000..057ddf4fdde --- /dev/null +++ b/build-tests/localization-plugin-test-02/config/heft.json @@ -0,0 +1,42 @@ +/** + * Defines configuration used by core Heft. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + + // TODO: Add comments + "phasesByName": { + "build": { + "cleanFiles": [{ "includeGlobs": ["dist", "lib", "lib-commonjs"] }], + + "tasksByName": { + "loc-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-localization-typings-plugin", + "options": { + "generatedTsFolder": "temp/loc-json-ts" + } + } + }, + "typescript": { + "taskDependencies": ["loc-typings"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-typescript-plugin" + } + }, + "lint": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-lint-plugin" + } + }, + "webpack": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-webpack4-plugin" + } + } + } + } + } +} diff --git a/build-tests/localization-plugin-test-02/config/rush-project.json b/build-tests/localization-plugin-test-02/config/rush-project.json index 514e557d5eb..543278bebd4 100644 --- a/build-tests/localization-plugin-test-02/config/rush-project.json +++ b/build-tests/localization-plugin-test-02/config/rush-project.json @@ -4,7 +4,7 @@ "operationSettings": [ { "operationName": "_phase:build", - "outputFolderNames": ["lib", "dist"] + "outputFolderNames": ["lib", "dist-dev", "dist-prod"] } ] } diff --git a/build-tests/localization-plugin-test-02/config/typescript.json b/build-tests/localization-plugin-test-02/config/typescript.json new file mode 100644 index 00000000000..95ea4894e69 --- /dev/null +++ b/build-tests/localization-plugin-test-02/config/typescript.json @@ -0,0 +1,10 @@ +/** + * Configures the TypeScript plugin for Heft. This plugin also manages linting. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/typescript.schema.json", + + "staticAssetsToCopy": { + "fileExtensions": [".resx", ".json", ".resjson"] + } +} diff --git a/build-tests/localization-plugin-test-02/package.json b/build-tests/localization-plugin-test-02/package.json index ad191ec69f0..abeba748bf0 100644 --- a/build-tests/localization-plugin-test-02/package.json +++ b/build-tests/localization-plugin-test-02/package.json @@ -4,20 +4,24 @@ "version": "0.1.0", "private": true, "scripts": { - "build": "node build.js", - "serve": "node serve.js", - "_phase:build": "node build.js" + "build": "heft build --clean", + "start": "heft start", + "_phase:build": "heft run --only build -- --clean" }, "dependencies": { - "@rushstack/webpack4-localization-plugin": "workspace:*", - "@rushstack/webpack4-module-minifier-plugin": "workspace:*", + "@rushstack/heft-lint-plugin": "workspace:*", + "@rushstack/heft-localization-typings-plugin": "workspace:*", + "@rushstack/heft-typescript-plugin": "workspace:*", + "@rushstack/heft-webpack4-plugin": "workspace:*", + "@rushstack/heft": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/set-webpack-public-path-plugin": "^4.1.16", + "@rushstack/webpack4-localization-plugin": "workspace:*", + "@rushstack/webpack4-module-minifier-plugin": "workspace:*", "@types/lodash": "4.14.116", "@types/webpack-env": "1.18.0", "html-webpack-plugin": "~4.5.2", "lodash": "~4.17.15", - "ts-loader": "6.0.0", "typescript": "~5.4.2", "webpack-bundle-analyzer": "~4.5.0", "webpack-cli": "~3.3.2", diff --git a/build-tests/localization-plugin-test-02/serve.js b/build-tests/localization-plugin-test-02/serve.js deleted file mode 100644 index ffa5c333f00..00000000000 --- a/build-tests/localization-plugin-test-02/serve.js +++ /dev/null @@ -1,18 +0,0 @@ -const { FileSystem } = require('@rushstack/node-core-library'); -const child_process = require('child_process'); -const path = require('path'); -const process = require('process'); - -function executeCommand(command) { - console.log('---> ' + command); - child_process.execSync(command, { stdio: 'inherit' }); -} - -// Clean the old build outputs -console.log(`==> Starting build.js for ${path.basename(process.cwd())}`); -FileSystem.ensureEmptyFolder('dist'); -FileSystem.ensureEmptyFolder('lib'); -FileSystem.ensureEmptyFolder('temp'); - -// Run Webpack -executeCommand('node node_modules/webpack-dev-server/bin/webpack-dev-server'); diff --git a/build-tests/localization-plugin-test-02/src/indexA.ts b/build-tests/localization-plugin-test-02/src/indexA.ts index 56097124821..0f291375341 100644 --- a/build-tests/localization-plugin-test-02/src/indexA.ts +++ b/build-tests/localization-plugin-test-02/src/indexA.ts @@ -1,5 +1,5 @@ import { string1 } from './strings1.loc.json'; -import * as strings3 from './strings3.loc.json'; +import * as strings3 from './strings3.resjson'; import * as strings5 from './strings5.resx'; console.log(string1); diff --git a/build-tests/localization-plugin-test-02/src/indexB.ts b/build-tests/localization-plugin-test-02/src/indexB.ts index ee1d8396f4a..3bb70cfd44d 100644 --- a/build-tests/localization-plugin-test-02/src/indexB.ts +++ b/build-tests/localization-plugin-test-02/src/indexB.ts @@ -1,4 +1,4 @@ -import { string1, string2 } from './strings3.loc.json'; +import { string1, string2 } from './strings3.resjson'; const strings4: string = require('./strings4.loc.json'); console.log(string1); diff --git a/build-tests/localization-plugin-test-02/src/strings3.loc.json b/build-tests/localization-plugin-test-02/src/strings3.loc.json deleted file mode 100644 index c85ab94c731..00000000000 --- a/build-tests/localization-plugin-test-02/src/strings3.loc.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "string1": { - "value": "string three with a \\ backslash", - "comment": "the third string" - }, - "string2": { - "value": "string four with an ' apostrophe", - "comment": "the fourth string" - }, - "string3": { - "value": "UNUSED STRING", - "comment": "UNUSED STRING" - } -} diff --git a/build-tests/localization-plugin-test-02/src/strings3.resjson b/build-tests/localization-plugin-test-02/src/strings3.resjson new file mode 100644 index 00000000000..b6a221cc406 --- /dev/null +++ b/build-tests/localization-plugin-test-02/src/strings3.resjson @@ -0,0 +1,10 @@ +{ + "string1": "string three with a \\ backslash", + "_string1.comment": "the third string", + + "string2": "string four with an ' apostrophe", + "_string2.comment": "the fourth string", + + "string3": "UNUSED STRING", + "_string3.comment": "UNUSED STRING" +} diff --git a/build-tests/localization-plugin-test-02/webpack.config.js b/build-tests/localization-plugin-test-02/webpack.config.js index 421cb97712e..8e5e3800d18 100644 --- a/build-tests/localization-plugin-test-02/webpack.config.js +++ b/build-tests/localization-plugin-test-02/webpack.config.js @@ -12,30 +12,13 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); function generateConfiguration(mode, outputFolderName) { return { mode: mode, - module: { - rules: [ - { - test: /\.tsx?$/, - loader: require.resolve('ts-loader'), - exclude: /(node_modules)/, - options: { - compiler: require.resolve('typescript'), - logLevel: 'ERROR', - configFile: path.resolve(__dirname, 'tsconfig.json') - } - } - ] - }, - resolve: { - extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'] - }, entry: { - 'localization-test-A': path.join(__dirname, 'src', 'indexA.ts'), - 'localization-test-B': path.join(__dirname, 'src', 'indexB.ts'), - 'localization-test-C': path.join(__dirname, 'src', 'indexC.ts') + 'localization-test-A': `${__dirname}/lib/indexA.js`, + 'localization-test-B': `${__dirname}/lib/indexB.js`, + 'localization-test-C': `${__dirname}/lib/indexC.js` }, output: { - path: path.join(__dirname, outputFolderName), + path: `${__dirname}/${outputFolderName}`, filename: '[name]_[locale]_[contenthash].js', chunkFilename: '[id].[name]_[locale]_[contenthash].js' }, @@ -84,22 +67,17 @@ function generateConfiguration(mode, outputFolderName) { normalizeResxNewlines: 'crlf', ignoreMissingResxComments: true }, - typingsOptions: { - generatedTsFolder: path.resolve(__dirname, 'temp', 'loc-json-ts'), - sourceRoot: path.resolve(__dirname, 'src'), - processComment: (comment) => (comment ? `${comment} (processed)` : comment) - }, localizationStats: { - dropPath: path.resolve(__dirname, 'temp', 'localization-stats.json') + dropPath: `${__dirname}/temp/localization-stats.json` }, ignoreString: (filePath, stringName) => stringName === '__IGNORED_STRING__' }), new BundleAnalyzerPlugin({ openAnalyzer: false, analyzerMode: 'static', - reportFilename: path.resolve(__dirname, 'temp', 'stats.html'), + reportFilename: `${__dirname}/temp/stats.html`, generateStatsFile: true, - statsFilename: path.resolve(__dirname, 'temp', 'stats.json'), + statsFilename: `${__dirname}/temp/stats.json`, logLevel: 'error' }), new SetPublicPathPlugin({ diff --git a/common/changes/@microsoft/api-extractor/loc-typings-heft-plugin_2024-08-21-00-29.json b/common/changes/@microsoft/api-extractor/loc-typings-heft-plugin_2024-08-21-00-29.json new file mode 100644 index 00000000000..752ad7dd46f --- /dev/null +++ b/common/changes/@microsoft/api-extractor/loc-typings-heft-plugin_2024-08-21-00-29.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/api-extractor" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/loc-typings-heft-plugin_2024-08-20-22-59.json b/common/changes/@microsoft/rush/loc-typings-heft-plugin_2024-08-20-22-59.json new file mode 100644 index 00000000000..bd7ff97cb34 --- /dev/null +++ b/common/changes/@microsoft/rush/loc-typings-heft-plugin_2024-08-20-22-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-config-file/loc-typings-heft-plugin_2024-08-20-22-59.json b/common/changes/@rushstack/heft-config-file/loc-typings-heft-plugin_2024-08-20-22-59.json new file mode 100644 index 00000000000..9b24a4fdcca --- /dev/null +++ b/common/changes/@rushstack/heft-config-file/loc-typings-heft-plugin_2024-08-20-22-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-config-file", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-config-file" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-localization-typings-plugin/loc-typings-heft-plugin-impl_2024-08-17-00-18.json b/common/changes/@rushstack/heft-localization-typings-plugin/loc-typings-heft-plugin-impl_2024-08-17-00-18.json new file mode 100644 index 00000000000..d86e5f8a2ee --- /dev/null +++ b/common/changes/@rushstack/heft-localization-typings-plugin/loc-typings-heft-plugin-impl_2024-08-17-00-18.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-localization-typings-plugin", + "comment": "Initial release.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-localization-typings-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-sass-plugin/loc-typings-heft-plugin_2024-08-21-00-55.json b/common/changes/@rushstack/heft-sass-plugin/loc-typings-heft-plugin_2024-08-21-00-55.json new file mode 100644 index 00000000000..501a07dfec7 --- /dev/null +++ b/common/changes/@rushstack/heft-sass-plugin/loc-typings-heft-plugin_2024-08-21-00-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-sass-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-sass-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft/loc-typings-heft-plugin_2024-08-21-00-55.json b/common/changes/@rushstack/heft/loc-typings-heft-plugin_2024-08-21-00-55.json new file mode 100644 index 00000000000..f91498d47d9 --- /dev/null +++ b/common/changes/@rushstack/heft/loc-typings-heft-plugin_2024-08-21-00-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft", + "comment": "Add a `slashNormalizedBuildFolderPath` property to `HeftConfiguration`.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft" +} \ No newline at end of file diff --git a/common/changes/@rushstack/localization-utilities/loc-typings-heft-plugin_2024-08-16-22-17.json b/common/changes/@rushstack/localization-utilities/loc-typings-heft-plugin_2024-08-16-22-17.json new file mode 100644 index 00000000000..37ac0d9a441 --- /dev/null +++ b/common/changes/@rushstack/localization-utilities/loc-typings-heft-plugin_2024-08-16-22-17.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/localization-utilities", + "comment": "Expand the typings generator to take a richer set of options for default exports. See `exportAsDefault` in @rushstack/typings-generator's `StringValuesTypingsGenerator`. Also included is another property in `exportAsDefault`: `inferInterfaceNameFromFilename`.", + "type": "minor" + } + ], + "packageName": "@rushstack/localization-utilities" +} \ No newline at end of file diff --git a/common/changes/@rushstack/module-minifier/loc-typings-heft-plugin_2024-08-21-04-37.json b/common/changes/@rushstack/module-minifier/loc-typings-heft-plugin_2024-08-21-04-37.json new file mode 100644 index 00000000000..f0a60db970f --- /dev/null +++ b/common/changes/@rushstack/module-minifier/loc-typings-heft-plugin_2024-08-21-04-37.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/module-minifier", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/module-minifier" +} \ No newline at end of file diff --git a/common/changes/@rushstack/node-core-library/loc-typings-heft-plugin_2024-08-21-00-22.json b/common/changes/@rushstack/node-core-library/loc-typings-heft-plugin_2024-08-21-00-22.json new file mode 100644 index 00000000000..6fd972d7df6 --- /dev/null +++ b/common/changes/@rushstack/node-core-library/loc-typings-heft-plugin_2024-08-21-00-22.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Introduce a `Text.splitByNewLines` function.", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library" +} \ No newline at end of file diff --git a/common/changes/@rushstack/terminal/loc-typings-heft-plugin_2024-08-20-22-59.json b/common/changes/@rushstack/terminal/loc-typings-heft-plugin_2024-08-20-22-59.json new file mode 100644 index 00000000000..b9436a29897 --- /dev/null +++ b/common/changes/@rushstack/terminal/loc-typings-heft-plugin_2024-08-20-22-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/terminal", + "comment": "Create a new instance function called `getVerboseOutput` on `StringBufferTerminalProvider` and mark `getVerbose` as deprecated.", + "type": "minor" + } + ], + "packageName": "@rushstack/terminal" +} \ No newline at end of file diff --git a/common/changes/@rushstack/typings-generator/loc-typings-heft-plugin_2024-08-16-22-17.json b/common/changes/@rushstack/typings-generator/loc-typings-heft-plugin_2024-08-16-22-17.json new file mode 100644 index 00000000000..d22adbd0279 --- /dev/null +++ b/common/changes/@rushstack/typings-generator/loc-typings-heft-plugin_2024-08-16-22-17.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/typings-generator", + "comment": "Expand the `exportAsDefault` option for `StringValuesTypingsGenerator` to take an object with the following properties: `interfaceName` and `documentationComment`. Note that the `exportAsDefaultInterfaceName` option has been deprecated.", + "type": "minor" + } + ], + "packageName": "@rushstack/typings-generator" +} \ No newline at end of file diff --git a/common/changes/@rushstack/typings-generator/loc-typings-heft-plugin_2024-08-16-23-44.json b/common/changes/@rushstack/typings-generator/loc-typings-heft-plugin_2024-08-16-23-44.json new file mode 100644 index 00000000000..79f8882deda --- /dev/null +++ b/common/changes/@rushstack/typings-generator/loc-typings-heft-plugin_2024-08-16-23-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/typings-generator", + "comment": "Add an optional `exportAsDefault` property to the return value of `parseAndGenerateTypings` that overrides options provided by the same property in the `StringValuesTypingsGenerator`'s options object.", + "type": "minor" + } + ], + "packageName": "@rushstack/typings-generator" +} \ No newline at end of file diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 289f3740acb..47146fb6bde 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -162,6 +162,10 @@ "name": "@rushstack/heft-lint-plugin", "allowedCategories": [ "libraries", "tests" ] }, + { + "name": "@rushstack/heft-localization-typings-plugin", + "allowedCategories": [ "tests" ] + }, { "name": "@rushstack/heft-node-rig", "allowedCategories": [ "libraries", "tests", "vscode-extensions" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 1643e1c3d73..0c0dccf9ef1 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -2070,6 +2070,21 @@ importers: ../../../build-tests/localization-plugin-test-02: dependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/heft-lint-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-localization-typings-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-localization-typings-plugin + '@rushstack/heft-typescript-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-typescript-plugin + '@rushstack/heft-webpack4-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-webpack4-plugin '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library @@ -2094,9 +2109,6 @@ importers: lodash: specifier: ~4.17.15 version: 4.17.21 - ts-loader: - specifier: 6.0.0 - version: 6.0.0(typescript@5.4.2) typescript: specifier: ~5.4.2 version: 5.4.2 @@ -2780,6 +2792,25 @@ importers: specifier: ~5.4.2 version: 5.4.2 + ../../../heft-plugins/heft-localization-typings-plugin: + dependencies: + '@rushstack/localization-utilities': + specifier: workspace:* + version: link:../../libraries/localization-utilities + devDependencies: + '@microsoft/api-extractor': + specifier: workspace:* + version: link:../../apps/api-extractor + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + eslint: + specifier: ~8.57.0 + version: 8.57.0(supports-color@8.1.1) + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../heft-plugins/heft-sass-plugin: dependencies: '@rushstack/heft-config-file': diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index 19caa0d097f..ec3382f7b97 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -53,6 +53,7 @@ export class HeftConfiguration { get projectPackageJson(): IPackageJson; get rigConfig(): IRigConfig; get rigPackageResolver(): IRigPackageResolver; + get slashNormalizedBuildFolderPath(): string; get tempFolderPath(): string; get terminalProvider(): ITerminalProvider; } diff --git a/common/reviews/api/localization-utilities.api.md b/common/reviews/api/localization-utilities.api.md index 02713d93b56..4fd931a25dd 100644 --- a/common/reviews/api/localization-utilities.api.md +++ b/common/reviews/api/localization-utilities.api.md @@ -4,6 +4,7 @@ ```ts +import { IExportAsDefaultOptions } from '@rushstack/typings-generator'; import type { ITerminal } from '@rushstack/terminal'; import { ITypingsGeneratorBaseOptions } from '@rushstack/typings-generator'; import { NewlineKind } from '@rushstack/node-core-library'; @@ -15,6 +16,11 @@ export function getPseudolocalizer(options: IPseudolocaleOptions): (str: string) // @public (undocumented) export type IgnoreStringFunction = (filePath: string, stringName: string) => boolean; +// @public (undocumented) +export interface IInferInterfaceNameExportAsDefaultOptions extends Omit { + inferInterfaceNameFromFilename?: boolean; +} + // @public (undocumented) export interface ILocalizationFile { // (undocumented) @@ -79,13 +85,13 @@ export interface IPseudolocaleOptions { // @public (undocumented) export interface ITypingsGeneratorOptions extends ITypingsGeneratorBaseOptions { // (undocumented) - exportAsDefault?: boolean; + exportAsDefault?: boolean | IExportAsDefaultOptions | IInferInterfaceNameExportAsDefaultOptions; // (undocumented) ignoreMissingResxComments?: boolean | undefined; // (undocumented) ignoreString?: IgnoreStringFunction; // (undocumented) - processComment?: (comment: string | undefined, resxFilePath: string, stringName: string) => string | undefined; + processComment?: (comment: string | undefined, relativeFilePath: string, stringName: string) => string | undefined; // (undocumented) resxNewlineNormalization?: NewlineKind | undefined; } diff --git a/common/reviews/api/module-minifier.api.md b/common/reviews/api/module-minifier.api.md index 11dd7862ee3..a412088ee68 100644 --- a/common/reviews/api/module-minifier.api.md +++ b/common/reviews/api/module-minifier.api.md @@ -6,9 +6,9 @@ /// -import type { MessagePort as MessagePort_2 } from 'worker_threads'; import { MinifyOptions } from 'terser'; import type { RawSourceMap } from 'source-map'; +import type * as WorkerThreads from 'worker_threads'; // @public export function getIdentifier(ordinal: number): string; @@ -92,13 +92,13 @@ export class LocalMinifier implements IModuleMinifier { // @public export class MessagePortMinifier implements IModuleMinifier { - constructor(port: MessagePort_2); + constructor(port: WorkerThreads.MessagePort); // @deprecated (undocumented) connect(): Promise; connectAsync(): Promise; minify(request: IModuleMinificationRequest, callback: IModuleMinificationCallback): void; // (undocumented) - readonly port: MessagePort_2; + readonly port: WorkerThreads.MessagePort; } export { MinifyOptions } diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 359426fe265..4c7786d3668 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -864,6 +864,11 @@ export class Text { static readLinesFromIterableAsync(iterable: AsyncIterable, options?: IReadLinesFromIterableOptions): AsyncGenerator; static replaceAll(input: string, searchValue: string, replaceValue: string): string; static reverse(s: string): string; + static splitByNewLines(s: undefined): undefined; + // (undocumented) + static splitByNewLines(s: string): string[]; + // (undocumented) + static splitByNewLines(s: string | undefined): string[] | undefined; static truncateWithEllipsis(s: string, maximumLength: number): string; } diff --git a/common/reviews/api/terminal.api.md b/common/reviews/api/terminal.api.md index 33b0c77fbd7..17da9dd1390 100644 --- a/common/reviews/api/terminal.api.md +++ b/common/reviews/api/terminal.api.md @@ -335,7 +335,9 @@ export class StringBufferTerminalProvider implements ITerminalProvider { getDebugOutput(options?: IStringBufferOutputOptions): string; getErrorOutput(options?: IStringBufferOutputOptions): string; getOutput(options?: IStringBufferOutputOptions): string; + // @deprecated (undocumented) getVerbose(options?: IStringBufferOutputOptions): string; + getVerboseOutput(options?: IStringBufferOutputOptions): string; getWarningOutput(options?: IStringBufferOutputOptions): string; get supportsColor(): boolean; write(data: string, severity: TerminalProviderSeverity): void; diff --git a/common/reviews/api/typings-generator.api.md b/common/reviews/api/typings-generator.api.md index 0b52f2023d5..44bd4d5c7c4 100644 --- a/common/reviews/api/typings-generator.api.md +++ b/common/reviews/api/typings-generator.api.md @@ -6,9 +6,16 @@ import { ITerminal } from '@rushstack/terminal'; +// @public (undocumented) +export interface IExportAsDefaultOptions { + documentationComment?: string; + interfaceName?: string; +} + // @public (undocumented) export interface IStringValuesTypingsGeneratorBaseOptions { - exportAsDefault?: boolean; + exportAsDefault?: boolean | IExportAsDefaultOptions; + // @deprecated (undocumented) exportAsDefaultInterfaceName?: string; } @@ -30,6 +37,7 @@ export interface IStringValueTyping { // @public (undocumented) export interface IStringValueTypings { + exportAsDefault?: boolean | IExportAsDefaultOptions; // (undocumented) typings: IStringValueTyping[]; } @@ -64,8 +72,6 @@ export interface ITypingsGeneratorOptionsWithCustomReadFile extends ITypingsGeneratorBaseOptions { // (undocumented) fileExtensions: string[]; - // @deprecated (undocumented) - filesToIgnore?: string[]; // (undocumented) getAdditionalOutputFiles?: (relativePath: string) => string[]; // (undocumented) @@ -91,11 +97,13 @@ export class TypingsGenerator { readonly ignoredFileGlobs: readonly string[]; readonly inputFileGlob: string; // (undocumented) - protected _options: ITypingsGeneratorOptionsWithCustomReadFile; + protected readonly _options: ITypingsGeneratorOptionsWithCustomReadFile; registerDependency(consumer: string, rawDependency: string): void; // (undocumented) runWatcherAsync(): Promise; readonly sourceFolderPath: string; + // (undocumented) + protected readonly terminal: ITerminal; } ``` diff --git a/heft-plugins/heft-localization-typings-plugin/.eslintrc.js b/heft-plugins/heft-localization-typings-plugin/.eslintrc.js new file mode 100644 index 00000000000..27dc0bdff95 --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/.eslintrc.js @@ -0,0 +1,12 @@ +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('local-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution'); +// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 +require('local-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names'); + +module.exports = { + extends: [ + 'local-node-rig/profiles/default/includes/eslint/profile/node-trusted-tool', + 'local-node-rig/profiles/default/includes/eslint/mixins/friendly-locals' + ], + parserOptions: { tsconfigRootDir: __dirname } +}; diff --git a/heft-plugins/heft-localization-typings-plugin/.npmignore b/heft-plugins/heft-localization-typings-plugin/.npmignore new file mode 100644 index 00000000000..e15a94aeb84 --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/.npmignore @@ -0,0 +1,33 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README.md +# LICENSE + +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- + diff --git a/heft-plugins/heft-localization-typings-plugin/config/rig.json b/heft-plugins/heft-localization-typings-plugin/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "local-node-rig" +} diff --git a/heft-plugins/heft-localization-typings-plugin/heft-plugin.json b/heft-plugins/heft-localization-typings-plugin/heft-plugin.json new file mode 100644 index 00000000000..e523631a154 --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/heft-plugin.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/heft-plugin.schema.json", + + "taskPlugins": [ + { + "pluginName": "localization-typings-plugin", + "entryPoint": "./lib/LocalizationTypingsPlugin", + "optionsSchema": "./lib/schemas/options.schema.json" + } + ] +} diff --git a/heft-plugins/heft-localization-typings-plugin/package.json b/heft-plugins/heft-localization-typings-plugin/package.json new file mode 100644 index 00000000000..106cdb4406a --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/package.json @@ -0,0 +1,30 @@ +{ + "name": "@rushstack/heft-localization-typings-plugin", + "version": "0.0.0", + "description": "Heft plugin for generating types for localization files.", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rushstack.git", + "directory": "heft-plugins/heft-localization-typings-plugin" + }, + "homepage": "https://rushstack.io/pages/heft/overview/", + "license": "MIT", + "scripts": { + "build": "heft build --clean", + "start": "heft test --clean --watch", + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" + }, + "peerDependencies": { + "@rushstack/heft": "^0.66.26" + }, + "devDependencies": { + "@microsoft/api-extractor": "workspace:*", + "@rushstack/heft": "workspace:*", + "local-node-rig": "workspace:*", + "eslint": "~8.57.0" + }, + "dependencies": { + "@rushstack/localization-utilities": "workspace:*" + } +} diff --git a/heft-plugins/heft-localization-typings-plugin/src/LocalizationTypingsPlugin.ts b/heft-plugins/heft-localization-typings-plugin/src/LocalizationTypingsPlugin.ts new file mode 100644 index 00000000000..923b2d24c1e --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/src/LocalizationTypingsPlugin.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { + HeftConfiguration, + IHeftTaskPlugin, + IHeftTaskRunIncrementalHookOptions, + IHeftTaskSession, + IScopedLogger, + IWatchedFileState +} from '@rushstack/heft'; +import { type ITypingsGeneratorOptions, TypingsGenerator } from '@rushstack/localization-utilities'; + +export interface ILocalizationTypingsPluginOptions { + /** + * Source code root directory. + * Defaults to "src/". + */ + srcFolder?: string; + + /** + * Output directory for generated typings. + * Defaults to "temp/loc-ts/". + */ + generatedTsFolder?: string; + + /** + * Additional folders, relative to the project root, where the generated typings should be emitted to. + */ + secondaryGeneratedTsFolders?: string[]; + + exportAsDefault?: ITypingsGeneratorOptions['exportAsDefault']; + + /** + * An array of string names to ignore when generating typings. + */ + stringNamesToIgnore?: string[]; +} + +const PLUGIN_NAME: 'localization-typings-plugin' = 'localization-typings-plugin'; + +export default class LocalizationTypingsPlugin implements IHeftTaskPlugin { + public apply( + taskSession: IHeftTaskSession, + { slashNormalizedBuildFolderPath }: HeftConfiguration, + options?: ILocalizationTypingsPluginOptions + ): void { + const { + srcFolder, + generatedTsFolder, + stringNamesToIgnore, + secondaryGeneratedTsFolders: secondaryGeneratedTsFoldersFromOptions + } = options ?? {}; + + let secondaryGeneratedTsFolders: string[] | undefined; + if (secondaryGeneratedTsFoldersFromOptions) { + secondaryGeneratedTsFolders = []; + for (const secondaryGeneratedTsFolder of secondaryGeneratedTsFoldersFromOptions) { + secondaryGeneratedTsFolders.push(`${slashNormalizedBuildFolderPath}/${secondaryGeneratedTsFolder}`); + } + } + + const logger: IScopedLogger = taskSession.logger; + const stringNamesToIgnoreSet: Set | undefined = stringNamesToIgnore + ? new Set(stringNamesToIgnore) + : undefined; + + const typingsGenerator: TypingsGenerator = new TypingsGenerator({ + ...options, + srcFolder: `${slashNormalizedBuildFolderPath}/${srcFolder ?? 'src'}`, + generatedTsFolder: `${slashNormalizedBuildFolderPath}/${generatedTsFolder ?? 'temp/loc-ts'}`, + terminal: logger.terminal, + ignoreString: stringNamesToIgnoreSet + ? (stringName: string) => stringNamesToIgnoreSet.has(stringName) + : undefined, + secondaryGeneratedTsFolders + }); + + taskSession.hooks.run.tapPromise(PLUGIN_NAME, async () => { + await this._runLocalizationTypingsGeneratorAsync(typingsGenerator, logger, undefined); + }); + + taskSession.hooks.runIncremental.tapPromise( + PLUGIN_NAME, + async (runIncrementalOptions: IHeftTaskRunIncrementalHookOptions) => { + await this._runLocalizationTypingsGeneratorAsync(typingsGenerator, logger, runIncrementalOptions); + } + ); + } + + private async _runLocalizationTypingsGeneratorAsync( + typingsGenerator: TypingsGenerator, + { terminal }: IScopedLogger, + runIncrementalOptions: IHeftTaskRunIncrementalHookOptions | undefined + ): Promise { + // If we have the incremental options, use them to determine which files to process. + // Otherwise, process all files. The typings generator also provides the file paths + // as relative paths from the sourceFolderPath. + let changedRelativeFilePaths: string[] | undefined; + if (runIncrementalOptions) { + changedRelativeFilePaths = []; + const relativeFilePaths: Map = await runIncrementalOptions.watchGlobAsync( + typingsGenerator.inputFileGlob, + { + cwd: typingsGenerator.sourceFolderPath, + ignore: Array.from(typingsGenerator.ignoredFileGlobs), + absolute: false + } + ); + for (const [relativeFilePath, { changed }] of relativeFilePaths) { + if (changed) { + changedRelativeFilePaths.push(relativeFilePath); + } + } + if (changedRelativeFilePaths.length === 0) { + return; + } + } + + terminal.writeLine('Generating localization typings...'); + await typingsGenerator.generateTypingsAsync(changedRelativeFilePaths); + } +} diff --git a/heft-plugins/heft-localization-typings-plugin/src/schemas/options.schema.json b/heft-plugins/heft-localization-typings-plugin/src/schemas/options.schema.json new file mode 100644 index 00000000000..efae58f0e9b --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/src/schemas/options.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "type": "object", + "additionalProperties": false, + "properties": { + "exportAsDefaultOptions": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "documentationComment": { + "type": "string", + "description": "This value is placed in a documentation comment for the exported default interface." + }, + "interfaceName": { + "type": "string", + "description": "The interface name for the default wrapped export. Defaults to \"IExport\"" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "documentationComment": { + "type": "string", + "description": "This value is placed in a documentation comment for the exported default interface." + }, + "inferInterfaceNameFromFilename": { + "type": "boolean", + "description": "When set to true, the default export interface name will be inferred from the filename. This takes precedence over the interfaceName option." + } + } + } + ] + }, + + "srcFolder": { + "type": "string", + "description": "Source code root directory. Defaults to \"src/\"." + }, + + "generatedTsFolder": { + "type": "string", + "description": "Output directory for generated typings. Defaults to \"temp/loc-ts/\"." + }, + + "secondaryGeneratedTsFolders": { + "type": "array", + "description": "Additional folders, relative to the project root, where the generated typings should be emitted to.", + "items": { + "type": "string" + } + }, + + "stringNamesToIgnore": { + "type": "array", + "description": "An array of string names to ignore when generating typings.", + "items": { + "type": "string" + } + } + } +} diff --git a/heft-plugins/heft-localization-typings-plugin/tsconfig.json b/heft-plugins/heft-localization-typings-plugin/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/heft-plugins/heft-localization-typings-plugin/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/heft-plugins/heft-sass-plugin/src/SassPlugin.ts b/heft-plugins/heft-sass-plugin/src/SassPlugin.ts index 22c7c3d023d..b819d276a1f 100644 --- a/heft-plugins/heft-sass-plugin/src/SassPlugin.ts +++ b/heft-plugins/heft-sass-plugin/src/SassPlugin.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { Path } from '@rushstack/node-core-library'; import type { HeftConfiguration, IHeftTaskSession, @@ -30,25 +29,14 @@ export default class SassPlugin implements IHeftPlugin { * Generate typings for Sass files before TypeScript compilation. */ public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void { - const slashNormalizedBuildFolderPath: string = Path.convertToSlashes(heftConfiguration.buildFolderPath); - taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions: IHeftTaskRunHookOptions) => { - await this._runSassTypingsGeneratorAsync( - taskSession, - heftConfiguration, - slashNormalizedBuildFolderPath - ); + await this._runSassTypingsGeneratorAsync(taskSession, heftConfiguration); }); taskSession.hooks.runIncremental.tapPromise( PLUGIN_NAME, async (runIncrementalOptions: IHeftTaskRunIncrementalHookOptions) => { - await this._runSassTypingsGeneratorAsync( - taskSession, - heftConfiguration, - slashNormalizedBuildFolderPath, - runIncrementalOptions - ); + await this._runSassTypingsGeneratorAsync(taskSession, heftConfiguration, runIncrementalOptions); } ); } @@ -56,13 +44,11 @@ export default class SassPlugin implements IHeftPlugin { private async _runSassTypingsGeneratorAsync( taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration, - slashNormalizedBuildFolderPath: string, runIncrementalOptions?: IHeftTaskRunIncrementalHookOptions ): Promise { taskSession.logger.terminal.writeVerboseLine('Starting sass typings generation...'); const sassProcessor: SassProcessor = await this._loadSassProcessorAsync( heftConfiguration, - slashNormalizedBuildFolderPath, taskSession.logger ); // If we have the incremental options, use them to determine which files to process. @@ -98,26 +84,23 @@ export default class SassPlugin implements IHeftPlugin { private async _loadSassProcessorAsync( heftConfiguration: HeftConfiguration, - slashNormalizedBuildFolderPath: string, logger: IScopedLogger ): Promise { if (!this._sassProcessor) { const sassConfiguration: ISassConfiguration = await this._loadSassConfigurationAsync( heftConfiguration, - slashNormalizedBuildFolderPath, logger ); this._sassProcessor = new SassProcessor({ sassConfiguration, - buildFolder: slashNormalizedBuildFolderPath + buildFolder: heftConfiguration.slashNormalizedBuildFolderPath }); } return this._sassProcessor; } private async _loadSassConfigurationAsync( - heftConfiguration: HeftConfiguration, - slashNormalizedBuildFolderPath: string, + { rigConfig, slashNormalizedBuildFolderPath }: HeftConfiguration, logger: IScopedLogger ): Promise { if (!this._sassConfiguration) { @@ -132,7 +115,7 @@ export default class SassPlugin implements IHeftPlugin { await SassPlugin._sassConfigurationLoader.tryLoadConfigurationFileForProjectAsync( logger.terminal, slashNormalizedBuildFolderPath, - heftConfiguration.rigConfig + rigConfig ); if (sassConfigurationJson) { if (sassConfigurationJson.srcFolder) { diff --git a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts index c5c2bba3a48..985a5c10c55 100644 --- a/libraries/heft-config-file/src/test/ConfigurationFile.test.ts +++ b/libraries/heft-config-file/src/test/ConfigurationFile.test.ts @@ -28,7 +28,7 @@ describe(ConfigurationFile.name, () => { log: terminalProvider.getOutput(), warning: terminalProvider.getWarningOutput(), error: terminalProvider.getErrorOutput(), - verbose: terminalProvider.getVerbose(), + verbose: terminalProvider.getVerboseOutput(), debug: terminalProvider.getDebugOutput() }).toMatchSnapshot(); }); diff --git a/libraries/localization-utilities/src/TypingsGenerator.ts b/libraries/localization-utilities/src/TypingsGenerator.ts index 483726b6fd4..86fac8f0ec1 100644 --- a/libraries/localization-utilities/src/TypingsGenerator.ts +++ b/libraries/localization-utilities/src/TypingsGenerator.ts @@ -3,6 +3,7 @@ import { StringValuesTypingsGenerator, + type IExportAsDefaultOptions, type IStringValueTyping, type ITypingsGeneratorBaseOptions } from '@rushstack/typings-generator'; @@ -11,17 +12,33 @@ import type { NewlineKind } from '@rushstack/node-core-library'; import type { IgnoreStringFunction, ILocalizationFile } from './interfaces'; import { parseLocFile } from './LocFileParser'; +/** + * @public + */ +export interface IInferInterfaceNameExportAsDefaultOptions + extends Omit { + /** + * When `exportAsDefault` is true and this option is true, the default export interface name will be inferred + * from the filename. + */ + inferInterfaceNameFromFilename?: boolean; +} + /** * @public */ export interface ITypingsGeneratorOptions extends ITypingsGeneratorBaseOptions { - exportAsDefault?: boolean; + exportAsDefault?: boolean | IExportAsDefaultOptions | IInferInterfaceNameExportAsDefaultOptions; + resxNewlineNormalization?: NewlineKind | undefined; + ignoreMissingResxComments?: boolean | undefined; + ignoreString?: IgnoreStringFunction; + processComment?: ( comment: string | undefined, - resxFilePath: string, + relativeFilePath: string, stringName: string ) => string | undefined; } @@ -33,17 +50,27 @@ export interface ITypingsGeneratorOptions extends ITypingsGeneratorBaseOptions { */ export class TypingsGenerator extends StringValuesTypingsGenerator { public constructor(options: ITypingsGeneratorOptions) { - const { ignoreString, processComment } = options; + const { + ignoreString, + processComment, + resxNewlineNormalization, + ignoreMissingResxComments, + exportAsDefault + } = options; + const inferDefaultExportInterfaceNameFromFilename: boolean | undefined = + typeof exportAsDefault === 'object' + ? (exportAsDefault as IInferInterfaceNameExportAsDefaultOptions).inferInterfaceNameFromFilename + : undefined; super({ ...options, fileExtensions: ['.resx', '.resx.json', '.loc.json', '.resjson'], - parseAndGenerateTypings: (fileContents: string, filePath: string, resxFilePath: string) => { + parseAndGenerateTypings: (content: string, filePath: string, relativeFilePath: string) => { const locFileData: ILocalizationFile = parseLocFile({ - filePath: filePath, - content: fileContents, - terminal: this._options.terminal!, - resxNewlineNormalization: options.resxNewlineNormalization, - ignoreMissingResxComments: options.ignoreMissingResxComments, + filePath, + content, + terminal: this.terminal, + resxNewlineNormalization, + ignoreMissingResxComments, ignoreString }); @@ -53,7 +80,7 @@ export class TypingsGenerator extends StringValuesTypingsGenerator { for (const stringName in locFileData) { let comment: string | undefined = locFileData[stringName].comment; if (processComment) { - comment = processComment(comment, resxFilePath, stringName); + comment = processComment(comment, relativeFilePath, stringName); } typings.push({ @@ -62,7 +89,31 @@ export class TypingsGenerator extends StringValuesTypingsGenerator { }); } - return { typings }; + let exportAsDefaultInterfaceName: string | undefined; + if (inferDefaultExportInterfaceNameFromFilename) { + const lastSlashIndex: number = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); + let extensionIndex: number = filePath.lastIndexOf('.'); + if (filePath.slice(extensionIndex).toLowerCase() === '.json') { + extensionIndex = filePath.lastIndexOf('.', extensionIndex - 1); + } + + const fileNameWithoutExtension: string = filePath.substring(lastSlashIndex + 1, extensionIndex); + const normalizedFileName: string = fileNameWithoutExtension.replace(/[^a-zA-Z0-9$_]/g, ''); + const firstCharUpperCased: string = normalizedFileName.charAt(0).toUpperCase(); + exportAsDefaultInterfaceName = `I${firstCharUpperCased}${normalizedFileName.slice(1)}`; + + if ( + !exportAsDefaultInterfaceName.endsWith('strings') && + !exportAsDefaultInterfaceName.endsWith('Strings') + ) { + exportAsDefaultInterfaceName += 'Strings'; + } + } + + return { + typings, + exportAsDefaultInterfaceName + }; } }); } diff --git a/libraries/localization-utilities/src/index.ts b/libraries/localization-utilities/src/index.ts index 45f6ed78058..b72ad1bc3ee 100644 --- a/libraries/localization-utilities/src/index.ts +++ b/libraries/localization-utilities/src/index.ts @@ -18,5 +18,9 @@ export { parseLocJson } from './parsers/parseLocJson'; export { parseResJson } from './parsers/parseResJson'; export { parseResx, type IParseResxOptions, type IParseResxOptionsBase } from './parsers/parseResx'; export { parseLocFile, type IParseLocFileOptions, type ParserKind } from './LocFileParser'; -export { type ITypingsGeneratorOptions, TypingsGenerator } from './TypingsGenerator'; +export { + type ITypingsGeneratorOptions, + type IInferInterfaceNameExportAsDefaultOptions, + TypingsGenerator +} from './TypingsGenerator'; export { getPseudolocalizer } from './Pseudolocalization'; diff --git a/libraries/localization-utilities/src/parsers/test/parseResx.test.ts b/libraries/localization-utilities/src/parsers/test/parseResx.test.ts index a2b1c2dfcef..bc914847d18 100644 --- a/libraries/localization-utilities/src/parsers/test/parseResx.test.ts +++ b/libraries/localization-utilities/src/parsers/test/parseResx.test.ts @@ -23,7 +23,7 @@ describe(parseResx.name, () => { outputObject.output = output; } - const verboseOutput: string = terminalProvider.getVerbose(); + const verboseOutput: string = terminalProvider.getVerboseOutput(); if (verboseOutput) { outputObject.verboseOutput = verboseOutput; } diff --git a/libraries/module-minifier/src/MessagePortMinifier.ts b/libraries/module-minifier/src/MessagePortMinifier.ts index d0827b4a026..4b7fb6a8ae7 100644 --- a/libraries/module-minifier/src/MessagePortMinifier.ts +++ b/libraries/module-minifier/src/MessagePortMinifier.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import { once } from 'events'; -import type { MessagePort } from 'worker_threads'; +import type * as WorkerThreads from 'worker_threads'; import type { IMinifierConnection, @@ -17,11 +17,11 @@ import type { * @public */ export class MessagePortMinifier implements IModuleMinifier { - public readonly port: MessagePort; + public readonly port: WorkerThreads.MessagePort; private readonly _callbacks: Map; - public constructor(port: MessagePort) { + public constructor(port: WorkerThreads.MessagePort) { this.port = port; this._callbacks = new Map(); } diff --git a/libraries/node-core-library/src/Text.ts b/libraries/node-core-library/src/Text.ts index e3e801e6595..1737087604a 100644 --- a/libraries/node-core-library/src/Text.ts +++ b/libraries/node-core-library/src/Text.ts @@ -279,4 +279,15 @@ export class Text { // Benchmarks of several algorithms: https://jsbench.me/4bkfflcm2z return s.split('').reduce((newString, char) => char + newString, ''); } + + /** + * Splits the provided string by newlines. Note that leading and trailing newlines will produce + * leading or trailing empty string array entries. + */ + public static splitByNewLines(s: undefined): undefined; + public static splitByNewLines(s: string): string[]; + public static splitByNewLines(s: string | undefined): string[] | undefined; + public static splitByNewLines(s: string | undefined): string[] | undefined { + return s?.split(/\r?\n/); + } } diff --git a/libraries/node-core-library/src/test/Text.test.ts b/libraries/node-core-library/src/test/Text.test.ts index 5e6ce48f51c..a4567b25f98 100644 --- a/libraries/node-core-library/src/test/Text.test.ts +++ b/libraries/node-core-library/src/test/Text.test.ts @@ -119,4 +119,17 @@ describe(Text.name, () => { expect(Text.escapeRegExp('a\\c')).toEqual('a\\\\c'); }); }); + + describe(Text.splitByNewLines.name, () => { + it('splits a string by newlines', () => { + expect(Text.splitByNewLines(undefined)).toEqual(undefined); + expect(Text.splitByNewLines('')).toEqual(['']); + expect(Text.splitByNewLines('abc')).toEqual(['abc']); + expect(Text.splitByNewLines('a\nb\nc')).toEqual(['a', 'b', 'c']); + expect(Text.splitByNewLines('a\nb\nc\n')).toEqual(['a', 'b', 'c', '']); + expect(Text.splitByNewLines('a\nb\nc\n\n')).toEqual(['a', 'b', 'c', '', '']); + expect(Text.splitByNewLines('\n\na\nb\nc\n\n')).toEqual(['', '', 'a', 'b', 'c', '', '']); + expect(Text.splitByNewLines('a\r\nb\nc')).toEqual(['a', 'b', 'c']); + }); + }); }); diff --git a/libraries/rush-lib/src/api/test/CustomTipsConfiguration.test.ts b/libraries/rush-lib/src/api/test/CustomTipsConfiguration.test.ts index 59150474534..1f6a769653d 100644 --- a/libraries/rush-lib/src/api/test/CustomTipsConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/CustomTipsConfiguration.test.ts @@ -62,7 +62,7 @@ describe(CustomTipsConfiguration.name, () => { appendOutputLines(terminalProvider.getOutput(), 'normal output'); appendOutputLines(terminalProvider.getErrorOutput(), 'error output'); appendOutputLines(terminalProvider.getWarningOutput(), 'warning output'); - appendOutputLines(terminalProvider.getVerbose(), 'verbose output'); + appendOutputLines(terminalProvider.getVerboseOutput(), 'verbose output'); appendOutputLines(terminalProvider.getDebugOutput(), 'debug output'); expect(outputLines).toMatchSnapshot(); diff --git a/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts b/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts index f86d631befb..e4bbc4971fd 100644 --- a/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts @@ -65,7 +65,7 @@ function validateConfiguration(rushProjectConfiguration: RushProjectConfiguratio expect(terminalProvider.getOutput()).toMatchSnapshot('validation: terminal output'); expect(terminalProvider.getErrorOutput()).toMatchSnapshot('validation: terminal error'); expect(terminalProvider.getWarningOutput()).toMatchSnapshot('validation: terminal warning'); - expect(terminalProvider.getVerbose()).toMatchSnapshot('validation: terminal verbose'); + expect(terminalProvider.getVerboseOutput()).toMatchSnapshot('validation: terminal verbose'); } } } @@ -168,9 +168,8 @@ describe(RushProjectConfiguration.name, () => { }); it('returns undefined if the operation is a no-op', async () => { - const config: RushProjectConfiguration | undefined = await loadProjectConfigurationAsync( - 'test-project-c' - ); + const config: RushProjectConfiguration | undefined = + await loadProjectConfigurationAsync('test-project-c'); if (!config) { throw new Error('Failed to load config'); @@ -181,9 +180,8 @@ describe(RushProjectConfiguration.name, () => { }); it('returns reason if the operation is runnable', async () => { - const config: RushProjectConfiguration | undefined = await loadProjectConfigurationAsync( - 'test-project-c' - ); + const config: RushProjectConfiguration | undefined = + await loadProjectConfigurationAsync('test-project-c'); if (!config) { throw new Error('Failed to load config'); diff --git a/libraries/rush-lib/src/cli/RushCommandLineParser.ts b/libraries/rush-lib/src/cli/RushCommandLineParser.ts index af07ad197ba..26bf565a927 100644 --- a/libraries/rush-lib/src/cli/RushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushCommandLineParser.ts @@ -8,7 +8,7 @@ import { type CommandLineFlagParameter, CommandLineHelper } from '@rushstack/ts-command-line'; -import { InternalError, AlreadyReportedError } from '@rushstack/node-core-library'; +import { InternalError, AlreadyReportedError, Text } from '@rushstack/node-core-library'; import { ConsoleTerminalProvider, Terminal, @@ -456,8 +456,7 @@ export class RushCommandLineParser extends CommandLineParser { // The colors package will eat multi-newlines, which could break formatting // in user-specified messages and instructions, so we prefer to color each // line individually. - const message: string = PrintUtilities.wrapWords(prefix + error.message) - .split(/\r?\n/) + const message: string = Text.splitByNewLines(PrintUtilities.wrapWords(prefix + error.message)) .map((line) => Colorize.red(line)) .join('\n'); // eslint-disable-next-line no-console diff --git a/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts b/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts index 33b651a5053..460e8900935 100644 --- a/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts +++ b/libraries/rush-lib/src/logic/setup/SetupPackageRegistry.ts @@ -496,7 +496,7 @@ export class SetupPackageRegistry { * @returns the JSON section, or `undefined` if a JSON object could not be detected */ private static _tryFindJson(dirtyOutput: string): string | undefined { - const lines: string[] = dirtyOutput.split(/\r?\n/g); + const lines: string[] = Text.splitByNewLines(dirtyOutput); let startIndex: number | undefined; let endIndex: number | undefined; diff --git a/libraries/terminal/src/PrintUtilities.ts b/libraries/terminal/src/PrintUtilities.ts index b59ea247961..1c9732d4896 100644 --- a/libraries/terminal/src/PrintUtilities.ts +++ b/libraries/terminal/src/PrintUtilities.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { Text } from '@rushstack/node-core-library'; import type { ITerminal } from './ITerminal'; /** @@ -116,7 +117,7 @@ export class PrintUtilities { // Apply word wrapping and the provided line prefix, while also respecting existing newlines // and prefix spaces that may exist in the text string already. - const lines: string[] = text.split(/\r?\n/); + const lines: string[] = Text.splitByNewLines(text); const wrappedLines: string[] = []; for (const line of lines) { diff --git a/libraries/terminal/src/StringBufferTerminalProvider.ts b/libraries/terminal/src/StringBufferTerminalProvider.ts index ea6b67fdcc6..72c9bc2d352 100644 --- a/libraries/terminal/src/StringBufferTerminalProvider.ts +++ b/libraries/terminal/src/StringBufferTerminalProvider.ts @@ -93,9 +93,16 @@ export class StringBufferTerminalProvider implements ITerminalProvider { } /** - * Get everything that has been written at verbose-level severity. + * @deprecated - use {@link StringBufferTerminalProvider.getVerboseOutput} */ public getVerbose(options?: IStringBufferOutputOptions): string { + return this.getVerboseOutput(options); + } + + /** + * Get everything that has been written at verbose-level severity. + */ + public getVerboseOutput(options?: IStringBufferOutputOptions): string { return this._normalizeOutput(this._verboseBuffer.toString(), options); } diff --git a/libraries/terminal/src/test/PrefixProxyTerminalProvider.test.ts b/libraries/terminal/src/test/PrefixProxyTerminalProvider.test.ts index 9c15fe522f8..c0a69b8b960 100644 --- a/libraries/terminal/src/test/PrefixProxyTerminalProvider.test.ts +++ b/libraries/terminal/src/test/PrefixProxyTerminalProvider.test.ts @@ -17,7 +17,7 @@ function runTestsForTerminalProvider( log: baseProvider.getOutput(), warning: baseProvider.getWarningOutput(), error: baseProvider.getErrorOutput(), - verbose: baseProvider.getVerbose(), + verbose: baseProvider.getVerboseOutput(), debug: baseProvider.getDebugOutput() }).toMatchSnapshot(); } diff --git a/libraries/terminal/src/test/Terminal.test.ts b/libraries/terminal/src/test/Terminal.test.ts index 9e1eaf9b07d..903432963df 100644 --- a/libraries/terminal/src/test/Terminal.test.ts +++ b/libraries/terminal/src/test/Terminal.test.ts @@ -14,7 +14,7 @@ describe(Terminal.name, () => { log: provider.getOutput(), warning: provider.getWarningOutput(), error: provider.getErrorOutput(), - verbose: provider.getVerbose(), + verbose: provider.getVerboseOutput(), debug: provider.getDebugOutput() }).toMatchSnapshot(); } diff --git a/libraries/terminal/src/test/TerminalStreamWritable.test.ts b/libraries/terminal/src/test/TerminalStreamWritable.test.ts index 049a9735f84..4fa0960965f 100644 --- a/libraries/terminal/src/test/TerminalStreamWritable.test.ts +++ b/libraries/terminal/src/test/TerminalStreamWritable.test.ts @@ -15,7 +15,7 @@ function verifyProvider(): void { log: provider.getOutput(), warning: provider.getWarningOutput(), error: provider.getErrorOutput(), - verbose: provider.getVerbose(), + verbose: provider.getVerboseOutput(), debug: provider.getDebugOutput() }).toMatchSnapshot(); } diff --git a/libraries/typings-generator/package.json b/libraries/typings-generator/package.json index 7306c2988f7..21c788dfd72 100644 --- a/libraries/typings-generator/package.json +++ b/libraries/typings-generator/package.json @@ -17,7 +17,8 @@ }, "scripts": { "build": "heft build --clean", - "_phase:build": "heft run --only build -- --clean" + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" }, "dependencies": { "@rushstack/node-core-library": "workspace:*", diff --git a/libraries/typings-generator/src/StringValuesTypingsGenerator.ts b/libraries/typings-generator/src/StringValuesTypingsGenerator.ts index 8568f2e08a6..a9775dd0d9b 100644 --- a/libraries/typings-generator/src/StringValuesTypingsGenerator.ts +++ b/libraries/typings-generator/src/StringValuesTypingsGenerator.ts @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. import { EOL } from 'os'; +import { Text } from '@rushstack/node-core-library'; import { type ITypingsGeneratorOptions, @@ -22,6 +23,30 @@ export interface IStringValueTyping { */ export interface IStringValueTypings { typings: IStringValueTyping[]; + + /** + * Options for default exports. Note that options provided here will override + * options provided in {@link IStringValuesTypingsGeneratorBaseOptions.exportAsDefault}. + */ + exportAsDefault?: boolean | IExportAsDefaultOptions; +} + +/** + * @public + */ +export interface IExportAsDefaultOptions { + /** + * This setting overrides the the interface name for the default wrapped export. + * + * @defaultValue "IExport" + */ + interfaceName?: string; + + /** + * This value is placed in a documentation comment for the + * exported default interface. + */ + documentationComment?: string; } /** @@ -31,11 +56,11 @@ export interface IStringValuesTypingsGeneratorBaseOptions { /** * Setting this option wraps the typings export in a default property. */ - exportAsDefault?: boolean; + exportAsDefault?: boolean | IExportAsDefaultOptions; /** - * When `exportAsDefault` is true, this optional setting determines the interface name - * for the default wrapped export. Ignored when `exportAsDefault` is false. + * @deprecated Use {@link IStringValuesTypingsGeneratorBaseOptions.exportAsDefault}'s + * {@link IExportAsDefaultOptions.interfaceName} instead. */ exportAsDefaultInterfaceName?: string; } @@ -63,12 +88,32 @@ const EXPORT_AS_DEFAULT_INTERFACE_NAME: string = 'IExport'; function convertToTypingsGeneratorOptions( options: IStringValuesTypingsGeneratorOptionsWithCustomReadFile ): ITypingsGeneratorOptionsWithCustomReadFile { - async function parseAndGenerateTypings( + const { + exportAsDefault: exportAsDefaultOptions, + exportAsDefaultInterfaceName: exportAsDefaultInterfaceName_deprecated, + parseAndGenerateTypings + } = options; + let defaultSplitExportAsDefaultDocumentationComment: string[] | undefined; + let defaultExportAsDefaultInterfaceName: string | undefined; + if (typeof exportAsDefaultOptions === 'object') { + defaultSplitExportAsDefaultDocumentationComment = Text.splitByNewLines( + exportAsDefaultOptions.documentationComment + ); + defaultExportAsDefaultInterfaceName = + exportAsDefaultOptions.interfaceName ?? + exportAsDefaultInterfaceName_deprecated ?? + EXPORT_AS_DEFAULT_INTERFACE_NAME; + } else if (exportAsDefaultOptions) { + defaultExportAsDefaultInterfaceName = + exportAsDefaultInterfaceName_deprecated ?? EXPORT_AS_DEFAULT_INTERFACE_NAME; + } + + async function parseAndGenerateTypingsOuter( fileContents: TFileContents, filePath: string, relativePath: string ): Promise { - const stringValueTypings: IStringValueTypings | undefined = await options.parseAndGenerateTypings( + const stringValueTypings: IStringValueTypings | undefined = await parseAndGenerateTypings( fileContents, filePath, relativePath @@ -78,32 +123,64 @@ function convertToTypingsGeneratorOptions( return; } + const { exportAsDefault: exportAsDefaultOptionsOverride, typings } = stringValueTypings; + let exportAsDefaultInterfaceName: string | undefined; + let interfaceDocumentationCommentLines: string[] | undefined; + if (typeof exportAsDefaultOptionsOverride === 'boolean') { + if (exportAsDefaultOptionsOverride) { + exportAsDefaultInterfaceName = + defaultExportAsDefaultInterfaceName ?? EXPORT_AS_DEFAULT_INTERFACE_NAME; + interfaceDocumentationCommentLines = defaultSplitExportAsDefaultDocumentationComment; + } + } else if (exportAsDefaultOptionsOverride) { + const { interfaceName, documentationComment } = exportAsDefaultOptionsOverride; + exportAsDefaultInterfaceName = + interfaceName ?? defaultExportAsDefaultInterfaceName ?? EXPORT_AS_DEFAULT_INTERFACE_NAME; + interfaceDocumentationCommentLines = + Text.splitByNewLines(documentationComment) ?? defaultSplitExportAsDefaultDocumentationComment; + } else { + exportAsDefaultInterfaceName = defaultExportAsDefaultInterfaceName; + interfaceDocumentationCommentLines = defaultSplitExportAsDefaultDocumentationComment; + } + const outputLines: string[] = []; - const interfaceName: string = options.exportAsDefaultInterfaceName - ? options.exportAsDefaultInterfaceName - : EXPORT_AS_DEFAULT_INTERFACE_NAME; let indent: string = ''; - if (options.exportAsDefault) { - outputLines.push(`export interface ${interfaceName} {`); + if (exportAsDefaultInterfaceName) { + if (interfaceDocumentationCommentLines) { + outputLines.push(`/**`); + for (const line of interfaceDocumentationCommentLines) { + outputLines.push(` * ${line}`); + } + + outputLines.push(` */`); + } + + outputLines.push(`export interface ${exportAsDefaultInterfaceName} {`); indent = ' '; } - for (const stringValueTyping of stringValueTypings.typings) { + for (const stringValueTyping of typings) { const { exportName, comment } = stringValueTyping; if (comment && comment.trim() !== '') { outputLines.push(`${indent}/**`, `${indent} * ${comment.replace(/\*\//g, '*\\/')}`, `${indent} */`); } - if (options.exportAsDefault) { + if (exportAsDefaultInterfaceName) { outputLines.push(`${indent}'${exportName}': string;`, ''); } else { outputLines.push(`export declare const ${exportName}: string;`, ''); } } - if (options.exportAsDefault) { - outputLines.push('}', '', `declare const strings: ${interfaceName};`, '', 'export default strings;'); + if (exportAsDefaultInterfaceName) { + outputLines.push( + '}', + '', + `declare const strings: ${exportAsDefaultInterfaceName};`, + '', + 'export default strings;' + ); } return outputLines.join(EOL); @@ -111,7 +188,7 @@ function convertToTypingsGeneratorOptions( const convertedOptions: ITypingsGeneratorOptionsWithCustomReadFile = { ...options, - parseAndGenerateTypings + parseAndGenerateTypings: parseAndGenerateTypingsOuter }; return convertedOptions; diff --git a/libraries/typings-generator/src/TypingsGenerator.ts b/libraries/typings-generator/src/TypingsGenerator.ts index 938e7a7f7e6..2ed8e91d7f1 100644 --- a/libraries/typings-generator/src/TypingsGenerator.ts +++ b/libraries/typings-generator/src/TypingsGenerator.ts @@ -33,12 +33,6 @@ export interface ITypingsGeneratorOptionsWithoutReadFile< relativePath: string ) => TTypingsResult | Promise; getAdditionalOutputFiles?: (relativePath: string) => string[]; - /** - * @deprecated - * - * TODO: Remove when version 1.0.0 is released. - */ - filesToIgnore?: string[]; } /** @@ -86,7 +80,9 @@ export class TypingsGenerator { // Map of resolved file path -> relative file path private readonly _relativePaths: Map; - protected _options: ITypingsGeneratorOptionsWithCustomReadFile; + protected readonly _options: ITypingsGeneratorOptionsWithCustomReadFile; + + protected readonly terminal: ITerminal; /** * The folder path that contains all input source files. @@ -118,10 +114,6 @@ export class TypingsGenerator { FileSystem.readFileAsync(filePath) as Promise) }; - if (options.filesToIgnore) { - throw new Error('The filesToIgnore option is no longer supported. Please use globsToIgnore instead.'); - } - if (!options.generatedTsFolder) { throw new Error('generatedTsFolder must be provided'); } @@ -145,9 +137,7 @@ export class TypingsGenerator { this.ignoredFileGlobs = options.globsToIgnore || []; - if (!options.terminal) { - this._options.terminal = new Terminal(new ConsoleTerminalProvider({ verboseEnabled: true })); - } + this.terminal = options.terminal ?? new Terminal(new ConsoleTerminalProvider({ verboseEnabled: true })); this._options.fileExtensions = this._normalizeFileExtensions(options.fileExtensions); @@ -354,7 +344,7 @@ export class TypingsGenerator { }); } } catch (e) { - this._options.terminal!.writeError( + this.terminal.writeError( `Error occurred parsing and generating typings for file "${resolvedPath}": ${e}` ); } @@ -376,10 +366,10 @@ export class TypingsGenerator { private *_getTypingsFilePaths(relativePath: string): Iterable { const { generatedTsFolder, secondaryGeneratedTsFolders } = this._options; const dtsFilename: string = `${relativePath}.d.ts`; - yield path.resolve(generatedTsFolder, dtsFilename); + yield `${generatedTsFolder}/${dtsFilename}`; if (secondaryGeneratedTsFolders) { for (const secondaryGeneratedTsFolder of secondaryGeneratedTsFolders) { - yield path.resolve(secondaryGeneratedTsFolder, dtsFilename); + yield `${secondaryGeneratedTsFolder}/${dtsFilename}`; } } } diff --git a/libraries/typings-generator/src/index.ts b/libraries/typings-generator/src/index.ts index 802788b3d24..20e2e1dfcfe 100644 --- a/libraries/typings-generator/src/index.ts +++ b/libraries/typings-generator/src/index.ts @@ -21,6 +21,7 @@ export { export { type IStringValueTyping, type IStringValueTypings, + type IExportAsDefaultOptions, type IStringValuesTypingsGeneratorBaseOptions, type IStringValuesTypingsGeneratorOptions, type IStringValuesTypingsGeneratorOptionsWithCustomReadFile, diff --git a/libraries/typings-generator/src/test/StringValuesTypingsGenerator.test.ts b/libraries/typings-generator/src/test/StringValuesTypingsGenerator.test.ts new file mode 100644 index 00000000000..b6f0dfbf105 --- /dev/null +++ b/libraries/typings-generator/src/test/StringValuesTypingsGenerator.test.ts @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; +import type { + IStringValuesTypingsGeneratorBaseOptions, + IStringValueTypings +} from '../StringValuesTypingsGenerator'; + +let inputFs: Record; +let outputFs: Record; + +jest.mock('@rushstack/node-core-library', () => { + const realNcl: typeof import('@rushstack/node-core-library') = jest.requireActual( + '@rushstack/node-core-library' + ); + return { + ...realNcl, + FileSystem: { + readFileAsync: async (filePath: string) => { + const result: string | undefined = inputFs[filePath]; + if (result === undefined) { + const error: NodeJS.ErrnoException = new Error( + `Cannot read file ${filePath}` + ) as NodeJS.ErrnoException; + error.code = 'ENOENT'; + throw error; + } else { + return result; + } + }, + writeFileAsync: async (filePath: string, contents: string) => { + outputFs[filePath] = contents; + } + } + }; +}); + +describe('StringValuesTypingsGenerator', () => { + beforeEach(() => { + inputFs = {}; + outputFs = {}; + }); + + function runTests( + baseOptions: IStringValuesTypingsGeneratorBaseOptions, + extraStringTypings?: Partial + ): void { + it('should generate typings', async () => { + const [{ StringValuesTypingsGenerator }, { Terminal, StringBufferTerminalProvider }] = + await Promise.all([import('../StringValuesTypingsGenerator'), import('@rushstack/terminal')]); + const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(); + const terminal: Terminal = new Terminal(terminalProvider); + + inputFs['/src/test.ext'] = ''; + + const fileContents: {} = { a: 1 }; + const generator = new StringValuesTypingsGenerator({ + srcFolder: '/src', + generatedTsFolder: '/out', + readFile: (filePath: string, relativePath: string) => { + expect(relativePath).toEqual('test.ext'); + return Promise.resolve(fileContents); + }, + fileExtensions: ['.ext'], + parseAndGenerateTypings: (contents: {}, filePath: string, relativePath: string) => { + expect(contents).toBe(fileContents); + return { + typings: [ + { + exportName: 'test', + comment: 'test comment\nsecond line' + } + ], + ...extraStringTypings + }; + }, + terminal, + ...baseOptions + }); + + await generator.generateTypingsAsync(['test.ext']); + expect(outputFs).toMatchSnapshot(); + + expect(terminalProvider.getOutput()).toEqual(''); + expect(terminalProvider.getWarningOutput()).toEqual(''); + expect(terminalProvider.getErrorOutput()).toEqual(''); + expect(terminalProvider.getVerboseOutput()).toEqual(''); + expect(terminalProvider.getDebugOutput()).toEqual(''); + }); + } + + describe('non-default exports', () => { + runTests({}); + }); + + describe('default exports', () => { + describe('with { exportAsDefault: true }', () => { + runTests({ exportAsDefault: true }); + }); + + describe("with { exportAsDefault: true, exportAsDefaultInterfaceName: 'IOverride' }", () => { + runTests({ + exportAsDefault: true, + exportAsDefaultInterfaceName: 'IOverride' + }); + }); + + describe("with { exportAsDefault: {}, exportAsDefaultInterfaceName: 'IOverride' }", () => { + runTests({ + exportAsDefault: {}, + exportAsDefaultInterfaceName: 'IOverride' + }); + }); + + describe("with { exportAsDefault: { interfaceName: 'IOverride' }, exportAsDefaultInterfaceName: 'IDeprecated' }", () => { + runTests({ + exportAsDefault: { + interfaceName: 'IOverride' + }, + exportAsDefaultInterfaceName: 'IDeprecated' + }); + }); + + describe("with { exportAsDefault: { documentationComment: 'doc-comment\\nsecond line' } }", () => { + runTests({ + exportAsDefault: { + documentationComment: 'doc-comment\nsecond line' + } + }); + }); + + describe('overrides for individual files', () => { + describe('with exportAsDefault unset', () => { + describe('overriding with { exportAsDefault: false }', () => { + runTests( + {}, + { + exportAsDefault: false + } + ); + }); + + describe("overriding with { interfaceName: 'IOverride' } ", () => { + runTests( + {}, + { + exportAsDefault: { + interfaceName: 'IOverride' + } + } + ); + }); + + describe('overriding with a new doc comment ', () => { + runTests( + {}, + { + exportAsDefault: { + documentationComment: 'doc-comment\nsecond line' + } + } + ); + }); + }); + + describe('with exportAsDefault set to true', () => { + describe('overriding with { exportAsDefault: false }', () => { + runTests( + { + exportAsDefault: true + }, + { + exportAsDefault: false + } + ); + }); + + describe("overriding with { interfaceName: 'IOverride' } ", () => { + runTests( + { + exportAsDefault: true + }, + { + exportAsDefault: { + interfaceName: 'IOverride' + } + } + ); + }); + + describe('overriding with a new doc comment ', () => { + runTests( + { + exportAsDefault: true + }, + { + exportAsDefault: { + documentationComment: 'doc-comment\nsecond line' + } + } + ); + }); + }); + + describe('with exportAsDefault set to {}', () => { + describe('overriding with { exportAsDefault: false }', () => { + runTests( + { + exportAsDefault: {} + }, + { + exportAsDefault: false + } + ); + }); + + describe("overriding with { interfaceName: 'IOverride' } ", () => { + runTests( + { + exportAsDefault: {} + }, + { + exportAsDefault: { + interfaceName: 'IOverride' + } + } + ); + }); + + describe('overriding with a new doc comment ', () => { + runTests( + { + exportAsDefault: {} + }, + { + exportAsDefault: { + documentationComment: 'doc-comment\nsecond line' + } + } + ); + }); + }); + + describe('with exportAsDefault filled', () => { + describe('overriding with { exportAsDefault: false }', () => { + runTests( + { + exportAsDefault: { + interfaceName: 'IBase', + documentationComment: 'base-comment' + } + }, + { + exportAsDefault: false + } + ); + }); + + describe("overriding with { interfaceName: 'IOverride' } ", () => { + runTests( + { + exportAsDefault: { + interfaceName: 'IBase', + documentationComment: 'base-comment' + } + }, + { + exportAsDefault: { + interfaceName: 'IOverride' + } + } + ); + }); + + describe('overriding with a new doc comment ', () => { + runTests( + { + exportAsDefault: { + interfaceName: 'IBase', + documentationComment: 'base-comment' + } + }, + { + exportAsDefault: { + documentationComment: 'doc-comment\nsecond line' + } + } + ); + }); + }); + }); + }); +}); diff --git a/libraries/typings-generator/src/test/__snapshots__/StringValuesTypingsGenerator.test.ts.snap b/libraries/typings-generator/src/test/__snapshots__/StringValuesTypingsGenerator.test.ts.snap new file mode 100644 index 00000000000..04dc2be464b --- /dev/null +++ b/libraries/typings-generator/src/test/__snapshots__/StringValuesTypingsGenerator.test.ts.snap @@ -0,0 +1,336 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault filled overriding with { exportAsDefault: false } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * test comment +second line + */ +export declare const test: string; +", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault filled overriding with { interfaceName: 'IOverride' } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * base-comment + */ +export interface IOverride { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IOverride; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault filled overriding with a new doc comment should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * doc-comment + * second line + */ +export interface IBase { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IBase; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault set to {} overriding with { exportAsDefault: false } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * test comment +second line + */ +export declare const test: string; +", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault set to {} overriding with { interfaceName: 'IOverride' } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +export interface IOverride { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IOverride; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault set to {} overriding with a new doc comment should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * doc-comment + * second line + */ +export interface IExport { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IExport; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault set to true overriding with { exportAsDefault: false } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * test comment +second line + */ +export declare const test: string; +", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault set to true overriding with { interfaceName: 'IOverride' } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +export interface IOverride { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IOverride; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault set to true overriding with a new doc comment should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * doc-comment + * second line + */ +export interface IExport { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IExport; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault unset overriding with { exportAsDefault: false } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * test comment +second line + */ +export declare const test: string; +", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault unset overriding with { interfaceName: 'IOverride' } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +export interface IOverride { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IOverride; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports overrides for individual files with exportAsDefault unset overriding with a new doc comment should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * doc-comment + * second line + */ +export interface IExport { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IExport; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports with { exportAsDefault: { documentationComment: 'doc-comment\\nsecond line' } } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * doc-comment + * second line + */ +export interface IExport { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IExport; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports with { exportAsDefault: { interfaceName: 'IOverride' }, exportAsDefaultInterfaceName: 'IDeprecated' } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +export interface IOverride { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IOverride; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports with { exportAsDefault: {}, exportAsDefaultInterfaceName: 'IOverride' } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +export interface IOverride { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IOverride; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports with { exportAsDefault: true } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +export interface IExport { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IExport; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator default exports with { exportAsDefault: true, exportAsDefaultInterfaceName: 'IOverride' } should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +export interface IOverride { + /** + * test comment +second line + */ + 'test': string; + +} + +declare const strings: IOverride; + +export default strings;", +} +`; + +exports[`StringValuesTypingsGenerator non-default exports should generate typings 1`] = ` +Object { + "/out/test.ext.d.ts": "// This file was generated by a tool. Modifying it will produce unexpected behavior + +/** + * test comment +second line + */ +export declare const test: string; +", +} +`; diff --git a/rush.json b/rush.json index 6f3c6156818..b0977f05e42 100644 --- a/rush.json +++ b/rush.json @@ -962,6 +962,12 @@ "shouldPublish": true, "cyclicDependencyProjects": ["@rushstack/heft-node-rig"] }, + { + "packageName": "@rushstack/heft-localization-typings-plugin", + "projectFolder": "heft-plugins/heft-localization-typings-plugin", + "reviewCategory": "libraries", + "shouldPublish": true + }, { "packageName": "@rushstack/heft-sass-plugin", "projectFolder": "heft-plugins/heft-sass-plugin",