Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/how_tos/migrate-frontend-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*",
Expand All @@ -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
===================
Expand Down
6 changes: 5 additions & 1 deletion test-site/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 1 addition & 4 deletions tools/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
"outDir": "dist",
"noEmit": false,
"allowJs": true,
"resolveJsonModule": true,
"paths": {
"@src/*": ["./src/*"]
}
"resolveJsonModule": true
},
"include": [
"babel/**/*",
Expand Down
62 changes: 62 additions & 0 deletions tools/webpack/plugins/ClosestSrcResolverPlugin.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion tools/webpack/webpack.config.build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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: [
Expand Down
3 changes: 2 additions & 1 deletion tools/webpack/webpack.config.dev.shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion tools/webpack/webpack.config.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down
5 changes: 1 addition & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
"extends": "./tools/typescript/tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "dist",
"paths": {
"@src/*": ["./src/*"]
}
"outDir": "dist"
},
"include": [
"runtime/**/*",
Expand Down
Loading