feat: upgrade react-intl workflow in example (#16215)

Changes:
- Migrate to TypeScript. `react-intl` natively supports TypeScript now.
- Upgrade corresponding `formatjs` packages.
- Dynamically polyfill Intl API per locale since those polyfills are huge.
- Migrate to recommended workflow per https://formatjs.io/docs/getting-started/application-workflow
This commit is contained in:
Long Ho 2020-08-27 18:59:33 -04:00 committed by GitHub
parent f658b7641d
commit 5a478b4eef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 362 additions and 352 deletions

View file

@ -0,0 +1,10 @@
{
"presets": ["next/babel"],
"plugins": [
["babel-plugin-react-intl", {
"ast": true,
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"extractFromFormatMessageCall": true
}]
]
}

View file

@ -33,3 +33,4 @@ yarn-error.log*
# vercel
.vercel
compiled-lang

View file

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

View file

@ -1,11 +1,9 @@
# Example app with [React Intl][]
This example app shows how to integrate [React Intl][] with Next.
This example app shows how to integrate [React Intl][] with Next.js.
## How to use
### Using `create-next-app`
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
@ -14,39 +12,21 @@ npx create-next-app --example with-react-intl with-react-intl-app
yarn create next-app --example with-react-intl with-react-intl-app
```
### Download manually
Download the example:
```bash
curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-react-intl
cd with-react-intl
```
Install it and run:
```bash
npm install
npm run dev
# or
yarn
yarn dev
```
Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
### Features of this example app
## Features of this example app
- Server-side language negotiation
- React Intl locale data loading via `pages/_document.js` customization
- React Intl integration with [custom App](https://github.com/vercel/next.js#custom-app) component
- `<IntlProvider>` creation with `locale`, `messages` props
- Default message extraction via `babel-plugin-react-intl` integration
- Default message extraction via `@formatjs/cli` integration
- Pre-compile messages into AST with `babel-plugin-react-intl` for performance
- Translation management via build script and customized Next server
### Translation Management
This app stores translations and default strings in the `lang/` dir. This dir has `.messages/` subdir which is where React Intl's Babel plugin outputs the default messages it extracts from the source code. The default messages (`en.json` in this example app) is also generated by the build script. This file can then be sent to a translation service to perform localization for the other locales the app should support.
This app stores translations and default strings in the `lang/` dir. The default messages (`en.json` in this example app) is also generated by the build script. This file can then be sent to a translation service to perform localization for the other locales the app should support.
The translated messages files that exist at `lang/*.json` are only used during production, and are automatically provided to the `<IntlProvider>`. During development the `defaultMessage`s defined in the source code are used. To prepare the example app for localization and production run the build script and start the server in production mode:
@ -57,21 +37,4 @@ $ npm start
You can then switch your browser's language preferences to French and refresh the page to see the UI update accordingly.
### FormattedHTMLMessage support (react-intl pre-v4)
Out of the box, this example does not support the use of the `FormattedHTMLMessage` component on the server due to `DOMParser` not being present in a Node environment.
This functionality is deprecated and has been removed as of react-intl 4.0
If you still want to enable this feature, you should install a `DOMParser` implementation (e.g. `xmldom` or `jsdom`) and enable the polyfill in `server.js`:
```js
// Polyfill Node with `DOMParser` required by formatjs.
// See: https://github.com/vercel/next.js/issues/10533
const { DOMParser } = require('xmldom')
global.DOMParser = DOMParser
```
[react intl]: https://github.com/yahoo/react-intl
### Transpile react-intl
According to [react-intl docs](https://github.com/formatjs/react-intl/blob/53f2c826c7b1e50ad37215ce46b5e1c6f5d142cc/docs/Getting-Started.md#esm-build), react-intl and its underlying libraries must be transpiled to support older browsers (eg IE11). This is done by [next-transpile-modules](https://www.npmjs.com/package/next-transpile-modules) in next.config.js.
[react intl]: https://formatjs.io

View file

@ -1,29 +0,0 @@
import { defineMessages, useIntl } from 'react-intl'
import Head from 'next/head'
import Nav from './Nav'
const messages = defineMessages({
title: {
id: 'title',
defaultMessage: 'React Intl Next.js Example',
},
})
export default function Layout({ title, children }) {
const intl = useIntl()
return (
<div>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title || intl.formatMessage(messages.title)}</title>
</Head>
<header>
<Nav />
</header>
{children}
</div>
)
}

View file

@ -0,0 +1,28 @@
import * as React from 'react';
import {useIntl} from 'react-intl';
import Head from 'next/head';
import Nav from './Nav';
export default function Layout({title, children}) {
const intl = useIntl();
return (
<div>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
{title ||
intl.formatMessage({
defaultMessage: 'React Intl Next.js Example',
})}
</title>
</Head>
<header>
<Nav />
</header>
{children}
</div>
);
}

View file

@ -1,5 +1,6 @@
import { FormattedMessage } from 'react-intl'
import Link from 'next/link'
import * as React from 'react';
import {FormattedMessage} from 'react-intl';
import Link from 'next/link';
export default function Nav() {
return (
@ -7,14 +8,14 @@ export default function Nav() {
<li>
<Link href="/">
<a>
<FormattedMessage id="nav.home" defaultMessage="Home" />
<FormattedMessage defaultMessage="Home" />
</a>
</Link>
</li>
<li>
<Link href="/about">
<a>
<FormattedMessage id="nav.about" defaultMessage="About" />
<FormattedMessage defaultMessage="About" />
</a>
</Link>
</li>
@ -29,5 +30,5 @@ export default function Nav() {
}
`}</style>
</nav>
)
);
}

View file

@ -1,7 +1,7 @@
{
"title": "React Intl Next.js Example",
"nav.home": "Home",
"nav.about": "About",
"description": "An example app integrating React Intl with Next.js",
"greeting": "Hello, World!"
"11754": "An example app integrating React Intl with Next.js",
"65a8e": "Hello, World!",
"8cf04": "Home",
"8f7f4": "About",
"9c817": "React Intl Next.js Example"
}

View file

@ -1,7 +1,7 @@
{
"title": "React Intl Next.js Exemple",
"nav.home": "Accueil",
"nav.about": "À propos de nous",
"description": "Un exemple d'application intégrant React Intl avec Next.js",
"greeting": "Bonjour le monde!"
"11754": "Un exemple d'application intégrant React Intl avec Next.js",
"65a8e": "Bonjour le monde!",
"8cf04": "Accueil",
"8f7f4": "À propos de nous",
"9c817": "React Intl Next.js Exemple"
}

View file

@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

View file

@ -1,10 +0,0 @@
const withTM = require('next-transpile-modules')([
'@formatjs/intl-relativetimeformat',
'@formatjs/intl-utils',
'react-intl',
'intl-format-cache',
'intl-messageformat-parser',
'intl-messageformat',
])
module.exports = withTM()

View file

@ -2,28 +2,41 @@
"name": "with-react-intl",
"version": "1.0.0",
"scripts": {
"dev": "node --icu-data-dir=node_modules/full-icu server.js",
"build": "next build && npm run extract",
"extract": "node ./scripts/extract '{pages,components}/*.{js,ts,tsx}'",
"start": "NODE_ENV=production node --icu-data-dir=node_modules/full-icu server.js"
"dev": "NODE_ICU_DATA=node_modules/full-icu ts-node --project tsconfig.server.json server.ts",
"build": "npm run extract:i18n && npm run compile:i18n && next build && tsc -p tsconfig.server.json",
"extract:i18n": "formatjs extract '{pages,components}/*.{js,ts,tsx}' --format simple --out-file lang/en.json",
"compile:i18n": "formatjs compile-folder --ast --format simple lang/ compiled-lang/",
"start": "NODE_ENV=production NODE_ICU_DATA=node_modules/full-icu node dist/server"
},
"dependencies": {
"@formatjs/cli": "1.1.12",
"@formatjs/intl-relativetimeformat": "^2.8.2",
"@formatjs/intl-utils": "^0.6.1",
"@formatjs/cli": "^2.7.3",
"@formatjs/intl-datetimeformat": "^2.4.3",
"@formatjs/intl-getcanonicallocales": "^1.3.2",
"@formatjs/intl-numberformat": "^5.4.1",
"@formatjs/intl-pluralrules": "^3.4.0",
"@formatjs/intl-relativetimeformat": "^7.1.1",
"accepts": "^1.3.7",
"babel-plugin-react-intl": "^8.1.1",
"full-icu": "^1.3.0",
"glob": "^7.1.4",
"intl": "^1.2.5",
"intl-locales-supported": "1.8.4",
"next": "latest",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-intl": "^3.1.12"
"react-intl": "^5.6.3"
},
"license": "ISC",
"devDependencies": {
"@types/accepts": "^1.3.5",
"cross-spawn": "7.0.3",
"next-transpile-modules": "^4.0.2"
"prettier": "2.0.5",
"ts-node": "8.0.0",
"typescript": "3.9.7"
},
"prettier": {
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": false,
"endOfLine": "lf",
"arrowParens": "avoid"
}
}

View file

@ -1,36 +0,0 @@
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl'
// This is optional but highly recommended
// since it prevents memory leak
const cache = createIntlCache()
function MyApp({ Component, pageProps, locale, messages }) {
const intl = createIntl(
{
locale,
messages,
},
cache
)
return (
<RawIntlProvider value={intl}>
<Component {...pageProps} />
</RawIntlProvider>
)
}
MyApp.getInitialProps = async ({ Component, ctx }) => {
let pageProps = {}
const { req } = ctx
const locale = req?.locale ?? ''
const messages = req?.messages ?? {}
if (Component.getInitialProps) {
Object.assign(pageProps, await Component.getInitialProps(ctx))
}
return { pageProps, locale, messages }
}
export default MyApp

View file

@ -0,0 +1,43 @@
import * as React from 'react';
import {IntlProvider} from 'react-intl';
import {polyfill} from '../polyfills';
import App from 'next/app';
function MyApp({Component, pageProps, locale, messages}) {
return (
<IntlProvider locale={locale} messages={messages}>
<Component {...pageProps} />
</IntlProvider>
);
}
// We need to load and expose the translations on the request for the user's
// locale. These will only be used in production, in dev the `defaultMessage` in
// each message description in the source code will be used.
const getMessages = (locale: string = 'en') => {
switch (locale) {
default:
return import('../compiled-lang/en.json');
case 'fr':
return import('../compiled-lang/fr.json');
}
};
const getInitialProps: typeof App.getInitialProps = async appContext => {
const {
ctx: {req},
} = appContext;
const locale = (req as any)?.locale ?? 'en';
const [appProps, messages] = await Promise.all([
polyfill(locale),
getMessages(locale),
App.getInitialProps(appContext),
]);
return {...(appProps as any), locale, messages};
};
MyApp.getInitialProps = getInitialProps;
export default MyApp;

View file

@ -1,38 +0,0 @@
import Document, { Head, Main, NextScript } from 'next/document'
// The document (which is SSR-only) needs to be customized to expose the locale
// data for the user's locale for React Intl to work in the browser.
export default class IntlDocument extends Document {
static async getInitialProps(context) {
const props = await super.getInitialProps(context)
const {
req: { locale, localeDataScript },
} = context
return {
...props,
locale,
localeDataScript,
}
}
render() {
// Polyfill Intl API for older browsers
const polyfill = `https://cdn.polyfill.io/v3/polyfill.min.js?features=Intl.~locale.${this.props.locale}`
return (
<html>
<Head />
<body>
<Main />
<script src={polyfill} />
<script
dangerouslySetInnerHTML={{
__html: this.props.localeDataScript,
}}
/>
<NextScript />
</body>
</html>
)
}
}

View file

@ -1,19 +0,0 @@
import { FormattedRelativeTime } from 'react-intl'
import { selectUnit } from '@formatjs/intl-utils'
import Layout from '../components/Layout'
export default function About() {
const { value, unit } = selectUnit(Date.now())
return (
<Layout>
<p>
<FormattedRelativeTime
numeric="auto"
value={value}
unit={unit}
updateIntervalInSeconds={1}
/>
</p>
</Layout>
)
}

View file

@ -0,0 +1,14 @@
import * as React from 'react';
import {FormattedRelativeTime, useIntl} from 'react-intl';
import Layout from '../components/Layout';
export default function About() {
const intl = useIntl();
return (
<Layout title={intl.formatMessage({defaultMessage: 'About'})}>
<p>
<FormattedRelativeTime numeric="auto" value={1} unit="hour" />
</p>
</Layout>
);
}

View file

@ -1,33 +0,0 @@
import {
FormattedMessage,
FormattedNumber,
defineMessages,
useIntl,
} from 'react-intl'
import Head from 'next/head'
import Layout from '../components/Layout'
const { description } = defineMessages({
description: {
id: 'description',
defaultMessage: 'An example app integrating React Intl with Next.js',
},
})
export default function Home() {
const intl = useIntl()
return (
<Layout>
<Head>
<meta name="description" content={intl.formatMessage(description)} />
</Head>
<p>
<FormattedMessage id="greeting" defaultMessage="Hello, World!" />
</p>
<p>
<FormattedNumber value={1000} />
</p>
</Layout>
)
}

View file

@ -0,0 +1,32 @@
import * as React from 'react';
import {FormattedMessage, FormattedNumber, useIntl} from 'react-intl';
import Head from 'next/head';
import Layout from '../components/Layout';
export default function Home() {
const intl = useIntl();
return (
<Layout
title={intl.formatMessage({
defaultMessage: 'Home',
})}
>
<Head>
<meta
name="description"
content={intl.formatMessage({
defaultMessage:
'An example app integrating React Intl with Next.js',
})}
/>
</Head>
<p>
<FormattedMessage defaultMessage="Hello, World!" />
</p>
<p>
<FormattedNumber value={1000} />
</p>
</Layout>
);
}

View file

@ -0,0 +1,127 @@
import {shouldPolyfill as shouldPolyfillGetCanonicalLocales} from '@formatjs/intl-getcanonicallocales/should-polyfill';
import {shouldPolyfill as shouldPolyfillPluralRules} from '@formatjs/intl-pluralrules/should-polyfill';
import {shouldPolyfill as shouldPolyfillNumberFormat} from '@formatjs/intl-numberformat/should-polyfill';
import {shouldPolyfill as shouldPolyfillDateTimeFormat} from '@formatjs/intl-datetimeformat/should-polyfill';
import {shouldPolyfill as shouldPolyfillRelativeTimeFormat} from '@formatjs/intl-relativetimeformat/should-polyfill';
/**
* Dynamically polyfill Intl API & its locale data
* @param locale locale to polyfill
*/
export async function polyfill(locale: string = '') {
const dataPolyfills = [];
// Polyfill Intl.getCanonicalLocales if necessary
if (shouldPolyfillGetCanonicalLocales()) {
await import(
/* webpackChunkName: "intl-getcanonicallocales" */ '@formatjs/intl-getcanonicallocales/polyfill'
);
}
// Polyfill Intl.PluralRules if necessary
if (shouldPolyfillPluralRules()) {
await import(
/* webpackChunkName: "intl-pluralrules" */ '@formatjs/intl-pluralrules/polyfill'
);
}
if ((Intl.PluralRules as any).polyfilled) {
const lang = locale.split('-')[0];
switch (lang) {
default:
dataPolyfills.push(
import(
/* webpackChunkName: "intl-pluralrules" */ '@formatjs/intl-pluralrules/locale-data/en'
)
);
break;
case 'fr':
dataPolyfills.push(
import(
/* webpackChunkName: "intl-pluralrules" */ '@formatjs/intl-pluralrules/locale-data/fr'
)
);
break;
}
}
// Polyfill Intl.NumberFormat if necessary
if (shouldPolyfillNumberFormat()) {
await import(
/* webpackChunkName: "intl-numberformat" */ '@formatjs/intl-numberformat/polyfill'
);
}
if ((Intl.NumberFormat as any).polyfilled) {
switch (locale) {
default:
dataPolyfills.push(
import(
/* webpackChunkName: "intl-numberformat" */ '@formatjs/intl-numberformat/locale-data/en'
)
);
break;
case 'fr':
dataPolyfills.push(
import(
/* webpackChunkName: "intl-numberformat" */ '@formatjs/intl-numberformat/locale-data/fr'
)
);
break;
}
}
// Polyfill Intl.DateTimeFormat if necessary
if (shouldPolyfillDateTimeFormat()) {
await import(
/* webpackChunkName: "intl-datetimeformat" */ '@formatjs/intl-datetimeformat/polyfill'
);
}
if ((Intl.DateTimeFormat as any).polyfilled) {
dataPolyfills.push(import('@formatjs/intl-datetimeformat/add-all-tz'));
switch (locale) {
default:
dataPolyfills.push(
import(
/* webpackChunkName: "intl-datetimeformat" */ '@formatjs/intl-datetimeformat/locale-data/en'
)
);
break;
case 'fr':
dataPolyfills.push(
import(
/* webpackChunkName: "intl-datetimeformat" */ '@formatjs/intl-datetimeformat/locale-data/fr'
)
);
break;
}
}
// Polyfill Intl.RelativeTimeFormat if necessary
if (shouldPolyfillRelativeTimeFormat()) {
await import(
/* webpackChunkName: "intl-relativetimeformat" */ '@formatjs/intl-relativetimeformat/polyfill'
);
}
if ((Intl as any).RelativeTimeFormat.polyfilled) {
switch (locale) {
default:
dataPolyfills.push(
import(
/* webpackChunkName: "intl-relativetimeformat" */ '@formatjs/intl-relativetimeformat/locale-data/en'
)
);
break;
case 'fr':
dataPolyfills.push(
import(
/* webpackChunkName: "intl-relativetimeformat" */ '@formatjs/intl-relativetimeformat/locale-data/fr'
)
);
break;
}
}
await Promise.all(dataPolyfills);
}

View file

@ -1,35 +0,0 @@
const { readFileSync, writeFileSync } = require('fs')
const { resolve } = require('path')
const glob = require('glob')
const spawn = require('cross-spawn')
// formatjs cli doesn't currently support globbing, so we perform it ourselves
// as a workaround. see https://github.com/formatjs/formatjs/issues/383
const sourceFiles = glob.sync(process.argv[2])
spawn.sync('npx', [
'formatjs',
'extract',
'--messages-dir',
'lang/.messages/',
'--remove-default-message',
...sourceFiles,
])
const defaultMessages = glob
.sync('./lang/.messages/**/*.json')
.map((filename) => readFileSync(filename, 'utf8'))
.map((file) => JSON.parse(file))
.reduce((messages, descriptors) => {
descriptors.forEach(({ id, defaultMessage }) => {
if (messages.hasOwnProperty(id) && messages[id] !== defaultMessage) {
throw new Error(
`Duplicate message id: ${id} (duplicate message ids are allowed, but only if the defaultMessages match!)`
)
}
messages[id] = defaultMessage
})
return messages
}, {})
writeFileSync('./lang/en.json', JSON.stringify(defaultMessages, null, 2))
console.log(`> Wrote default messages to: "${resolve('./lang/en.json')}"`)

View file

@ -1,83 +0,0 @@
const { basename } = require('path')
const glob = require('glob')
const areIntlLocalesSupported = require('intl-locales-supported').default
// Get the supported languages by looking for translations in the `lang/` dir.
const supportedLanguages = glob
.sync('./lang/*.json')
.map((f) => basename(f, '.json'))
// Polyfill Node with `Intl` that has data for all locales.
// See: https://formatjs.io/guides/runtime-environments/#server
if (global.Intl) {
// Determine if the built-in `Intl` has the locale data we need.
if (!areIntlLocalesSupported(supportedLanguages)) {
// `Intl` exists, but it doesn't have the data we need, so load the
// polyfill and patch the constructors we need with the polyfills.
const IntlPolyfill = require('intl')
Intl.NumberFormat = IntlPolyfill.NumberFormat
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat
Intl.__disableRegExpRestore = IntlPolyfill.__disableRegExpRestore
}
} else {
// No `Intl`, so use and load the polyfill.
global.Intl = require('intl')
}
// Fix: https://github.com/vercel/next.js/issues/11777
// See related issue: https://github.com/andyearnshaw/Intl.js/issues/308
if (Intl.__disableRegExpRestore) {
Intl.__disableRegExpRestore()
}
// Polyfill DOMParser for **pre-v4** react-intl used by formatjs.
// Only needed when using FormattedHTMLMessage. Make sure to install `xmldom`.
// See: https://github.com/vercel/next.js/issues/10533
// const { DOMParser } = require('xmldom')
// global.DOMParser = DOMParser
const { readFileSync } = require('fs')
const { createServer } = require('http')
const accepts = require('accepts')
const next = require('next')
const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
// We need to expose React Intl's locale data on the request for the user's
// locale. This function will also cache the scripts by lang in memory.
const localeDataCache = new Map()
const getLocaleDataScript = (locale) => {
const lang = locale.split('-')[0]
if (!localeDataCache.has(lang)) {
const localeDataFile = require.resolve(
`@formatjs/intl-relativetimeformat/dist/locale-data/${lang}`
)
const localeDataScript = readFileSync(localeDataFile, 'utf8')
localeDataCache.set(lang, localeDataScript)
}
return localeDataCache.get(lang)
}
// We need to load and expose the translations on the request for the user's
// locale. These will only be used in production, in dev the `defaultMessage` in
// each message description in the source code will be used.
const getMessages = (locale) => {
return require(`./lang/${locale}.json`)
}
app.prepare().then(() => {
createServer((req, res) => {
const accept = accepts(req)
const locale = accept.language(supportedLanguages) || 'en'
req.locale = locale
req.localeDataScript = getLocaleDataScript(locale)
req.messages = dev ? {} : getMessages(locale)
handle(req, res)
}).listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})

View file

@ -0,0 +1,29 @@
import {basename} from 'path';
import {sync as globSync} from 'glob';
import {createServer} from 'http';
import accepts from 'accepts';
import next from 'next';
import {polyfill} from './polyfills';
// Get the supported languages by looking for translations in the `lang/` dir.
const supportedLanguages = globSync('./compiled-lang/*.json').map(f =>
basename(f, '.json')
);
const SUPPORTED_LOCALES = ['en', 'fr'];
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({dev});
const handle = app.getRequestHandler();
Promise.all([app.prepare(), ...SUPPORTED_LOCALES.map(polyfill)]).then(() => {
createServer((req, res) => {
const accept = accepts(req);
const locale = accept.language(supportedLanguages) || 'en';
(req as any).locale = locale;
handle(req, res);
}).listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"jsx": "preserve",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext", "ESNext.Intl", "ES2018.Intl"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"noEmit": false
}
}