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/) | [](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/) | [](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/) | [](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/) | [](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/) | [](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/) | [](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/) | [](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",