diff --git a/README.md b/README.md index be4cb6d41cb..e49d4c1c729 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/heft-plugins/heft-sass-load-themed-styles-plugin](./heft-plugins/heft-sass-load-themed-styles-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-sass-load-themed-styles-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-sass-load-themed-styles-plugin) | [changelog](./heft-plugins/heft-sass-load-themed-styles-plugin/CHANGELOG.md) | [@rushstack/heft-sass-load-themed-styles-plugin](https://www.npmjs.com/package/@rushstack/heft-sass-load-themed-styles-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-static-asset-typings-plugin](./heft-plugins/heft-static-asset-typings-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-static-asset-typings-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-static-asset-typings-plugin) | [changelog](./heft-plugins/heft-static-asset-typings-plugin/CHANGELOG.md) | [@rushstack/heft-static-asset-typings-plugin](https://www.npmjs.com/package/@rushstack/heft-static-asset-typings-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) | | [/heft-plugins/heft-typescript-plugin](./heft-plugins/heft-typescript-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-typescript-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-typescript-plugin) | [changelog](./heft-plugins/heft-typescript-plugin/CHANGELOG.md) | [@rushstack/heft-typescript-plugin](https://www.npmjs.com/package/@rushstack/heft-typescript-plugin) | | [/heft-plugins/heft-vscode-extension-plugin](./heft-plugins/heft-vscode-extension-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-vscode-extension-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-vscode-extension-plugin) | [changelog](./heft-plugins/heft-vscode-extension-plugin/CHANGELOG.md) | [@rushstack/heft-vscode-extension-plugin](https://www.npmjs.com/package/@rushstack/heft-vscode-extension-plugin) | diff --git a/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx b/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx index 1fb8c83d1d4..3ef0dfdafab 100644 --- a/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx +++ b/build-tests-samples/heft-web-rig-app-tutorial/src/ExampleApp.tsx @@ -4,6 +4,8 @@ import * as React from 'react'; import { ToggleSwitch, type IToggleEventArgs } from 'heft-web-rig-library-tutorial'; +import exampleImage from './example-image.png'; + /** * This React component renders the application page. */ @@ -24,10 +26,7 @@ export class ExampleApp extends React.Component {

Here is an example image:

- + ); } diff --git a/build-tests/heft-node-everything-esm-module-test/config/heft.json b/build-tests/heft-node-everything-esm-module-test/config/heft.json index bfb4f25a904..e2e0619b020 100644 --- a/build-tests/heft-node-everything-esm-module-test/config/heft.json +++ b/build-tests/heft-node-everything-esm-module-test/config/heft.json @@ -9,7 +9,18 @@ "cleanFiles": [{ "includeGlobs": ["dist", "lib-commonjs", "lib", "lib-esm", "lib-esnext", "lib-umd"] }], "tasksByName": { + "text-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "source-assets-plugin", + "options": { + "configType": "file", + "configFileName": "source-assets.json" + } + } + }, "typescript": { + "taskDependencies": ["text-typings"], "taskPlugin": { "pluginPackage": "@rushstack/heft-typescript-plugin" } diff --git a/build-tests/heft-node-everything-esm-module-test/config/source-assets.json b/build-tests/heft-node-everything-esm-module-test/config/source-assets.json new file mode 100644 index 00000000000..7d3efa3121a --- /dev/null +++ b/build-tests/heft-node-everything-esm-module-test/config/source-assets.json @@ -0,0 +1,6 @@ +{ + "fileExtensions": [".html"], + "cjsOutputFolders": ["lib-commonjs"], + "esmOutputFolders": ["lib-esm"], + "generatedTsFolders": ["temp/text-typings"] +} diff --git a/build-tests/heft-node-everything-esm-module-test/etc/heft-node-everything-esm-module-test.api.md b/build-tests/heft-node-everything-esm-module-test/etc/heft-node-everything-esm-module-test.api.md index 9d5bceee423..86361b68da7 100644 --- a/build-tests/heft-node-everything-esm-module-test/etc/heft-node-everything-esm-module-test.api.md +++ b/build-tests/heft-node-everything-esm-module-test/etc/heft-node-everything-esm-module-test.api.md @@ -4,6 +4,9 @@ ```ts +// @public +export const templateContent: string; + // @public (undocumented) export class TestClass { } diff --git a/build-tests/heft-node-everything-esm-module-test/package.json b/build-tests/heft-node-everything-esm-module-test/package.json index 0f4196f00ab..cffaca35a45 100644 --- a/build-tests/heft-node-everything-esm-module-test/package.json +++ b/build-tests/heft-node-everything-esm-module-test/package.json @@ -17,6 +17,7 @@ "@rushstack/heft-api-extractor-plugin": "workspace:*", "@rushstack/heft-jest-plugin": "workspace:*", "@rushstack/heft-lint-plugin": "workspace:*", + "@rushstack/heft-static-asset-typings-plugin": "workspace:*", "@rushstack/heft-typescript-plugin": "workspace:*", "@types/heft-jest": "1.0.1", "@types/node": "20.17.19", diff --git a/build-tests/heft-node-everything-esm-module-test/src/exampleTemplate.html b/build-tests/heft-node-everything-esm-module-test/src/exampleTemplate.html new file mode 100644 index 00000000000..44ab8a9ae74 --- /dev/null +++ b/build-tests/heft-node-everything-esm-module-test/src/exampleTemplate.html @@ -0,0 +1 @@ +

This is an example template imported as a text asset.

diff --git a/build-tests/heft-node-everything-esm-module-test/src/index.ts b/build-tests/heft-node-everything-esm-module-test/src/index.ts index 659610ef84f..22b50eb31b1 100644 --- a/build-tests/heft-node-everything-esm-module-test/src/index.ts +++ b/build-tests/heft-node-everything-esm-module-test/src/index.ts @@ -1,6 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import exampleTemplate from './exampleTemplate.html'; + +/** + * The content of exampleTemplate.html, imported as a text asset. + * @public + */ +export const templateContent: string = exampleTemplate; + /** * @public */ diff --git a/build-tests/heft-node-everything-esm-module-test/tsconfig.json b/build-tests/heft-node-everything-esm-module-test/tsconfig.json index d08637a6a20..550c1aa1ab3 100644 --- a/build-tests/heft-node-everything-esm-module-test/tsconfig.json +++ b/build-tests/heft-node-everything-esm-module-test/tsconfig.json @@ -4,7 +4,7 @@ "compilerOptions": { "outDir": "lib-commonjs", "declarationDir": "lib-dts", - "rootDir": "src", + "rootDirs": ["src", "temp/text-typings"], "forceConsistentCasingInFileNames": true, "jsx": "react", diff --git a/build-tests/heft-node-everything-test/config/heft.json b/build-tests/heft-node-everything-test/config/heft.json index 717d3c6576c..ba8a4e8318b 100644 --- a/build-tests/heft-node-everything-test/config/heft.json +++ b/build-tests/heft-node-everything-test/config/heft.json @@ -16,7 +16,23 @@ "cleanFiles": [{ "includeGlobs": ["dist", "lib-commonjs", "lib", "lib-esm", "lib-esnext", "lib-umd"] }], "tasksByName": { + "text-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "source-assets-plugin", + "options": { + "configType": "inline", + "config": { + "fileExtensions": [".html"], + "cjsOutputFolders": ["lib-commonjs"], + "esmOutputFolders": ["lib-esm"], + "generatedTsFolders": ["temp/text-typings"] + } + } + } + }, "typescript": { + "taskDependencies": ["text-typings"], "taskPlugin": { "pluginPackage": "@rushstack/heft-typescript-plugin" } diff --git a/build-tests/heft-node-everything-test/config/jest.config.json b/build-tests/heft-node-everything-test/config/jest.config.json index c0687c6d488..d22ada97183 100644 --- a/build-tests/heft-node-everything-test/config/jest.config.json +++ b/build-tests/heft-node-everything-test/config/jest.config.json @@ -1,6 +1,9 @@ { "extends": "@rushstack/heft-jest-plugin/includes/jest-shared.config.json", + "roots": ["/lib-commonjs"], + "testMatch": ["/lib-commonjs/**/*.test.{cjs,js}"], + // Enable code coverage for Jest "collectCoverage": true, "coverageDirectory": "/coverage", diff --git a/build-tests/heft-node-everything-test/config/rush-project.json b/build-tests/heft-node-everything-test/config/rush-project.json index 40a0d93f857..50339f80875 100644 --- a/build-tests/heft-node-everything-test/config/rush-project.json +++ b/build-tests/heft-node-everything-test/config/rush-project.json @@ -4,7 +4,7 @@ "operationSettings": [ { "operationName": "_phase:build", - "outputFolderNames": ["dist", "lib-commonjs", "lib-esm", "lib-umd", "lib-dts"] + "outputFolderNames": ["dist", "lib-commonjs", "lib-esm", "lib-umd", "lib-dts", "temp/text-typings"] }, { "operationName": "_phase:test", diff --git a/build-tests/heft-node-everything-test/config/source-assets.json b/build-tests/heft-node-everything-test/config/source-assets.json new file mode 100644 index 00000000000..e35e877e016 --- /dev/null +++ b/build-tests/heft-node-everything-test/config/source-assets.json @@ -0,0 +1,4 @@ +{ + "fileExtensions": [".html"], + "generatedTsFolder": "temp/text-typings" +} diff --git a/build-tests/heft-node-everything-test/etc/heft-node-everything-test.api.md b/build-tests/heft-node-everything-test/etc/heft-node-everything-test.api.md index 73e6e6a967e..906e342ef52 100644 --- a/build-tests/heft-node-everything-test/etc/heft-node-everything-test.api.md +++ b/build-tests/heft-node-everything-test/etc/heft-node-everything-test.api.md @@ -4,6 +4,9 @@ ```ts +// @public +export const templateContent: string; + // @public (undocumented) export class TestClass { } diff --git a/build-tests/heft-node-everything-test/package.json b/build-tests/heft-node-everything-test/package.json index bd60e9a4a55..fcf92f04c29 100644 --- a/build-tests/heft-node-everything-test/package.json +++ b/build-tests/heft-node-everything-test/package.json @@ -16,6 +16,7 @@ "@microsoft/api-extractor": "workspace:*", "@rushstack/heft": "workspace:*", "@rushstack/heft-api-extractor-plugin": "workspace:*", + "@rushstack/heft-static-asset-typings-plugin": "workspace:*", "@rushstack/heft-jest-plugin": "workspace:*", "@rushstack/heft-lint-plugin": "workspace:*", "@rushstack/heft-typescript-plugin": "workspace:*", diff --git a/build-tests/heft-node-everything-test/src/exampleTemplate.html b/build-tests/heft-node-everything-test/src/exampleTemplate.html new file mode 100644 index 00000000000..bba457469cc --- /dev/null +++ b/build-tests/heft-node-everything-test/src/exampleTemplate.html @@ -0,0 +1,9 @@ + + + + Example Template + + +

Hello, world!

+ + diff --git a/build-tests/heft-node-everything-test/src/index.ts b/build-tests/heft-node-everything-test/src/index.ts index 659610ef84f..22b50eb31b1 100644 --- a/build-tests/heft-node-everything-test/src/index.ts +++ b/build-tests/heft-node-everything-test/src/index.ts @@ -1,6 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import exampleTemplate from './exampleTemplate.html'; + +/** + * The content of exampleTemplate.html, imported as a text asset. + * @public + */ +export const templateContent: string = exampleTemplate; + /** * @public */ diff --git a/build-tests/heft-node-everything-test/src/test/ExampleTest.test.ts b/build-tests/heft-node-everything-test/src/test/ExampleTest.test.ts index ccae242d321..26fa4ae4bb1 100644 --- a/build-tests/heft-node-everything-test/src/test/ExampleTest.test.ts +++ b/build-tests/heft-node-everything-test/src/test/ExampleTest.test.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { templateContent } from '../index'; + interface IInterface { element: string; } @@ -20,4 +22,10 @@ describe('Example Test', () => { }; expect(interfaceInstance).toBeTruthy(); }); + + it('Correctly imports text assets', () => { + expect(typeof templateContent).toBe('string'); + expect(templateContent).toContain('Example Template'); + expect(templateContent).toContain('Hello, world!'); + }); }); diff --git a/build-tests/heft-node-everything-test/tsconfig.json b/build-tests/heft-node-everything-test/tsconfig.json index d08637a6a20..f5ce1bd1dcc 100644 --- a/build-tests/heft-node-everything-test/tsconfig.json +++ b/build-tests/heft-node-everything-test/tsconfig.json @@ -5,6 +5,7 @@ "outDir": "lib-commonjs", "declarationDir": "lib-dts", "rootDir": "src", + "rootDirs": ["src", "temp/text-typings"], "forceConsistentCasingInFileNames": true, "jsx": "react", diff --git a/build-tests/heft-rspack-everything-test/config/heft.json b/build-tests/heft-rspack-everything-test/config/heft.json index 7d826b05556..9221aa19e99 100644 --- a/build-tests/heft-rspack-everything-test/config/heft.json +++ b/build-tests/heft-rspack-everything-test/config/heft.json @@ -10,7 +10,21 @@ "cleanFiles": [{ "includeGlobs": ["dist", "lib", "lib-commonjs"] }], "tasksByName": { + "image-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "resource-assets-plugin", + "options": { + "configType": "inline", + "config": { + "fileExtensions": [".png"], + "generatedTsFolders": ["temp/image-typings"] + } + } + } + }, "typescript": { + "taskDependencies": ["image-typings"], "taskPlugin": { "pluginPackage": "@rushstack/heft-typescript-plugin" } diff --git a/build-tests/heft-rspack-everything-test/config/rush-project.json b/build-tests/heft-rspack-everything-test/config/rush-project.json index 1c0d7da9a5d..f1de027644c 100644 --- a/build-tests/heft-rspack-everything-test/config/rush-project.json +++ b/build-tests/heft-rspack-everything-test/config/rush-project.json @@ -4,7 +4,7 @@ "operationSettings": [ { "operationName": "_phase:build", - "outputFolderNames": ["lib-esm", "lib-commonjs", "dist"] + "outputFolderNames": ["lib-esm", "lib-commonjs", "dist", "temp/image-typings"] }, { "operationName": "_phase:test", diff --git a/build-tests/heft-rspack-everything-test/package.json b/build-tests/heft-rspack-everything-test/package.json index 081fc7ac997..9ef1796e971 100644 --- a/build-tests/heft-rspack-everything-test/package.json +++ b/build-tests/heft-rspack-everything-test/package.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@rushstack/heft-dev-cert-plugin": "workspace:*", + "@rushstack/heft-static-asset-typings-plugin": "workspace:*", "@rushstack/heft-jest-plugin": "workspace:*", "@rushstack/heft-lint-plugin": "workspace:*", "@rushstack/heft-typescript-plugin": "workspace:*", diff --git a/build-tests/heft-rspack-everything-test/src/chunks/image.d.png.ts b/build-tests/heft-rspack-everything-test/src/chunks/image.d.png.ts deleted file mode 100644 index f38a285dfd9..00000000000 --- a/build-tests/heft-rspack-everything-test/src/chunks/image.d.png.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -declare const path: string; - -export default path; diff --git a/build-tests/heft-rspack-everything-test/tsconfig.json b/build-tests/heft-rspack-everything-test/tsconfig.json index e36104a9897..619397b3a75 100644 --- a/build-tests/heft-rspack-everything-test/tsconfig.json +++ b/build-tests/heft-rspack-everything-test/tsconfig.json @@ -3,9 +3,8 @@ "compilerOptions": { "outDir": "lib-esm", - "rootDir": "src", + "rootDirs": ["src", "temp/image-typings"], - "allowArbitraryExtensions": true, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, diff --git a/build-tests/heft-webpack4-everything-test/config/heft.json b/build-tests/heft-webpack4-everything-test/config/heft.json index 2f7ae591fd6..e90bf3d3c74 100644 --- a/build-tests/heft-webpack4-everything-test/config/heft.json +++ b/build-tests/heft-webpack4-everything-test/config/heft.json @@ -10,7 +10,18 @@ "cleanFiles": [{ "includeGlobs": ["dist", "lib", "lib-commonjs"] }], "tasksByName": { + "image-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "resource-assets-plugin", + "options": { + "configType": "file", + "configFileName": "resource-assets.json" + } + } + }, "typescript": { + "taskDependencies": ["image-typings"], "taskPlugin": { "pluginPackage": "@rushstack/heft-typescript-plugin" } diff --git a/build-tests/heft-webpack4-everything-test/config/resource-assets.json b/build-tests/heft-webpack4-everything-test/config/resource-assets.json new file mode 100644 index 00000000000..09dfe4998d3 --- /dev/null +++ b/build-tests/heft-webpack4-everything-test/config/resource-assets.json @@ -0,0 +1,4 @@ +{ + "fileExtensions": [".png"], + "generatedTsFolders": ["temp/image-typings"] +} diff --git a/build-tests/heft-webpack4-everything-test/config/rush-project.json b/build-tests/heft-webpack4-everything-test/config/rush-project.json index 1c0d7da9a5d..f1de027644c 100644 --- a/build-tests/heft-webpack4-everything-test/config/rush-project.json +++ b/build-tests/heft-webpack4-everything-test/config/rush-project.json @@ -4,7 +4,7 @@ "operationSettings": [ { "operationName": "_phase:build", - "outputFolderNames": ["lib-esm", "lib-commonjs", "dist"] + "outputFolderNames": ["lib-esm", "lib-commonjs", "dist", "temp/image-typings"] }, { "operationName": "_phase:test", diff --git a/build-tests/heft-webpack4-everything-test/package.json b/build-tests/heft-webpack4-everything-test/package.json index affffe9d6f7..0742ce3648d 100644 --- a/build-tests/heft-webpack4-everything-test/package.json +++ b/build-tests/heft-webpack4-everything-test/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@rushstack/heft-dev-cert-plugin": "workspace:*", + "@rushstack/heft-static-asset-typings-plugin": "workspace:*", "@rushstack/heft-jest-plugin": "workspace:*", "@rushstack/heft-lint-plugin": "workspace:*", "@rushstack/heft-typescript-plugin": "workspace:*", diff --git a/build-tests/heft-webpack4-everything-test/src/chunks/ChunkClass.ts b/build-tests/heft-webpack4-everything-test/src/chunks/ChunkClass.ts index ddbf7d148c7..50e541f593d 100644 --- a/build-tests/heft-webpack4-everything-test/src/chunks/ChunkClass.ts +++ b/build-tests/heft-webpack4-everything-test/src/chunks/ChunkClass.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import image from './image.png'; + export class ChunkClass { public doStuff(): void { // eslint-disable-next-line no-console @@ -8,7 +10,6 @@ export class ChunkClass { } public getImageUrl(): string { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('./image.png'); + return image; } } diff --git a/build-tests/heft-webpack4-everything-test/tsconfig.json b/build-tests/heft-webpack4-everything-test/tsconfig.json index 2b5b7ac9673..ebef84f93fc 100644 --- a/build-tests/heft-webpack4-everything-test/tsconfig.json +++ b/build-tests/heft-webpack4-everything-test/tsconfig.json @@ -3,9 +3,11 @@ "compilerOptions": { "outDir": "lib-esm", - "rootDir": "src", + "rootDirs": ["src", "temp/image-typings"], "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "jsx": "react", "declaration": true, "sourceMap": true, diff --git a/build-tests/heft-webpack5-everything-test/config/heft.json b/build-tests/heft-webpack5-everything-test/config/heft.json index db369f8f99d..319963689d5 100644 --- a/build-tests/heft-webpack5-everything-test/config/heft.json +++ b/build-tests/heft-webpack5-everything-test/config/heft.json @@ -10,7 +10,21 @@ "cleanFiles": [{ "includeGlobs": ["dist", "lib", "lib-commonjs"] }], "tasksByName": { + "image-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "resource-assets-plugin", + "options": { + "configType": "inline", + "config": { + "fileExtensions": [".png"], + "generatedTsFolders": ["temp/image-typings"] + } + } + } + }, "typescript": { + "taskDependencies": ["image-typings"], "taskPlugin": { "pluginPackage": "@rushstack/heft-typescript-plugin" } diff --git a/build-tests/heft-webpack5-everything-test/config/rush-project.json b/build-tests/heft-webpack5-everything-test/config/rush-project.json index 1c0d7da9a5d..f1de027644c 100644 --- a/build-tests/heft-webpack5-everything-test/config/rush-project.json +++ b/build-tests/heft-webpack5-everything-test/config/rush-project.json @@ -4,7 +4,7 @@ "operationSettings": [ { "operationName": "_phase:build", - "outputFolderNames": ["lib-esm", "lib-commonjs", "dist"] + "outputFolderNames": ["lib-esm", "lib-commonjs", "dist", "temp/image-typings"] }, { "operationName": "_phase:test", diff --git a/build-tests/heft-webpack5-everything-test/package.json b/build-tests/heft-webpack5-everything-test/package.json index 204dc75de8c..3e891b7778c 100644 --- a/build-tests/heft-webpack5-everything-test/package.json +++ b/build-tests/heft-webpack5-everything-test/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@rushstack/heft-dev-cert-plugin": "workspace:*", + "@rushstack/heft-static-asset-typings-plugin": "workspace:*", "@rushstack/heft-jest-plugin": "workspace:*", "@rushstack/heft-lint-plugin": "workspace:*", "@rushstack/heft-typescript-plugin": "workspace:*", diff --git a/build-tests/heft-webpack5-everything-test/src/chunks/ChunkClass.ts b/build-tests/heft-webpack5-everything-test/src/chunks/ChunkClass.ts index ddbf7d148c7..50e541f593d 100644 --- a/build-tests/heft-webpack5-everything-test/src/chunks/ChunkClass.ts +++ b/build-tests/heft-webpack5-everything-test/src/chunks/ChunkClass.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import image from './image.png'; + export class ChunkClass { public doStuff(): void { // eslint-disable-next-line no-console @@ -8,7 +10,6 @@ export class ChunkClass { } public getImageUrl(): string { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('./image.png'); + return image; } } diff --git a/build-tests/heft-webpack5-everything-test/src/chunks/image.d.png.ts b/build-tests/heft-webpack5-everything-test/src/chunks/image.d.png.ts deleted file mode 100644 index f38a285dfd9..00000000000 --- a/build-tests/heft-webpack5-everything-test/src/chunks/image.d.png.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -declare const path: string; - -export default path; diff --git a/build-tests/heft-webpack5-everything-test/tsconfig.json b/build-tests/heft-webpack5-everything-test/tsconfig.json index e3c0ce2496a..1707aad311e 100644 --- a/build-tests/heft-webpack5-everything-test/tsconfig.json +++ b/build-tests/heft-webpack5-everything-test/tsconfig.json @@ -3,9 +3,8 @@ "compilerOptions": { "outDir": "lib-esm", - "rootDir": "src", + "rootDirs": ["src", "temp/image-typings"], - "allowArbitraryExtensions": true, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, diff --git a/common/changes/@rushstack/heft-static-asset-typings-plugin/main_2026-02-23-21-40.json b/common/changes/@rushstack/heft-static-asset-typings-plugin/main_2026-02-23-21-40.json new file mode 100644 index 00000000000..5b137c4bf1d --- /dev/null +++ b/common/changes/@rushstack/heft-static-asset-typings-plugin/main_2026-02-23-21-40.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-static-asset-typings-plugin", + "comment": "Initial release.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-static-asset-typings-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-web-rig/heft-image-typings-generator-plugin_2026-02-23-22-01.json b/common/changes/@rushstack/heft-web-rig/heft-image-typings-generator-plugin_2026-02-23-22-01.json new file mode 100644 index 00000000000..51e93336e7b --- /dev/null +++ b/common/changes/@rushstack/heft-web-rig/heft-image-typings-generator-plugin_2026-02-23-22-01.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-web-rig", + "comment": "Add `@rushstack/heft-static-asset-typings-plugin` to the `app` and `library` profiles, generating `.d.ts` typings for static asset files (images and text). This enables type-safe asset imports in TypeScript without needing `allowArbitraryExtensions` or `require()` calls. Also add `.webp` and `.avif` to the webpack asset resource rule.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft-web-rig" +} \ 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 3f6756df1a8..4eb4bb89b8d 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -198,6 +198,10 @@ "name": "@rushstack/heft-dev-cert-plugin", "allowedCategories": [ "libraries", "tests" ] }, + { + "name": "@rushstack/heft-static-asset-typings-plugin", + "allowedCategories": [ "libraries", "tests" ] + }, { "name": "@rushstack/heft-isolated-typescript-transpile-plugin", "allowedCategories": [ "tests" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index b992ec346ba..da03c0d6e70 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -1950,6 +1950,9 @@ importers: '@rushstack/heft-lint-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-static-asset-typings-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-static-asset-typings-plugin '@rushstack/heft-typescript-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-typescript-plugin @@ -1995,6 +1998,9 @@ importers: '@rushstack/heft-lint-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-static-asset-typings-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-static-asset-typings-plugin '@rushstack/heft-typescript-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-typescript-plugin @@ -2104,6 +2110,9 @@ importers: '@rushstack/heft-rspack-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-rspack-plugin + '@rushstack/heft-static-asset-typings-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-static-asset-typings-plugin '@rushstack/heft-typescript-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-typescript-plugin @@ -2414,6 +2423,9 @@ importers: '@rushstack/heft-lint-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-static-asset-typings-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-static-asset-typings-plugin '@rushstack/heft-typescript-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-typescript-plugin @@ -2474,6 +2486,9 @@ importers: '@rushstack/heft-lint-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-static-asset-typings-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-static-asset-typings-plugin '@rushstack/heft-typescript-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-typescript-plugin @@ -3467,6 +3482,31 @@ importers: specifier: workspace:* version: link:../../rigs/local-node-rig + ../../../heft-plugins/heft-static-asset-typings-plugin: + dependencies: + '@rushstack/heft-config-file': + specifier: workspace:* + version: link:../../libraries/heft-config-file + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../../libraries/node-core-library + '@rushstack/terminal': + specifier: workspace:* + version: link:../../libraries/terminal + '@rushstack/typings-generator': + specifier: workspace:* + version: link:../../libraries/typings-generator + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + eslint: + specifier: ~9.37.0 + version: 9.37.0 + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../heft-plugins/heft-storybook-plugin: dependencies: '@rushstack/node-core-library': @@ -4719,6 +4759,9 @@ importers: '@rushstack/heft-sass-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-sass-plugin + '@rushstack/heft-static-asset-typings-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-static-asset-typings-plugin '@rushstack/heft-typescript-plugin': specifier: workspace:* version: link:../../heft-plugins/heft-typescript-plugin @@ -30145,7 +30188,7 @@ snapshots: eslint@8.57.1: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.37.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) '@eslint-community/regexpp': 4.12.2 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.57.1 diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 9b13d6b9d1a..dc8854a2b5e 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "425296840bc63395649600913e66041583bdc287", + "pnpmShrinkwrapHash": "ad46a0d8a791a22ba6d3c72e0637cdf782624f0f", "preferredVersionsHash": "93bf435032db8da4a18734f1eaa359c12ad147c1" } diff --git a/heft-plugins/heft-static-asset-typings-plugin/.npmignore b/heft-plugins/heft-static-asset-typings-plugin/.npmignore new file mode 100644 index 00000000000..ffb155d74e6 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/.npmignore @@ -0,0 +1,34 @@ +# 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. +# --------------------------------------------------------------------------- + +!/includes/** diff --git a/heft-plugins/heft-static-asset-typings-plugin/LICENSE b/heft-plugins/heft-static-asset-typings-plugin/LICENSE new file mode 100644 index 00000000000..9570e2a1a6f --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/LICENSE @@ -0,0 +1,24 @@ +@rushstack/heft-static-asset-typings-plugin + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/heft-plugins/heft-static-asset-typings-plugin/README.md b/heft-plugins/heft-static-asset-typings-plugin/README.md new file mode 100644 index 00000000000..e73795a4223 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/README.md @@ -0,0 +1,230 @@ +# @rushstack/heft-static-asset-typings-plugin + +This Heft plugin generates TypeScript `.d.ts` typings for static asset files, enabling type-safe +`import` statements for non-TypeScript files. It provides two task plugins: + +- **`resource-assets-plugin`** — Generates `.d.ts` typings for _resource_ files such as images (`.png`, + `.jpg`, `.svg`, etc.) and fonts. These are opaque binary blobs whose content is not meaningful to + JavaScript; the generated typing simply exports a default `string` representing the asset URL + (e.g. as resolved by a bundler's asset loader). + +- **`source-assets-plugin`** — Generates `.d.ts` typings _and_ JavaScript module output for _source_ + files (`.html`, `.css`, `.txt`, `.md`, etc.) whose textual content is consumed at runtime. The + generated JS modules read the file and re-export its content as a default `string`, making these + assets importable as ES modules. + +The terminology follows the [webpack convention](https://webpack.js.org/guides/asset-modules/) +where _resource_ assets are emitted as separate files referenced by URL, while _source_ assets are +inlined as strings. + +Both plugins support incremental and watch-mode builds. + +## Setup + +1. Add the plugin as a `devDependency` of your project: + + ```bash + rush add -p @rushstack/heft-static-asset-typings-plugin --dev + ``` + +2. Load the appropriate plugin(s) in your project's **config/heft.json**: + + ### Resource assets (images, fonts, etc.) + + **Inline configuration** — specify options directly in heft.json: + + ```jsonc + { + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "phasesByName": { + "build": { + "tasksByName": { + "image-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "resource-assets-plugin", + "options": { + "configType": "inline", + "config": { + "fileExtensions": [".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif"], + "generatedTsFolders": ["temp/image-typings"] + } + } + } + }, + "typescript": { + "taskDependencies": ["image-typings"] + // ... + } + } + } + } + } + ``` + + **File configuration** — load settings from a riggable config file: + + ```jsonc + { + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "phasesByName": { + "build": { + "tasksByName": { + "image-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "resource-assets-plugin", + "options": { + "configType": "file", + "configFileName": "resource-assets.json" + } + } + }, + "typescript": { + "taskDependencies": ["image-typings"] + // ... + } + } + } + } + } + ``` + + And create a **config/resource-assets.json** file (which can be provided by a rig): + + ```jsonc + { + "fileExtensions": [".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif"], + "generatedTsFolders": ["temp/image-typings"] + } + ``` + + ### Source assets + + **Inline configuration:** + + ```jsonc + { + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "phasesByName": { + "build": { + "tasksByName": { + "text-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "source-assets-plugin", + "options": { + "configType": "inline", + "config": { + "fileExtensions": [".html"], + "cjsOutputFolders": ["lib-commonjs"], + "esmOutputFolders": ["lib-esm"], + "generatedTsFolders": ["temp/text-typings"] + } + } + } + }, + "typescript": { + "taskDependencies": ["text-typings"] + // ... + } + } + } + } + } + ``` + + **File configuration:** + + ```jsonc + { + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "phasesByName": { + "build": { + "tasksByName": { + "text-typings": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "source-assets-plugin", + "options": { + "configType": "file", + "configFileName": "source-assets.json" + } + } + }, + "typescript": { + "taskDependencies": ["text-typings"] + // ... + } + } + } + } + } + ``` + + And create a **config/source-assets.json** file (which can be provided by a rig): + + ```jsonc + { + "fileExtensions": [".html"], + "cjsOutputFolders": ["lib-commonjs"], + "esmOutputFolders": ["lib-esm"], + "generatedTsFolders": ["temp/text-typings"] + } + ``` + +3. Add the generated typings folder to your **tsconfig.json** `rootDirs` so that + TypeScript can resolve the declarations: + + ```jsonc + { + "compilerOptions": { + "rootDirs": ["src", "temp/image-typings"] + } + } + ``` + +## Plugin options + +Both plugins support two configuration modes via the `configType` option: + +### Inline mode (`configType: "inline"`) + +Provide configuration directly in heft.json under `options.config`: + +#### `resource-assets-plugin` inline config + +| Option | Type | Default | Description | +| ------------------- | ---------- | ------------------------ | ----------------------------------------------- | +| `fileExtensions` | `string[]` | — | **(required)** File extensions to generate typings for. | +| `generatedTsFolders`| `string[]` | `["temp/static-asset-ts"]` | Folders where generated `.d.ts` files are written. The first entry should be listed in `rootDirs` so TypeScript can resolve the asset imports during type-checking. Additional entries are typically your project's published typings folder(s). | +| `sourceFolderPath` | `string` | `"src"` | Source folder to scan for asset files. | + +#### `source-assets-plugin` inline config + +Includes all the above, plus: + +| Option | Type | Default | Description | +| ------------------- | ---------- | ------------------------ | ---------------------------------------------------- | +| `cjsOutputFolders` | `string[]` | — | **(required)** Output folders for generated CommonJS `.js` modules. | +| `esmOutputFolders` | `string[]` | `[]` | Output folders for generated ESM `.js` modules. | + +### File mode (`configType: "file"`) + +Load configuration from a riggable JSON config file in the project's `config/` folder: + +| Option | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------------- | +| `configFileName` | `string` | **(required)** Name of the JSON config file in the `config/` folder. | + +The config file supports the same properties as inline mode (see tables above). Config files +can be provided by a rig, making file mode ideal for shared build configurations. + +## Links + +- [CHANGELOG.md]( + https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-static-asset-typings-plugin/CHANGELOG.md) - Find + out what's new in the latest version +- [@rushstack/heft](https://www.npmjs.com/package/@rushstack/heft) - Heft is a config-driven toolchain that invokes popular tools such as TypeScript, ESLint, Jest, Webpack, and API Extractor. + +Heft is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/heft-plugins/heft-static-asset-typings-plugin/config/heft.json b/heft-plugins/heft-static-asset-typings-plugin/config/heft.json new file mode 100644 index 00000000000..0e52387039a --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/config/heft.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "extends": "local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "copy-json-schemas": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "copy-files-plugin", + "options": { + "copyOperations": [ + { + "sourcePath": "src/schemas", + "destinationFolders": ["temp/json-schemas/heft/v1"], + "fileExtensions": [".schema.json"], + "hardlink": true + } + ] + } + } + } + } + } + } +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/config/jest.config.json b/heft-plugins/heft-static-asset-typings-plugin/config/jest.config.json new file mode 100644 index 00000000000..d1749681d90 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/config/jest.config.json @@ -0,0 +1,3 @@ +{ + "extends": "local-node-rig/profiles/default/config/jest.config.json" +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/config/rig.json b/heft-plugins/heft-static-asset-typings-plugin/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/heft-plugins/heft-static-asset-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-static-asset-typings-plugin/eslint.config.js b/heft-plugins/heft-static-asset-typings-plugin/eslint.config.js new file mode 100644 index 00000000000..c15e6077310 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/eslint.config.js @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeTrustedToolProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); +const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); + +module.exports = [ + ...nodeTrustedToolProfile, + ...friendlyLocalsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + } + } +]; diff --git a/heft-plugins/heft-static-asset-typings-plugin/heft-plugin.json b/heft-plugins/heft-static-asset-typings-plugin/heft-plugin.json new file mode 100644 index 00000000000..73e8a17518f --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/heft-plugin.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", + + "taskPlugins": [ + { + "pluginName": "resource-assets-plugin", + "entryPoint": "./lib-commonjs/ResourceAssetsPlugin", + "optionsSchema": "./lib-commonjs/schemas/resource-assets-options.schema.json" + }, + { + "pluginName": "source-assets-plugin", + "entryPoint": "./lib-commonjs/SourceAssetsPlugin", + "optionsSchema": "./lib-commonjs/schemas/source-assets-options.schema.json" + } + ] +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/package.json b/heft-plugins/heft-static-asset-typings-plugin/package.json new file mode 100644 index 00000000000..ed0af701c28 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/package.json @@ -0,0 +1,50 @@ +{ + "name": "@rushstack/heft-static-asset-typings-plugin", + "version": "0.0.0", + "description": "A Heft plugin that generates TypeScript typings for static asset files such as images and text files.", + "scripts": { + "build": "heft build --clean", + "start": "heft test --clean --watch", + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rushstack.git", + "directory": "heft-plugins/heft-static-asset-typings-plugin" + }, + "homepage": "https://rushstack.io/pages/heft/overview/", + "exports": { + "./lib/*.schema.json": "./lib-commonjs/*.schema.json", + "./lib/*": { + "types": "./lib-dts/*.d.ts", + "node": "./lib-commonjs/*.js", + "import": "./lib-esm/*.js", + "require": "./lib-commonjs/*.js" + }, + "./heft-plugin.json": "./heft-plugin.json", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "lib/*": [ + "lib-dts/*" + ] + } + }, + "license": "MIT", + "peerDependencies": { + "@rushstack/heft": "^1.2.3" + }, + "dependencies": { + "@rushstack/heft-config-file": "workspace:*", + "@rushstack/node-core-library": "workspace:*", + "@rushstack/terminal": "workspace:*", + "@rushstack/typings-generator": "workspace:*" + }, + "devDependencies": { + "@rushstack/heft": "workspace:*", + "eslint": "~9.37.0", + "local-node-rig": "workspace:*" + } +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/ResourceAssetsPlugin.ts b/heft-plugins/heft-static-asset-typings-plugin/src/ResourceAssetsPlugin.ts new file mode 100644 index 00000000000..253f625fea6 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/ResourceAssetsPlugin.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { HeftConfiguration, IHeftTaskSession, IHeftTaskPlugin } from '@rushstack/heft'; + +import { + createTypingsGeneratorAsync, + tryGetConfigFromPluginOptionsAsync, + type IRunGeneratorOptions, + type IStaticAssetGeneratorOptions, + type IStaticAssetTypingsGenerator +} from './StaticAssetTypingsGenerator'; +import type { IAssetPluginOptions, IResourceStaticAssetTypingsConfigurationJson } from './types'; + +const PLUGIN_NAME: 'static-asset-typings-plugin' = 'static-asset-typings-plugin'; + +export default class ResourceAssetsPlugin + implements IHeftTaskPlugin> +{ + /** + * Generate typings for text files before TypeScript compilation. + */ + public apply( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + options: IAssetPluginOptions + ): void { + const { slashNormalizedBuildFolderPath, rigConfig } = heftConfiguration; + const staticAssetGeneratorOptions: IStaticAssetGeneratorOptions = { + tryGetConfigAsync: async (terminal) => { + return await tryGetConfigFromPluginOptionsAsync( + terminal, + slashNormalizedBuildFolderPath, + rigConfig, + options, + 'resource' + ); + }, + slashNormalizedBuildFolderPath, + getVersionAndEmitOutputFilesAsync: async () => 'versionless' + }; + + let generatorPromise: Promise | undefined; + + async function createAndRunGeneratorAsync(runOptions: IRunGeneratorOptions): Promise { + if (generatorPromise === undefined) { + generatorPromise = createTypingsGeneratorAsync(taskSession, staticAssetGeneratorOptions); + } + + const generator: IStaticAssetTypingsGenerator | false = await generatorPromise; + if (generator === false) { + return; + } + + await generator.runIncrementalAsync(runOptions); + } + + taskSession.hooks.run.tapPromise(PLUGIN_NAME, createAndRunGeneratorAsync); + taskSession.hooks.runIncremental.tapPromise(PLUGIN_NAME, createAndRunGeneratorAsync); + } +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/SourceAssetsPlugin.ts b/heft-plugins/heft-static-asset-typings-plugin/src/SourceAssetsPlugin.ts new file mode 100644 index 00000000000..edc1b09cf2a --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/SourceAssetsPlugin.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { createHash } from 'node:crypto'; + +import type { HeftConfiguration, IHeftTaskSession, IHeftTaskPlugin } from '@rushstack/heft'; +import { Async, FileSystem } from '@rushstack/node-core-library'; + +import { + createTypingsGeneratorAsync, + tryGetConfigFromPluginOptionsAsync, + type IRunGeneratorOptions, + type IStaticAssetGeneratorOptions, + type IStaticAssetTypingsGenerator +} from './StaticAssetTypingsGenerator'; +import type { IAssetPluginOptions, ISourceStaticAssetTypingsConfigurationJson } from './types'; + +const PLUGIN_NAME: 'source-assets-plugin' = 'source-assets-plugin'; + +// Pre-allocated preamble/postamble buffers to avoid repeated allocations. +// Used with FileSystem.writeBuffersToFileAsync (writev) for efficient output. +const CJS_PREAMBLE: Buffer = Buffer.from( + '"use strict"\nObject.defineProperty(exports, "__esModule", { value: true });\nvar content = ' +); +const CJS_POSTAMBLE: Buffer = Buffer.from(';\nexports.default = content;\n'); +const ESM_PREAMBLE: Buffer = Buffer.from('const content = '); +const ESM_POSTAMBLE: Buffer = Buffer.from(';\nexport default content;\n'); + +export default class SourceAssetsPlugin + implements IHeftTaskPlugin> +{ + /** + * Generate typings for text files before TypeScript compilation. + */ + public apply( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + pluginOptions: IAssetPluginOptions + ): void { + let generatorPromise: Promise | undefined; + + async function initializeGeneratorAsync(): Promise { + const { slashNormalizedBuildFolderPath, rigConfig } = heftConfiguration; + + const options: ISourceStaticAssetTypingsConfigurationJson | undefined = + await tryGetConfigFromPluginOptionsAsync( + taskSession.logger.terminal, + slashNormalizedBuildFolderPath, + rigConfig, + pluginOptions, + 'source' + ); + + if (options) { + const { fileExtensions, sourceFolderPath, generatedTsFolders, cjsOutputFolders, esmOutputFolders } = + options; + + const resolvedCjsOutputFolders: string[] = cjsOutputFolders.map( + (jsPath) => `${slashNormalizedBuildFolderPath}/${jsPath}` + ); + const resolvedEsmOutputFolders: string[] = + esmOutputFolders?.map((jsPath) => `${slashNormalizedBuildFolderPath}/${jsPath}`) ?? []; + const jsOutputFolders: string[] = [...resolvedCjsOutputFolders, ...resolvedEsmOutputFolders]; + + function getAdditionalOutputFiles(relativePath: string): string[] { + return jsOutputFolders.map((folder) => `${folder}/${relativePath}.js`); + } + + async function getVersionAndEmitOutputFilesAsync( + filePath: string, + relativePath: string, + oldVersion: string | undefined + ): Promise { + const fileContents: Buffer = await FileSystem.readFileToBufferAsync(filePath); + const fileVersion: string = createHash('sha1').update(fileContents).digest('base64'); + if (fileVersion === oldVersion) { + return; + } + + const stringFileContents: string = fileContents.toString('utf8'); + + const contentBuffer: Buffer = Buffer.from(JSON.stringify(stringFileContents)); + + const outputs: { path: string; buffers: NodeJS.ArrayBufferView[] }[] = []; + for (const folder of resolvedCjsOutputFolders) { + outputs.push({ + path: `${folder}/${relativePath}.js`, + buffers: [CJS_PREAMBLE, contentBuffer, CJS_POSTAMBLE] + }); + } + for (const folder of resolvedEsmOutputFolders) { + outputs.push({ + path: `${folder}/${relativePath}.js`, + buffers: [ESM_PREAMBLE, contentBuffer, ESM_POSTAMBLE] + }); + } + + await Async.forEachAsync( + outputs, + async ({ path, buffers }) => { + await FileSystem.writeBuffersToFileAsync(path, buffers, { ensureFolderExists: true }); + }, + { concurrency: 10 } + ); + + return fileVersion; + } + + const staticAssetGeneratorOptions: IStaticAssetGeneratorOptions = { + tryGetConfigAsync: async () => { + return { + fileExtensions, + sourceFolderPath, + generatedTsFolders + }; + }, + slashNormalizedBuildFolderPath, + getAdditionalOutputFiles, + getVersionAndEmitOutputFilesAsync + }; + + return createTypingsGeneratorAsync(taskSession, staticAssetGeneratorOptions); + } else { + return false; + } + } + + async function createAndRunGeneratorAsync(runOptions: IRunGeneratorOptions): Promise { + if (generatorPromise === undefined) { + generatorPromise = initializeGeneratorAsync(); + } + + const generator: IStaticAssetTypingsGenerator | false = await generatorPromise; + if (generator === false) { + return; + } + + await generator.runIncrementalAsync(runOptions); + } + + taskSession.hooks.run.tapPromise(PLUGIN_NAME, createAndRunGeneratorAsync); + taskSession.hooks.runIncremental.tapPromise(PLUGIN_NAME, createAndRunGeneratorAsync); + } +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/StaticAssetTypingsGenerator.ts b/heft-plugins/heft-static-asset-typings-plugin/src/StaticAssetTypingsGenerator.ts new file mode 100644 index 00000000000..41fc438f00b --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/StaticAssetTypingsGenerator.ts @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { createHash } from 'node:crypto'; + +import type { + HeftConfiguration, + IHeftTaskRunHookOptions, + IHeftTaskRunIncrementalHookOptions, + IHeftTaskSession, + IWatchedFileState +} from '@rushstack/heft'; +import { TypingsGenerator } from '@rushstack/typings-generator'; +import { FileSystem, Sort } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; + +import type { + IAssetPluginOptions, + IResourceStaticAssetTypingsConfigurationJson, + ISourceStaticAssetTypingsConfigurationJson, + StaticAssetConfigurationFileLoader +} from './types'; + +// Use explicit \n to avoid platform-dependent line endings in template literals. +const DECLARATION: string = [ + '/**', + ' * @public', + ' */', + 'declare const content: string;', + 'export default content;', + '' +].join('\n'); + +// Include a hash of DECLARATION so the cache is invalidated if the declaration content changes. +const PLUGIN_VERSION: string = `1-${createHash('sha1').update(DECLARATION).digest('hex').slice(0, 8)}`; + +/** + * Options for constructing a static asset typings generator + */ +export interface IStaticAssetGeneratorOptions { + /** + * A getter for the loader for the riggable config file in the project. + */ + tryGetConfigAsync: StaticAssetConfigurationFileLoader; + + /** + * The path to the build folder, normalized to use forward slashes as the directory separator. + */ + slashNormalizedBuildFolderPath: string; + + /** + * @param relativePath - The relative path of the file to get additional output files for. + * @returns An array of output file names. + */ + getAdditionalOutputFiles?: (relativePath: string) => string[]; + + /** + * @param relativePath - The relative path of the file being processed. + * @param filePath - The absolute path of the file being processed. + * @param oldVersion - The old version of the file, if any. + * @returns The new version of the file, if emit should occur. + */ + getVersionAndEmitOutputFilesAsync: ( + relativePath: string, + filePath: string, + oldVersion: string | undefined + ) => Promise; +} + +export type IRunGeneratorOptions = IHeftTaskRunHookOptions & + Partial>; + +export interface IStaticAssetTypingsGenerator { + /** + * Runs this generator in incremental mode. + * + * @param runOptions - The task run hook options from Heft + * @returns A promise that resolves when the generator has finished processing. + */ + runIncrementalAsync: (runOptions: IRunGeneratorOptions) => Promise; +} + +interface IStaticAssetTypingsBuildInfoFile { + fileVersions: [string, string][]; + pluginVersion: string; +} + +export async function tryGetConfigFromPluginOptionsAsync( + terminal: ITerminal, + buildFolder: string, + rigConfig: HeftConfiguration['rigConfig'], + options: IAssetPluginOptions, + type: 'resource' +): Promise; +export async function tryGetConfigFromPluginOptionsAsync( + terminal: ITerminal, + buildFolder: string, + rigConfig: HeftConfiguration['rigConfig'], + options: IAssetPluginOptions, + type: 'source' +): Promise; +export async function tryGetConfigFromPluginOptionsAsync( + terminal: ITerminal, + buildFolder: string, + rigConfig: HeftConfiguration['rigConfig'], + options: IAssetPluginOptions< + IResourceStaticAssetTypingsConfigurationJson | ISourceStaticAssetTypingsConfigurationJson + >, + type: import('./getConfigFromConfigFileAsync').FileLoaderType +): Promise< + IResourceStaticAssetTypingsConfigurationJson | ISourceStaticAssetTypingsConfigurationJson | undefined +> { + if (options?.configType === 'inline') { + return options.config; + } else { + const { getConfigFromConfigFileAsync } = await import('./getConfigFromConfigFileAsync'); + const { configFileName } = options; + return getConfigFromConfigFileAsync(configFileName, type, terminal, buildFolder, rigConfig); + } +} + +/** + * Constructs a typings generator for processing static assets + * + * @param taskSession - The Heft task session + * @param rigConfig - The Heft configuration + * @param options - Options for the generator + * @returns + */ +export async function createTypingsGeneratorAsync( + taskSession: IHeftTaskSession, + options: IStaticAssetGeneratorOptions +): Promise { + const { tryGetConfigAsync, slashNormalizedBuildFolderPath } = options; + + const { terminal } = taskSession.logger; + + const configuration: IResourceStaticAssetTypingsConfigurationJson | undefined = + await tryGetConfigAsync(terminal); + + if (!configuration) { + return false; + } + + const { + generatedTsFolders = ['temp/static-asset-ts'], + sourceFolderPath = 'src', + fileExtensions + } = configuration; + const resolvedGeneratedTsFolders: string[] | undefined = generatedTsFolders.map( + (folder) => `${slashNormalizedBuildFolderPath}/${folder}` + ); + const [generatedTsFolder, ...secondaryGeneratedTsFolders] = resolvedGeneratedTsFolders; + + const { getAdditionalOutputFiles, getVersionAndEmitOutputFilesAsync } = options; + + const fileVersions: Map = new Map(); + + const typingsGenerator: TypingsGenerator = new TypingsGenerator({ + srcFolder: `${slashNormalizedBuildFolderPath}/${sourceFolderPath}`, + generatedTsFolder, + secondaryGeneratedTsFolders, + fileExtensions, + terminal, + // eslint-disable-next-line @typescript-eslint/naming-convention + parseAndGenerateTypings: async ( + fileContents: boolean, + filePath: string, + relativePath: string + ): Promise => { + const oldFileVersion: string | undefined = fileVersions.get(relativePath); + const fileVersion: string | undefined = await getVersionAndEmitOutputFilesAsync( + filePath, + relativePath, + oldFileVersion + ); + if (fileVersion === undefined) { + return; + } + + fileVersions.set(relativePath, fileVersion); + if (oldFileVersion) { + // Since DECLARATION is constant, no point re-emitting the declarations just because the input content changed. + return; + } + + return DECLARATION; + }, + readFile: (filePath: string, relativePath: string): boolean => { + return false; + }, + getAdditionalOutputFiles + }); + + // TODO: Heft has an internal incremental cache layer (IncrementalBuildInfo) used by built-in + // plugins like CopyFilesPlugin. It is not currently part of Heft's public API surface. If it + // becomes public, we should migrate to it instead of managing our own cache file. + const cacheFilePath: string = `${taskSession.tempFolderPath}/static-assets.json`; + try { + const cacheFileContent: string = await FileSystem.readFileAsync(cacheFilePath); + const oldCacheFile: IStaticAssetTypingsBuildInfoFile = JSON.parse(cacheFileContent); + if (oldCacheFile.pluginVersion === PLUGIN_VERSION) { + for (const [relativePath, version] of oldCacheFile.fileVersions) { + fileVersions.set(relativePath, version); + } + } + } catch (e) { + terminal.writeVerboseLine(`Failed to read cache file: ${e}`); + } + + return { + async runIncrementalAsync(runOptions: IRunGeneratorOptions): Promise { + await runTypingsGeneratorIncrementalAsync( + taskSession, + typingsGenerator, + cacheFilePath, + fileVersions, + runOptions + ); + } + }; +} + +/** + * Invokes the specified typings generator on any files changed since the last invocation. + * If the cache file has been deleted (e.g. via a `--clean` run), will process all files. + * + * @param taskSession - The Heft task session. + * @param typingsGenerator - The typings generator to invoke. + * @param cacheFilePath - The path to the file that will contain the last build file version metadata. + * @param fileVersions - The map of current file versions. + * @param heftRunOptions - The task options from Heft. + * @returns A promise that resolves when the typings generator has finished processing. + */ +async function runTypingsGeneratorIncrementalAsync( + taskSession: IHeftTaskSession, + typingsGenerator: TypingsGenerator, + cacheFilePath: string, + fileVersions: Map, + heftRunOptions: IRunGeneratorOptions +): Promise { + const { terminal } = taskSession.logger; + + const originalFileVersions: ReadonlyMap = new Map(fileVersions); + + // 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; + const { watchGlobAsync } = heftRunOptions as IHeftTaskRunIncrementalHookOptions; + if (watchGlobAsync) { + changedRelativeFilePaths = []; + const relativeFilePaths: Map = await 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('Processing static assets...'); + await typingsGenerator.generateTypingsAsync(changedRelativeFilePaths); + + if (hasChanges(fileVersions, originalFileVersions)) { + const fileVersionsArray: [string, string][] = Array.from(fileVersions); + Sort.sortBy(fileVersionsArray, ([relativePath]) => relativePath); + + const buildFile: IStaticAssetTypingsBuildInfoFile = { + fileVersions: fileVersionsArray, + pluginVersion: PLUGIN_VERSION + }; + await FileSystem.writeFileAsync(cacheFilePath, JSON.stringify(buildFile), { ensureFolderExists: true }); + } + terminal.writeLine('Finished processing static assets.'); +} + +/** + * @internal + * Returns true if the current map has different entries than the old map. + */ +export function hasChanges(current: ReadonlyMap, old: ReadonlyMap): boolean { + if (current.size !== old.size) { + return true; + } + + for (const [key, value] of current) { + if (old.get(key) !== value) { + return true; + } + } + + return false; +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/getConfigFromConfigFileAsync.ts b/heft-plugins/heft-static-asset-typings-plugin/src/getConfigFromConfigFileAsync.ts new file mode 100644 index 00000000000..c25341890c4 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/getConfigFromConfigFileAsync.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { HeftConfiguration } from '@rushstack/heft'; +import { InheritanceType, ProjectConfigurationFile } from '@rushstack/heft-config-file'; +import type { ITerminal } from '@rushstack/terminal'; + +import type { + IResourceStaticAssetTypingsConfigurationJson, + ISourceStaticAssetTypingsConfigurationJson +} from './types'; +import resourceStaticAssetSchema from './schemas/resource-static-asset-typings.schema.json'; +import sourceStaticAssetSchema from './schemas/source-static-asset-typings.schema.json'; + +const configurationFileLoaderByFileName: Map< + string, + ProjectConfigurationFile< + IResourceStaticAssetTypingsConfigurationJson | ISourceStaticAssetTypingsConfigurationJson + > +> = new Map(); + +export type FileLoaderType = 'resource' | 'source'; + +function createConfigurationFileLoader( + configFileName: string, + fileLoaderType: FileLoaderType +): ProjectConfigurationFile< + IResourceStaticAssetTypingsConfigurationJson | ISourceStaticAssetTypingsConfigurationJson +> { + return new ProjectConfigurationFile< + IResourceStaticAssetTypingsConfigurationJson | ISourceStaticAssetTypingsConfigurationJson + >({ + jsonSchemaObject: fileLoaderType === 'resource' ? resourceStaticAssetSchema : sourceStaticAssetSchema, + projectRelativeFilePath: `config/${configFileName}`, + propertyInheritance: { + fileExtensions: { + inheritanceType: InheritanceType.append + } + } + }); +} + +export function getConfigFromConfigFileAsync( + configFileName: string, + fileLoaderType: FileLoaderType, + terminal: ITerminal, + slashNormalizedBuildFolderPath: string, + rigConfig: HeftConfiguration['rigConfig'] +): Promise< + IResourceStaticAssetTypingsConfigurationJson | ISourceStaticAssetTypingsConfigurationJson | undefined +> { + let configurationFileLoader: + | ProjectConfigurationFile< + IResourceStaticAssetTypingsConfigurationJson | ISourceStaticAssetTypingsConfigurationJson + > + | undefined = configurationFileLoaderByFileName.get(configFileName); + if (!configurationFileLoader) { + configurationFileLoader = createConfigurationFileLoader(configFileName, fileLoaderType); + configurationFileLoaderByFileName.set(configFileName, configurationFileLoader); + } + + return configurationFileLoader.tryLoadConfigurationFileForProjectAsync( + terminal, + slashNormalizedBuildFolderPath, + rigConfig + ); +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/schemas/resource-assets-options.schema.json b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/resource-assets-options.schema.json new file mode 100644 index 00000000000..749715a9f63 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/resource-assets-options.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["configType", "config"], + "properties": { + "configType": { + "type": "string", + "enum": ["inline"] + }, + "config": { + "required": ["fileExtensions"], + "type": "object", + "additionalProperties": false, + "properties": { + "fileExtensions": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\.[^\\\\/]+$" + } + }, + "generatedTsFolders": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[^\\\\]+$" + } + }, + "sourceFolderPath": { + "type": "string", + "pattern": "^[^\\\\]+$" + } + } + } + } + }, + + { + "type": "object", + "additionalProperties": false, + "required": ["configType", "configFileName"], + "properties": { + "configType": { + "type": "string", + "enum": ["file"] + }, + "configFileName": { + "type": "string", + "pattern": "^[^\\\\\\/]+\\.json$" + } + } + } + ] +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/schemas/resource-static-asset-typings.schema.json b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/resource-static-asset-typings.schema.json new file mode 100644 index 00000000000..640f68eb4dd --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/resource-static-asset-typings.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "required": ["fileExtensions"], + "properties": { + "$schema": { + "type": "string" + }, + "fileExtensions": { + "type": "array", + "items": { + "pattern": "\\.[^\\\\/]+$", + "type": "string" + } + }, + "generatedTsFolders": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[^\\\\]+$" + } + }, + "sourceFolderPath": { + "type": "string", + "pattern": "^[^\\\\]+$" + } + } +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/schemas/source-assets-options.schema.json b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/source-assets-options.schema.json new file mode 100644 index 00000000000..6dd4d724767 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/source-assets-options.schema.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["configType", "config"], + "properties": { + "configType": { + "type": "string", + "enum": ["inline"] + }, + "config": { + "required": ["fileExtensions", "cjsOutputFolders"], + "type": "object", + "additionalProperties": false, + "properties": { + "fileExtensions": { + "type": "array", + "items": { + "type": "string", + "pattern": "\\.[^\\\\/]+$" + } + }, + "generatedTsFolders": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[^\\\\]+$" + } + }, + "sourceFolderPath": { + "type": "string", + "pattern": "^[^\\\\]+$" + }, + "cjsOutputFolders": { + "type": "array", + "items": { + "pattern": "^[^\\\\]+$", + "type": "string" + } + }, + "esmOutputFolders": { + "type": "array", + "items": { + "pattern": "^[^\\\\]+$", + "type": "string" + } + } + } + } + } + }, + + { + "type": "object", + "additionalProperties": false, + "required": ["configType", "configFileName"], + "properties": { + "configType": { + "type": "string", + "enum": ["file"] + }, + "configFileName": { + "type": "string", + "pattern": "^[^\\\\\\/]+\\.json$" + } + } + } + ] +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/schemas/source-static-asset-typings.schema.json b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/source-static-asset-typings.schema.json new file mode 100644 index 00000000000..482c5181c87 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/schemas/source-static-asset-typings.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "required": ["fileExtensions", "cjsOutputFolders"], + "properties": { + "$schema": { + "type": "string" + }, + "fileExtensions": { + "type": "array", + "items": { + "pattern": "\\.[^\\\\/]+$", + "type": "string" + } + }, + "generatedTsFolders": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[^\\\\]+$" + } + }, + "sourceFolderPath": { + "type": "string", + "pattern": "^[^\\\\]+$" + }, + "cjsOutputFolders": { + "type": "array", + "items": { + "pattern": "^[^\\\\]+$", + "type": "string" + } + }, + "esmOutputFolders": { + "type": "array", + "items": { + "pattern": "^[^\\\\]+$", + "type": "string" + } + } + } +} diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/test/StaticAssetTypingsGenerator.test.ts b/heft-plugins/heft-static-asset-typings-plugin/src/test/StaticAssetTypingsGenerator.test.ts new file mode 100644 index 00000000000..bc550c55ce6 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/test/StaticAssetTypingsGenerator.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { hasChanges } from '../StaticAssetTypingsGenerator'; + +describe('hasChanges', () => { + it('returns false for two empty maps', () => { + const current: Map = new Map(); + const old: Map = new Map(); + expect(hasChanges(current, old)).toBe(false); + }); + + it('returns false for identical maps', () => { + const current: Map = new Map([ + ['a.png', 'v1'], + ['b.png', 'v2'] + ]); + const old: Map = new Map([ + ['a.png', 'v1'], + ['b.png', 'v2'] + ]); + expect(hasChanges(current, old)).toBe(false); + }); + + it('returns true when current has more entries', () => { + const current: Map = new Map([ + ['a.png', 'v1'], + ['b.png', 'v2'] + ]); + const old: Map = new Map([['a.png', 'v1']]); + expect(hasChanges(current, old)).toBe(true); + }); + + it('returns true when old has more entries', () => { + const current: Map = new Map([['a.png', 'v1']]); + const old: Map = new Map([ + ['a.png', 'v1'], + ['b.png', 'v2'] + ]); + expect(hasChanges(current, old)).toBe(true); + }); + + it('returns true when a value differs', () => { + const current: Map = new Map([ + ['a.png', 'v1'], + ['b.png', 'v3'] + ]); + const old: Map = new Map([ + ['a.png', 'v1'], + ['b.png', 'v2'] + ]); + expect(hasChanges(current, old)).toBe(true); + }); + + it('returns true when a key differs', () => { + const current: Map = new Map([['a.png', 'v1']]); + const old: Map = new Map([['b.png', 'v1']]); + expect(hasChanges(current, old)).toBe(true); + }); + + it('returns true when current is empty and old has entries', () => { + const current: Map = new Map(); + const old: Map = new Map([['a.png', 'v1']]); + expect(hasChanges(current, old)).toBe(true); + }); + + it('returns true when current has entries and old is empty', () => { + const current: Map = new Map([['a.png', 'v1']]); + const old: Map = new Map(); + expect(hasChanges(current, old)).toBe(true); + }); +}); diff --git a/heft-plugins/heft-static-asset-typings-plugin/src/types.ts b/heft-plugins/heft-static-asset-typings-plugin/src/types.ts new file mode 100644 index 00000000000..c5c9256ce02 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/src/types.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ITerminal } from '@rushstack/terminal'; + +export interface IAssetsInlineConfigPluginOptionsBase< + TConfig extends IResourceStaticAssetTypingsConfigurationJson +> { + configType: 'inline'; + /** + * The inline configuration object. + */ + config: TConfig; +} + +export interface IAssetsFileConfigPluginOptions { + configType: 'file'; + /** + * The name of the riggable config file in the config/ folder. + */ + configFileName: string; +} + +export type IAssetPluginOptions = + | IAssetsInlineConfigPluginOptionsBase + | IAssetsFileConfigPluginOptions; + +export interface IResourceStaticAssetTypingsConfigurationJson { + fileExtensions: string[]; + generatedTsFolders?: string[]; + sourceFolderPath?: string; +} + +export interface ISourceStaticAssetTypingsConfigurationJson + extends IResourceStaticAssetTypingsConfigurationJson { + cjsOutputFolders: string[]; + esmOutputFolders?: string[]; +} + +export type StaticAssetConfigurationFileLoader = ( + terminal: ITerminal +) => Promise; diff --git a/heft-plugins/heft-static-asset-typings-plugin/tsconfig.json b/heft-plugins/heft-static-asset-typings-plugin/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/heft-plugins/heft-static-asset-typings-plugin/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/rigs/heft-web-rig/package.json b/rigs/heft-web-rig/package.json index 93b08e3d100..12c10225e3e 100644 --- a/rigs/heft-web-rig/package.json +++ b/rigs/heft-web-rig/package.json @@ -19,6 +19,7 @@ "@microsoft/api-extractor": "workspace:*", "@rushstack/eslint-config": "workspace:*", "@rushstack/heft-api-extractor-plugin": "workspace:*", + "@rushstack/heft-static-asset-typings-plugin": "workspace:*", "@rushstack/heft-jest-plugin": "workspace:*", "@rushstack/heft-lint-plugin": "workspace:*", "@rushstack/heft-sass-plugin": "workspace:*", diff --git a/rigs/heft-web-rig/profiles/app/config/heft.json b/rigs/heft-web-rig/profiles/app/config/heft.json index 8e80c606118..92e332665a0 100644 --- a/rigs/heft-web-rig/profiles/app/config/heft.json +++ b/rigs/heft-web-rig/profiles/app/config/heft.json @@ -47,8 +47,22 @@ "pluginPackage": "@rushstack/heft-sass-plugin" } }, + "image-typings": { + "taskDependencies": ["set-browserslist-ignore-old-data-env-var"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "resource-assets-plugin", + "options": { + "configType": "inline", + "config": { + "fileExtensions": [".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif"], + "generatedTsFolders": ["temp/image-typings"] + } + } + } + }, "typescript": { - "taskDependencies": ["sass"], + "taskDependencies": ["sass", "image-typings"], "taskPlugin": { "pluginPackage": "@rushstack/heft-typescript-plugin" } diff --git a/rigs/heft-web-rig/profiles/app/tsconfig-base.json b/rigs/heft-web-rig/profiles/app/tsconfig-base.json index 0594f2e7b70..3ba69ee3728 100644 --- a/rigs/heft-web-rig/profiles/app/tsconfig-base.json +++ b/rigs/heft-web-rig/profiles/app/tsconfig-base.json @@ -4,7 +4,7 @@ "compilerOptions": { "outDir": "../../../../../lib", "rootDir": "../../../../../src", - "rootDirs": ["../../../../../src", "../../../../../temp/sass-ts"], + "rootDirs": ["../../../../../src", "../../../../../temp/sass-ts", "../../../../../temp/image-typings"], "forceConsistentCasingInFileNames": true, "jsx": "react", diff --git a/rigs/heft-web-rig/profiles/library/config/heft.json b/rigs/heft-web-rig/profiles/library/config/heft.json index 8e80c606118..92e332665a0 100644 --- a/rigs/heft-web-rig/profiles/library/config/heft.json +++ b/rigs/heft-web-rig/profiles/library/config/heft.json @@ -47,8 +47,22 @@ "pluginPackage": "@rushstack/heft-sass-plugin" } }, + "image-typings": { + "taskDependencies": ["set-browserslist-ignore-old-data-env-var"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-static-asset-typings-plugin", + "pluginName": "resource-assets-plugin", + "options": { + "configType": "inline", + "config": { + "fileExtensions": [".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif"], + "generatedTsFolders": ["temp/image-typings"] + } + } + } + }, "typescript": { - "taskDependencies": ["sass"], + "taskDependencies": ["sass", "image-typings"], "taskPlugin": { "pluginPackage": "@rushstack/heft-typescript-plugin" } diff --git a/rigs/heft-web-rig/profiles/library/tsconfig-base.json b/rigs/heft-web-rig/profiles/library/tsconfig-base.json index 0594f2e7b70..3ba69ee3728 100644 --- a/rigs/heft-web-rig/profiles/library/tsconfig-base.json +++ b/rigs/heft-web-rig/profiles/library/tsconfig-base.json @@ -4,7 +4,7 @@ "compilerOptions": { "outDir": "../../../../../lib", "rootDir": "../../../../../src", - "rootDirs": ["../../../../../src", "../../../../../temp/sass-ts"], + "rootDirs": ["../../../../../src", "../../../../../temp/sass-ts", "../../../../../temp/image-typings"], "forceConsistentCasingInFileNames": true, "jsx": "react", diff --git a/rigs/heft-web-rig/shared/webpack-base.config.js b/rigs/heft-web-rig/shared/webpack-base.config.js index 8ca3bf5d978..e848ead26c6 100644 --- a/rigs/heft-web-rig/shared/webpack-base.config.js +++ b/rigs/heft-web-rig/shared/webpack-base.config.js @@ -205,7 +205,7 @@ function createWebpackConfig({ env, argv, projectRoot, configOverride, extractCs }, { - test: /\.(jpeg|jpg|png|gif|svg|ico|woff|woff2|ttf|eot)$/, + test: /\.(jpeg|jpg|png|gif|svg|ico|webp|avif|woff|woff2|ttf|eot)$/, // Allows import/require() to be used with an asset file. The file will be copied to the output folder, // and the import statement will return its URL. // https://webpack.js.org/guides/asset-modules/#resource-assets diff --git a/rush.json b/rush.json index 8d2151e9cc1..cced18d1f10 100644 --- a/rush.json +++ b/rush.json @@ -1150,6 +1150,12 @@ "reviewCategory": "libraries", "shouldPublish": true }, + { + "packageName": "@rushstack/heft-static-asset-typings-plugin", + "projectFolder": "heft-plugins/heft-static-asset-typings-plugin", + "reviewCategory": "libraries", + "shouldPublish": true + }, { "packageName": "@rushstack/heft-storybook-plugin", "projectFolder": "heft-plugins/heft-storybook-plugin",