diff --git a/docs/how_tos/migrate-frontend-app.md b/docs/how_tos/migrate-frontend-app.md index 890cca7c..c3a0ca23 100644 --- a/docs/how_tos/migrate-frontend-app.md +++ b/docs/how_tos/migrate-frontend-app.md @@ -259,8 +259,12 @@ Create a `tsconfig.json` file and add the following contents to it: { "extends": "@openedx/frontend-base/config/tsconfig.json", "compilerOptions": { + "baseUrl": ".", "rootDir": ".", "outDir": "dist", + "paths": { + "@src/*": ["./src/*"] + } }, "include": [ "src/**/*", @@ -275,6 +279,26 @@ Create a `tsconfig.json` file and add the following contents to it: This assumes you have a `src` folder and your build goes in `dist`, which is the best practice. +The `@src` path alias +--------------------- + +The `paths` configuration above sets up the `@src` alias, which allows you to import from your app's `src` directory using `@src/...` instead of relative paths. For example: + +```typescript +// Instead of: +import { MyComponent } from '../../../components/MyComponent'; + +// You can use: +import { MyComponent } from '@src/components/MyComponent'; +``` + +Each consuming app must define its own `@src` path mapping in its `tsconfig.json`. This is because: + +1. **TypeScript** uses the static path mapping in your `tsconfig.json` for IDE support (autocomplete, go-to-definition, type checking) +2. **Webpack** uses a resolver plugin that dynamically finds the closest `src` directory relative to the importing file at build time + +This approach ensures that `@src` always resolves to your app's own `src` directory, even in complex project structures. + Edit jest.config.js =================== diff --git a/test-site/tsconfig.json b/test-site/tsconfig.json index e07305c3..704176f4 100644 --- a/test-site/tsconfig.json +++ b/test-site/tsconfig.json @@ -1,8 +1,12 @@ { "extends": "@openedx/frontend-base/config/tsconfig.json", "compilerOptions": { + "baseUrl": ".", "rootDir": ".", - "outDir": "dist" + "outDir": "dist", + "paths": { + "@src/*": ["./src/*"] + } }, "include": [ "eslint.config.js", diff --git a/tools/tsconfig.json b/tools/tsconfig.json index 8cc7f694..c1eb390c 100644 --- a/tools/tsconfig.json +++ b/tools/tsconfig.json @@ -5,10 +5,7 @@ "outDir": "dist", "noEmit": false, "allowJs": true, - "resolveJsonModule": true, - "paths": { - "@src/*": ["./src/*"] - } + "resolveJsonModule": true }, "include": [ "babel/**/*", diff --git a/tools/webpack/plugins/ClosestSrcResolverPlugin.ts b/tools/webpack/plugins/ClosestSrcResolverPlugin.ts new file mode 100644 index 00000000..21c733e8 --- /dev/null +++ b/tools/webpack/plugins/ClosestSrcResolverPlugin.ts @@ -0,0 +1,62 @@ +import fs from 'fs'; +import path from 'path'; +import { Resolver } from 'webpack'; + +/** + * A webpack resolver plugin that resolves `@src` imports to the closest + * `src` directory by walking up from the importing file's location. + * + * This allows apps to have their own `src` directories, with `@src` always + * resolving to the nearest one relative to the file doing the import. + */ +class ClosestSrcResolverPlugin { + apply(resolver: Resolver) { + const target = resolver.ensureHook('resolve'); + + resolver.getHook('resolve').tapAsync( + 'ClosestSrcResolverPlugin', + (request: any, resolveContext: any, callback: (err?: null | Error, result?: any) => void) => { + if (!request.request?.startsWith('@src')) { + return callback(); + } + + // Get the directory of the file doing the import + const issuer = request.context?.issuer; + if (!issuer) { + return callback(); + } + + // Walk up from the issuer to find closest 'src' directory, + // but don't go above the current working directory + const cwd = process.cwd(); + let dir = path.dirname(issuer); + let srcPath: string | null = null; + + while (dir.startsWith(cwd) && dir !== path.parse(dir).root) { + const candidate = path.join(dir, 'src'); + if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) { + srcPath = candidate; + break; + } + dir = path.dirname(dir); + } + + if (!srcPath) { + return callback(); + } + + // Replace @src with the actual path + const newRequest = request.request.replace(/^@src/, srcPath); + + const obj = { + ...request, + request: newRequest, + }; + + resolver.doResolve(target, obj, null, resolveContext, callback); + } + ); + } +} + +export default ClosestSrcResolverPlugin; diff --git a/tools/webpack/webpack.config.build.ts b/tools/webpack/webpack.config.build.ts index 101385a0..918e903f 100644 --- a/tools/webpack/webpack.config.build.ts +++ b/tools/webpack/webpack.config.build.ts @@ -13,6 +13,7 @@ import { getStylesheetRule } from './common-config'; +import ClosestSrcResolverPlugin from './plugins/ClosestSrcResolverPlugin'; import getLocalAliases from './utils/getLocalAliases'; import getPublicPath from './utils/getPublicPath'; import getResolvedSiteConfigPath from './utils/getResolvedSiteConfigPath'; @@ -36,9 +37,9 @@ const config: Configuration = { alias: { ...aliases, 'site.config': resolvedSiteConfigPath, - '@src': path.resolve(process.cwd(), 'src'), }, extensions: ['.js', '.jsx', '.ts', '.tsx'], + plugins: [new ClosestSrcResolverPlugin()], }, module: { rules: [ diff --git a/tools/webpack/webpack.config.dev.shell.ts b/tools/webpack/webpack.config.dev.shell.ts index 244731c7..d5c9719e 100644 --- a/tools/webpack/webpack.config.dev.shell.ts +++ b/tools/webpack/webpack.config.dev.shell.ts @@ -15,6 +15,7 @@ import { } from './common-config'; import HtmlWebpackPlugin from 'html-webpack-plugin'; +import ClosestSrcResolverPlugin from './plugins/ClosestSrcResolverPlugin'; import getLocalAliases from './utils/getLocalAliases'; import getPublicPath from './utils/getPublicPath'; import getResolvedSiteConfigPath from './utils/getResolvedSiteConfigPath'; @@ -34,9 +35,9 @@ const config: Configuration = { alias: { ...aliases, 'site.config': resolvedSiteConfigPath, - '@src': path.resolve(process.cwd(), 'src'), }, extensions: ['.js', '.jsx', '.ts', '.tsx'], + plugins: [new ClosestSrcResolverPlugin()], }, mode: 'development', devtool: 'eval-source-map', diff --git a/tools/webpack/webpack.config.dev.ts b/tools/webpack/webpack.config.dev.ts index cb9d2ae1..847a09be 100644 --- a/tools/webpack/webpack.config.dev.ts +++ b/tools/webpack/webpack.config.dev.ts @@ -14,6 +14,7 @@ import { getStylesheetRule } from './common-config'; +import ClosestSrcResolverPlugin from './plugins/ClosestSrcResolverPlugin'; import getLocalAliases from './utils/getLocalAliases'; import getPublicPath from './utils/getPublicPath'; import getResolvedSiteConfigPath from './utils/getResolvedSiteConfigPath'; @@ -33,9 +34,9 @@ const config: Configuration = { alias: { ...aliases, 'site.config': resolvedSiteConfigPath, - '@src': path.resolve(process.cwd(), 'src'), }, extensions: ['.js', '.jsx', '.ts', '.tsx'], + plugins: [new ClosestSrcResolverPlugin()], }, mode: 'development', devtool: 'eval-source-map', diff --git a/tsconfig.json b/tsconfig.json index e0f9f58b..6e702bbf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,7 @@ "extends": "./tools/typescript/tsconfig.json", "compilerOptions": { "rootDir": ".", - "outDir": "dist", - "paths": { - "@src/*": ["./src/*"] - } + "outDir": "dist" }, "include": [ "runtime/**/*",