typescript - 在 monorepo 代码共享中创建 React App + Typescript
问题描述
我目前有一个 monorepo,其中我有两个(+)使用纱线工作区的包:
root
/packages
/common
/web
...
root/package.json
...
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**"
],
},
web
是一个带有 typescript 模板的普通 create-react-app。我有一些 TS 代码common
我想在 in 中使用web
,但似乎 CRA 不支持在外部编译代码src
,当我尝试从common
in导入时给我一个错误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
编译它自己的代码,但我不想每次我想要进行更改时都必须重新编译共同的代码。让每个应用程序自己做是理想的。
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,
),
),
...
路径.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));
}
});
配置:
// 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"
]
}
解决方案
我想加我的 2 美分,因为我遇到了这个问题。我最初确实尝试过编译共享组件文件夹,但是太糟糕了……花了太长时间。
在这里,我将向您展示如何使用react-scripts
v4 执行此操作并且TypeScript
不弹出(我们将需要使用 craco,否则这是不可能的)。
这是一步一步的。
- 在这种情况下,将您的共享组件文件夹
common
指向主 .tsx 或 .ts 文件。 - 创建一个
craco.config.js
加载 babel-loader 并对其进行转译的文件。这个文件需要在每个试图加载 common 的应用程序中复制。
- 指向您的共享组件文件夹。打开你
common
的package.json
(不是根!):
检查您是否具有要从中导入它的名称(我在图片中使用的是我的实际应用程序的示例),将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
...
}
- 在您希望从中引用包的所有其他 repo
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: {
// },
// },
};
},
},
};
推荐阅读
- java - 如何在 Swing 中创建下拉组件?
- css - CSS中名词“样式”的定义是什么?
- angular - 通过 ng-template 进行表单验证
- git - GitHub 子模块跟踪最新的分支,没有“git 子模块更新”
- google-cloud-platform - 如何获取 GKE 节点池中的节点数作为堆栈驱动程序指标?
- asp.net-core - 在 asp.net core 2.2 mvc 应用程序中获取 AzureAD 刷新令牌
- c++ - C++中的递归函数和性能
- class - 如何获取有关 dart 中的类的信息?
- javascript - 无法从 CodeBehind 调用 JavaScript 函数
- amazon-web-services - AWS Elastic beanstalk 挂钩失败:- 无法将文件复制到 c:/windows/fonts