diff --git a/.codeclimate.yml b/.codeclimate.yml index dab96e409..c60da8a7b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -16,7 +16,7 @@ checks: method-complexity: enabled: true config: - threshold: 10 + threshold: 25 # 10 by default method-count: enabled: true config: @@ -24,7 +24,7 @@ checks: method-lines: enabled: true config: - threshold: 100 # 25 by default + threshold: 300 # 25 by default nested-control-flow: enabled: true config: @@ -62,7 +62,9 @@ plugins: exclude_patterns: - "**/*.test.*" - "**/*.spec.*" - - "src/svg/" + - "svg-to-react/" + - "cypress/" + - "src/modules/core/testing/mocks" # Default CC excluded paths: - "config/" diff --git a/.env b/.env new file mode 100644 index 000000000..405c04cf0 --- /dev/null +++ b/.env @@ -0,0 +1,55 @@ +# See https://nextjs.org/docs/basic-features/environment-variables + +# XXX Tips: How is this file meant to be used? +# This file is tracked by git and must only contains NON-SENSITIVE information, which is usually meant to be available in the browser. +# Sensitive information (server-side only) MUST be written in ".env.local" file instead (which isn't tracked by git). + +# XXX Tips: When is this file being used? +# This file is used only when building the Next.js app locally (localhost), whether it's for running `next dev` or `next build`. +# For staging/production stages, the app relies on "vercel.{NEXT_PUBLIC_CUSTOMER_REF}.{NEXT_PUBLIC_APP_STAGE}.yml:build.env". + +# XXX Tips: What's the difference between env vars starting with "NEXT_PUBLIC_" and the others? +# All env variables that DON'T start with "NEXT_PUBLIC_" MUST be manually exposed by ./next.config.js for the project to work locally +# "NEXT_PUBLIC_" has a semantic purpose. If you mean to use a variable on the browser, then you should use "NEXT_PUBLIC_". +# Any non-sensitive env variable should start with "NEXT_PUBLIC_". +# Sensitive information MUST NOT start with "NEXT_PUBLIC_". +# You must be careful to use sensitive information only on the server-side, because if you use them on the browser or getInitialProps, they'll be leaked, even if the variable doesn't start with "NEXT_PUBLIC_". +# Any change to this file needs a server restart to be applied. + +# The stage is "how" the application is running. +# It can be either "development", "staging" or "production". +# This value is also set in each "vercel.*.json" files, so that other stages use their own value. +# Tip: This value must not be changed. +# Tip: You may override it from ".env.local" if you want to simulate another stage, locally. +NEXT_PUBLIC_APP_STAGE=development + +# The name of the NRN preset being used. +# Used by the demo to redirect to the preset branch/documentation. +NEXT_PUBLIC_NRN_PRESET=v2-mst-aptd-gcms-lcz-sty + +# The customer that is being used. +# Tip: You may override it from ".env.local" if you want to simulate another customer, locally. +NEXT_PUBLIC_CUSTOMER_REF=customer1 + +# Locize project ID, can be found in the project "settings" page +# Used to fetch the i18n translations +# Tip: The value being used below is valid, so that you can run the demo locally without having to create your own Locize account, but you cannot make any change +NEXT_PUBLIC_LOCIZE_PROJECT_ID=658fc999-dfa8-4307-b9d7-b4870ad5b968 + +# Amplitude API key, can be found under "Manage Data > Project name > Project settings > API Key" +# Used to send analytics usage +# Tip: The value being used below is valid, so that you can run the demo locally without having to create your own Amplitude account, but you cannot access the data +NEXT_PUBLIC_AMPLITUDE_API_KEY=5ea02d86a6840c165fcc01377131fa13 + +# GraphQL v2 API endpoint (using GraphCMS vendor) +# Used to fetch content from GraphCMS +# Tip: The value being used below is valid, so that you can run the demo locally without having to create your own GraphCMS account +# XXX We only use one stage ("master") due to the free plan's limitation, but using two stages is more secure for enterprise-grade apps +GRAPHQL_API_ENDPOINT=https://api-eu-central-1.graphcms.com/v2/ck73ixhlv09yt01dv2ga1bkbp/master + +# GraphQL API key (using GraphCMS vendor), can be found under "Settings > API Access > Permanent Auth Tokens" +# Used to fetch content from GraphCMS, this token only has read-only permissions to avoid data tempering (especially since we made it public within NRN) +# This variable is only used server-side +# Tip: The value being used below is valid, so that you can run the demo locally without having to create your own GraphCMS account +# XXX For the purpose of this demo, we're tracking this in git, but unless you're building an open API you'll want to move it to ".env.local" (especially if this is your "master" token) +GRAPHQL_API_KEY=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdjbXMtbWFpbi1wcm9kdWN0aW9uIn0.eyJ2ZXJzaW9uIjozLCJpYXQiOjE2MDYyMTEyNDgsImF1ZCI6WyJodHRwczovL2FwaS1ldS1jZW50cmFsLTEuZ3JhcGhjbXMuY29tL3YyL2NrNzNpeGhsdjA5eXQwMWR2MmdhMWJrYnAvbWFzdGVyIl0sImlzcyI6Imh0dHBzOi8vbWFuYWdlbWVudC5ncmFwaGNtcy5jb20vIiwic3ViIjoiMzRiNGMxZDgtNWY1Mi00MWM3LWJjNDYtMDM5MzM1NDc0OTVlIiwianRpIjoiY2todnNwZjA4bXg0aDAxdzliMmN6YXVwZCJ9.TfJDn4V9gGK5n7sI8limca4DnFjxREgsPDV5lf5yhA74BSwHq8XH-nU6cP18qf-IQ_wN7Ujw4MRY4LcazylR0IBli4dzrtKHZ0pEvZilZ0AfJ-80LBJQCVLNEiSuFKnIDUHe58MqMUZT8UoYFaWMH_cl7a3SOmdRCwsm93U5JYkjt7mqoH5Y-WaC3yl4Cq9IYexjHCJZLiFRZh8CL0jV6ddu8ghScegYTo_nJsZxHow12tuKI0I_3cJhdpcRL3HsTZWTQV8PPL0bhbLE1F7y0GksquKuSRScrTMBewYCgDWkxt7EcjRSuUR9WixQPVcTjHpnm_c3lL0HqUsDc_5aJV0POJUa9_T8m4_uFstZtBMtfmg6yBGOB8f06f27JyrCA5NZLNXMScmG06DbGOM8gbquN4-XigTj4t3jwSrzCRZ1zuFZEMoBruH0ElOqJ9hq05_WnGSpyD4NQl9l0bV1TXz5WMzoThlShrpL3f0A74ojXdel8awq9uTPB8qX7AAQa5m7n7lEmsD0tfom4ijiJeFdJakyNpoxY8TXPAgO819MPxE_z3qEynxu3QLS3jW2hc0mteLO0pNkl_twPGIwn7eBlv4u6S31pLXOT5aAIIMqsa1eoi8XwGKFLGhjZzDN_AsF-aBudoK8uhmNYCA7DyqWMXi2rvGyuYNU9Wu3XAs diff --git a/.env.build.example b/.env.build.example deleted file mode 100644 index cae7cc9e9..000000000 --- a/.env.build.example +++ /dev/null @@ -1,34 +0,0 @@ -# XXX Copy this file as ".env.build", will be used only in development stage. For staging/production stages, check the config at "now.CUSTOMER_REF.APP_STAGE.yml". -# XXX Used during the build step in next.config.js, all env variables here must be bundled in ./next.config.js for the project to work locally -# See https://zeit.co/docs/v2/environment-variables-and-secrets#local-development - -# The environment is "where" the application is running. It can be either "development" (localhost) or "production" (on Zeit's servers) -NODE_ENV=development - -# The stage is "how" the application is running. It can be either "development" (localhost), "staging" or "production" (on Zeit's servers) -APP_STAGE=development - -# The NRN preset used for the demo -NRN_PRESET=v1-ssr-mst-aptd-gcms-lcz-sty - -# The customer that is being deployed -CUSTOMER_REF=customer1 - -# Locize project ID, can be found in the project settings page (valid value) -LOCIZE_PROJECT_ID=658fc999-dfa8-4307-b9d7-b4870ad5b968 - -# Locize API key, used to automatically save missing translations - Example: 615384ff-0f39-4c7b-89ca-9b0acbfd0869 (fake value) -# TODO Create a Locize account at https://www.locize.app/?ref=unly-nrn -LOCIZE_API_KEY= - -# Amplitude API key, used to report analytics usage - Example: 615384ff0f394c7b89ca9b0acbfd0869 (fake value) -AMPLITUDE_API_KEY= - -# Sentry DSN, used to report events (errors, etc.) - Example: https://14fa1cae05079675b18cd05403ae5c48@sentry.io/1234567 (fake value) -SENTRY_DSN= - -# GraphQL API endpoint - XXX We only use one stage due to our plan's limitation, but using two stages is the recommended way (valid value) -GRAPHQL_API_ENDPOINT=https://api-euwest.graphcms.com/v1/ck73ixhlv09yt01dv2ga1bkbp/master - -# GraphQL API key (GraphCMS auth token) - Readonly because we want everyone to fetch data but not mess with it (valid value) -GRAPHQL_API_KEY=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdjbXMtbWFpbi1wcm9kdWN0aW9uIn0.eyJ2ZXJzaW9uIjoyLCJ0b2tlbklkIjoiMGEyZWYwZWYtYmY4Ny00ZmVlLTkzYmQtMzBkMzIzYzAwODM4IiwiaWF0IjoxNTgyNzQ2MDc1LCJpc3MiOiJodHRwczovL21hbmFnZW1lbnQuZ3JhcGhjbXMuY29tLyJ9.wE-IDwFnCNpwUj5AMeGsbf2CGcTgkT1KkE9_l6s-bU03sckdDgV2LXCHH5Fb8XwJGAkoeD5bK9yDnY6oNapiusvg4xRqY-hkRxgh8lB_lTXXpIka6HhjIJz1RQO_dObNGFqr41dKihseBqN5Ce4AJvQBIHyJasKX63xe2eEzdUnWfuNUmrG_XStRu1-xDKKp7vFVox272rr-LqgwRymF3eYcw7J-IAag4qpztoU6zNhKJNYzQKdMMejvMDNg34bHNp4TRIbIUIYpytkwCaAb5TsH98xEiFziU5rUr4EYToSltgE46VnqX2npm56qK-AGp5zauMZgvA20Djtb7BuYqGAqCdOHEQtjjyVLufCu6Y72i9gNQqFQ-WGQ6AFN84KT7BJgRoTruduYG9VhGMOR59HR3jG2QIWXOCt55aI9YwAGNQii0b_QqaoSO08Pb_Ooji5abFLISs70jQb-z1QcnvHIzHnsKqymEWwZhbkxpwf8bv8C6-8k4JGB5YdVj3T_0XQ-OCyvWQIGwVxKysLj8HBeVvXOKUyz7p-thOHbO4qSaRaV7w6_Yy2XtdwBlkIiTTqLezN34vCnsyhZ7N1IgLzp0bwNCyCoPOFs5Q9Ccw7hwJRP3kDT2cW4COJWVt-V5YF_9nnlZN8JjcIgv7FMZKoKRHi004vSosPYGd-v3Uw diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 000000000..3b32667a0 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,40 @@ +# XXX Template example +# This file is an example, meant to be duplicated as ".env.local" for local override + +# See https://nextjs.org/docs/basic-features/environment-variables + +# XXX Tips: How is this file meant to be used? +# This file is NOT tracked by git and can contain sensitive information, or override variables from ".env". + +# XXX Tips: When is this file being used? +# This file is used only when building the Next.js app locally (localhost), whether it's for running `next dev` or `next build`. +# For staging/production stages, the app relies on "vercel.{NEXT_PUBLIC_CUSTOMER_REF}.{NEXT_PUBLIC_APP_STAGE}.yml:build.env". + +# XXX Tips: What's the difference between env vars starting with "NEXT_PUBLIC_" and the others? +# All env variables that DON'T start with "NEXT_PUBLIC_" MUST be manually exposed by ./next.config.js for the project to work locally +# "NEXT_PUBLIC_" has a semantic purpose. If you mean to use a variable on the browser, then you should use "NEXT_PUBLIC_". +# Any non-sensitive env variable should start with "NEXT_PUBLIC_". +# Sensitive information MUST NOT start with "NEXT_PUBLIC_". +# You must be careful to use sensitive information only on the server-side, because if you use them on the browser or getInitialProps, they'll be leaked, even if the variable doesn't start with "NEXT_PUBLIC_". +# Any change to this file needs a server restart to be applied. + +# Locize API key, can be found under "Your project > Settings > Api Keys" at https://www.locize.app/?ref=unly-nrn +# Used to automatically save missing translations when working locally +# Optional - If not set, the app will work anyway, it just won't create new keys automatically +# Example (fake value): 615384ff-0f39-4c7b-89ca-9b0acbfd0869 +LOCIZE_API_KEY= + +# Sentry DSN, can be found under "Your project > Client Keys (DSN)" at https://sentry.io/settings/YOUR_ORG/projects/YOUR_PROJECT/keys/ +# Used to send monitoring events (errors, etc.) +# Optional - If not set, the app will work anyway, it just won't send any event +# Example (fake value): https://14fa1cae05079675b18cd05403ae5c48@sentry.io/1234567 +SENTRY_DSN= + +# Github "personal access token", can be generated at "Settings > Developer settings > Personal access tokens" at https://github.com/settings/tokens +# Used by "Workflow Dispatch" GitHub Actions +# Needs the following scopes: +# - Repo (FULL) +# - Workflow +# Optional - If not set, the app will work anyway, it just won't be able to deploy new instances through the "startVercelDeployment" API +# Example (fake value): 278c560c1314b8b032c1314b856072bdaaaaaaaa +GITHUB_DISPATCH_TOKEN= diff --git a/.eslintignore b/.eslintignore index b955f23b7..4bd531986 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,8 +1,10 @@ node_modules .next +cypress src/**/*.test* src/gql/** src/propTypes/** src/svg/** src/types/** src/components/svg/** +src/stories/sb-examples/** diff --git a/.eslintrc.yml b/.eslintrc.yml index 7b7e8a4da..c5fa7d697 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -6,15 +6,15 @@ env: node: true extends: - plugin:@typescript-eslint/recommended - - plugin:react/recommended - plugin:jsx-a11y/recommended + - next # Recommended to be added last after all other "recommended" tools - See https://nextjs.org/docs/basic-features/eslint#disabling-rules + - next/core-web-vitals # Enforce stronger rules for Core Web Vitals - See https://nextjs.org/docs/basic-features/eslint#core-web-vitals globals: Atomics: readonly SharedArrayBuffer: readonly plugins: - jest - - react - - react-hooks + - '@emotion' parser: '@typescript-eslint/parser' parserOptions: project: ./tsconfig.json @@ -44,9 +44,7 @@ rules: # See https://eslint.org/docs/rules strict: 'off' no-console: 1 # Shouldn't use "console", but "logger" instead allowArrowFunctions: 0 - no-unused-vars: - - warn # Warn otherwise it false-positive with needed React imports - - args: none # Allow to declare unused variables in function arguments, meant to be used later + no-unused-vars: 0 # Disabled, already handled by @typescript-eslint/no-unused-vars import/prefer-default-export: 0 # When there is only a single export from a module, don't enforce a default export, but rather let developer choose what's best no-else-return: 0 # Don't enforce, let developer choose. Sometimes we like to specifically use "return" for the sake of comprehensibility and avoid ambiguity no-underscore-dangle: 0 # Allow _ before/after variables and functions, convention for something meant to be "private" @@ -78,12 +76,12 @@ rules: # See https://eslint.org/docs/rules react-hooks/exhaustive-deps: warn react/jsx-no-target-blank: warn # Not using "noreferrer" is not a security risk, but "noopener" should always be used indeed react/prop-types: warn # Should be handled with TS instead - react/no-unescaped-entities: warn # Causes text mismatch when enabled + react/no-unescaped-entities: 0 # Causes text mismatch when enabled jsx-a11y/anchor-is-valid: warn linebreak-style: - error - unix - '@typescript-eslint/ban-ts-ignore': warn # ts-ignore are sometimes the only way to bypass a TS issue, we trust we will use them for good and not abuse them + '@typescript-eslint/ban-ts-comment': warn # ts-ignore are sometimes the only way to bypass a TS issue, we trust we will use them for good and not abuse them '@typescript-eslint/no-use-before-define': warn '@typescript-eslint/no-unused-vars': - warn @@ -91,6 +89,19 @@ rules: # See https://eslint.org/docs/rules vars: 'all' # We don't want unused variables (noise) - XXX Note that this will be a duplicate of "no-unused-vars" rule args: 'none' # Sometimes it's useful to have unused arguments for later use, such as describing what args are available (DX) ignoreRestSiblings: true # Sometimes it's useful to have unused props for later use, such as describing what props are available (DX) + varsIgnorePattern: 'React|logger|rest' + '@typescript-eslint/ban-types': + - error + - + extendDefaults: true + types: + '{}': false # Allow writing `type Props = {}` - See https://github.com/typescript-eslint/typescript-eslint/issues/2063#issuecomment-632833366 + '@typescript-eslint/no-explicit-any': off + + # Emotion rules - See https://github.com/emotion-js/emotion/tree/master/packages/eslint-plugin + # Emotion 11 - See https://emotion.sh/docs/emotion-11 + '@emotion/pkg-renaming': error + overrides: - files: ['**/*.tsx'] rules: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..ee1e475f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + +**Smartphone (please complete the following information):** + - OS: [e.g. iOS8.1] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/having-a-question-.md b/.github/ISSUE_TEMPLATE/having-a-question-.md new file mode 100644 index 000000000..17614ba74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/having-a-question-.md @@ -0,0 +1,10 @@ +--- +name: Having a question? +about: Use the community Discussions +title: '' +labels: '' +assignees: '' + +--- + +Please ask questions in the [Discussions](https://github.com/UnlyEd/next-right-now/discussions)! :) diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..4fc774b3f --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1 @@ +Learn more about [our CI/CD configuration](https://unlyed.github.io/next-right-now/guides/ci-cd/) diff --git a/.github/workflows/auto-git-release.yml b/.github/workflows/auto-git-release.yml new file mode 100644 index 000000000..9cd571c3c --- /dev/null +++ b/.github/workflows/auto-git-release.yml @@ -0,0 +1,69 @@ +# Summary: +# Automatically tag and release when changes land on any branch. +# Tag and release changes on the master branch, as releases. (one release per commit) +# Tag and pre-release changes on the other branches, as pre-releases with a "x" as "patch" indicator. +# (one release per branch, the release is updated at every push) +# +# Dependencies overview: +# - See https://github.com/PaulHatch/semantic-version https://github.com/PaulHatch/semantic-version/tree/v3.2 +# - See https://github.com/marvinpinto/action-automatic-releases https://github.com/marvinpinto/action-automatic-releases/tree/v1.1.1 +# - See https://github.com/rlespinasse/github-slug-action https://github.com/rlespinasse/github-slug-action/tree/3.x + +name: 'Auto release' +on: + push: + branches: + - '*' + +jobs: + tag-and-release: + runs-on: ubuntu-latest + timeout-minutes: 5 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - name: Expose GitHub slug/short variables # See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + uses: rlespinasse/github-slug-action@v3.x # See https://github.com/rlespinasse/github-slug-action + - uses: actions/checkout@v2 # See https://github.com/actions/checkout + with: + fetch-depth: 0 # See https://github.com/PaulHatch/semantic-version#important-note-regarding-the-checkout-action + - uses: paulhatch/semantic-version@v3.2 # See https://github.com/PaulHatch/semantic-version + id: next_semantic_version # Output: https://github.com/PaulHatch/semantic-version/blob/master/index.js#L63-L69 + with: # See https://github.com/PaulHatch/semantic-version#usage + branch: ${{ env.GITHUB_REF_SLUG }} # Use current branch + tag_prefix: "v" # The prefix to use to identify tags + major_pattern: "(MAJOR)" # A string which, if present in a git commit, indicates that a change represents a major (breaking) change + minor_pattern: "(MINOR)" # Same as above except indicating a minor change + format: "${major}.${minor}.${patch}-${{ env.GITHUB_REF_SLUG }}" # A string to determine the format of the version output + short_tags: false # If set to false, short tags like "v1" will not be considered, only full versions tags such as "v1.0.0" will be used to determine the version. + bump_each_commit: true # If this input is set to true, every commit will be treated as a new version, bumping the patch, minor, or major version based on the commit message. + - run: | + echo "Branch name: ${{ env.GITHUB_REF_SLUG }}" + echo ${{ join(steps.next_semantic_version.outputs.*, ' - ') }} + echo "Next version: ${{ steps.next_semantic_version.outputs.version }}" + echo "Next version tag: ${{ steps.next_semantic_version.outputs.version_tag }}" + + # Auto-release when a commit hit "master" + - uses: marvinpinto/action-automatic-releases@2aa16da5b7423a298a2d56cf5d9c2dafcef5f542 # Pin "latest" https://github.com/marvinpinto/action-automatic-releases/commit/2aa16da5b7423a298a2d56cf5d9c2dafcef5f542 necessary to avoid "The `set-env` command is disabled." + if: startsWith(env.GITHUB_REF_SLUG, 'v2-') || env.GITHUB_REF_SLUG == 'master' || env.GITHUB_REF_SLUG == 'main' # If master, mark release as official release + with: # See https://github.com/marvinpinto/action-automatic-releases/tree/v1.1.1#supported-parameters + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "v${{ steps.next_semantic_version.outputs.version }}" + prerelease: false + title: "Automatic release ${{ steps.next_semantic_version.outputs.version_tag }} (from `${{ env.GITHUB_REF_SLUG }}`)" + files: | + README.md + CHANGELOG.md + LICENSE + + # Auto-release (as pre-release) in a shared tag (using the branch's name) when a commit hits another branch + - uses: marvinpinto/action-automatic-releases@2aa16da5b7423a298a2d56cf5d9c2dafcef5f542 # Pin "latest" https://github.com/marvinpinto/action-automatic-releases/commit/2aa16da5b7423a298a2d56cf5d9c2dafcef5f542 necessary to avoid "The `set-env` command is disabled." + # When using "!", must use expression syntax - See https://github.community/t/if-not-startswith-mutually-exclusive-steps/141841/2?u=vadorequest + if: ${{ !startsWith(env.GITHUB_REF_SLUG, 'v2-') && env.GITHUB_REF_SLUG != 'master' && env.GITHUB_REF_SLUG != 'main' }} # If not master, mark release as pre-release and include branch name to avoid tagging conflicts + with: # See https://github.com/marvinpinto/action-automatic-releases/tree/v1.1.1#supported-parameters + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "v${{ steps.next_semantic_version.outputs.major }}.${{ steps.next_semantic_version.outputs.minor }}.x-rc-${{ env.GITHUB_REF_SLUG }}" + prerelease: true + title: "Automatic pre-release `v${{ steps.next_semantic_version.outputs.major }}.${{ steps.next_semantic_version.outputs.minor }}.x-rc-${{ env.GITHUB_REF_SLUG }}`" + files: | + README.md + CHANGELOG.md + LICENSE diff --git a/.github/workflows/deploy-vercel-production.yml b/.github/workflows/deploy-vercel-production.yml new file mode 100644 index 000000000..1b1d42aa8 --- /dev/null +++ b/.github/workflows/deploy-vercel-production.yml @@ -0,0 +1,367 @@ +# Summary: +# Creates a new production deployment on Vercel's platform, when anything is pushed in the "master" branch. +# Once the deployment has been performed by Vercel, we wait for its status to reach the "READY" state before doing anything else.defaults: +# - Once ready, an HTTP call is sent to the webhook that is configured within the customer config file (VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK). (optional) +# - Once ready, E2E tests are executed on the newly deployed domain (Cypress). +# - Once ready, quality tests are executed on the newly deployed domain (LightHouse). +# +# This workflow can also be triggered using "Workflow Dispatch". +# See https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch +# See https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#workflow_dispatch +# This workflow can also be triggered using the API (uses Workflow Dispatch in the background). +# See src/pages/api/startVercelDeployment.ts +# +# LEARN MORE AT https://unlyed.github.io/next-right-now/guides/ci-cd/ +# +# Dependencies overview: +# - See https://github.com/actions/setup-node https://github.com/actions/setup-node/tree/v2 +# - See https://github.com/actions/checkout https://github.com/actions/checkout/tree/v1 +# - See https://github.com/actions/upload-artifact https://github.com/actions/upload-artifact/tree/v1 +# - See https://github.com/rlespinasse/github-slug-action https://github.com/rlespinasse/github-slug-action/tree/3.x +# - See https://github.com/jwalton/gh-find-current-pr https://github.com/jwalton/gh-find-current-pr/tree/v1 +# - See https://github.com/peter-evans/create-or-update-comment https://github.com/peter-evans/create-or-update-comment/tree/v1 +# - See https://github.com/UnlyEd/github-action-await-vercel https://github.com/UnlyEd/github-action-await-vercel/tree/v1.1.1 +# - See https://github.com/UnlyEd/github-action-store-variable https://github.com/UnlyEd/github-action-store-variable/tree/v2.1.1 +# - See https://github.com/cypress-io/github-action https://github.com/cypress-io/github-action/tree/v2 +# - See https://github.com/foo-software/lighthouse-check-action https://github.com/foo-software/lighthouse-check-action/tree/v1.0.1 +# - See https://github.com/bobheadxi/deployments https://github.com/bobheadxi/deployments/tree/v0.4.3 + +name: Deploy to Vercel (production) + +on: + # There are several ways to trigger Github actions - See https://help.github.com/en/actions/reference/events-that-trigger-workflows#example-using-a-single-event for a comprehensive list: + # - "push": Triggers each time a commit is pushed + # - "pull_request": Triggers each time a commit is pushed within a pull request, it makes it much easier to write comments within the PR, but it suffers some strong limitations: + # - There is no way to trigger when a PR is merged into another - See https://github.community/t/pull-request-action-does-not-run-on-merge/16092?u=vadorequest + # - It won't trigger when the PR is conflicting with its base branch - See https://github.community/t/run-actions-on-pull-requests-with-merge-conflicts/17104/2?u=vadorequest + push: # Triggers on each pushed commit + branches: + - 'master' + - 'main' + - 'v2-mst-aptd-gcms-lcz-sty' # XXX Name of the NRN preset branch, acting as a main branch (all commits pushed to the preset branch are deployed to production) + + # Runs the deployment workflow only when there are changes made to specific files, in any of the below paths + # Optimizes our CI/CD by not deploying the Next.js site needlessly (faster deploy, lower cost) + # XXX Filter pattern cheat sheet https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet + paths: + - '.github/workflows/deploy-vercel-production.yml' + - 'cypress/**' + - '!cypress/_examples/**' + - '!cypress/integration-storybook/**' + - '!cypress/config-*.json' # Exclude all config files + - 'cypress/config-customer-ci-cd.json' # Force include CI/CD config file + - 'public/**' + - 'scripts/populate-git-env.sh' + - 'src/**' + - '!src/stories/**' + - '.eslint*' + - '*.js*' # Includes all .js/.json at the root level + - '*.ts' # Includes all .ts at the root level + - '.*ignore' # Includes .gitignore and .vercelignore + - 'yarn.lock' + - '!**/*.md' # Exclude all markdown files + + # Allow manual trigger via a button in github or a HTTP call - See https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#manually-running-a-workflow + # XXX See https://unlyed.github.io/next-right-now/guides/ci-cd/gha-deploy-vercel#triggering-the-action-remotely-using-workflow_dispatch + workflow_dispatch: + inputs: + customer: + description: 'Customer to deploy' + required: true + +env: + STAGE: production + +jobs: + # Configures the deployment environment, install dependencies (like node, npm, etc.) that are requirements for the upcoming jobs + # Ex: Necessary to run `yarn deploy` + setup-environment: + name: Setup deployment environment (Ubuntu latest - Node 14.x) + runs-on: ubuntu-latest + steps: + - name: Installing node.js + uses: actions/setup-node@v2 # Used to install node environment - https://github.com/actions/setup-node + with: + node-version: '14.x' # Use the same node.js version as the one Vercel's uses (currently node14.x) + + # Starts a Vercel deployment, using the production configuration file of the default customer + # The default customer is the one defined in the `vercel.json` file (which is a symlink to the actual file) + # N.B: It's Vercel that will perform the actual deployment + start-production-deployment: + name: Starts Vercel deployment (production) (Ubuntu latest) + runs-on: ubuntu-latest + needs: setup-environment + timeout-minutes: 40 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Resolves customer to deploy from github event input (falls back to resolving it from vercel.json file) + - name: Resolve customer to deploy + run: | + # Resolving customer to deploy based on the github event input, when using manual deployment triggerer through "workflow_dispatch" event + # Falls back to the customer specified in the vercel.json file, which is most useful when deployment is triggered through "push" event + MANUAL_TRIGGER_CUSTOMER="${{ github.event.inputs.customer}}" + echo "MANUAL_TRIGGER_CUSTOMER: " $MANUAL_TRIGGER_CUSTOMER + echo "MANUAL_TRIGGER_CUSTOMER=$MANUAL_TRIGGER_CUSTOMER" >> $GITHUB_ENV + + CUSTOMER_REF_TO_DEPLOY="${MANUAL_TRIGGER_CUSTOMER:-$(cat vercel.json | jq --raw-output '.build.env.NEXT_PUBLIC_CUSTOMER_REF')}" + echo "Customer to deploy: " $CUSTOMER_REF_TO_DEPLOY + echo "CUSTOMER_REF_TO_DEPLOY=$CUSTOMER_REF_TO_DEPLOY" >> $GITHUB_ENV + + # Create a GitHub deployment (within a GitHub environment), useful to keep a public track of all deployments directly in GitHub + - name: Start GitHub deployment + uses: bobheadxi/deployments@v0.4.3 # See https://github.com/bobheadxi/deployments + id: start-github-deployment + with: + step: start + token: ${{ secrets.GITHUB_TOKEN }} + env: ${{ env.CUSTOMER_REF_TO_DEPLOY }}-${{ env.STAGE }} # Uses "$customer-$env" as GitHub environment name, because it's easier this way to see what has changed per customer, per stage + no_override: true # Disables auto marking previous environments as "inactive", as they're still active (Vercel deployments don't auto-deactivate) and it would remove the previous deployment links needlessly + + - name: Deploying on Vercel (${{ env.STAGE }}) + uses: UnlyEd/github-action-deploy-on-vercel@94d41ec1ff9b5b1de5256312e385632b6fcd8fa4 # Pin "v1.2.1" - See https://github.com/UnlyEd/github-action-deploy-on-vercel/commit/94d41ec1ff9b5b1de5256312e385632b6fcd8fa4 + with: + command: "yarn deploy:ci:gha:production --token ${{ secrets.VERCEL_TOKEN }}" + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} # Passing github's secret to the worker + GIT_COMMIT_REF: ${{ github.ref }} # Passing current branch/tag to the worker + GIT_COMMIT_SHA: ${{ github.ref }} # Passing current commit SHA to the worker + CUSTOMER_REF: ${{ env.CUSTOMER_REF_TO_DEPLOY }} # Passing current customer to deploy + + # Update the previously created GitHub deployment, and link it to our Vercel deployment + - name: Link GitHub deployment to Vercel + uses: bobheadxi/deployments@v0.4.3 # See https://github.com/bobheadxi/deployments + id: link-github-deployment-to-vercel + if: always() + with: + step: finish + token: ${{ secrets.GITHUB_TOKEN }} + status: ${{ job.status }} + deployment_id: ${{ steps.start-github-deployment.outputs.deployment_id }} + env_url: ${{ env.VERCEL_DEPLOYMENT_URL }} # Link the Vercel deployment url to the GitHub environment + + # At the end of the job, store all variables we will need in the following jobs + # The variables will be stored in and retrieved from a GitHub Artifact (each variable is stored in a different file) + - name: Store variables for next jobs + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + with: + variables: | + CUSTOMER_REF_TO_DEPLOY=${{ env.CUSTOMER_REF_TO_DEPLOY }} + VERCEL_DEPLOYMENT_URL=${{ env.VERCEL_DEPLOYMENT_URL }} + VERCEL_DEPLOYMENT_DOMAIN=${{ env.VERCEL_DEPLOYMENT_DOMAIN }} + MANUAL_TRIGGER_CUSTOMER=${{ env.MANUAL_TRIGGER_CUSTOMER }} + GITHUB_PULL_REQUEST_ID=${{ steps.pr_id_finder.outputs.number }} + + # Waits for the Vercel deployment to reach "READY" state, so that other actions will be applied on a domain that is really online + await-for-vercel-deployment: + name: Await current deployment to be ready (Ubuntu latest) + runs-on: ubuntu-latest + needs: start-production-deployment + timeout-minutes: 5 # Limit current job timeout (including action timeout setup down there) https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + VERCEL_DEPLOYMENT_DOMAIN + + # Wait for deployment to be ready, before running E2E (otherwise Cypress might start testing too early, and gets redirected to Vercel's "Login page", and tests fail) + - name: Awaiting Vercel deployment to be ready + uses: UnlyEd/github-action-await-vercel@v1.2.14 # See https://github.com/UnlyEd/github-action-await-vercel + id: await-vercel + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + with: + deployment-url: ${{ env.VERCEL_DEPLOYMENT_DOMAIN }} # Must only contain the domain name (no http prefix, etc.) + timeout: 90 # Wait for 90 seconds before failing + + - name: Display deployment status + run: "echo The deployment is ${{ env.readyState }}" + + # Send a HTTP call to the webhook url that's provided in the customer configuration file (vercel.*.json) + send-webhook-callback-once-deployment-ready: + name: Invoke webhook callback url defined by the customer (Ubuntu latest) + runs-on: ubuntu-latest + needs: await-for-vercel-deployment + timeout-minutes: 5 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + - name: Expose GitHub slug/short variables # See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + uses: rlespinasse/github-slug-action@v3.x # See https://github.com/rlespinasse/github-slug-action + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + MANUAL_TRIGGER_CUSTOMER + CUSTOMER_REF_TO_DEPLOY + + - name: Expose git environment variables and call webhook (if provided) + # Workflow overview: + # - Resolves webhook url from customer config file + # - If a webhook url was defined in the customer config file, send an HTTP request, as POST request, with a JSON request body dynamically generated + # - Prints the headers of the POST HTTP request (curl) + # TODO Convert this into an external bash script if possible, to simplify this step as much as possible + run: | + VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK=$(cat vercel.$CUSTOMER_REF_TO_DEPLOY.${{ env.STAGE }}.json | jq --raw-output '.build.env.VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK') + echo "Vercel deployment webhook url: " $VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK + + # Checking if a webhook url is defined + if [ -n "$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK" ]; then + # Run script that populates git-related variables as ENV variables + echo "Running script generate-post-data" + . ./scripts/generate-post-data.sh + + echo "Print generate_post_data():" + echo "$(generate_post_data)" + + echo "Calling webhook at '$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK'" + echo "Sending HTTP request (curl):" + curl POST \ + "$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK" \ + -vs \ + --header "Accept: application/json" \ + --header "Content-type: application/json" \ + --data "$(generate_post_data)" \ + 2>&1 | sed '/^* /d; /bytes data]$/d; s/> //; s/< //' + + # XXX See https://stackoverflow.com/a/54225157/2391795 + # -vs - add headers (-v) but remove progress bar (-s) + # 2>&1 - combine stdout and stderr into single stdout + # sed - edit response produced by curl using the commands below + # /^* /d - remove lines starting with '* ' (technical info) + # /bytes data]$/d - remove lines ending with 'bytes data]' (technical info) + # s/> // - remove '> ' prefix + # s/< // - remove '< ' prefix + + else + echo "No webhook url defined in 'vercel.$CUSTOMER_REF_TO_DEPLOY.${{ env.STAGE }}.json:.build.env.VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK' (found '$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK')" + fi + env: + GIT_COMMIT_REF: ${{ github.ref }} # Passing current branch/tag to the worker + GIT_COMMIT_SHA: ${{ github.sha }} # Passing current commit SHA to the worker + # Passing exposed GitHub environment variables - See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + DEPLOYMENT_STAGE: ${{ env.STAGE }} + GITHUB_REF_SLUG: ${{ env.GITHUB_REF_SLUG }} + GITHUB_HEAD_REF_SLUG: ${{ env.GITHUB_HEAD_REF_SLUG }} + GITHUB_BASE_REF_SLUG: ${{ env.GITHUB_BASE_REF_SLUG }} + GITHUB_EVENT_REF_SLUG: ${{ env.GITHUB_EVENT_REF_SLUG }} + GITHUB_REPOSITORY_SLUG: ${{ env.GITHUB_REPOSITORY_SLUG }} + GITHUB_REF_SLUG_URL: ${{ env.GITHUB_REF_SLUG_URL }} + GITHUB_HEAD_REF_SLUG_URL: ${{ env.GITHUB_HEAD_REF_SLUG_URL }} + GITHUB_BASE_REF_SLUG_URL: ${{ env.GITHUB_BASE_REF_SLUG_URL }} + GITHUB_EVENT_REF_SLUG_URL: ${{ env.GITHUB_EVENT_REF_SLUG_URL }} + GITHUB_REPOSITORY_SLUG_URL: ${{ env.GITHUB_REPOSITORY_SLUG_URL }} + GITHUB_SHA_SHORT: ${{ env.GITHUB_SHA_SHORT }} + + # Runs E2E tests against the Vercel deployment + run-2e2-tests: + name: Run end to end (E2E) tests (Ubuntu latest) + runs-on: ubuntu-latest + # Docker image with Cypress pre-installed + # https://github.com/cypress-io/cypress-docker-images/tree/master/included + container: cypress/included:7.4.0 + needs: await-for-vercel-deployment + timeout-minutes: 20 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + VERCEL_DEPLOYMENT_URL + + # Runs the E2E tests against the new Vercel deployment + - name: Run E2E tests (Cypress) + uses: cypress-io/github-action@v2 # See https://github.com/cypress-io/github-action + with: + # XXX We disabled "wait-on" option, because it's useless. Cypress will fail anyway, because it gets redirected to some internal Vercel URL if the domain isn't yet available - See https://github.com/cypress-io/github-action/issues/270 + # wait-on: '${{ env.VERCEL_DEPLOYMENT_URL }}' # Be sure that the endpoint is ready by pinging it before starting tests, using a default timeout of 60 seconds + config-file: 'cypress/config-customer-ci-cd.json' # Use Cypress config file for CI/CD, and override it below + config: baseUrl=${{ env.VERCEL_DEPLOYMENT_URL }} # Overriding baseUrl provided by config file to test the new deployment + env: + # Enables Cypress debugging logs, very useful if Cypress crashes, like out-of-memory issues. + # DEBUG: "cypress:*" # Enable all logs. See https://docs.cypress.io/guides/references/troubleshooting.html#Print-DEBUG-logs + DEBUG: "cypress:server:util:process_profiler" # Enable logs for "memory and CPU usage". See https://docs.cypress.io/guides/references/troubleshooting.html#Log-memory-and-CPU-usage + + # On E2E failure, upload screenshots + - name: Upload screenshots artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: screenshots + path: cypress/screenshots/ + + # On E2E failure, upload videos + - name: Upload videos artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: videos + path: cypress/videos/ + + # Runs LightHouse reports in parallel of E2E tests + run-lighthouse-tests: + name: Run LightHouse checks (Ubuntu latest) + runs-on: ubuntu-latest + needs: await-for-vercel-deployment + timeout-minutes: 5 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # To store reports and then upload them, we must create the folder beforehand + - name: Create temporary folder for artifacts storage + run: mkdir /tmp/lighthouse-artifacts + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: VERCEL_DEPLOYMENT_URL + + # Runs LightHouse for a given url and create HTML reports in the specified directory (outputDirectory) + # Action documentation: https://github.com/marketplace/actions/lighthouse-check#usage-standard-example + - name: Run Lighthouse + uses: foo-software/lighthouse-check-action@v2.0.5 + id: lighthouseCheck + with: # See https://github.com/marketplace/actions/lighthouse-check#inputs for all options + # XXX We don't enable comments, because there is no branch to write them into + outputDirectory: /tmp/lighthouse-artifacts # Used to upload artifacts. + emulatedFormFactor: all # Run LightHouse against "mobile", "desktop", or "all" devices + urls: ${{ env.VERCEL_DEPLOYMENT_URL }}, ${{ env.VERCEL_DEPLOYMENT_URL }}/en, ${{ env.VERCEL_DEPLOYMENT_URL }}/fr + locale: en + + # Upload HTML report created by lighthouse as an artifact. + # XXX Disable this if you don't use them, as they are a bit heavy (~3MB) and might cost you money, if you're using a private repository + - name: Upload artifacts + uses: actions/upload-artifact@v1 + with: + name: Lighthouse reports + path: /tmp/lighthouse-artifacts + + # Using a pre-build action to make the action fail if your score is too low. It can be really interesting to track a low score on a commit + # You can remove this action IF you don't want lighthouse to be a blocking point in your CI + # Official documentation: https://github.com/foo-software/lighthouse-check-status-action + - name: Handle Lighthouse Check results + uses: foo-software/lighthouse-check-status-action@v1.0.1 # See https://github.com/foo-software/lighthouse-check-action + with: + lighthouseCheckResults: ${{ steps.lighthouseCheck.outputs.lighthouseCheckResults }} + minAccessibilityScore: "50" + minBestPracticesScore: "50" + minPerformanceScore: "30" + minProgressiveWebAppScore: "50" + minSeoScore: "50" diff --git a/.github/workflows/deploy-vercel-staging.yml b/.github/workflows/deploy-vercel-staging.yml new file mode 100644 index 000000000..27769da4e --- /dev/null +++ b/.github/workflows/deploy-vercel-staging.yml @@ -0,0 +1,442 @@ +# Summary: +# Creates a new preview deployment on Vercel's platform, when anything is pushed in any branch (except for the "master" branch). +# Once the deployment has been performed by Vercel, we wait for its status to reach the "READY" state before doing anything else.defaults: +# - Once ready, an HTTP call is sent to the webhook that is configured within the customer config file (VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK). (optional) +# - Once ready, E2E tests are executed on the newly deployed domain (Cypress). +# - Once ready, quality tests are executed on the newly deployed domain (LightHouse). +# +# This workflow can also be triggered using "Workflow Dispatch". +# See https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch +# See https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#workflow_dispatch +# This workflow can also be triggered using the API (uses Workflow Dispatch in the background). +# See src/pages/api/startVercelDeployment.ts +# +# LEARN MORE AT https://unlyed.github.io/next-right-now/guides/ci-cd/ +# +# Dependencies overview: +# - See https://github.com/actions/setup-node https://github.com/actions/setup-node/tree/v2 +# - See https://github.com/actions/checkout https://github.com/actions/checkout/tree/v1 +# - See https://github.com/actions/upload-artifact https://github.com/actions/upload-artifact/tree/v1 +# - See https://github.com/rlespinasse/github-slug-action https://github.com/rlespinasse/github-slug-action/tree/3.x +# - See https://github.com/jwalton/gh-find-current-pr https://github.com/jwalton/gh-find-current-pr/tree/v1 +# - See https://github.com/peter-evans/create-or-update-comment https://github.com/peter-evans/create-or-update-comment/tree/v1 +# - See https://github.com/UnlyEd/github-action-await-vercel https://github.com/UnlyEd/github-action-await-vercel/tree/v1.1.1 +# - See https://github.com/UnlyEd/github-action-store-variable https://github.com/UnlyEd/github-action-store-variable/tree/v2.1.1 +# - See https://github.com/cypress-io/github-action https://github.com/cypress-io/github-action/tree/v2 +# - See https://github.com/foo-software/lighthouse-check-action https://github.com/foo-software/lighthouse-check-action/tree/v1.0.1 +# - See https://github.com/bobheadxi/deployments https://github.com/bobheadxi/deployments/tree/v0.4.3 + +name: Deploy to Vercel (staging) + +on: + # There are several ways to trigger Github actions - See https://help.github.com/en/actions/reference/events-that-trigger-workflows#example-using-a-single-event for a comprehensive list: + # - "push": Triggers each time a commit is pushed + # - "pull_request": Triggers each time a commit is pushed within a pull request, it makes it much easier to write comments within the PR, but it suffers some strong limitations: + # - There is no way to trigger when a PR is merged into another - See https://github.community/t/pull-request-action-does-not-run-on-merge/16092?u=vadorequest + # - It won't trigger when the PR is conflicting with its base branch - See https://github.community/t/run-actions-on-pull-requests-with-merge-conflicts/17104/2?u=vadorequest + push: # Triggers on each pushed commit + branches-ignore: + - 'master' + - 'main' + - 'v2-mst-aptd-gcms-lcz-sty' # XXX Name of the NRN preset branch, acting as a main branch (all commits pushed to the preset branch are deployed to production) + + # Runs the deployment workflow only when there are changes made to specific files, in any of the below paths + # Optimizes our CI/CD by not deploying the Next.js site needlessly (faster deploy, lower cost) + # XXX Filter pattern cheat sheet https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet + paths: + - '.github/workflows/deploy-vercel-staging.yml' + - 'cypress/**' + - '!cypress/_examples/**' + - '!cypress/integration-storybook/**' + - '!cypress/config-*.json' # Exclude all config files + - 'cypress/config-customer-ci-cd.json' # Force include CI/CD config file + - 'public/**' + - 'scripts/populate-git-env.sh' + - 'src/**' + - '!src/stories/**' + - '.eslint*' + - '*.js*' # Includes all .js/.json at the root level + - '*.ts' # Includes all .ts at the root level + - '.*ignore' # Includes .gitignore and .vercelignore + - 'yarn.lock' + - '!**/*.md' # Exclude all markdown files + + # Allow manual trigger via a button in github or a HTTP call - See https://docs.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#manually-running-a-workflow + # XXX See https://unlyed.github.io/next-right-now/guides/ci-cd/gha-deploy-vercel#triggering-the-action-remotely-using-workflow_dispatch + workflow_dispatch: + inputs: + customer: + description: 'Customer to deploy' + required: true + +env: + STAGE: staging + +jobs: + # Configures the deployment environment, install dependencies (like node, npm, etc.) that are requirements for the upcoming jobs + # Ex: Necessary to run `yarn deploy` + setup-environment: + name: Setup deployment environment (Ubuntu latest - Node 14.x) + runs-on: ubuntu-latest + steps: + - name: Installing node.js + uses: actions/setup-node@v2 # Used to install node environment - https://github.com/actions/setup-node + with: + node-version: '14.x' # Use the same node.js version as the one Vercel's uses (currently node14.x) + + # Starts a Vercel deployment, using the staging configuration file of the default customer + # The default customer is the one defined in the `vercel.json` file (which is a symlink to the actual file) + # N.B: It's Vercel that will perform the actual deployment + start-staging-deployment: + name: Starts Vercel deployment (staging) (Ubuntu latest) + runs-on: ubuntu-latest + needs: setup-environment + timeout-minutes: 40 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + - name: Expose GitHub slug/short variables # See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + uses: rlespinasse/github-slug-action@v3.x # See https://github.com/rlespinasse/github-slug-action + + # Resolves customer to deploy from github event input (falls back to resolving it from vercel.json file) + - name: Resolve customer to deploy + run: | + # Resolving customer to deploy based on the github event input, when using manual deployment triggerer through "workflow_dispatch" event + # Falls back to the customer specified in the vercel.json file, which is most useful when deployment is triggered through "push" event + MANUAL_TRIGGER_CUSTOMER="${{ github.event.inputs.customer}}" + echo "MANUAL_TRIGGER_CUSTOMER: " $MANUAL_TRIGGER_CUSTOMER + echo "MANUAL_TRIGGER_CUSTOMER=$MANUAL_TRIGGER_CUSTOMER" >> $GITHUB_ENV + + CUSTOMER_REF_TO_DEPLOY="${MANUAL_TRIGGER_CUSTOMER:-$(cat vercel.json | jq --raw-output '.build.env.NEXT_PUBLIC_CUSTOMER_REF')}" + echo "Customer to deploy: " $CUSTOMER_REF_TO_DEPLOY + echo "CUSTOMER_REF_TO_DEPLOY=$CUSTOMER_REF_TO_DEPLOY" >> $GITHUB_ENV + + # Build the alias domain based on our own rules (NRN own rules) + # Basically, if a branch starts with "v[0-9]" then we consider it to be a "preset branch" and we use the branch name as alias (e.g: "v2-mst-xxx") + # Otherwise, we use the deployment name and suffix it using the branch name (e.g: "v2-mst-xxx-branch-name") + if [[ ${GIT_COMMIT_REF##*/} =~ ^v[0-9]{1,}- ]]; then # Checking if pattern matches with "vX-" where X is a number + BRANCH_ALIAS_DOMAIN=${CUSTOMER_REF_TO_DEPLOY}-${{ env.GITHUB_REF_SLUG }} + else + BRANCH_ALIAS_DOMAIN=$(cat vercel.$CUSTOMER_REF_TO_DEPLOY.staging.json | jq --raw-output '.name')-${{ env.GITHUB_REF_SLUG }} + fi + echo "Resolved branch domain alias: " $BRANCH_ALIAS_DOMAIN + echo "BRANCH_ALIAS_DOMAIN=$BRANCH_ALIAS_DOMAIN" >> $GITHUB_ENV + + # Create a GitHub deployment (within a GitHub environment), useful to keep a public track of all deployments directly in GitHub + - name: Start GitHub deployment + uses: bobheadxi/deployments@v0.4.3 # See https://github.com/bobheadxi/deployments + id: start-github-deployment + with: + step: start + token: ${{ secrets.GITHUB_TOKEN }} + env: ${{ env.CUSTOMER_REF_TO_DEPLOY }}-${{ env.STAGE }} # Uses "$customer-$env" as GitHub environment name, because it's easier this way to see what has changed per customer, per stage + no_override: true # Disables auto marking previous environments as "inactive", as they're still active (Vercel deployments don't auto-deactivate) and it would remove the previous deployment links needlessly + + - name: Deploying on Vercel (${{ env.STAGE }}) + uses: UnlyEd/github-action-deploy-on-vercel@94d41ec1ff9b5b1de5256312e385632b6fcd8fa4 # Pin "v1.2.1" - See https://github.com/UnlyEd/github-action-deploy-on-vercel/commit/94d41ec1ff9b5b1de5256312e385632b6fcd8fa4 + with: + command: "yarn deploy:ci:gha --token ${{ secrets.VERCEL_TOKEN }}" + extraAliases: >- + ${{ env.BRANCH_ALIAS_DOMAIN }}.vercel.app + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} # Passing github's secret to the worker + GIT_COMMIT_REF: ${{ github.ref }} # Passing current branch/tag to the worker + GIT_COMMIT_SHA: ${{ github.ref }} # Passing current commit SHA to the worker + CUSTOMER_REF: ${{ env.CUSTOMER_REF_TO_DEPLOY }} # Passing current customer to deploy + + # Update the previously created GitHub deployment, and link it to our Vercel deployment + - name: Link GitHub deployment to Vercel + uses: bobheadxi/deployments@v0.4.3 # See https://github.com/bobheadxi/deployments + id: link-github-deployment-to-vercel + if: always() + with: + step: finish + token: ${{ secrets.GITHUB_TOKEN }} + status: ${{ job.status }} + deployment_id: ${{ steps.start-github-deployment.outputs.deployment_id }} + env_url: ${{ env.VERCEL_DEPLOYMENT_URL }} # Link the Vercel deployment url to the GitHub environment + + # We need to find the PR id. Will be used later to comment on that PR. + - name: Finding Pull Request ID + uses: jwalton/gh-find-current-pr@v1 # See https://github.com/jwalton/gh-find-current-pr + id: pr_id_finder + if: always() # It forces the job to be always executed, even if a previous job fail. + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # On deployment failure, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (Deployment failure) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: steps.pr_id_finder.outputs.number && failure() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ steps.pr_id_finder.outputs.number }} + body: | + :x:  Deployment **FAILED** + Commit ${{ github.sha }} failed to deploy to [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + [click to see logs](https://github.com/UnlyEd/next-right-now/pull/${{ steps.pr_id_finder.outputs.number }}/checks) + + # On deployment success, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (Deployment success) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: steps.pr_id_finder.outputs.number && success() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ steps.pr_id_finder.outputs.number }} + body: | + :white_check_mark:  Deployment **SUCCESS** + Commit ${{ github.sha }} successfully deployed **Next.js app** :rocket: to [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + Deployment aliases (${{ env.VERCEL_ALIASES_CREATED_COUNT }}): ${{ env.VERCEL_ALIASES_CREATED_URLS_MD }} + + # At the end of the job, store all variables we will need in the following jobs + # The variables will be stored in and retrieved from a GitHub Artifact (each variable is stored in a different file) + - name: Store variables for next jobs + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + with: + variables: | + CUSTOMER_REF_TO_DEPLOY=${{ env.CUSTOMER_REF_TO_DEPLOY }} + VERCEL_DEPLOYMENT_URL=${{ env.VERCEL_DEPLOYMENT_URL }} + VERCEL_DEPLOYMENT_DOMAIN=${{ env.VERCEL_DEPLOYMENT_DOMAIN }} + MANUAL_TRIGGER_CUSTOMER=${{ env.MANUAL_TRIGGER_CUSTOMER }} + GITHUB_PULL_REQUEST_ID=${{ steps.pr_id_finder.outputs.number }} + + # Waits for the Vercel deployment to reach "READY" state, so that other actions will be applied on a domain that is really online + await-for-vercel-deployment: + name: Await current deployment to be ready (Ubuntu latest) + runs-on: ubuntu-latest + needs: start-staging-deployment + timeout-minutes: 5 # Limit current job timeout (including action timeout setup down there) https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + VERCEL_DEPLOYMENT_DOMAIN + + # Wait for deployment to be ready, before running E2E (otherwise Cypress might start testing too early, and gets redirected to Vercel's "Login page", and tests fail) + - name: Awaiting Vercel deployment to be ready + uses: UnlyEd/github-action-await-vercel@v1.2.14 # See https://github.com/UnlyEd/github-action-await-vercel + id: await-vercel + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + with: + deployment-url: ${{ env.VERCEL_DEPLOYMENT_DOMAIN }} # Must only contain the domain name (no http prefix, etc.) + timeout: 90 # Wait for 90 seconds before failing + + - name: Display deployment status + run: "echo The deployment is ${{ env.readyState }}" + + # Send a HTTP call to the webhook url that's provided in the customer configuration file (vercel.*.json) + send-webhook-callback-once-deployment-ready: + name: Invoke webhook callback url defined by the customer (Ubuntu latest) + runs-on: ubuntu-latest + needs: await-for-vercel-deployment + timeout-minutes: 5 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + - name: Expose GitHub slug/short variables # See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + uses: rlespinasse/github-slug-action@v3.x # See https://github.com/rlespinasse/github-slug-action + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + MANUAL_TRIGGER_CUSTOMER + CUSTOMER_REF_TO_DEPLOY + + - name: Expose git environment variables and call webhook (if provided) + # Workflow overview: + # - Resolves webhook url from customer config file + # - If a webhook url was defined in the customer config file, send an HTTP request, as POST request, with a JSON request body dynamically generated + # - Prints the headers of the POST HTTP request (curl) + # TODO Convert this into an external bash script if possible, to simplify this step as much as possible + run: | + VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK=$(cat vercel.$CUSTOMER_REF_TO_DEPLOY.${{ env.STAGE }}.json | jq --raw-output '.build.env.VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK') + echo "Vercel deployment webhook url: " $VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK + + # Checking if a webhook url is defined + if [ -n "$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK" ]; then + # Run script that populates git-related variables as ENV variables + echo "Running script generate-post-data" + . ./scripts/generate-post-data.sh + + echo "Print generate_post_data():" + echo "$(generate_post_data)" + + echo "Calling webhook at '$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK'" + echo "Sending HTTP request (curl):" + curl POST \ + "$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK" \ + -vs \ + --header "Accept: application/json" \ + --header "Content-type: application/json" \ + --data "$(generate_post_data)" \ + 2>&1 | sed '/^* /d; /bytes data]$/d; s/> //; s/< //' + + # XXX See https://stackoverflow.com/a/54225157/2391795 + # -vs - add headers (-v) but remove progress bar (-s) + # 2>&1 - combine stdout and stderr into single stdout + # sed - edit response produced by curl using the commands below + # /^* /d - remove lines starting with '* ' (technical info) + # /bytes data]$/d - remove lines ending with 'bytes data]' (technical info) + # s/> // - remove '> ' prefix + # s/< // - remove '< ' prefix + + else + echo "No webhook url defined in 'vercel.$CUSTOMER_REF_TO_DEPLOY.${{ env.STAGE }}.json:.build.env.VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK' (found '$VERCEL_DEPLOYMENT_COMPLETED_WEBHOOK')" + fi + env: + GIT_COMMIT_REF: ${{ github.ref }} # Passing current branch/tag to the worker + GIT_COMMIT_SHA: ${{ github.sha }} # Passing current commit SHA to the worker + # Passing exposed GitHub environment variables - See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + DEPLOYMENT_STAGE: ${{ env.STAGE }} + GITHUB_REF_SLUG: ${{ env.GITHUB_REF_SLUG }} + GITHUB_HEAD_REF_SLUG: ${{ env.GITHUB_HEAD_REF_SLUG }} + GITHUB_BASE_REF_SLUG: ${{ env.GITHUB_BASE_REF_SLUG }} + GITHUB_EVENT_REF_SLUG: ${{ env.GITHUB_EVENT_REF_SLUG }} + GITHUB_REPOSITORY_SLUG: ${{ env.GITHUB_REPOSITORY_SLUG }} + GITHUB_REF_SLUG_URL: ${{ env.GITHUB_REF_SLUG_URL }} + GITHUB_HEAD_REF_SLUG_URL: ${{ env.GITHUB_HEAD_REF_SLUG_URL }} + GITHUB_BASE_REF_SLUG_URL: ${{ env.GITHUB_BASE_REF_SLUG_URL }} + GITHUB_EVENT_REF_SLUG_URL: ${{ env.GITHUB_EVENT_REF_SLUG_URL }} + GITHUB_REPOSITORY_SLUG_URL: ${{ env.GITHUB_REPOSITORY_SLUG_URL }} + GITHUB_SHA_SHORT: ${{ env.GITHUB_SHA_SHORT }} + + # Runs E2E tests against the Vercel deployment + run-2e2-tests: + name: Run end to end (E2E) tests (Ubuntu latest) + runs-on: ubuntu-latest + # Docker image with Cypress pre-installed + # https://github.com/cypress-io/cypress-docker-images/tree/master/included + container: cypress/included:7.4.0 + needs: await-for-vercel-deployment + timeout-minutes: 20 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + VERCEL_DEPLOYMENT_URL + GITHUB_PULL_REQUEST_ID + + # Runs the E2E tests against the new Vercel deployment + - name: Run E2E tests (Cypress) + uses: cypress-io/github-action@v2 # See https://github.com/cypress-io/github-action + with: + # XXX We disabled "wait-on" option, because it's useless. Cypress will fail anyway, because it gets redirected to some internal Vercel URL if the domain isn't yet available - See https://github.com/cypress-io/github-action/issues/270 + # wait-on: '${{ env.VERCEL_DEPLOYMENT_URL }}' # Be sure that the endpoint is ready by pinging it before starting tests, using a default timeout of 60 seconds + config-file: 'cypress/config-customer-ci-cd.json' # Use Cypress config file for CI/CD, and override it below + config: baseUrl=${{ env.VERCEL_DEPLOYMENT_URL }} # Overriding baseUrl provided by config file to test the new deployment + env: + # Enables Cypress debugging logs, very useful if Cypress crashes, like out-of-memory issues. + # DEBUG: "cypress:*" # Enable all logs. See https://docs.cypress.io/guides/references/troubleshooting.html#Print-DEBUG-logs + DEBUG: "cypress:server:util:process_profiler" # Enable logs for "memory and CPU usage". See https://docs.cypress.io/guides/references/troubleshooting.html#Log-memory-and-CPU-usage + + # On E2E failure, upload screenshots + - name: Upload screenshots artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: screenshots + path: cypress/screenshots/ + + # On E2E failure, upload videos + - name: Upload videos artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: videos + path: cypress/videos/ + + # On E2E failure, add a comment to the PR with additional information, if there is an open PR for the current branch + - name: Comment PR (E2E failure) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: env.GITHUB_PULL_REQUEST_ID && failure() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ env.GITHUB_PULL_REQUEST_ID }} + body: | + :x:  E2E tests **FAILED** for commit ${{ github.sha }} previously deployed at [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + Download artifacts (screenshots + videos) from [`checks`](https://github.com/UnlyEd/next-right-now/pull/${{ env.GITHUB_PULL_REQUEST_ID }}/checks) section + + # On E2E success, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (E2E success) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: env.GITHUB_PULL_REQUEST_ID && success() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ env.GITHUB_PULL_REQUEST_ID }} + body: | + :white_check_mark:  E2E tests **SUCCESS** for commit ${{ github.sha }} previously deployed at [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Runs LightHouse reports in parallel of E2E tests + run-lighthouse-tests: + name: Run LightHouse checks (Ubuntu latest) + runs-on: ubuntu-latest + needs: await-for-vercel-deployment + timeout-minutes: 5 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # To store reports and then upload them, we must create the folder beforehand + - name: Create temporary folder for artifacts storage + run: mkdir /tmp/lighthouse-artifacts + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: VERCEL_DEPLOYMENT_URL + + # Runs LightHouse for a given url and create HTML reports in the specified directory (outputDirectory) + # Action documentation: https://github.com/marketplace/actions/lighthouse-check#usage-standard-example + - name: Run Lighthouse + uses: foo-software/lighthouse-check-action@v2.0.5 + id: lighthouseCheck + with: # See https://github.com/marketplace/actions/lighthouse-check#inputs for all options + accessToken: ${{ secrets.GITHUB_TOKEN }} # Providing a token to comment the PR if you are using a "pull_request" as trigger, or using "commentUrl" option. + commentUrl: https://api.github.com/repos/UnlyEd/next-right-now/commits/${{ github.sha }}/comments + prCommentEnabled: true # Whether to comment on the PR (default: true). Disabled because we use our own "Comment PR (LightHouse report)" action + prCommentSaveOld: true # If true, then add a new comment for each commit. Otherwise, update the latest comment (default: false). + outputDirectory: /tmp/lighthouse-artifacts # Used to upload artifacts. + emulatedFormFactor: all # Run LightHouse against "mobile", "desktop", or "all" devices + urls: ${{ env.VERCEL_DEPLOYMENT_URL }}, ${{ env.VERCEL_DEPLOYMENT_URL }}/en, ${{ env.VERCEL_DEPLOYMENT_URL }}/fr + locale: en + + # Upload HTML report created by lighthouse as an artifact. + # XXX Disable this if you don't use them, as they are a bit heavy (~3MB) and might cost you money, if you're using a private repository + - name: Upload artifacts + uses: actions/upload-artifact@v1 + with: + name: Lighthouse reports + path: /tmp/lighthouse-artifacts + + # Using a pre-build action to make the action fail if your score is too low. It can be really interesting to track a low score on a commit + # You can remove this action IF you don't want lighthouse to be a blocking point in your CI + # Official documentation: https://github.com/foo-software/lighthouse-check-status-action + - name: Handle Lighthouse Check results + uses: foo-software/lighthouse-check-status-action@v1.0.1 # See https://github.com/foo-software/lighthouse-check-action + with: + lighthouseCheckResults: ${{ steps.lighthouseCheck.outputs.lighthouseCheckResults }} + minAccessibilityScore: "50" + minBestPracticesScore: "50" + minPerformanceScore: "30" + minProgressiveWebAppScore: "50" + minSeoScore: "50" diff --git a/.github/workflows/deploy-vercel-storybook.yml b/.github/workflows/deploy-vercel-storybook.yml new file mode 100644 index 000000000..7852cf7b6 --- /dev/null +++ b/.github/workflows/deploy-vercel-storybook.yml @@ -0,0 +1,259 @@ +# Summary: +# Builds a static version of the Storybook website and triggers a new deployment on Vercel's platform, when anything is pushed in any branch. +# +# LEARN MORE AT https://unlyed.github.io/next-right-now/guides/ci-cd/ +# +# Dependencies overview: +# - See https://github.com/actions/setup-node https://github.com/actions/setup-node/tree/v2 +# - See https://github.com/actions/checkout https://github.com/actions/checkout/tree/v1 +# - See https://github.com/actions/upload-artifact https://github.com/actions/upload-artifact/tree/v1 +# - See https://github.com/rlespinasse/github-slug-action https://github.com/rlespinasse/github-slug-action/tree/3.x +# - See https://github.com/jwalton/gh-find-current-pr https://github.com/jwalton/gh-find-current-pr/tree/v1 +# - See https://github.com/peter-evans/create-or-update-comment https://github.com/peter-evans/create-or-update-comment/tree/v1 +# - See https://github.com/UnlyEd/github-action-await-vercel https://github.com/UnlyEd/github-action-await-vercel/tree/v1.1.1 +# - See https://github.com/UnlyEd/github-action-store-variable https://github.com/UnlyEd/github-action-store-variable/tree/v2.1.1 +# - See https://github.com/cypress-io/github-action https://github.com/cypress-io/github-action/tree/v2 + +name: Deploy Storybook static site to Vercel + +on: + # There are several ways to trigger Github actions - See https://help.github.com/en/actions/reference/events-that-trigger-workflows#example-using-a-single-event for a comprehensive list: + # - "push": Triggers each time a commit is pushed + # - "pull_request": Triggers each time a commit is pushed within a pull request, it makes it much easier to write comments within the PR, but it suffers some strong limitations: + # - There is no way to trigger when a PR is merged into another - See https://github.community/t/pull-request-action-does-not-run-on-merge/16092?u=vadorequest + # - It won't trigger when the PR is conflicting with its base branch - See https://github.community/t/run-actions-on-pull-requests-with-merge-conflicts/17104/2?u=vadorequest + push: # Triggers on each pushed commit + branches: + - '*' + + # Runs the deployment workflow only when there are changes made to specific files, in any of the below paths + # Optimizes our CI/CD by not deploying the Storybook static site needlessly (faster deploy, lower cost) + # XXX Filter pattern cheat sheet https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet + paths: + - '.github/workflows/deploy-vercel-storybook.yml' + - '.storybook/**' + - 'cypress/**' + - '!cypress/_examples/**' + - '!cypress/fixture/**' + - '!cypress/integration/**' + - '!cypress/config-*.json' # Exclude all config files + - 'cypress/config-storybook.json' # Force include storybook config file + - 'src/stories/**' + - 'src/common/components/**' + - 'src/**/components/**' # Any component in any "components" folder under "src". (e.g: "src/auth/components/comp1", "src/modules/core/auth/components/comp1", etc.) + - '!src/app/components/*Bootstrap*' # Ignore bootstrap components, as they aren't used by Storybook + - '*.js*' # Includes all .js/.json at the root level + - '*.ts' # Includes all .ts at the root level + - '.*ignore' # Includes .gitignore and .vercelignore + - 'yarn.lock' + - '!**/*.md' # Exclude all markdown files + - 'stories/**/*.md' # Force include all markdown files within stories (because they're used for storybook) + +env: + STAGE: staging + +jobs: + # Configures the deployment environment, install dependencies (like node, npm, etc.) that are requirements for the upcoming jobs + # Ex: Necessary to run `yarn deploy` + setup-environment: + name: Setup deployment environment (Ubuntu latest - Node 14.x) + runs-on: ubuntu-latest + steps: + - name: Installing node.js + uses: actions/setup-node@v2 # Used to install node environment - https://github.com/actions/setup-node + with: + node-version: '14.x' # Use the same node.js version as the one Vercel's uses (currently node14.x) + + # Starts a Vercel deployment, using the storybook configuration file + # N.B: It's Vercel that will perform the actual deployment + start-deployment: + name: Starts Vercel deployment (Ubuntu latest) + runs-on: ubuntu-latest + timeout-minutes: 40 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + needs: setup-environment + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + - name: Expose GitHub slug/short variables # See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + uses: rlespinasse/github-slug-action@v3.x # See https://github.com/rlespinasse/github-slug-action + + # Create a GitHub deployment (within a GitHub environment), useful to keep a public track of all deployments directly in GitHub + - name: Start GitHub deployment + uses: bobheadxi/deployments@v0.4.3 # See https://github.com/bobheadxi/deployments + id: start-github-deployment + with: + step: start + token: ${{ secrets.GITHUB_TOKEN }} + env: storybook # Uses "storybook" as GitHub environment name, because we don't need to manage multiple environments for storybook + no_override: true # Disables auto marking previous environments as "inactive", as they're still active (Vercel deployments don't auto-deactivate) and it would remove the previous deployment links needlessly + + - name: Install storybook dependencies + run: yarn install + + - name: Deploying on Vercel + uses: UnlyEd/github-action-deploy-on-vercel@94d41ec1ff9b5b1de5256312e385632b6fcd8fa4 # Pin "v1.2.1" - See https://github.com/UnlyEd/github-action-deploy-on-vercel/commit/94d41ec1ff9b5b1de5256312e385632b6fcd8fa4 + with: + command: "yarn deploy:sb:gha --token ${{ secrets.VERCEL_TOKEN }}" + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} # Passing github's secret to the worker + # Passing exposed GitHub environment variables - See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + GITHUB_REF_SLUG: ${{ env.GITHUB_REF_SLUG }} + + # Update the previously created GitHub deployment, and link it to our Vercel deployment + - name: Link GitHub deployment to Vercel + uses: bobheadxi/deployments@v0.4.3 # See https://github.com/bobheadxi/deployments + id: link-github-deployment-to-vercel + if: always() + with: + step: finish + token: ${{ secrets.GITHUB_TOKEN }} + status: ${{ job.status }} + deployment_id: ${{ steps.start-github-deployment.outputs.deployment_id }} + env_url: ${{ env.VERCEL_DEPLOYMENT_URL }} # Link the Vercel deployment url to the GitHub environment + + # We need to find the PR id. Will be used later to comment on that PR. + - name: Finding Pull Request ID + uses: jwalton/gh-find-current-pr@v1 # See https://github.com/jwalton/gh-find-current-pr + id: pr_id_finder + if: always() # It forces the job to be always executed, even if a previous job fail. + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # On deployment failure, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (Deployment failure) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: steps.pr_id_finder.outputs.number && failure() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ steps.pr_id_finder.outputs.number }} + body: | + :x:  Deployment **FAILED** + Commit ${{ github.sha }} failed to deploy **Storybook static site** to [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + [click to see logs](https://github.com/UnlyEd/next-right-now/pull/${{ steps.pr_id_finder.outputs.number }}/checks) + + # On deployment success, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (Deployment success) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: steps.pr_id_finder.outputs.number && success() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ steps.pr_id_finder.outputs.number }} + body: | + :white_check_mark:  Deployment **SUCCESS** + Commit ${{ github.sha }} successfully deployed **Storybook static site** :open_book: to [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + Deployment aliases (${{ env.VERCEL_ALIASES_CREATED_COUNT }}): ${{ env.VERCEL_ALIASES_CREATED_URLS_MD }} + + # At the end of the job, store all variables we will need in the following jobs + # The variables will be stored in and retrieved from a GitHub Artifact (each variable is stored in a different file) + - name: Store variables for next jobs + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + with: + variables: | + VERCEL_DEPLOYMENT_URL=${{ env.VERCEL_DEPLOYMENT_URL }} + VERCEL_DEPLOYMENT_DOMAIN=${{ env.VERCEL_DEPLOYMENT_DOMAIN }} + GITHUB_PULL_REQUEST_ID=${{ steps.pr_id_finder.outputs.number }} + + # Waits for the Vercel deployment to reach "READY" state, so that other actions will be applied on a domain that is really online + await-for-vercel-deployment: + name: Await current deployment to be ready (Ubuntu latest) + timeout-minutes: 5 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + runs-on: ubuntu-latest + needs: start-deployment + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + VERCEL_DEPLOYMENT_DOMAIN + + # Wait for deployment to be ready, before running E2E (otherwise Cypress might start testing too early, and gets redirected to Vercel's "Login page", and tests fail) + - name: Awaiting Vercel deployment to be ready + uses: UnlyEd/github-action-await-vercel@v1.1.1 # See https://github.com/UnlyEd/github-action-await-vercel + id: await-vercel + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + with: + deployment-url: ${{ env.VERCEL_DEPLOYMENT_DOMAIN }} # Must only contain the domain name (no http prefix, etc.) + timeout: 90 # Wait for 90 seconds before failing + + - name: Display deployment status + run: "echo The deployment is ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).readyState }}" + + # Runs E2E tests against the Vercel deployment + run-2e2-tests: + name: Run end to end (E2E) tests (Ubuntu latest) + timeout-minutes: 20 # Limit current job timeout https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes + runs-on: ubuntu-latest + # Docker image with Cypress pre-installed + # https://github.com/cypress-io/cypress-docker-images/tree/master/included + container: cypress/included:7.4.0 + needs: await-for-vercel-deployment + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v2.1.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + failIfNotFound: true + variables: | + VERCEL_DEPLOYMENT_URL + GITHUB_PULL_REQUEST_ID + + # Runs the E2E tests against the new Vercel deployment + - name: Run E2E tests (Cypress) + uses: cypress-io/github-action@v2 # See https://github.com/cypress-io/github-action + with: + # XXX We disabled "wait-on" option, because it's useless. Cypress will fail anyway, because it gets redirected to some internal Vercel URL if the domain isn't yet available - See https://github.com/cypress-io/github-action/issues/270 + # wait-on: '${{ env.VERCEL_DEPLOYMENT_URL }}' # Be sure that the endpoint is ready by pinging it before starting tests, using a default timeout of 60 seconds + config-file: 'cypress/config-storybook.json' # Use Cypress config file for Storybook, and override it below + config: baseUrl=${{ env.VERCEL_DEPLOYMENT_URL }} # Overriding baseUrl provided by config file to test the new deployment + env: + # Enables Cypress debugging logs, very useful if Cypress crashes, like out-of-memory issues. + # DEBUG: "cypress:*" # Enable all logs. See https://docs.cypress.io/guides/references/troubleshooting.html#Print-DEBUG-logs + DEBUG: "cypress:server:util:process_profiler" # Enable logs for "memory and CPU usage". See https://docs.cypress.io/guides/references/troubleshooting.html#Log-memory-and-CPU-usage + + # On E2E failure, upload screenshots + - name: Upload screenshots artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: screenshots + path: cypress/screenshots/ + + # On E2E failure, upload videos + - name: Upload videos artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: videos + path: cypress/videos/ + + # On E2E failure, add a comment to the PR with additional information, if there is an open PR for the current branch + - name: Comment PR (E2E failure) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: env.GITHUB_PULL_REQUEST_ID && failure() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ env.GITHUB_PULL_REQUEST_ID }} + body: | + :x:  E2E tests **FAILED** for commit ${{ github.sha }} previously deployed **Storybook static site** at [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + Download artifacts (screenshots + videos) from [`checks`](https://github.com/UnlyEd/next-right-now/pull/${{ env.GITHUB_PULL_REQUEST_ID }}/checks) section + + # On E2E success, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (E2E success) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: env.GITHUB_PULL_REQUEST_ID && success() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ env.GITHUB_PULL_REQUEST_ID }} + body: | + :white_check_mark:  E2E tests **SUCCESS** for commit ${{ github.sha }} previously deployed **Storybook static site** at [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy-zeit-production.yml b/.github/workflows/deploy-zeit-production.yml deleted file mode 100644 index 8c8394eb8..000000000 --- a/.github/workflows/deploy-zeit-production.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Summary: -# Creates a new deployment on Zeit's platform, when anything is pushed in any branch (except for the "master" branch). -# Read ./README.md for extensive documentation - -name: Deploy to Zeit (production) - -on: - push: - branches: - - 'master' - -jobs: - # Configures the deployment environment, install dependencies (like node, npm, etc.) that are requirements for the upcoming jobs - # Ex: Necessary to run `yarn deploy` - setup-environment: - name: Setup deployment environment (Ubuntu 18.04 - Node 10.x) - runs-on: ubuntu-18.04 - steps: - - name: Installing node.js - uses: actions/setup-node@v1 # Used to install node environment - XXX https://github.com/actions/setup-node - with: - node-version: '10.x' # Use the same node.js version as the one Zeit's uses (currently node10.x) - - # Starts a Zeit deployment, using the production configuration file of the default institution - # The default institution is the one defined in the `now.json` file (which is a symlink to the actual file) - # N.B: It's Zeit that will perform the actual deployment - start-production-deployment: - name: Starts Zeit deployment (production) (Ubuntu 18.04) - runs-on: ubuntu-18.04 - needs: setup-environment - steps: - - uses: actions/checkout@v1 # Get last commit pushed - XXX See https://github.com/actions/checkout - - name: Deploying on Zeit - run: yarn deploy:$(cat now.json | jq -r '.build.env.CUSTOMER_REF'):production --token $ZEIT_TOKEN - env: - ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }} # Passing github's secret to the worker - - # Runs E2E tests against the Zeit deployment - run-2e2-tests: - name: Run end to end (E2E) tests (Ubuntu 18.04) - runs-on: ubuntu-18.04 - # Docker image with Cypress pre-installed - # https://github.com/cypress-io/cypress-docker-images/tree/master/included - container: cypress/included:3.8.3 - needs: start-production-deployment - steps: - - uses: actions/checkout@v1 # Get last commit pushed - XXX See https://github.com/actions/checkout - - name: Resolving deployment url from Zeit - # The following workflow is: - # - getting all deployments data (by using the scope in `now.json`) - # - then we get the last url (in Node.js it corresponds as `response.deployments[0].url` - # - and then we remove the `"` character to pre-format url - # We need to set env the url for next step, formatted as `https://${url provided by API}` - run: | - apt update -y >/dev/null && apt install -y jq >/dev/null - ZEIT_DEPLOYMENT=`curl -H 'Accept: application/json' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${{ secrets.ZEIT_TOKEN }}' https://api.zeit.co/v5/now/deployments?teamId=$(cat now.json | jq -r '.scope') | jq '.deployments [0].url' | tr -d \"` - echo "::set-env name=ZEIT_DEPLOYMENT_URL::https://$ZEIT_DEPLOYMENT" - - # Run the E2E tests against the new Zeit deployment - - name: Run E2E tests (Cypress) - uses: cypress-io/github-action@v1 # XXX See https://github.com/cypress-io/github-action - with: - wait-on: ${{ env.ZEIT_DEPLOYMENT_URL }} # Be sure that the endpoint is ready by pinging it before starting tests, it has a timeout of 60seconds - config-file: cypress/config-customer1.json # The config file itself doesn't matter because we will override most settings anyway. We just need `projectId` to run the tests. - config: baseUrl=${{ env.ZEIT_DEPLOYMENT_URL }} # Overriding baseUrl provided by config file to test the new deployment - - # On E2E failure, upload screenshots - - name: Uplad screenshots artifacts (E2E failure) - uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts - if: failure() - with: - name: screenshots - path: cypress/screenshots/ - - # On E2E failure, upload videos - - name: Uplad videos artifacts (E2E failure) - uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts - if: failure() - with: - name: videos - path: cypress/videos/ diff --git a/.github/workflows/deploy-zeit-staging.yml b/.github/workflows/deploy-zeit-staging.yml deleted file mode 100644 index 2cecbe90b..000000000 --- a/.github/workflows/deploy-zeit-staging.yml +++ /dev/null @@ -1,144 +0,0 @@ -# Summary: -# Creates a new deployment on Zeit's platform, when anything is pushed in any branch (except for the "master" branch). -# Read ./README.md for extensive documentation - -name: Deploy to Zeit (staging) - -on: - push: - branches-ignore: - - 'master' - -jobs: - # Configures the deployment environment, install dependencies (like node, npm, etc.) that are requirements for the upcoming jobs - # Ex: Necessary to run `yarn deploy` - setup-environment: - name: Setup deployment environment (Ubuntu 18.04 - Node 10.x) - runs-on: ubuntu-18.04 - steps: - - name: Installing node.js - uses: actions/setup-node@v1 # Used to install node environment - XXX https://github.com/actions/setup-node - with: - node-version: '10.x' # Use the same node.js version as the one Zeit's uses (currently node10.x) - - # Starts a Zeit deployment, using the staging configuration file of the default institution - # The default institution is the one defined in the `now.json` file (which is a symlink to the actual file) - # N.B: It's Zeit that will perform the actual deployment - start-staging-deployment: - name: Starts Zeit deployment (staging) (Ubuntu 18.04) - runs-on: ubuntu-18.04 - needs: setup-environment - steps: - - uses: actions/checkout@v1 # Get last commit pushed - XXX See https://github.com/actions/checkout - - name: Deploying on Zeit - # Workflow: - # - Get stdout from deploy command (stderr shows build steps and stdout shows final url, what we are looking for) - # - Set deployment url to show on PR message - # - Create alias and link it - run: | - ZEIT_DEPLOYMENT_OUTPUT=`yarn deploy:$(cat now.json | jq -r '.build.env.CUSTOMER_REF') --token $ZEIT_TOKEN` - - ZEIT_DEPLOYMENT_URL=`echo $ZEIT_DEPLOYMENT_OUTPUT | egrep -o 'https?://[^ ]+.now.sh'` - echo "::set-env name=ZEIT_DEPLOYMENT_URL::$ZEIT_DEPLOYMENT_URL" - - if [[ ${CURRENT_BRANCH##*/} =~ ^v[0-9]{1,}- ]]; then # Checking if pattern matches with "vX-" where X is a number - ZEIT_DEPLOYMENT_ALIAS=${CURRENT_BRANCH##*/} - else - ZEIT_DEPLOYMENT_ALIAS=$(cat now.json | jq -r '.name')-${CURRENT_BRANCH##*/} - fi - - # Zeit alias only allows 53 characters in the domain name, so we only keep the first 46 characters (because Zeit needs 7 chars for ".now.sh" at the end of the domain name) - # Also, in order to remove forbidden characters, we create a sequence from ascii numbers using ranges of numbers (we forbid characters by using `seq X Y` and we add others by using ';'). - # All special characters are converted to `-` (using `tr $0 $1` where $0 is replaced by $1), and only 0-9 and a-Z chars are kept intact. - # We then use `awk` to convert "ascii numbers" back into actual characters. - # You can find the numbers equivalence by tapping `man ascii` and look at the "decimal" set. - ZEIT_DEPLOYMENT_ALIAS=$(echo $ZEIT_DEPLOYMENT_ALIAS | head -c 46 | tr "`(seq 0 47 ; seq 58 64 ; seq 91 96 && seq 123 127) | awk '{printf("%c",$1)}'`" -).now.sh - echo "::set-env name=ZEIT_DEPLOYMENT_ALIAS::https://$ZEIT_DEPLOYMENT_ALIAS" - - npx now alias $ZEIT_DEPLOYMENT_URL https://$ZEIT_DEPLOYMENT_ALIAS --token $ZEIT_TOKEN - env: - ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }} # Passing github's secret to the worker - CURRENT_BRANCH: ${{ github.ref }} # Passing current branch to worker - - # On deployment failure, add a comment to the PR - - name: Comment PR (Deployment failure) - uses: unsplash/comment-on-pr@master - if: failure() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_CI_PR_COMMENT }} - with: - msg: "[GitHub Actions]\nDeployment FAILED\n\t Commit ${{ github.sha }} failed to deploy to ${{ env.ZEIT_DEPLOYMENT_URL }} (click to see logs)" - - # On deployment success, add a comment to the PR - - name: Comment PR (Deployment success) - uses: unsplash/comment-on-pr@master - if: success() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_CI_PR_COMMENT }} - with: - msg: "[GitHub Actions]\nDeployment SUCCESS\n\t Commit ${{ github.sha }} successfully deployed to ${{ env.ZEIT_DEPLOYMENT_URL }}\n\tDeployment aliased as ${{ env.ZEIT_DEPLOYMENT_ALIAS }}" - - # Runs E2E tests against the Zeit deployment - run-2e2-tests: - name: Run end to end (E2E) tests (Ubuntu 18.04) - runs-on: ubuntu-18.04 - # Docker image with Cypress pre-installed - # https://github.com/cypress-io/cypress-docker-images/tree/master/included - container: cypress/included:3.8.3 - needs: start-staging-deployment - steps: - - uses: actions/checkout@v1 # Get last commit pushed - XXX See https://github.com/actions/checkout - - name: Resolving deployment url from Zeit - # The following workflow is: - # - getting all deployments data (by using the scope in `now.json`) - # - then we get the last url (in Node.js it corresponds as `response.deployments[0].url` - # - and then we remove the `"` character to pre-format url - # We need to set env the url for next step, formatted as `https://${url provided by API}` - run: | - apt update -y >/dev/null && apt install -y jq >/dev/null - ZEIT_DEPLOYMENT=`curl -H 'Accept: application/json' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${{ secrets.ZEIT_TOKEN }}' https://api.zeit.co/v5/now/deployments?teamId=$(cat now.json | jq -r '.scope') | jq '.deployments [0].url' | tr -d \"` - echo "::set-env name=ZEIT_DEPLOYMENT_URL::https://$ZEIT_DEPLOYMENT" - env: - ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }} # Passing github's secret to the worker - - # Run the E2E tests against the new Zeit deployment - - name: Run E2E tests (Cypress) - uses: cypress-io/github-action@v1 # XXX See https://github.com/cypress-io/github-action - with: - wait-on: ${{ env.ZEIT_DEPLOYMENT_URL }} # Be sure that the endpoint is ready by pinging it before starting tests, it has a timeout of 60seconds - config-file: cypress/config-customer1.json # The config file itself doesn't matter because we will override most settings anyway. We just need `projectId` to run the tests. - config: baseUrl=${{ env.ZEIT_DEPLOYMENT_URL }} # Overriding baseUrl provided by config file to test the new deployment - - # On E2E failure, upload screenshots - - name: Uplad screenshots artifacts (E2E failure) - uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts - if: failure() - with: - name: screenshots - path: cypress/screenshots/ - - # On E2E failure, upload videos - - name: Uplad videos artifacts (E2E failure) - uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts - if: failure() - with: - name: videos - path: cypress/videos/ - - # On E2E failure, add a comment to the PR with additional information - - name: Comment PR (E2E failure) - uses: unsplash/comment-on-pr@master - if: failure() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_CI_PR_COMMENT }} - with: - msg: "[GitHub Actions]\nE2E tests FAILED\n Download artifacts (screenshots + videos) from `checks` section at the top" - - # On E2E success, add a comment to the PR - - name: Comment PR (E2E success) - uses: unsplash/comment-on-pr@master - if: success() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_CI_PR_COMMENT }} - with: - msg: "[GitHub Actions]\nE2E tests SUCCESS" diff --git a/.github/workflows/update-codeclimate-coverage.yml b/.github/workflows/update-codeclimate-coverage.yml new file mode 100644 index 000000000..261fed320 --- /dev/null +++ b/.github/workflows/update-codeclimate-coverage.yml @@ -0,0 +1,40 @@ +# Summary: +# Runs unit and coverage tests, then uploads the coverage results to the Code Climate dashboard. +# +# LEARN MORE AT https://unlyed.github.io/next-right-now/guides/ci-cd/ +# +# Dependencies overview: +# - See https://github.com/actions/setup-node https://github.com/actions/setup-node/tree/v2 +# - https://github.com/paambaati/codeclimate-action https://github.com/paambaati/codeclimate-action/tree/v2.6.0 + +name: Update Code Climate test coverage + +on: + push: + branches: + - 'v2-mst-aptd-at-lcz-sty' # Change this branch name by your CodeClimate "main" branch + +jobs: + # Configures the deployment environment, install dependencies (like node, npm, etc.) that are requirements for the upcoming jobs + # Ex: Necessary to run `yarn test:coverage` + setup-environment: + name: Setup deployment environment (Ubuntu latest - Node 14.x) + runs-on: ubuntu-latest + steps: + - name: Installing node.js + uses: actions/setup-node@v2 # Used to install node environment - XXX https://github.com/actions/setup-node + with: + node-version: '14.x' # Use the same node.js version as the one Vercel's uses (currently node14.x) + + run-tests-coverage: + name: Run tests coverage and send report to Code Climate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Installing dependencies + run: yarn install + - uses: paambaati/codeclimate-action@v2.6.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} # XXX Define this secret in "Github repo > Settings > Secrets", you can get it from Code Climate in "Repo settings > Test coverage". + with: + coverageCommand: yarn test:coverage:group:no-integration diff --git a/.gitignore b/.gitignore index 95f6f4fbe..509e31c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ fabric.properties *.iml modules.xml .idea/misc.xml +.idea/codeStyles *.ipr vcs.xml @@ -118,9 +119,10 @@ coverage/ /.next/ /out/ -# misc -.env* -!.env.build.example +# Next.js environment variables - See https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables +.env.build +.env*.local +!.env*.example # debug npm-debug.log* @@ -135,10 +137,20 @@ cypress/videos src/svg/*.js src/svg/*.tsx -# Zeit +# Vercel .now +.vercel # Jekyll - Github Pages _site .sass-cache .jekyll-metadata + +# Tmp files (cache, etc.) +*.cache + +# Storybook +storybook-static/ + +# Jest +.jest-test-results.json diff --git a/.graphqlconfig b/.graphqlconfig index ae939895e..32115e290 100644 --- a/.graphqlconfig +++ b/.graphqlconfig @@ -3,7 +3,7 @@ "extensions": { "endpoints": { "master": { - "url": "https://api-euwest.graphcms.com/v1/ck73ixhlv09yt01dv2ga1bkbp/master", + "url": "https://api-eu-central-1.graphcms.com/v2/ck73ixhlv09yt01dv2ga1bkbp/master", "introspect": true, "headers": { "user-agent": "JS GraphQL", diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 000000000..d23208fbb --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nowignore b/.nowignore deleted file mode 100644 index 4cbc376fd..000000000 --- a/.nowignore +++ /dev/null @@ -1,14 +0,0 @@ -# Optimize speed/size of deployments by not uploading those files -.idea/ -node_modules/ -cypress/ -coverage/ -.codeclimate.yml -.editorconfig -.env.build.example -.graphqlconfig -buildspec.yml -README.md -schema.graphql -yarn.lock -yarn-error.log diff --git a/.nvmrc b/.nvmrc index 59db31cba..158c00641 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v12.16.1 +v14.16.0 diff --git a/.storybook/.gitignore b/.storybook/.gitignore new file mode 100644 index 000000000..f3e842096 --- /dev/null +++ b/.storybook/.gitignore @@ -0,0 +1 @@ +*.cache* diff --git a/.storybook/babel.config.js b/.storybook/babel.config.js new file mode 100644 index 000000000..ff1dbcd83 --- /dev/null +++ b/.storybook/babel.config.js @@ -0,0 +1,13 @@ +/** + * Babel configuration for Storybook + * + * Doesn't affect Next.js babel configuration, specific file for Storybook only. + * Need to apply Emotion babel configuration, otherwise Emotion "css" cannot be used in Storybook. + * + * XXX We use the "classic" way instead of the "automatic" way for Storybook, that's because MDX isn't compatible with "automatic". + * + * @see https://emotion.sh/docs/css-prop#babel-preset Configuring Emotion 11 + */ +module.exports = { + "presets": ["@emotion/babel-preset-css-prop"] +}; diff --git a/.storybook/jsconfig.json b/.storybook/jsconfig.json new file mode 100644 index 000000000..84c874b98 --- /dev/null +++ b/.storybook/jsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/app/*": [ + "../src/app/*" + ], + "@/common/*": [ + "../src/common/*" + ], + "@/components/*": [ + "../src/common/components/*" + ], + "@/utils/*": [ + "../src/common/utils/*" + ], + "@/layouts/*": [ + "../src/layouts/*" + ], + "@/modules/*": [ + "../src/modules/*" + ], + "@/pages/*": [ + "../src/pages/*" + ] + } + } +} diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 000000000..c6d9efe68 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,255 @@ +const { promises: fs } = require('fs'); +const path = require('path'); +const fetch = require('node-fetch'); + +const toPath = (_path) => path.join(process.cwd(), _path); + +try { + require('../.jest-test-results.json'); + console.warn(`[Storybook Jest config] Found "/.jest-test-results.json". \nStories will display the latest Jest test results. \nRunning "yarn:test" in parallel of storybook will keep Storybook up-to-date with the latest test result.`); +} catch (e) { + console.warn(`[Storybook Jest config] The test results file couldn't be found in "/.jest-test-results.json". \nStories will not display Jest test results. \nRunning "yarn test:generate-output" prior to running storybook will fix this.`); +} + +/** + * Fetches translations from Locize and store them in the filesystem. + * They will be loaded in preview.js, which will configure Locize so that components can display their translations. + * + * @param environment + */ +const fetchLocizeTranslation = async (environment) => { + const cacheFileName = '.sb-translations.cache.json'; + const version = environment === 'development' ? 'latest' : 'production'; + const languages = ['en', 'fr']; + const namespaces = ['common']; + const allI18nTranslations = {}; + + for (let i = 0; i < languages.length; i++) { + const lang = languages[i]; + for (let j = 0; j < namespaces.length; j++) { + const namespace = namespaces[j]; + const locizeAPIEndpoint = `https://api.locize.app/${process.env.NEXT_PUBLIC_LOCIZE_PROJECT_ID}/${version}/${lang}/${namespace}`; + console.log('Fetching translations from:', locizeAPIEndpoint); + const defaultI18nTranslationsResponse = await fetch(locizeAPIEndpoint); + const i18nTranslations = await defaultI18nTranslationsResponse.json(); + + allI18nTranslations[lang] = allI18nTranslations[lang] || {}; + allI18nTranslations[lang][namespace] = i18nTranslations; + } + } + + // Store translations + const translationCacheFile = path.join(__dirname, cacheFileName); + console.log('Writing translations cache to:', translationCacheFile); + + await fs.writeFile(translationCacheFile, JSON.stringify(allI18nTranslations, null, 2), 'utf8'); +}; + +module.exports = { + stories: [ + '../src/**/*.stories.mdx', + '../src/**/*.stories.@(js|jsx|ts|tsx)', + ], + addons: [ + /** + * The Storybook Links addon can be used to create links that navigate between stories in Storybook. + * + * @see https://www.npmjs.com/package/@storybook/addon-links + */ + '@storybook/addon-links', + + /** + * Present including "essential" Storybook addons, such as: + * + * - Actions - Storybook Addon Actions can be used to display data received by event handlers in Storybook. + * It's where the action you do are being logged. + * + * - Backgrounds - Storybook Addon Backgrounds can be used to change background colors inside the preview in Storybook. + * + * - Controls - Controls gives you a graphical UI to interact with a component's arguments dynamically, without needing to code. + * It creates an addon panel next to your component examples ("stories"), so you can edit them live. + * + * - Docs - Storybook Docs transforms your Storybook stories into world-class component documentation. + * - DocsPage: Out of the box, all your stories get a DocsPage. DocsPage is a zero-config aggregation + * of your component stories, text descriptions, docgen comments, props tables, and code examples into clean, readable pages. + * - MDX: If you want more control, MDX allows you to write long-form markdown documentation and stories in one file. + * You can also use it to write pure documentation pages and embed them inside your Storybook alongside your stories. + * + * - Viewport - Storybook Viewport Addon allows your stories to be displayed in different sizes and layouts in Storybook. + * This helps build responsive components inside of Storybook. + * + * - Toolbars - The Toolbars addon controls global story rendering options from Storybook's toolbar UI. It's a general purpose addon that can be used to: + * - set a theme for your components + * - set your components' internationalization (i18n) locale + * - configure just about anything in Storybook that makes use of a global variable + * + * @see https://storybook.js.org/addons/essentials + * @see https://github.com/storybookjs/storybook/tree/master/addons/essentials + * @see https://github.com/storybookjs/storybook/tree/next/addons/actions + * @see https://github.com/storybookjs/storybook/tree/next/addons/backgrounds + * @see https://github.com/storybookjs/storybook/tree/next/addons/controls + * @see https://github.com/storybookjs/storybook/tree/next/addons/docs + * @see https://github.com/storybookjs/storybook/tree/next/addons/viewport + * @see https://github.com/storybookjs/storybook/tree/next/addons/toolbars + * + * You can disable addons you don't want through configuration. + * @see https://github.com/storybookjs/storybook/tree/master/addons/essentials#disabling-addons + */ + { + name: '@storybook/addon-essentials', + options: { + actions: true, + backgrounds: true, + controls: true, + docs: true, + viewport: true, + toolbars: true, + }, + }, + + /** + * Storybook Addon Knobs has been replaced by Controls and is being deprecated, it will be removed in v7. + * + * It is listed below for documentation purpose and help you avoid using it thinking it's still legit. + * + * @see https://github.com/storybookjs/storybook/blob/next/addons/controls/README.md#how-will-this-replace-addon-knobs + */ + // '@storybook/addon-knobs', + + /** + * We use Google Analytics for tracking analytics usage. + * + * It's much easier to setup than Amplitude, because there is an official dedicated plugin for this. + * See ".storybook/manager.js" for Google Analytics configuration. + * + * @see https://github.com/storybookjs/storybook/tree/master/addons/google-analytics + */ + '@storybook/addon-google-analytics', + + /** + * Shows stories source in the addon panel. (display the source code of the story in a dedicated panel) + * + * Adds an "Story" tab. + * + * XXX Disabled for now, because of https://github.com/storybookjs/storybook/issues/13657 (brings no useful information at the moment) + * Better to use the Docs panel and display source code, that's a good workaround for now. + * + * @see https://github.com/storybookjs/storybook/tree/master/addons/storysource + */ + // '@storybook/addon-storysource', + + /** + * This storybook addon can be helpful to make your UI components more accessible. + * + * Adds an "Accessibility" tab. + * + * @see https://www.npmjs.com/package/@storybook/addon-a11y + */ + '@storybook/addon-a11y', + + // ------------------- Non official addons below ------------------ + + /** + * Adds support for CSS Modules. + * + * Even though Next Right Now doesn't encourage the use of CSS Modules, + * we thought it's an interesting feature to support, which is natively supported by Next.js. + * + * @see https://www.npmjs.com/package/storybook-css-modules-preset How to configure Storybook to support CSS Modules + * @see https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css How to use CSS Modules with Next.js + */ + 'storybook-css-modules-preset', + + /** + * Brings Jest results in storybook. + * + * @see https://github.com/storybookjs/storybook/tree/master/addons/jest + */ + '@storybook/addon-jest', + + /** + * A storybook addon to help better understand and debug performance for React components. + * + * Adds a "Performance" tab. + * + * @see https://github.com/atlassian-labs/storybook-addon-performance + */ + 'storybook-addon-performance/register', + + /** + * Offers suggestions on how you can improve the HTML, CSS and UX of your components to be more mobile-friendly. + * + * Adds a "Mobile" tab. + * + * @see https://github.com/aholachek/storybook-mobile + */ + 'storybook-mobile', + + /** + * A Storybook addon that embed Figma, websites, PDF or images in the addon panel. + * + * Adds a "Design" tab. + * + * @see https://github.com/pocka/storybook-addon-designs + */ + 'storybook-addon-designs', + ], + + /** + * Customize webpack configuration for Storybook. + * + * This doesn't affect the Next.js application, only the Storybook compilation. + * + * @param config + * @see https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project + */ + webpackFinal: async (config) => { + const { + mode: environment, + plugins, + module, + } = config; + await fetchLocizeTranslation(environment); + + return { + ...config, + resolve: { + ...config.resolve, + alias: { + ...config.resolve.alias, + + /** + * Map Emotion 10 libraries to Emotion 11 libraries. + * + * Otherwise Storybook fails to compile with "Module not found: Error: Can't resolve '@emotion/styled/base'", etc. + * It wasn't necessary to do this until we imported React component using "@emotion/styled". + * This issue is probably caused because Storybook uses Emotion 10 while we have Emotion 11 used by the Next.js app. + * + * @see https://github.com/storybookjs/storybook/issues/13277#issuecomment-751747964 + */ + '@emotion/core': toPath('node_modules/@emotion/react'), + '@emotion/styled': toPath('node_modules/@emotion/styled'), + 'emotion-theming': toPath('node_modules/@emotion/react'), + + /** + * Map our module path aliases, so that Storybook can understand modules loaded using "@/common" and load the proper file. + * Required, or Storybook will fail to import dependencies from Stories. + * + * XXX The below list must match `tsconfig.json:compilerOptions.paths`, so the Next.js app and Storybook resolve all aliases the same way. + * The paths mapping must also match the `jsconfig.json:compilerOptions.paths` file, which is necessary for WebStorm to understand them for .js files. + * + * @see https://nextjs.org/docs/advanced-features/module-path-aliases + * @see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003361399/comments/360002636080 + */ + "@/app": path.resolve(__dirname, "../src/app"), + "@/common": path.resolve(__dirname, "../src/common"), + "@/components": path.resolve(__dirname, "../src/common/components"), + "@/utils": path.resolve(__dirname, "../src/common/utils"), + "@/layouts": path.resolve(__dirname, "../src/layouts"), + "@/modules": path.resolve(__dirname, "../src/modules"), + "@/pages": path.resolve(__dirname, "../src/pages"), + }, + }, + }; + }, +}; diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 000000000..2df85ffaa --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,36 @@ +import { addons } from '@storybook/addons'; +import { themes } from '@storybook/theming'; + +/** + * Configure Storybook UI layout. + * + * XXX The Storybook manager seems to suffer from a cache invalidation issue, which forces us to run with `--no-manager-cache` option. + * @see https://github.com/storybookjs/storybook/issues/13649#issuecomment-761076960 + * @see https://github.com/storybookjs/storybook/issues/13200 + * + * @see https://storybook.js.org/docs/react/configure/features-and-behavior + */ +addons.setConfig({ + theme: themes.dark, +}); + +/** + * Your Google Analytics tracking ID. + * + * If you're creating a dedicated Google Analytics property for this (you should), + * Then make sure to create a "Universal Analytics property", not a Google Analytics 4 property (default since 2021). + * + * @see https://support.google.com/analytics/answer/10269537 + * @see https://github.com/storybookjs/storybook/tree/master/addons/google-analytics Google Analytic addon for Storybook + */ +window.STORYBOOK_GA_ID = 'UA-89785688-10'; // Replace by your own "UA-XXXXXXX-XX" + +/** + * React-ga options object + * + * @example { debug: true, gaOptions: { userId: 123 }} + * @see https://github.com/react-ga/react-ga#api + * @see https://github.com/storybookjs/storybook/blob/4f5ab9fe9e590da7b841ec37cb1bed8d6327ea4b/addons/google-analytics/src/register.ts#L8 + * @see https://github.com/storybookjs/storybook/tree/master/addons/google-analytics Google Analytic addon for Storybook + */ +window.STORYBOOK_REACT_GA_OPTIONS = {}; diff --git a/.storybook/mock/sb-dataset.js b/.storybook/mock/sb-dataset.js new file mode 100644 index 000000000..826f344b2 --- /dev/null +++ b/.storybook/mock/sb-dataset.js @@ -0,0 +1,239 @@ +/** + * Dataset used by Storybook stories. + * + * Copied from a NRN instance "window.__CYPRESS_DATA__" and pasted there. + */ +const dataset = { + 'reci9HYsoqd1xScsi': { + '__typename': 'Customer', + 'id': 'reci9HYsoqd1xScsi', + 'ref': 'customer1', + 'label': 'Client 1', + 'availableLanguages': ['en', 'fr'], + 'theme': { + '__typename': 'Theme', + 'id': 'recrcZANU6L73OA9v', + 'primaryColor': '#00536F', + 'primaryColorVariant1': null, + 'onPrimaryColor': null, + 'secondaryColor': '#C90016', + 'secondaryColorVariant1': null, + 'onSecondaryColor': null, + 'backgroundColor': null, + 'onBackgroundColor': null, + 'surfaceColor': null, + 'onSurfaceColor': null, + 'errorColor': null, + 'onErrorColor': null, + 'fonts': null, + 'logo': { + 'id': 'attlGNQqFXvhDYOrR', + 'url': 'https://dl.airtable.com/.attachments/a16bd38af1f3fea3f894dd2a37dbf4bd/baa538c3/apple-touch-icon.png', + 'filename': 'apple-touch-icon.png', + 'size': 11769, + 'type': 'image/png', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/c8528519fa364ebc6c01a35834a06975/1720e171', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/f0f1a95475af253ef157f36faf598c99/2da93bc7', + 'width': 180, + 'height': 180, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/26551c4457369ea157cab82a2ac24368/8ea162cf', + 'width': 3000, + 'height': 3000, + }, + }, + }, + 'logoTitle': 'Awesome-looking Logo', + }, + 'products': [ + { + '__typename': 'Product', + 'id': 'recFSrY2znI6Z8Dbj', + 'ref': 'hellur', + 'title': 'Hellur', + 'images': [ + { + 'id': 'att6JU52f5PlMuiRu', + 'url': 'https://dl.airtable.com/Uvg7ldEEQpqKhR3NKTGt_348s.jpg', + 'filename': '348s.jpg', + 'size': 17866, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/8C4cVNCES89lt6PnFH5W_348s.jpg', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/TdSPVnVQISc0P0EdiiQw_348s.jpg', + 'width': 256, + 'height': 256, + }, + }, + }, + ], + 'imagesTitle': ['Big City'], + 'description': 'Super longue **description**\n\nVous pouvez même [utiliser des liens](https://bluebottlecoffee.com/releases/costa-rica-vista-al-valle-honey)\n', + 'price': 25, + 'status': 'DRAFT', + }, + { + '__typename': 'Product', + 'id': 'recXxSwjiehedMFPf', + 'ref': 'wow', + 'title': 'wow', + 'images': [ + { + 'id': 'attIQzHRvFMgmdytF', + 'url': 'https://dl.airtable.com/.attachments/00f7560832b1500d06d169233424ccd0/c5d69a9e/lXL1TfOBTiikTCW8DPT2', + 'filename': 'lXL1TfOBTiikTCW8DPT2', + 'size': 64273, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/8e2ab049c04e918ccf068eeb2363a24d/88511a89', + 'width': 54, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/6897834d90716b4221e88de8a7e5d17b/beacabda', + 'width': 729, + 'height': 486, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/d10c2858cbdd091db8ea5eb283d15ff5/8a483a20', + 'width': 3000, + 'height': 3000, + }, + }, + }, + ], + 'imagesTitle': ['wow'], + 'description': 'w\n', + 'price': null, + 'status': 'PUBLISHED', + }, + ], + 'serviceLabel': 'NRN demo 1', + 'termsDescription': '\nWe use this "terms" page to showcase what\'s achievable using Markdown + HTML + JSX.
\nYou can edit this through Stacker, see "Go to CMS" nav link.\n
\n\n---\n\nUsing Markdown\n\nHeading 2\nHeading 3\n\nBold\n\nItalic\n\nStrikethrough\n\nLink in new tab\n\n---\n\n

Using HTML

\n\n

Heading 4

\n
Heading 5
\n
Heading 6
\n\n
Text in div
\n\nBold\n\n
\n\nItalic\n\n
\n\nLink (same tab)\n\n
\n\nLink (new tab)\n\n---\n\nUsing JSX (React components)\n\nNote: Only a small subset of components are made available. It works based on a whitelist. You can see the full list here.\n\nComponents from Reactstrap\n\nAn Alert "info"\n\nAn Alert "success"\n\n\n\n
\n
\n\n\n\n
\n
\n\n\n\nCol 1 in a Row (with custom CSS)\n\n\nCol 2 in a Row\n\n\n\nCustom components\n\nHelp tooltips, using Tooltip component:
\nSome complex stuff (click/hover me!)\n\n
\n\nLocalised links, using I18nLink component:
\nLink to homepage, keeping current locale\n\n
\n\nButton to change the current locale, using I18nBtnChangeLocale component:
\n\n\n\n---\n\nNote: All links always open in a new tab with "noopener" to ensure proper security defaults. This only work when used from the app (not from Stacker)\n\nNote: Stacker can preview Markdown but not HTML/JSX. The behaviour between Stacker Markdown preview and real rendering can be different.\n\n\nAs you can see above, using JSX brings quite a few interesting capabilities. But it isn\'t all-powerful though.
\n\nIt\'s not possible to use JavaScript, so forget about using an onClick event for instance. You\'ll need to find workarounds for this kinds of things.
\n\nAlso, it\'s not possible to provide non-scalar props. Forget about providing a component with an object, or array, for instance.
\n\nNevertheless, it brings quite a few possibilities to your app. The secret is to keep things simple, using simple JSX components that rely on few props. The I18nBtnChangeLocale is a great example of that. No props, but changes the language for the whole app anyway, it\'s quite a powerful integration and very simple to use.\n
\n\n', + 'privacyDescription': '{serviceLabel} doesn\'t track any of your personal data.\n\nAnalytic data (such as page views) are being tracked for the whole site, **anonymously**.', + }, + 'recFSrY2znI6Z8Dbj': { + '__typename': 'Product', + 'id': 'recFSrY2znI6Z8Dbj', + 'ref': 'hellur', + 'title': 'Hellur', + 'images': [ + { + 'id': 'att6JU52f5PlMuiRu', + 'url': 'https://dl.airtable.com/Uvg7ldEEQpqKhR3NKTGt_348s.jpg', + 'filename': '348s.jpg', + 'size': 17866, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/8C4cVNCES89lt6PnFH5W_348s.jpg', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/TdSPVnVQISc0P0EdiiQw_348s.jpg', + 'width': 256, + 'height': 256, + }, + }, + }, + ], + 'imagesTitle': ['Big City'], + 'description': 'Super longue **description**\n\nVous pouvez même [utiliser des liens](https://bluebottlecoffee.com/releases/costa-rica-vista-al-valle-honey)\n', + 'price': 25, + 'status': 'DRAFT', + }, + 'recXxSwjiehedMFPf': { + '__typename': 'Product', + 'id': 'recXxSwjiehedMFPf', + 'ref': 'wow', + 'title': 'wow', + 'images': [ + { + 'id': 'attIQzHRvFMgmdytF', + 'url': 'https://dl.airtable.com/.attachments/00f7560832b1500d06d169233424ccd0/c5d69a9e/lXL1TfOBTiikTCW8DPT2', + 'filename': 'lXL1TfOBTiikTCW8DPT2', + 'size': 64273, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/8e2ab049c04e918ccf068eeb2363a24d/88511a89', + 'width': 54, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/6897834d90716b4221e88de8a7e5d17b/beacabda', + 'width': 729, + 'height': 486, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/d10c2858cbdd091db8ea5eb283d15ff5/8a483a20', + 'width': 3000, + 'height': 3000, + }, + }, + }, + ], + 'imagesTitle': ['wow'], + 'description': 'w\n', + 'price': null, + 'status': 'PUBLISHED', + }, + 'recrcZANU6L73OA9v': { + '__typename': 'Theme', + 'id': 'recrcZANU6L73OA9v', + 'primaryColor': 'black', + 'primaryColorVariant1': null, + 'onPrimaryColor': null, + 'secondaryColor': null, + 'secondaryColorVariant1': null, + 'onSecondaryColor': null, + 'backgroundColor': null, + 'onBackgroundColor': null, + 'surfaceColor': null, + 'onSurfaceColor': null, + 'errorColor': null, + 'onErrorColor': null, + 'fonts': null, + 'logo': { + 'id': 'attlGNQqFXvhDYOrR', + 'url': 'https://dl.airtable.com/.attachments/a16bd38af1f3fea3f894dd2a37dbf4bd/baa538c3/apple-touch-icon.png', + 'filename': 'apple-touch-icon.png', + 'size': 11769, + 'type': 'image/png', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/c8528519fa364ebc6c01a35834a06975/1720e171', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/f0f1a95475af253ef157f36faf598c99/2da93bc7', + 'width': 180, + 'height': 180, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/26551c4457369ea157cab82a2ac24368/8ea162cf', + 'width': 3000, + 'height': 3000, + }, + }, + }, + 'logoTitle': 'Awesome-looking Logo', + }, +}; + +export default dataset; diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html new file mode 100644 index 000000000..dbafc706d --- /dev/null +++ b/.storybook/preview-body.html @@ -0,0 +1,122 @@ + + + diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 000000000..8dafd5621 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,6 @@ + + diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 000000000..cd9618299 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,293 @@ +import '@/app/components/MultiversalGlobalExternalStyles'; // Import the same 3rd party libraries global styles as the pages/_app.tsx (for UI consistency) +import MultiversalGlobalStyles from '@/app/components/MultiversalGlobalStyles'; +import '@/common/utils/ignoreNoisyWarningsHacks'; +import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitudeBrowserClient'; +import amplitudeContext from '@/modules/core/amplitude/context/amplitudeContext'; +import customerContext from '@/modules/core/data/contexts/customerContext'; +import datasetContext from '@/modules/core/data/contexts/datasetContext'; +import '@/modules/core/fontAwesome/fontAwesome'; +import i18nContext from '@/modules/core/i18n/contexts/i18nContext'; +import { defaultLocale, getLangFromLocale, supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; +import previewModeContext from '@/modules/core/previewMode/contexts/previewModeContext'; +import quickPreviewContext from '@/modules/core/quickPreview/contexts/quickPreviewContext'; +import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; +import { initCustomerTheme } from '@/modules/core/theming/theme'; +import userConsentContext from '@/modules/core/userConsent/contexts/userConsentContext'; +import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; +import { Amplitude, AmplitudeProvider } from '@amplitude/react-amplitude'; +import { ThemeProvider } from '@emotion/react'; +import '@storybook/addon-console'; // Automatically forwards all logs in the "Actions" panel - See https://github.com/storybookjs/storybook-addon-console +import { withTests } from '@storybook/addon-jest'; +import { addDecorator } from '@storybook/react'; +import { themes } from '@storybook/theming'; +import find from 'lodash.find'; +import React from 'react'; +import { withNextRouter } from 'storybook-addon-next-router'; +import { withPerformance } from 'storybook-addon-performance'; +import dataset from './mock/sb-dataset'; + +// Loads translations from local file cache (Locize) +const i18nTranslations = require('./.sb-translations.cache.json'); + +/** + * Story Global parameters for Storybook. + * + * Parameters are a set of static, named metadata about a story, typically used to control the behavior of Storybook features and addons. + * Parameters are applied at the top-level and act as default values. + * + * XXX They can be overridden per component and per story. + * See https://storybook.js.org/docs/react/writing-stories/parameters#rules-of-parameter-inheritance + * + * @see https://storybook.js.org/docs/react/writing-stories/parameters Parameters documentation + * @see https://github.com/storybookjs/storybook/blob/master/addons/actions/ADVANCED.md#configuration + * @see https://storybook.js.org/docs/react/essentials/backgrounds#configuration + * + * Theme: + * Configure Storybook theme, using dark by default. + * You can customise this behavior per story using parameters. + * Configuring the theme in "manager.js" didn't work out. + * Also, the "Docs" section is better using the "normal" theme, for readability. + * + * @see https://storybook.js.org/docs/react/configure/theming#global-theming Global theming + * @see https://storybook.js.org/docs/react/configure/theming#theming-docs Per story theming (parameter) + * @see https://storybook.js.org/docs/react/configure/theming#create-a-theme-quickstart Creating your own theme + */ +export const parameters = { + actions: { + argTypesRegex: '^on[A-Z].*', + + /** + * Since Controls is built on the same engine as Storybook Docs, it can also show property documentation alongside your controls using the expanded parameter (defaults to false). + * We enable this for all stories by default. + * + * @see https://storybook.js.org/docs/react/essentials/controls#show-full-documentation-for-each-property + */ + expanded: true, + }, + + /** + * Configure stories argTypes for all stories. + * + * @deprecated Should not be used at the moment. See https://github.com/storybookjs/storybook/issues/11697 + * @see https://storybook.js.org/docs/react/essentials/controls + */ + // argTypes: {}, + + /** + * Options. + * Couldn't find centralized documentation about it. + */ + options: { + /** + * @see https://storybook.js.org/docs/react/writing-stories/naming-components-and-hierarchy#sorting-stories + */ + storySort: { + method: 'alphabetical', + order: [ + 'App', // Should be first + 'Next Right Now', // Should be second, if kept around + 'Storybook Examples', // Should be last, if kept around + ], + }, + }, + docs: { + theme: themes.normal, + }, +}; + +/** + * Storybook ships with toolbar items to control the viewport and background the story renders in. + * + * Below, we extend the native toolbar to add a few more options, such as i18n. + * Those global types can then be used in decorators, for both global decorators and story decorators. + * + * @description toolbar.item Can be either an array of plain strings, or a MenuItem. + * See https://storybook.js.org/docs/react/essentials/toolbars-and-globals#advanced-usage + * + * @description toolbar.icon The icon the will be displayed in the top toolbar. + * See https://www.chromatic.com/component?appId=5a375b97f4b14f0020b0cda3&name=Basics%7CIcon&mode=interactive&buildNumber=13899 + * + * @see https://storybook.js.org/docs/react/essentials/toolbars-and-globals + */ +export const globalTypes = { + locale: { + name: 'Locale', + description: 'Global locale for components', + defaultValue: defaultLocale, + toolbar: { + icon: 'globe', // See https://www.chromatic.com/component?appId=5a375b97f4b14f0020b0cda3&name=Basics%7CIcon&mode=interactive&buildNumber=13899 + items: supportedLocales.map(locale => locale.name), + }, + }, +}; + +/** + * Allow to use Next.js Router in Storybook stories. + * + * If you need to customise a component/story, then you should see https://github.com/lifeiscontent/storybook-addon-next-router#as-a-decorator-in-a-story + * You'll need to specify the Router behavior per-story if the below default config doesn't suit you. + * + * @see https://github.com/lifeiscontent/storybook-addon-next-router#usage-in-previewjs + */ +addDecorator( + withNextRouter({ + path: '/', // defaults to `/` + asPath: '/', // defaults to `/` + query: {}, // defaults to `{}` + // @formatter:off Disables odd WebStorm formatting for next line + push() {}, // defaults to using addon actions integration, can override any method in the router + // @formatter:on + }), +); + +/** + * Decorators in .storybook/preview.js are useful to mock Stories. + * + * Like parameters, decorators can be defined globally, at the component level and for a single story (as we’ve seen). + * All decorators, defined at all levels that apply to a story will run whenever that story is rendered, in the order: + * - Global decorators, in the order they are defined + * - Component decorators, in the order they are defined + * - Story decorators, in the order they are defined. + * + * @see https://storybook.js.org/docs/react/writing-stories/decorators#context-for-mocking + * @see https://storybook.js.org/docs/react/writing-stories/decorators#global-decorators + */ +export const decorators = [ + /** + * Mock variables used to initialize all stories. + * + * Mocking those ensures the components relying on them will work as expected. + * Basically, plays a similar role to _app and appBootstrap components (MultiversalAppBootstrap, etc.) + * + * About Amplitude analytics: + * - We don't want to track analytics using Amplitude. + * - All analytics is disabled when running a component through Storybook preview. + * + * About Google analytics, see ".storybook/main.js" documentation. + * + * @see https://storybook.js.org/docs/react/essentials/toolbars-and-globals#create-a-decorator Context and globals + */ + (Story, context) => { + // console.log('context', context) // Prints useful information about the Story's configuration + // Configure i18n. In Storybook, the locale can be set from the top Toolbar. + const locale = context?.globals?.locale || defaultLocale; + const lang = getLangFromLocale(locale); + + // Applies i18next configuration with Locize backend + // Extra features like saveMissing, etc. will be disabled in production because Storybook doesn't have access to NEXT_PUBLIC_* environment variables there + // Although, they are configured in the same way as the Next.js app during development mode + i18nextLocize(lang, i18nTranslations); + + const customer = find(dataset, { __typename: 'Customer' }); + const customerTheme = initCustomerTheme(customer); + const customerRef = 'storybook'; // Fake customer ref + const amplitudeApiKey = ''; // Use invalid amplitude tracking key to force disable all amplitude analytics + const userConsent = { + isUserOptedOutOfAnalytics: true, // Disables all amplitude analytics tracking (even if a proper api key was being used) + hasUserGivenAnyCookieConsent: false, + }; + const userId = 'storybook'; // Fake id (would avoid user tracking even if correct api key was being used) + const amplitudeInstance = getAmplitudeInstance({ + customerRef, + iframeReferrer: null, + isInIframe: false, + lang, + locale, + userId, + userConsent: userConsent, + }); + + // Configure all providers, similarly to what being done by MultiversalAppBootstrap and BrowserPageBootstrap + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, +]; + +/** + * Enables storybook-addon-performance for all stories by default. + * + * @see https://github.com/atlassian-labs/storybook-addon-performance#installation + */ +addDecorator(withPerformance); + +/** + * Configure Jest Storybook for all stories. + * + * Each story must define which test it's associated to, to show the associated tests results in the preview. + * See https://github.com/storybookjs/storybook/tree/master/addons/jest#usage + * + * @see https://github.com/storybookjs/storybook/tree/master/addons/jest + */ +try { + let testResults; + testResults = require('../.jest-test-results.json'); + + addDecorator( + withTests({ + results: testResults, + }), + ); +} catch (e) { + console.log(`Couldn't find ../.jest-test-results.json, Jest tests might not work properly.`); +} + diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js new file mode 100644 index 000000000..ec60ae64b --- /dev/null +++ b/.storybook/webpack.config.js @@ -0,0 +1,36 @@ +/** + * The doc doesn't really mention using webpack.config.js, but .storybook/main.js instead. + * + * Nevertheless, configuring the webpack.config.js seems to work fine. + * + * @param config + * @param mode + * @return {Promise<*>} + * @see https://storybook.js.org/docs/react/configure/webpack + * @see https://storybook.js.org/docs/react/configure/webpack#using-your-existing-config + */ +module.exports = async ({ + config, + mode, +}) => { + /** + * Fixes npm packages that depend on `fs` module, etc. + * + * E.g: "winston" would fail to load without this, because it relies on fs, which isn't available during browser build. + * + * @see https://github.com/storybookjs/storybook/issues/4082#issuecomment-495370896 + */ + config.node = { + fs: 'empty', + tls: 'empty', + net: 'empty', + module: 'empty', + console: true, + }; + + // XXX See https://github.com/vercel/next.js/blob/canary/examples/with-sentry-simple/next.config.js + // Because StoryBook only compiles for client and has no server runtime, we must replace backend-related libs like @sentry/node to their browser counterpart + config.resolve.alias['@sentry/node'] = '@sentry/browser'; + + return config; +}; diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 000000000..4ace5e5de --- /dev/null +++ b/.vercelignore @@ -0,0 +1,23 @@ +# Optimize speed/size of deployments by not uploading those files +.idea/ +.github/ +node_modules/ +cypress/ +docs/ +coverage/ +scripts/ +svg-to-react/ +.codeclimate.yml +.editorconfig +.env* +.graphqlconfig +.hybrid-cache-*.cache +.jest-test-results.json +*.md +LICENSE +schema.graphql +yarn-error.log + +# Avoid tests being deployed as Vercel Serverless Functions (they would increase bundle size, and count towards Vercel's limits) +src/pages/api/*.test.ts +src/pages/api/**/*.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a108a2b6..47486ff51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -[GO TO CHANGELOG](https://unlyed.github.io/next-right-now/CHANGELOG) +[GO TO CHANGELOG](https://unlyed.github.io/next-right-now/changelog) diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..12d89e60c --- /dev/null +++ b/babel.config.js @@ -0,0 +1,24 @@ +/** + * Babel configuration for Next.js + * + * The official documentation uses a ".babelrc" file, but we prefer using "babel.config.js" for better documentation support. + * + * @see https://nextjs.org/docs/advanced-features/customizing-babel-config Official doc reference v10 + * @see https://github.com/vercel/next.js/blob/canary/packages/next/build/babel/preset.ts You can take a look at this file to learn about the presets included by next/babel. + * @see https://emotion.sh/docs/css-prop#babel-preset Configuring Emotion 11 + * @example https://github.com/vercel/next.js/tree/canary/examples/with-custom-babel-config Next.js official example of customizing Babel + */ +module.exports = { + presets: [ + [ + "next/babel", + { + "preset-react": { + "runtime": "automatic", + "importSource": "@emotion/react" + } + } + ] + ], + plugins: ["@emotion/babel-plugin"], +}; diff --git a/cypress/README.md b/cypress/README.md new file mode 100644 index 000000000..693d8bf0c --- /dev/null +++ b/cypress/README.md @@ -0,0 +1,69 @@ +Cypress End-to-end tests suite (E2E) +=== + +This `cypress` folder is used for E2E testing. + +Read more about [the folder structure](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Folder-Structure) + +- `_examples`: Tests created by Cypress during initial install (initially located in `integration/examples`), they've been moved to this folder so that they don't run but can still be used as learning resource +- `integration`: The folder where tests must be written +- `screenshot`/`videos`: Will contain assets when tests fail, useful during development. When tests are executed by Github Actions, you'll find those assets under the "Artifacts" section. (e.g: https://github.com/UnlyEd/next-right-now/runs/862302266) +- `fixtures`: Fixtures are used as external pieces of static data that can be used by your tests. (We've kept the fixtures installed during the Cypress initial install) + +## Cypress config files + +The files `cypress/config-*` are used for different purposes. + +- `config-customer-ci-cd.json`: This file is a mock config file used by CI/CD GitHub Actions by the workflows `deploy-vercel-production.yml` and `deploy-vercel-staging.yml`. + The `baseUrl` is a fake value (required by Cypress, but not used) which is replaced at runtime by the real `baseUrl` which is a dynamic Vercel deployment url. +- `config-development.json`: This file is only used when running `yarn e2e:run` and `yarn e2e:open` locally. + It uses `baseUrl=http://localhost:8888` which is where our local server is running. It's only meant for local testing +- `config-$CUSTOMER_REF.json`: This file is only used when running `yarn deploy:$CUSTOMER_REF` locally. _It is not used by CI/CD workflows._ + +## Tests ordering + +[Sanity Checks](./integration/app/_sanity/README.md) are executed first. Then, tests are executed by their folder/file alphabetical order by default. + +## Resources about how to write tests better + +- [[MUST WATCH!] Best Practices by the Author (2018) - 27mn](https://docs.cypress.io/examples/examples/tutorials.html#Best-Practices) +- [Organize tests by type of devices (mobile/desktop)](https://docs.cypress.io/api/commands/viewport.html#Width-Height) +- [Run tests on multiple subdomains](https://docs.cypress.io/faq/questions/using-cypress-faq.html#Can-I-run-the-same-tests-on-multiple-subdomains) +- [Detect if Cypress is running](https://docs.cypress.io/faq/questions/using-cypress-faq.html#Is-there-any-way-to-detect-if-my-app-is-running-under-Cypress) +- [Can my tests interact with Redux / Vuex data store? (AKA "Dynamic testing")](https://docs.cypress.io/faq/questions/using-cypress-faq.html#Can-my-tests-interact-with-Redux-Vuex-data-store) +- [Check a custom property from the `window` object](https://docs.cypress.io/api/commands/window.html#Check-a-custom-property) +- [Dynamic tests](https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/fundamentals__dynamic-tests) +- [Filters and data-driven tests](https://docs.cypress.io/examples/examples/tutorials.html#7-Filters-and-data-driven-tests) +- [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app/blob/develop/cypress/tests/ui/transaction-feeds.spec.ts) + +## Officiel Cypress recommandations + +> We see organizations starting with Cypress by placing end-to-end tests in a separate repo. +> This is a great practice that allows someone on the team to prototype a few tests and evaluate Cypress within minutes. +> As the time passes and the number of tests grows, we strongly suggest moving end-to-end tests to live right alongside your front end code. +> +> This brings many benefits: +> - engages developers in writing end-to-end tests sooner +> - keeps tests and the features they test in sync +> - tests can be run every time the code changes +> - allows code sharing between the application code and the tests (like selectors) + +_[Source](https://docs.cypress.io/faq/questions/using-cypress-faq.html#What-are-your-best-practices-for-organizing-tests)_ + +[Cypress releases "Real World App" (RWA) - Blog post](https://www.cypress.io/blog/2020/06/11/introducing-the-cypress-real-world-app/) + +## Module path alias mapping + +We use module alias path mappings, to avoid using relative paths (e.g: `../../src/common`) but absolute paths (AKA "module paths") instead (e.g: `@/common`). + +Although it's simpler to use, it's harder to configure because it affects several configuration files: +- The paths mapping in `tsconfig.json:compilerOptions.paths` must match those in `../tsconfig.json:compilerOptions.paths` +- They must also match those in `jsconfig.json` file, which is necessary for WebStorm to understand them for .js files. + +If the module path mappings aren't properly set everywhere, it won't work. + +> You can still use relative paths. + +Reference: +- See [Next.js "Module path aliases" documentation](https://nextjs.org/docs/advanced-features/module-path-aliases) +- See [WebStorm issue](https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003361399/comments/360002636080) diff --git a/cypress/config-customer-ci-cd.json b/cypress/config-customer-ci-cd.json new file mode 100644 index 000000000..2f014ed1f --- /dev/null +++ b/cypress/config-customer-ci-cd.json @@ -0,0 +1,10 @@ +{ + "//": "This file is used by CI/CD GitHub Actions (deploy-vercel-staging/production) and not meant to be used locally", + "baseUrl": "https://nrn-customer.vercel.app", + "projectId": "4dvdog", + "screenshotsFolder": "cypress/screenshots/customer", + "videosFolder": "cypress/videos/customer", + "env": {}, + "ignoreTestFiles": "*.md", + "experimentalInteractiveRunEvents": true +} diff --git a/cypress/config-customer1.json b/cypress/config-customer1.json index 76a1e8ac6..6b517ab73 100644 --- a/cypress/config-customer1.json +++ b/cypress/config-customer1.json @@ -1,7 +1,9 @@ { - "baseUrl": "https://nrn-v1-ssr-mst-aptd-gcms-lcz-sty-c1.now.sh/", + "baseUrl": "https://nrn-v2-mst-aptd-gcms-lcz-sty-c1.vercel.app", "projectId": "4dvdog", "screenshotsFolder": "cypress/screenshots/customer1", "videosFolder": "cypress/videos/customer1", - "env": {} + "env": {}, + "ignoreTestFiles": "*.md", + "experimentalInteractiveRunEvents": true } diff --git a/cypress/config-customer2.json b/cypress/config-customer2.json index 069f85bb9..06a64a7e8 100644 --- a/cypress/config-customer2.json +++ b/cypress/config-customer2.json @@ -1,7 +1,9 @@ { - "baseUrl": "https://nrn-v1-ssr-mst-aptd-gcms-lcz-sty-c2.now.sh/", + "baseUrl": "https://nrn-v2-mst-aptd-gcms-lcz-sty-c2.vercel.app", "projectId": "4dvdog", "screenshotsFolder": "cypress/screenshots/customer2", "videosFolder": "cypress/videos/customer2", - "env": {} + "env": {}, + "ignoreTestFiles": "*.md", + "experimentalInteractiveRunEvents": true } diff --git a/cypress/config-development.json b/cypress/config-development.json index b265a4d74..294c735d9 100644 --- a/cypress/config-development.json +++ b/cypress/config-development.json @@ -3,5 +3,7 @@ "projectId": "4dvdog", "screenshotsFolder": "cypress/screenshots/development", "videosFolder": "cypress/videos/development", - "env": {} + "env": {}, + "ignoreTestFiles": "*.md", + "experimentalInteractiveRunEvents": true } diff --git a/cypress/config-storybook.json b/cypress/config-storybook.json new file mode 100644 index 000000000..8a9b72a79 --- /dev/null +++ b/cypress/config-storybook.json @@ -0,0 +1,11 @@ +{ + "//": "This file is used by CI/CD GitHub Actions (deploy-vercel-storybook) and not meant to be used locally", + "baseUrl": "http://localhost:6006", + "projectId": "4dvdog", + "screenshotsFolder": "cypress/screenshots/storybook", + "videosFolder": "cypress/videos/storybook", + "integrationFolder": "cypress/integration-storybook", + "env": {}, + "ignoreTestFiles": "*.md", + "experimentalInteractiveRunEvents": true +} diff --git a/cypress/global.d.ts b/cypress/global.d.ts new file mode 100644 index 000000000..6d4cbd5a6 --- /dev/null +++ b/cypress/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/cypress/integration-storybook/app/_sanity/1-domain.ts b/cypress/integration-storybook/app/_sanity/1-domain.ts new file mode 100644 index 000000000..51947c49b --- /dev/null +++ b/cypress/integration-storybook/app/_sanity/1-domain.ts @@ -0,0 +1,22 @@ +const baseUrl = Cypress.config().baseUrl; + +describe('Sanity checks > Domain', () => { + /* + * Visits the home page before any test + */ + before(() => { + cy.visit('/'); + }); + + it(`should be running on the domain "${baseUrl}"`, () => { + cy.url().then((url) => { + cy.log(`Expected to be running on:`); + cy.log(baseUrl); + cy.log(`Actually running at:`); + cy.log(url); + cy.url({ timeout: 300000 }).should('contains', baseUrl); // Wait at least 5 minute before timing out + }); + }); +}); + +export {}; diff --git a/cypress/integration-storybook/app/_sanity/README.md b/cypress/integration-storybook/app/_sanity/README.md new file mode 100644 index 000000000..77784d7b8 --- /dev/null +++ b/cypress/integration-storybook/app/_sanity/README.md @@ -0,0 +1,12 @@ +Sanity checks +=== + +The purpose of this folder is to contain sanity tests that should be executed before the others. +We use it to make sure we're on the right domain before running our tests, because it can happen E2E test start before Vercel if fully configured, and thus the tests would run on the wrong page. +To avoid this to happen, we first wait up to 5mn before timing out when checking the domain name. This ensures other tests don't run on an unexpected domain and fail, which would be misleading. + +This way, the first tests ensure we're running in the expected environment and will fail if we don't, which helps understand the actual issue. + +Read more [Ability to run spec files in a specific order](https://github.com/cypress-io/cypress/issues/390) + +Work around to order your tests: [Cypress - How can I run test files in order](https://stackoverflow.com/questions/58936891/cypress-how-can-i-run-test-files-in-order/59690611#59690611) diff --git a/cypress/integration-storybook/app/stories/dataDisplay/btn.ts b/cypress/integration-storybook/app/stories/dataDisplay/btn.ts new file mode 100644 index 000000000..0aca78e43 --- /dev/null +++ b/cypress/integration-storybook/app/stories/dataDisplay/btn.ts @@ -0,0 +1,30 @@ +describe('Btn story', () => { + /** + * Visits the story page before any test. + */ + before(() => { + cy.visit('/?path=/story/next-right-now-welcome-to-nrn--page'); + }); + + /** + * XXX The role of this test is to make sure to detect regressions affecting the whole Storybook site. + * We don't intend to test all components here, we only want to be warned if we ever break Storybook. + */ + it('should have a "Data display" menu on the left navigation panel', () => { + cy.get('#next-right-now-data-display').should('have.length', 1).should('have.text', 'Data display').click(); + }); + + it('should have a "Data display" > "Btn" story', () => { + cy.get('#next-right-now-data-display-btn').should('have.length', 1).should('have.text', 'Btn').click(); + }); + + it('should have a writable "#text" control property', () => { + cy.get('#text').should('have.length', 1).should('have.text', 'Hello').type(' Cypress!'); + }); + + it('should have changed the Btn text to "Hello Cypress!', () => { + cy.findIframe('iframe#storybook-preview-iframe').find('#root button').should('have.text', 'Hello Cypress!') + }); +}); + +export {}; diff --git a/cypress/integration/app/_sanity/1-domain.ts b/cypress/integration/app/_sanity/1-domain.ts new file mode 100644 index 000000000..f2933e704 --- /dev/null +++ b/cypress/integration/app/_sanity/1-domain.ts @@ -0,0 +1,26 @@ +const baseUrl = Cypress.config().baseUrl; + +describe('Sanity checks > Domain', { + // retries: { + // runMode: 2, // Allows 2 retries (for a total of 3 attempts) to reduce the probability of failing the whole tests suite because Vercel hasn't finished to deploy yet (which makes Cypress fail by trying to test the Vercel "waiting page", instead of our app) + // } +}, () => { + /* + * Visits the home page before any test + */ + before(() => { + cy.visit('/en'); + }); + + it(`should be running on the domain "${baseUrl}"`, () => { + cy.url().then((url) => { + cy.log(`Expected to be running on:`); + cy.log(baseUrl); + cy.log(`Actually running at:`); + cy.log(url); + cy.url({ timeout: 300000 }).should('contains', baseUrl); // Wait at least 5 minute before timing out + }); + }); +}); + +export {}; diff --git a/cypress/integration/app/_sanity/2-customer.ts b/cypress/integration/app/_sanity/2-customer.ts new file mode 100644 index 000000000..5cfb7022f --- /dev/null +++ b/cypress/integration/app/_sanity/2-customer.ts @@ -0,0 +1,31 @@ +import { Customer } from '@/modules/core/data/types/Customer'; +import { CYPRESS_WINDOW_NS } from '@/modules/core/testing/cypress'; + +describe('Sanity checks > Browser data', () => { + /** + * Visits the home page before any test. + */ + before(() => { + cy.visit('/en'); + }); + + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + + it(`should have "window.${CYPRESS_WINDOW_NS}.dataset" defined`, () => { + cy.get('@dataset').then((dataset) => { + assert.isDefined(dataset); + expect(Object.keys(dataset).length).to.be.greaterThan(0); + }); + }); + + it(`should have "window.${CYPRESS_WINDOW_NS}.customer" defined`, () => { + cy.get('@customer').then((customer: Customer) => { + assert.isDefined(customer.label); + }); + }); +}); diff --git a/cypress/integration/app/_sanity/README.md b/cypress/integration/app/_sanity/README.md new file mode 100644 index 000000000..77784d7b8 --- /dev/null +++ b/cypress/integration/app/_sanity/README.md @@ -0,0 +1,12 @@ +Sanity checks +=== + +The purpose of this folder is to contain sanity tests that should be executed before the others. +We use it to make sure we're on the right domain before running our tests, because it can happen E2E test start before Vercel if fully configured, and thus the tests would run on the wrong page. +To avoid this to happen, we first wait up to 5mn before timing out when checking the domain name. This ensures other tests don't run on an unexpected domain and fail, which would be misleading. + +This way, the first tests ensure we're running in the expected environment and will fail if we don't, which helps understand the actual issue. + +Read more [Ability to run spec files in a specific order](https://github.com/cypress-io/cypress/issues/390) + +Work around to order your tests: [Cypress - How can I run test files in order](https://stackoverflow.com/questions/58936891/cypress-how-can-i-run-test-files-in-order/59690611#59690611) diff --git a/cypress/integration/app/common/footer.ts b/cypress/integration/app/common/footer.ts new file mode 100644 index 000000000..598439487 --- /dev/null +++ b/cypress/integration/app/common/footer.ts @@ -0,0 +1,49 @@ +import { Customer } from '@/modules/core/data/types/Customer'; + +const baseUrl = Cypress.config().baseUrl; + +describe('Common > Footer section', () => { + /** + * Visits the home page before any test. + */ + before(() => { + cy.visit('/en'); + }); + + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + + it('should have the Unly logo in the footer', () => { + cy.get('#footer-logo-unly-brand').should('have.length', 1); + }); + + // Disabled because footer logo can be removed by anyone on the public demo and this makes tests crash for no valid reason (annoying) + // it('should have the customer logo in the footer', () => { + // cy.get('#footer-logo').should('have.length', 1); + // }); + + it('should display the i18n button to change language', () => { + cy.get('@customer').then((customer: Customer) => { + const availableLanguagesCount = 2; + cy.log(`Available language(s): ${availableLanguagesCount}`); + + if (availableLanguagesCount > 1) { + it('should have a button to change the language which changes the language upon click', () => { + cy.get('#footer-btn-change-locale').should('have.length', 1).click({ force: true }); + cy.url().should('eq', `${baseUrl}/fr`); + }); + } else { + it('should not have a button to change the language', () => { + cy.get('#footer-btn-change-locale').should('not.have.length', 1); + }); + } + }); + }); + +}); + +export {}; diff --git a/cypress/integration/app/common/nav.ts b/cypress/integration/app/common/nav.ts new file mode 100644 index 000000000..abe668b61 --- /dev/null +++ b/cypress/integration/app/common/nav.ts @@ -0,0 +1,35 @@ +import { Customer } from '@/modules/core/data/types/Customer'; + +const baseUrl = Cypress.config().baseUrl; + +describe('Common > Nav section', () => { + /** + * Visits the home page before any test. + */ + before(() => { + cy.visit('/en'); + }); + + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + + it('should have 3 links in the navigation bar', () => { + cy.get('#nav .navbar-nav > .nav-item').should('have.length', 5); + }); + + it('should have a link in the navbar that redirects to the home page', () => { + cy.get('@customer').then((customer: Customer) => { + const isPageInEnglish = true; + cy.get('#nav-link-home') + .should('have.text', isPageInEnglish ? 'Home' : 'Accueil') + .click(); + cy.url({ timeout: 10000 }).should('eq', `${baseUrl}/${isPageInEnglish ? 'en' : 'fr'}`); + }); + }); +}); + +export {}; diff --git a/cypress/integration/app/pages/index.js b/cypress/integration/app/pages/index.js deleted file mode 100644 index b20f25b23..000000000 --- a/cypress/integration/app/pages/index.js +++ /dev/null @@ -1,36 +0,0 @@ -const baseUrl = Cypress.config().baseUrl; - -describe('Index page', () => { - /* - * Visits the page before each test - */ - beforeEach(() => { - cy.visit('/'); - }); - - /** - * Footer section - */ - it('should have the Unly logo in the footer', () => { - cy.get('#footer-logo-unly-brand').should('have.length', 1); - }); - - it('should have the customer logo in the footer', () => { - cy.get('#footer-logo-organisation-brand').should('have.length', 1); - }); - - /** - * Navbar section - */ - it('should have 5 links in the navigation bar', () => { - cy.get('#nav a.nav-link').should('have.length', 5); - }); - - it('should have a link in the navbar that redirects to the examples page', () => { - cy.url().should('eq', `${baseUrl}/`); - cy.get('#nav-link-examples') - .should('have.text', 'Examples') - .click(); - cy.url().should('eq', `${baseUrl}/examples`); - }); -}); diff --git a/cypress/integration/app/pages/index.ts b/cypress/integration/app/pages/index.ts new file mode 100644 index 000000000..df098f959 --- /dev/null +++ b/cypress/integration/app/pages/index.ts @@ -0,0 +1,23 @@ +const baseUrl = Cypress.config().baseUrl; + +describe('Index page', () => { + /** + * Visits the home page before any test. + */ + before(() => { + cy.visit('/en'); + }); + + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + + it('should display a main title', () => { + cy.get('h1').should('have.length', 1).should('have.text', 'Next Right Now Demo'); + }); +}); + +export {}; diff --git a/cypress/jsconfig.json b/cypress/jsconfig.json new file mode 100644 index 000000000..84c874b98 --- /dev/null +++ b/cypress/jsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/app/*": [ + "../src/app/*" + ], + "@/common/*": [ + "../src/common/*" + ], + "@/components/*": [ + "../src/common/components/*" + ], + "@/utils/*": [ + "../src/common/utils/*" + ], + "@/layouts/*": [ + "../src/layouts/*" + ], + "@/modules/*": [ + "../src/modules/*" + ], + "@/pages/*": [ + "../src/pages/*" + ] + } + } +} diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index fd170fba6..7d9ae3d2c 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -11,7 +11,10 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) +/// + module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config + return config; } diff --git a/cypress/support/commands.d.ts b/cypress/support/commands.d.ts new file mode 100644 index 000000000..66c0bfe79 --- /dev/null +++ b/cypress/support/commands.d.ts @@ -0,0 +1,6 @@ +declare namespace Cypress { + interface cy extends Chainable { + prepareDOMAliases: () => Chainable; + findIframe: (iframeSelector: string) => Chainable; + } +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ca4d256f3..ba42fe337 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -7,19 +7,49 @@ // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +import { CYPRESS_WINDOW_NS } from '@/modules/core/testing/cypress'; + +/** + * Prepare DOM aliases by fetching the customer data from the browser window and aliasing them for later use. + * + * @example cy.prepareDOMAliases(); + */ +Cypress.Commands.add('prepareDOMAliases', () => { + return cy.window().then((window) => { + cy.get('.page-wrapper').then(() => { // Wait for the DOM element to be created by Next.js before trying to read any dynamic data from the "window" object + cy.log(`window[${CYPRESS_WINDOW_NS}]`, window[CYPRESS_WINDOW_NS]); + + const { + customer, + dataset, + } = window[CYPRESS_WINDOW_NS]; + + // Use aliases to make our variables reusable across tests - See https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Sharing-Context + cy.wrap(customer).as('customer'); + cy.wrap(dataset).as('dataset'); + }); + }); +}); + +/** + * Finds an iframe and returns it "body" HTML element. + * + * XXX Alternatively, look for the cypress-iframe NPM plugin if you need more iframe-related features! See https://www.npmjs.com/package/cypress-iframe + * + * @example cy.findIframe(); + * @see https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/ + */ +Cypress.Commands.add('findIframe', (iframeSelector) => { + // get the iframe > document > body + // and retry until the body element is not empty + cy.log('findIframe') + + return cy + .get(iframeSelector, { log: false }) + .its('0.contentDocument.body', { log: false }).should('not.be.empty') + // wraps "body" DOM element to allow + // chaining more Cypress commands, like ".find(...)" + // https://on.cypress.io/wrap + .then((body) => cy.wrap(body, { log: false })) +}) diff --git a/cypress/support/index.js b/cypress/support/index.js index d68db96df..7e5e802e9 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -13,8 +13,13 @@ // https://on.cypress.io/configuration // *********************************************************** -// Import commands.js using ES2015 syntax: -import './commands' +// See https://dev.to/cuichenli/how-do-i-setup-my-nextjs-development-environment-2kao +import './commands'; -// Alternatively you can use CommonJS syntax: -// require('./commands') +// See https://docs.cypress.io/api/events/catalog-of-events.html#Uncaught-Exceptions +Cypress.on('uncaught:exception', (err, runnable) => { + // returning false here prevents Cypress from + // failing the test + console.error('Application error caught:', err, runnable); + return false; +}); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 000000000..509f3f67a --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,38 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./**/*.ts*" + ], + "exclude": [], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/app/*": [ + "../src/app/*" + ], + "@/common/*": [ + "../src/common/*" + ], + "@/components/*": [ + "../src/common/components/*" + ], + "@/utils/*": [ + "../src/common/utils/*" + ], + "@/layouts/*": [ + "../src/layouts/*" + ], + "@/modules/*": [ + "../src/modules/*" + ], + "@/pages/*": [ + "../src/pages/*" + ] + }, + "types": [ + "cypress" + ], + "sourceMap": false, + "isolatedModules": true + } +} diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..45c150536 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +_site +.sass-cache +.jekyll-metadata diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 000000000..eeaa17753 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,25 @@ +--- +layout: default +nav_exclude: true +--- + + + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 000000000..8ecd84cf7 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,27 @@ +# XXX This file is only used when working on your local machine, in development +# Run `yarn doc:start` to start the development server +# Read ./CONTRIBUTING.md for further instructions + +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem 'just-the-docs', '0.3.0' # XXX Our Jekyll theme - See https://pmarsceill.github.io/just-the-docs/ + gem "github-pages" # XXX Necessary to reproduce the behaviour of GitHub Pages - When this is loaded, "jekyll" must not be bundled because it's included within + gem 'jemoji' # XXX GitHub-flavored Emoji plugin for Jekyll - See https://github.com/jekyll/jemoji +end + +# ------- WINDOWS SUPPORT --------- + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +# and associated library. +install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do + gem "tzinfo", "~> 1.2" + gem "tzinfo-data" +end + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform? + diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 000000000..402d6fb20 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,261 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.0.2.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.11.1) + colorator (1.1.0) + commonmarker (0.17.13) + ruby-enum (~> 0.5) + concurrent-ruby (1.1.6) + dnsruby (1.61.3) + addressable (~> 2.5) + em-websocket (0.5.1) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0.6.0) + ethon (0.12.0) + ffi (>= 1.3.0) + eventmachine (1.2.7) + execjs (2.7.0) + faraday (1.0.0) + multipart-post (>= 1.2, < 3) + ffi (1.12.2) + forwardable-extended (2.6.0) + gemoji (3.0.1) + github-pages (204) + github-pages-health-check (= 1.16.1) + jekyll (= 3.8.5) + jekyll-avatar (= 0.7.0) + jekyll-coffeescript (= 1.1.1) + jekyll-commonmark-ghpages (= 0.1.6) + jekyll-default-layout (= 0.1.4) + jekyll-feed (= 0.13.0) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.13.0) + jekyll-mentions (= 1.5.1) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.15.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.1) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.6.1) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.1.1) + jekyll-theme-cayman (= 0.1.1) + jekyll-theme-dinky (= 0.1.1) + jekyll-theme-hacker (= 0.1.1) + jekyll-theme-leap-day (= 0.1.1) + jekyll-theme-merlot (= 0.1.1) + jekyll-theme-midnight (= 0.1.1) + jekyll-theme-minimal (= 0.1.1) + jekyll-theme-modernist (= 0.1.1) + jekyll-theme-primer (= 0.5.4) + jekyll-theme-slate (= 0.1.1) + jekyll-theme-tactile (= 0.1.1) + jekyll-theme-time-machine (= 0.1.1) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.11.1) + kramdown (= 1.17.0) + liquid (= 4.0.3) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.10.4, < 2.0) + rouge (= 3.13.0) + terminal-table (~> 1.4) + github-pages-health-check (1.16.1) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (~> 4.0) + public_suffix (~> 3.0) + typhoeus (~> 1.3) + html-pipeline (2.12.3) + activesupport (>= 2) + nokogiri (>= 1.4) + http_parser.rb (0.6.0) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jekyll (3.8.5) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 0.7) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 2.0) + kramdown (~> 1.14) + liquid (~> 4.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (>= 1.7, < 4) + safe_yaml (~> 1.0) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.1.1) + coffee-script (~> 2.2) + coffee-script-source (~> 1.11.1) + jekyll-commonmark (1.3.1) + commonmarker (~> 0.14) + jekyll (>= 3.7, < 5.0) + jekyll-commonmark-ghpages (0.1.6) + commonmarker (~> 0.17.6) + jekyll-commonmark (~> 1.2) + rouge (>= 2.0, < 4.0) + jekyll-default-layout (0.1.4) + jekyll (~> 3.0) + jekyll-feed (0.13.0) + jekyll (>= 3.7, < 5.0) + jekyll-gist (1.5.0) + octokit (~> 4.2) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) + octokit (~> 4.0, != 4.4.0) + jekyll-mentions (1.5.1) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-paginate (1.1.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.15.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.1) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + rubyzip (>= 1.3.0) + jekyll-sass-converter (1.5.2) + sass (~> 3.4) + jekyll-seo-tag (2.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-cayman (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.5.4) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + jemoji (0.11.1) + gemoji (~> 3.0) + html-pipeline (~> 2.2) + jekyll (>= 3.0, < 5.0) + just-the-docs (0.3.0) + jekyll (>= 3.8.5) + jekyll-seo-tag (~> 2.0) + rake (>= 12.3.1, < 13.1.0) + kramdown (1.17.0) + liquid (4.0.3) + listen (3.2.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.3.6) + mini_portile2 (2.4.0) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.14.0) + multipart-post (2.1.1) + nokogiri (1.10.9) + mini_portile2 (~> 2.4.0) + octokit (4.18.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (3.1.1) + rake (13.0.1) + rb-fsevent (0.10.3) + rb-inotify (0.10.1) + ffi (~> 1.0) + rouge (3.13.0) + ruby-enum (0.7.2) + i18n + rubyzip (2.3.0) + safe_yaml (1.0.5) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + thread_safe (0.3.6) + typhoeus (1.3.1) + ethon (>= 0.9.0) + tzinfo (1.2.6) + thread_safe (~> 0.1) + tzinfo-data (1.2019.3) + tzinfo (>= 1.0.0) + unicode-display_width (1.7.0) + wdm (0.1.1) + zeitwerk (2.3.0) + +PLATFORMS + ruby + +DEPENDENCIES + github-pages + jemoji + just-the-docs (= 0.3.0) + tzinfo (~> 1.2) + tzinfo-data + wdm (~> 0.1.0) + +BUNDLED WITH + 1.17.3 diff --git a/docs/_config-development.yml b/docs/_config-development.yml new file mode 100644 index 000000000..70456bf23 --- /dev/null +++ b/docs/_config-development.yml @@ -0,0 +1,48 @@ +# XXX This file is only used when working on your local machine, in development +# Checkout https://pages.github.com/versions/ for the list of Github Pages built-in plugins + +# XXX --------- Specific to local config ------------ +theme: 'just-the-docs' # XXX Our Jekyll theme - See https://pmarsceill.github.io/just-the-docs/ + +# XXX --------- Common to all configs (local + GHP) -------------- + +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'bundle exec jekyll serve'. If you change this file, please restart the server process. + +# Site settings +# These are used to personalize your new site. If you look in the HTML files, +# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. +# You can create any custom variable you would like, and they will be accessible +# in the templates via {{ site.myvariable }}. +title: Next Right Now +description: >- # this means to ignore newlines until "baseurl:" + Flexible production-grade boilerplate with Next.js 9, Zeit and TypeScript. + Includes multiple opt-in presets using GraphQL, Analytics, CSS-in-JS, Monitoring, End-to-end testing, + Internationalization, CI/CD and B2B multiple single-tenants (monorepo) support +#baseurl: "" # the subpath of your site, e.g. /blog +#url: "" # the base hostname & protocol for your site, e.g. http://example.com XXX Auto-resolved by GitHub + +# Aux links for the upper right navigation +aux_links: + "Home": "/" + "Github": "https://github.com/UnlyEd/next-right-now" + "About us": "https://github.com/UnlyEd/Unly" + +footer_content: "Copyright © 2020 Unly. MIT license." +#color_scheme: "dark" + +plugins: + - jekyll-sitemap + - jemoji + +# Exclude from processing. +# The following items will not be processed, by default. Create a custom list +# to override the default setting. +# exclude: +# - Gemfile +# - Gemfile.lock +# - node_modules +# - vendor/bundle/ +# - vendor/cache/ +# - vendor/gems/ +# - vendor/ruby/ diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..1b0dc84f3 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,49 @@ +# XXX This file is only used by GitHub Pages, when deploying online +# Checkout https://pages.github.com/versions/ for the list of Github Pages built-in plugins + +# XXX --------- Specific to GHP config ------------ +remote_theme: pmarsceill/just-the-docs@v0.3.0 # XXX Our Jekyll theme - See https://pmarsceill.github.io/just-the-docs/ +#ga_tracking: + +# XXX --------- Common to all configs (local + GHP) -------------- + +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'bundle exec jekyll serve'. If you change this file, please restart the server process. + +# Site settings +# These are used to personalize your new site. If you look in the HTML files, +# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. +# You can create any custom variable you would like, and they will be accessible +# in the templates via {{ site.myvariable }}. +title: Next Right Now +description: >- # this means to ignore newlines until "baseurl:" + Flexible production-grade boilerplate with Next.js 9, Zeit and TypeScript. + Includes multiple opt-in presets using GraphQL, Analytics, CSS-in-JS, Monitoring, End-to-end testing, + Internationalization, CI/CD and B2B multiple single-tenants (monorepo) support +#baseurl: "" # the subpath of your site, e.g. /blog +#url: "" # the base hostname & protocol for your site, e.g. http://example.com XXX Auto-resolved by GitHub + +# Aux links for the upper right navigation +aux_links: + "Home": "/" + "Github": "https://github.com/UnlyEd/next-right-now" + "About us": "https://github.com/UnlyEd/Unly" + +footer_content: "Copyright © 2020 Unly. MIT license." +#color_scheme: "dark" + +plugins: + - jekyll-sitemap + - jemoji + +# Exclude from processing. +# The following items will not be processed, by default. Create a custom list +# to override the default setting. +# exclude: +# - Gemfile +# - Gemfile.lock +# - node_modules +# - vendor/bundle/ +# - vendor/cache/ +# - vendor/gems/ +# - vendor/ruby/ diff --git a/docs/_includes/nav.html b/docs/_includes/nav.html new file mode 100644 index 000000000..c20afffa6 --- /dev/null +++ b/docs/_includes/nav.html @@ -0,0 +1,55 @@ + diff --git a/docs/_includes/page-toc.md b/docs/_includes/page-toc.md new file mode 100644 index 000000000..57f2d2fe1 --- /dev/null +++ b/docs/_includes/page-toc.md @@ -0,0 +1,7 @@ +--- + +## Table of contents +{: .no_toc .text-delta } + +- TOC +{:toc} diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss new file mode 100644 index 000000000..50a2c78f7 --- /dev/null +++ b/docs/_sass/custom/custom.scss @@ -0,0 +1,53 @@ +// XXX Overrides CSS styles - See https://pmarsceill.github.io/just-the-docs/docs/customization/#override-and-completely-custom-styles + +blockquote { + background: #f9f9f9; + border-left: 3px solid #a493f2; + margin: 1.5em 10px; + padding: 0.5em 10px; + + &:before, + &:after { + color: #a493f2; + } + + &:before { + content: '"'; + } + + &:after { + content: '"'; + } + + p:first-child, + p:last-child { + display: inline; + } +} + +.pagination-section { + display: flex; + justify-content: space-between; + + &.space-even { + justify-content: space-evenly; + } +} + +// Overrides https://github.com/jekyll/jemoji for better positioning alongside text +img.emoji { + top: 5px; + position: relative; +} + +h1 { + code { + font-size: 36px; + } +} + +h2 { + code { + font-size: 24px; + } +} diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 000000000..73a07193f --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,21 @@ +--- +layout: default +title: CHANGELOG +nav_order: 80 +--- + +Changelog +=== + + +- v1.0.0 - 2020-02-28 + - Initial release, production-ready (doc to be improved) + + +--- + +
+ + [CONTRIBUTING](./contributing){: .btn .btn } + +
diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..9cb629a09 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,75 @@ +--- +layout: default +title: CONTRIBUTING +nav_order: 90 +--- + +# Contributing +{: .no_toc } + +{% include page-toc.md %} + +--- + +## Contributing about documentation + +Our documentation lives in the `docs/` folder. It is generated and hosted by Github Pages. + +Only the `master` branch generates the online documentation. + +It uses [Jekyll](https://jekyllrb.com/) behind the wheel, and [`just-the-docs`](https://pmarsceill.github.io/just-the-docs/) theme for Jekyll. + +--- + +### Installing Jekyll locally + +In order to contribute to the docs, you may need to install Jekyll locally (especially for non-trivial changes). +Jekyll needs Ruby binary. + +1. Install and configure Jekyll on your computer, follow [https://jekyllrb.com/docs/](https://jekyllrb.com/docs/) +1. Once Jekyll is installed, you can install all Ruby gems using `yarn doc:gem:install` +1. Once gems are installed, you can run the local Jekyll server by using `yarn doc:start` which will start the server at localhost:4000 + +--- + +### Configuring Jekyll properly + +Jekyll configuration uses 2 different files. +- [`docs/_config.yml`](docs/_config.yml) used by Github Pages +- [`docs/_config-development.yml`](docs/_config-development.yml) used by your local installation + +There are a few, but important differences between both. The custom configuration must be written at the top of each config file. +The shared configuration must be written below. + +> **N.B**: If you add custom/shared configuration, don't forget update both config files, as needed. + +--- + +### Reference + +Jekyll theme used: [`just-the-docs`](https://pmarsceill.github.io/just-the-docs/) + +#### How to build a custom TOC + +See [just-the-docs documentation](https://pmarsceill.github.io/just-the-docs/docs/navigation-structure/#in-page-navigation-with-table-of-contents) + +#### How to write comments in Markdown files + +```md +[//]: # (Some markdown comment) +``` + +--- + +### Known issues + +- Using `yarn doc:start` will rebuild the whole documentation but it's slower. Using `yarn doc:start:fast` won't rebuild the whole thing and it's faster. + If you're working on the navigation menu, be warned the fast mode won't apply changes and your menu links won't update. + +--- + +
+ + [CHANGELOG](./changelog){: .btn .btn } + +
diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 000000000..8a258869e Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..1fcd15110 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,32 @@ +--- +layout: default +title: Introduction +nav_order: 10 +--- + +# My GitHub Pages docs site + +Hello! Welcome to your docs site that comes built-in with NRN :smiley: + +Take a look at the [CONTRIBUTING](./contributing) section about how to install/run those docs locally. + +All the documentation you need about how to build your own docs site is available at [`just-the-docs`](https://pmarsceill.github.io/just-the-docs/). + +If you want to look at a more complex example, take a look at the [NRN docs source code](https://github.com/UnlyEd/next-right-now/tree/gh-pages)! + +One small difference though, NRN docs lives at the / folder, because it uses gh-pages, but the built-in docs that comes with a NRN preset are in the `docs/` folder instead. + +Using a `gh-pages` or `master` dedicated branch, or using your `master` branch `docs` folder really is up to you, you can choose what option you want on your GitHub Settings pages, in the **GitHub Pages** section. + +By default, GitHub won't use your `/docs` folder, until you manually configure it. + +> You can definitely leave this folder be for now on, and keep it around for a later use, or remove it altogether. +> It's completely unrelated to the rest of this boilerplate and won't have any side effect. + +--- + +
+ + [CONTRIBUTING](./contributing){: .btn .btn } + +
diff --git a/docs/sep-faq.md b/docs/sep-faq.md new file mode 100644 index 000000000..f4dd0ced1 --- /dev/null +++ b/docs/sep-faq.md @@ -0,0 +1,5 @@ +--- +layout: default +title: "-" +nav_order: 61 +--- diff --git a/jest.config.js b/jest.config.js index 070ea967e..d7901f880 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,4 @@ +// "package.json:jest" config cannot be used alongside this config, all Jest config must be centralised in this file - See https://github.com/facebook/jest/issues/10123#issuecomment-638796267 module.exports = { preset: 'ts-jest', testEnvironment: 'node', @@ -5,12 +6,39 @@ module.exports = { // XXX we must specify a custom tsconfig for tests because we need the typescript transform // to transform jsx into js rather than leaving it jsx such as the next build requires. you // can see this setting in tsconfig.jest.json -> "jsx": "react" - // See https://github.com/zeit/next.js/issues/8663 + // See https://github.com/vercel/next.js/issues/8663 'ts-jest': { - tsConfig: 'tsconfig.jest.json', + tsconfig: 'tsconfig.jest.json', }, }, + + /** + * Map our module path aliases, so that Jest can understand modules loaded using "@/common" and load the proper file. + * Required, or Jest will fail to import dependencies from tests. + * + * XXX The below list must match `tsconfig.json:compilerOptions.paths`, so the Next.js app and Jest resolve all aliases the same way. + * + * @see https://nextjs.org/docs/advanced-features/module-path-aliases + * @see https://github.com/ilearnio/module-alias/issues/46#issuecomment-546154015 + */ + moduleNameMapper: { + '^@/app/(.*)$': '/src/app/$1', + '^@/common/(.*)$': '/src/common/$1', + '^@/components/(.*)$': '/src/common/components/$1', + '^@/utils/(.*)$': '/src/common/utils/$1', + '^@/layouts/(.*)$': '/src/layouts/$1', + '^@/modules/(.*)$': '/src/modules/$1', + '^@/pages/(.*)$': '/src/pages/$1', + }, modulePathIgnorePatterns: [ - 'cypress' - ] + '.next/', + 'cypress', + ], + runner: 'groups', // Allow to use jest-runner-groups - See https://github.com/eugene-manuilov/jest-runner-groups#update-jest-config + setupFilesAfterEnv: [ + 'jest-extended', // Extends native "expect" abilities - See https://github.com/jest-community/jest-extended + 'jest-expect-message', // Allows to add additional message when test fails - See https://github.com/mattphillips/jest-expect-message + './jest.setup.js', + './jest.extends.ts', + ], }; diff --git a/jest.d.ts b/jest.d.ts new file mode 100644 index 000000000..b086bf019 --- /dev/null +++ b/jest.d.ts @@ -0,0 +1,16 @@ +import 'jest-extended'; + +/** + * Enhance the Node.js environment "global" variable to add our own types + * + * @see https://stackoverflow.com/a/42304473/2391795 + */ +declare global { + namespace NodeJS { + interface Global { + muteConsole: () => any; + muteConsoleButLog: () => any; + unmuteConsole: () => any; + } + } +} diff --git a/jest.extends.ts b/jest.extends.ts new file mode 100644 index 000000000..96c6f1afe --- /dev/null +++ b/jest.extends.ts @@ -0,0 +1,13 @@ +// XXX All expect.extend() utilities loaded here will be available for all tests, they also might need to be declared in jest.d.ts +import { + toMatchOneOf, + toMatchShapeOf, +} from 'jest-to-match-shape-of'; // See https://www.npmjs.com/package/jest-to-match-shape-of +// Import utilities that extend Jest "expect" function by themselves +import '@/modules/core/testing/toContainObject'; + +// Extend Jest "expect" function +expect.extend({ + toMatchOneOf, + toMatchShapeOf, +}); diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 000000000..3c0daf650 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,46 @@ +// XXX Unlike what could be expected, once an ENV var is found by dotenv, it won't be overridden +// So, the order must be from the most important to the less important +// See https://github.com/motdotla/dotenv/issues/256#issuecomment-598676663 +require('dotenv').config({ path: '.env.local' }); +require('dotenv').config({ path: '.env' }); + +/** + * Importing next during test applies automated polyfills: + * - Polyfill the built-in "fetch" provided by Next.js + * + * @see https://github.com/vercel/next.js/discussions/13678#discussioncomment-22383 How to use built-in fetch in tests? + * @see https://nextjs.org/blog/next-9-4#improved-built-in-fetch-support Next.js Blog - Improved Built-in Fetch Support + * @see https://jestjs.io/docs/en/configuration#setupfilesafterenv-array About setupFilesAfterEnv usage + */ +require('next'); + +// Backup of the native console object for later re-use +global._console = global.console; + +// Force mute console by returning a mock object that mocks the props we use +global.muteConsole = () => { + return { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + }; +}; + +// Force mute console by returning a mock object that mocks the props we use, except for "log" +global.muteConsoleButLog = () => { + return { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + log: _console.log, + warn: jest.fn(), + }; +}; + +// Restore previously made "console" object +global.unmuteConsole = () => _console; + +// Mock __non_webpack_require__ to use the standard node.js "require" +global['__non_webpack_require__'] = require; diff --git a/next.config.js b/next.config.js index cdd89c1aa..7a3ce7f70 100644 --- a/next.config.js +++ b/next.config.js @@ -1,48 +1,353 @@ -const withSourceMaps = require('@zeit/next-source-maps')(); -const withCSS = require('@zeit/next-css'); // Allows to import ".css" files, like bootstrap.css +const bundleAnalyzer = require('@next/bundle-analyzer'); +const nextSourceMaps = require('@zeit/next-source-maps'); +const createNextPluginPreval = require('next-plugin-preval/config'); const packageJson = require('./package'); +const i18nConfig = require('./src/modules/core/i18n/i18nConfig'); + +const withNextPluginPreval = createNextPluginPreval(); +const withSourceMaps = nextSourceMaps(); +const withBundleAnalyzer = bundleAnalyzer({ // Run with "yarn analyse:bundle" - See https://www.npmjs.com/package/@next/bundle-analyzer + enabled: process.env.ANALYZE_BUNDLE === 'true', +}); +const supportedLocales = i18nConfig.supportedLocales.map((supportedLocale) => { + return supportedLocale.name; +}); +const noRedirectBlacklistedPaths = ['_next', 'api']; // Paths that mustn't have rewrite applied to them, to avoid the whole app to behave inconsistently +const publicBasePaths = ['robots', 'static', 'favicon.ico']; // All items (folders, files) under /public directory should be added there, to avoid redirection when an asset isn't found +const noRedirectBasePaths = [...supportedLocales, ...publicBasePaths, ...noRedirectBlacklistedPaths]; // Will disable url rewrite for those items (should contain all supported languages and all public base paths) const date = new Date(); +const GIT_COMMIT_SHA_SHORT = typeof process.env.GIT_COMMIT_SHA === 'string' && process.env.GIT_COMMIT_SHA.substring(0, 8); + +console.debug(`Building Next with NODE_ENV="${process.env.NODE_ENV}" NEXT_PUBLIC_APP_STAGE="${process.env.NEXT_PUBLIC_APP_STAGE}" for NEXT_PUBLIC_CUSTOMER_REF="${process.env.NEXT_PUBLIC_CUSTOMER_REF}" using GIT_COMMIT_SHA=${process.env.GIT_COMMIT_SHA} and GIT_COMMIT_REF=${process.env.GIT_COMMIT_REF}`); + +const GIT_COMMIT_TAGS = (process.env.GIT_COMMIT_TAGS ? process.env.GIT_COMMIT_TAGS.trim() : '').replace('refs/tags/', ''); +console.debug(`Deployment will be tagged automatically, using GIT_COMMIT_TAGS: "${GIT_COMMIT_TAGS}"`); + +// Iterate over all tags and extract the first the match "v*" +const APP_RELEASE_TAG = GIT_COMMIT_TAGS ? GIT_COMMIT_TAGS.split(' ').find((tag) => tag.startsWith('v')) : `unknown-${GIT_COMMIT_SHA_SHORT}`; +console.debug(`Release version resolved from tags: "${APP_RELEASE_TAG}" (matching first tag starting with "v")`); + +/** + * This file is for advanced configuration of the Next.js framework. + * + * The below config applies to the whole application. + * next.config.js gets used by the Next.js server and build phases, and it's not included in the browser build. + * + * XXX Not all configuration options are listed below, we only kept those of most interest. + * You'll need to dive into Next.js own documentation to find out about what's not included. + * Basically, we focused on options that seemed important for a SSG/SSR app running on serverless mode (Vercel). + * Also, we included some options by are not using them, this is mostly to help make you aware of those options, in case you'd need them. + * + * @see https://nextjs.org/docs/api-reference/next.config.js/introduction + */ +module.exports = withNextPluginPreval(withBundleAnalyzer(withSourceMaps({ + // basepath: '', // If you want Next.js to cover only a subsection of the domain. See https://nextjs.org/docs/api-reference/next.config.js/basepath + // target: 'serverless', // Automatically enabled on Vercel, you may need to manually opt-in if you're not using Vercel. See https://nextjs.org/docs/api-reference/next.config.js/build-target#serverless-target + // trailingSlash: false, // By default Next.js will redirect urls with trailing slashes to their counterpart without a trailing slash. See https://nextjs.org/docs/api-reference/next.config.js/trailing-slash -console.debug(`Building Next with NODE_ENV="${process.env.NODE_ENV}" APP_STAGE="${process.env.APP_STAGE}" for CUSTOMER_REF="${process.env.CUSTOMER_REF}"`); + /** + * React's Strict Mode is a development mode only feature for highlighting potential problems in an application. + * It helps to identify unsafe lifecycles, legacy API usage, and a number of other features. + * + * Officially suggested by Next.js: + * We strongly suggest you enable Strict Mode in your Next.js application to better prepare your application for the future of React. + * + * If you or your team are not ready to use Strict Mode in your entire application, that's OK! You can incrementally migrate on a page-by-page basis using . + * + * @see https://nextjs.org/docs/api-reference/next.config.js/react-strict-mode + */ + reactStrictMode: true, -module.exports = withCSS(withSourceMaps({ + /** + * Environment variables added to JS bundle + * + * XXX All env variables defined in ".env*" files that aren't public (those that don't start with "NEXT_PUBLIC_") MUST manually be made available at build time below. + * They're necessary on Vercel for runtime execution (SSR, SSG with revalidate, everything that happens server-side will need those). + * + * XXX This is a duplication of the environment variables. + * The variables defined below are only used locally, while those in "vercel.*.json:build:env" will be used on the Vercel platform. + * See https://vercel.com/docs/v2/build-step/#providing-environment-variables + * + * @see https://nextjs.org/docs/api-reference/next.config.js/environment-variables + */ env: { - // XXX Duplication of the environment variables, this is only used locally (See https://github.com/zeit/next.js#build-time-configuration) - // while now.json:build:env will be used on the Now platform (See https://zeit.co/docs/v2/build-step/#providing-environment-variables) - NRN_PRESET: process.env.NRN_PRESET, - CUSTOMER_REF: process.env.CUSTOMER_REF, - APP_STAGE: process.env.APP_STAGE, + GITHUB_DISPATCH_TOKEN: process.env.GITHUB_DISPATCH_TOKEN, GRAPHQL_API_ENDPOINT: process.env.GRAPHQL_API_ENDPOINT, GRAPHQL_API_KEY: process.env.GRAPHQL_API_KEY, - LOCIZE_PROJECT_ID: process.env.LOCIZE_PROJECT_ID, LOCIZE_API_KEY: process.env.LOCIZE_API_KEY, - AMPLITUDE_API_KEY: process.env.AMPLITUDE_API_KEY, SENTRY_DSN: process.env.SENTRY_DSN, - // Non duplicated environment variables (automatically resolved, must not be specified in the .env.build file) - BUILD_TIME: date.toString(), - BUILD_TIMESTAMP: +date, - APP_NAME: packageJson.name, - APP_VERSION: packageJson.version, - UNLY_SIMPLE_LOGGER_ENV: process.env.APP_STAGE, // Used by @unly/utils-simple-logger - Fix missing staging logs because it believes we're in production + // Vercel env variables - See https://vercel.com/docs/environment-variables#system-environment-variables + VERCEL: process.env.VERCEL, + VERCEL_ENV: process.env.VERCEL_ENV, + VERCEL_URL: process.env.VERCEL_URL, + CI: process.env.CI, + + // Dynamic env variables + NEXT_PUBLIC_APP_DOMAIN: process.env.VERCEL_URL, // Alias + NEXT_PUBLIC_APP_BASE_URL: process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:8888', + NEXT_PUBLIC_APP_BUILD_TIME: date.toString(), + NEXT_PUBLIC_APP_BUILD_TIMESTAMP: +date, + NEXT_PUBLIC_APP_NAME: packageJson.name, + NEXT_PUBLIC_APP_NAME_VERSION: `${packageJson.name}-${APP_RELEASE_TAG}`, + GIT_COMMIT_SHA_SHORT, + GIT_COMMIT_SHA: process.env.GIT_COMMIT_SHA, // Resolve commit hash from ENV first (set through CI), fallbacks to reading git (when used locally, through "/scripts/populate-git-env.sh") + GIT_COMMIT_REF: process.env.GIT_COMMIT_REF, // Resolve commit ref (branch/tag) from ENV first (set through CI), fallbacks to reading git (when used locally, through "/scripts/populate-git-env.sh") + GIT_COMMIT_TAGS: process.env.GIT_COMMIT_TAGS || '', // Resolve commit tags/releases from ENV first (set through CI), fallbacks to reading git (when used locally, through "/scripts/populate-git-env.sh") }, - webpack: (config, { isServer, buildId }) => { - const APP_VERSION_RELEASE = `${packageJson.version}_${buildId}`; - // Dynamically add some "env" variables that will be replaced during the build - config.plugins[1].definitions['process.env.APP_RELEASE'] = JSON.stringify(buildId); - config.plugins[1].definitions['process.env.APP_VERSION_RELEASE'] = JSON.stringify(APP_VERSION_RELEASE); + /** + * Headers allow you to set custom HTTP headers for an incoming request path. + * + * Headers allow you to set route specific headers like CORS headers, content-types, and any other headers that may be needed. + * They are applied at the very top of the routes. + * + * @example source: '/(.*?)', // Match all paths, including "/" + * @example source: '/:path*', // Match all paths, excluding "/" + * + * @return {Promise>} + * @see https://nextjs.org/docs/api-reference/next.config.js/headers + * @since 9.5 - See https://nextjs.org/blog/next-9-5#headers + */ + async headers() { + // XXX We need to embed our website into external websites for the NRN demo, but you might want to disable this + const DISABLE_IFRAME_EMBED_FROM_3RD_PARTIES = false; - if (isServer) { // Trick to only log once - console.debug(`[webpack] Building release "${APP_VERSION_RELEASE}"`); + const headers = [ + { + // Make all fonts immutable and cached for one year + 'source': '/static/fonts/(.*?)', + 'headers': [ + { + 'key': 'Cache-Control', + // See https://www.keycdn.com/blog/cache-control-immutable#what-is-cache-control-immutable + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#browser_compatibility + 'value': `public, max-age=31536000, immutable`, + }, + ], + }, + { + // Make all other static assets immutable and cached for one hour + 'source': '/static/(.*?)', + 'headers': [ + { + 'key': 'Cache-Control', + // See https://www.keycdn.com/blog/cache-control-immutable#what-is-cache-control-immutable + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#browser_compatibility + 'value': `public, max-age=3600, immutable`, + }, + ], + }, + { + source: '/(.*?)', // Match all paths, including "/" - See https://github.com/vercel/next.js/discussions/17991#discussioncomment-112028 + headers: [ + // This directive helps protect against some XSS attacks + // See https://infosec.mozilla.org/guidelines/web_security#x-content-type-options + { + key: 'X-Content-Type-Options', + value: `nosniff`, + }, + ], + }, + { + source: '/(.*?)', // Match all paths, including "/" - See https://github.com/vercel/next.js/discussions/17991#discussioncomment-112028 + headers: [ + // This directive helps protect user's privacy and might avoid leaking sensitive data in urls to 3rd parties (e.g: when loading a 3rd party asset) + // See https://infosec.mozilla.org/guidelines/web_security#referrer-policy + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + // See https://scotthelme.co.uk/a-new-security-header-referrer-policy/ + { + key: 'Referrer-Policy', + // "no-referrer-when-downgrade" is the default behaviour + // XXX You might want to restrict even more the referrer policy + value: `no-referrer-when-downgrade`, + }, + ], + }, + ]; + + /** + * Because the NRN demo uses Stacker provider to show our app as an embedded iframe, we need to allow our pages to be embedded by other websites. + * + * In staging, we don't want to allow any website to embed our app by default, to avoid customers mistakenly use our preview URL in their production app. + * Although, we want to allow Stacker to do it, so we can preview our website from Stacker (quick-preview). + * + * In production, we want to allow any website to embed our app by default, because we don't want to manage the list of websites that might embed our content. + * Alternatively, we could also generate the CSP dynamically by pre-fetching the allowed websites from our CMS/API. + */ + if (!DISABLE_IFRAME_EMBED_FROM_3RD_PARTIES && process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { + headers.push({ + source: '/(.*?)', // Match all paths, including "/" - See https://github.com/vercel/next.js/discussions/17991#discussioncomment-112028 + headers: [ + { + key: 'Content-Security-Policy', + value: 'frame-ancestors *.stacker.app *.airportal.app', // Airportal is the former name of Stacker + }, + ], + }); } - // Fixes npm packages that depend on `fs` module - config.node = { - fs: 'empty', - }; + // When 3rd party embeds are forbidden, only allow same origin to embed iframe by default + // This is a stronger default, if you don't to embed your site in any external website + if (DISABLE_IFRAME_EMBED_FROM_3RD_PARTIES) { + headers.push({ + // This directive's "ALLOW-FROM" option is deprecated in favor of "Content-Security-Policy" "frame-ancestors" + // So, we use a combination of both the CSP directive and the "X-Frame-Options" for browser that don't support CSP + // See https://infosec.mozilla.org/guidelines/web_security#x-frame-options + key: 'X-Frame-Options', + value: `SAMEORIGIN`, + }); + headers.push({ + source: '/(.*?)', // Match all paths, including "/" - See https://github.com/vercel/next.js/discussions/17991#discussioncomment-112028 + // See https://infosec.mozilla.org/guidelines/web_security#x-frame-options + headers: [ + { + key: 'Content-Security-Policy', + value: `frame-ancestors 'self`, + }, + ], + }); + } + + console.info('Using headers:', JSON.stringify(headers, null, 2)); + + return headers; + }, + + /** + * Rewrites allow you to map an incoming request path to a different destination path. + * + * Rewrites are only available on the Node.js environment and do not affect client-side routing. + * Rewrites are the most commonly used form of custom routing — they're used for dynamic routes (pretty URLs), user-land routing libraries (e.g. next-routes), internationalization, and other advanced use cases. + * + * For example, the route /user/:id rendering a specific user's profile page is a rewrite. + * Rendering your company's about page for both /about and /fr/a-propos is also a rewrite. + * The destination url can be internal, or external. + * + * @return { Promise }>> } + * @see https://nextjs.org/docs/api-reference/next.config.js/rewrites + * @since 9.5 - See https://nextjs.org/blog/next-9-5#rewrites + */ + async rewrites() { + const rewrites = [ + // I18n rewrites + { + // XXX Doesn't work locally (maybe because of rewrites), but works online + source: '/', + destination: '/api/autoRedirectToLocalisedPage', + }, + { + source: `/:locale((?!${noRedirectBasePaths.join('|')})[^/]+)(.*)`, + destination: '/api/autoRedirectToLocalisedPage', + }, + + // Robots rewrites + { + source: `/robots.txt`, + destination: process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? `/robots/production.txt` : '/robots/!production.txt', + }, + ]; + + console.info('Using rewrites:', rewrites); + + return rewrites; + }, - // XXX See https://github.com/zeit/next.js/blob/canary/examples/with-sentry-simple/next.config.js + /** + * Redirects allow you to redirect an incoming request path to a different destination path. + * + * Redirects are only available on the Node.js environment and do not affect client-side routing. + * By redirects, we mean HTTP Redirects (aka URL forwarding). + * Redirects are most commonly used when a website is reorganized — ensuring search engines and bookmarks are forwarded to their new locations. + * The destination url can be internal, or external. + * + * @return { Promise> } + * @see https://nextjs.org/docs/api-reference/next.config.js/redirects + * @since 9.5 - See https://nextjs.org/blog/next-9-5#redirects + */ + async redirects() { + const redirects = [ + // I18n redirects + { + // Redirect root link with trailing slash to non-trailing slash, avoids 404 - See https://github.com/vercel/next.js/discussions/10651#discussioncomment-8270 + source: '/:locale/', + destination: '/:locale', + permanent: process.env.NEXT_PUBLIC_APP_STAGE !== 'development', // Do not use permanent redirect locally to avoid browser caching when working on it + }, + { + // Redirect link with trailing slash to non-trailing slash (any depth), avoids 404 - See https://github.com/vercel/next.js/discussions/10651#discussioncomment-8270 + source: '/:locale/:path*/', + destination: '/:locale/:path*', + permanent: process.env.NEXT_PUBLIC_APP_STAGE !== 'development', // Do not use permanent redirect locally to avoid browser caching when working on it + }, + ]; + + console.info('Using redirects:', redirects); + + return redirects; + }, + + // See https://nextjs.org/docs/messages/webpack5 + // Necessary to manually specify to use webpack 5, because we use a custom "webpack" config (see below) + webpack5: true, + + resolve: { + fallback: { + // Fixes npm packages that depend on `fs` module + fs: false, + }, + }, + + /** + * The webpack function is executed twice, once for the server and once for the client. + * This allows you to distinguish between client and server configuration using the isServer property. + * + * @param config Current webpack config. Useful to reuse parts of what's already configured while overridding other parts. + * @param buildId The build id, used as a unique identifier between builds. + * @param dev Indicates if the compilation will be done in development. + * @param isServer It's true for server-side compilation, and false for client-side compilation. + * @param defaultLoaders Default loaders used internally by Next.js: + * - babel Default babel-loader configuration + * @see https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config + */ + webpack: (config, { + buildId, + dev, + isServer, + defaultLoaders, + }) => { + if (isServer) { + /** + * This special server-only environment variable isn't string-replaced by webpack during bundling (it isn't added to the DefinePlugin definitions). + * + * Therefore, it's: + * - Always '1' on the server, during development + * - Always '1' on the server, during the Next.js build step + * - Always undefined on the browser + * - Always undefined in API endpoints + * - Always undefined during static pages re-generations (ISG) and server-side pages + * + * It can be useful when performing processing that should only happen during the initial build, or not during the initial build. + */ + process.env.IS_SERVER_INITIAL_BUILD = '1'; + } + + const APP_VERSION_RELEASE = APP_RELEASE_TAG || buildId; + config.plugins.map((plugin, i) => { + // Inject custom environment variables in "DefinePlugin" - See https://webpack.js.org/plugins/define-plugin/ + if (plugin.__proto__.constructor.name === 'DefinePlugin') { + // Dynamically add some "public env" variables that will be replaced during the build through "DefinePlugin" + // Those variables are considered public because they are available at build time and at run time (they'll be replaced during initial build, by their value) + plugin.definitions['process.env.NEXT_PUBLIC_APP_BUILD_ID'] = JSON.stringify(buildId); + plugin.definitions['process.env.NEXT_PUBLIC_APP_VERSION_RELEASE'] = JSON.stringify(APP_VERSION_RELEASE); + } + }); + + if (isServer) { // Trick to only log once + console.debug(`[webpack] Building release "${APP_VERSION_RELEASE}" using NODE_ENV="${process.env.NODE_ENV}" ${process.env.IS_SERVER_INITIAL_BUILD ? 'with IS_SERVER_INITIAL_BUILD="1"' : ''}`); + } + + // XXX See https://github.com/vercel/next.js/blob/canary/examples/with-sentry-simple/next.config.js // In `pages/_app.js`, Sentry is imported from @sentry/node. While // @sentry/browser will run in a Node.js environment, @sentry/node will use // Node.js-only APIs to catch even more unhandled exceptions. @@ -63,5 +368,35 @@ module.exports = withCSS(withSourceMaps({ return config; }, - poweredByHeader: 'NRN - With love', -})); + + /** + * Next.js uses a constant id generated at build time to identify which version of your application is being served. + * + * This can cause problems in multi-server deployments when next build is ran on every server. + * In order to keep a static build id between builds you can provide your own build id. + * + * XXX We documented this function in case you might want to use it, but we aren't using it. + * + * @see https://nextjs.org/docs/api-reference/next.config.js/configuring-the-build-id + */ + // generateBuildId: async () => { + // // You can, for example, get the latest git commit hash here + // return 'my-build-id' + // }, + + /** + * Next.js exposes some options that give you some control over how the server will dispose or keep in memory built pages in development. + * + * XXX We documented this function in case you might want to use it, but we aren't using it. + * + * @see https://nextjs.org/docs/api-reference/next.config.js/configuring-onDemandEntries + */ + // onDemandEntries: { + // // period (in ms) where the server will keep pages in the buffer + // maxInactiveAge: 25 * 1000, + // // number of pages that should be kept simultaneously without being disposed + // pagesBufferLength: 2, + // }, + + poweredByHeader: false, // See https://nextjs.org/docs/api-reference/next.config.js/disabling-x-powered-by +}))); diff --git a/now.customer1.production.json b/now.customer1.production.json deleted file mode 100644 index cfe62438f..000000000 --- a/now.customer1.production.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": 2, - "name": "nrn-v1-ssr-mst-aptd-gcms-lcz-sty-c1", - "scope": "team_qnVfSEVc2WwmOE1OYhZr4VST", - "env": {}, - "build": { - "env": { - "APP_STAGE": "production", - "NRN_PRESET": "v1-ssr-mst-aptd-gcms-lcz-sty", - "CUSTOMER_REF": "customer1", - "GRAPHQL_API_ENDPOINT": "https://api-euwest.graphcms.com/v1/ck73ixhlv09yt01dv2ga1bkbp/master", - "GRAPHQL_API_KEY": "@nrn-graphql-api-key", - "LOCIZE_PROJECT_ID": "@nrn-locize-project-id", - "LOCIZE_API_KEY": "@nrn-locize-api-key", - "AMPLITUDE_API_KEY": "@nrn-amplitude-api-key-production", - "SENTRY_DSN": "@nrn-sentry-dsn" - } - }, - "routes": [ - { - "src": "/robots.txt", - "dest": "/robots/production.txt" - } - ], - "public": false -} diff --git a/now.customer1.staging.json b/now.customer1.staging.json deleted file mode 100644 index 6aafacc70..000000000 --- a/now.customer1.staging.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": 2, - "name": "nrn-v1-ssr-mst-aptd-gcms-lcz-sty-c1", - "scope": "team_qnVfSEVc2WwmOE1OYhZr4VST", - "env": {}, - "build": { - "env": { - "APP_STAGE": "staging", - "NRN_PRESET": "v1-ssr-mst-aptd-gcms-lcz-sty", - "CUSTOMER_REF": "customer1", - "GRAPHQL_API_ENDPOINT": "https://api-euwest.graphcms.com/v1/ck73ixhlv09yt01dv2ga1bkbp/master", - "GRAPHQL_API_KEY": "@nrn-graphql-api-key", - "LOCIZE_PROJECT_ID": "@nrn-locize-project-id", - "LOCIZE_API_KEY": "@nrn-locize-api-key", - "AMPLITUDE_API_KEY": "@nrn-amplitude-api-key-staging", - "SENTRY_DSN": "@nrn-sentry-dsn" - } - }, - "routes": [ - { - "src": "/robots.txt", - "dest": "/robots/!production.txt" - } - ], - "public": false -} diff --git a/now.customer2.production.json b/now.customer2.production.json deleted file mode 100644 index bbb2a3b44..000000000 --- a/now.customer2.production.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": 2, - "name": "nrn-v1-ssr-mst-aptd-gcms-lcz-sty-c2", - "scope": "team_qnVfSEVc2WwmOE1OYhZr4VST", - "env": {}, - "build": { - "env": { - "APP_STAGE": "production", - "NRN_PRESET": "v1-ssr-mst-aptd-gcms-lcz-sty", - "CUSTOMER_REF": "customer2", - "GRAPHQL_API_ENDPOINT": "https://api-euwest.graphcms.com/v1/ck73ixhlv09yt01dv2ga1bkbp/master", - "GRAPHQL_API_KEY": "@nrn-graphql-api-key", - "LOCIZE_PROJECT_ID": "@nrn-locize-project-id", - "LOCIZE_API_KEY": "@nrn-locize-api-key", - "AMPLITUDE_API_KEY": "@nrn-amplitude-api-key-production", - "SENTRY_DSN": "@nrn-sentry-dsn" - } - }, - "routes": [ - { - "src": "/robots.txt", - "dest": "/robots/production.txt" - } - ], - "public": false -} diff --git a/now.customer2.staging.json b/now.customer2.staging.json deleted file mode 100644 index d5940d475..000000000 --- a/now.customer2.staging.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": 2, - "name": "nrn-v1-ssr-mst-aptd-gcms-lcz-sty-c2", - "scope": "team_qnVfSEVc2WwmOE1OYhZr4VST", - "env": {}, - "build": { - "env": { - "APP_STAGE": "staging", - "NRN_PRESET": "v1-ssr-mst-aptd-gcms-lcz-sty", - "CUSTOMER_REF": "customer2", - "GRAPHQL_API_ENDPOINT": "https://api-euwest.graphcms.com/v1/ck73ixhlv09yt01dv2ga1bkbp/master", - "GRAPHQL_API_KEY": "@nrn-graphql-api-key", - "LOCIZE_PROJECT_ID": "@nrn-locize-project-id", - "LOCIZE_API_KEY": "@nrn-locize-api-key", - "AMPLITUDE_API_KEY": "@nrn-amplitude-api-key-staging", - "SENTRY_DSN": "@nrn-sentry-dsn" - } - }, - "routes": [ - { - "src": "/robots.txt", - "dest": "/robots/!production.txt" - } - ], - "public": false -} diff --git a/now.json b/now.json deleted file mode 120000 index b2d66da44..000000000 --- a/now.json +++ /dev/null @@ -1 +0,0 @@ -./now.customer1.staging.json \ No newline at end of file diff --git a/package.json b/package.json index b160080eb..cc2b5bf60 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,44 @@ { "name": "next-right-now", - "version": "1.0.1", "private": true, "scripts": { - "start": "now dev --listen 8888", - "build": "yarn test:once && next build", - "next": "next start", - "svg": "npx svgr -d src/svg src/svg --ext tsx --template ./src/utils/svgTemplate.ts", + "start": ". ./scripts/populate-git-env.sh && next dev --port 8888", + "start:windows": "next dev --port 8888", + "start:tunnel": "ngrok http 8888", + "build": "yarn test:once:group:no-integration && next build", + "build:fast": "next build", + "build:local": ". ./scripts/populate-git-env.sh && next build", + "build:profiler": ". ./scripts/populate-git-env.sh && next build --profile --debug", + "analyse:bundle": "yarn analyse:bundle:production", + "analyse:bundle:development": "ANALYZE_BUNDLE=true yarn start", + "analyse:bundle:production": ". ./scripts/populate-git-env.sh && ANALYZE_BUNDLE=true next build", + "analyse:unused": "next-unused", + "svg": "npx svgr -d svg-to-react svg-to-react --ext tsx --template svg-to-react/svgTemplate.ts", "deploy": "yarn deploy:customer1", "deploy:all": "yarn deploy:customer1 && yarn deploy:customer2", "deploy:all:production": "yarn deploy:customer1:production && yarn deploy:customer2:production", + "deploy:all:production:simple": "yarn deploy:customer1:production:simple && yarn deploy:customer2:production:simple", "deploy:all:all": "yarn deploy:all && yarn deploy:all:production", + "deploy:ci:gha": "yarn vercel:deploy --local-config=vercel.${CUSTOMER_REF}.staging.json", + "deploy:ci:gha:production": "yarn vercel:deploy --local-config=vercel.${CUSTOMER_REF}.production.json --prod", + "deploy:sb:gha": "yarn vercel:cleanup && yarn storybook:export && vercel storybook-static --local-config=vercel.storybook.json --confirm --debug", + "deploy:sb:gha:fast": "yarn vercel:cleanup && vercel storybook-static --local-config=vercel.storybook.json --confirm --debug", "deploy:customer1:all": "yarn deploy:customer1 && yarn deploy:customer1:production", - "deploy:customer1": "yarn now:cleanup && now --confirm --local-config=now.customer1.staging.json --debug", - "deploy:customer1:production": "yarn now:cleanup && now --confirm --local-config=now.customer1.production.json --prod --debug", + "deploy:customer1": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer1.staging.json", + "deploy:customer1:production": "yarn deploy:customer1:production:simple && yarn e2e:customer1:production", + "deploy:customer1:production:simple": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer1.production.json --prod", "deploy:customer2:all": "yarn deploy:customer2 && yarn deploy:customer2:production", - "deploy:customer2": "yarn now:cleanup && now --confirm --local-config=now.customer2.staging.json --debug", - "deploy:customer2:production": "yarn now:cleanup && now --confirm --local-config=now.customer2.production.json --prod --debug", - "now:cleanup": "npx del-cli .now/", + "deploy:customer2": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer2.staging.json", + "deploy:customer2:production": "yarn deploy:customer2:production:simple && yarn e2e:customer2:production", + "deploy:customer2:production:simple": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer2.production.json --prod", + "deploy:fake": "git commit --allow-empty -m \"Fake empty commit (force CI trigger)\"", + "vercel:cleanup": "npx del-cli .vercel/", + "vercel:deploy": ". ./scripts/populate-git-env.sh && vercel --build-env GIT_COMMIT_TAGS=$GIT_COMMIT_TAGS --build-env GIT_COMMIT_REF=$GIT_COMMIT_REF --build-env GIT_COMMIT_SHA=$GIT_COMMIT_SHA --confirm --debug --force", + "script:populate-git-env:print": ". ./scripts/populate-git-env.sh && echo 'SHA: '${GIT_COMMIT_SHA} && echo 'REF (current branch/tag): '${GIT_COMMIT_REF} && echo 'TAGS: '${GIT_COMMIT_TAGS}", + "git:getReleasesAndTags": "git tag --points-at HEAD | tr '\\r\\n' ' '", + "git:getCommitSHA": "git rev-parse HEAD", + "git:getCommitSHA:short": "git rev-parse --short HEAD", + "git:getCommitRef": "git symbolic-ref HEAD", "e2e": "yarn e2e:run", "e2e:all:production": "yarn e2e:customer1:production && yarn e2e:customer2:production", "e2e:customer1:production": "CYPRESS_STAGE=customer1 yarn e2e:run", @@ -27,129 +48,218 @@ "e2e:install": "cypress install", "e2e:open": "CYPRESS_STAGE=${CYPRESS_STAGE:-development}; cypress open --config-file cypress/config-$CYPRESS_STAGE.json", "e2e:run": "CYPRESS_STAGE=${CYPRESS_STAGE:-development}; cypress run --config-file cypress/config-$CYPRESS_STAGE.json", + "e2e:run:spec:common": "CYPRESS_STAGE=${CYPRESS_STAGE:-development}; cypress run --config-file cypress/config-$CYPRESS_STAGE.json --spec 'cypress/integration/app/common/*.js'", "e2e:ci": "yarn e2e:install && cypress run --record", - "preversion": "yarn lint:once && yarn test:once", - "release": "yarn bump --commit --tag && git add CHANGELOG.md README.md && git commit --amend --no-edit && git push && git push --tags", + "e2e:sb": "yarn e2e:sb:run", + "e2e:sb:run": "cypress run --config-file cypress/config-storybook.json", + "e2e:sb:open": "cypress open --config-file cypress/config-storybook.json", "doc:start": "cd docs/ && bundle exec jekyll serve --config _config-development.yml", "doc:start:fast": "cd docs/ && bundle exec jekyll serve --config _config-development.yml --incremental", "doc:gem:install": "cd docs/ && bundle install", + "doc": "yarn doc:online", + "doc:online": "open-cli https://unlyed.github.io/next-right-now/", + "doc:online:scripts": "open-cli https://unlyed.github.io/next-right-now/guides/scripts-and-utilities", + "bump:major": "git commit --allow-empty -m \"(MAJOR) Empty commit, bumps MAJOR version\"", + "bump:minor": "git commit --allow-empty -m \"(MINOR) Empty commit, bumps MINOR version\"", "lint": "esw src/ -w --ext .ts --ext .tsx", "lint:once": "eslint src/ --ext .ts --ext .tsx", "lint:fix": "eslint src/ --ext .ts --ext .tsx --fix", "lint:fix:preview": "eslint src/ --ext .ts --ext .tsx --fix-dry-run", - "test": "NODE_ENV=test jest --watchAll", - "test:once": "NODE_ENV=test jest", + "test": "NODE_ENV=test jest --watch", + "test:generate-output": "jest --json --outputFile=.jest-test-results.json || true", + "test:group:api": "NODE_ENV=test jest --group=api --watchAll", + "test:group:components": "NODE_ENV=test jest --group=components --watchAll", + "test:group:integration": "NODE_ENV=test jest --group=integration --watchAll", + "test:group:unit": "NODE_ENV=test jest --group=unit --watchAll", + "test:group:utils": "NODE_ENV=test jest --group=utils --watchAll", + "test:once": "NODE_ENV=test jest --runInBand --detectOpenHandles", + "test:once:group:no-integration": "NODE_ENV=test jest --group=-integration", "test:coverage": "NODE_ENV=test jest --coverage", + "test:coverage:group:no-integration": "NODE_ENV=test jest --group=-integration --coverage", + "test:config": "NODE_ENV=test jest --showConfig", + "sb": "yarn storybook", + "storybook": "STORYBOOK=true start-storybook -h localhost -p 6006 --no-manager-cache", + "storybook:export": "STORYBOOK=true yarn test:generate-output && build-storybook", + "sb:upgrade": "sb upgrade", + "codemod:update-react-imports": "npx react-codemod update-react-imports src/", + "codemod:name-default-component": "npx @next/codemod name-default-component src/", + "codemod:withamp-to-config": "npx @next/codemod withamp-to-config src/", + "codemod:module-path-aliases": "yarn codemod:module-path-aliases:src && yarn codemod:module-path-aliases:cypress && yarn codemod:module-path-aliases:sb", + "codemod:module-path-aliases:src": "npx relative-to-alias --src 'src' --alias '@/app' --alias-path './src/app' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/common' --alias-path './src/common' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/components' --alias-path './src/common/components' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/utils' --alias-path './src/common/utils' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/layouts' --alias-path './src/layouts' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/modules' --alias-path './src/modules' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/pages' --alias-path './src/pages' --extensions 'js,ts,tsx' --language 'typescript'", + "codemod:module-path-aliases:cypress": "npx relative-to-alias --src 'cypress' --alias '@/app' --alias-path './src/app' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/common' --alias-path './src/common' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/components' --alias-path './src/common/components' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/utils' --alias-path './src/common/utils' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/layouts' --alias-path './src/layouts' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/modules' --alias-path './src/modules' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/pages' --alias-path './src/pages' --extensions 'js,ts,tsx' --language 'typescript'", + "codemod:module-path-aliases:sb": "npx relative-to-alias --src '.storybook' --alias '@/app' --alias-path './src/app' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/common' --alias-path './src/common' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/components' --alias-path './src/common/components' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/utils' --alias-path './src/common/utils' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/layouts' --alias-path './src/layouts' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/modules' --alias-path './src/modules' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/pages' --alias-path './src/pages' --extensions 'js,ts,tsx' --language 'typescript'", "security:audit": "yarn audit", "packages:upgrade": "yarn upgrade-interactive --latest" }, - "jest": { - "setupFilesAfterEnv": [ - "jest-extended" + "next-unused": { + "alias": {}, + "debug": true, + "include": [ + "src" + ], + "exclude": [ + ".test.", + "src/utils/extend-jest/*", + "src/utils/svg/svgTemplate.ts" + ], + "entrypoints": [ + "src/pages" ] }, "dependencies": { + "@amplitude/node": "1.6.1", "@amplitude/react-amplitude": "1.0.0", - "@apollo/react-ssr": "3.1.4", - "@emotion/core": "10.0.28", - "@emotion/styled": "10.0.27", - "@fortawesome/fontawesome-svg-core": "1.2.28", - "@fortawesome/free-brands-svg-icons": "5.13.0", - "@fortawesome/free-solid-svg-icons": "5.13.0", - "@fortawesome/react-fontawesome": "0.1.9", - "@sentry/browser": "5.15.4", - "@sentry/node": "5.15.4", + "@apollo/client": "3.3.17", + "@emotion/react": "11.4.0", + "@emotion/styled": "11.3.0", + "@fortawesome/fontawesome-svg-core": "1.2.35", + "@fortawesome/free-brands-svg-icons": "5.15.3", + "@fortawesome/free-regular-svg-icons": "5.15.3", + "@fortawesome/free-solid-svg-icons": "5.15.3", + "@fortawesome/react-fontawesome": "0.1.14", + "@sentry/browser": "6.7.2", + "@sentry/node": "6.7.2", + "@types/lodash.isequal": "4.5.5", + "@unly/simple-logger": "1.0.0", "@unly/universal-language-detector": "2.0.3", "@unly/utils": "1.0.3", - "@unly/utils-simple-logger": "1.4.0", - "amplitude-js": "5.10.0", - "animate.css": "3.7.2", - "apollo-boost": "0.4.7", - "apollo-cache-inmemory": "1.6.5", - "apollo-client": "2.6.8", - "apollo-link-http": "1.5.16", - "bootstrap": "4.4.1", - "classnames": "2.2.6", + "amplitude-js": "8.2.1", + "animate.css": "4.1.1", + "append-query": "2.1.0", + "bootstrap": "4.6.0", + "chalk": "4.1.1", + "classnames": "2.3.1", + "cookieconsent": "3.1.1", "cookies": "0.8.0", "css-to-react-native": "3.0.0", "deepmerge": "4.2.2", - "emotion-theming": "10.0.27", - "graphql": "15.0.0", - "graphql-tag": "2.10.3", - "i18next": "19.3.4", - "i18next-locize-backend": "3.1.1", - "i18next-node-locize-backend": "3.1.0", - "isomorphic-unfetch": "3.0.0", + "graphql": "15.5.0", + "graphql-tag": "2.12.4", + "i18next": "20.2.2", + "i18next-locize-backend": "4.2.0", "js-cookie": "2.2.1", "json-stringify-safe": "5.0.1", - "locize-editor": "3.0.0", - "locize-node-lastused": "2.0.0", + "locize-editor": "3.1.1", + "locize-lastused": "3.0.11", "lodash.clonedeep": "4.5.0", + "lodash.filter": "4.6.0", "lodash.find": "4.6.0", + "lodash.findindex": "4.6.0", "lodash.get": "4.4.2", + "lodash.groupby": "4.6.0", "lodash.includes": "4.3.0", "lodash.isarray": "4.0.0", "lodash.isempty": "4.4.0", + "lodash.isequal": "4.5.0", "lodash.isplainobject": "4.0.6", + "lodash.kebabcase": "4.1.1", "lodash.map": "4.6.0", + "lodash.reduce": "4.6.0", "lodash.remove": "4.7.0", + "lodash.size": "4.2.0", + "lodash.some": "4.6.0", + "lodash.startswith": "4.2.1", + "lodash.uniq": "4.5.0", "lodash.xorby": "4.7.0", - "next": "9.3.4", + "markdown-to-jsx": "7.1.2", + "next": "11.0.0", "next-cookies": "2.0.3", - "next-with-apollo": "5.0.0", + "next-plugin-preval": "1.2.0", "prop-types": "15.7.2", - "rc-tooltip": "4.0.3", - "react": "16.13.1", - "react-apollo": "3.1.4", - "react-dom": "16.13.1", - "react-i18next": "11.3.4", + "rc-tooltip": "5.1.1", + "react": "17.0.2", + "react-code-blocks": "0.0.8", + "react-dom": "17.0.2", + "react-i18next": "11.8.15", "react-style-proptype": "3.2.2", - "reactstrap": "8.4.1", + "reactstrap": "8.9.0", "recompose": "0.30.0", - "webfontloader": "1.6.28", - "winston": "3.2.1" + "tinycolor2": "1.4.2", + "uuid": "8.3.2" }, "devDependencies": { - "@now/node": "1.5.0", - "@svgr/cli": "5.3.0", - "@types/amplitude-js": "5.8.0", - "@types/cookies": "0.7.4", - "@types/jest": "25.1.5", - "@types/js-cookie": "2.2.5", + "@cypress/react": "5.8.1", + "@emotion/babel-preset-css-prop": "11.2.0", + "@emotion/eslint-plugin": "11.2.0", + "@next/bundle-analyzer": "10.2.0", + "@storybook/addon-a11y": "6.2.9", + "@storybook/addon-actions": "6.2.9", + "@storybook/addon-console": "1.2.3", + "@storybook/addon-essentials": "6.2.9", + "@storybook/addon-google-analytics": "6.2.9", + "@storybook/addon-jest": "6.2.9", + "@storybook/addon-links": "6.2.9", + "@storybook/addon-storysource": "6.2.9", + "@storybook/addons": "6.2.9", + "@storybook/react": "6.2.9", + "@storybook/theming": "6.2.9", + "@svgr/cli": "5.5.0", + "@types/amplitude-js": "8.0.0", + "@types/cookies": "0.7.6", + "@types/jest": "26.0.23", + "@types/jest-expect-message": "1.0.3", + "@types/js-cookie": "2.2.6", "@types/lodash.clonedeep": "4.5.6", "@types/lodash.find": "4.6.6", + "@types/lodash.findindex": "4.6.6", "@types/lodash.get": "4.4.6", + "@types/lodash.groupby": "4.6.6", "@types/lodash.includes": "4.3.6", "@types/lodash.isarray": "4.0.6", "@types/lodash.isempty": "4.4.6", "@types/lodash.isplainobject": "4.0.6", + "@types/lodash.kebabcase": "4.1.6", "@types/lodash.map": "4.6.13", + "@types/lodash.reduce": "4.6.6", "@types/lodash.remove": "4.7.6", + "@types/lodash.size": "4.2.6", + "@types/lodash.some": "4.6.6", + "@types/lodash.startswith": "4.2.6", + "@types/lodash.uniq": "4.5.6", "@types/lodash.xorby": "4.7.6", - "@types/react": "16.9.32", - "@types/webfontloader": "1.6.29", - "@types/webpack-env": "1.15.1", - "@typescript-eslint/eslint-plugin": "2.26.0", - "@typescript-eslint/parser": "2.26.0", - "@zeit/next-css": "1.0.1", + "@types/popper.js": "1.11.0", + "@types/react": "17.0.11", + "@types/react-dom": "17.0.7", + "@types/react-test-renderer": "17.0.1", + "@types/reactstrap": "8.7.2", + "@types/uuid": "8.3.0", + "@types/webpack-env": "1.16.0", + "@typescript-eslint/eslint-plugin": "4.23.0", + "@typescript-eslint/parser": "4.23.0", "@zeit/next-source-maps": "0.0.4-canary.1", - "concurrently": "5.1.0", - "cross-env": "7.0.2", - "cypress": "4.3.0", - "del-cli": "3.0.0", - "eslint": "6.8.0", - "eslint-plugin-jest": "23.8.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.19.0", - "eslint-plugin-react-hooks": "3.0.0", - "eslint-watch": "6.0.1", - "jest": "25.2.7", + "concurrently": "6.1.0", + "cross-env": "7.0.3", + "current-git-branch": "1.1.0", + "cypress": "7.4.0", + "del-cli": "3.0.1", + "dotenv": "9.0.2", + "eslint": "7.26.0", + "eslint-config-next": "11.0.0", + "eslint-plugin-jest": "24.3.6", + "eslint-plugin-jsx-a11y": "6.4.1", + "eslint-plugin-react": "7.23.2", + "eslint-plugin-react-hooks": "4.2.0", + "eslint-watch": "7.0.0", + "jest": "26.6.3", + "jest-expect-message": "1.0.2", "jest-extended": "0.11.5", - "node-mocks-http": "1.8.1", - "now": "17.1.1", - "react-test-renderer": "16.13.1", - "ts-jest": "25.3.0", - "typescript": "3.8.3", - "version-bump-prompt": "6.0.3" + "jest-runner-groups": "2.0.1", + "jest-to-match-shape-of": "1.3.1", + "next-unused": "0.0.6", + "ngrok": "4.0.1", + "node-fetch": "2.6.1", + "node-mocks-http": "1.10.1", + "open-cli": "6.0.1", + "react-test-renderer": "17.0.2", + "relative-to-alias": "2.0.1", + "storybook-addon-designs": "5.4.5", + "storybook-addon-next-router": "2.0.4", + "storybook-addon-performance": "0.15.2", + "storybook-css-modules-preset": "1.1.1", + "storybook-mobile": "0.1.31", + "ts-jest": "26.5.6", + "type-fest": "1.1.1", + "typescript": "4.2.4", + "vercel": "22.0.1" } } diff --git a/process.env.d.ts b/process.env.d.ts new file mode 100644 index 000000000..febd9b29c --- /dev/null +++ b/process.env.d.ts @@ -0,0 +1,60 @@ +/** + * Declare known environment variables. + * Enables auto-completion when using "process.env.". + * Makes it easier to find env vars, and helps avoid typo mistakes. + * + * Unlisted env vars will still be usable. + * + * @see https://stackoverflow.com/a/53981706/2391795 + */ +declare global { + namespace NodeJS { + interface ProcessEnv { + // NRN env variables + GITHUB_DISPATCH_TOKEN: string; + GRAPHQL_API_ENDPOINT: string; + GRAPHQL_API_KEY: string; + IS_SERVER_INITIAL_BUILD: '1' | undefined; + LOCIZE_API_KEY: string; + NODE_ENV: 'development' | 'production'; + NEXT_PUBLIC_AMPLITUDE_API_KEY: string; + NEXT_PUBLIC_APP_BASE_URL: string; + NEXT_PUBLIC_APP_BUILD_ID: string; + NEXT_PUBLIC_APP_DOMAIN: string; + NEXT_PUBLIC_APP_NAME: string; + NEXT_PUBLIC_APP_NAME_VERSION: string; + NEXT_PUBLIC_APP_VERSION_RELEASE: string; + NEXT_PUBLIC_APP_STAGE: 'test' | 'development' | 'staging' | 'production'; + NEXT_PUBLIC_APP_BUILD_TIME: string; + NEXT_PUBLIC_APP_BUILD_TIMESTAMP: string; + NEXT_PUBLIC_CUSTOMER_REF: string; + NEXT_PUBLIC_LOCIZE_PROJECT_ID: string; + NEXT_PUBLIC_NRN_PRESET: string; + SENTRY_DSN: string; + + // Git env variables + GIT_COMMIT_SHA_SHORT: string; + GIT_COMMIT_SHA: string; + GIT_COMMIT_REF: string; + GIT_COMMIT_TAGS: string; + + // Vercel (+ AWS) env variables - See https://vercel.com/docs/environment-variables#system-environment-variables + AWS_EXECUTION_ENV: string; + AWS_LAMBDA_FUNCTION_MEMORY_SIZE: string; + AWS_REGION: string; + VERCEL: string; // An indicator that the app is deployed and running on Vercel. Example: '1'. + VERCEL_ENV: string; // The Environment that the app is deployed an running on. The value can be either 'production', 'preview', or 'development'. + VERCEL_REGION: string; // The ID of the Region where the app is running. Example: 'cdg1'. XXX: This Variable is only exposed during Runtime for Serverless Functions. + VERCEL_URL: string; // The URL of the deployment. Example: 'my-site-7q03y4pi5.vercel.app'. XXX Do not use. Using NEXT_PUBLIC_APP_DOMAIN is preferred (alias) + CI: string; // An indicator that the code is running in a Continuous Integration environment. Example: '1'. XXX: This Variable is only exposed during Build Step. + + // Other + TZ: string; // TimeZone (":UTC") + } + } +} + +// Trick to make this a valid module: +// If this file has no import/export statements (i.e. is a script) +// convert it into a module by adding an empty export statement. +export {}; diff --git a/public/static/CDN/README.md b/public/static/CDN/README.md deleted file mode 100644 index fe3397487..000000000 --- a/public/static/CDN/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# CDN - -> This folder is a copy of our CDN and is meant to be used as reference. -> -> **It is not directly used by the project itself.** - ---- - -## `detect-outdated-browser` - -> Based on https://github.com/mikemaccana/outdated-browser-rework - -Copy of https://github.com/mikemaccana/outdated-browser-rework/blob/5cc5dd986d17bc168ba9699a8f4d54b42401155d/dist/outdated-browser-rework.min.js, based on commit https://github.com/mikemaccana/outdated-browser-rework/commit/5cc5dd986d17bc168ba9699a8f4d54b42401155d - -**Changes**: -- The copy has been customised to auto run the script once loaded, using the default settings -- Also, links pointing to `http://outdatedbrowser.com/` have been replaced by `https://browser-update.org/update-browser.html` because of https://github.com/mikemaccana/outdated-browser-rework/issues/72 - -> See [CDN folder](https://console.cloud.google.com/storage/browser/the-funding-place/assets/libs/outdated-browser-rework?organizationId=581986496946&project=loan-advisor-194400) - -### Note about Next.js -There are no way of performing outdated browser support other than using external JS/CSS files, because everything in `src/components/Head.tsx` is bundled by Next.js -and ends up bundled together, which completely fails to be loaded by outdated browsers such as IE11 which may not be able to parse the source code - -Because of this, we load an external JS file and CSS file (async if possible for perfs) from a CDN so that it doesn't get bundled with the rest of the source code, but is executed on its own instead. diff --git a/public/static/fonts/CircularStd-Book/CircularStd-Book.ttf b/public/static/fonts/CircularStd-Book/CircularStd-Book.ttf deleted file mode 100644 index e69de29bb..000000000 diff --git a/public/static/fonts/CircularStd-Book/CircularStd-Book.woff b/public/static/fonts/CircularStd-Book/CircularStd-Book.woff deleted file mode 100644 index a4be1de57..000000000 Binary files a/public/static/fonts/CircularStd-Book/CircularStd-Book.woff and /dev/null differ diff --git a/public/static/fonts/CircularStd-Book/CircularStd-Book.woff2 b/public/static/fonts/CircularStd-Book/CircularStd-Book.woff2 deleted file mode 100644 index acaa8148d..000000000 Binary files a/public/static/fonts/CircularStd-Book/CircularStd-Book.woff2 and /dev/null differ diff --git a/public/static/fonts/CircularStd-Book/font.css b/public/static/fonts/CircularStd-Book/font.css deleted file mode 100644 index ad0ddd161..000000000 --- a/public/static/fonts/CircularStd-Book/font.css +++ /dev/null @@ -1,18 +0,0 @@ -/* - XXX See https://creativemarket.com/blog/the-missing-guide-to-font-formats -*/ - -/* - This font doesn't play well with paragraphs and must only be used for headings - Also, it's automatically "bold", there is no "light" variant -*/ -@font-face { - font-family: 'CircularStd-Book'; - font-display: fallback; /* See https://drafts.csswg.org/css-fonts-4/#font-display-desc */ - src: local('Circular Std Book'), local('CircularStd-Book'), - url('CircularStd-Book.woff2') format('woff2'), - url('CircularStd-Book.woff') format('woff'), - url('CircularStd-Book.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} diff --git a/public/static/fonts/Inter/inter-variable-latin.woff2 b/public/static/fonts/Inter/inter-variable-latin.woff2 new file mode 100644 index 000000000..2fbab974a Binary files /dev/null and b/public/static/fonts/Inter/inter-variable-latin.woff2 differ diff --git a/public/static/fonts/Manrope/Manrope-variable-latin.woff b/public/static/fonts/Manrope/Manrope-variable-latin.woff new file mode 100644 index 000000000..dae12344d Binary files /dev/null and b/public/static/fonts/Manrope/Manrope-variable-latin.woff differ diff --git a/public/static/fonts/Manrope/Manrope-variable-latin.woff2 b/public/static/fonts/Manrope/Manrope-variable-latin.woff2 new file mode 100644 index 000000000..2cd43bc5d Binary files /dev/null and b/public/static/fonts/Manrope/Manrope-variable-latin.woff2 differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.eot b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.eot deleted file mode 100644 index e44c97399..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.eot and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.ttf b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.ttf deleted file mode 100644 index 363658a28..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.ttf and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.woff b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.woff deleted file mode 100644 index a2a814c55..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.woff and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.woff2 b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.woff2 deleted file mode 100644 index ea91df6e7..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-black.woff2 and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.eot b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.eot deleted file mode 100644 index 5e3731e3f..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.eot and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.ttf b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.ttf deleted file mode 100644 index c1d50eddb..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.ttf and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.woff b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.woff deleted file mode 100644 index b8355cf12..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.woff and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.woff2 b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.woff2 deleted file mode 100644 index 8464c9389..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-bold.woff2 and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.eot b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.eot deleted file mode 100644 index 79768d7af..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.eot and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.ttf b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.ttf deleted file mode 100644 index 4f44e86d5..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.ttf and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.woff b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.woff deleted file mode 100644 index ebf38af01..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.woff and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.woff2 b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.woff2 deleted file mode 100644 index 9727c929f..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-light.woff2 and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.eot b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.eot deleted file mode 100644 index 65506ebac..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.eot and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.ttf b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.ttf deleted file mode 100644 index 7d0310f36..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.ttf and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.woff b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.woff deleted file mode 100644 index 22107174b..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.woff and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.woff2 b/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.woff2 deleted file mode 100644 index 59c9231ea..000000000 Binary files a/public/static/fonts/NeuzeitGrotesk/NeuzeitGrotesk-regular.woff2 and /dev/null differ diff --git a/public/static/fonts/NeuzeitGrotesk/font.css b/public/static/fonts/NeuzeitGrotesk/font.css deleted file mode 100644 index 9bdf026c2..000000000 --- a/public/static/fonts/NeuzeitGrotesk/font.css +++ /dev/null @@ -1,43 +0,0 @@ -@font-face { - font-family: 'neuzeit-grotesk'; - src: url('NeuzeitGrotesk-light.eot'); - src: url('NeuzeitGrotesk-light.eot?#iefix') format('embedded-opentype'), - url('NeuzeitGrotesk-light.woff2') format('woff2'), - url('NeuzeitGrotesk-light.woff') format('woff'), - url('NeuzeitGrotesk-light.ttf') format('truetype'); - font-style: normal; - font-weight: 200; -} - -@font-face { - font-family: 'neuzeit-grotesk'; - src: url('NeuzeitGrotesk-regular.eot'); - src: url('NeuzeitGrotesk-regular.eot?#iefix') format('embedded-opentype'), - url('NeuzeitGrotesk-regular.woff2') format('woff2'), - url('NeuzeitGrotesk-regular.woff') format('woff'), - url('NeuzeitGrotesk-regular.ttf') format('truetype'); - font-style: normal; - font-weight: 900; -} - -@font-face { - font-family: 'neuzeit-grotesk'; - src: url('NeuzeitGrotesk-bold.eot'); - src: url('NeuzeitGrotesk-bold.eot?#iefix') format('embedded-opentype'), - url('NeuzeitGrotesk-bold.woff2') format('woff2'), - url('NeuzeitGrotesk-bold.woff') format('woff'), - url('NeuzeitGrotesk-bold.ttf') format('truetype'); - font-style: normal; - font-weight: 400; -} - -@font-face { - font-family: 'neuzeit-grotesk'; - src: url('NeuzeitGrotesk-black.eot'); - src: url('NeuzeitGrotesk-black.eot?#iefix') format('embedded-opentype'), - url('NeuzeitGrotesk-black.woff2') format('woff2'), - url('NeuzeitGrotesk-black.woff') format('woff'), - url('NeuzeitGrotesk-black.ttf') format('truetype'); - font-style: normal; - font-weight: 700; -} diff --git a/public/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg b/public/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg new file mode 100644 index 000000000..f416f76ee --- /dev/null +++ b/public/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg @@ -0,0 +1 @@ + diff --git a/public/static/images/LOGO_Powered_by_UNLY_monochrome_WHITE.svg b/public/static/images/LOGO_Powered_by_UNLY_monochrome_WHITE.svg deleted file mode 100644 index 8bc2f018e..000000000 --- a/public/static/images/LOGO_Powered_by_UNLY_monochrome_WHITE.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - diff --git a/public/static/libs/detect-outdated-browser/README.md b/public/static/libs/detect-outdated-browser/README.md new file mode 100644 index 000000000..4973af2b3 --- /dev/null +++ b/public/static/libs/detect-outdated-browser/README.md @@ -0,0 +1,27 @@ +# `detect-outdated-browser` + +> Based on https://github.com/mikemaccana/outdated-browser-rework + +Copy of https://github.com/mikemaccana/outdated-browser-rework/blob/5cc5dd986d17bc168ba9699a8f4d54b42401155d/dist/outdated-browser-rework.min.js, +based on commit https://github.com/mikemaccana/outdated-browser-rework/commit/5cc5dd986d17bc168ba9699a8f4d54b42401155d + +**Changes**: +- The copy has been customised to auto run the script once loaded, using the default settings. +- Also, links pointing to `http://outdatedbrowser.com/` have been replaced by `https://browser-update.org/update-browser.html` because of https://github.com/mikemaccana/outdated-browser-rework/issues/72 + +## Note about Next.js + +There is no way to provide outdated browser support other than using external JS/CSS files, because everything in `src/components/Head.tsx` is bundled by Next.js, +and ends up bundled together, which completely fails to be loaded by outdated browsers such as IE11 which may not be able to parse the source code. + +Because of this, we load an external JS file and CSS file (async if possible for perfs) so that it doesn't get bundled with the rest of the source code, but is executed on its own instead. + +## Unused + +This module is an experiment and has been **disabled**. + +Basically, browser detection works fine, **unless running on a mobile device**, +where apps (facebook, linkedin, medium) display false-positive warnings about **embedded browsers** if they're too old, +and the user can't do anything about it (e.g: Facebook Chrome, Linkedin Chrome, etc.) because those aren't managed by the user but by the vendor instead. + +The code has been kept for now, until a better solution comes to light, but it is not being used by the app. diff --git a/public/static/CDN/detect-outdated-browser/outdated-browser-rework.css b/public/static/libs/detect-outdated-browser/outdated-browser-rework.css similarity index 100% rename from public/static/CDN/detect-outdated-browser/outdated-browser-rework.css rename to public/static/libs/detect-outdated-browser/outdated-browser-rework.css diff --git a/public/static/CDN/detect-outdated-browser/outdated-browser-rework.min.js b/public/static/libs/detect-outdated-browser/outdated-browser-rework.min.js similarity index 99% rename from public/static/CDN/detect-outdated-browser/outdated-browser-rework.min.js rename to public/static/libs/detect-outdated-browser/outdated-browser-rework.min.js index c4b7db4e7..b82bbbb79 100644 --- a/public/static/CDN/detect-outdated-browser/outdated-browser-rework.min.js +++ b/public/static/libs/detect-outdated-browser/outdated-browser-rework.min.js @@ -1,4 +1,6 @@ !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).outdatedBrowserRework=e()}}(function(){return function u(r,s,l){function n(i,e){if(!s[i]){if(!r[i]){var o="function"==typeof require&&require;if(!e&&o)return o(i,!0);if(d)return d(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var t=s[i]={exports:{}};r[i][0].call(t.exports,function(e){var o=r[i][1][e];return n(o||e)},t,t.exports,u,r,s,l)}return s[i].exports}for(var d="function"==typeof require&&require,e=0;e"+f.update.web+''+f.callToAction+"

",googlePlay:"

"+f.update.googlePlay+''+f.callToAction+"

",appStore:"

"+f.update[u]+"

"}[u],v=f.outOfDate,k()&&f.unsupported&&(v=f.unsupported),'
'+v+"
"+h+'

×

'),c=document.getElementById("buttonCloseUpdateBrowser"),w=document.getElementById("buttonUpdateBrowser"),i.style.backgroundColor=t,i.style.color=r,i.children[0].children[0].style.color=r,i.children[0].children[1].style.color=r,w&&(w.style.color=r,w.style.borderColor&&(w.style.borderColor=r),w.onmouseover=function(){this.style.color=t,this.style.backgroundColor=r},w.onmouseout=function(){this.style.color=r,this.style.backgroundColor=t}),c.style.color=r,c.onmousedown=function(){return!(i.style.display="none")}}},o=window.onload;"function"!=typeof window.onload?window.onload=e:window.onload=function(){o&&o(),e()}}},{"./extend":1,"./languages.json":3,"ua-parser-js":4}],3:[function(e,o,i){o.exports={br:{outOfDate:"O seu navegador está desatualizado!",update:{web:"Atualize o seu navegador para ter uma melhor experiência e visualização deste site. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#br",callToAction:"Atualize o seu navegador agora",close:"Fechar"},ca:{outOfDate:"El vostre navegador no està actualitzat!",update:{web:"Actualitzeu el vostre navegador per veure correctament aquest lloc web. ",googlePlay:"Instal·leu Chrome des de Google Play",appStore:"Actualitzeu iOS des de l'aplicació Configuració"},url:"https://browser-update.org/update-browser.html#es",callToAction:"Actualitzar el meu navegador ara",close:"Tancar"},cn:{outOfDate:"您的浏览器已过时",update:{web:"要正常浏览本网站请升级您的浏览器。",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#cn",callToAction:"现在升级",close:"关闭"},cz:{outOfDate:"Váš prohlížeč je zastaralý!",update:{web:"Pro správné zobrazení těchto stránek aktualizujte svůj prohlížeč. ",googlePlay:"Nainstalujte si Chrome z Google Play",appStore:"Aktualizujte si systém iOS"},url:"https://browser-update.org/update-browser.html#cz",callToAction:"Aktualizovat nyní svůj prohlížeč",close:"Zavřít"},da:{outOfDate:"Din browser er forældet!",update:{web:"Opdatér din browser for at få vist denne hjemmeside korrekt. ",googlePlay:"Installér venligst Chrome fra Google Play",appStore:"Opdatér venligst iOS"},url:"https://browser-update.org/update-browser.html#da",callToAction:"Opdatér din browser nu",close:"Luk"},de:{outOfDate:"Ihr Browser ist veraltet!",update:{web:"Bitte aktualisieren Sie Ihren Browser, um diese Website korrekt darzustellen. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#de",callToAction:"Den Browser jetzt aktualisieren ",close:"Schließen"},ee:{outOfDate:"Sinu veebilehitseja on vananenud!",update:{web:"Palun uuenda oma veebilehitsejat, et näha lehekülge korrektselt. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#ee",callToAction:"Uuenda oma veebilehitsejat kohe",close:"Sulge"},en:{outOfDate:"Your browser is out-of-date!",update:{web:"Update your browser to view this website correctly. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#",callToAction:"Update my browser now",close:"Close"},es:{outOfDate:"¡Tu navegador está anticuado!",update:{web:"Actualiza tu navegador para ver esta página correctamente. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#es",callToAction:"Actualizar mi navegador ahora",close:"Cerrar"},fa:{rightToLeft:!0,outOfDate:"مرورگر شما منسوخ شده است!",update:{web:"جهت مشاهده صحیح این وبسایت، مرورگرتان را بروز رسانی نمایید. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#",callToAction:"همین حالا مرورگرم را بروز کن",close:"Close"},fi:{outOfDate:"Selaimesi on vanhentunut!",update:{web:"Lataa ajantasainen selain nähdäksesi tämän sivun oikein. ",googlePlay:"Asenna uusin Chrome Google Play -kaupasta",appStore:"Päivitä iOS puhelimesi asetuksista"},url:"https://browser-update.org/update-browser.html#fi",callToAction:"Päivitä selaimeni nyt ",close:"Sulje"},fr:{outOfDate:"Votre navigateur n'est plus compatible !",update:{web:"Mettez à jour votre navigateur pour afficher correctement ce site Web. ",googlePlay:"Merci d'installer Chrome depuis le Google Play Store",appStore:"Merci de mettre à jour iOS depuis l'application Réglages"},url:"https://browser-update.org/update-browser.html#fr",callToAction:"Mettre à jour maintenant ",close:"Fermer"},hu:{outOfDate:"A böngészője elavult!",update:{web:"Firssítse vagy cserélje le a böngészőjét. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#hu",callToAction:"A böngészőm frissítése ",close:"Close"},id:{outOfDate:"Browser yang Anda gunakan sudah ketinggalan zaman!",update:{web:"Perbaharuilah browser Anda agar bisa menjelajahi website ini dengan nyaman. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#",callToAction:"Perbaharui browser sekarang ",close:"Close"},it:{outOfDate:"Il tuo browser non è aggiornato!",update:{web:"Aggiornalo per vedere questo sito correttamente. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#it",callToAction:"Aggiorna ora",close:"Chiudi"},lt:{outOfDate:"Jūsų naršyklės versija yra pasenusi!",update:{web:"Atnaujinkite savo naršyklę, kad galėtumėte peržiūrėti šią svetainę tinkamai. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#",callToAction:"Atnaujinti naršyklę ",close:"Close"},nl:{outOfDate:"Je gebruikt een oude browser!",update:{web:"Update je browser om deze website correct te bekijken. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#nl",callToAction:"Update mijn browser nu ",close:"Sluiten"},pl:{outOfDate:"Twoja przeglądarka jest przestarzała!",update:{web:"Zaktualizuj swoją przeglądarkę, aby poprawnie wyświetlić tę stronę. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#pl",callToAction:"Zaktualizuj przeglądarkę już teraz",close:"Close"},pt:{outOfDate:"O seu browser está desatualizado!",update:{web:"Atualize o seu browser para ter uma melhor experiência e visualização deste site. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#pt",callToAction:"Atualize o seu browser agora",close:"Fechar"},ro:{outOfDate:"Browserul este învechit!",update:{web:"Actualizați browserul pentru a vizualiza corect acest site. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#",callToAction:"Actualizați browserul acum!",close:"Close"},ru:{outOfDate:"Ваш браузер устарел!",update:{web:"Обновите ваш браузер для правильного отображения этого сайта. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#ru",callToAction:"Обновить мой браузер ",close:"Закрыть"},si:{outOfDate:"Vaš brskalnik je zastarel!",update:{web:"Za pravilen prikaz spletne strani posodobite vaš brskalnik. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#si",callToAction:"Posodobi brskalnik ",close:"Zapri"},sv:{outOfDate:"Din webbläsare stödjs ej längre!",update:{web:"Uppdatera din webbläsare för att webbplatsen ska visas korrekt. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#",callToAction:"Uppdatera min webbläsare nu",close:"Stäng"},ua:{outOfDate:"Ваш браузер застарів!",update:{web:"Оновіть ваш браузер для правильного відображення цього сайта. ",googlePlay:"Please install Chrome from Google Play",appStore:"Please update iOS from the Settings App"},url:"https://browser-update.org/update-browser.html#ua",callToAction:"Оновити мій браузер ",close:"Закрити"}}},{}],4:[function(e,k,P){!function(t,p){"use strict";var c="function",e="undefined",o="model",i="name",a="type",r="vendor",s="version",l="architecture",n="console",d="mobile",u="tablet",w="smarttv",m="wearable",g={extend:function(e,o){var i={};for(var a in e)o[a]&&o[a].length%2==0?i[a]=o[a].concat(e[a]):i[a]=e[a];return i},has:function(e,o){return"string"==typeof e&&-1!==o.toLowerCase().indexOf(e.toLowerCase())},lowerize:function(e){return e.toLowerCase()},major:function(e){return"string"==typeof e?e.replace(/[^\d\.]/g,"").split(".")[0]:p},trim:function(e){return e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},b={rgx:function(e,o){for(var i,a,t,r,s,l,n=0;n&1 | sed '/^* /d; /bytes data]$/d; s/> //; s/< //' + +# See https://stackoverflow.com/a/54225157/2391795 +# -vs - add headers (-v) but remove progress bar (-s) +# 2>&1 - combine stdout and stderr into single stdout +# sed - edit response produced by curl using the commands below +# /^* /d - remove lines starting with '* ' (technical info) +# /bytes data]$/d - remove lines ending with 'bytes data]' (technical info) +# s/> // - remove '> ' prefix +# s/< // - remove '< ' prefix diff --git a/src/README.md b/src/README.md new file mode 100644 index 000000000..6bf664c35 --- /dev/null +++ b/src/README.md @@ -0,0 +1,6 @@ +Sources folder +=== + +[Read more about the folder structure](https://unlyed.github.io/next-right-now/reference/folder-structure). + +Also, you might be interested to understand how "[module path aliases](https://unlyed.github.io/next-right-now/reference/module-path-aliases)" work. diff --git a/src/app/README.md b/src/app/README.md new file mode 100644 index 000000000..92d3b3d2c --- /dev/null +++ b/src/app/README.md @@ -0,0 +1,11 @@ +App +=== + +> Check out the [documentation about the folder structure](../README.md#app-folder) + +Summary: + +- This folder is a way to organize what happens in `pages/_app`. +- It also contains app-wide configuration (constants). +- `Multiversal` means code executed "no matter what". +- Next Right Now `pages/_app` split the server and the browser code into 2 different files, to make it easier to understand. diff --git a/src/app/components/BrowserPageBootstrap.tsx b/src/app/components/BrowserPageBootstrap.tsx new file mode 100644 index 000000000..ad3f416cd --- /dev/null +++ b/src/app/components/BrowserPageBootstrap.tsx @@ -0,0 +1,217 @@ +import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; +import { OnlyBrowserPageProps } from '@/layouts/core/types/OnlyBrowserPageProps'; +import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitudeBrowserClient'; +import amplitudeContext from '@/modules/core/amplitude/context/amplitudeContext'; +import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; +import useCustomer from '@/modules/core/data/hooks/useCustomer'; +import useDataset from '@/modules/core/data/hooks/useDataset'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { detectLightHouse } from '@/modules/core/lightHouse/lighthouse'; +import { createLogger } from '@/modules/core/logging/logger'; +import { + ClientNetworkConnectionType, + ClientNetworkInformationSpeed, + getClientNetworkConnectionType, + getClientNetworkInformationSpeed, +} from '@/modules/core/networkInformation/networkInformation'; +import { configureSentryBrowserMetadata } from '@/modules/core/sentry/browser'; +import { configureSentryUserMetadata } from '@/modules/core/sentry/universal'; +import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; +import { + CYPRESS_WINDOW_NS, + detectCypress, +} from '@/modules/core/testing/cypress'; +import userConsentContext from '@/modules/core/userConsent/contexts/userConsentContext'; +import initCookieConsent, { getUserConsent } from '@/modules/core/userConsent/cookieConsent'; +import { UserConsent } from '@/modules/core/userConsent/types/UserConsent'; +import { UserSemiPersistentSession } from '@/modules/core/userSession/types/UserSemiPersistentSession'; +import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; +import { + getIframeReferrer, + isRunningInIframe, +} from '@/utils/iframe'; +import { + Amplitude, + AmplitudeProvider, +} from '@amplitude/react-amplitude'; +import { useTheme } from '@emotion/react'; +import * as Sentry from '@sentry/node'; +import { AmplitudeClient } from 'amplitude-js'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { MultiversalAppBootstrapPageProps } from '../types/MultiversalAppBootstrapPageProps'; +import { MultiversalAppBootstrapProps } from '../types/MultiversalAppBootstrapProps'; + +const fileLabel = 'app/components/BrowserPageBootstrap'; +const logger = createLogger({ + fileLabel, +}); + +export type BrowserPageBootstrapProps = MultiversalAppBootstrapProps; + +/** + * Bootstraps the page, only when rendered on the browser + * + * @param props + */ +const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => { + const { + Component, + err, + router, + } = props; + // When the page is served by the browser, some browser-only properties are available + const pageProps = props.pageProps as unknown as MultiversalPageProps; + const { + customerRef, + lang, + locale, + } = pageProps; + const { + t, + i18n, + } = useTranslation(); + const dataset = useDataset(); + const customer: Customer = useCustomer(); + const isInIframe: boolean = isRunningInIframe(); + const iframeReferrer: string = getIframeReferrer(); + const cookiesManager: UniversalCookiesManager = new UniversalCookiesManager(); // On browser, we can access cookies directly (doesn't need req/res or page context) + const userSession: UserSemiPersistentSession = cookiesManager.getUserData(); + const userId = userSession.id; + const injectedPageProps: MultiversalPageProps = { + ...props.pageProps, + isInIframe, + iframeReferrer, + cookiesManager, + userSession, + }; + const theme = useTheme(); + const isCypressRunning = detectCypress(); + const isLightHouseRunning = detectLightHouse(); + const networkSpeed: ClientNetworkInformationSpeed = getClientNetworkInformationSpeed(); + const networkConnectionType: ClientNetworkConnectionType = getClientNetworkConnectionType(); + + // Configure Sentry user and track navigation through breadcrumb + configureSentryUserMetadata(userSession); + configureSentryBrowserMetadata(networkSpeed, networkConnectionType, isInIframe, iframeReferrer); + + Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs + category: fileLabel, + message: `Rendering ${fileLabel}`, + level: Sentry.Severity.Debug, + }); + + const userConsent: UserConsent = getUserConsent(); + const { + isUserOptedOutOfAnalytics, + hasUserGivenAnyCookieConsent, + } = userConsent; + const amplitudeInstance: AmplitudeClient = getAmplitudeInstance({ + customerRef, + iframeReferrer, + isInIframe, + lang, + locale, + userId, + userConsent, + networkSpeed, + networkConnectionType, + }); + + // Init the Cookie Consent popup, which will open on the browser + initCookieConsent({ + allowedPages: [ // We only allow it on those pages to avoid display that boring popup on every page + `${window.location.origin}/${locale}/demo/terms`, + `${window.location.origin}/${locale}/demo/privacy`, + `${window.location.origin}/${locale}/demo/built-in-features/cookies-consent`, + ], + amplitudeInstance, + locale, + t, + theme, + userConsent, + }); + + // XXX Inject data so that Cypress can use them to run dynamic tests. + // Those data mustn't be sensitive. They'll be available in the DOM, no matter the stage of the app. + // This is needed to run E2E tests that are specific to a customer. (dynamic testing) + window[CYPRESS_WINDOW_NS] = { + dataset, + customer, + }; + + // In non-production stages, bind some utilities to the browser's DOM, for ease of quick testing + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { + window['amplitudeInstance'] = amplitudeInstance; + window['i18n'] = i18n; + window['router'] = router; + window['t'] = t; + logger.info(`Utilities have been bound to the DOM for quick testing (only in non-production stages): + - amplitudeInstance + - i18n + - router + - t + `); + } + + return ( + + + + + + + + + + + + + + ); +}; + +export default BrowserPageBootstrap; diff --git a/src/app/components/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx new file mode 100644 index 000000000..9e0e2c536 --- /dev/null +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -0,0 +1,412 @@ +import Loader from '@/components/animations/Loader'; +import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; +import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; +import { useApollo } from '@/modules/core/apollo/apolloClient'; +import customerContext from '@/modules/core/data/contexts/customerContext'; +import datasetContext from '@/modules/core/data/contexts/datasetContext'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; +import { GraphCMSDataset } from '@/modules/core/data/types/GraphCMSDataset'; +import DefaultErrorLayout from '@/modules/core/errorHandling/DefaultErrorLayout'; +import i18nContext from '@/modules/core/i18n/contexts/i18nContext'; +import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; +import { stringifyQueryParameters } from '@/modules/core/i18n/i18nRouter'; +import { detectLightHouse } from '@/modules/core/lightHouse/lighthouse'; +import { createLogger } from '@/modules/core/logging/logger'; +import previewModeContext from '@/modules/core/previewMode/contexts/previewModeContext'; +import { + startPreviewMode, + stopPreviewMode, +} from '@/modules/core/previewMode/previewMode'; +import quickPreviewContext from '@/modules/core/quickPreview/contexts/quickPreviewContext'; +import { configureSentryI18n } from '@/modules/core/sentry/universal'; +import deserializeSafe from '@/modules/core/serializeSafe/deserializeSafe'; +import { detectCypress } from '@/modules/core/testing/cypress'; +import { initCustomerTheme } from '@/modules/core/theming/theme'; +import { NotFound404PageName } from '@/pages/404'; +import ErrorPage from '@/pages/_error'; +import { NO_AUTO_PREVIEW_MODE_KEY } from '@/pages/api/preview'; +import { ApolloProvider } from '@apollo/client'; +import { ThemeProvider } from '@emotion/react'; +import * as Sentry from '@sentry/node'; +import { isBrowser } from '@unly/utils'; +import { i18n } from 'i18next'; +import isEmpty from 'lodash.isempty'; +import size from 'lodash.size'; +import React, { useState } from 'react'; +import getComponentName from '../getComponentName'; +import { MultiversalAppBootstrapProps } from '../types/MultiversalAppBootstrapProps'; +import BrowserPageBootstrap, { BrowserPageBootstrapProps } from './BrowserPageBootstrap'; +import MultiversalGlobalStyles from './MultiversalGlobalStyles'; +import ServerPageBootstrap, { ServerPageBootstrapProps } from './ServerPageBootstrap'; + +const fileLabel = 'app/components/MultiversalAppBootstrap'; +const logger = createLogger({ + fileLabel, +}); + +type Props = MultiversalAppBootstrapProps | MultiversalAppBootstrapProps; + +/** + * Bootstraps a page and renders it + * + * Basically does everything a Page component needs to be rendered. + * All behaviors defined here are applied across the whole application (they're common to all pages) + * + * @param props + */ +const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Element => { + const { + pageProps, + router, + } = props; + // When using SSG with "fallback: true" and the page hasn't been generated yet then isSSGFallbackInitialBuild is true + const [isSSGFallbackInitialBuild] = useState(isEmpty(pageProps) && router?.isFallback === true); + const pageComponentName = getComponentName(props.Component); + const apolloClient = useApollo(pageProps); + + Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs + category: fileLabel, + message: `Rendering ${fileLabel}`, + level: Sentry.Severity.Debug, + }); + + // Configure meaningful Next.js props in Sentry for easier debugging (all errors will report the props being passed to the page) + // Filter out all entities that are too large, which might cause Sentry to fail sending the request + Sentry.configureScope((scope): void => { + const { + Component, + pageProps, + err, + router, + ...restProps + } = props; + const { + asPath, + basePath, + defaultLocale, + isFallback, + isSsr, + locale, + locales, + pathname, + query, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ...restRouter // Other router props aren't interesting to track and are being ignored + } = router; + const { + serializedDataset, // Size might be too big + i18nTranslations, // Size might be too big + ...restPageProps + } = pageProps; // XXX Exclude all non-meaningful props that might be too large for Sentry to handle, to avoid "403 Entity too large" + const serializedDatasetLength = (serializedDataset || '').length; + + // Track meaningful _app.props + unknown props. Other props (router, pageProps) will be tracked in another Sentry context for readability (DX) + scope.setContext('_app.props (filtered)', { + pageComponentName: pageComponentName, + err: props.err, + ...restProps, + }); + scope.setTag('hasCaughtNextErr', !!props?.err); + scope.setContext('_app.router (filtered)', { + asPath, + basePath, + defaultLocale, + isFallback, + isSsr, + locale, + locales, + pathname, + query, + }); + scope.setContext('_app.pageProps (filtered)', { + serializedDatasetLength, + ...restPageProps, + }); + scope.setContext('build', { + buildTime: process.env.NEXT_PUBLIC_BUILD_TIME, + buildTimeISO: (new Date(process.env.NEXT_PUBLIC_APP_BUILD_TIME || null)).toISOString(), + buildId: process.env.NEXT_PUBLIC_APP_BUILD_ID, + }); + }); + + if (isBrowser() && process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { // Avoids log clutter on server + console.debug('MultiversalAppBootstrap.props', props); // eslint-disable-line no-console + } + + // Display a loader (we could use a skeleton too) when this happens, so that the user doesn't face a white page until the page is generated and displayed + if (isSSGFallbackInitialBuild && router?.isFallback) { // When router.isFallback becomes "false", then it'll mean the page has been generated and rendered and we can display it, instead of the loader + return ( + + ); + } + + if (pageProps.isReadyToRender || pageProps.statusCode === 404) { + if (!process.env.IS_SERVER_INITIAL_BUILD) { // Avoids noise when building the whole app + logger.info('MultiversalAppBootstrap - App is ready, rendering...'); + } + + const { + serializedDataset, + i18nTranslations, + lang, + locale, + }: SSGPageProps | SSRPageProps = pageProps; + configureSentryI18n(lang, locale); + + // Unrecoverable error, we can't even display the layout because we don't have the minimal required information to properly do so. + // The reason can be a UI crash (something broke due to the user's interaction) and a top-level error was thrown in props.err. + // Or, it can be because no serializedDataset was provided. + // Either way, we display the error page, which will take care of reporting the error to Sentry and display an error message depending on the environment. + if (typeof serializedDataset !== 'string') { + // eslint-disable-next-line no-console + console.log('props', props); + + if (props.err) { + const error = new Error(`Fatal error - A top-level error was thrown by the application, which caused the Page.props to be lost. \n + The page cannot be shown to the end-user, an error page will be displayed.`); + logger.error(error); + + return ( + + + + ); + } else { + const error = new Error(`Fatal error - Unexpected "serializedDataset" passed as page props.\n + Expecting string, but got "${typeof serializedDataset}".\n + This error is often caused by returning an invalid "serializedDataset" from a getStaticProps/getServerSideProps.\n + Make sure you return a correct value, using "serializeSafe".`); + + return ( + + + + ); + } + } + + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && !process.env.IS_SERVER_INITIAL_BUILD) { + // XXX It's too cumbersome to do proper typings when type changes + // The "customer" was forwarded as a JSON-ish string (using Flatten) in order to avoid circular dependencies issues (SSG/SSR) + // It now being converted back into an object to be actually usable on all pages + // eslint-disable-next-line no-console + logger.debug('pageProps.serializedDataset length (bytes)', (serializedDataset as unknown as string)?.length); + // logger.debug('serializedDataset', serializedDataset); + } + + const dataset: GraphCMSDataset = deserializeSafe(serializedDataset); + const customer: Customer = dataset?.customer; + + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && isBrowser()) { + // eslint-disable-next-line no-console + console.debug(`pageProps.dataset (${size(Object.keys(dataset))} items)`, dataset); + // eslint-disable-next-line no-console + console.debug('dataset.customer', customer); + } + + let isPreviewModeEnabled; + let previewData; + let isQuickPreviewPage; + + if ('preview' in pageProps) { + // SSG + isPreviewModeEnabled = pageProps?.preview; + previewData = pageProps?.previewData; + + if (isBrowser()) { + const queryParameters: string = stringifyQueryParameters(router); + const isCypressRunning = detectCypress(); + const isLightHouseRunning = detectLightHouse(); + const noAutoPreviewMode = new URLSearchParams(window?.location?.search)?.get(NO_AUTO_PREVIEW_MODE_KEY) === 'true'; + + // XXX If we are running in staging stage and the preview mode is not enabled, then we force enable it + // We do this to enforce the staging stage is being used as a "preview environment" so it satisfies our publication workflow + // If we're running in development, then we don't enforce anything + // If we're running in production, then we force disable the preview mode, because we don't want to allow it in production + // XXX Also, don't enable preview mode when Cypress or LightHouse are running to avoid bad performances + if (process.env.NEXT_PUBLIC_APP_STAGE === 'staging' && !isPreviewModeEnabled && !isCypressRunning && !isLightHouseRunning && !noAutoPreviewMode) { + startPreviewMode(queryParameters); + } else if (process.env.NEXT_PUBLIC_APP_STAGE === 'production' && isPreviewModeEnabled) { + logger.error('Preview mode is not allowed in production, but was detected as enabled. It will now be disabled by force.'); + Sentry.captureMessage('Preview mode is not allowed in production, but was detected as enabled. It will now be disabled by force.', Sentry.Severity.Error); + stopPreviewMode(queryParameters); + } + } + } else { + // SSR + isPreviewModeEnabled = false; + previewData = null; + isQuickPreviewPage = pageProps?.isQuickPreviewPage; + } + + // Don't treat 404 pages like errors, it's expected in 404 pages not to have all the props the app needs + if (props?.Component?.name !== NotFound404PageName) { + // If the app is misconfigured, simulate a native Next.js error to catch the misconfiguration early + if (!customer || !i18nTranslations || !lang || !locale) { + let error = props.err || null; + + // Unrecoverable error, we can't even display the layout because we don't have the minimal required information to properly do so + // This most likely means something went wrong, and we must display the error page in such case + if (!error) { + // The most-likely issue could be that we failed to fetch the customer related to "process.env.NEXT_PUBLIC_CUSTOMER_REF" + // E.g: This will happens when an instance was deployed for a customer, but the customer.ref was changed since then. + if (process.env.NEXT_PUBLIC_CUSTOMER_REF !== customer?.ref) { + error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? + `Fatal error - An error happened, the page cannot be displayed. (customer doesn't match)` : + `Fatal error when bootstrapping the app ("${props?.Component?.name}"). The "customer.ref" doesn't match (expected: "${process.env.NEXT_PUBLIC_CUSTOMER_REF}", received: "${customer?.ref}").`, + ); + } else { + error = new Error(process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? + `Fatal error - An error happened, the page cannot be displayed.` : + `Fatal error when bootstrapping the app ("${props?.Component?.name}"). It might happen when lang/locale/translations couldn't be resolved.`, + ); + } + } else { + // If an error was detected by Next, then it means the current state is due to a top-level that was caught before + // We don't have anything to do, as it's automatically logged into Sentry + const error = new Error(`Fatal error - Misconfiguration detected, the page cannot be displayed.`); + logger.error(error); + } + + return ( + + + + ); + } else if (props?.err) { + // If an error was caught by Next.js (but wasn't fatal since we reached this point), we log it to Sentry to make sure we'll be notified + Sentry.withScope((scope): void => { + Sentry.captureException(props.err); + }); + } + } else { + if (!process.env.IS_SERVER_INITIAL_BUILD) { // Avoids capturing false-positive 404 pages when building the 404 page + // XXX Opinionated: Record an exception in Sentry for 404, if you don't want this then uncomment the below code + const err = new Error(`Page not found (404) for "${router?.asPath}"`); + + logger.warn(err); + Sentry.captureException(err); + } + } + + const i18nextInstance: i18n = i18nextLocize(lang, i18nTranslations); // Apply i18next configuration with Locize backend + const customerTheme: CustomerTheme = initCustomerTheme(customer); + + /* + * We split the rendering between server and browser + * There are actually 3 rendering modes, each of them has its own set of limitations + * 1. SSR (doesn't have access to browser-related features (LocalStorage), but it does have access to request-related data (cookies, HTTP headers)) + * 2. Server during SSG (doesn't have access to browser-related features (LocalStorage), nor to request-related data (cookies, localStorage, HTTP headers)) + * 3. Static rendering (doesn't have access to server-related features (HTTP headers), but does have access to request-related data (cookie) and browser-related features (LocalStorage)) + * + * What we do here, is to avoid rendering browser-related stuff if we're not running in a browser, because it cannot work properly. + * (e.g: Generating cookies will work, but they won't be stored on the end-user device, and it would create "Text content did not match" warnings, if generated from the server during SSG) + * + * So, the BrowserPageBootstrap does browser-related stuff and then call the PageBootstrap which takes care of stuff that is universal (identical between browser and server) + * + * XXX If you're concerned regarding React rehydration, read our talk with Josh, author of https://joshwcomeau.com/react/the-perils-of-rehydration/ + * https://twitter.com/Vadorequest/status/1257658553361408002 + * + * XXX There may be more rendering modes - See https://github.com/vercel/next.js/discussions/12558#discussioncomment-12303 + */ + let browserPageBootstrapProps: BrowserPageBootstrapProps; + let serverPageBootstrapProps: ServerPageBootstrapProps; + + if (isBrowser()) { + browserPageBootstrapProps = { + ...props, + router, + pageProps: { + ...pageProps, + i18nextInstance, + isSSGFallbackInitialBuild: isSSGFallbackInitialBuild, + customerTheme, + }, + }; + } else { + serverPageBootstrapProps = { + ...props, + router, + pageProps: { + ...pageProps, + i18nextInstance, + isSSGFallbackInitialBuild: isSSGFallbackInitialBuild, + customerTheme, + }, + }; + } + + return ( + + + + + + + {/* XXX Global styles that applies to all pages go there */} + + + + { + isBrowser() ? ( + + ) : ( + + ) + } + + + + + + + + ); + + } else { + // We wait for out props to contain "isReadyToRender: true", which means they've been set correctly by either getInitialProps/getStaticProps/getServerProps + // This helps avoid multiple useless renders (especially in development mode) and thus avoid noisy logs + // XXX I've recently tested without it and didn't notice any more logs than expected/usual. Maybe this was from a time where there were multiple full-renders? It may be removed if so (TODO later with proper testing) + // eslint-disable-next-line no-console + console.info('MultiversalAppBootstrap - App is not ready yet, waiting for isReadyToRender'); + return null; + } +}; + +export default MultiversalAppBootstrap; diff --git a/src/app/components/MultiversalGlobalExternalStyles.tsx b/src/app/components/MultiversalGlobalExternalStyles.tsx new file mode 100644 index 000000000..5cb506ba1 --- /dev/null +++ b/src/app/components/MultiversalGlobalExternalStyles.tsx @@ -0,0 +1,12 @@ +/** + * Contains all imports of external CSS libs (.css files) that must be injected in all Next.js pages. + * + * This approach is preferred over importing them all one by one within the _app.tsx file, because it's easier to maintain. + * + * Also, this file is being imported by both "src/pages/_app.tsx" and ".storybook/preview.js", + * so that global 3rd party CSS are included when previewing components, too. + */ +import 'animate.css/animate.min.css'; // Loads animate.css CSS file. See https://github.com/daneden/animate.css +import 'bootstrap/dist/css/bootstrap.min.css'; // Loads bootstrap CSS file. See https://stackoverflow.com/a/50002905/2391795 +import 'cookieconsent/build/cookieconsent.min.css'; // Loads CookieConsent CSS file. See https://github.com/osano/cookieconsent +import 'rc-tooltip/assets/bootstrap.css'; diff --git a/src/app/components/MultiversalGlobalStyles.tsx b/src/app/components/MultiversalGlobalStyles.tsx new file mode 100644 index 000000000..62cb1c5f3 --- /dev/null +++ b/src/app/components/MultiversalGlobalStyles.tsx @@ -0,0 +1,288 @@ +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; +import { + AllowedVariableFont, + injectFontFamily, +} from '@/modules/core/fonts/fonts'; +import { + css, + Global, +} from '@emotion/react'; +import React from 'react'; +import { + NRN_DEFAULT_FALLBACK_FONTS, + NRN_DEFAULT_FONT, +} from '../constants'; + +type Props = { + customerTheme: CustomerTheme; +} + +/** + * Those styles are applied + * - universally (browser + server) + * - globally (applied to all pages), through Layouts + * + * @param props + */ +const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Element => { + const { customerTheme } = props; + const { + primaryColor, + primaryColorVariant1, + onPrimaryColor, + secondaryColor, + secondaryColorVariant1, + onSecondaryColor, + backgroundColor, + onBackgroundColor, + surfaceColor, + onSurfaceColor, + errorColor, + onErrorColor, + fonts: activeFont, + ...rest + } = customerTheme; + const fontName: AllowedVariableFont = activeFont || NRN_DEFAULT_FONT; + const fontFamily = injectFontFamily(fontName); + + return ( + + ); +}; + +export default MultiversalGlobalStyles; diff --git a/src/app/components/ServerPageBootstrap.tsx b/src/app/components/ServerPageBootstrap.tsx new file mode 100644 index 000000000..a5a1d4d79 --- /dev/null +++ b/src/app/components/ServerPageBootstrap.tsx @@ -0,0 +1,56 @@ +import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; +import { OnlyServerPageProps } from '@/layouts/core/types/OnlyServerPageProps'; +import { createLogger } from '@/modules/core/logging/logger'; +import { configureSentryUserMetadata } from '@/modules/core/sentry/universal'; +import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; +import * as Sentry from '@sentry/node'; +import React from 'react'; +import { MultiversalAppBootstrapPageProps } from '../types/MultiversalAppBootstrapPageProps'; +import { MultiversalAppBootstrapProps } from '../types/MultiversalAppBootstrapProps'; + +const fileLabel = 'app/components/ServerPageBootstrap'; +const logger = createLogger({ + fileLabel, +}); + +export type ServerPageBootstrapProps = MultiversalAppBootstrapProps; + +/** + * Bootstraps the page, only when rendered on the server + * + * @param props + */ +const ServerPageBootstrap = (props: ServerPageBootstrapProps): JSX.Element => { + const { + Component, + err, + } = props; + // When the page is served by the server, some server-only properties are available + const pageProps = props.pageProps as unknown as MultiversalPageProps; + const injectedPageProps: MultiversalPageProps = { + ...pageProps, + }; + const { + userSession, + } = pageProps; + + // Configure Sentry user and track navigation through breadcrumb + configureSentryUserMetadata(userSession); + Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs + category: fileLabel, + message: `Rendering ${fileLabel}`, + level: Sentry.Severity.Debug, + }); + + return ( + + + + ); +}; + +export default ServerPageBootstrap; diff --git a/src/app/constants.ts b/src/app/constants.ts new file mode 100644 index 000000000..07a87aebf --- /dev/null +++ b/src/app/constants.ts @@ -0,0 +1,147 @@ +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; +import { AllowedVariableFont } from '@/modules/core/fonts/fonts'; +import { resolveVariantColor } from '@/modules/core/theming/colors'; + +export const NRN_DEFAULT_SERVICE_LABEL = process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? 'Next Right Now' : `[${process.env.NEXT_PUBLIC_APP_STAGE === 'staging' ? 'Preview' : 'Dev'}] Next Right Now`; + +/** + * Co-branding logo displayed in the footer ("powered by Unly") + */ +export const NRN_CO_BRANDING_LOGO_URL = '/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg'; + +/** + * Fallback fonts used until our own fonts have been loaded by the browser. + * Should only use native fonts that are installed on all devices by default. + * + * @see https://leerob.io/blog/fonts#system-fonts Inspired by + * @see https://systemfontstack.com/ Basic system font stacks + * @see https://www.w3schools.com/cssref/css_websafe_fonts.asp + * @see https://developer.mozilla.org/en-US/docs/Learn/CSS/Styling_text/Fundamentals#web_safe_fonts + */ +export const NRN_DEFAULT_FALLBACK_FONTS = `-apple-system, BlinkMacSystemFont, sans-serif`; + +/** + * Font used once our font have been loaded by the browser. + */ +export const NRN_DEFAULT_FONT: AllowedVariableFont = `Manrope`; + +/** + * Theme applied by default when no theme is defined. + * Will be used on a variable-by-variable basis based on what's configured on the CMS for the customer. + * + * Applied through "src/modules/core/theming/theme.ts" default value transformations. + * Strongly inspired from Material Design Color System. + * + * @see The below documentation comes from https://material.io/design/color/the-color-system.html + */ +export const NRN_DEFAULT_THEME: Omit & { + primaryColorVariant1: (primaryColor: string) => string; + secondaryColorVariant1: (primaryColor: string) => string; +} = { + /** + * A primary color is the color displayed most frequently across your app's screens and components. + */ + primaryColor: '#0028FF', + + /** + * Primary color first variant, meant to highlight interactive elements using the primary color (hover, etc.). + */ + primaryColorVariant1: resolveVariantColor, + + /** + * Color applied to text, icons, surfaces displayed on top of the primary color. + */ + onPrimaryColor: '#fff', + + /** + * A secondary color provides more ways to accent and distinguish your product. + * Having a secondary color is optional, and should be applied sparingly to accent select parts of your UI. + * If you don’t have a secondary color, your primary color can also be used to accent elements. + * + * The secondary color should be the same as the primary color, when no particular secondary color is being used. + * + * Secondary colors are best for: + * - Floating action buttons + * - Selection controls, like sliders and switches + * - Highlighting selected text + * - Progress bars + * - Links and headlines + */ + secondaryColor: '#000', + + /** + * Secondary color first variant, meant to highlight interactive elements using the primary color (hover, etc.). + */ + secondaryColorVariant1: resolveVariantColor, + + /** + * Color applied to text, icons, surfaces displayed on top of the secondary color. + */ + onSecondaryColor: '#fff', + + /** + * Background colors don’t represent brand. + * + * The background color appears behind top-level content. + * + * Used by/for: + * - All pages background + */ + backgroundColor: '#f4f4f4', + + /** + * Color applied to text, icons, surfaces displayed on top of the background color. + */ + onBackgroundColor: '#000', + + /** + * Surface colors don’t represent brand. + * + * Surface colors affect surfaces of components, such as cards, sheets, and menus. + */ + surfaceColor: '#fff', + + /** + * Color applied to text, icons, surfaces displayed on top of the surface color. + */ + onSurfaceColor: '#000', + + /** + * Error colors don’t represent brand. + * + * Error color indicates errors in components, such as invalid text in a text field. + * The baseline error color is #B00020. + */ + errorColor: '#FFE0E0', + + /** + * Color applied to text, icons, surfaces displayed on top of the error color. + */ + onErrorColor: '#FE6262', + + /** + * Logo displayed on the top footer. + */ + logo: null, + + /** + * Fonts used by the application. + * + * XXX At the moment, we don't allow the customer to define its own font, even though it's part of the customer theme. + */ + fonts: NRN_DEFAULT_FONT, +}; + +export const GITHUB_API_BASE_URL = 'https://api.github.com/'; + +/** + * Your GitHub Owner name. + * Used by "startVercelDeployment" API endpoint. + */ +export const GITHUB_OWNER_NAME = 'UnlyEd'; + +/** + * Your GitHub Repository name. + * Used by "startVercelDeployment" API endpoint. + */ +export const GITHUB_REPO_NAME = 'next-right-now'; diff --git a/src/app/getComponentName.ts b/src/app/getComponentName.ts new file mode 100644 index 000000000..cde9d37b7 --- /dev/null +++ b/src/app/getComponentName.ts @@ -0,0 +1,24 @@ +import { + NextComponentType, + NextPageContext, +} from 'next'; + +/** + * Resolves the name of a Next.js "pageProps.Component". + * A "Component" is a "Page". + * + * Extract the name from the component function source code. + * + * @param Component + */ +export const getComponentName = (Component: NextComponentType): string | null => { + try { + const componentAsString = Component.toString(); + + return componentAsString.split('(')[0].split(' ')[1]; + } catch (e) { + return null; + } +}; + +export default getComponentName; diff --git a/src/app/isNextApiRequest.ts b/src/app/isNextApiRequest.ts new file mode 100644 index 000000000..f523bd9f3 --- /dev/null +++ b/src/app/isNextApiRequest.ts @@ -0,0 +1,14 @@ +import { IncomingMessage } from 'http'; +import { NextApiRequest } from 'next'; + +/** + * TS type guard resolving whether "req" matches a "NextApiRequest" object. + * + * @param req + * + * @see https://www.typescripttutorial.net/typescript-tutorial/typescript-type-guards/ + * @see https://www.logicbig.com/tutorials/misc/typescript/type-guards.html + */ +export const isNextApiRequest = (req: NextApiRequest | IncomingMessage): req is NextApiRequest => { + return (req as NextApiRequest).body !== undefined; +}; diff --git a/src/app/types/CommonServerSideParams.ts b/src/app/types/CommonServerSideParams.ts new file mode 100644 index 000000000..31f58f4be --- /dev/null +++ b/src/app/types/CommonServerSideParams.ts @@ -0,0 +1,15 @@ +import { ParsedUrlQuery } from 'querystring'; + +/** + * Server side params provided to any page (SSG or SSR) + * - Static params provided to getStaticProps and getStaticPaths for static pages (when building SSG pages) + * - Dynamic params provided to getServerSideProps (when using SSR) + * + * Those params come from the route (url) being used, they are affected by "redirects" and the route name (e.g: "/folder/[id].tsx" + * + * @see next.config.js "redirects" section for url params + */ +export type CommonServerSideParams = { + albumId?: string; // Used by album-[albumId]-with-ssg-and-fallback page + locale?: string; // The first path of the url is the "locale" +} & E; diff --git a/src/app/types/MultiversalAppBootstrapPageProps.ts b/src/app/types/MultiversalAppBootstrapPageProps.ts new file mode 100644 index 000000000..16a431136 --- /dev/null +++ b/src/app/types/MultiversalAppBootstrapPageProps.ts @@ -0,0 +1,11 @@ +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; +import { i18n } from 'i18next'; + +/** + * Additional props that are injected by MultiversalAppBootstrap to all pages + */ +export type MultiversalAppBootstrapPageProps = { + i18nextInstance: i18n; + isSSGFallbackInitialBuild: boolean; // When true, means the app is loading a SSG page, with fallback mode enabled, and this page hasn't been built before + customerTheme: CustomerTheme; +} diff --git a/src/app/types/MultiversalAppBootstrapProps.ts b/src/app/types/MultiversalAppBootstrapProps.ts new file mode 100644 index 000000000..ab7a57a9a --- /dev/null +++ b/src/app/types/MultiversalAppBootstrapProps.ts @@ -0,0 +1,24 @@ +import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; +import { + NextComponentType, + NextPageContext, +} from 'next'; +import { Router } from 'next/router'; + +/** + * Props that are provided to the render function of the application (in _app) + * Those props can be consolidated by either getInitialProps, getServerProps or getStaticProps, depending on the page and its configuration + * + * @see MultiversalAppBootstrap for usage + */ +export type MultiversalAppBootstrapProps = { + Component?: NextComponentType; // Page component, not provided if pageProps.statusCode is 3xx or 4xx + err?: Error; // Only defined if there was an error + pageProps?: PP; // Props forwarded to the Page component + router?: Router; + + // XXX Next.js internals (unstable API) - See https://github.com/vercel/next.js/discussions/12558#discussioncomment-9177 + __N_SSG?: boolean; // Stands for "server-side generated" or "static site generation", indicates the page was generated through getStaticProps + __N_SSR?: boolean; // Stands for "server-side rendering", indicates the page was generated through getServerSideProps + __N_SSP?: boolean; // Stands for "server-side props" +}; diff --git a/src/app/types/StaticPath.ts b/src/app/types/StaticPath.ts new file mode 100644 index 000000000..929176aa1 --- /dev/null +++ b/src/app/types/StaticPath.ts @@ -0,0 +1,5 @@ +import { CommonServerSideParams } from './CommonServerSideParams'; + +export type StaticPath = { + params: CommonServerSideParams; +} diff --git a/src/app/types/StaticPathsOutput.ts b/src/app/types/StaticPathsOutput.ts new file mode 100644 index 000000000..7c4f476df --- /dev/null +++ b/src/app/types/StaticPathsOutput.ts @@ -0,0 +1,11 @@ +import { CommonServerSideParams } from './CommonServerSideParams'; + +/** + * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation + */ +export type StaticPathsOutput = { + fallback: boolean | 'blocking'; // See https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required + paths: (string | { + params: CommonServerSideParams; + })[]; +} diff --git a/src/app/types/StaticPropsInput.ts b/src/app/types/StaticPropsInput.ts new file mode 100644 index 000000000..6efacba4c --- /dev/null +++ b/src/app/types/StaticPropsInput.ts @@ -0,0 +1,14 @@ +import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; +import { CommonServerSideParams } from './CommonServerSideParams'; + +/** + * Static props given as inputs for getStaticProps + * + * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation + * @see node_modules/next/types/index.d.ts + */ +export type StaticPropsInput = { + params?: CommonServerSideParams; + preview: boolean; + previewData: PreviewData; +} diff --git a/src/common/README.md b/src/common/README.md new file mode 100644 index 000000000..409a7e559 --- /dev/null +++ b/src/common/README.md @@ -0,0 +1,10 @@ +Common +=== + +> Check out the [documentation about the folder structure](../README.md#common-folder) + +Summary: + +- This folder uses an MVC-ish design pattern, where you split your files in separated folders, depending on their kind. +- This folder is great to quickly write some piece of code, you don't need to think a lot about how organized your code should be, and can get started quickly. +- If you don't know or are unsure whether to go for `common` or `modules`, pick `common`. You can always change your mind later. diff --git a/src/common/components/ComponentTemplate.tsx b/src/common/components/ComponentTemplate.tsx new file mode 100644 index 000000000..2c63798ae --- /dev/null +++ b/src/common/components/ComponentTemplate.tsx @@ -0,0 +1,23 @@ +import { css } from '@emotion/react'; +import React from 'react'; + +type Props = {} + +/** + * This component is a template meant to be duplicated to quickly get started with new React components. + */ +const ComponentTemplate: React.FunctionComponent = (props): JSX.Element => { + return ( +
+ This component is a template meant to be duplicated to quickly get started with new React components.
+
+ Feel free to adapt it at your convenience +
+ ); +}; + +export default ComponentTemplate; diff --git a/src/common/components/animations/Animated3Dots.tsx b/src/common/components/animations/Animated3Dots.tsx new file mode 100644 index 000000000..d498dc5e4 --- /dev/null +++ b/src/common/components/animations/Animated3Dots.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +type Props = { + /** + * Color of the dots. + * + * @default white + */ + fill?: string; +}; + +/** + * An animated composant featuring 3 animated dots "...". + * + * Each dot is animated separately, in alternation. + * Requires animate.css library. + * + * @see https://animate.style + */ +const Animated3Dots: React.FunctionComponent = (props): JSX.Element => { + return ( + + + + + + ); +}; + +export default Animated3Dots; diff --git a/src/components/svg/AnimatedLoader.tsx b/src/common/components/animations/AnimatedLoader.tsx similarity index 72% rename from src/components/svg/AnimatedLoader.tsx rename to src/common/components/animations/AnimatedLoader.tsx index 9fa6241ae..a2922f689 100644 --- a/src/components/svg/AnimatedLoader.tsx +++ b/src/common/components/animations/AnimatedLoader.tsx @@ -1,6 +1,8 @@ import React from 'react'; -const AnimatedLoader = props => { +type Props = {}; + +const AnimatedLoader: React.FunctionComponent = (props): JSX.Element => { return ( { }} {...props} > - + ); diff --git a/src/common/components/animations/AnimatedTextBubble.tsx b/src/common/components/animations/AnimatedTextBubble.tsx new file mode 100644 index 000000000..16f357312 --- /dev/null +++ b/src/common/components/animations/AnimatedTextBubble.tsx @@ -0,0 +1,70 @@ +import { + css, + useTheme, +} from '@emotion/react'; +import React from 'react'; + +type Props = { + /** + * Color of the dots. + * + * @default + */ + fill?: string; +}; + +const AnimatedTextBubble: React.FunctionComponent = (props) => { + const theme = useTheme(); + const { surfaceColor } = theme; + + return ( +
+ + + + + + +
+ ); +}; + +export default AnimatedTextBubble; diff --git a/src/components/Loader.tsx b/src/common/components/animations/Loader.tsx similarity index 55% rename from src/components/Loader.tsx rename to src/common/components/animations/Loader.tsx index e3f8a6a9b..808f98429 100644 --- a/src/components/Loader.tsx +++ b/src/common/components/animations/Loader.tsx @@ -1,10 +1,10 @@ -/** @jsx jsx */ -import { css, jsx } from '@emotion/core'; +import { css } from '@emotion/react'; import React from 'react'; +import AnimatedLoader from './AnimatedLoader'; -import AnimatedLoader from '../components/svg/AnimatedLoader'; +type Props = {} -const Loader: React.FunctionComponent = (props: Prop): JSX.Element => { +const Loader: React.FunctionComponent = (props): JSX.Element => { return (
= (props: Prop): JSX.Element => { ); }; -type Prop = {} - export default Loader; diff --git a/src/common/components/countryFlags/EnglishHybridFlag.tsx b/src/common/components/countryFlags/EnglishHybridFlag.tsx new file mode 100644 index 000000000..797d53f68 --- /dev/null +++ b/src/common/components/countryFlags/EnglishHybridFlag.tsx @@ -0,0 +1,401 @@ +import React from 'react'; + +type Props = {} & React.SVGProps; + +const SvgEnglishHybridFlag = (props: Props): JSX.Element => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SvgEnglishHybridFlag; diff --git a/src/common/components/countryFlags/EnglishUkFlag.tsx b/src/common/components/countryFlags/EnglishUkFlag.tsx new file mode 100644 index 000000000..72d45f274 --- /dev/null +++ b/src/common/components/countryFlags/EnglishUkFlag.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +type Props = {} & React.SVGProps; + +const SvgEnglishUkFlag = (props: Props): JSX.Element => { + return ( + + + + + + + + + + + + + + ); +}; + +export default SvgEnglishUkFlag; diff --git a/src/common/components/countryFlags/FrenchFlag.tsx b/src/common/components/countryFlags/FrenchFlag.tsx new file mode 100644 index 000000000..7f1294fc6 --- /dev/null +++ b/src/common/components/countryFlags/FrenchFlag.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +type Props = {} & React.SVGProps; + +const SvgFrenchFlag = (props: Props): JSX.Element => { + return ( + + + + + ); +}; + +export default SvgFrenchFlag; diff --git a/src/common/components/dataDisplay/AllProducts.tsx b/src/common/components/dataDisplay/AllProducts.tsx new file mode 100644 index 000000000..02176ef43 --- /dev/null +++ b/src/common/components/dataDisplay/AllProducts.tsx @@ -0,0 +1,43 @@ +import { Product } from '@/modules/core/data/types/Product'; +import filter from 'lodash.filter'; +import map from 'lodash.map'; +import React from 'react'; +import Products from './Products'; + +type Props = { + products: Product[]; +} + +const AllProducts: React.FunctionComponent = (props) => { + const { products } = props; + const productsPublished = []; + map(products, (productInStage: { documentInStages: Product[] } & Product) => { + const productsInStage: Product[] = productInStage?.documentInStages; // Contains an array of 1 element, when there is a PUBLISHED record (otherwise is empty) + map(productsInStage, (product: Product) => { + if (product?.stage === 'PUBLISHED') { + productsPublished.push(product); + } + }); + }); + const productsDraft = filter(products, { stage: 'DRAFT' }); + + return ( + <> +

Published products

+ + + +
+ +

Draft products

+ + + + ); +}; + +export default AllProducts; diff --git a/src/common/components/dataDisplay/Btn.tsx b/src/common/components/dataDisplay/Btn.tsx new file mode 100644 index 000000000..4939b33d7 --- /dev/null +++ b/src/common/components/dataDisplay/Btn.tsx @@ -0,0 +1,86 @@ +import { ReactButtonProps } from '@/modules/core/react/types/ReactButtonProps'; +import { + ComponentThemeMode, + resolveThemedComponentColors, + ThemedComponentProps, +} from '@/modules/core/theming/themedComponentColors'; +import { + css, + useTheme, +} from '@emotion/react'; +import classnames from 'classnames'; +import React, { ReactNode } from 'react'; + +type Props = { + /** + * What's being displayed within the button. + */ + children: ReactNode; + + /** + * Always adds the "btn" class, more CSS classes can be added. + */ + className?: string; +} & ReactButtonProps & ThemedComponentProps; + +/** + * Flexible HTML `button` component that can take many shapes and be used with various colors and background colors. + * + * Implements `ThemedComponentColors`. + */ +const Btn: React.FunctionComponent = (props): JSX.Element => { + const { + children, + className, + mode = 'primary' as ComponentThemeMode, + isTransparent, + ...rest + } = props; + const customerTheme = useTheme(); + const { + color, + backgroundColor, + borderColor, + hoverColor, + hoverBackgroundColor, + hoverBorderColor, + hoverBoxShadowColor, + } = resolveThemedComponentColors(customerTheme, mode, isTransparent); + + return ( + + ); +}; + +export default Btn; diff --git a/src/common/components/dataDisplay/BubbleTimer.tsx b/src/common/components/dataDisplay/BubbleTimer.tsx new file mode 100644 index 000000000..844b42d2c --- /dev/null +++ b/src/common/components/dataDisplay/BubbleTimer.tsx @@ -0,0 +1,48 @@ +import { CSSStyles } from '@/modules/core/css/types/CSSStyles'; +import React, { + Fragment, + useEffect, + useState, +} from 'react'; +import AnimatedTextBubble from '../animations/AnimatedTextBubble'; + +type Props = { + children: React.ReactElement; + /** + * @default 200 + */ + duration?: number; + className?: string; + style?: CSSStyles; + fill?: string; +} + +const BubbleTimer: React.FunctionComponent = (props): JSX.Element => { + const { + children, + duration = 200, + ...rest + } = props; + const [isWaiting, setIsWaiting] = useState(true); + + useEffect(() => { + const timerFunction = setTimeout(() => setIsWaiting(false), duration); + return (): void => clearTimeout(timerFunction); // See https://upmostly.com/tutorials/settimeout-in-react-components-using-hooks + }, [duration]); + + return ( + + { + isWaiting ? ( + + ) : ( + + {children} + + ) + } + + ); +}; + +export default BubbleTimer; diff --git a/src/common/components/dataDisplay/Cards.tsx b/src/common/components/dataDisplay/Cards.tsx new file mode 100644 index 000000000..794f3a5e6 --- /dev/null +++ b/src/common/components/dataDisplay/Cards.tsx @@ -0,0 +1,76 @@ +import { css } from '@emotion/react'; +import classnames from 'classnames'; +import React, { ReactNode } from 'react'; +import { CardDeck } from 'reactstrap'; + +type Props = { + /** + * Must be an array of `Card`. + */ + children: ReactNode; + + /** + * Max cards per row. + * Current implementation only support `2 | 3`. + * + * @default 3 + */ + maxCards?: number; +} + +/** + * Wrapper for Reactstrap `Card` component, to display cards as a Deck and apply common styling on all cards. + * + * @param props + */ +const Cards: React.FunctionComponent = (props): JSX.Element => { + const { + children, + maxCards = 3, + } = props; + + return ( + + {children} + + ); +}; + +export default Cards; diff --git a/src/common/components/dataDisplay/CircleBtn.tsx b/src/common/components/dataDisplay/CircleBtn.tsx new file mode 100644 index 000000000..197e12985 --- /dev/null +++ b/src/common/components/dataDisplay/CircleBtn.tsx @@ -0,0 +1,108 @@ +import { ReactDivProps } from '@/modules/core/react/types/ReactDivProps'; +import { + ComponentThemeMode, + resolveThemedComponentColors, + ThemedComponentProps, +} from '@/modules/core/theming/themedComponentColors'; +import { + css, + useTheme, +} from '@emotion/react'; +import React from 'react'; + +type Props = { + /** + * React children, usually text. + */ + children: React.ReactNode; + + /** + * CSS margin. Can be pixels, em, etc. + * + * @default 0px + */ + margin?: string; +} & ReactDivProps & ThemedComponentProps; + +/** + * Displays a 1-3 characters, or an icon, within a as-close-as-perfect circle. + * + * Example: Can be used in replacement of Reactstrap Badge. + * + * Implements `ThemedComponentColors`. + */ +const CircleBtn: React.FunctionComponent = (props): JSX.Element => { + const { + children, + margin = '0', + mode = 'primary-reverse' as ComponentThemeMode, + isTransparent, + ...rest + } = props; + const customerTheme = useTheme(); + const { + color, + backgroundColor, + borderColor, + } = resolveThemedComponentColors(customerTheme, mode, isTransparent); + + return ( +