mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-31 11:49:10 +00:00
Compare commits
3 Commits
@vercel/cl
...
Fix-Packag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59f51a1bb6 | ||
|
|
494164a3a4 | ||
|
|
b45052adda |
@@ -1,8 +0,0 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": [
|
||||
"@svitejs/changesets-changelog-github-compact",
|
||||
{ "repo": "vercel/vercel" }
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -26,6 +26,12 @@ packages/hydrogen/edge-entry.js
|
||||
packages/next/test/integration/middleware
|
||||
packages/next/test/integration/middleware-eval
|
||||
|
||||
# node-bridge
|
||||
packages/node-bridge/bridge.js
|
||||
packages/node-bridge/launcher.js
|
||||
packages/node-bridge/helpers.js
|
||||
packages/node-bridge/source-map-support.js
|
||||
|
||||
# middleware
|
||||
packages/middleware/src/entries.js
|
||||
|
||||
|
||||
24
.github/CODEOWNERS
vendored
24
.github/CODEOWNERS
vendored
@@ -1,18 +1,16 @@
|
||||
# Documentation
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
# Restricted Paths
|
||||
* @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @trek
|
||||
/.github/workflows @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @trek @ijjk
|
||||
/packages/fs-detectors @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @trek @agadzik @chloetedder
|
||||
/packages/next @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @trek @ijjk @ztanner
|
||||
/packages/routing-utils @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @trek @ijjk
|
||||
/packages/static-build @TooTallNate @EndangeredMassa @cb1kenobi @Ethan-Arrowood @trek
|
||||
/packages/edge @vercel/compute @TooTallNate @EndangeredMassa @cb1kenobi @Ethan-Arrowood @trek
|
||||
* @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood
|
||||
/.github/workflows @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
|
||||
/packages/fs-detectors @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @agadzik @chloetedder
|
||||
/packages/node-bridge @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
|
||||
/packages/next @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
|
||||
/packages/routing-utils @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @ijjk
|
||||
/packages/edge @vercel/edge-compute
|
||||
/examples @leerob
|
||||
/examples/create-react-app @Timer
|
||||
/examples/nextjs @timneutkens @ijjk @styfle @ztanner @huozhi
|
||||
/packages/node @TooTallNate @EndangeredMassa @styfle @cb1kenobi @Ethan-Arrowood @trek @Kikobeats
|
||||
|
||||
# Unrestricted Paths
|
||||
.changeset/
|
||||
/examples/nextjs @timneutkens @ijjk @styfle
|
||||
/examples/hugo @styfle
|
||||
/examples/jekyll @styfle
|
||||
/examples/zola @styfle
|
||||
|
||||
1
.github/CONTRIBUTING.md
vendored
1
.github/CONTRIBUTING.md
vendored
@@ -15,6 +15,7 @@ git clone https://github.com/vercel/vercel
|
||||
cd vercel
|
||||
corepack enable
|
||||
pnpm install
|
||||
pnpm bootstrap
|
||||
pnpm build
|
||||
pnpm lint
|
||||
pnpm test-unit
|
||||
|
||||
12
.github/DISCUSSION_TEMPLATE/general.yml
vendored
12
.github/DISCUSSION_TEMPLATE/general.yml
vendored
@@ -1,12 +0,0 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Note**: This category is intended for discussions related to Vercel CLI or Runtimes.
|
||||
|
||||
If you post in this repository seeking help with other Vercel tools and features, it may be missed by our support team. For help with topics other than the CLI and Runtimes, please visit the [Vercel Community](https://github.com/orgs/vercel/discussions).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
validations:
|
||||
required: true
|
||||
12
.github/DISCUSSION_TEMPLATE/help.yml
vendored
12
.github/DISCUSSION_TEMPLATE/help.yml
vendored
@@ -1,12 +0,0 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Note**: This category is intended for discussions related to Vercel CLI or Runtimes.
|
||||
|
||||
If you post in this repository seeking help with other Vercel tools and features, it may be missed by our support team. For help with topics other than the CLI and Runtimes, please visit the [Vercel Community](https://github.com/orgs/vercel/discussions).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Question
|
||||
validations:
|
||||
required: true
|
||||
12
.github/DISCUSSION_TEMPLATE/ideas.yml
vendored
12
.github/DISCUSSION_TEMPLATE/ideas.yml
vendored
@@ -1,12 +0,0 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Note**: This category is intended for sharing ideas related to Vercel CLI or Runtimes.
|
||||
|
||||
Please visit the [Vercel Community](https://github.com/orgs/vercel/discussions) to share ideas for other Vercel tools and features.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Idea
|
||||
validations:
|
||||
required: true
|
||||
12
.github/DISCUSSION_TEMPLATE/show-and-tell.yml
vendored
12
.github/DISCUSSION_TEMPLATE/show-and-tell.yml
vendored
@@ -1,12 +0,0 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Note**: This category is intended for discussions related to Vercel CLI or Runtimes.
|
||||
|
||||
For topics related to other Vercel features, please visit the [Vercel Community](https://github.com/orgs/vercel/discussions).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
validations:
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -4,8 +4,8 @@ contact_links:
|
||||
url: https://vercel.com/support/request
|
||||
about: Report a bug using the Vercel support form
|
||||
- name: Feature Request
|
||||
url: https://github.com/orgs/vercel/discussions/new?category=ideas
|
||||
url: https://github.com/vercel/vercel/discussions/new?category=ideas
|
||||
about: Share ideas for new features
|
||||
- name: Ask a Question
|
||||
url: https://github.com/orgs/vercel/discussions/new?category=help
|
||||
url: https://github.com/vercel/vercel/discussions/new?category=help
|
||||
about: Ask the community for help
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Cron Update Gatsby Fixtures
|
||||
|
||||
on:
|
||||
# Allow manual runs
|
||||
workflow_dispatch:
|
||||
# Run once a week https://crontab.guru/once-a-week
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
update-gatsby-fixtures:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
# 0 means fetch all commits so we can commit and push in the script below
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Enable corepack
|
||||
run: corepack enable pnpm
|
||||
- name: Update Gatsby Fixtures
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
# See https://github.com/actions/github-script#run-a-separate-file-with-an-async-function
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
script: |
|
||||
const script = require('./utils/update-gatsby-fixtures.js')
|
||||
await script({ github, context })
|
||||
4
.github/workflows/cron-update-next.yml
vendored
4
.github/workflows/cron-update-next.yml
vendored
@@ -16,16 +16,12 @@ jobs:
|
||||
# 0 means fetch all commits so we can commit and push in the script below
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Enable corepack
|
||||
run: corepack enable pnpm
|
||||
- name: Create Pull Request
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
# See https://github.com/actions/github-script#run-a-separate-file-with-an-async-function
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
script: |
|
||||
const script = require('./utils/update-next.js')
|
||||
await script({ github, context })
|
||||
|
||||
6
.github/workflows/cron-update-turbo.yml
vendored
6
.github/workflows/cron-update-turbo.yml
vendored
@@ -16,16 +16,14 @@ jobs:
|
||||
# 0 means fetch all commits so we can commit and push in the script below
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Enable corepack
|
||||
run: corepack enable pnpm
|
||||
- name: install pnpm@7.26.0
|
||||
run: npm i -g pnpm@7.26.0
|
||||
- name: Create Pull Request
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
# See https://github.com/actions/github-script#run-a-separate-file-with-an-async-function
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
script: |
|
||||
const script = require('./utils/update-turbo.js')
|
||||
await script({ github, context })
|
||||
|
||||
4
.github/workflows/label-feature-request.yml
vendored
4
.github/workflows/label-feature-request.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Thank you for taking the time to created this request!
|
||||
|
||||
|
||||
We added it to our backlog but need to discuss design/architecture before we can accept a PR.
|
||||
|
||||
|
||||
Please let us know if you would be interested in sending a PR once we approve the design.
|
||||
|
||||
6
.github/workflows/label-support.yml
vendored
6
.github/workflows/label-support.yml
vendored
@@ -16,9 +16,9 @@ jobs:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
comment: |
|
||||
Thank you so much for filing this issue.
|
||||
|
||||
|
||||
We do try to keep issues in this repository focused on the vercel command line and related code.
|
||||
|
||||
|
||||
At this point we think that this issue is best handled by our friendly Vercel support team. They can be found by contacting them at: https://vercel.com/help#issues
|
||||
|
||||
|
||||
If you think closing of this is a mistake, then please re-open this issue and we'll take another look :bow:
|
||||
|
||||
6
.github/workflows/label-triaged.yml
vendored
6
.github/workflows/label-triaged.yml
vendored
@@ -16,9 +16,9 @@ jobs:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Thank you for taking the time to file this issue!
|
||||
|
||||
|
||||
We have confirmed this is a bug and added it to our backlog.
|
||||
|
||||
|
||||
We don't have a timeline for when this issue will be fixed, but we will accept a Pull Request with a fix and a test.
|
||||
|
||||
|
||||
See the [contributing guidelines](https://github.com/vercel/vercel/blob/main/.github/CONTRIBUTING.md) for more info.
|
||||
|
||||
61
.github/workflows/publish.yml
vendored
Normal file
61
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '!*'
|
||||
|
||||
env:
|
||||
TURBO_REMOTE_ONLY: 'true'
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check Release
|
||||
id: check-release
|
||||
run: |
|
||||
tag="$(git describe --tags --exact-match 2> /dev/null || :)"
|
||||
if [[ -z "$tag" ]];
|
||||
then
|
||||
echo "IS_RELEASE=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "IS_RELEASE=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Setup Go
|
||||
if: ${{ steps.check-release.outputs.IS_RELEASE == 'true' }}
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.13.15'
|
||||
- name: Setup Node
|
||||
if: ${{ steps.check-release.outputs.IS_RELEASE == 'true' }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: install pnpm@7.24.2
|
||||
run: npm i -g pnpm@7.24.2
|
||||
- name: Install
|
||||
if: ${{ steps.check-release.outputs.IS_RELEASE == 'true' }}
|
||||
run: pnpm install
|
||||
- name: Build
|
||||
if: ${{ steps.check-release.outputs.IS_RELEASE == 'true' }}
|
||||
run: pnpm build
|
||||
env:
|
||||
GA_TRACKING_ID: ${{ secrets.GA_TRACKING_ID }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
- name: Publish
|
||||
if: ${{ steps.check-release.outputs.IS_RELEASE == 'true' }}
|
||||
run: pnpm publish-from-github
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
|
||||
GA_TRACKING_ID: ${{ secrets.GA_TRACKING_ID }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
78
.github/workflows/release.yml
vendored
78
.github/workflows/release.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
TURBO_REMOTE_ONLY: 'true'
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Fetch git tags
|
||||
run: git fetch origin 'refs/tags/*:refs/tags/*'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: install npm@9
|
||||
run: npm i -g npm@9
|
||||
|
||||
- name: install pnpm@8.3.1
|
||||
run: npm i -g pnpm@8.3.1
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Packages
|
||||
run: pnpm build
|
||||
env:
|
||||
GA_TRACKING_ID: ${{ secrets.GA_TRACKING_ID }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm ci:version
|
||||
publish: pnpm ci:publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
NPM_CONFIG_PROVENANCE: 'true'
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
|
||||
GA_TRACKING_ID: ${{ secrets.GA_TRACKING_ID }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
|
||||
- name: Trigger Update (if a Publish Happened)
|
||||
if: steps.changesets.outputs.published == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
script: |
|
||||
const script = require('./utils/trigger-update-workflow.js')
|
||||
await script({ github, context })
|
||||
|
||||
- name: Set latest Release to `vercel` (if a Publish Happened)
|
||||
if: steps.changesets.outputs.published == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
script: |
|
||||
const script = require('./utils/update-latest-release.js')
|
||||
await script({ github, context })
|
||||
26
.github/workflows/required-pr-label.yml
vendored
Normal file
26
.github/workflows/required-pr-label.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Required PR Label
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, labeled, unlabeled, synchronize]
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Labels
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let missing = false;
|
||||
const labels = context.payload.pull_request.labels.map(l => l.name);
|
||||
if (labels.filter(l => l.startsWith('area:')).length === 0) {
|
||||
console.error('::error::Missing label: Please add at least one "area" label.');
|
||||
missing = true;
|
||||
}
|
||||
if (labels.filter(l => l.startsWith('semver:')).length !== 1) {
|
||||
console.error('::error::Missing label: Please add exactly one "semver" label.');
|
||||
missing = true;
|
||||
}
|
||||
if (missing) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('::notice::Success: This pull request has correct labels, thanks!');
|
||||
47
.github/workflows/test-integration-cli.yml
vendored
Normal file
47
.github/workflows/test-integration-cli.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '!*'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
TURBO_REMOTE_ONLY: 'true'
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: CLI
|
||||
timeout-minutes: 40
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node: [16]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: install pnpm@7.24.2
|
||||
run: npm i -g pnpm@7.24.2
|
||||
- run: pnpm install
|
||||
- run: pnpm run build
|
||||
- run: pnpm test-cli
|
||||
env:
|
||||
VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }}
|
||||
VERCEL_TEST_REGISTRATION_URL: ${{ secrets.VERCEL_TEST_REGISTRATION_URL }}
|
||||
53
.github/workflows/test-lint.yml
vendored
53
.github/workflows/test-lint.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '!*'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
TURBO_REMOTE_ONLY: 'true'
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
NODE_VERSION: '16'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
enforce-changeset:
|
||||
name: Enforce Changeset
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.title != 'Version Packages'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
- run: git checkout ${{ github.event.pull_request.head.ref }}
|
||||
- name: install pnpm@8.3.1
|
||||
run: npm i -g pnpm@8.3.1
|
||||
- run: pnpm install
|
||||
# Enforce a changeset file to be present
|
||||
- run: pnpm exec changeset status --since=main
|
||||
|
||||
test:
|
||||
name: Lint
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: install pnpm@8.3.1
|
||||
run: npm i -g pnpm@8.3.1
|
||||
- run: pnpm install
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run prettier-check
|
||||
- run: pnpm run build
|
||||
- run: pnpm run type-check
|
||||
50
.github/workflows/test-unit.yml
vendored
Normal file
50
.github/workflows/test-unit.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Unit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '!*'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
TURBO_REMOTE_ONLY: 'true'
|
||||
TURBO_TEAM: 'vercel'
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Unit
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: [16]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.13.15'
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: install pnpm@7.24.2
|
||||
run: npm i -g pnpm@7.24.2
|
||||
- run: pnpm install
|
||||
- run: pnpm run build
|
||||
- run: pnpm run lint
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node == 14 # only run lint once
|
||||
- run: pnpm run test-unit
|
||||
- run: pnpm -C packages/cli run coverage
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node == 14 # only run coverage once
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
68
.github/workflows/test.yml
vendored
68
.github/workflows/test.yml
vendored
@@ -3,9 +3,9 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- main
|
||||
tags:
|
||||
- '!*'
|
||||
- '!*'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
@@ -29,11 +29,14 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.13.15'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: install pnpm@8.3.1
|
||||
run: npm i -g pnpm@8.3.1
|
||||
- name: install pnpm@7.24.2
|
||||
run: npm i -g pnpm@7.24.2
|
||||
- run: pnpm install
|
||||
- id: set-tests
|
||||
run: |
|
||||
@@ -41,12 +44,13 @@ jobs:
|
||||
echo "Files to test:"
|
||||
echo "$TESTS_ARRAY"
|
||||
echo "tests=$TESTS_ARRAY" >> $GITHUB_OUTPUT
|
||||
- uses: patrickedqvist/wait-for-vercel-preview@bfdff514ff78a669f2536e9f4dd4ef5813a704a2
|
||||
- uses: patrickedqvist/wait-for-vercel-preview@ae34b392ef30297f2b672f9afb3c329bde9bd487
|
||||
id: waitForTarball
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
max_timeout: 360
|
||||
check_interval: 5
|
||||
|
||||
test:
|
||||
timeout-minutes: 120
|
||||
runs-on: ${{ matrix.runner }}
|
||||
@@ -62,68 +66,40 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.13.15'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.nodeVersion || env.NODE_VERSION }}
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install Hugo
|
||||
if: matrix.runner == 'macos-latest'
|
||||
run: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/cli/test/dev/fixtures/08-hugo/
|
||||
|
||||
- name: install pnpm@8.3.1
|
||||
run: npm i -g pnpm@8.3.1
|
||||
- name: install pnpm@7.24.2
|
||||
run: npm i -g pnpm@7.24.2
|
||||
|
||||
- run: pnpm install
|
||||
- name: fetch ssl certificate before build (linux, os x)
|
||||
if: matrix.runner != 'windows-latest'
|
||||
run: echo | openssl s_client -showcerts -servername 'api.vercel.com' -connect 76.76.21.21:443
|
||||
|
||||
- name: Build ${{matrix.packageName}} and all its dependencies
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run build --cache-dir=".turbo" --log-order=stream --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
|
||||
env:
|
||||
FORCE_COLOR: '1'
|
||||
- name: Test ${{matrix.packageName}}
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run test --summarize --cache-dir=".turbo" --log-order=stream --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
run: node utils/gen.js && node_modules/.bin/turbo run test --cache-dir=".turbo" --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
|
||||
shell: bash
|
||||
env:
|
||||
JEST_JUNIT_OUTPUT_FILE: ${{github.workspace}}/.junit-reports/${{matrix.scriptName}}-${{matrix.packageName}}-${{matrix.chunkNumber}}-${{ matrix.runner }}.xml
|
||||
VERCEL_CLI_VERSION: ${{ needs.setup.outputs.dplUrl }}/tarballs/vercel.tgz
|
||||
VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }}
|
||||
VERCEL_TEST_REGISTRATION_URL: ${{ secrets.VERCEL_TEST_REGISTRATION_URL }}
|
||||
FORCE_COLOR: '1'
|
||||
- name: 'Determine Turbo HIT or MISS'
|
||||
if: ${{ !cancelled() }}
|
||||
id: turbo-summary
|
||||
shell: bash
|
||||
run: |
|
||||
TURBO_MISS_COUNT=`node utils/determine-turbo-hit-or-miss.js`
|
||||
echo "MISS COUNT: $TURBO_MISS_COUNT"
|
||||
echo "misses=$TURBO_MISS_COUNT" >> $GITHUB_OUTPUT
|
||||
- name: fetch ssl certificate after tests (linux, os x)
|
||||
if: matrix.runner != 'windows-latest'
|
||||
run: echo | openssl s_client -showcerts -servername 'api.vercel.com' -connect 76.76.21.21:443
|
||||
- name: 'Upload Test Report to Datadog'
|
||||
if: ${{ steps['turbo-summary'].outputs.misses != '0' && !cancelled() }}
|
||||
run: 'npx @datadog/datadog-ci@2.18.1 junit upload --service vercel-cli .junit-reports'
|
||||
env:
|
||||
DATADOG_API_KEY: ${{secrets.DATADOG_API_KEY_CLI}}
|
||||
DD_ENV: ci
|
||||
|
||||
summary:
|
||||
name: Summary
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
if: always()
|
||||
conclusion:
|
||||
needs:
|
||||
- test
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E
|
||||
steps:
|
||||
- name: Check All
|
||||
run: |-
|
||||
for status in ${{ join(needs.*.result, ' ') }}
|
||||
do
|
||||
if [ "$status" != "success" ] && [ "$status" != "skipped" ]
|
||||
then
|
||||
echo "Some checks failed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- name: Done
|
||||
run: echo "Done."
|
||||
|
||||
30
.github/workflows/update-remix-run-dev.yml
vendored
30
.github/workflows/update-remix-run-dev.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: Update @remix-run/dev
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
new-version:
|
||||
type: string
|
||||
description: 'Optional version to update @remix-run/dev to inside of @vercel/remix-builder'
|
||||
|
||||
jobs:
|
||||
update-remix-run-dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Enable corepack
|
||||
run: corepack enable pnpm
|
||||
- name: Update @remix-run/dev
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN_PULL_REQUESTS }}
|
||||
script: |
|
||||
const script = require('./utils/update-remix-run-dev.js')
|
||||
await script({ github, context }, "${{ inputs.new-version }}")
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,4 +31,3 @@ __pycache__
|
||||
.turbo
|
||||
.eslintcache
|
||||
turbo-cache-key.json
|
||||
junit.xml
|
||||
1
.npmrc
1
.npmrc
@@ -1,4 +1,3 @@
|
||||
provenance=true
|
||||
save-exact=true
|
||||
hoist-pattern[]=!"**/@types/**"
|
||||
hoist-pattern[]=!"**/typedoc"
|
||||
|
||||
@@ -7,29 +7,3 @@ examples/sveltekit-1
|
||||
|
||||
# gatsby-plugin-vercel-analytics
|
||||
packages/gatsby-plugin-vercel-analytics
|
||||
|
||||
# ignore directories that are not source code
|
||||
node_modules
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
.changeset
|
||||
.vscode
|
||||
.DS_Store
|
||||
.next
|
||||
.vercel
|
||||
.turbo
|
||||
.eslintcache
|
||||
.output
|
||||
.vercel_build_output
|
||||
.vercel
|
||||
coverage
|
||||
turbo-cache-key.json
|
||||
/examples
|
||||
/public
|
||||
packages/*/dist
|
||||
packages/*/node_modules
|
||||
packages/**/test/fixtures
|
||||
packages/**/test/dev/fixtures
|
||||
packages/**/test/build-fixtures
|
||||
packages/**/test/cache-fixtures
|
||||
packages/cli/src/util/dev/templates/*.ts
|
||||
|
||||
@@ -79,23 +79,18 @@ project. An example use-case is that `@vercel/node` uses this function to cache
|
||||
the `node_modules` directory, making it faster to install npm dependencies for
|
||||
future builds.
|
||||
|
||||
> Note: Only files within the repo root directory can be cached.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
import { relative } from 'path';
|
||||
import { glob, PrepareCache } from '@vercel/build-utils';
|
||||
import { PrepareCacheOptions } from '@vercel/build-utils';
|
||||
|
||||
export const prepareCache: PrepareCache = async ({
|
||||
workPath,
|
||||
repoRootPath,
|
||||
}) => {
|
||||
export async function prepareCache(options: PrepareCacheOptions) {
|
||||
// Create a mapping of file names and `File` object instances to cache here…
|
||||
const rootDirectory = relative(repoRootPath, workPath);
|
||||
const cache = await glob(`${rootDirectory}/some/dir/**`, repoRootPath);
|
||||
return cache;
|
||||
};
|
||||
|
||||
return {
|
||||
'path-to-file': File,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### `shouldServe()`
|
||||
@@ -387,6 +382,7 @@ This is an abstract enumeration type that is implemented by one of the following
|
||||
|
||||
- `nodejs18.x`
|
||||
- `nodejs16.x`
|
||||
- `nodejs14.x`
|
||||
- `go1.x`
|
||||
- `java11`
|
||||
- `python3.9`
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
|
||||
## Vercel
|
||||
|
||||
Vercel's frontend cloud gives developers frameworks, workflows, and infrastructure to build a faster, more personalized web.
|
||||
Vercel is the platform for frontend developers, providing the speed and reliability innovators need to create at the moment of inspiration.
|
||||
|
||||
We enable teams to iterate quickly and develop, preview, and ship delightful user experiences. Vercel has zero-configuration support for 35+ frontend frameworks and integrates with your headless content, commerce, or database of choice.
|
||||
|
||||
## Deploy
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# api
|
||||
|
||||
## 0.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix RSC matching behavior & 404 status code on `fallback: false` ([#10388](https://github.com/vercel/vercel/pull/10388))
|
||||
@@ -51,11 +51,8 @@ export async function getGitHubRepoInfo(repo: Repo) {
|
||||
data.subdir = repo.path.slice(subdirPath.length).split('/');
|
||||
}
|
||||
|
||||
if (
|
||||
data.id === 'vercel/vercel' &&
|
||||
data.subdir &&
|
||||
data.subdir[0] === 'examples'
|
||||
) {
|
||||
if (data.id === 'vercel/vercel' && data.subdir && data.subdir[0] === 'examples') {
|
||||
|
||||
// from our examples, add `homepage` and `description` fields
|
||||
const example = data.subdir[1];
|
||||
const exampleList = await getExampleList();
|
||||
|
||||
@@ -33,12 +33,7 @@ async function main() {
|
||||
});
|
||||
|
||||
const existingExamples = exampleDirs
|
||||
.filter(
|
||||
dir =>
|
||||
dir.isDirectory() &&
|
||||
dir.name !== 'node_modules' &&
|
||||
dir.name !== '__tests__'
|
||||
)
|
||||
.filter(dir => dir.isDirectory())
|
||||
.map(dir => ({
|
||||
name: dir.name,
|
||||
visible: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "api",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.0",
|
||||
"description": "API for the vercel/vercel repo",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
@@ -16,6 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "16.18.11",
|
||||
"@types/node-fetch": "2.5.4",
|
||||
"@vercel/node": "*"
|
||||
"@vercel/node": "*",
|
||||
"typescript": "4.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"target": "ES2020",
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
10
codecov.yml
Normal file
10
codecov.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
fixes:
|
||||
- "::packages/cli/" # move root e.g., "path/" => "after/path/"
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
You ran `vercel dev` inside a project that contains a `vercel.json` file with `env` or `build.env` properties that use [Vercel Secrets](https://vercel.com/docs/concepts/projects/environment-variables).
|
||||
|
||||
In order to use environment variables in your project locally that have values defined using the Vercel Secrets format (e.g. `@my-secret-value`), you will need to provide the value as an environment variable using a `.env.local`.
|
||||
In order to use environment variables in your project locally that have values defined using the Vercel Secrets format (e.g. `@my-secret-value`), you will need to provide the value as an environment variable using a `.env`.
|
||||
|
||||
We require this to ensure your app works as you intend it to, in the development environment, and to provide you with a way to mirror or separate private environment variables within your applications, for example when connecting to a database.
|
||||
|
||||
@@ -12,11 +12,11 @@ Read below for how to address this error.
|
||||
|
||||
#### Possible Ways to Fix It
|
||||
|
||||
The error message will list environment variables that are required and which file they are required to be included in `.env.local`.
|
||||
The error message will list environment variables that are required and which file they are required to be included in `.env`.
|
||||
|
||||
If the file does not exist yet, please create the file that the error message mentions and insert the missing environment variable into it.
|
||||
|
||||
For example, if the error message shows that the environment variable `TEST` is missing from `.env.local`, then the `.env.local` file should look like this:
|
||||
For example, if the error message shows that the environment variable `TEST` is missing from `.env`, then the `.env` file should look like this:
|
||||
|
||||
```
|
||||
TEST=value
|
||||
|
||||
@@ -14,9 +14,7 @@ In order to create the smallest possible lambdas Next.js has to be configured to
|
||||
npm install next --save
|
||||
```
|
||||
|
||||
2. Check [Node.js Version](https://vercel.link/node-version) in your Project Settings. Using an old or incompatible version of Node.js can cause the Build Step to fail with this error message.
|
||||
|
||||
3. Add the `now-build` script to your `package.json` [deprecated]
|
||||
2. Add the `now-build` script to your `package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -26,7 +24,7 @@ npm install next --save
|
||||
}
|
||||
```
|
||||
|
||||
4. Add `target: 'serverless'` to `next.config.js` [deprecated]
|
||||
3. Add `target: 'serverless'` to `next.config.js` [deprecated]
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
@@ -35,9 +33,9 @@ module.exports = {
|
||||
};
|
||||
```
|
||||
|
||||
5. Remove `distDir` from `next.config.js` as `@vercel/next` can't parse this file and expects your build output at `/.next`
|
||||
4. Remove `distDir` from `next.config.js` as `@vercel/next` can't parse this file and expects your build output at `/.next`
|
||||
|
||||
6. Optionally make sure the `"src"` in `"builds"` points to your application `package.json`
|
||||
5. Optionally make sure the `"src"` in `"builds"` points to your application `package.json`
|
||||
|
||||
```js
|
||||
{
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
#### Why This Error Occurred
|
||||
|
||||
This error is often caused by a misconfigured "Build Command" or "Output Directory" for your Next.js project.
|
||||
This could be caused by a misconfigured "Build Command" or "Output Directory" for your Next.js project.
|
||||
|
||||
#### Possible Ways to Fix It
|
||||
|
||||
In the Vercel dashboard, open your "Project Settings" and draw attention to "Build & Development Settings":
|
||||
|
||||
1. Ensure that the "Build Command" setting is not overridden, or that it calls `next build`. If this command is not overridden but you are seeing this error, double check that your `build` script in `package.json` calls `next build`. If `buildCommand` exists in `vercel.json`, make sure it calls `next build`.
|
||||
2. Ensure that the "Output Directory" setting is not overridden. This value almost never needs to be configured, and is only necessary if you override `distDir` in `next.config.js`. If `outputDirectory` exists in `vercel.json`, remove that property.
|
||||
3. For `next export` users: **do not override the "Output Directory"**, even if you customized the `next export` output directory. It will automatically detects the correct output.
|
||||
1. Ensure that the "Build Command" setting is not changed, or that it calls `next build`. If this command is not changed but you are seeing this error, double check that your `build` script in `package.json` calls `next build`.
|
||||
2. Ensure that the "Output Directory" setting is not changed. This value almost never needs to be configured, and is only necessary if you override `distDir` in `next.config.js`.
|
||||
3. For `next export` users: **do not override the "Output Directory"**. Next.js automatically detects what folder you outputted `next export` to.
|
||||
|
||||
In rare scenarios, this error message can also be caused by a Next.js build failure (if your "Build Command" accidentally returns an exit code that is not 0).
|
||||
Double check for any error messages above the Routes Manifest error, which may provide additional details.
|
||||
|
||||
7
examples/CHANGELOG.md
vendored
7
examples/CHANGELOG.md
vendored
@@ -1,7 +0,0 @@
|
||||
# examples
|
||||
|
||||
## null
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- update examples to use at least node@16 ([#10395](https://github.com/vercel/vercel/pull/10395))
|
||||
@@ -1,5 +0,0 @@
|
||||
import { deployExample } from '../test-utils';
|
||||
it('should deploy', async () => {
|
||||
await deployExample(__filename);
|
||||
});
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { deployExample } from '../test-utils';
|
||||
it('should deploy', async () => {
|
||||
await deployExample(__filename);
|
||||
});
|
||||
@@ -1,14 +1,6 @@
|
||||
# Astro
|
||||
|
||||
This directory is a brief example of an [Astro](https://astro.build/) site that can be deployed to Vercel with zero configuration. This demo showcases:
|
||||
|
||||
- `/` - A static page (pre-rendered)
|
||||
- `/ssr` - A page that uses server-side rendering (through [Vercel Edge Functions](https://vercel.com/docs/functions/edge-functions))
|
||||
- `/ssr-with-swr-caching` - Similar to the previous page, but also caches the response on the [Vercel Edge Network](https://vercel.com/docs/edge-network/overview) using `cache-control` headers
|
||||
- `/image` - Astro [Asset](https://docs.astro.build/en/guides/assets/) using Vercel [Image Optimization](https://vercel.com/docs/image-optimization)
|
||||
- `/edge.json` - An Astro API Endpoint that returns JSON data using [Vercel Edge Functions](https://vercel.com/docs/functions/edge-functions)
|
||||
|
||||
Learn more about [Astro on Vercel](https://vercel.com/docs/frameworks/astro).
|
||||
This directory is a brief example of an [Astro](https://astro.build/) site that can be deployed to Vercel with zero configuration.
|
||||
|
||||
## Deploy Your Own
|
||||
|
||||
@@ -20,7 +12,21 @@ _Live Example: https://astro-template.vercel.app_
|
||||
|
||||
## Project Structure
|
||||
|
||||
Astro looks for `.astro`, `.md`, or `.js` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
/
|
||||
├── public/
|
||||
│ └── favicon.ico
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ └── Layout.astro
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components or layouts.
|
||||
|
||||
@@ -32,10 +38,9 @@ All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :--------------------- | :------------------------------------------------- |
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm run dev` | Starts local dev server at `localhost:3000` |
|
||||
| `pnpm run build` | Build your production site to `./dist/` |
|
||||
| `pnpm run preview` | Preview your build locally, before deploying |
|
||||
| `pnpm run start` | Starts a production dev server at `localhost:3000` |
|
||||
| `pnpm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
|
||||
| `pnpm run astro --help` | Get help using the Astro CLI |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
|
||||
| `npm run astro --help` | Get help using the Astro CLI |
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
// Use Vercel Edge Functions (Recommended)
|
||||
import vercel from '@astrojs/vercel/edge';
|
||||
// Can also use Serverless Functions
|
||||
// import vercel from '@astrojs/vercel/serverless';
|
||||
// Or a completely static build
|
||||
// import vercel from '@astrojs/vercel/static';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
experimental: {
|
||||
assets: true
|
||||
},
|
||||
adapter: vercel({
|
||||
imageService: true,
|
||||
}),
|
||||
});
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/vercel": "3.8.2",
|
||||
"astro": "^2.10.14",
|
||||
"react": "18.2.0",
|
||||
"web-vitals": "^3.3.1"
|
||||
"devDependencies": {
|
||||
"astro": "^2.0.6",
|
||||
"web-vitals": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
2953
examples/astro/pnpm-lock.yaml
generated
Normal file
2953
examples/astro/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
2
examples/astro/src/env.d.ts
vendored
2
examples/astro/src/env.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/// <reference types="astro/client-image" />
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly PUBLIC_VERCEL_ANALYTICS_ID: string;
|
||||
|
||||
@@ -32,7 +32,7 @@ export function sendToAnalytics(metric, options) {
|
||||
};
|
||||
|
||||
if (options.debug) {
|
||||
console.log("[Web Vitals]", metric.name, JSON.stringify(body, null, 2));
|
||||
console.log("[Analytics]", metric.name, JSON.stringify(body, null, 2));
|
||||
}
|
||||
|
||||
const blob = new Blob([new URLSearchParams(body).toString()], {
|
||||
@@ -61,6 +61,6 @@ export function webVitals(options) {
|
||||
onCLS((metric) => sendToAnalytics(metric, options));
|
||||
onFCP((metric) => sendToAnalytics(metric, options));
|
||||
} catch (err) {
|
||||
console.error("[Web Vitals]", err);
|
||||
console.error("[Analytics]", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export async function get() {
|
||||
return new Response(JSON.stringify({ time: new Date() }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 's-maxage=10, stale-while-revalidate',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import astroLogo from '../assets/logo.png';
|
||||
---
|
||||
|
||||
<Image src={astroLogo} alt="Astro Logo" width={50} quality={75} />
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Card from '../components/Card.astro';
|
||||
|
||||
export const prerender = true;
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
Astro.response.headers.set('Cache-Control', 's-maxage=10, stale-while-revalidate');
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
---
|
||||
|
||||
<h1>{time}</h1>
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
const time = new Date().toLocaleTimeString();
|
||||
---
|
||||
|
||||
<h1>{time}</h1>
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
@@ -5,11 +5,5 @@
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false,
|
||||
|
||||
/**
|
||||
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
||||
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
||||
*/
|
||||
"isTypeScriptProject": false
|
||||
"disableAnalytics": false
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
/vendor/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/coverage/
|
||||
!.*
|
||||
.*/
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/package.json.ember-try
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@babel/eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
requireConfigFile: false,
|
||||
babelOptions: {
|
||||
plugins: [
|
||||
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: ['ember'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:ember/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
extends: ['eslint:recommended', 'plugin:ember/recommended'],
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
@@ -27,16 +14,14 @@ module.exports = {
|
||||
// node files
|
||||
{
|
||||
files: [
|
||||
'./.eslintrc.js',
|
||||
'./.prettierrc.js',
|
||||
'./.stylelintrc.js',
|
||||
'./.template-lintrc.js',
|
||||
'./ember-cli-build.js',
|
||||
'./testem.js',
|
||||
'./blueprints/*/index.js',
|
||||
'./config/**/*.js',
|
||||
'./lib/*/index.js',
|
||||
'./server/**/*.js',
|
||||
'.eslintrc.js',
|
||||
'.template-lintrc.js',
|
||||
'ember-cli-build.js',
|
||||
'testem.js',
|
||||
'blueprints/*/index.js',
|
||||
'config/**/*.js',
|
||||
'lib/*/index.js',
|
||||
'server/**/*.js',
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
@@ -45,12 +30,18 @@ module.exports = {
|
||||
browser: false,
|
||||
node: true,
|
||||
},
|
||||
extends: ['plugin:n/recommended'],
|
||||
},
|
||||
{
|
||||
// test files
|
||||
files: ['tests/**/*-test.{js,ts}'],
|
||||
extends: ['plugin:qunit/recommended'],
|
||||
plugins: ['node'],
|
||||
rules: Object.assign(
|
||||
{},
|
||||
require('eslint-plugin-node').configs.recommended.rules,
|
||||
{
|
||||
// add your custom rules and overrides for node files here
|
||||
|
||||
// this can be removed once the following is fixed
|
||||
// https://github.com/mysticatea/eslint-plugin-node/issues/77
|
||||
'node/no-unpublished-require': 'off',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
47
examples/ember/.github/workflows/ci.yml
vendored
47
examples/ember/.github/workflows/ci.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request: {}
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: "Lint"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
test:
|
||||
name: "Test"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Run Tests
|
||||
run: npm test
|
||||
16
examples/ember/.gitignore
vendored
16
examples/ember/.gitignore
vendored
@@ -1,32 +1,32 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/.env*
|
||||
/.pnp*
|
||||
/.eslintcache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage/
|
||||
/libpeerconnection.log
|
||||
/npm-debug.log*
|
||||
/testem.log
|
||||
/yarn-error.log
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/bower.json.ember-try
|
||||
/package.json.ember-try
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
|
||||
# broccoli-debug
|
||||
/DEBUG/
|
||||
|
||||
# Environment Variables
|
||||
.env
|
||||
.env.build
|
||||
.env.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
|
||||
# misc
|
||||
/coverage/
|
||||
!.*
|
||||
.*/
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
@@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: '*.{js,ts}',
|
||||
options: {
|
||||
singleQuote: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
# unconventional files
|
||||
/blueprints/*/files/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
|
||||
# addons
|
||||
/.node_modules.ember-try/
|
||||
@@ -1,5 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ignore_dirs": ["dist"]
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ _Live Example: https://ember-template.vercel.app_
|
||||
|
||||
### How We Created This Example
|
||||
|
||||
To get started with Ember for deployment with Vercel, you can use the [Ember CLI](https://cli.emberjs.com) to initialize the project:
|
||||
To get started with Ember for deployment with Vercel, you can use the [Ember CLI](https://ember-cli.com/) to initialize the project:
|
||||
|
||||
```shell
|
||||
$ npx ember-cli new ember-project
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Application from '@ember/application';
|
||||
import Resolver from 'ember-resolver';
|
||||
import Resolver from './resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import config from 'ember-quickstart/config/environment';
|
||||
import config from './config/environment';
|
||||
|
||||
export default class App extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
}
|
||||
const App = Application.extend({
|
||||
modulePrefix: config.modulePrefix,
|
||||
podModulePrefix: config.podModulePrefix,
|
||||
Resolver,
|
||||
});
|
||||
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>EmberQuickstart</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>HelloWorld</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
{{content-for "head"}}
|
||||
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/ember-quickstart.css">
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/hello-world.css">
|
||||
|
||||
{{content-for "head-footer"}}
|
||||
</head>
|
||||
@@ -17,7 +18,7 @@
|
||||
{{content-for "body"}}
|
||||
|
||||
<script src="{{rootURL}}assets/vendor.js"></script>
|
||||
<script src="{{rootURL}}assets/ember-quickstart.js"></script>
|
||||
<script src="{{rootURL}}assets/hello-world.js"></script>
|
||||
|
||||
{{content-for "body-footer"}}
|
||||
</body>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import EmberRouter from '@ember/routing/router';
|
||||
import config from 'ember-quickstart/config/environment';
|
||||
import config from './config/environment';
|
||||
|
||||
export default class Router extends EmberRouter {
|
||||
location = config.locationType;
|
||||
rootURL = config.rootURL;
|
||||
}
|
||||
const Router = EmberRouter.extend({
|
||||
location: config.locationType,
|
||||
rootURL: config.rootURL,
|
||||
});
|
||||
|
||||
Router.map(function () {});
|
||||
Router.map(function() {});
|
||||
|
||||
export default Router;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{{page-title "EmberQuickstart"}}
|
||||
|
||||
{{! The following component displays Ember's default welcome message. }}
|
||||
{{!-- The following component displays Ember's default welcome message. --}}
|
||||
<WelcomePage />
|
||||
{{! Feel free to remove this! }}
|
||||
{{!-- Feel free to remove this! --}}
|
||||
|
||||
{{outlet}}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "ember-cli",
|
||||
"version": "5.1.0",
|
||||
"blueprints": [
|
||||
{
|
||||
"name": "app",
|
||||
"outputRepo": "https://github.com/ember-cli/ember-new-output",
|
||||
"codemodsSource": "ember-app-codemods-manifest@1",
|
||||
"isBaseBlueprint": true,
|
||||
"options": [
|
||||
"--ci-provider=github"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function (environment) {
|
||||
const ENV = {
|
||||
modulePrefix: 'ember-quickstart',
|
||||
module.exports = function(environment) {
|
||||
let ENV = {
|
||||
modulePrefix: 'hello-world',
|
||||
environment,
|
||||
rootURL: '/',
|
||||
locationType: 'history',
|
||||
locationType: 'auto',
|
||||
EmberENV: {
|
||||
EXTEND_PROTOTYPES: false,
|
||||
FEATURES: {
|
||||
// Here you can enable experimental features on an ember canary build
|
||||
// e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true
|
||||
},
|
||||
EXTEND_PROTOTYPES: {
|
||||
// Prevent Ember Data from overriding Date.parse.
|
||||
Date: false,
|
||||
},
|
||||
},
|
||||
|
||||
APP: {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
{
|
||||
"application-template-wrapper": false,
|
||||
"default-async-observers": true,
|
||||
"jquery-integration": false,
|
||||
"template-only-glimmer-components": true
|
||||
"jquery-integration": true
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@ const browsers = [
|
||||
'last 1 Safari versions',
|
||||
];
|
||||
|
||||
const isCI = !!process.env.CI;
|
||||
const isProduction = process.env.EMBER_ENV === 'production';
|
||||
|
||||
if (isCI || isProduction) {
|
||||
browsers.push('ie 11');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
browsers,
|
||||
};
|
||||
|
||||
@@ -2,13 +2,23 @@
|
||||
|
||||
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
|
||||
|
||||
module.exports = function (defaults) {
|
||||
const app = new EmberApp(defaults, {
|
||||
module.exports = function(defaults) {
|
||||
let app = new EmberApp(defaults, {
|
||||
// Add options here
|
||||
'ember-welcome-page': {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
|
||||
// Use `app.import` to add additional libraries to the generated
|
||||
// output files.
|
||||
//
|
||||
// If you need to use different assets in different
|
||||
// environments, specify an object as the first parameter. That
|
||||
// object's keys should be the environment name and the values
|
||||
// should be the asset to use in that environment.
|
||||
//
|
||||
// If the library that you are including contains AMD or ES6
|
||||
// modules that you would like to import into your application
|
||||
// please specify an object with the list of modules as keys
|
||||
// along with the exports of each module as its value.
|
||||
|
||||
return app.toTree();
|
||||
};
|
||||
|
||||
22894
examples/ember/package-lock.json
generated
22894
examples/ember/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "ember-quickstart",
|
||||
"name": "hello-world",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Small description for ember-quickstart goes here",
|
||||
"description": "Small description for hello-world goes here",
|
||||
"repository": "",
|
||||
"license": "MIT",
|
||||
"author": "",
|
||||
@@ -11,66 +11,43 @@
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ember build --environment=production",
|
||||
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
|
||||
"lint:css": "stylelint \"**/*.css\"",
|
||||
"lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
|
||||
"lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
|
||||
"build": "ember build",
|
||||
"lint:hbs": "ember-template-lint .",
|
||||
"lint:hbs:fix": "ember-template-lint . --fix",
|
||||
"lint:js": "eslint . --cache",
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"lint:js": "eslint .",
|
||||
"dev": "ember serve --port $PORT",
|
||||
"start": "ember serve",
|
||||
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
|
||||
"test:ember": "ember test"
|
||||
"test": "ember test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.22.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.22.5",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.1.1",
|
||||
"@ember/test-helpers": "^3.1.0",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"@ember/jquery": "^0.6.0",
|
||||
"@ember/optional-features": "^0.7.0",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-cli": "~5.1.0",
|
||||
"ember-cli-app-version": "^6.0.1",
|
||||
"ember-cli-babel": "^7.26.11",
|
||||
"ember-cli-clean-css": "^2.0.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-ajax": "^5.0.0",
|
||||
"ember-cli": "~3.11.0",
|
||||
"ember-cli-app-version": "^3.2.0",
|
||||
"ember-cli-babel": "^7.7.3",
|
||||
"ember-cli-dependency-checker": "^3.1.0",
|
||||
"ember-cli-eslint": "^5.1.0",
|
||||
"ember-cli-htmlbars": "^3.0.1",
|
||||
"ember-cli-htmlbars-inline-precompile": "^2.1.0",
|
||||
"ember-cli-inject-live-reload": "^1.8.2",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-data": "~5.1.0",
|
||||
"ember-fetch": "^8.1.2",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-modifier": "^4.1.0",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^7.0.0",
|
||||
"ember-resolver": "^10.1.1",
|
||||
"ember-source": "~5.1.1",
|
||||
"ember-template-lint": "^5.11.0",
|
||||
"ember-welcome-page": "^7.0.2",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-ember": "^11.9.0",
|
||||
"eslint-plugin-n": "^16.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-qunit": "^8.0.0",
|
||||
"ember-cli-template-lint": "^1.0.0-beta.1",
|
||||
"ember-cli-uglify": "^2.1.0",
|
||||
"ember-data": "~3.11.0",
|
||||
"ember-export-application-global": "^2.0.0",
|
||||
"ember-load-initializers": "^2.0.0",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
"ember-qunit": "^4.4.1",
|
||||
"ember-resolver": "^5.0.1",
|
||||
"ember-source": "~3.11.1",
|
||||
"ember-welcome-page": "^4.0.0",
|
||||
"eslint-plugin-ember": "^6.2.0",
|
||||
"eslint-plugin-node": "^9.0.1",
|
||||
"loader.js": "^4.7.0",
|
||||
"prettier": "^2.8.8",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"stylelint": "^15.9.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"stylelint-prettier": "^3.0.0",
|
||||
"tracked-built-ins": "^3.1.1",
|
||||
"webpack": "^5.88.1"
|
||||
"qunit-dom": "^0.8.4"
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
"engines": {
|
||||
"node": "14.x"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
test_page: 'tests/index.html?hidepassed',
|
||||
disable_watching: true,
|
||||
launch_in_ci: ['Chrome'],
|
||||
launch_in_dev: ['Chrome'],
|
||||
browser_start_timeout: 120,
|
||||
browser_args: {
|
||||
Chrome: {
|
||||
ci: [
|
||||
// --no-sandbox is needed when running Chrome inside a container
|
||||
process.env.CI ? '--no-sandbox' : null,
|
||||
'--headless',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-software-rasterizer',
|
||||
'--mute-audio',
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
setupApplicationTest as upstreamSetupApplicationTest,
|
||||
setupRenderingTest as upstreamSetupRenderingTest,
|
||||
setupTest as upstreamSetupTest,
|
||||
} from 'ember-qunit';
|
||||
|
||||
// This file exists to provide wrappers around ember-qunit's / ember-mocha's
|
||||
// test setup functions. This way, you can easily extend the setup that is
|
||||
// needed per test type.
|
||||
|
||||
function setupApplicationTest(hooks, options) {
|
||||
upstreamSetupApplicationTest(hooks, options);
|
||||
|
||||
// Additional setup for application tests can be done here.
|
||||
//
|
||||
// For example, if you need an authenticated session for each
|
||||
// application test, you could do:
|
||||
//
|
||||
// hooks.beforeEach(async function () {
|
||||
// await authenticateSession(); // ember-simple-auth
|
||||
// });
|
||||
//
|
||||
// This is also a good place to call test setup functions coming
|
||||
// from other addons:
|
||||
//
|
||||
// setupIntl(hooks); // ember-intl
|
||||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
function setupRenderingTest(hooks, options) {
|
||||
upstreamSetupRenderingTest(hooks, options);
|
||||
|
||||
// Additional setup for rendering tests can be done here.
|
||||
}
|
||||
|
||||
function setupTest(hooks, options) {
|
||||
upstreamSetupTest(hooks, options);
|
||||
|
||||
// Additional setup for unit tests can be done here.
|
||||
}
|
||||
|
||||
export { setupApplicationTest, setupRenderingTest, setupTest };
|
||||
@@ -2,7 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>EmberQuickstart Tests</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>HelloWorld Tests</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
@@ -10,7 +11,7 @@
|
||||
{{content-for "test-head"}}
|
||||
|
||||
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
||||
<link rel="stylesheet" href="{{rootURL}}assets/ember-quickstart.css">
|
||||
<link rel="stylesheet" href="{{rootURL}}assets/hello-world.css">
|
||||
<link rel="stylesheet" href="{{rootURL}}assets/test-support.css">
|
||||
|
||||
{{content-for "head-footer"}}
|
||||
@@ -20,17 +21,10 @@
|
||||
{{content-for "body"}}
|
||||
{{content-for "test-body"}}
|
||||
|
||||
<div id="qunit"></div>
|
||||
<div id="qunit-fixture">
|
||||
<div id="ember-testing-container">
|
||||
<div id="ember-testing"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/testem.js" integrity="" data-embroider-ignore></script>
|
||||
<script src="/testem.js" integrity=""></script>
|
||||
<script src="{{rootURL}}assets/vendor.js"></script>
|
||||
<script src="{{rootURL}}assets/test-support.js"></script>
|
||||
<script src="{{rootURL}}assets/ember-quickstart.js"></script>
|
||||
<script src="{{rootURL}}assets/hello-world.js"></script>
|
||||
<script src="{{rootURL}}assets/tests.js"></script>
|
||||
|
||||
{{content-for "body-footer"}}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import Application from 'ember-quickstart/app';
|
||||
import config from 'ember-quickstart/config/environment';
|
||||
import * as QUnit from 'qunit';
|
||||
import Application from '../app';
|
||||
import config from '../config/environment';
|
||||
import { setApplication } from '@ember/test-helpers';
|
||||
import { setup } from 'qunit-dom';
|
||||
import { start } from 'ember-qunit';
|
||||
|
||||
setApplication(Application.create(config.APP));
|
||||
|
||||
setup(QUnit.assert);
|
||||
|
||||
start();
|
||||
|
||||
8912
examples/ember/yarn.lock
Normal file
8912
examples/ember/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
# The variables added in this file are only available locally in MiniOxygen
|
||||
|
||||
SESSION_SECRET="foobar"
|
||||
PUBLIC_STORE_DOMAIN="mock.shop"
|
||||
@@ -1,5 +0,0 @@
|
||||
build
|
||||
node_modules
|
||||
bin
|
||||
*.d.ts
|
||||
dist
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* @type {import("@types/eslint").Linter.BaseConfig}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: [
|
||||
'@remix-run/eslint-config',
|
||||
'plugin:hydrogen/recommended',
|
||||
'plugin:hydrogen/typescript',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'hydrogen/prefer-image-component': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
},
|
||||
};
|
||||
9
examples/hydrogen-2/.gitignore
vendored
9
examples/hydrogen-2/.gitignore
vendored
@@ -1,9 +0,0 @@
|
||||
node_modules
|
||||
/.cache
|
||||
/build
|
||||
/dist
|
||||
/public/build
|
||||
/.mf
|
||||
.env
|
||||
.shopify
|
||||
.vercel
|
||||
@@ -1 +0,0 @@
|
||||
schema: node_modules/@shopify/hydrogen-react/storefront.schema.json
|
||||
@@ -1,3 +0,0 @@
|
||||
.cache
|
||||
dist
|
||||
.shopify
|
||||
@@ -1,48 +0,0 @@
|
||||
# Hydrogen v2
|
||||
|
||||
This directory is a brief example of a [Hydrogen v2](https://shopify.dev/custom-storefronts/hydrogen) storefront that can be deployed to Vercel with zero configuration.
|
||||
|
||||
## Deploy Your Own
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/vercel/vercel/tree/main/examples/hydrogen-2&template=hydrogen-2)
|
||||
|
||||
_Live Example: https://hydrogen-v2-template.vercel.app_
|
||||
|
||||
You can also deploy using the [Vercel CLI](https://vercel.com/cli):
|
||||
|
||||
```sh
|
||||
npm i -g vercel
|
||||
vercel
|
||||
```
|
||||
|
||||
Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
|
||||
|
||||
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
|
||||
[Get familiar with Remix](https://remix.run/docs/en/v1)
|
||||
|
||||
## What's included
|
||||
|
||||
- Remix
|
||||
- Hydrogen
|
||||
- Oxygen
|
||||
- Shopify CLI
|
||||
- ESLint
|
||||
- Prettier
|
||||
- GraphQL generator
|
||||
- TypeScript and JavaScript flavors
|
||||
- Minimal setup of components and routes
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Using Hydrogen requires a few [environment variables](https://shopify.dev/docs/custom-storefronts/hydrogen/environment-variables) to be set in order to properly connect to Shopify. For this template, the minimal set of environment variables are defined in the `vercel.json` file, which will be applied to the deployment when deployed to Vercel. However, you should migrate these default environment variables to your Project's Environment Variables configuration in the Vercel dashboard (or using the `vc env` commands), and update them according to your needs (also change the `SESSION_SECRET` to your own value). Once that is done, delete the `vercel.json` file from your project to prevent the environment variables defined there from taking precedence.
|
||||
|
||||
## Local development
|
||||
|
||||
Rename the `.env.example` file to `.env` in order for the Shopify dev server to use those environment variables during local development. If you defined/modified additional environment variables based on the section above, be sure to apply those changes in your `.env` file as well.
|
||||
|
||||
Then run the following commands:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* A side bar component with Overlay that works without JavaScript.
|
||||
* @example
|
||||
* ```ts
|
||||
* <Aside id="search-aside" heading="SEARCH">`
|
||||
* <input type="search" />
|
||||
* ...
|
||||
* </Aside>
|
||||
* ```
|
||||
*/
|
||||
export function Aside({
|
||||
children,
|
||||
heading,
|
||||
id = 'aside',
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
heading: React.ReactNode;
|
||||
id?: string;
|
||||
}) {
|
||||
return (
|
||||
<div aria-modal className="overlay" id={id} role="dialog">
|
||||
<button
|
||||
className="close-outside"
|
||||
onClick={() => {
|
||||
history.go(-1);
|
||||
window.location.hash = '';
|
||||
}}
|
||||
/>
|
||||
<aside>
|
||||
<header>
|
||||
<h3>{heading}</h3>
|
||||
<CloseAside />
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseAside() {
|
||||
return (
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-is-valid */
|
||||
<a className="close" href="#" onChange={() => history.go(-1)}>
|
||||
×
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
import {CartForm, Image, Money} from '@shopify/hydrogen';
|
||||
import type {CartLineUpdateInput} from '@shopify/hydrogen/storefront-api-types';
|
||||
import {Link} from '@remix-run/react';
|
||||
import type {CartApiQueryFragment} from 'storefrontapi.generated';
|
||||
import {useVariantUrl} from '~/utils';
|
||||
|
||||
type CartLine = CartApiQueryFragment['lines']['nodes'][0];
|
||||
|
||||
type CartMainProps = {
|
||||
cart: CartApiQueryFragment | null;
|
||||
layout: 'page' | 'aside';
|
||||
};
|
||||
|
||||
export function CartMain({layout, cart}: CartMainProps) {
|
||||
const linesCount = Boolean(cart?.lines?.nodes?.length || 0);
|
||||
const withDiscount =
|
||||
cart &&
|
||||
Boolean(cart.discountCodes.filter((code) => code.applicable).length);
|
||||
const className = `cart-main ${withDiscount ? 'with-discount' : ''}`;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<CartEmpty hidden={linesCount} layout={layout} />
|
||||
<CartDetails cart={cart} layout={layout} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartDetails({layout, cart}: CartMainProps) {
|
||||
const cartHasItems = !!cart && cart.totalQuantity > 0;
|
||||
|
||||
return (
|
||||
<div className="cart-details">
|
||||
<CartLines lines={cart?.lines} layout={layout} />
|
||||
{cartHasItems && (
|
||||
<CartSummary cost={cart.cost} layout={layout}>
|
||||
<CartDiscounts discountCodes={cart.discountCodes} />
|
||||
<CartCheckoutActions checkoutUrl={cart.checkoutUrl} />
|
||||
</CartSummary>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartLines({
|
||||
lines,
|
||||
layout,
|
||||
}: {
|
||||
layout: CartMainProps['layout'];
|
||||
lines: CartApiQueryFragment['lines'] | undefined;
|
||||
}) {
|
||||
if (!lines) return null;
|
||||
|
||||
return (
|
||||
<div aria-labelledby="cart-lines">
|
||||
<ul>
|
||||
{lines.nodes.map((line) => (
|
||||
<CartLineItem key={line.id} line={line} layout={layout} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartLineItem({
|
||||
layout,
|
||||
line,
|
||||
}: {
|
||||
layout: CartMainProps['layout'];
|
||||
line: CartLine;
|
||||
}) {
|
||||
const {id, merchandise} = line;
|
||||
const {product, title, image, selectedOptions} = merchandise;
|
||||
const lineItemUrl = useVariantUrl(product.handle, selectedOptions);
|
||||
|
||||
return (
|
||||
<li key={id} className="cart-line">
|
||||
{image && (
|
||||
<Image
|
||||
alt={title}
|
||||
aspectRatio="1/1"
|
||||
data={image}
|
||||
height={100}
|
||||
loading="lazy"
|
||||
width={100}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Link
|
||||
prefetch="intent"
|
||||
to={lineItemUrl}
|
||||
onClick={() => {
|
||||
if (layout === 'aside') {
|
||||
// close the drawer
|
||||
window.location.href = lineItemUrl;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<strong>{product.title}</strong>
|
||||
</p>
|
||||
</Link>
|
||||
<CartLinePrice line={line} as="span" />
|
||||
<ul>
|
||||
{selectedOptions.map((option) => (
|
||||
<li key={option.name}>
|
||||
<small>
|
||||
{option.name}: {option.value}
|
||||
</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<CartLineQuantity line={line} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function CartCheckoutActions({checkoutUrl}: {checkoutUrl: string}) {
|
||||
if (!checkoutUrl) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a href={checkoutUrl} target="_self">
|
||||
<p>Continue to Checkout →</p>
|
||||
</a>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CartSummary({
|
||||
cost,
|
||||
layout,
|
||||
children = null,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
cost: CartApiQueryFragment['cost'];
|
||||
layout: CartMainProps['layout'];
|
||||
}) {
|
||||
const className =
|
||||
layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside';
|
||||
|
||||
return (
|
||||
<div aria-labelledby="cart-summary" className={className}>
|
||||
<h4>Totals</h4>
|
||||
<dl className="cart-subtotal">
|
||||
<dt>Subtotal</dt>
|
||||
<dd>
|
||||
{cost?.subtotalAmount?.amount ? (
|
||||
<Money data={cost?.subtotalAmount} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartLineRemoveButton({lineIds}: {lineIds: string[]}) {
|
||||
return (
|
||||
<CartForm
|
||||
route="/cart"
|
||||
action={CartForm.ACTIONS.LinesRemove}
|
||||
inputs={{lineIds}}
|
||||
>
|
||||
<button type="submit">Remove</button>
|
||||
</CartForm>
|
||||
);
|
||||
}
|
||||
|
||||
function CartLineQuantity({line}: {line: CartLine}) {
|
||||
if (!line || typeof line?.quantity === 'undefined') return null;
|
||||
const {id: lineId, quantity} = line;
|
||||
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
|
||||
const nextQuantity = Number((quantity + 1).toFixed(0));
|
||||
|
||||
return (
|
||||
<div className="cart-line-quantiy">
|
||||
<small>Quantity: {quantity} </small>
|
||||
<CartLineUpdateButton lines={[{id: lineId, quantity: prevQuantity}]}>
|
||||
<button
|
||||
aria-label="Decrease quantity"
|
||||
disabled={quantity <= 1}
|
||||
name="decrease-quantity"
|
||||
value={prevQuantity}
|
||||
>
|
||||
<span>− </span>
|
||||
</button>
|
||||
</CartLineUpdateButton>
|
||||
|
||||
<CartLineUpdateButton lines={[{id: lineId, quantity: nextQuantity}]}>
|
||||
<button
|
||||
aria-label="Increase quantity"
|
||||
name="increase-quantity"
|
||||
value={nextQuantity}
|
||||
>
|
||||
<span>+</span>
|
||||
</button>
|
||||
</CartLineUpdateButton>
|
||||
|
||||
<CartLineRemoveButton lineIds={[lineId]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartLinePrice({
|
||||
line,
|
||||
priceType = 'regular',
|
||||
...passthroughProps
|
||||
}: {
|
||||
line: CartLine;
|
||||
priceType?: 'regular' | 'compareAt';
|
||||
[key: string]: any;
|
||||
}) {
|
||||
if (!line?.cost?.amountPerQuantity || !line?.cost?.totalAmount) return null;
|
||||
|
||||
const moneyV2 =
|
||||
priceType === 'regular'
|
||||
? line.cost.totalAmount
|
||||
: line.cost.compareAtAmountPerQuantity;
|
||||
|
||||
if (moneyV2 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Money withoutTrailingZeros {...passthroughProps} data={moneyV2} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CartEmpty({
|
||||
hidden = false,
|
||||
layout = 'aside',
|
||||
}: {
|
||||
hidden: boolean;
|
||||
layout?: CartMainProps['layout'];
|
||||
}) {
|
||||
return (
|
||||
<div hidden={hidden}>
|
||||
<br />
|
||||
<p>
|
||||
Looks like you haven’t added anything yet, let’s get you
|
||||
started!
|
||||
</p>
|
||||
<br />
|
||||
<Link
|
||||
to="/collections"
|
||||
onClick={() => {
|
||||
if (layout === 'aside') {
|
||||
window.location.href = '/collections';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue shopping →
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartDiscounts({
|
||||
discountCodes,
|
||||
}: {
|
||||
discountCodes: CartApiQueryFragment['discountCodes'];
|
||||
}) {
|
||||
const codes: string[] =
|
||||
discountCodes
|
||||
?.filter((discount) => discount.applicable)
|
||||
?.map(({code}) => code) || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Have existing discount, display it with a remove option */}
|
||||
<dl hidden={!codes.length}>
|
||||
<div>
|
||||
<dt>Discount(s)</dt>
|
||||
<UpdateDiscountForm>
|
||||
<div className="cart-discount">
|
||||
<code>{codes?.join(', ')}</code>
|
||||
|
||||
<button>Remove</button>
|
||||
</div>
|
||||
</UpdateDiscountForm>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{/* Show an input to apply a discount */}
|
||||
<UpdateDiscountForm discountCodes={codes}>
|
||||
<div>
|
||||
<input type="text" name="discountCode" placeholder="Discount code" />
|
||||
|
||||
<button type="submit">Apply</button>
|
||||
</div>
|
||||
</UpdateDiscountForm>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateDiscountForm({
|
||||
discountCodes,
|
||||
children,
|
||||
}: {
|
||||
discountCodes?: string[];
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<CartForm
|
||||
route="/cart"
|
||||
action={CartForm.ACTIONS.DiscountCodesUpdate}
|
||||
inputs={{
|
||||
discountCodes: discountCodes || [],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CartForm>
|
||||
);
|
||||
}
|
||||
|
||||
function CartLineUpdateButton({
|
||||
children,
|
||||
lines,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
lines: CartLineUpdateInput[];
|
||||
}) {
|
||||
return (
|
||||
<CartForm
|
||||
route="/cart"
|
||||
action={CartForm.ACTIONS.LinesUpdate}
|
||||
inputs={{lines}}
|
||||
>
|
||||
{children}
|
||||
</CartForm>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import {useMatches, NavLink} from '@remix-run/react';
|
||||
import type {FooterQuery} from 'storefrontapi.generated';
|
||||
|
||||
export function Footer({menu}: FooterQuery) {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<FooterMenu menu={menu} />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterMenu({menu}: Pick<FooterQuery, 'menu'>) {
|
||||
const [root] = useMatches();
|
||||
const publicStoreDomain = root?.data?.publicStoreDomain;
|
||||
return (
|
||||
<nav className="footer-menu" role="navigation">
|
||||
{(menu || FALLBACK_FOOTER_MENU).items.map((item) => {
|
||||
if (!item.url) return null;
|
||||
// if the url is internal, we strip the domain
|
||||
const url =
|
||||
item.url.includes('myshopify.com') ||
|
||||
item.url.includes(publicStoreDomain)
|
||||
? new URL(item.url).pathname
|
||||
: item.url;
|
||||
const isExternal = !url.startsWith('/');
|
||||
return isExternal ? (
|
||||
<a href={url} key={item.id} rel="noopener noreferrer" target="_blank">
|
||||
{item.title}
|
||||
</a>
|
||||
) : (
|
||||
<NavLink
|
||||
end
|
||||
key={item.id}
|
||||
prefetch="intent"
|
||||
style={activeLinkStyle}
|
||||
to={url}
|
||||
>
|
||||
{item.title}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
const FALLBACK_FOOTER_MENU = {
|
||||
id: 'gid://shopify/Menu/199655620664',
|
||||
items: [
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461633060920',
|
||||
resourceId: 'gid://shopify/ShopPolicy/23358046264',
|
||||
tags: [],
|
||||
title: 'Privacy Policy',
|
||||
type: 'SHOP_POLICY',
|
||||
url: '/policies/privacy-policy',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461633093688',
|
||||
resourceId: 'gid://shopify/ShopPolicy/23358013496',
|
||||
tags: [],
|
||||
title: 'Refund Policy',
|
||||
type: 'SHOP_POLICY',
|
||||
url: '/policies/refund-policy',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461633126456',
|
||||
resourceId: 'gid://shopify/ShopPolicy/23358111800',
|
||||
tags: [],
|
||||
title: 'Shipping Policy',
|
||||
type: 'SHOP_POLICY',
|
||||
url: '/policies/shipping-policy',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461633159224',
|
||||
resourceId: 'gid://shopify/ShopPolicy/23358079032',
|
||||
tags: [],
|
||||
title: 'Terms of Service',
|
||||
type: 'SHOP_POLICY',
|
||||
url: '/policies/terms-of-service',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function activeLinkStyle({
|
||||
isActive,
|
||||
isPending,
|
||||
}: {
|
||||
isActive: boolean;
|
||||
isPending: boolean;
|
||||
}) {
|
||||
return {
|
||||
fontWeight: isActive ? 'bold' : '',
|
||||
color: isPending ? 'grey' : 'white',
|
||||
};
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import {Await, NavLink, useMatches} from '@remix-run/react';
|
||||
import {Suspense} from 'react';
|
||||
import type {LayoutProps} from './Layout';
|
||||
|
||||
type HeaderProps = Pick<LayoutProps, 'header' | 'cart' | 'isLoggedIn'>;
|
||||
|
||||
type Viewport = 'desktop' | 'mobile';
|
||||
|
||||
export function Header({header, isLoggedIn, cart}: HeaderProps) {
|
||||
const {shop, menu} = header;
|
||||
return (
|
||||
<header className="header">
|
||||
<NavLink prefetch="intent" to="/" style={activeLinkStyle} end>
|
||||
<strong>{shop.name}</strong>
|
||||
</NavLink>
|
||||
<HeaderMenu menu={menu} viewport="desktop" />
|
||||
<HeaderCtas isLoggedIn={isLoggedIn} cart={cart} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeaderMenu({
|
||||
menu,
|
||||
viewport,
|
||||
}: {
|
||||
menu: HeaderProps['header']['menu'];
|
||||
viewport: Viewport;
|
||||
}) {
|
||||
const [root] = useMatches();
|
||||
const publicStoreDomain = root?.data?.publicStoreDomain;
|
||||
const className = `header-menu-${viewport}`;
|
||||
|
||||
function closeAside(event: React.MouseEvent<HTMLAnchorElement>) {
|
||||
if (viewport === 'mobile') {
|
||||
event.preventDefault();
|
||||
window.location.href = event.currentTarget.href;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={className} role="navigation">
|
||||
{viewport === 'mobile' && (
|
||||
<NavLink
|
||||
end
|
||||
onClick={closeAside}
|
||||
prefetch="intent"
|
||||
style={activeLinkStyle}
|
||||
to="/"
|
||||
>
|
||||
Home
|
||||
</NavLink>
|
||||
)}
|
||||
{(menu || FALLBACK_HEADER_MENU).items.map((item) => {
|
||||
if (!item.url) return null;
|
||||
|
||||
// if the url is internal, we strip the domain
|
||||
const url =
|
||||
item.url.includes('myshopify.com') ||
|
||||
item.url.includes(publicStoreDomain)
|
||||
? new URL(item.url).pathname
|
||||
: item.url;
|
||||
return (
|
||||
<NavLink
|
||||
className="header-menu-item"
|
||||
end
|
||||
key={item.id}
|
||||
onClick={closeAside}
|
||||
prefetch="intent"
|
||||
style={activeLinkStyle}
|
||||
to={url}
|
||||
>
|
||||
{item.title}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderCtas({
|
||||
isLoggedIn,
|
||||
cart,
|
||||
}: Pick<HeaderProps, 'isLoggedIn' | 'cart'>) {
|
||||
return (
|
||||
<nav className="header-ctas" role="navigation">
|
||||
<HeaderMenuMobileToggle />
|
||||
<NavLink prefetch="intent" to="/account" style={activeLinkStyle}>
|
||||
{isLoggedIn ? 'Account' : 'Sign in'}
|
||||
</NavLink>
|
||||
<SearchToggle />
|
||||
<CartToggle cart={cart} />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderMenuMobileToggle() {
|
||||
return (
|
||||
<a className="header-menu-mobile-toggle" href="#mobile-menu-aside">
|
||||
<h3>☰</h3>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchToggle() {
|
||||
return <a href="#search-aside">Search</a>;
|
||||
}
|
||||
|
||||
function CartBadge({count}: {count: number}) {
|
||||
return <a href="#cart-aside">Cart {count}</a>;
|
||||
}
|
||||
|
||||
function CartToggle({cart}: Pick<HeaderProps, 'cart'>) {
|
||||
return (
|
||||
<Suspense fallback={<CartBadge count={0} />}>
|
||||
<Await resolve={cart}>
|
||||
{(cart) => {
|
||||
if (!cart) return <CartBadge count={0} />;
|
||||
return <CartBadge count={cart.totalQuantity || 0} />;
|
||||
}}
|
||||
</Await>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const FALLBACK_HEADER_MENU = {
|
||||
id: 'gid://shopify/Menu/199655587896',
|
||||
items: [
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461609500728',
|
||||
resourceId: null,
|
||||
tags: [],
|
||||
title: 'Collections',
|
||||
type: 'HTTP',
|
||||
url: '/collections',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461609533496',
|
||||
resourceId: null,
|
||||
tags: [],
|
||||
title: 'Blog',
|
||||
type: 'HTTP',
|
||||
url: '/blogs/journal',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461609566264',
|
||||
resourceId: null,
|
||||
tags: [],
|
||||
title: 'Policies',
|
||||
type: 'HTTP',
|
||||
url: '/policies',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'gid://shopify/MenuItem/461609599032',
|
||||
resourceId: 'gid://shopify/Page/92591030328',
|
||||
tags: [],
|
||||
title: 'About',
|
||||
type: 'PAGE',
|
||||
url: '/pages/about',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function activeLinkStyle({
|
||||
isActive,
|
||||
isPending,
|
||||
}: {
|
||||
isActive: boolean;
|
||||
isPending: boolean;
|
||||
}) {
|
||||
return {
|
||||
fontWeight: isActive ? 'bold' : '',
|
||||
color: isPending ? 'grey' : 'black',
|
||||
};
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import {Await} from '@remix-run/react';
|
||||
import {Suspense} from 'react';
|
||||
import type {
|
||||
CartApiQueryFragment,
|
||||
FooterQuery,
|
||||
HeaderQuery,
|
||||
} from 'storefrontapi.generated';
|
||||
import {Aside} from '~/components/Aside';
|
||||
import {Footer} from '~/components/Footer';
|
||||
import {Header, HeaderMenu} from '~/components/Header';
|
||||
import {CartMain} from '~/components/Cart';
|
||||
import {
|
||||
PredictiveSearchForm,
|
||||
PredictiveSearchResults,
|
||||
} from '~/components/Search';
|
||||
|
||||
export type LayoutProps = {
|
||||
cart: Promise<CartApiQueryFragment | null>;
|
||||
children?: React.ReactNode;
|
||||
footer: Promise<FooterQuery>;
|
||||
header: HeaderQuery;
|
||||
isLoggedIn: boolean;
|
||||
};
|
||||
|
||||
export function Layout({
|
||||
cart,
|
||||
children = null,
|
||||
footer,
|
||||
header,
|
||||
isLoggedIn,
|
||||
}: LayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<CartAside cart={cart} />
|
||||
<SearchAside />
|
||||
<MobileMenuAside menu={header.menu} />
|
||||
<Header header={header} cart={cart} isLoggedIn={isLoggedIn} />
|
||||
<main>{children}</main>
|
||||
<Suspense>
|
||||
<Await resolve={footer}>
|
||||
{(footer) => <Footer menu={footer.menu} />}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CartAside({cart}: {cart: LayoutProps['cart']}) {
|
||||
return (
|
||||
<Aside id="cart-aside" heading="CART">
|
||||
<Suspense fallback={<p>Loading cart ...</p>}>
|
||||
<Await resolve={cart}>
|
||||
{(cart) => {
|
||||
return <CartMain cart={cart} layout="aside" />;
|
||||
}}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</Aside>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchAside() {
|
||||
return (
|
||||
<Aside id="search-aside" heading="SEARCH">
|
||||
<div className="predictive-search">
|
||||
<br />
|
||||
<PredictiveSearchForm>
|
||||
{({fetchResults, inputRef}) => (
|
||||
<div>
|
||||
<input
|
||||
name="q"
|
||||
onChange={fetchResults}
|
||||
onFocus={fetchResults}
|
||||
placeholder="Search"
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
/>
|
||||
|
||||
<button type="submit">Search</button>
|
||||
</div>
|
||||
)}
|
||||
</PredictiveSearchForm>
|
||||
<PredictiveSearchResults />
|
||||
</div>
|
||||
</Aside>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileMenuAside({menu}: {menu: HeaderQuery['menu']}) {
|
||||
return (
|
||||
<Aside id="mobile-menu-aside" heading="MENU">
|
||||
<HeaderMenu menu={menu} viewport="mobile" />
|
||||
</Aside>
|
||||
);
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
import {
|
||||
useParams,
|
||||
useFetcher,
|
||||
Link,
|
||||
Form,
|
||||
type FormProps,
|
||||
} from '@remix-run/react';
|
||||
import {Image, Money, Pagination} from '@shopify/hydrogen';
|
||||
import React, {useRef, useEffect} from 'react';
|
||||
import {useFetchers} from '@remix-run/react';
|
||||
|
||||
import type {
|
||||
PredictiveProductFragment,
|
||||
PredictiveCollectionFragment,
|
||||
PredictiveArticleFragment,
|
||||
SearchQuery,
|
||||
} from 'storefrontapi.generated';
|
||||
|
||||
type PredicticeSearchResultItemImage =
|
||||
| PredictiveCollectionFragment['image']
|
||||
| PredictiveArticleFragment['image']
|
||||
| PredictiveProductFragment['variants']['nodes'][0]['image'];
|
||||
|
||||
type PredictiveSearchResultItemPrice =
|
||||
| PredictiveProductFragment['variants']['nodes'][0]['price'];
|
||||
|
||||
export type NormalizedPredictiveSearchResultItem = {
|
||||
__typename: string | undefined;
|
||||
handle: string;
|
||||
id: string;
|
||||
image?: PredicticeSearchResultItemImage;
|
||||
price?: PredictiveSearchResultItemPrice;
|
||||
styledTitle?: string;
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type NormalizedPredictiveSearchResults = Array<
|
||||
| {type: 'queries'; items: Array<NormalizedPredictiveSearchResultItem>}
|
||||
| {type: 'products'; items: Array<NormalizedPredictiveSearchResultItem>}
|
||||
| {type: 'collections'; items: Array<NormalizedPredictiveSearchResultItem>}
|
||||
| {type: 'pages'; items: Array<NormalizedPredictiveSearchResultItem>}
|
||||
| {type: 'articles'; items: Array<NormalizedPredictiveSearchResultItem>}
|
||||
>;
|
||||
|
||||
export type NormalizedPredictiveSearch = {
|
||||
results: NormalizedPredictiveSearchResults;
|
||||
totalResults: number;
|
||||
};
|
||||
|
||||
type FetchSearchResultsReturn = {
|
||||
searchResults: {
|
||||
results: SearchQuery | null;
|
||||
totalResults: number;
|
||||
};
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export const NO_PREDICTIVE_SEARCH_RESULTS: NormalizedPredictiveSearchResults = [
|
||||
{type: 'queries', items: []},
|
||||
{type: 'products', items: []},
|
||||
{type: 'collections', items: []},
|
||||
{type: 'pages', items: []},
|
||||
{type: 'articles', items: []},
|
||||
];
|
||||
|
||||
export function SearchForm({searchTerm}: {searchTerm: string}) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// focus the input when cmd+k is pressed
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'k' && event.metaKey) {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Form method="get">
|
||||
<input
|
||||
defaultValue={searchTerm}
|
||||
name="q"
|
||||
placeholder="Search…"
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
/>
|
||||
|
||||
<button type="submit">Search</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchResults({
|
||||
results,
|
||||
}: Pick<FetchSearchResultsReturn['searchResults'], 'results'>) {
|
||||
if (!results) {
|
||||
return null;
|
||||
}
|
||||
const keys = Object.keys(results) as Array<keyof typeof results>;
|
||||
return (
|
||||
<div>
|
||||
{results &&
|
||||
keys.map((type) => {
|
||||
const resourceResults = results[type];
|
||||
|
||||
if (resourceResults.nodes[0]?.__typename === 'Page') {
|
||||
const pageResults = resourceResults as SearchQuery['pages'];
|
||||
return resourceResults.nodes.length ? (
|
||||
<SearchResultPageGrid key="pages" pages={pageResults} />
|
||||
) : null;
|
||||
}
|
||||
|
||||
if (resourceResults.nodes[0]?.__typename === 'Product') {
|
||||
const productResults = resourceResults as SearchQuery['products'];
|
||||
return resourceResults.nodes.length ? (
|
||||
<SearchResultsProductsGrid
|
||||
key="products"
|
||||
products={productResults}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
if (resourceResults.nodes[0]?.__typename === 'Article') {
|
||||
const articleResults = resourceResults as SearchQuery['articles'];
|
||||
return resourceResults.nodes.length ? (
|
||||
<SearchResultArticleGrid
|
||||
key="articles"
|
||||
articles={articleResults}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResultsProductsGrid({products}: Pick<SearchQuery, 'products'>) {
|
||||
return (
|
||||
<div className="search-result">
|
||||
<h3>Products</h3>
|
||||
<Pagination connection={products}>
|
||||
{({nodes, isLoading, NextLink, PreviousLink}) => {
|
||||
const itemsMarkup = nodes.map((product) => (
|
||||
<div className="search-results-item" key={product.id}>
|
||||
<Link prefetch="intent" to={`/products/${product.handle}`}>
|
||||
<span>{product.title}</span>
|
||||
</Link>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<PreviousLink>
|
||||
{isLoading ? 'Loading...' : <span>↑ Load previous</span>}
|
||||
</PreviousLink>
|
||||
</div>
|
||||
<div>
|
||||
{itemsMarkup}
|
||||
<br />
|
||||
</div>
|
||||
<div>
|
||||
<NextLink>
|
||||
{isLoading ? 'Loading...' : <span>Load more ↓</span>}
|
||||
</NextLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Pagination>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResultPageGrid({pages}: Pick<SearchQuery, 'pages'>) {
|
||||
return (
|
||||
<div className="search-result">
|
||||
<h2>Pages</h2>
|
||||
<div>
|
||||
{pages?.nodes?.map((page) => (
|
||||
<div className="search-results-item" key={page.id}>
|
||||
<Link prefetch="intent" to={`/pages/${page.handle}`}>
|
||||
{page.title}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResultArticleGrid({articles}: Pick<SearchQuery, 'articles'>) {
|
||||
return (
|
||||
<div className="search-result">
|
||||
<h2>Articles</h2>
|
||||
<div>
|
||||
{articles?.nodes?.map((article) => (
|
||||
<div className="search-results-item" key={article.id}>
|
||||
<Link prefetch="intent" to={`/blog/${article.handle}`}>
|
||||
{article.title}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoSearchResults() {
|
||||
return <p>No results, try a different search.</p>;
|
||||
}
|
||||
|
||||
type ChildrenRenderProps = {
|
||||
fetchResults: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
fetcher: ReturnType<typeof useFetcher<NormalizedPredictiveSearchResults>>;
|
||||
inputRef: React.MutableRefObject<HTMLInputElement | null>;
|
||||
};
|
||||
|
||||
type SearchFromProps = {
|
||||
action?: FormProps['action'];
|
||||
method?: FormProps['method'];
|
||||
className?: string;
|
||||
children: (passedProps: ChildrenRenderProps) => React.ReactNode;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Search form component that posts search requests to the `/search` route
|
||||
**/
|
||||
export function PredictiveSearchForm({
|
||||
action,
|
||||
children,
|
||||
className = 'predictive-search-form',
|
||||
method = 'POST',
|
||||
...props
|
||||
}: SearchFromProps) {
|
||||
const params = useParams();
|
||||
const fetcher = useFetcher<NormalizedPredictiveSearchResults>();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
function fetchResults(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const searchAction = action ?? '/api/predictive-search';
|
||||
const localizedAction = params.locale
|
||||
? `/${params.locale}${searchAction}`
|
||||
: searchAction;
|
||||
const newSearchTerm = event.target.value || '';
|
||||
fetcher.submit(
|
||||
{q: newSearchTerm, limit: '6'},
|
||||
{method, action: localizedAction},
|
||||
);
|
||||
}
|
||||
|
||||
// ensure the passed input has a type of search, because SearchResults
|
||||
// will select the element based on the input
|
||||
useEffect(() => {
|
||||
inputRef?.current?.setAttribute('type', 'search');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<fetcher.Form
|
||||
{...props}
|
||||
className={className}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!inputRef?.current || inputRef.current.value === '') {
|
||||
return;
|
||||
}
|
||||
inputRef.current.blur();
|
||||
}}
|
||||
>
|
||||
{children({fetchResults, inputRef, fetcher})}
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function PredictiveSearchResults() {
|
||||
const {results, totalResults, searchInputRef, searchTerm} =
|
||||
usePredictiveSearch();
|
||||
|
||||
function goToSearchResult(event: React.MouseEvent<HTMLAnchorElement>) {
|
||||
if (!searchInputRef.current) return;
|
||||
searchInputRef.current.blur();
|
||||
searchInputRef.current.value = '';
|
||||
// close the aside
|
||||
window.location.href = event.currentTarget.href;
|
||||
}
|
||||
|
||||
if (!totalResults) {
|
||||
return <NoPredictiveSearchResults searchTerm={searchTerm} />;
|
||||
}
|
||||
return (
|
||||
<div className="predictive-search-results">
|
||||
<div>
|
||||
{results.map(({type, items}) => (
|
||||
<PredictiveSearchResult
|
||||
goToSearchResult={goToSearchResult}
|
||||
items={items}
|
||||
key={type}
|
||||
searchTerm={searchTerm}
|
||||
type={type}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* view all results /search?q=term */}
|
||||
{searchTerm.current && (
|
||||
<Link onClick={goToSearchResult} to={`/search?q=${searchTerm.current}`}>
|
||||
<p>
|
||||
View all results for <q>{searchTerm.current}</q>
|
||||
→
|
||||
</p>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoPredictiveSearchResults({
|
||||
searchTerm,
|
||||
}: {
|
||||
searchTerm: React.MutableRefObject<string>;
|
||||
}) {
|
||||
if (!searchTerm.current) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
No results found for <q>{searchTerm.current}</q>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
type SearchResultTypeProps = {
|
||||
goToSearchResult: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
items: NormalizedPredictiveSearchResultItem[];
|
||||
searchTerm: UseSearchReturn['searchTerm'];
|
||||
type: NormalizedPredictiveSearchResults[number]['type'];
|
||||
};
|
||||
|
||||
function PredictiveSearchResult({
|
||||
goToSearchResult,
|
||||
items,
|
||||
searchTerm,
|
||||
type,
|
||||
}: SearchResultTypeProps) {
|
||||
const isSuggestions = type === 'queries';
|
||||
const categoryUrl = `/search?q=${
|
||||
searchTerm.current
|
||||
}&type=${pluralToSingularSearchType(type)}`;
|
||||
|
||||
return (
|
||||
<div className="predictive-search-result" key={type}>
|
||||
<Link prefetch="intent" to={categoryUrl} onClick={goToSearchResult}>
|
||||
<h5>{isSuggestions ? 'Suggestions' : type}</h5>
|
||||
</Link>
|
||||
<ul>
|
||||
{items.map((item: NormalizedPredictiveSearchResultItem) => (
|
||||
<SearchResultItem
|
||||
goToSearchResult={goToSearchResult}
|
||||
item={item}
|
||||
key={item.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SearchResultItemProps = Pick<SearchResultTypeProps, 'goToSearchResult'> & {
|
||||
item: NormalizedPredictiveSearchResultItem;
|
||||
};
|
||||
|
||||
function SearchResultItem({goToSearchResult, item}: SearchResultItemProps) {
|
||||
return (
|
||||
<li className="predictive-search-result-item" key={item.id}>
|
||||
<Link onClick={goToSearchResult} to={item.url}>
|
||||
{item.image?.url && (
|
||||
<Image
|
||||
alt={item.image.altText ?? ''}
|
||||
src={item.image.url}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{item.styledTitle ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.styledTitle,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{item.title}</span>
|
||||
)}
|
||||
{item?.price && (
|
||||
<small>
|
||||
<Money data={item.price} />
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
type UseSearchReturn = NormalizedPredictiveSearch & {
|
||||
searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
|
||||
searchTerm: React.MutableRefObject<string>;
|
||||
};
|
||||
|
||||
function usePredictiveSearch(): UseSearchReturn {
|
||||
const fetchers = useFetchers();
|
||||
const searchTerm = useRef<string>('');
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const searchFetcher = fetchers.find((fetcher) => fetcher.data?.searchResults);
|
||||
|
||||
if (searchFetcher?.state === 'loading') {
|
||||
searchTerm.current = (searchFetcher.formData?.get('q') || '') as string;
|
||||
}
|
||||
|
||||
const search = (searchFetcher?.data?.searchResults || {
|
||||
results: NO_PREDICTIVE_SEARCH_RESULTS,
|
||||
totalResults: 0,
|
||||
}) as NormalizedPredictiveSearch;
|
||||
|
||||
// capture the search input element as a ref
|
||||
useEffect(() => {
|
||||
if (searchInputRef.current) return;
|
||||
searchInputRef.current = document.querySelector('input[type="search"]');
|
||||
}, []);
|
||||
|
||||
return {...search, searchInputRef, searchTerm};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plural search type to a singular search type
|
||||
* @param type - The plural search type
|
||||
* @returns The singular search type
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* pluralToSingularSearchType('articles') // => 'ARTICLE'
|
||||
* pluralToSingularSearchType(['articles', 'products']) // => 'ARTICLE,PRODUCT'
|
||||
* ```
|
||||
*/
|
||||
function pluralToSingularSearchType(
|
||||
type:
|
||||
| NormalizedPredictiveSearchResults[number]['type']
|
||||
| Array<NormalizedPredictiveSearchResults[number]['type']>,
|
||||
) {
|
||||
const plural = {
|
||||
articles: 'ARTICLE',
|
||||
collections: 'COLLECTION',
|
||||
pages: 'PAGE',
|
||||
products: 'PRODUCT',
|
||||
queries: 'QUERY',
|
||||
};
|
||||
|
||||
if (typeof type === 'string') {
|
||||
return plural[type];
|
||||
}
|
||||
|
||||
return type.map((t) => plural[t]).join(',');
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user