From 112d425d34ac6b3f7553c7559d56cc1e8d90acdc Mon Sep 17 00:00:00 2001 From: lotyp Date: Mon, 17 Jul 2023 12:54:35 +0300 Subject: [PATCH] feat: switch to ESM module types --- apps/docs/package.json | 20 +- apps/storybook/package.json | 24 +- apps/web/{.eslintrc.js => .eslintrc.cjs} | 19 +- apps/web/{.size-limit.js => .size-limit.cjs} | 13 +- apps/web/jest.config.js | 36 +- ...next.config.js => next-i18next.config.mjs} | 9 +- ...emap.config.js => next-sitemap.config.cjs} | 0 apps/web/next.config.mjs | 235 +- apps/web/package.json | 80 +- apps/web/playwright.config.old.ts | 109 - apps/web/playwright.config.ts | 1 - .../{postcss.config.js => postcss.config.cjs} | 0 apps/web/sentry.client.config.ts | 28 +- apps/web/src/config/validated-server-env.mjs | 36 + .../features/auth/components/LoginForm.tsx | 17 +- .../src/features/system/pages/ErrorPage.tsx | 2 +- .../web/src/lib/i18n/getServerTranslations.ts | 2 +- apps/web/src/pages/_app.tsx | 7 +- apps/web/src/pages/_document.tsx | 19 +- apps/web/src/themes/shared/browser-fonts.js | 15 +- .../web/src/themes/tailwind/tailwind.theme.js | 6 +- ...{tailwind.config.js => tailwind.config.ts} | 21 +- apps/web/tsconfig.jest.json | 5 +- package.json | 44 +- packages/common-i18n/package.json | 12 +- packages/facebook-pixel/package.json | 6 +- packages/google-tag-manager/package.json | 6 +- packages/ui/package.json | 38 +- pnpm-lock.yaml | 5327 ++++++++++------- tsconfig.base.json | 11 +- 30 files changed, 3764 insertions(+), 2384 deletions(-) rename apps/web/{.eslintrc.js => .eslintrc.cjs} (81%) rename apps/web/{.size-limit.js => .size-limit.cjs} (75%) rename apps/web/{next-i18next.config.js => next-i18next.config.mjs} (80%) rename apps/web/{next-sitemap.config.js => next-sitemap.config.cjs} (100%) delete mode 100644 apps/web/playwright.config.old.ts rename apps/web/{postcss.config.js => postcss.config.cjs} (100%) create mode 100644 apps/web/src/config/validated-server-env.mjs rename apps/web/{tailwind.config.js => tailwind.config.ts} (60%) diff --git a/apps/docs/package.json b/apps/docs/package.json index 929c8732..565faf15 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -14,20 +14,20 @@ "dependencies": { "@vercel/analytics": "1.0.1", "@wayofdev/ui": "workspace:*", - "next": "13.4.6", - "nextra": "2.7.1", - "nextra-theme-docs": "2.7.1", + "next": "13.4.10", + "nextra": "2.10.0", + "nextra-theme-docs": "2.10.0", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { - "@types/node": "18.16.18", - "@types/react": "18.2.12", - "@types/react-dom": "18.2.5", - "@wayofdev/eslint-config-bases": "2.0.7", + "@types/node": "20.4.2", + "@types/react": "18.2.15", + "@types/react-dom": "18.2.7", + "@wayofdev/eslint-config-bases": "3.0.1", "es-check": "7.1.1", - "eslint": "8.36.0", - "sharp": "0.32.1", - "typescript": "5.0.2" + "eslint": "8.45.0", + "sharp": "0.32.3", + "typescript": "5.1.6" } } diff --git a/apps/storybook/package.json b/apps/storybook/package.json index a58b36e0..0feecc10 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -28,24 +28,24 @@ "@storybook/react-vite": "7.0.21", "@storybook/testing-library": "0.1.0", "@tailwindcss/aspect-ratio": "^0.4.2", - "@tailwindcss/forms": "^0.5.3", - "@types/react": "18.2.12", - "@types/react-dom": "18.2.5", + "@tailwindcss/forms": "^0.5.4", + "@types/react": "18.2.15", + "@types/react-dom": "18.2.7", "@vitejs/plugin-react-swc": "3.3.2", - "@wayofdev/eslint-config-bases": "2.0.7", - "@wayofdev/postcss-config": "2.0.8", + "@wayofdev/eslint-config-bases": "3.0.1", + "@wayofdev/postcss-config": "3.0.1", "autoprefixer": "^10.4.14", - "eslint": "8.36.0", - "postcss": "8.4.24", + "eslint": "8.45.0", + "postcss": "8.4.26", "postcss-100vh-fix": "1.0.2", "postcss-cli": "10.1.0", "postcss-flexbugs-fixes": "5.0.2", "postcss-normalize": "10.0.1", - "postcss-preset-env": "8.5.0", + "postcss-preset-env": "9.0.0", "postcss-reporter": "7.0.5", - "storybook": "7.0.21", - "tailwindcss": "3.2.7", - "typescript": "5.0.2", - "vite": "4.3.9" + "storybook": "7.0.27", + "tailwindcss": "3.3.3", + "typescript": "5.1.6", + "vite": "4.4.4" } } diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.cjs similarity index 81% rename from apps/web/.eslintrc.js rename to apps/web/.eslintrc.cjs index 729ebf75..6cafb570 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.cjs @@ -7,7 +7,9 @@ // Workaround for https://github.com/eslint/eslint/issues/3458 (re-export of @rushstack/eslint-patch) require('@wayofdev/eslint-config-bases/patch/modern-module-resolution') -const { getDefaultIgnorePatterns } = require('@wayofdev/eslint-config-bases/helpers') +const { + getDefaultIgnorePatterns, +} = require('@wayofdev/eslint-config-bases/helpers') module.exports = { root: true, @@ -32,6 +34,8 @@ module.exports = { '@wayofdev/eslint-config-bases/prettier-plugin', ], rules: { + // https://medium.com/@steven-lemon182/are-typescript-barrel-files-an-anti-pattern-72a713004250 + 'import/no-cycle': 2, // https://github.com/vercel/next.js/discussions/16832 '@next/next/no-img-element': 'off', // For the sake of example @@ -41,16 +45,23 @@ module.exports = { }, overrides: [ { - files: ['src/pages/\\_*.{ts,tsx}'], + files: ['next.config.mjs'], rules: { - 'react/display-name': 'off', + 'import/order': 'off', + '@typescript-eslint/ban-ts-comment': 'off', }, }, { - files: ['src/stories/*.ts'], + files: ['tailwind.config.ts'], rules: { '@typescript-eslint/naming-convention': 'off', }, }, + { + files: ['src/pages/\\_*.{ts,tsx}'], + rules: { + 'react/display-name': 'off', + }, + }, ], } diff --git a/apps/web/.size-limit.js b/apps/web/.size-limit.cjs similarity index 75% rename from apps/web/.size-limit.js rename to apps/web/.size-limit.cjs index 8adc853c..a132e946 100644 --- a/apps/web/.size-limit.js +++ b/apps/web/.size-limit.cjs @@ -5,7 +5,9 @@ let manifest try { manifest = require('./.next/build-manifest.json') } catch (e) { - throw new Error('Cannot find a NextJs build folder, did you forget to build ?') + throw new Error( + 'Cannot find a NextJs build folder, did you forget to build ?' + ) } const pages = manifest.pages @@ -16,10 +18,11 @@ const limitCfg = { '/404': '150kb', '/_app': '180kb', '/_error': '140kb', - '/_monitor/sentry/csr-page': '85kb', - '/_monitor/sentry/ssr-page': '85kb', + '/_monitor/sentry/csr-page': '105kb', + '/_monitor/sentry/ssr-page': '105kb', + '/_monitor/preview/error-page': '105kb', '/auth/login': '160kb', - '/home': '105kb', + '/home': '120kb', }, } @@ -40,6 +43,6 @@ module.exports = [ { name: 'CSS', path: ['./.next/static/css/**/*.css'], - limit: '10 kB', + limit: '15 kB', }, ] diff --git a/apps/web/jest.config.js b/apps/web/jest.config.js index 7678a435..2354414d 100644 --- a/apps/web/jest.config.js +++ b/apps/web/jest.config.js @@ -1,12 +1,14 @@ // @ts-check -const { pathsToModuleNameMapper } = require('ts-jest') -const { getTsconfig } = require('get-tsconfig') -const { getJestCachePath } = require('../../cache.config') +// const { getJestCachePath } = require('../../cache.config') +// const packageJson = require('./package.json') +// const tsConfigFile = './tsconfig.jest.json' -const packageJson = require('./package.json') +import { getTsconfig } from 'get-tsconfig' +import { pathsToModuleNameMapper } from 'ts-jest' -const tsConfigFile = './tsconfig.jest.json' +const tsConfigFile = new URL('./tsconfig.jest.json', import.meta.url).pathname +const tsConfigPathsFile = new URL('./tsconfig.json', import.meta.url).pathname /** * Transform the tsconfig paths into jest compatible one (support extends) @@ -20,20 +22,20 @@ const getTsConfigBasePaths = tsConfigFile => { const tsPaths = parsedTsConfig.config.compilerOptions?.paths return tsPaths ? pathsToModuleNameMapper(tsPaths, { - prefix: '/src', + prefix: '/', }) : {} } /** @type {import('ts-jest').JestConfigWithTsJest} */ const config = { - displayName: `${packageJson.name}:unit`, - cacheDirectory: getJestCachePath(packageJson.name), + displayName: `web:unit`, testEnvironment: 'jsdom', + extensionsToTreatAsEsm: ['.ts', '.tsx'], verbose: true, - rootDir: './', - testMatch: ['/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], + rootDir: './src', setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], + testMatch: ['/**/*.{spec,test}.{js,jsx,ts,tsx}'], transform: { '^.+\\.m?[tj]sx?$': [ 'ts-jest', @@ -43,15 +45,19 @@ const config = { ], }, moduleNameMapper: { + '^.+\\.(svg)$': '/../config/tests/ReactSvgrMock.tsx', '.+\\.(css|styl|less|sass|scss)$': 'jest-css-modules-transform', - '\\.svg$': '/config/tests/ReactSvgrMock.tsx', - ...getTsConfigBasePaths(tsConfigFile), + ...getTsConfigBasePaths(tsConfigPathsFile), }, // false by default, overrides in cli, ie: yarn test:unit --collect-coverage=true collectCoverage: false, - coverageDirectory: '/coverage', - collectCoverageFrom: ['/**/*.{ts,tsx,js,jsx}', '!**/*.test.{js,ts}', '!**/__mock__/*'], + coverageDirectory: '/../coverage', + collectCoverageFrom: [ + '/**/*.{ts,tsx,js,jsx}', + '!**/*.test.{js,ts}', + '!**/__mock__/*', + ], transformIgnorePatterns: ['/node_modules/(?!@vercel/analytics)/'], } -module.exports = config +export default config diff --git a/apps/web/next-i18next.config.js b/apps/web/next-i18next.config.mjs similarity index 80% rename from apps/web/next-i18next.config.js rename to apps/web/next-i18next.config.mjs index 6c2a9915..a770a35f 100644 --- a/apps/web/next-i18next.config.js +++ b/apps/web/next-i18next.config.mjs @@ -1,12 +1,15 @@ -const defaultLocale = 'en' +import path from 'path' + const debugI18n = ['true', 1].includes(process?.env?.NEXT_DEBUG_I18N ?? 'false') const localePublicFolder = undefined +export const defaultLocale = 'en' + /** * @type {import('next-i18next').UserConfig} */ -module.exports = { +export default { i18n: { defaultLocale, locales: ['en', 'fr'], @@ -26,6 +29,6 @@ module.exports = { */ localePath: typeof window === 'undefined' - ? require('path').resolve('../../packages/common-i18n/src/locales') + ? path.resolve('../../packages/common-i18n/src/locales') : localePublicFolder, } diff --git a/apps/web/next-sitemap.config.js b/apps/web/next-sitemap.config.cjs similarity index 100% rename from apps/web/next-sitemap.config.js rename to apps/web/next-sitemap.config.cjs diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ec1c806d..9c175846 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,16 +1,31 @@ // @ts-check -// This file sets a custom webpack configuration to use your Next.js app -// with Sentry. -// @link https://nextjs.org/docs/api-reference/next.config.js/introduction -// @link https://docs.sentry.io/platforms/javascript/guides/nextjs/ -// @link https://github.com/vercel/next.js/tree/canary/examples/with-sentry - +/** + * This file sets a custom webpack configuration to use your Next.js app + * with Sentry. + * @link https://nextjs.org/docs/api-reference/next.config.js/introduction + * @link https://docs.sentry.io/platforms/javascript/guides/nextjs/ + * @link https://github.com/vercel/next.js/tree/canary/examples/with-sentry + * + */ import { readFileSync } from 'node:fs' +import path from 'node:path' +import url from 'node:url' import withBundleAnalyzer from '@next/bundle-analyzer' import { withSentryConfig } from '@sentry/nextjs' // https://docs.sentry.io/platforms/javascript/guides/nextjs/ +import { createSecureHeaders } from 'next-secure-headers' import pc from 'picocolors' -import nextI18nConfig from './next-i18next.config.js' +import nextI18nConfig from './next-i18next.config.mjs' +import { getValidatedServerEnv } from './src/config/validated-server-env.mjs' + +// validate server env +getValidatedServerEnv() + +const workspaceRoot = path.resolve( + path.dirname(url.fileURLToPath(import.meta.url)), + '..', + '..' +) /** * Once supported replace by node / eslint / ts and out of experimental, replace by @@ -24,10 +39,20 @@ const packageJson = JSON.parse( const trueEnv = ['true', '1', 'yes'] const isProd = process.env.NODE_ENV === 'production' const isCI = trueEnv.includes(process.env?.CI ?? 'false') +const enableCSP = true -const NEXT_IGNORE_TYPE_CHECK = trueEnv.includes(process.env?.NEXT_IGNORE_TYPE_CHECK ?? 'false') -const NEXT_IGNORE_ESLINT = trueEnv.includes(process.env?.NEXT_IGNORE_ESLINT ?? 'false') -const SENTRY_UPLOAD_DRY_RUN = trueEnv.includes(process.env?.SENTRY_UPLOAD_DRY_RUN ?? 'false') +const NEXT_STANDALONE_BUILD = trueEnv.includes( + process.env?.NEXT_STANDALONE_BUILD ?? 'false' +) +const NEXT_IGNORE_TYPE_CHECK = trueEnv.includes( + process.env?.NEXT_IGNORE_TYPE_CHECK ?? 'false' +) +const NEXT_IGNORE_ESLINT = trueEnv.includes( + process.env?.NEXT_IGNORE_ESLINT ?? 'false' +) +const SENTRY_UPLOAD_DRY_RUN = trueEnv.includes( + process.env?.SENTRY_UPLOAD_DRY_RUN ?? 'false' +) const DISABLE_SENTRY = trueEnv.includes(process.env?.DISABLE_SENTRY ?? 'false') const SENTRY_DEBUG = trueEnv.includes(process.env?.SENTRY_DEBUG ?? 'false') const SENTRY_TRACING = trueEnv.includes(process.env?.SENTRY_TRACING ?? 'false') @@ -37,7 +62,9 @@ const SENTRY_TRACING = trueEnv.includes(process.env?.SENTRY_TRACING ?? 'false') * to deliver an image or deploy the files. * @link https://nextjs.org/docs/advanced-features/source-maps */ -const disableSourceMaps = trueEnv.includes(process.env?.NEXT_DISABLE_SOURCEMAPS ?? 'false') +const disableSourceMaps = trueEnv.includes( + process.env?.NEXT_DISABLE_SOURCEMAPS ?? 'false' +) if (disableSourceMaps) { console.log( @@ -47,6 +74,57 @@ if (disableSourceMaps) { ) } +// @link https://github.com/jagaapple/next-secure-headers +const secureHeaders = createSecureHeaders({ + contentSecurityPolicy: { + directives: enableCSP + ? { + defaultSrc: "'self'", + styleSrc: [ + "'self'", + "'unsafe-inline'", + 'https://unpkg.com/@graphql-yoga/graphiql/dist/style.css', + 'https://meet.jitsi.si', + 'https://8x8.vc', + ], + scriptSrc: [ + "'self'", + "'unsafe-eval'", + "'unsafe-inline'", + 'https://unpkg.com/@graphql-yoga/graphiql', + // 'https://meet.jit.si/external_api.js', + // 'https://8x8.vc/external_api.js', + ], + frameSrc: [ + "'self'", + // 'https://meet.jit.si', + // 'https://8x8.vc', + ], + connectSrc: [ + "'self'", + 'https://vitals.vercel-insights.com', + 'https://*.sentry.io', + // 'wss://ws.pusherapp.com', + // 'wss://ws-eu.pusher.com', + // 'https://sockjs.pusher.com', + // 'https://sockjs-eu.pusher.com', + ], + imgSrc: ["'self'", 'https:', 'http:', 'data:'], + workerSrc: ['blob:'], + } + : {}, + }, + ...(enableCSP && process.env.NODE_ENV === 'production' + ? { + forceHTTPSRedirect: [ + true, + { maxAge: 60 * 60 * 24 * 4, includeSubDomains: true }, + ], + } + : {}), + referrerPolicy: 'same-origin', +}) + /** * @type {import('next').NextConfig} */ @@ -56,8 +134,10 @@ const nextConfig = { i18n: nextI18nConfig.i18n, optimizeFonts: true, - // @link https://beta.nextjs.org/docs/api-reference/next.config.js#transpilepackages - transpilePackages: ['@wayofdev/ui'], + httpAgentOptions: { + // @link https://nextjs.org/blog/next-11-1#builds--data-fetching + keepAlive: true, + }, onDemandEntries: { // period (in ms) where the server will keep pages in the buffer @@ -72,33 +152,88 @@ const nextConfig = { // emotion: true, }, + sentry: { + hideSourceMaps: true, + // To disable the automatic instrumentation of API route handlers and server-side data fetching functions + // In other words, disable if you prefer to explicitly handle sentry per api routes (ie: wrapApiHandlerWithSentry) + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#configure-server-side-auto-instrumentation + autoInstrumentServerFunctions: false, + }, + + // @link https://nextjs.org/docs/basic-features/image-optimization + images: { + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + minimumCacheTTL: 60, + formats: ['image/webp'], + loader: 'default', + dangerouslyAllowSVG: false, + disableStaticImages: false, + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + remotePatterns: [ + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, + ], + unoptimized: false, + }, + + // Packages to be transpiled part of nextjs build to follow nextjs/browserslist compatibility. + // This replaces next-transpile-modules starting from nextjs 13.1, if you're relying on css + transpilePackages: isProd ? ['ky', '@wayofdev/ui'] : [], + // Standalone build // @link https://nextjs.org/docs/advanced-features/output-file-tracing#automatically-copying-traced-files-experimental - output: 'standalone', + ...(NEXT_STANDALONE_BUILD + ? { output: 'standalone', outputFileTracing: true } + : {}), - // Optional build-time configuration options - sentry: { - // See the sections below for information on the following options: - // 'Configure Source Maps': - // - disableServerWebpackPlugin - // - disableClientWebpackPlugin - // - hideSourceMaps - // - widenClientFileUpload - // 'Configure Legacy Browser Support': - // - transpileClientSDK - // 'Configure Serverside Auto-instrumentation': - // - autoInstrumentServerFunctions - // - excludeServerRoutes - // 'Configure Tunneling to avoid Ad-Blockers': - // - tunnelRoute + experimental: { + // @link https://nextjs.org/docs/advanced-features/output-file-tracing#caveats + ...(NEXT_STANDALONE_BUILD ? { outputFileTracingRoot: workspaceRoot } : {}), + + // Useful in conjunction with to `output: 'standalone'` and `outputFileTracing: true` + // to keep lambdas sizes / docker images low when vercel/nft isn't able to + // drop unneeded deps for you. ie: esbuil-musl, swc-musl... when not actually needed // - // Use `hidden-source-map` rather than `source-map` as the Webpack `devtool` - // for client-side builds. (This will be the default starting in - // `@sentry/nextjs` version 8.0.0.) See - // https://webpack.js.org/configuration/devtool/ and - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map - // for more information. - hideSourceMaps: true, + // Note that yarn 3+/4 is less impacted thanks to supportedArchitectures. + // See https://yarnpkg.com/configuration/yarnrc#supportedArchitectures and + // config example in https://github.com/belgattitude/nextjs-monorepo-example/pull/3582 + // NPM/PNPM might adopt https://github.com/npm/rfcs/pull/519 in the future. + // + // Caution: use it with care because you'll have to maintain this over time. + // + // How to debug in vercel: set NEXT_DEBUG_FUNCTION_SIZE=1 in vercel env, then + // check the last lines of vercel build. + // + // Related issue: https://github.com/vercel/next.js/issues/42641 + + // Caution if using pnpm you might also need to consider that things are hoisted + // under node_modules/.pnpm/. Depends on version + // + // outputFileTracingExcludes: { + // '*': [ + // '**/node_modules/@swc/core-linux-x64-gnu/**/*', + // '**/node_modules/@swc/core-linux-x64-musl/**/*', + // // If you're nor relying on mdx-remote... drop this + // '**/node_modules/esbuild/linux/**/*', + // '**/node_modules/webpack/**/*', + // '**/node_modules/terser/**/*', + // // If you're not relying on sentry edge or any weird stuff... drop this too + // // https://github.com/getsentry/sentry-javascript/pull/6982 + // '**/node_modules/rollup/**/*', + // ], + // }, + + // Prefer loading of ES Modules over CommonJS + // @link {https://nextjs.org/blog/next-11-1#es-modules-support|Blog 11.1.0} + // @link {https://github.com/vercel/next.js/discussions/27876|Discussion} + esmExternals: true, + // Experimental monorepo support + // @link {https://github.com/vercel/next.js/pull/22867|Original PR} + // @link {https://github.com/vercel/next.js/discussions/26420|Discussion} + externalDir: true, }, typescript: { @@ -110,6 +245,20 @@ const nextConfig = { // dirs: [`${__dirname}/src`], }, + async headers() { + return [ + { + // All page routes, not the api ones + source: '/:path((?!api).*)*', + headers: [ + ...secureHeaders, + { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' }, + { key: 'Cross-Origin-Embedder-Policy', value: 'same-origin' }, + ], + }, + ] + }, + // @link https://nextjs.org/docs/api-reference/next.config.js/rewrites async rewrites() { return [ @@ -145,7 +294,7 @@ const nextConfig = { loader: '@svgr/webpack', // https://react-svgr.com/docs/webpack/#passing-options options: { - svgo: true, + svgo: isProd, // @link https://github.com/svg/svgo#configuration svgoConfig: { multipass: false, @@ -172,6 +321,7 @@ const nextConfig = { let config = nextConfig if (!DISABLE_SENTRY) { + // @ts-ignore cause sentry is not always following nextjs types config = withSentryConfig(config, { // Additional config options for the Sentry Webpack plugin. Keep in mind that // the following options are set automatically, and overriding them is not @@ -188,17 +338,6 @@ if (!DISABLE_SENTRY) { // Suppresses all logs (useful for --json option). Defaults to false. silent: isProd, - - // release: '', - // url: '', - // org: '', - // project: '', - // authToken: '', - // configFile: '', - // stripPrefix: '', - // urlPrefix: '', - // include: '', - // ignore: '', }) } else { const { sentry, ...rest } = config diff --git a/apps/web/package.json b/apps/web/package.json index e8013671..52aa64aa 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,10 +2,11 @@ "name": "@wayofdev/web", "version": "1.3.0", "private": true, + "type": "module", "scripts": { "analyze": "ANALYZE=true NEXT_IGNORE_TYPE_CHECK=true NEXT_IGNORE_ESLINT=true SENTRY_UPLOAD_DRY_RUN=true next build", "build": "next build", - "postbuild": "next-sitemap", + "postbuild": "next-sitemap --config next-sitemap.config.cjs", "dev": "next dev", "lint": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.mdx --cache --cache-location ../../.cache/eslint/web.eslintcache", "lint:dist": "es-check -v", @@ -18,76 +19,79 @@ "test:watch": "jest --config ./jest.config.js --watch" }, "dependencies": { - "@fontsource/inter": "4.5.15", + "@fontsource-variable/inter": "^5.0.5", + "@fontsource/inter": "5.0.5", "@heroicons/react": "2.0.18", "@hookform/resolvers": "3.1.1", - "@httpx/exception": "1.8.1", + "@httpx/exception": "1.8.2", + "@soluble/dsn-parser": "^1.9.2", "@vercel/analytics": "1.0.1", "@wayofdev/facebook-pixel": "workspace:*", "@wayofdev/google-tag-manager": "workspace:*", - "@wayofdev/lint-staged-config": "2.0.7", + "@wayofdev/lint-staged-config": "2.1.1", "@wayofdev/ui": "workspace:*", - "clsx": "1.2.1", - "i18next": "22.5.1", - "next": "13.4.6", + "clsx": "2.0.0", + "i18next": "23.2.11", + "next": "13.4.10", "next-auth": "4.22.1", - "next-i18next": "13.3.0", - "next-seo": "6.0.0", - "next-sitemap": "4.1.3", + "next-i18next": "14.0.0", + "next-secure-headers": "^2.2.0", + "next-seo": "6.1.0", + "next-sitemap": "4.1.8", "react": "18.2.0", "react-dom": "18.2.0", - "react-hook-form": "7.44.3", - "react-i18next": "12.3.1", - "type-fest": "3.12.0", + "react-hook-form": "7.45.1", + "react-i18next": "13.0.2", + "type-fest": "3.13.0", "zod": "3.21.4" }, "devDependencies": { - "@next/bundle-analyzer": "13.4.6", - "@next/env": "13.4.6", - "@playwright/test": "1.35.1", - "@sentry/nextjs": "7.45.0", - "@size-limit/file": "8.2.4", + "@next/bundle-analyzer": "13.4.10", + "@next/env": "13.4.10", + "@playwright/test": "1.36.1", + "@sentry/nextjs": "7.58.1", + "@size-limit/file": "8.2.6", "@tailwindcss/aspect-ratio": "0.4.2", - "@tailwindcss/forms": "0.5.3", + "@tailwindcss/forms": "0.5.4", "@testing-library/dom": "9.3.1", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.0.0", "@testing-library/user-event": "14.4.3", "@types/facebook-pixel": "0.0.25", "@types/hoist-non-react-statics": "3.3.1", - "@types/jest": "29.5.2", - "@types/node": "18.16.18", - "@types/react": "18.2.12", - "@types/react-dom": "18.2.5", + "@types/jest": "29.5.3", + "@types/node": "20.4.2", + "@types/react": "18.2.15", + "@types/react-dom": "18.2.7", "@types/react-test-renderer": "18.0.0", - "@types/testing-library__jest-dom": "5.14.6", + "@types/testing-library__jest-dom": "5.14.8", "@wayofdev/common-i18n": "workspace:*", - "@wayofdev/eslint-config-bases": "2.0.7", - "@wayofdev/postcss-config": "2.0.8", + "@wayofdev/eslint-config-bases": "3.0.1", + "@wayofdev/postcss-config": "3.0.1", "autoprefixer": "10.4.14", "css-loader": "6.8.1", "cssnano": "6.0.1", "es-check": "7.1.1", - "eslint": "8.36.0", - "eslint-config-next": "13.4.6", - "get-tsconfig": "4.6.0", - "jest": "29.5.0", + "eslint": "8.45.0", + "eslint-config-next": "13.4.10", + "get-tsconfig": "4.6.2", + "jest": "29.6.1", "jest-css-modules-transform": "4.4.2", - "jest-environment-jsdom": "29.5.0", + "jest-environment-jsdom": "29.6.1", "picocolors": "1.0.0", - "postcss": "8.4.24", + "postcss": "8.4.26", "postcss-100vh-fix": "1.0.2", "postcss-flexbugs-fixes": "5.0.2", "postcss-import": "15.1.0", - "postcss-preset-env": "8.5.0", + "postcss-preset-env": "9.0.0", "postcss-reporter": "7.0.5", "react-test-renderer": "18.2.0", "sanitize.css": "13.0.0", - "size-limit": "8.2.4", + "size-limit": "8.2.6", "style-loader": "3.3.3", - "tailwindcss": "3.2.7", - "ts-jest": "29.1.0", - "tslib": "2.5.3", - "typescript": "5.0.2" + "tailwindcss": "3.3.3", + "ts-jest": "29.1.1", + "tslib": "2.6.0", + "typescript": "5.1.6" } } diff --git a/apps/web/playwright.config.old.ts b/apps/web/playwright.config.old.ts deleted file mode 100644 index 5cc72c0c..00000000 --- a/apps/web/playwright.config.old.ts +++ /dev/null @@ -1,109 +0,0 @@ -import path from 'path' -import { loadEnvConfig } from '@next/env' -import type { PlaywrightTestConfig } from '@playwright/test' -import { devices } from '@playwright/test' -import pc from 'picocolors' - -const isCI = ['true', '1'].includes(process.env?.CI ?? '') -const openBrowserReport = process.env?.PLAYWRIGHT_OPEN_BROWSER_REPORT ?? 'never' -const outputDir = path.join(__dirname, 'e2e/.out') - -// Use process.env.PORT by default and fallback to port 3000 -const port = process.env.PORT || 3000 - -// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port -const baseURL = `http://localhost:${port}` - -function getNextJsEnv(): Record { - const { combinedEnv, loadedEnvFiles } = loadEnvConfig(__dirname) - loadedEnvFiles.forEach(file => { - console.log(`${pc.green('notice')}- Loaded nextjs environment file: './${file.path}'`) - }) - return Object.keys(combinedEnv).reduce>((acc, key) => { - const v = combinedEnv[key] - if (v !== undefined) acc[key] = v - return acc - }, {}) -} - -// Reference: https://playwright.dev/docs/test-configuration -const config: PlaywrightTestConfig = { - // Timeout per test - timeout: 30 * 1000, - // Test directory - testDir: path.join(__dirname, 'e2e'), - // If a test fails, retry it additional 2 times - retries: 2, - // Artifacts folder where screenshots, videos, and traces are stored. - outputDir: `${outputDir}/output`, - preserveOutput: 'always', - - /* Opt out of parallel tests on CI. */ - workers: isCI ? 1 : undefined, - - reporter: [ - isCI ? ['github'] : ['list'], - [ - 'json', - { - outputFile: `${outputDir}/reports/test-results.json`, - }, - ], - [ - 'html', - { - outputFolder: `${outputDir}/reports/html`, - open: isCI ? 'never' : openBrowserReport, - }, - ], - ], - - // Run your local dev server before starting the tests: - // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests - webServer: { - command: 'NEXT_IGNORE_TYPE_CHECKS=1 pnpm --filter=web build && pnpm --filter=web start', - url: baseURL, - timeout: 60 * 1000, - reuseExistingServer: !isCI, - env: getNextJsEnv(), - }, - - use: { - // Use baseURL so to make navigations relative. - // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url - baseURL, - - // Retry a test if it's failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc. - // More information: https://playwright.dev/docs/trace-viewer - trace: 'retry-with-trace', - - // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context - contextOptions: { - ignoreHTTPSErrors: true, - }, - }, - - projects: [ - { - name: 'Desktop Chrome', - use: { - ...devices['Desktop Chrome'], - }, - }, - { - name: 'Mobile Chrome', - use: { - ...devices['Pixel 5'], - }, - }, - ...(isCI - ? [] - : [ - { - name: 'Mobile Safari', - use: devices['iPhone 12'], - }, - ]), - ], -} -export default config diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index fa414dce..3e12bba4 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -4,7 +4,6 @@ import type { PlaywrightTestConfig } from '@playwright/test' import { devices } from '@playwright/test' const isCI = ['true', '1'].includes(process.env?.CI ?? '') - const outputDir = new URL('./e2e/.out', import.meta.url).pathname const testDir = new URL('e2e', import.meta.url).pathname diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.cjs similarity index 100% rename from apps/web/postcss.config.js rename to apps/web/postcss.config.cjs diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts index 9a4bbfad..e4a5ae30 100644 --- a/apps/web/sentry.client.config.ts +++ b/apps/web/sentry.client.config.ts @@ -7,11 +7,25 @@ import { init as sentryInit, Replay } from '@sentry/nextjs' sentryInit({ dsn: process.env.SENTRY_DSN || process.env.NEXT_SENTRY_DSN, + integrations: [new Replay()], + // Adjust this value in production, or use tracesSampler for greater control // @see https://develop.sentry.dev/sdk/performance/ // To turn it off, remove the line // @see https://github.com/getsentry/sentry-javascript/discussions/4503#discussioncomment-2143116 - tracesSampleRate: ['false', '0'].includes(process.env.SENTRY_TRACING ?? '') ? undefined : 1, + tracesSampleRate: ['false', '0'].includes(process.env.SENTRY_TRACING ?? '') + ? undefined + : 1, + + // Note: The Replay integration only needs to be added to your sentry.client.config.js file. + // It will not run if it is added into sentry.server.config.js. + // + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + // If the entire session is not sampled, use the below sample rate to sample + // sessions when an error occurs. + replaysOnErrorSampleRate: 1.0, // ... // Note: if you want to override the automatic release value, do not set a @@ -31,16 +45,4 @@ sentryInit({ */ 'ResizeObserver loop limit exceeded', ], - - // Note: The Replay integration only needs to be added to your sentry.client.config.js file. - // It will not run if it is added into sentry.server.config.js. - // - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - replaysSessionSampleRate: 0.1, - // If the entire session is not sampled, use the below sample rate to sample - // sessions when an error occurs. - replaysOnErrorSampleRate: 1.0, - - integrations: [new Replay()], }) diff --git a/apps/web/src/config/validated-server-env.mjs b/apps/web/src/config/validated-server-env.mjs new file mode 100644 index 00000000..8759b5e3 --- /dev/null +++ b/apps/web/src/config/validated-server-env.mjs @@ -0,0 +1,36 @@ +// @ts-check + +import { isParsableDsn } from '@soluble/dsn-parser' +import pc from 'picocolors' +import { z } from 'zod' + +const dsnSchema = z.custom(dsn => isParsableDsn(dsn), 'Invalid DSN format.') + +export const serverEnvSchema = z.object({ + APP_CACHE_DSN: z.union([dsnSchema, z.literal('')]), + NEXTAUTH_SECRET: z.string().min(15), + NEXTAUTH_URL: z.string().url(), +}) + +export const getValidatedServerEnv = () => { + const parsedEnv = serverEnvSchema.safeParse(process.env) + if (!parsedEnv.success) { + if (process) { + console.error( + pc.red('error'.padEnd(6)).concat('- Invalid server env(s):'), + Object.keys(parsedEnv.error.flatten().fieldErrors).join(',') + ) + console.error(JSON.stringify(parsedEnv.error.format(), null, 2)) + process.exit(1) + } else { + throw new Error( + `Invalid server env(s): ${JSON.stringify( + parsedEnv.error.format(), + null, + 2 + )}}` + ) + } + } + return parsedEnv.data +} diff --git a/apps/web/src/features/auth/components/LoginForm.tsx b/apps/web/src/features/auth/components/LoginForm.tsx index d746b66a..64fc76f4 100644 --- a/apps/web/src/features/auth/components/LoginForm.tsx +++ b/apps/web/src/features/auth/components/LoginForm.tsx @@ -38,7 +38,12 @@ export const LoginForm: FC = _props => { // callbackUrl: '/', redirect: false, }) - const { ok = false, url, status = 500, error = 'Server or network Error' } = result ?? {} + const { + ok = false, + url, + status = 500, + error = 'Server or network Error', + } = result ?? {} if (ok) { console.log('Will redirect to ' + url) @@ -62,7 +67,9 @@ export const LoginForm: FC = _props => { placeholder="Username or email" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" /> - {errors?.username &&

{errors.username.message}

} + {errors?.username && ( +

{errors.username.message}

+ )}