ایجاد اپلیکیشن React + Typescript در اشتراکگذاری کد مونورپو
در حال حاضر یک مونورپو دارم که در آن دو بسته (+) استفادهکننده از فضای کاری یارن دارم:
root
/packages
/common
/web
...
root/package.json
...
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**"
],
},
web یک create-react-app ساده با قالب تایپاسکریپت است. من مقداری کد TS در common دارم که میخواهم در web استفاده کنم، اما به نظر میرسد CRA از کامپایل کردن کد خارج از src پشتیبانی نمیکند و وقتی سعی میکنم از common در web ایمپورت کنم، خطا میدهد:
../common/state/user/actions.ts 4:66
Module parse failed: Unexpected token (4:66)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| import { UpdateUserParams } from './reducer';
|
> export const login = createAction('USER/LOGIN')<{ username: string; password: string }>();
بنابراین، پس از خروج برای افزودن مسیرها در /common، دستورالعملهای این نظر را دنبال کردم، اما به نظر میرسد که babel-loader فایلها را بارگذاری میکند، اما اکنون آنها را از تایپاسکریپت کامپایل نمیکند:
./common/state/user/actions.ts
SyntaxError: /Users/brandon/Code/apps/packages/common/state/user/actions.ts: Unexpected token, expected "," (4:66)
2 | import { UpdateUserParams } from './reducer';
3 |
> 4 | export const login = createAction('USER/LOGIN')<{ username: string; password: string }>();
| ^
5 |
6 | export const logout = createAction('USER/LOGOUT')();
چطور میتوانم کاری کنم که webpack/babel کد را در common به درستی کامپایل کند؟ میدانم میتوانم common خودش کدش را کامپایل کند، اما ترجیح میدهم هر بار که میخواهم تغییری بدهم، مجبور نباشم کد در common را دوباره کامپایل کنم. اینکه هر برنامه خودش این کار را انجام دهد، ایدهآل است.
Webpack.config.js:
....
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
{ parser: { requireEnsure: false } },
// First, run the linter.
// It's important to do this before Babel processes the JS.
// {
// test: /\.(js|mjs|jsx|ts|tsx)$/,
// enforce: 'pre',
// use: [
// {
// options: {
// cache: true,
// formatter: require.resolve('react-dev-utils/eslintFormatter'),
// eslintPath: require.resolve('eslint'),
// resolvePluginsRelativeTo: __dirname,
// },
// loader: require.resolve('eslint-loader'),
// },
// ],
// include: paths.appSrc,
// },
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appLernaModules.concat(paths.appSrc), // MAIN CHANGE HERE!!!
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve('babel-preset-react-app/webpack-overrides'),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[require.resolve('babel-preset-react-app/dependencies'), { helpers: true }],
],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader',
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader',
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined,
),
),
...
paths.js
'use strict';
const path = require('path');
const fs = require('fs');
const url = require('url');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
const envPublicUrl = process.env.PUBLIC_URL;
function ensureSlash(inputPath, needsSlash) {
const hasSlash = inputPath.endsWith('/');
if (hasSlash && !needsSlash) {
return inputPath.substr(0, inputPath.length - 1);
} else if (!hasSlash && needsSlash) {
return `${inputPath}/`;
} else {
return inputPath;
}
}
const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage;
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// Webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
function getServedPath(appPackageJson) {
const publicUrl = getPublicUrl(appPackageJson);
const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
return ensureSlash(servedUrl, true);
}
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`)),
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
publicUrl: getPublicUrl(resolveApp('package.json')),
servedPath: getServedPath(resolveApp('package.json')),
};
module.exports.moduleFileExtensions = moduleFileExtensions;
module.exports.lernaRoot = path.resolve(module.exports.appPath, '../../');
module.exports.appLernaModules = [];
module.exports.allLernaModules = fs.readdirSync(path.join(module.exports.lernaRoot, 'packages'));
fs.readdirSync(module.exports.appNodeModules).forEach(folderName => {
if (folderName === 'react-scripts') {
return;
}
const fullName = path.join(module.exports.appNodeModules, folderName);
if (fs.lstatSync(fullName).isSymbolicLink()) {
module.exports.appLernaModules.push(fs.realpathSync(fullName));
}
});
tsconfig:
// base in root
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"esModuleInterop": true,
"downlevelIteration": true,
"lib": ["esnext", "dom"],
"noUnusedLocals": true,
"skipLibCheck": true,
"strict": true,
"target": "esnext",
"noEmit": true,
"moduleResolution": "node",
"resolveJsonModule": true
}
}
//packages/web/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react",
"baseUrl": "src",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"isolatedModules": true,
"typeRoots": ["./node_modules/@types"],
},
"include": [
"src"
]
}
پاسخ شماره ۱
من میخواستم نظر خودم را اضافه کنم، چون من هم این مشکل را داشتم. ابتدا سعی کردم پوشه اجزای مشترک را کامپایل کنم، اما آن وحشتناک بود... زمان زیادی میبرد.
در اینجا به شما نشان میدهم چگونه این کار را با react-scripts نسخه ۴ و TypeScript بدون خارج کردن انجام دهید (ما نیاز به استفاده از craco خواهیم داشت، در غیر این صورت امکانپذیر نیست).
این مراحل به صورت گام به گام است.
- پوشه مؤلفه مشترک خود را، در این مورد،
commonبه فایل اصلی .tsx یا .ts اشاره دهید. - یک فایل
craco.config.jsایجاد کنید که babel-loader را بارگذاری کرده و آن را ترنسپایل کند. این فایل باید در هر برنامه جداگانهای که تلاش میشود common به آن بارگذاری شود، کپی شود.
- اشاره به پوشه کامپوننت مشترک خود. فایل
package.jsonپوشهcommonخود را باز کنید (نه ریشه!):
بررسی کنید که نامی که میخواهید از آن وارد کنید را دارید (در تصویر از مثالی از برنامه واقعی خودم استفاده کردهام)، private را روی true تنظیم کنید، و ورودیهای source، main و module را داشته باشید و آنها را به فایل ورودی خود اشاره دهید. در مورد من، این فایل index.ts بود.
{
"name": "common",
"version": "0.1.0",
"private": true,
"source": "index.ts",
"main": "./index.ts",
"module": "./index.ts",
// The common's package.json content
...
}
- در هر مخزن دیگری که میخواهید از بسته
commonخود ارجاع دهید، یک فایلcraco.config.jsایجاد کنید و موارد زیر را درج نمایید:
const path = require('path');
/**
* ALlows us to edit create-react-app configuration
* without ejecting.
*
*
*/
const { getLoader, loaderByName } = require('@craco/craco');
// Replace `components` with your folder's structure.
// Again, Here I'm showcasing my current project.
const absolutePath = path.join(__dirname, '../components');
/**
* This is extremely important if you're using a CI/CD to build and deploy
* your code!!! Read!
*
* If you create a package with a namespace, such as name is @schon/components, you need
* to add what I've commented down below. Otherwise Yarn will fail in CI/CD environments!!
*/
// const schonComponents = path.join(
// __dirname,
// './node_modules/@schon/components',
// );
module.exports = {
webpack: {
configure: (webpackConfig, { env, paths }) => {
// https://medium.com/frontend-digest/using-create-react-app-in-a-monorepo-a4e6f25be7aa
const { isFound, match } = getLoader(
webpackConfig,
loaderByName('babel-loader'),
);
if (isFound) {
const include = Array.isArray(match.loader.include)
? match.loader.include
: [match.loader.include];
// match.loader.include = include.concat(absolutePath, schonComponents);
match.loader.include = include.concat(absolutePath);
}
return {
...webpackConfig,
/**
* Optionally, other webpack configuration details.
*/
// optimization: {
// splitChunks: {
// },
// },
};
},
},
};
پاسخ شماره ۲
در نهایت موفق شدم آن را راهاندازی کنم. برای اینکه TS کار کند، شما باید بهطور صریح پریستها را اضافه کنید: ['@babel/preset-typescript', { allExtensions: true, isTSX: true }], ['@babel/preset-react'], به لودر خود علاوه بر راهحل پیوند داده شده:
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: [paths.appSrc, ...paths.appLernaModules],
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve('babel-preset-react-app/webpack-overrides'),
presets: [// ADD THESE
['@babel/preset-typescript', { allExtensions: true, isTSX: true }],
['@babel/preset-react'],
],
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},

