Compare commits

...

270 Commits

Author SHA1 Message Date
Sean Massa
3d3774ee7e Publish Stable
- @vercel/build-utils@5.0.4
 - vercel@27.2.0
 - @vercel/client@12.1.3
 - @vercel/go@2.0.8
 - @vercel/hydrogen@0.0.5
 - @vercel/next@3.1.8
 - @vercel/node@2.4.5
 - @vercel/python@3.1.0
 - @vercel/redwood@1.0.9
 - @vercel/remix@1.0.10
 - @vercel/ruby@1.3.16
 - @vercel/static-build@1.0.9
2022-07-21 15:03:11 -05:00
Sean Massa
50f8eec7cb [cli][dev] support environment variables in edge functions during vc dev (#8207)
Edge Function support in `vc dev` was not passing through the environment variables, which is supported by [production Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions/edge-functions-api#environment-variables).

This PR passes those through. I updated a test for it and manually tested on a separate project.
2022-07-21 17:53:52 +00:00
Sean Massa
45374e2f90 [cli] improve isBundledBuilder logic (#8086)
The logic that determines if a builder needs to be installed has a check to see if the builder should already be bundled: `isBundledBuilder`. This was looking for specific conditions that made it (1) a bit hard to follow and (2) very sensitive to `canary` tags (and having "canary" in the version specifier).

This causes general development problems because local changes weren't always used by local CLI builds. Depedendant packages (like `@vercel/node`) would be installed from the latest `canary` release instead.

This caused problems in CI and released CLI versions where the latest `canary` of dependant packages might be rather old, causing that old code to be used instead of the latest non-canary releases.

The issue was mitigated for now by publishing canary releases for all packages.

---

Paired with @styfle @MatthewStanciu.

@TooTallNate: Is this change too broad? Are there cases where we wouldn't want to do this?
2022-07-21 15:45:29 +00:00
Steven
fd9142b6f3 [cli] Bump @vercel/fun to 1.0.4 (#8198)
Bump `@vercel/fun` to [1.0.4](https://github.com/vercel/fun/releases/tag/1.0.4)
2022-07-21 00:00:03 +00:00
JJ Kasper
8cf67b549b [next] Ensure manifests are specific to the included pages (#8172)
### Related Issues

This updates to filter the `routes-manifest` and `pages-manifest` to only include entries for the pages that are being included in the specific serverless function. This fixes the case where multiple dynamic routes could match a path but one is configured with `fallback: false` so shouldn't match when executing for a different dynamic route. 

A regression test for this specific scenario has been added in the `00-mixed-dynamic-routes` fixture. 

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-20 23:06:44 +00:00
Sean Massa
5dc6f48e44 [cli][dev] handle no response from edge functions (#8095)
When an edge function has no response during `vc dev`, we were seeing an unhelpful error message:

> The event listener did not respond.

Now, we'll see a much more specific error message:

> Unhandled rejection: Edge Function "api/edge-no-response.ts" did not return a response.
> Error! Failed to complete request to /api/edge-no-response: Error: socket hang up
2022-07-20 20:09:29 +00:00
Kevin Tan
66c8544e8f [python] support Sanic >=21 and python >= 3.10 (#8045)
### Related Issues

1. exception from python 3.10:
```
TypeError: As of 3.10, the *loop* parameter was removed from Queue() since it is no longer necessary`
```

Remove the deprecated argument `loop` from `Queue`, which can also be omitted in python version < 3.10

2. exception from Sanic > 21.3:
```
File "C:\Users\Kevin\AppData\Local\Temp\zeit-fun-03f18b2d2c7d7\sanic\signals.py", line 93, in get
    group, param_basket = self.find_route(
TypeError: 'NoneType' object is not callable
```
As of Sanic > 21.3, it cannot serve requests immediately after initializing, instead, we need implement the [ASGI lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) and wait for the startup event completed.  

here I complemented the protocol copied from (same source of the previous HTTP procotol): <https://github.com/jordaneremieff/mangum/blob/main/mangum/protocols/lifespan.py>


### Related link:
https://github.com/encode/uvicorn/pull/498

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR


Co-authored-by: Steven <steven@ceriously.com>
2022-07-20 09:50:53 -04:00
Matthew Stanciu
0140db38fa [cli] Support multiple remote URLs in Git config (#8145)
Two features that handle a user's local Git config have been shipped:

- #8100 
- #7910 

Both of these features currently pull only from the user's remote origin URL. This covers 90% of cases, but there are cases in which the user has more than one remote URL, and may want to use one other than the origin URL, especially in `vc git connect`. This PR:

- Adds support for multiple remote URLs in a user's Git config
- Updates `vc git connect` to prompt the user to select a URL if they have multiple remote URLs
- Updates `createGitMeta` to send the connected Git repository url by default, origin url otherwise

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-20 07:17:53 +00:00
Chris Barber
e5421c27e8 [cli][client][build-utils][node][static-build] updated node-fetch to fix high severity security vulnerability (#8180)
Update `node-fetch 2.6.1 -> 2.6.7` to fix high severity security vulnerability: Exposure of Sensitive Information to an Unauthorized Actor (https://github.com/advisories/GHSA-r683-j2x4-v87g).

`node-fetch` was updated in the root, `api`, `build-utils`, `cli`, `client`, `node`, and `static-build`.

### Related Issues

> https://linear.app/vercel/issue/VCCLI-196/update-vercelnode-dep-node-fetch-261-267

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-19 22:15:09 +00:00
Matthew Stanciu
5afc527233 [cli] Add --environment flag to vc env pull (#8162)
Right now, `vc env pull` only pulls development environment variables. This PR adds a new flag, `--environment,` which allows users to specify which environment to pull from.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-19 20:02:13 +00:00
Nathan Rajlich
de9518b010 [cli] Write top-level error to builds.json file in "vc build" (#8193)
If an error happens outside of a Builder (i.e. `detectBuilders()` function fails), then write the serialized error into the `builds.json` file at the top-level of the file (there is no `builds[]` when an error happens at the top-level).
2022-07-19 19:30:17 +00:00
Nathan Rajlich
c322d1dbba [cli] Set ignoreBuildScript: true option in "vc build" command (#8184)
This matches the behavior in production and prevents the error:

```
> Error! Your `package.json` file is missing a `build` property inside the `scripts` property.
```
2022-07-19 18:52:33 +00:00
Steven
18c19ead76 [tests] Upgrade python tests to 3.9 (#8181)
New python 3.6 deployments will fail today per the previous announcement https://vercel.com/changelog/python-3-6-is-being-deprecated

This PR updates the tests to use python 3.9 instead.
2022-07-19 17:43:20 +00:00
Steven
9d80c27382 [cli] Print full error when unknown/unexpected (#8059)
If we don’t know the error, we should not assume it has a stack prop and instead print the whole thing to avoid accidentally printing `undefined`. 

- related to https://github.com/vercel/vercel/discussions/8043
2022-07-18 10:40:09 -04:00
Steven
bef1aec766 Publish Stable
- @vercel/build-utils@5.0.3
 - vercel@27.1.5
 - @vercel/client@12.1.2
 - @vercel/frameworks@1.1.1
 - @vercel/fs-detectors@2.0.1
 - @vercel/go@2.0.7
 - @vercel/hydrogen@0.0.4
 - @vercel/next@3.1.7
 - @vercel/node@2.4.4
 - @vercel/python@3.0.7
 - @vercel/redwood@1.0.8
 - @vercel/remix@1.0.9
 - @vercel/routing-utils@2.0.0
 - @vercel/ruby@1.3.15
 - @vercel/static-build@1.0.8
2022-07-15 15:40:37 -04:00
Steven
4f4a42813f [build-utils][node][python][ruby] Update error message for EOL runtimes (#8167)
This PR updates the error message when the runtime version detected is EOL
2022-07-15 15:38:38 -04:00
Steven
181a492d91 [routing-utils] MAJOR refactor getTransformedRoutes and types (#8155)
This is a semver major change to the public API for `@vercel/routing-utils` which includes the following breaking changes.

1. `getTransformedRoutes({ nowConfig })` props changed to `getTransformedRoutes(nowConfig)`
2. `type Source` renamed `type RouteWithSrc`
3. `type Handler` renamed `type RouteWithHandle`
4. `interface VercelConfig` removed
5. `type NowConfig` removed
6. `type NowRewrite` removed
7. `type NowRedirect` removed
8. `type NowHeader` removed
9. `type NowHeaderKeyValue` removed
2022-07-15 14:05:08 -04:00
Sean Massa
1be7a80bb8 Publish Stable
- vercel@27.1.4
 - @vercel/next@3.1.6
2022-07-15 11:20:56 -05:00
Sean Massa
0428d4744e [cli] write config.json when exiting because of error in builder (#8163)
Co-authored-by: Steven <steven@ceriously.com>
2022-07-15 11:16:31 -05:00
JJ Kasper
2a929a4bb9 [next] Update allowQuery for prerendered paths (#8158)
This updates our `allowQuery` generating to ignore all query values for build-time prerender paths as these will match before dynamic routes since they are filesystem routes and the query values will not be overridden properly like they are for fallback prerender paths. This also adds testing for both prerender path types with on-demand ISR to ensure the cache is updated as expected regardless of the query.  

Deployment with patch can be seen here https://nextjs-issue-odr-simple-hrjt2dagm-ijjk-testing.vercel.app/

### Related Issues

x-ref: https://github.com/vercel/next.js/issues/38306
x-ref: https://github.com/vercel/next.js/issues/38653
2022-07-15 11:37:05 -04:00
JJ Kasper
accd308dc5 [tests] Update log for update-canary-tag script (#8156)
### Related Issues

Noticed this log was not being converted to a string so we're losing some context so this corrects in case we have a failure on this step in the future. 

Fixes: https://github.com/vercel/vercel/runs/7344626478?check_suite_focus=true#step:8:258

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-15 02:16:51 +00:00
Sean Massa
e2d4efab08 Publish Stable
- vercel@27.1.3
2022-07-14 12:04:05 -05:00
JJ Kasper
7e0dd6f808 [tests] Update to latest version of turbo (#8152)
### Related Issues

Updates to latest turbo which includes patches for cached files. 

x-ref: [slack thread](https://vercel.slack.com/archives/C02CDC2ALJH/p1657767763630359?thread_ts=1657757803.039099&cid=C02CDC2ALJH)

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-14 12:41:28 +00:00
Nathan Rajlich
8971e02e49 [cli] Normalize output file paths in vercel build (#8149)
When a Builder returns an output asset that contains irregular slashes (multiple slashes or leading/trailing slashes), then have them be removed from the file path before creating the output asset.

This fixes an edge case where `@vercel/next` could end up outputting a Serverless Function with a trailing slash (i.e. `en-US/`). Before this PR, that would be serialized to the filesystem at `en-US/.func`, but after this fix it's saved in the correct `en-US.func` directory.
2022-07-14 07:58:20 +00:00
Nathan Rajlich
10c91c8579 [cli] Store build error in "builds.json" file in vc build (#8148)
When a build fails, store the serialized Error in the "builds.json" file under the "build" object of the Builder that failed.

Example:

```json
{
  "//": "This file was generated by the `vercel build` command. It is not part of the Build Output API.",
  "target": "preview",
  "argv": [
    "/usr/local/bin/node",
    "/Users/nrajlich/Code/vercel/vercel/packages/cli/src/index.ts",
    "build",
    "--cwd",
    "/Users/nrajlich/Downloads/vc-build-next-repro/"
  ],
  "builds": [
    {
      "require": "@vercel/next",
      "requirePath": "/Users/nrajlich/Code/vercel/vercel/packages/next/dist/index",
      "apiVersion": 2,
      "src": "package.json",
      "use": "@vercel/next",
      "config": {
        "zeroConfig": true,
        "framework": "nextjs"
      },
      "error": {
        "name": "Error",
        "message": "Command \"pnpm run build\" exited with 1",
        "stack": "Error: Command \"pnpm run build\" exited with 1\n    at ChildProcess.<anonymous> (/Users/nrajlich/Code/vercel/vercel/packages/build-utils/dist/index.js:20591:20)\n    at ChildProcess.emit (node:events:527:28)\n    at ChildProcess.emit (node:domain:475:12)\n    at maybeClose (node:internal/child_process:1092:16)\n    at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)",
        "hideStackTrace": true,
        "code": "BUILD_UTILS_SPAWN_1"
      }
    }
  ]
}
```
2022-07-14 01:05:00 +00:00
Nathan Rajlich
bfdbe58675 Publish Stable
- vercel@27.1.2
 - @vercel/static-build@1.0.7
2022-07-13 13:51:02 -07:00
Matthew Stanciu
7bdaf107b7 [cli] Consume --no-clipboard (#8147)
#8085 removed the clipboard copy feature in `vc deploy`, along with the `--no-clipboard` flag. Right now, the CLI exits and returns an error when someone includes the `--no-clipboard` flag. This PR instead consumes the flag and warns the user that the flag is deprecated.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-13 20:32:21 +00:00
Nathan Rajlich
8de100f0e1 [static-build] Return BOA v3 result without vercel build (#8146)
This will allow Build Output API v3 to be produced by a build script without the `ENABLE_VC_BUILD=1` env var being necessary.
2022-07-13 18:17:53 +00:00
Steven
38a6785859 Publish Stable
- vercel@27.1.1
2022-07-13 12:00:03 -04:00
Steven
c67d1a8525 [cli] Fix vercel build sort order (#8144)
- Fixes #8063
2022-07-13 11:58:56 -04:00
Matthew Stanciu
c5a7c574a2 [tests] Add missing function import in Project mock (#8143)
A function `formatProvider` wasn't imported in the `Project` mock endpoint. This wasn't caught before #8100 was merged, and dodged CI.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-12 21:35:27 +00:00
Sean Massa
d2f8d178f7 Publish Stable
- @vercel/build-utils@5.0.2
 - vercel@27.1.0
 - @vercel/client@12.1.1
 - @vercel/fs-detectors@2.0.0
 - @vercel/go@2.0.6
 - @vercel/hydrogen@0.0.3
 - @vercel/next@3.1.5
 - @vercel/node@2.4.3
 - @vercel/python@3.0.6
 - @vercel/redwood@1.0.7
 - @vercel/remix@1.0.8
 - @vercel/ruby@1.3.14
 - @vercel/static-build@1.0.6
2022-07-12 15:34:28 -05:00
Nathan Rajlich
f9a747764c [remix] Fix isMonorepo check (#8142)
`config.projectSettings` is not available when running `@vercel/remix` directly (i.e. when not using `vc build`), so calculate the correct root directory value based on the `workPath` relative to the `repoRootPath` value.
2022-07-12 20:10:51 +00:00
Matthew Stanciu
27d80f13cd [cli] Add ability to connect Git provider repo (#8100)
This PR adds a new subcommand `vc git`, which allows you to create a Git provider repository from your local `.git` config to your Vercel project.

Usage:

- `vc git connect` – connects a Git provider repository
- `vc git disconnect` – disconnects a Git provider repository

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-12 17:55:40 +00:00
Nathan Rajlich
8c668c925d [fs-detectors] Remove "solidstart" framework from FS API v2 detection (#8134)
"solidstart" framework doesn't need to use the FS API v2 Builder anymore since:

  1) Newer versions output Build Output API v3
  2) Older versions that output FS API v2 will be handled by the compatbility shim that was added in https://github.com/vercel/vercel/pull/7690
2022-07-12 17:03:00 +00:00
Nathan Rajlich
4b1b33c143 [cli] Add createdAt to project settings in vc pull (#8138)
The `createdAt` property is checked in the `detectBuilders()` function, so it needs to be present in the local copy of the project settings.
2022-07-12 08:12:32 +00:00
Gal Schlezinger
a8d4147554 [next] Add EdgeRuntime provider environment variable to the builder (#8130)
This will enable Next.js to compile and set `vercel` as the edge runtime provider (`EdgeRuntime` global),
which can allow different libraries/customers to have different code running depends on the runtime environment (`edge-runtime` vs `vercel`).

### Related Issues

- https://github.com/vercel/next.js/pull/38331

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

The Next.js feature was not merged yet, so it still can't be tested.

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-12 06:33:14 +00:00
Gal Schlezinger
09339f494d [next][build-utils] allow to declare assets in EdgeFunction constructor (#8127)
There's a [Next.js PR](https://github.com/vercel/next.js/pull/38492) to include binary assets in Edge Functions. This PR enables to declare assets in the `EdgeFunction` constructor.

This is a configuration level setting, compared to WebAssembly modules which the `middleware-manifest.json` converts into `import` statements. This is because the asset generation is done in Webpack and it is not dynamic, it's a static string. We can use AST to replace it but we're risking in:

* worse performance
* accidentally replacing a string that isn't an asset
2022-07-11 23:13:21 +00:00
Steven
ee4d772ae9 [tests] Add retry to npm tests (#8133)
These tests fail occasionally on GH Actions due to flakey network IO, so this PR adds retry logic.

https://github.com/vercel/vercel/runs/7289659634?check_suite_focus=true#step:13:328
2022-07-11 21:39:10 +00:00
Matthew Stanciu
61e8103404 [tests] Fix flaky Git meta test (#8132)
A test I wrote for a previous PR (https://github.com/vercel/vercel/pull/8112) was failing once in ~every 5 runs. This PR makes the test reliable.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-11 21:01:16 +00:00
Steven
fb4f477325 Publish Stable
- vercel@27.0.2
 - @vercel/fs-detectors@1.0.2
 - @vercel/node@2.4.2
2022-07-11 16:21:30 -04:00
P.B. To
016bff848e [fs-detectors] process detectFramework in parallel (#8128)
The Vercel API's `detect-framework` API contacts GitHub's API using its own file adapter. 

However, currently the `detectFramework` function in the Vercel CLI (on which the API depends) calls the file system adapter in series using a `for` loop. This results in large amounts of lag when in a networked situation such as calling GitHub's API as each API call takes a couple hundred milliseconds.

I propose changing `detectFramework` to process the directories it scans in parallel.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-07-11 20:16:04 +00:00
Steven
183e411f7c [cli] Remove DEBUG=corepack env var (#8131)
This debug log was originally added in #7871 because corepack has no output by default. In particular, it was nice to see the first deployment was not stalled when the package manager is being installed.

That being said, this gets noisy really fast because cache detections also print a log line.
For example, here's a deployment that prints 3 times:

```
Detected ENABLE_EXPERIMENTAL_COREPACK=1 and "npm@8.10.0" in package.json
Running "install" command: `npm install --prefix=.. --no-audit --engine-strict=false`...
2022-07-11T18:27:00.696Z corepack Reusing npm@8.10.0
356 packages are looking for funding
Running "npm run vercel-build"
2022-07-11T18:27:06.664Z corepack Reusing npm@8.10.0
> front@0.0.0 vercel-build
> npm run buildonly && npm run build:rss
2022-07-11T18:27:07.088Z corepack Reusing npm@8.10.0
> front@0.0.0 buildonly
> next build
```

I think its best to let users add this env var themselves if they want to debug what corepack is doing, so this PR removes that environment variable.
2022-07-11 19:31:04 +00:00
Sean Massa
070e300148 [node] add links to edge error messages (#8048)
Add links to some Edge errors.

---

Follow up to: https://github.com/vercel/vercel/pull/8007
2022-07-11 18:50:15 +00:00
Steven
cbdf9b4a88 [cli] Fix beta label (#8129)
This PR is a follow up to #7991 which incorrectly used a the empty string instead of empty array

- Maybe related to #8125
2022-07-11 18:05:36 +00:00
Steven
ec9b55dc81 Publish Stable
- vercel@27.0.1
 - @vercel/remix@1.0.7
2022-07-10 15:04:09 -04:00
Leon Salsiccia
06829bc21a [remix] fix monorepo support (#8077)
Co-authored-by: Steven <steven@ceriously.com>
2022-07-10 13:28:39 -04:00
Matthew Stanciu
628071f659 [cli] Debug log error messages in create-git-meta (#8112)
This is a follow-up to #8094 which debug logs errors.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 19:30:28 +00:00
Matthew Stanciu
5a7461dfe3 [cli] Explicitly use vc project vs. vc projects (#8113)
This is a follow-up to #8091 which:

- Makes `vc project` the default command, with `vc projects` aliased to `vc project` (previously it was not clear in the code which one was the "real" command)
- Makes some helper names for `ls` more specific

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 18:56:35 +00:00
Sean Massa
599f8f675c [tests] remove commented code (#8109)
Removes a leftover commented line.
2022-07-08 16:52:55 +00:00
Steven
0a8bc494fc [tests] Try building with ENABLE_VC_BUILD (#8110)
This will dogfood `vc build` which happens to improve this deployment time from 4 min down to 2 min.
2022-07-08 12:30:03 -04:00
Lee Robinson
34e008f42e [cli] Remove beta warning for vercel build (#7991)
This removes the `(beta)` output from `vercel build` as we move towards stability.

![image](https://user-images.githubusercontent.com/9113740/174356399-65f3d6bb-a241-49c8-9edb-167b25d6fa44.png)
2022-07-08 12:12:22 +00:00
Matthew Stanciu
037633b3f1 [cli] Refactor vc project (#8091)
Since the `vc project` command is about to be expanded with 2 new subcommands, I think it makes sense to do a little bit of refactoring. The current `vc project` command is all in one file, with the logic for subcommands nested within if statements. I think the structure of `vc project` should look more like `vc env`, which is consistent with how commands with subcommands look throughout the rest of the codebase.

This PR moves the logic for the `project` subcommands into their own files.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 01:34:13 +00:00
Matthew Stanciu
1a6f3c0270 [cli] Fix vc deploy erroring when .git is corrupt (#8094)
### Related Issues

- Fixes https://github.com/vercel/customer-issues/issues/597

`vc deploy` currently fails with a confusing error if the user has a corrupt `.git` directory. This is caused by `create-git-meta` improperly handling the scenario where it detects a `.git` directory but cannot find git information. This PR handles this error.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-08 01:07:43 +00:00
Nathan Rajlich
0a2af4fb94 Publish Stable
- @vercel/build-utils@5.0.1
 - vercel@27.0.0
 - @vercel/client@12.1.0
 - @vercel/frameworks@1.1.0
 - @vercel/fs-detectors@1.0.1
 - @vercel/go@2.0.5
 - @vercel/hydrogen@0.0.2
 - @vercel/next@3.1.4
 - @vercel/node@2.4.1
 - @vercel/python@3.0.5
 - @vercel/redwood@1.0.6
 - @vercel/remix@1.0.6
 - @vercel/ruby@1.3.13
 - @vercel/static-build@1.0.5
2022-07-07 16:31:30 -07:00
Nathan Rajlich
3fb1c50142 [node] Provide correct middlewarePath when using "Root Directory" setting (#8097)
The `middlewarePath` property was not correctly stripping
the root directory setting from the value, causing Middleware
to not work correctly with Root Directory.
2022-07-07 15:25:24 -07:00
Nathan Rajlich
de033c43fd [cli] Support Builders that don't define version property (#8099)
`vercel-sapper` is a case of a 3rd party Builder that does not export a `version` property. In this case, we should treat it the same as a `version: 2` Builder since its return value is compatible.
2022-07-07 21:30:53 +00:00
Nathan Rajlich
f8e5df749c [frameworks] Add "hydrogen" framework preset (#8073)
Adds "hydrogen" framework preset for Shopify Hydrogen framework to allow for zero-config usage of the `@vercel/hydrogen` Builder.
2022-07-07 20:52:03 +00:00
Steven
5670acc2cc Revert "[cli] Add vc project connect command" (#8096)
Revert "[cli] Add `vc project connect` command (#8014)"

This reverts commit 32ebcd83a7.
2022-07-07 16:03:52 -04:00
Nathan Rajlich
5205047851 [tests] Fix invoke path of update-canary-tag.js script (#8092)
See related CI failure: https://github.com/vercel/vercel/runs/7228051741?check_suite_focus=true
2022-07-07 13:01:22 +00:00
Nathan Rajlich
1edc2d06c9 Publish Stable
- @vercel/hydrogen@0.0.1
2022-07-06 13:51:43 -07:00
Nathan Rajlich
fdb15b2539 [hydrogen] Add @vercel/hydrogen Builder (#8071)
Adds a new `@vercel/hydrogen` Builder package so that Vercel can support Shopify Hydrogen projects with zero config. It outputs an Edge Function for the server-side render code and includes a catch-all route to invoke that function after a `handle: "filesystem"` to serve static files that were generated by the build command.

**Examples:**

 * [`hello-world-ts` template](https://hydrogen-hello-world-otm2vmw6w-tootallnate.vercel.app/)
 * [`demo-store-ts` template](https://hydrogen-demo-store-1gko2fst3-tootallnate.vercel.app/)
2022-07-06 20:06:45 +00:00
Matthew Stanciu
32ebcd83a7 [cli] Add vc project connect command (#8014)
This PR adds a new subcommand, `vc project connect`, which connects a Git provider repository to the current project. Previously, this could only be done via the Dashboard.

This is the first part of a larger project—the goal is to include this functionality within `vc link`, so that you never have to leave the CLI if you want to set up a new Vercel project that's connected to Git.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-06 19:49:08 +00:00
Matthew Stanciu
2e43b2b88a [cli] Remove redundant mock project endpoint (#8089)
- https://github.com/vercel/vercel/pull/8053

While writing tests for this PR, I added a mock project endpoint that always returned a default project. This was probably incorrect and no longer needed (tests pass without it). I should have removed it before merging #8053, but I didn't catch this before merging.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-06 19:17:33 +00:00
Matthew Stanciu
f83d432fcd [cli] MAJOR: Scope vc ls to linked project (#8053)
Currently, `vc ls` is scoped to your team, and you have to type out a project name if you want to see deployments for a project. This PR instead scopes it to the linked project.

Under these changes, `vc ls` still works similarly to how it currently works: users can still specify a project name to get the deployments for a project. The difference is that:

1. The selected team is the one that the linked project belongs to, instead of the one that the user has selected.
2. `vc ls` with no arguments displays the latest deployments for the linked project.

This is the first part of a larger effort to change the behavior of `vc ls`, plucked from https://github.com/vercel/vercel/pull/7993. More PRs to follow.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-06 18:42:51 +00:00
Matthew Stanciu
87fc38e860 [cli] MAJOR: remove vc deploy clipboard copy feature (#8085)
https://vercel.slack.com/archives/C03F2CMNGKG/p1656971502881949

Right now, `vc deploy` automatically copies the deploy url to your clipboard after the deployment has finished. You can opt out via the `--no-clipboard` flag, but the feature is enabled by default.

This is strange behavior—there's no indication that the CLI will hijack your clipboard, and you don't know it's been hijacked until after it happens.

This PR removes the clipboard copying feature as well as the `--no-clipboard` flag.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-06 18:04:13 +00:00
JJ Kasper
afc4388fc0 [tests] Update canary dist-tag on publish (#8084)
This ensures we update the canary dist-tag when publishing stable releases as we no longer need to do separate canary publishes.  

### Related Issues

x-ref: [slack thread](https://vercel.slack.com/archives/C65QW9PN1/p1657049930618119?thread_ts=1656362480.574099&cid=C65QW9PN1)

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-07-05 23:45:45 +00:00
Steven
3c48b40b43 Publish Canary
- @vercel/build-utils@5.0.1-canary.0
 - vercel@26.0.1-canary.1
 - @vercel/client@12.0.5-canary.0
 - @vercel/go@2.0.5-canary.0
 - @vercel/next@3.1.4-canary.1
 - @vercel/node@2.4.1-canary.0
 - @vercel/python@3.0.5-canary.0
 - @vercel/redwood@1.0.6-canary.0
 - @vercel/remix@1.0.6-canary.0
 - @vercel/ruby@1.3.13-canary.0
 - @vercel/static-build@1.0.5-canary.0
2022-07-05 17:44:26 -04:00
JJ Kasper
ce89f00328 Publish Canary
- vercel@26.0.1-canary.0
 - @vercel/next@3.1.4-canary.0
2022-07-05 14:17:51 -05:00
Steven
621b53bc49 Publish Stable
- @vercel/build-utils@5.0.0
 - vercel@26.0.0
 - @vercel/client@12.0.4
 - @vercel/fs-detectors@1.0.0
 - @vercel/go@2.0.4
 - @vercel/next@3.1.3
 - @vercel/node@2.4.0
 - @vercel/python@3.0.4
 - @vercel/redwood@1.0.5
 - @vercel/remix@1.0.5
 - @vercel/ruby@1.3.12
 - @vercel/static-build@1.0.4
2022-07-05 08:47:49 -04:00
JJ Kasper
728b620355 Update trailing slash handling for _next/data resolving (#8080) 2022-07-04 09:52:08 -05:00
Luc Leray
7d16395038 [build-utils] Stricter getNodeBinPath return type (#8082)
In https://github.com/vercel/vercel/pull/8058, I made `getNodeBinPath` return type too broad. The function can never return `undefined` so we can make it more strict.
2022-07-04 13:22:35 +00:00
Lee Robinson
59e1259688 [examples] Remove stray console.log. (#8070) 2022-07-01 22:47:36 +00:00
Nathan Rajlich
169242157e [cli] Fix Middleware "matchers" with query string in vc dev (#8069)
`matchers` config in Middleware was not being properly… matched… in `vc dev` when a query string was present.
2022-07-01 20:38:02 +00:00
Steven
db10ffd679 [build-utils][next][redwood][remix][static-build] Fix corepack path prepend (#8065)
This PR fixes a bug where corepack (#7871) was not correctly setup because the lockfile autodetection and node version autodetection was overriding the PATH.

It also fixes a bug where the log output was printed twice because we incorrectly prepended the PATH twice.
2022-07-01 19:33:21 +00:00
Steven
c0d0744c4e [tests] Add yarn.lock to saber test fixture (#8068)
The Saber example had a lock file but the test did not.

This PR copies the lock file since the latest version of Saber doesn't work with the latest Vue.

```
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package' is not defined by "exports" in /vercel/path0/node_modules/vue/package.json
  at new NodeError (node:internal/errors:372:5)
  at throwExportsNotFound (node:internal/modules/esm/resolve:472:9)
  at packageExportsResolve (node:internal/modules/esm/resolve:753:3)
  at resolveExports (node:internal/modules/cjs/loader:482:36)
  at Function.Module._findPath (node:internal/modules/cjs/loader:522:31)
  at Function.Module._resolveFilename (node:internal/modules/cjs/loader:919:27)
  at Function.resolve (node:internal/modules/cjs/helpers:108:19)
  at module.exports (/vercel/path0/node_modules/saber/dist/webpack/webpack.config.js:30:58)
  at Saber.getWebpackConfig (/vercel/path0/node_modules/saber/dist/index.js:195:58)
  at VueRenderer.build (/vercel/path0/node_modules/saber/dist/vue-renderer/index.js:232:39)
```
2022-07-01 19:04:44 +00:00
Steven
9da67423a5 [node] Fix public TypeScript types (#8064)
There was a regression some time between [1.12.2-canary.6](https://unpkg.com/browse/@vercel/node@1.12.2-canary.6/dist/index.d.ts) and [1.12.2-canary.7](https://unpkg.com/browse/@vercel/node@1.12.2-canary.7/dist/index.d.ts) where the `index.d.ts` types changed.

This PR reverts back to the old behavior so users can properly use

```ts
import { VercelRequest, VercelResponse } from '@vercel/node';
```

The existing test was also changed to use the result of the build rather than copy at the beginning

cfae7ec3c2/packages/node/test/fixtures/15-helpers/ts/index.ts (L1)

- Fixes https://github.com/vercel/vercel/issues/7951
2022-06-30 22:45:49 +00:00
Steven
51fe09d5e9 [build-utils][fs-detectors][cli] MAJOR: move some of build-utils into new fs-detectors (#8054)
The `@vercel/build-utils` package was meant be shared functions necessary for writing a Vercel Builder (aka Runtime).

This package has since bloated into the catch-all package for anything that wasn't a Builder.

This PR removes the bloat in favor of a new package, `@vercel/fs-detectors`. It also removes the need for `@vercel/build-utils` to have a dependency on `@vercel/frameworks`.

- Related to #7951
2022-06-30 21:14:07 +00:00
Nathan Rajlich
695bfbdd60 [cli] Add full stdio mockability for unit tests (#8052)
This PR is a follow-up to #8039, which provides an intuitive syntax for writing unit tests for interactive CLI commands.

The heart of this is the new `await expect(stream).toOutput(test)` custom Jest matcher, which is intended for use with the mock Client `stdout` and `stderr` stream properties. The `test` is a string that will wait for the stream to output via "data" events until a match is found, or it will timeout (after 3 seconds by default). The timeout error has nice Jest-style formatting so that you can easily identify what was output:

<img width="553" alt="Screen Shot 2022-06-29 at 10 33 06 PM" src="https://user-images.githubusercontent.com/71256/176600324-cb1ebecb-e891-42d9-bdc9-4864d3594a8c.png">

Below is an example of a unit test that was added for an interactive `vc login` session:

```typescript
it('should allow login via email', async () => {
  const user = useUser();

  const exitCodePromise = login(client);

  // Wait for login interactive prompt
  await expect(client.stderr).toOutput(`> Log in to Vercel`);

  // Move down to "Email" option
  client.stdin.write('\x1B[B'); // Down arrow
  client.stdin.write('\x1B[B'); // Down arrow
  client.stdin.write('\x1B[B'); // Down arrow
  client.stdin.write('\r'); // Return key

  // Wait for email input prompt
  await expect(client.stderr).toOutput('> Enter your email address:');

  // Write user email address into prompt
  client.stdin.write(`${user.email}\n`);

  // Wait for login success message
  await expect(client.stderr).toOutput(
    `Success! Email authentication complete for ${user.email}`
  );

  // Assert that the `login()` command returned 0 exit code
  await expect(exitCodePromise).resolves.toEqual(0);
});
```

**Note:**  as a consequence of this PR, prompts are now written to stderr instead of stdout, so this change _may_ be considered a breaking change, in which case we should tag it for major release.
2022-06-30 20:17:22 +00:00
Nathan Rajlich
547e88228e Publish Stable
- @vercel/build-utils@4.2.1
 - vercel@25.2.3
 - @vercel/client@12.0.3
 - @vercel/go@2.0.3
 - @vercel/next@3.1.2
 - @vercel/node@2.3.3
 - @vercel/python@3.0.3
 - @vercel/redwood@1.0.4
 - @vercel/remix@1.0.4
 - @vercel/ruby@1.3.11
 - @vercel/static-build@1.0.3
2022-06-30 12:24:13 -07:00
Luc Leray
9bfb5dd535 [build-utils] Handle npm bin exit code 7 (#8058)
In some rare cases, `npm bin` exits with code 7, but still outputs the right bin path.

To reproduce, try:
```
npm init -y
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
vc
# enter "echo build" for the build command, leave the other configuration as default
```

The build will fail with `Error: Command exited with 7` because `npm bin` fails with code 7, for some reason.

In this PR, we do 2 things:
(1) Ignore exit codes from `npm bin`. It still outputs the right path when it exits with code 7 so we just read the output and check if it's a valid path.
(2) Throw a more specific error message when `npm bin` fails to give us the bin path. The current error was hard to debug because it looked like it was coming from the install commmand. We can do better by emitting a custom error.

Alternative considered for (2): Do not throw errors. If `npm bin` fails, emit a warning and let the build continue.

Related Issues:
- https://github.com/vercel/customer-issues/issues/585 (internal)
2022-06-30 17:27:52 +00:00
Nathan Rajlich
81ea84fae8 [cli] Fix vc build lambda serialization when there's a broken symlink (#8050)
There's some cleanup directory walking logic that was choking when
a Lambda outputs a file with a broken symlink. We shouldn't need to
traverse into those directories in the case of a symlink anyways, so use
`lstat()` instead of `stat()` to prevent that filesystem call from
throwing an error.
2022-06-29 16:04:31 -07:00
Nathan Rajlich
fa8bf07be4 [cli] Add Client#stdin / Client#stdout (#8039)
This will allow for mockability of the input streams (i.e. prompts)
for CLI commands in unit tests.

**Example:**

```typescript
import confirm from '../../src/util/input/confirm';
import { client } from '../mocks/client';

describe('MockClient', () => {
  it('should mock `confirm()`', async () => {
    const confirmedPromise = confirm(client, 'Do the thing?', false);

    client.stdin.write('yes\n');

    const confirmed = await confirmedPromise;
    expect(confirmed).toEqual(true);
  });
});
```
2022-06-29 16:03:56 -07:00
JJ Kasper
cc9dce73ad [next] Ensure uncompressed limit is correct (#8049)
* Revert "Revert "[next] Update max size warning to handle initial layer better" (#8047)"

This reverts commit 8c62de16ce.

* Ensure uncompressed limit is correct

* apply suggestion
2022-06-29 15:13:15 -05:00
Sean Massa
bba7cbd411 [cli][dev] fix: creating "api/some-func.js" after "vc dev" now works (#8041)
If there is no `api` directory, then you run `vc dev`, then you create a new function `api/some-func.js`, then this file would not be served as a new function.

This was being caused by incomplete "new file" handling logic. This PR ensures that the proper detection is done in each new file (`getVercelConfig`) that populates key properties (`apiDir`, `apiExtensions`, and extensionless `files`) for determining when a file is used to serve a request.
2022-06-29 18:37:48 +00:00
Steven
9a3739bebd Publish Stable
- vercel@25.2.2
 - @vercel/next@3.1.1
 - @vercel/node@2.3.2
 - @vercel/redwood@1.0.3
 - @vercel/remix@1.0.3
2022-06-29 09:27:34 -04:00
Gal Schlezinger
8c62de16ce Revert "[next] Update max size warning to handle initial layer better" (#8047)
Revert "[next] Update max size warning to handle initial layer better (#8013)"

This reverts commit f20703b15d.
2022-06-29 09:26:29 -04:00
Steven
e9333988d7 [next][node][redwood][remix] Bump @vercel/nft to 0.20.1 (#8042)
- https://github.com/vercel/nft/releases/tag/0.20.0
- https://github.com/vercel/nft/releases/tag/0.20.1
2022-06-29 00:21:14 +00:00
Nathan Rajlich
fb001ce7eb [tests] Remove TODO comments from Middleware matchers vc dev test (#8037) 2022-06-28 18:26:56 +00:00
Sean Massa
b399fe7037 Publish Stable
- vercel@25.2.1
 - @vercel/node@2.3.1
2022-06-28 12:04:57 -05:00
Sean Massa
88385b3c84 [cli][node] switch to esbuild for compiling edge functions (#8032) 2022-06-28 11:27:56 -05:00
Steven
eed39913e1 Publish Stable
- @vercel/build-utils@4.2.0
 - vercel@25.2.0
 - @vercel/client@12.0.2
 - @vercel/edge@0.0.1
 - @vercel/frameworks@1.0.2
 - @vercel/go@2.0.2
 - @vercel/next@3.1.0
 - @vercel/node@2.3.0
 - @vercel/python@3.0.2
 - @vercel/redwood@1.0.2
 - @vercel/remix@1.0.2
 - @vercel/routing-utils@1.13.5
 - @vercel/ruby@1.3.10
 - @vercel/static-build@1.0.2
2022-06-28 10:15:33 -04:00
Gal Schlezinger
03e9047bc9 [@vercel/edge] add header helpers (#8036)
* [@vercel/edge] add header helpers

* rename getIp => ipAddress, getGeo => geolocation, as suggested by @kikobeats
2022-06-28 11:26:18 +02:00
Nathan Rajlich
0e35205bf1 [cli][dev][node] Support matchers config for Middleware in vc dev (#8033)
Adds support for `config.matchers` exported property in Middleware during `vc dev`.
2022-06-28 08:34:48 +00:00
Steven
e42fe34c4a [tests] Bump turbo to 1.3.1 (#8011)
https://turborepo.org/blog/turbo-1-3-0
2022-06-28 04:48:35 +00:00
Sean Massa
3ece7ac969 [cli][node] make error handling of edge functions consistent with serverless functions in vc dev (#8007)
When edge functions error, they were showing a basic text response instead of the error template. They were also sending a 502 status code instead of a 500.

This PR makes the error handling of Edge Functions consistent with Serverless Functions when executed through `vc dev`. If we want to update the error templates themselves, we can do that in a separate PR.

**Note:** Production currently treats Edge Function errors differently from Serverless Function errors, but that's a known issue that will be resolved.

---

*I deleted the original outputs (terminal and browser screenshots) because they are out of date and added a lot of content to this page. See my latest comment for updated examples.*
2022-06-28 04:12:21 +00:00
Nathan Rajlich
4f832acf90 [remix] Don't depend on @remix-run/vercel (#8029)
Instead, just add it to the project's `package.json` file before installing the dependencies.

Fixes warning about missing peer dependencies when installing CLI.

<img width="409" alt="CleanShot 2022-05-22 at 09 40 09@2x" src="https://user-images.githubusercontent.com/71256/176084428-79e964b3-8b20-416d-bf3f-c5bd36f4b0ff.png">

Now, a warning is shown in the Deployment build logs, saying that the dep was added, but that the user should commit the change:

<img width="931" alt="Screen Shot 2022-06-27 at 8 15 19 PM" src="https://user-images.githubusercontent.com/71256/176084377-dab5f7d3-4e9f-4bf6-baee-63708b65f218.png">
2022-06-28 03:29:50 +00:00
Matthew Stanciu
918726e01d [cli] Support "http:" scheme in vc bisect (#8023)
`vc bisect` currently prepends `https://` to a passed-in url if it doesn't begin with https—which means that if someone passes in a url that begins with `http://`, it'll turn the url into `https://http://url.com`. This PR fixes this.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-28 02:15:59 +00:00
JJ Kasper
dc2ddf867b Publish Stable
- @vercel/next@3.0.5
2022-06-27 15:37:09 -05:00
Nathan Rajlich
ee1211416f [cli] Support root-level Middleware file in vc dev (#7973)
Adds initial support for a root-level `middleware.js` / `middleware.ts` file in the `vercel dev` CLI command. This leverages the existing Edge Function invoking logic in `@vercel/node`'s `startDevServer()` function and applies the necessary response / rewrites / mutations to the HTTP request based on the result of the middleware invocation.
2022-06-27 19:56:32 +00:00
JJ Kasper
570fd24e29 Publish Canary
- vercel@25.1.1-canary.11
 - @vercel/edge@0.0.1-canary.0
 - @vercel/next@3.0.5-canary.1
2022-06-27 12:35:09 -05:00
Gal Schlezinger
40681ad0f4 [next] allow to declare edge functions outside of /api/ in Next.js (#7997) 2022-06-27 11:55:39 -05:00
JJ Kasper
f20703b15d [next] Update max size warning to handle initial layer better (#8013)
* Update max size warning to handle initial layer better

* update test
2022-06-27 10:22:48 -05:00
Gal Schlezinger
68eb197112 Add @vercel/edge with helpers for Middleware (#8022) 2022-06-27 17:37:46 +03:00
JJ Kasper
b8b87b96da Publish Canary
- vercel@25.1.1-canary.10
 - @vercel/next@3.0.5-canary.0
2022-06-25 15:04:32 -05:00
JJ Kasper
967c24f1bb [next] Ensure trailing slash is handled while resolving _next/data rewrites (#8015)
* Ensure trailing slash is handled while resolving _next/data rewrites

* update regex
2022-06-25 12:39:30 -05:00
JJ Kasper
609f781234 Publish Stable
- @vercel/next@3.0.4
2022-06-24 23:16:40 -05:00
JJ Kasper
998f6bf6e6 Publish Canary
- vercel@25.1.1-canary.9
 - @vercel/next@3.0.4-canary.3
 - @vercel/node@2.2.1-canary.1
2022-06-23 15:45:31 -05:00
JJ Kasper
7511c2ef85 [next] Fix normalizing for _next/data/index.json route with middleware (#8005)
* Fix normalizing for _next/data/index.json route with middleware

* ensure / -> /index.json denormalizes as well

* add continue/override
2022-06-23 14:57:07 -05:00
Michaël De Boey
71425fac1f [examples] Update remix template (#7917) 2022-06-21 12:20:01 -07:00
Lee Robinson
6973cd5989 [examples] Address follow up from SvelteKit example update (#8002) 2022-06-21 12:10:55 -07:00
Nathan Rajlich
24785ff50a [node] Implement matcher config support for Middleware (#8001)
Allows to specify a string or array of paths/globs for when a root-level
Middleware should be invoked.

```javascript
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}
```
2022-06-21 12:10:13 -07:00
Nathan Rajlich
aa3ad4478c [cli] Only show "Removing .vercel/output" log when directory exists in vc dev (#8004)
Conditionally show the "Removing…" message, instead of always rendering it.
2022-06-21 19:04:24 +00:00
JJ Kasper
f0d73049ca Publish Canary
- vercel@25.1.1-canary.8
 - @vercel/next@3.0.4-canary.2
2022-06-21 10:09:24 -05:00
JJ Kasper
6cef07354a [next] Ensure basePath is matched correctly for _next/data resolving (#7999)
This ensures we correctly match `basePath` for the `_next/data` resolving routes. The tests in the below referenced PR already cover this change so no new fixtures have been added here as they would rely on those changes landing first. 

### Related Issues

x-ref: https://github.com/vercel/next.js/pull/37854

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-20 22:29:01 +00:00
Nathan Rajlich
50af9f5b75 Publish Canary
- vercel@25.1.1-canary.7
 - @vercel/next@3.0.4-canary.1
2022-06-20 00:19:54 -07:00
Seiya Nuta
af76b134d8 [next] Check the size of WASM files in the Edge Functions size validation (#7936)
This is the last missing piece in the size validation of edge functions. Since WASM binaries are not bundled in the user JavaScript file, we also need to count their sizes in the validation.

### Related Issues

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-20 04:58:36 +00:00
Nathan Rajlich
c7640005fd [cli] Implement vc build --output parameter (#7995)
Use `--output` parameter to output the Build Output API build artifacts to a different location than the default `.vercel/output` directory.
2022-06-18 23:10:33 +00:00
Nathan Rajlich
3deed977ba [cli] Add requirePath to "builds.json" in vc build (#7990)
Include a `requirePath` property to each "build" in the `builds.json` file which is the absolute path to the Builder entrypoint that was executed.

This gives context as to which Builder was invoked by `vc build` which helps with introspection / debugging.
2022-06-18 15:07:46 +00:00
Nathan Rajlich
b38c360e36 [cli] Include "argv" in builds.json produced by vc build (#7988)
It will be useful for debugging purposes to have access to the arguments that were passed to `vc build`.
2022-06-17 22:18:33 +00:00
JJ Kasper
1595e48414 Publish Canary
- vercel@25.1.1-canary.6
 - @vercel/next@3.0.4-canary.0
2022-06-17 16:32:59 -05:00
JJ Kasper
e6b0ee3e3c [next] Update data regex to be less specific (#7994)
Update data regex to be less specific
2022-06-17 16:32:33 -05:00
JJ Kasper
a247e31688 Publish Stable
- @vercel/next@3.0.3
2022-06-17 14:49:54 -05:00
JJ Kasper
dc02e763a4 Publish Canary
- vercel@25.1.1-canary.5
 - @vercel/next@3.0.3-canary.2
2022-06-17 14:24:39 -05:00
JJ Kasper
8567fc0de6 [next] Optimize _next/data route regex (#7992)
Optimize _next/data route regex
2022-06-17 14:19:37 -05:00
JJ Kasper
4f8f3d373f Publish Canary
- vercel@25.1.1-canary.4
 - @vercel/next@3.0.3-canary.1
 - @vercel/node@2.2.1-canary.0
2022-06-16 18:00:29 -05:00
JJ Kasper
debb85b690 [next] Update error for internal missing page (#7987)
Update error for internal missing page
2022-06-16 16:44:12 -05:00
Thai Pangsakulyanont
bfef989ada [examples] Delete pnpm-lock.yaml from examples/sveltekit because it already contains yarn.lock (#7983)
Delete pnpm-lock.yaml

Co-authored-by: Lee Robinson <lrobinson2011@gmail.com>
2022-06-16 10:24:18 -07:00
JJ Kasper
4e0b6c5eaf [next] Update to skip middleware for on-demand revalidate (#7978)
Update to skip middleware for on-demand revalidate
2022-06-16 11:23:30 -05:00
Nathan Rajlich
0ace69ef75 Publish Stable
- @vercel/node@2.2.0
2022-06-15 13:01:57 -07:00
Nathan Rajlich
b7b7923f92 Publish Canary
- vercel@25.1.1-canary.3
 - @vercel/next@3.0.3-canary.0
 - @vercel/node@2.1.1-canary.2
2022-06-15 11:56:32 -07:00
Nathan Rajlich
8167233c56 [tests] Create tarballs of all packages in Vercel deployment (#7967)
For the Vercel deployment, run `yarn pack` in each of the packages in the monorepo and place them in the "public/tarballs" directory so that we can have npm-installable URLs of each package for every commit. The `package.json` of each package is also updated to reference the tarball when a package depends on other packages within the monorepo.

Try it out like:

```
$ npm i -g https://vercel-biww73ffq.vercel.sh/tarballs/vercel.tgz

# Notice how the package.json has the monorepo dependencies
# updated to point to the related tarballs from the same deployment:

$ cat /usr/local/lib/node_modules/vercel/package.json | grep biww
    "@vercel/build-utils": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/build-utils.tgz",
    "@vercel/go": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/go.tgz",
    "@vercel/next": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/next.tgz",
    "@vercel/node": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/node.tgz",
    "@vercel/python": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/python.tgz",
    "@vercel/redwood": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/redwood.tgz",
    "@vercel/remix": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/remix.tgz",
    "@vercel/ruby": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/ruby.tgz",
    "@vercel/static-build": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/static-build.tgz",
    "@vercel/client": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/client.tgz",
    "@vercel/frameworks": "https://vercel-biww73ffq.vercel.sh/tarballs/@vercel/frameworks.tgz",

# Also notice that the "version" in `package.json` gets updated to include the commit
# SHA so that we can easily identify which commit a given tarball was generated from:

$ vercel --version
25.1.1-canary.1-727b290
```
2022-06-15 17:44:23 +00:00
Michaël De Boey
32ee6aba92 [node] Move @vercel/build-utils to "dependencies" (#7934)
fix(node): move `@vercel/build-utils` to `dependencies`

Co-authored-by: Steven <steven@ceriously.com>
2022-06-15 13:18:02 -04:00
Nathan Rajlich
b48f7a7e6e [node] Add missing dependencies for dev-server.ts (#7969)
Some new imports were introduced recently but are missing from the `"dependencies"` object, so they were missing when the package was installed from the npm registry.
2022-06-15 01:38:17 +00:00
JJ Kasper
a961c9b992 Publish Stable
- @vercel/next@3.0.2
2022-06-14 17:35:24 -05:00
JJ Kasper
cf7c50d691 Publish Canary
- vercel@25.1.1-canary.2
 - @vercel/next@3.0.2-canary.2
2022-06-14 14:57:54 -05:00
JJ Kasper
f4be388a1f [next] Fix _next/data resolving priority with afterFiles (#7968)
* Fix _next/data resolving priority with afterFiles

* ensure x-nextjs-matched-path header is set
2022-06-14 14:52:49 -05:00
Nathan Rajlich
c1bc53dea8 Publish Canary
- @vercel/build-utils@4.1.1-canary.1
 - vercel@25.1.1-canary.1
 - @vercel/client@12.0.2-canary.1
 - @vercel/frameworks@1.0.2-canary.0
 - @vercel/go@2.0.2-canary.1
 - @vercel/next@3.0.2-canary.1
 - @vercel/node@2.1.1-canary.1
 - @vercel/python@3.0.2-canary.1
 - @vercel/redwood@1.0.2-canary.1
 - @vercel/remix@1.0.2-canary.1
 - @vercel/routing-utils@1.13.5-canary.0
 - @vercel/ruby@1.3.10-canary.1
 - @vercel/static-build@1.0.2-canary.1
2022-06-14 09:15:20 -07:00
Nathan Rajlich
6855e3df54 [tests] Allow for multiple probes (#7957)
The `probes` in the vercel.json files in tests fixtures weren't allowing both `mustContain` _and_ `responseHeaders` probes due to an `else` block. This PR allows to combine those checks into the same probe test.
2022-06-14 06:54:16 +00:00
Nathan Rajlich
0d39dbd1d9 [routing-utils] Convert tests to TypeScript (#7959)
Allows for the tests to be run without rebuilding the source code.
2022-06-13 23:29:00 -07:00
Nathan Rajlich
509c85182a [tests] Update ts-jest to v28.0.5 (#7962)
Gets rid of the annoying ts-jest update warning:

```
ts-jest[ts-jest-transformer] (WARN) Use 'ts-jest' entry point in v28 will resolve in refactored transformer. If you wish to use legacy transformer, please adjust your Jest 'transform' config. For example:
     module.exports = {
        transform: {
           '^.+\\.tsx?$': 'ts-jest/legacy'
        }
     }
```
2022-06-13 23:18:23 -07:00
Sean Massa
ae801e563d [cli] add support for Edge Functions in vercel dev (#7925)
Add support for Edge Functions when using `vercel dev`. This uses the [`edge-runtime`](https://edge-runtime.vercel.app/) to emulate the proper environment, [`ncc`](https://github.com/vercel/ncc) to parse the entrypoint (supporting TypeScript and importing node modules), and some custom marshaling to pass the request/response values through.
2022-06-13 22:50:16 +00:00
Steven
0e8278f490 [cli] Fix vc dev when user has ipv6 configured (#7956)
Running tests from #7455 

- Fixes #7306
- Closes #7455
2022-06-13 21:04:00 +00:00
Steven
0d302a6f48 [cli] Add alias for vc develop => vc dev (#7955)
This will make it easier for users who are used to `gatsby develop` or other CLIs that abbreviate to [`develop`](https://twitter.com/DiverseCauses/status/1536358138676486146).

Fixes this:
![image](https://user-images.githubusercontent.com/229881/173387083-7afd01db-00ab-4670-a07d-fa6c8cecea5d.png)
2022-06-13 17:01:46 +00:00
Lee Robinson
4e4f5f28a2 [examples] Add Vercel Analytics for CRA/SvelteKit templates. (#7894)
Successfully tested with:

- [CRA](https://front-git-analytics-for-all-frameworks.vercel.sh/now-examples/create-react-app/analytics?device=desktop&timePeriod=1d&percentile=75&timeInterval=3h&sort=datapoints&search=)
- [SvelteKit](https://front-git-analytics-for-all-frameworks.vercel.sh/now-examples/sveltekit/analytics?device=desktop&timePeriod=1d&percentile=75&timeInterval=3h&sort=datapoints&search=)

Dependencies have also been updated 👍
2022-06-13 16:30:14 +00:00
Steven
5205a4ec4b [cli] Fix vc dev with dynamic paths and ESM (#7946)
In a previous PR, the entrypoint path extension was stripped in `vc dev` to match the behavior of production deployments (`/api/user.js` => `/api/user`). However, it was supposed to map back to the original file extension before invoking the matching builder. This PR fixes the mapping for dynamic paths with ESM, such as `/api/[id].mjs` => `/api/[id]`.
2022-06-12 17:54:21 +00:00
Nathan Rajlich
2c15e496ed [tests] Remove as any casting in unit tests (#7945)
Maybe this casting was necessary at some point, but it doesn't seem to be anymore.
2022-06-10 22:30:51 +00:00
Matthew Stanciu
1f0ca46626 [cli] Add git metadata to vc deploy (#7910)
Currently, git metadata is not included on the Vercel dashboard when a project is deployed via the CLI. This PR populates the git metadata.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-10 20:53:47 +00:00
Steven
17cb5f1bc6 [cli] Remove boxen for warnings (#7944)
This never worked correctly and its too verbose. Lets remove boxen in favor of yellow text.

## Before

<img width="1432" alt="before" src="https://user-images.githubusercontent.com/229881/173138338-1cd1c81c-f294-4752-a08f-f80466fbd21c.png" />

## After

<img width="1432" alt="after" src="https://user-images.githubusercontent.com/229881/173138813-f9ff4de1-5bb7-4b60-8609-6abdd0e86fa0.png">
2022-06-10 20:28:00 +00:00
Nathan Rajlich
b095031292 [cli] Normalize "src" property in vc build (#7943)
This matches the production behavior.
2022-06-10 18:53:01 +00:00
Ethan Arrowood
f50bcbc0ba [cli] Implement vercel.json configuration overrides (#7811)
### Related Issues

This PR adds vercel.json configuration overrides support for both new and existing project flows through the CLI. It includes integration and unit tests.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-10 16:46:46 +00:00
JJ Kasper
4bf6295d7a Publish Canary
- @vercel/build-utils@4.1.1-canary.0
 - vercel@25.1.1-canary.0
 - @vercel/client@12.0.2-canary.0
 - @vercel/go@2.0.2-canary.0
 - @vercel/next@3.0.2-canary.0
 - @vercel/node@2.1.1-canary.0
 - @vercel/python@3.0.2-canary.0
 - @vercel/redwood@1.0.2-canary.0
 - @vercel/remix@1.0.2-canary.0
 - @vercel/ruby@1.3.10-canary.0
 - @vercel/static-build@1.0.2-canary.0
2022-06-10 09:37:52 -05:00
JJ Kasper
a4001ce10b [next] Add handling for resolving _next/data URLs (#7935)
* Add handling for resolving _next/data URLs

* update check

* update routes

* remove extra routes

* Update resolving
2022-06-10 08:50:59 -05:00
Nathan Rajlich
2df3432d88 [node] Add zero-config support for root-level middleware.js/middleware.ts file (#7928)
Adds support for `config.middleware` property in `@vercel/node` to output an Edge Middleware (by including a catch-all route in the output).

Also updates the zero-config detection logic to schedule a build of root-level `middleware.js`/`middleware.ts` files with `@vercel/node` with this middleware option enabled.
2022-06-09 23:04:39 +00:00
chloetedder
bcfc19de12 [build-utils] Add getProjectPaths that will evaluate if there is a monorepo (#7826)
<img width="1174" alt="Screen Shot 2022-05-19 at 3 53 55 PM" src="https://user-images.githubusercontent.com/35947020/169402845-4eb70a24-9954-44ac-8726-a6b56161d58c.png">

If there is no workspace detected, we still want to check if there are potential projects in the monorepo. An example of this case would be:

<img width="202" alt="Screen Shot 2022-05-19 at 10 29 12 AM" src="https://user-images.githubusercontent.com/35947020/169336400-e69845e3-616e-4857-80fe-c2614a65352a.png">

The directories `backend/app-three`, `frontend/app-one`, `frontend/app-two` all have definitions within their `package.json` files that define a framework assosiated with the project. This is what distinguishes them from the `package.json` files within `backend` and `frontend`

`getProjectPaths` loops through a depth of 3 (this can be changed but in most cases of this it won't be more than 3). For each directory it recursively calls itself until it finds a `package.json` file. It then checks if the directory has a framework defined and if so it adds the path to the final return value of all projects.

If any `skipPaths` are sent into the function, no projects will be looked for within those path(s). This would include the case for a hybrid monorepo where a workspace has already been detected

### Related Issues

> Fixes #1
> Related to #2

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-09 15:15:25 +00:00
Matthew Stanciu
04381c669b [cli] Pull correct env variables in vc build command (#7933)
`vc build` runs `vc pull` if no project settings are found locally. By default, `vc pull` pulls development environment variables, but we want either preview or production environment variables in the context of `vc build`. This PR adds a flag to the internal `vc pull` call to pull the correct environment variables.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-06-09 06:15:30 +00:00
Steven
0c7b54edad Publish Stable
- @vercel/build-utils@4.1.0
 - vercel@25.1.0
 - @vercel/client@12.0.1
 - @vercel/go@2.0.1
 - @vercel/next@3.0.1
 - @vercel/node@2.1.0
 - @vercel/python@3.0.1
 - @vercel/redwood@1.0.1
 - @vercel/remix@1.0.1
 - @vercel/ruby@1.3.9
 - @vercel/static-build@1.0.1
 - @vercel/static-config@2.0.1
2022-06-08 10:44:17 -04:00
Steven
6d42816395 Publish Canary
- vercel@25.0.2-canary.1
 - @vercel/next@3.0.1-canary.1
 - @vercel/static-build@1.0.1-canary.1
2022-06-08 08:48:04 -04:00
Seiya Nuta
6fe6d05a42 Increase the maximum size of an edge function to 1MiB (#7903) 2022-06-08 16:30:49 +09:00
Steven
50a201f145 [cli][static-build] Fix case when vc dev is throwing the wrong error w/ BOAv3 (#7927)
Fixes an issue brought up in https://github.com/vercel/og-image/issues/207

The problem is that we allow a corner case with `vc dev` to run the build script, but this can break if the user ran `vc build` first due to the BOA V3 detection.

The workaround is to delete the BOA V3 directory when there is no Development Command provided since that will run the Build Command during development.

We also add a nicer error in the case when someone actually intended to emit BOA V3.
2022-06-07 21:31:43 +00:00
Gal Schlezinger
701a02ae9d [next] Support Edge API Endpoints (#7905)
* Add regression test for nested middleware

* [next] Allow edge api endpoints in Next.js

This reverts commit d4cef69cc9 (#7898)

* delete `functions`, not `middleware`

* Add an assertion

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-06-07 23:36:46 +03:00
agadzik
39f7586621 Publish Canary
- @vercel/build-utils@4.0.1-canary.0
 - vercel@25.0.2-canary.0
 - @vercel/client@12.0.1-canary.0
 - @vercel/go@2.0.1-canary.0
 - @vercel/next@3.0.1-canary.0
 - @vercel/node@2.0.2-canary.0
 - @vercel/python@3.0.1-canary.0
 - @vercel/redwood@1.0.1-canary.0
 - @vercel/remix@1.0.1-canary.0
 - @vercel/ruby@1.3.9-canary.0
 - @vercel/static-build@1.0.1-canary.0
 - @vercel/static-config@2.0.1-canary.0
2022-06-07 12:33:07 -04:00
Andrew Gadzik
c4a39c8d29 [build-utils] Add getWorkspacePackagePaths function (#7859)
Gets the package paths from the workspace definition for a given git repository

```ts
/** 
 *  Gets the list of workspace package paths based on the configuration 
 * 
 * @example
 *
 * my-repo
 * |-- packages
 * |    |-- api-1
 * |    |-- api-2
 * |-- package.json
 * |-- pnpm-workspaces.yaml // packages: ["packages/*"]
 *
 * const fs = new ... // based on 'my-repo'
 * const workspace = { ..., type: "pnpm" }
 * getWorkspacePackagePaths({ fs, workspace }) => [
 *   "my-repo/packages/api-1",
 *   "my-repo/packages/api-2"
 * ]
 */
function getWorkspacePackagePaths(fs: DetectorFilesystem, workspace: Workspace): string[]
```

### Related Issues

Closes https://github.com/vercel/vercel/issues/7749

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-06-07 15:55:23 +00:00
Nathan Rajlich
3ac238cf08 [node] Add support for EdgeFunction output type (#7877)
Adds support for `@vercel/node` to return an `EdgeFunction` output when it detects `runtime: 'experimental-edge'` from the static `config` object. This is implemented in `@vercel/node` so that it can share the same file extension (`.js`/`.ts`) as Node.js Serverless Functions without modifying the zero-config detection logic.

**Example:**

```js
export const config = {
  runtime: 'experimental-edge',
};

export default req => {
  return new Response(`Hi, from an Edge Function!`);
};
```
2022-06-07 02:20:08 +00:00
Nathan Rajlich
8384813a0d [cli] Output EdgeFunctions from version 3 Builders in vc build (#7924)
Serializes `EdgeFunction` outputs in `vc build` when provided by a version 3 Builder.
2022-06-07 01:42:33 +00:00
Nathan Rajlich
c4587de439 [cli] Add initial vc build unit tests (#7869)
Adds some initial unit tests for the `vc build` command which includes some fixes for Windows.
2022-06-06 22:16:18 +00:00
Nathan Rajlich
d997dc4fbc [static-config] Use "runtime" instead of "use" (#7922)
The static configuration property name has been decided to be `runtime`.

This is a precursor for #7877.
2022-06-06 19:02:32 +00:00
Nathan Rajlich
d15b90bd4d [build-utils] Allow EdgeFunction as a valid output type for version 3 Builders (#7923)
A version 3 Builder is allowed to output an `EdgeFunction` output type. This already is handled correctly by the Vercel build infrastructure.
2022-06-06 18:42:14 +00:00
JJ Kasper
5b31297f0c [next] Ensure middleware route comes before beforeFiles rewrites (#7912)
* Ensure middleware route comes before beforeFiles rewrites

* update test

* add version lock and more tests

* update nested middleware test for version lock
2022-06-06 13:03:31 -04:00
Steven
e232566cbe Publish Stable
- vercel@25.0.1
 - @vercel/node@2.0.1
2022-06-05 21:38:44 -04:00
Steven
592689cad1 Publish Canary
- vercel@25.0.1-canary.0
 - @vercel/node@2.0.1-canary.0
2022-06-05 20:58:34 -04:00
Steven
9b08e72f76 [node] Skip TS compile for .d.ts assets (#7918)
Fixes an issue where `.d.ts` assets were incorrectly being compiled after a fix to nft in https://github.com/vercel/nft/pull/289

- [Repro Steps](https://github.com/cheapsteak/repro-vercel-cli-build-dts-error)
- [Incident](https://app.kintaba.com/incident/755531679765890512)
- [Slack](https://vercel.slack.com/archives/C03JNHA844R/p1654334373086879)

<img src="https://user-images.githubusercontent.com/229881/172055403-d2edfe1d-3a60-459f-bf40-506fea1d1ca4.png" height=500 />
2022-06-06 00:36:41 +00:00
Sean Massa
bd0e10cfe7 [cli] update vc env pull --help (#7911)
* update `vc env pull --help`

* update `vc env pull` help documentation

* try longer timeout

* remove dupe "env"

* remove unused --env option

* Update packages/cli/src/commands/env/index.ts

Co-authored-by: Steven <steven@ceriously.com>

Co-authored-by: Steven <steven@ceriously.com>
2022-06-05 16:22:32 -05:00
Sean Massa
28436ade60 [tests] increase test timeout; it has been failing recently (#7913) 2022-06-03 10:06:36 -05:00
Sean Massa
de0d2fba0b Publish Stable
- @vercel/build-utils@4.0.0
 - vercel@25.0.0
 - @vercel/client@12.0.0
 - @vercel/frameworks@1.0.1
 - @vercel/go@2.0.0
 - @vercel/next@3.0.0
 - @vercel/node-bridge@3.0.0
 - @vercel/node@2.0.0
 - @vercel/python@3.0.0
 - @vercel/redwood@1.0.0
 - @vercel/remix@1.0.0
 - @vercel/routing-utils@1.13.4
 - @vercel/ruby@1.3.8
 - @vercel/static-build@1.0.0
 - @vercel/static-config@2.0.0
2022-06-02 14:17:21 -05:00
Sean Massa
e0900128d6 [cli][client][tests] update to node14-compatible target (drop support for node12) (#7865)
Node 12 is EOL and we're starting to reference packages that don't install on node lower than 14, such as nuxt@3 and remix. Let's update to 14 for now.

---

Reimplements: https://github.com/vercel/vercel/pull/7819
2022-06-02 19:01:34 +00:00
Nathan Rajlich
8d15f30579 [tests] Remove ava compileEnhancements property from package.json (#7904)
VS Code was complaining about this property, so I guess it's not doing anything:

> Property `compileEnhancements` is not allowed.
2022-06-01 21:14:01 +00:00
Nathan Rajlich
960c66584c [build-utils] Make repoRootPath non-optional (#7909)
This value is always set in the Vercel build infra, so mark the type as non-optional.
2022-06-01 19:39:45 +00:00
JJ Kasper
1c8f91031a [next] Add regression test for nested middleware (#7901)
Add regression test for nested middleware
2022-06-01 10:04:16 -05:00
Steven
68cb23c3cc [cli] Add experimental corepack support (#7871)
This PR adds support for experimental [corepack](https://nodejs.org/api/corepack.html) to `vc build`.

Since this is still experimental, we only enable if the env var `ENABLE_EXPERIMENTAL_COREPACK=1` is set.

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`
2022-06-01 14:31:33 +00:00
Nathan Rajlich
94f6ae2595 [examples] Remove ENABLE_FILE_SYSTEM_API env var from "remix" template (#7886)
Since the `@vercel/remix` Builder is now being used, this environment variable is no longer necessary.
2022-06-01 01:16:24 +00:00
Steven
b92aeac84d [build-utils] Fix warning for package.json engines (#7900)
This PR updates the way we handle warning for engines.node in `package.json`.

- should not warn when the engines version satisfies the project settings (previously it was an exact match)
- should warn when engines version is exact instead of range since it cannot be satisfied exactly
- should warn when engines version is greater than since it might introduce breaking changes for a future node.js version
2022-06-01 00:15:00 +00:00
JJ Kasper
00420b7a01 Publish Canary
- vercel@24.2.6-canary.0
 - @vercel/next@2.9.1-canary.0
 - @vercel/static-config@1.0.2-canary.0
2022-05-31 18:52:50 -05:00
Steven
a5128790d0 [tests] Fix turbo cache invalidation for GH Actions RUNNER_OS (#7899)
This PR fixes an issue where turbo was caching the result regardless of OS by adding the env var `RUNNER_OS` to the cache key.

https://docs.github.com/en/actions/learn-github-actions/environment-variables#detecting-the-operating-system
2022-05-31 22:42:26 +00:00
Seiya Nuta
ae9aa91f4f [static-config] Support extracting export const config from swc's AST (#7791)
This PR adds support for extracting `config` from swc's AST. `static-config` supports parsing from the source program but in some cases we already have parsed an AST to do other static code analysis.

### Related Issues

None

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-05-31 20:44:29 +00:00
JJ Kasper
d4cef69cc9 Revert "[next] Allow edge api endpoints in Next.js" (#7898)
Reverts vercel/vercel#7855

We can re-land after we ensure the cases we have found failing are resolved.
2022-05-31 20:07:46 +00:00
Sean Massa
323f67c31a [cli] update update-notifier to fix notification of non-latest (#7897)
Looks like this is a bug in update-notifier, but they [smoothed it over](https://github.com/yeoman/update-notifier/pull/192) with a change that was deployed in [`5.0.0`](https://github.com/yeoman/update-notifier/releases/tag/v5.0.0). We’re currently on `4.1.0`.

This can cause an update notification for a PREVIOUS version, like so:

```
$  vc --version
> UPDATE AVAILABLE Run `npm i -g vercel@latest` to install Vercel CLI 24.2.5-canary.3
> Changelog: https://github.com/vercel/vercel/releases/tag/vercel@24.2.5-canary.3
Vercel CLI 24.2.5
24.2.5
```

While we're here, also sets the [`updatecheckinterval`](https://github.com/yeoman/update-notifier#updatecheckinterval) to 1 week.

---

Previous PR: https://github.com/vercel/vercel/pull/4896
2022-05-31 19:14:39 +00:00
Steven
63c499a826 [tests] Update domain tests from .org to .com (#7896)
Use a regex matcher and change the .org to .com
2022-05-31 18:07:25 +00:00
Steven
ad436313e1 Publish Stable
- @vercel/build-utils@3.1.1
 - vercel@24.2.5
 - @vercel/client@11.0.4
 - @vercel/frameworks@1.0.0
 - @vercel/go@1.4.4
 - @vercel/next@2.9.0
 - @vercel/node@1.15.4
 - @vercel/python@2.3.4
 - @vercel/redwood@0.8.4
 - @vercel/remix@0.0.2
 - @vercel/ruby@1.3.7
 - @vercel/static-build@0.26.0
2022-05-31 10:44:36 -04:00
Lee Robinson
c414288b2f [examples] Update SvelteKit example with latest boilerplate (#7892)
Replaces https://github.com/vercel/vercel/pull/7674 with the latest SvelteKit boilerplate.
2022-05-30 07:57:46 +00:00
Steven
b07ff7431f [tests] Bump turbo to 1.2.14 (#7887)
This should fix the caching problem

https://github.com/vercel/turborepo/releases/tag/v1.2.14
2022-05-27 23:46:38 +00:00
Steven
79fde4475c [build-utils][go][next][redwood][static-build] Fix path delimiter on windows (#7881)
This PR fixes an issue where the path delimiter was incorrect for windows and caused yarn (and other global CLIs) to no longer be found.

https://github.com/vercel/vercel/runs/6602572000?check_suite_focus=true#step:13:1357
2022-05-27 22:59:28 +00:00
Nathan Rajlich
855197c699 [node] Remove mkdirp-promise dependency (#7878)
There's no need to use this module since Node.js has `recursive: true` since Node v10.

Co-authored-by: Steven <steven@ceriously.com>
2022-05-26 15:07:09 -04:00
Nathan Rajlich
fbd9080859 [tests] Add vc build integration test (#7876)
[cli] Add `vc build` integration test

Adds a `vc build` integration test to ensure the ncc'd CLI works as
expected to supplement the unit tests from #7869.

Co-authored-by: Steven <steven@ceriously.com>
2022-05-26 15:05:31 -04:00
Steven
b5c5b7b82c [tests] Fix tests with promises (#7880)
These tests are asynchronous and would sometimes fail if the second call started before the first call finished. This PR fixes the usage since the function returns a promise.
2022-05-26 10:28:49 -04:00
Gal Schlezinger
0a072ee850 Publish Canary
- vercel@24.2.5-canary.3
 - @vercel/next@2.8.67-canary.3
2022-05-26 09:20:12 +03:00
Dalpat Rathore
0b56caba45 [docs] Fix CONTRIBUTING.md grammar (#7575)
Fix: CONTRIBUTING.md grammar

Co-authored-by: Steven <steven@ceriously.com>
2022-05-25 17:44:41 -04:00
Steven
ab3db60824 [tests] Conditionally set env vars (#7874)
This PR is a follow up to #7873 which didn't work as intended
2022-05-25 17:36:20 -04:00
Steven
f2f2ff2c67 [tests] Skip env vars for forks (#7873)
This fixes an issue when an external contributor submits a PR and the remote cache is not accessible 

```
$ turbo run build
 ERROR No caches are enabled. You can try "turbo login", "turbo link", or ensuring you are not passing --remote-only to enable caching:  <nil>
• Packages in scope: @vercel/build-utils, @vercel/client, @vercel/frameworks, @vercel/go, @vercel/next, @vercel/node, @vercel/node-bridge, @vercel/python, @vercel/redwood, @vercel/remix, @vercel/routing-utils, @vercel/ruby, @vercel/static-build, @vercel/static-config, vercel
```
2022-05-25 16:54:35 -04:00
Gal Schlezinger
ba7dafff71 [next] Allow edge api endpoints in Next.js (#7855)
Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-05-25 15:19:14 +03:00
JJ Kasper
987fb4d4f7 [next] Ensure test-next-local is run in CI (#7868)
* Ensure test-next-local is run in CI

* update turbo config

* update timeout

* update flakey test
2022-05-24 14:22:40 -05:00
Nathan Rajlich
be74f79fa0 Publish Canary
- @vercel/build-utils@3.1.1-canary.2
 - vercel@24.2.5-canary.2
 - @vercel/client@11.0.4-canary.2
 - @vercel/go@1.4.4-canary.2
 - @vercel/next@2.8.67-canary.2
 - @vercel/node@1.15.4-canary.2
 - @vercel/python@2.3.4-canary.2
 - @vercel/redwood@0.8.4-canary.2
 - @vercel/remix@0.0.2-canary.2
 - @vercel/ruby@1.3.7-canary.2
 - @vercel/static-build@0.25.3-canary.2
2022-05-24 09:14:29 -07:00
Andrew Gadzik
86886e6b60 [build-utils] Add getWorkspaces helper function (#7775)
```ts
/*

  Example 1:
	
  my-repo
    |-- packages
    |    |-- api-1
    |    |-- api-2
    |-- package.json
    |-- pnpm-workspaces.yaml // packages: ["packages/*"]
	
  getWorkspaces("my-repo") => [
    {
      implementation: "pnpm",
      rootPath: "https://github.com/.../my-repo"
    }
  ]
  
  Example 2:
	
  my-repo
    |-- backend
    |    |-- api-1
    |    |-- api-2
    |    |-- package.json // workspaces: ["api-1", "api-2"]
    |-- frontend
    |    |-- nextjs-app
    |    |-- gatsby-app
    |    |-- package.json
    |    |-- pnpm-workspaces.yaml // packages: ["nextjs-app", "gatsby-app"]
	
  getWorkspaces("my-repo") => [
    {
      implementation: "yarn",
      rootPath: "https://github.com/.../my-repo/backend"
    }, 
    {
      implementation: "pnpm",
      rootPath: "https://github.com/.../my-repo/frontend"
    }
  ]

 */
type Workspace = {
  implementation: "yarn" | "npm" | "pnpm" 
  rootPath: string // the path to the root of the workspace
}

function getWorkspaces(fs: DetectorFilesystem): Workspace[]
```

### Related Issues

> Closes https://github.com/vercel/vercel/issues/7730

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-05-24 14:28:32 +00:00
Nathan Rajlich
e8a9000137 [cli] Symlink @now/build-utils when installing builders in "vc build" (#7844)
To support legacy Builders like `@frontity/now` which still depends on the old `@now/build-utils` package name, create a symlink to `@vercel/build-utils` to make import the package work transparently.

Depends on #7842 being merged first.
2022-05-24 05:36:34 +00:00
Nathan Rajlich
75ea68d445 [cli] Convert DevServer "Unit" suite tests to "Dev" suite tests (#7845)
The `DevServer` class as it's currently implement is not really suitable for unit testing, since it calls `process.exit()` when shutting down the server. This causes Jest to exit prematurely, and prevents tests after these one from running. This was in fact hiding some failing tests. So these tests were ported over to be "Dev" suite tests which spawn a child process of `vc dev` rather than running them in-process, so that the `process.exit()` doesn't exit Jest.
2022-05-23 23:38:02 +00:00
Sean Massa
a5a990995c [tests] improve error message when a deploy fails (#7864)
improve error message
2022-05-23 17:02:29 -05:00
Sean Massa
cd7dcc6731 [tests] bump node version to 14 (#7863) 2022-05-23 16:00:03 -05:00
Sean Massa
c4bd462b85 [tests] update fixture path (#7862)
It looks like the fixture location was shifted and these references to it were not updated. The crashing `dev` tests were hiding these failures.
2022-05-23 20:31:24 +00:00
Steven
5fc266bd8a [next][node] Bump @vercel/nft to 0.19.1 (#7860)
Bump `@vercel/nft` to 0.19.1 https://github.com/vercel/nft/releases/tag/0.19.1
2022-05-23 17:30:29 +00:00
JJ Kasper
08b04d0bda Publish Canary
- @vercel/build-utils@3.1.1-canary.1
 - vercel@24.2.5-canary.1
 - @vercel/client@11.0.4-canary.1
 - @vercel/frameworks@0.9.2-canary.0
 - @vercel/go@1.4.4-canary.1
 - @vercel/next@2.8.67-canary.1
 - @vercel/node@1.15.4-canary.1
 - @vercel/python@2.3.4-canary.1
 - @vercel/redwood@0.8.4-canary.1
 - @vercel/remix@0.0.2-canary.1
 - @vercel/ruby@1.3.7-canary.1
 - @vercel/static-build@0.25.3-canary.1
2022-05-23 10:38:42 -05:00
Nathan Rajlich
92ae51c6a6 [examples] Add missing semi to "solidstart" vite config (#7858) 2022-05-22 19:24:45 -05:00
JJ Kasper
909c493d1e [next][build-utils] Update to re-use build result type from build-utils (#7857)
### Related Issues

We were previously using a custom type in Next.js for the build result although there is an existing type for this in the build-utils package which we can centralize around. 

x-ref: https://github.com/vercel/vercel/pull/7854#issuecomment-1133934764

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-05-22 21:11:33 +00:00
Gal Schlezinger
98b54ff61f [next] Remove unused middleware definition (#7854)
Remove unused middleware definition in Next.js builder

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-05-22 20:46:49 +03:00
JJ Kasper
3defd0b2fd [next] Fix middleware order and add e2e test (#7847)
Fix middleware order and add e2e test
2022-05-22 11:25:56 -05:00
Nathan Rajlich
5a7851a7f7 [tests] Revert "update CI to use node@14 and drop support for node@12 (#7819)" (#7853)
Revert "[tests] update CI to use node@14 and drop support for node@12 (#7819)"

This reverts commit 89b5aad367.
2022-05-21 20:11:08 -07:00
Nathan Rajlich
7f0caa7dec [examples] Add vercel.json file back to "remix" template for now (#7851)
This file was removed prematurely in #7831.
2022-05-21 20:03:06 -07:00
Sean Massa
89b5aad367 [tests] update CI to use node@14 and drop support for node@12 (#7819)
Node 12 is EOL and we're starting to reference packages that don't install on node lower than 14, such as nuxt@3 and remix. Let's update to 14 for now.
2022-05-21 05:20:43 +00:00
Nathan Rajlich
c13775ba07 [frameworks] Remove defaultHeaders / defaultRedirects / defaultRewrites / getFsOutputDir() (#7832)
These properties are leftover from the previous iteration of `vc build` / File System API v2, and are no longer in use.
2022-05-20 20:24:51 +00:00
Nathan Rajlich
cc3c1d1eeb [cli] Refactor vc build installBuilders() logic to not use a loop (#7842)
This will prevent the possibility of an infinite loop occurring when importing builders that throw a `MODULE_NOT_FOUND` error. Instead, we'll just try to import the builders, then run yarn if necessary, and then try to import the builders a second time when needed. If that fails then we'll throw the original error.
2022-05-20 19:53:54 +00:00
Sean Massa
3aa2fbbb53 [static-build] Support subset of Build Output API v2 (#7808)
* Revert "Revert "[static-build] Support subset of Build Output API v2" (#7803)"

This reverts commit dfb6ef949b.

* more specific v2 detection

* use nuxt@3.0.0-rc.3 to make sure the incident is resolved

* set test timeout to see if this works in CI

* update CI node version to 14

* add node_modules because it was taking too long to install in CI

* remove timeout

* remove node modules

* remove the nuxt@3 dependency

* finish update to node 14

* blank yarn.lock

* revert node version update

* fix revert

* remove newline
2022-05-20 12:27:14 -07:00
Nathan Rajlich
3a8b8aa03a [cli] Hard-code VERCEL: 1 env var for "vc build" (#7843)
This env var should normally come from the `.env` file that is created from `vc pull`, but let's just be extra sure and hard-code it directly in to `vc build` command as well.

SvelteKit `adapter-vercel` depends on this env var being set for its detection logic.
2022-05-20 18:53:55 +00:00
Nathan Rajlich
d008617c0b [frameworks] Use @vercel/remix Builder for "remix" preset (#7831)
Specify the `useRuntimes` property for the "remix" framework
to use the `@vercel/remix` Builder.

The `ENABLE_FILE_SYSTEM_API` env var is no longer necessary so the
`vercel.json` file from the Remix template has also been deleted.
2022-05-20 11:41:36 -05:00
Steven
8aecb99447 [cli] Recommend vc env instead of vc secrets (#6217)
We no longer need `vercel secrets` command. The former use cases can be solved with `vercel env`.

This adds a note recommending to use `vc env` as an alternative.

<img width="544" alt="Screen Shot 2021-08-11 at 1 38 18 PM" src="https://user-images.githubusercontent.com/71256/129101031-73e915dd-8253-4901-8e34-4b37d9516100.png">
<img width="565" alt="Screen Shot 2021-08-11 at 1 41 06 PM" src="https://user-images.githubusercontent.com/71256/129101036-2ed053dc-3de6-4fc1-8071-1bf9206323c9.png">
2022-05-20 15:43:20 +00:00
Andrew Gadzik
1793fa643f Add list of supported monorepo managers (#7760)
This file will mimic the structure of `vercel/frameworks/frameworks.ts` in the sense that it defines the list of supported monorepo managers.  These are the ones were targeting for the initial launch,

- Turborepo

```ts
export const monorepoManagers = [
  {
    name: "Turborepo",
    slug: "turbo",
    detectors: {
      every: [
        {
          path: 'turbo.json'
        },
      ],
    },
  },
]
```

### Related Issues

> Closes https://github.com/vercel/vercel/issues/7757

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-05-20 15:20:50 +00:00
Nathan Rajlich
913683a8e6 [next] Use substring() instead of substr() (#7839)
Fixes linting warnings, i.e.:

> packages/next/src/server-build.ts#L858
>
> substr() is deprecated, use slice() or substring() instead
2022-05-20 14:58:39 +00:00
Nathan Rajlich
0d1dc23d0d [cli] Don't use tree-kill for shutting down startDevServer() process in vc dev (#7830)
Follow-up to #7824 since using `tree-kill` here seems to make a CLI test hang forever.
2022-05-19 18:16:09 -07:00
Nathan Rajlich
e829ce47c3 Publish Canary
- @vercel/build-utils@3.1.1-canary.0
 - vercel@24.2.5-canary.0
 - @vercel/client@11.0.4-canary.0
 - @vercel/go@1.4.4-canary.0
 - @vercel/next@2.8.67-canary.0
 - @vercel/node@1.15.4-canary.0
 - @vercel/python@2.3.4-canary.0
 - @vercel/redwood@0.8.4-canary.0
 - @vercel/remix@0.0.2-canary.0
 - @vercel/ruby@1.3.7-canary.0
 - @vercel/static-build@0.25.3-canary.0
2022-05-19 16:56:36 -07:00
Nathan Rajlich
474770e9b0 [remix] Add @vercel/remix Builder (#7814)
Adds a new Builder for the Remix framework. This Builder has support for a few different configurations of Remix including the "just the basics" Remix server as well as the "vercel" server build target configuration. If the project does not have the `@remix-run/vercel` runtime adapter as a dependency in package.json then this Builder will implicitly add it when generating the SSR serverless function.

In a follow-up PR, we can make this Builder be used in the frameworks configuration so that it is executed when `remix` framework is selected for the project, and we can update the remix example template in this repo to no longer require the `ENABLE_FILE_SYSTEM_API` env var.
2022-05-19 22:54:00 +00:00
Nathan Rajlich
e762d8d4e8 [cli] Add vercel build command (#7673)
This PR adds the latest iteration of the `vc build` command which invokes the Builders that a Project depends on locally and serializes the output into the `.vercel/output` directory.
2022-05-19 22:11:07 +00:00
Steven
6e8935883b Publish Stable
- @vercel/build-utils@3.1.0
 - vercel@24.2.4
 - @vercel/client@11.0.3
 - @vercel/frameworks@0.9.1
 - @vercel/go@1.4.3
 - @vercel/next@2.8.66
 - @vercel/node-bridge@2.2.2
 - @vercel/node@1.15.3
 - @vercel/python@2.3.3
 - @vercel/redwood@0.8.3
 - @vercel/routing-utils@1.13.3
 - @vercel/ruby@1.3.6
 - @vercel/static-build@0.25.2
 - @vercel/static-config@1.0.1
2022-05-19 16:30:30 -04:00
Steven
1c3497dc74 Publish Canary
- vercel@24.2.4-canary.1
 - @vercel/next@2.8.66-canary.1
 - @vercel/node@1.15.3-canary.1
 - @vercel/redwood@0.8.3-canary.1
2022-05-19 16:18:29 -04:00
JJ Kasper
9836fdb5ca [next][node][redwood] Update to latest version of @vercel/nft (#7827)
### Related Issues

x-ref: https://github.com/vercel/nft/releases/tag/0.19.0

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-05-19 19:49:48 +00:00
JJ Kasper
d6f88f019f [next] Fix loading next launcher with type: module in package.json (#7828)
### Related Issues

Currently if you have a `package.json` with `type: 'module'` the launcher will fail to load as it is not using ESM and isn't using the `.cjs` extension. This corrects the extension per the below error message. 

```sh
ReferenceError: exports is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/var/task/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
```

x-ref: https://github.com/vercel/next.js/issues/36903

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-05-19 19:23:29 +00:00
JJ Kasper
0f720472c4 [cli] Migrate Dev tests from ava to jest (#7822)
* Migrate ava cli dev tests to jest

* remove outdated test

* update tests

* Update utils

* update stdio to fix stalling

* test stdio tests

* add debug logs

* more logs

* use strip ansi

* update

* add exit flag and fix next dev test

* update timeout for mac ci

* update cancel

* update cancel tests.yml -> test.yml

* update other ids -> names

* remove outdated test

* de-dupe scripts

* remove redwood dev test due to size
2022-05-19 14:01:13 -05:00
JJ Kasper
8d0c5114e4 Update to latest version of turbo (#7829) 2022-05-19 13:35:26 -05:00
Nathan Rajlich
e3c4435606 [cli] Fix "now-dev-next" and "now-dev-static-build-routing" unit tests (#7824)
These two tests have been problematic on MacOS for a long time,
but now that we've re-introduced `@vercel/next` into this repo,
they've started to fail on Linux/Windows as well.

So the idea is to use the `devCommand` property so that
`@vercel/next` Builder doesn't get invoked at all.

Since the `devCommand` property was not yet being unit tested,
some logic in `vc build` needed to be adjusted in order to properly
shut down the dev command (via `tree-kill` module) when the
process was being stopped in order to cleanly shut down.
2022-05-19 09:27:51 -07:00
Steven
234c4dfa84 [tests] Update codeowners (#7825) 2022-05-19 11:43:27 -04:00
Nathan Rajlich
acd756436c [node] Fix 11-symlinks integration test fixture (#7823)
The `symlink` symlink wasn't being persisted through Turbo's cache,
so this test was failing. So move the symlink directly into the git repo
since there's no need to create it at build-time (git can store symlinks
just fine).

Additionally, the test itself was not testing that the symlink was indeed
re-created as a symlink from within the lambda environment (it actually
wasn't, due to the upload code not considering symlinks), so the test
has been updated to test for that as well.
2022-05-18 22:04:58 -07:00
Nathan Rajlich
f26858b735 Publish Canary
- @vercel/build-utils@3.0.2-canary.0
 - vercel@24.2.4-canary.0
 - @vercel/client@11.0.3-canary.0
 - @vercel/frameworks@0.9.1-canary.0
 - @vercel/go@1.4.3-canary.0
 - @vercel/next@2.8.66-canary.0
 - @vercel/node-bridge@2.2.2-canary.0
 - @vercel/node@1.15.3-canary.0
 - @vercel/python@2.3.3-canary.0
 - @vercel/redwood@0.8.3-canary.0
 - @vercel/routing-utils@1.13.3-canary.0
 - @vercel/ruby@1.3.6-canary.0
 - @vercel/static-build@0.25.2-canary.0
 - @vercel/static-config@1.0.1-canary.0
2022-05-18 16:54:08 -07:00
Nathan Rajlich
623e43f865 [next] Add @vercel/next Builder (#7793) 2022-05-18 23:51:48 +00:00
Sean Massa
3e696513a2 [examples] add .output to .vercelignore (#7817)
When running `nuxi build` locally, you'll end up with a `.output` directory. This is not used to produce a vercel deployment and can cause confusion with Build Output API v2 (which also expects a `.output` directory). It's better to leave this ignored.
2022-05-18 21:35:15 +00:00
JJ Kasper
285f62c9d0 Remove un-needed use of secret for team as it mangles logs (#7818)
### Related Issues

This removes the secret for turbo team as it filters anywhere vercel is printed in the logs and it doesn't really need to be secret. 

x-ref: https://github.com/vercel/vercel/runs/6496893033?check_suite_focus=true

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-05-18 21:02:36 +00:00
JJ Kasper
c43db1788c [tests] Add cross platform chunked testing and leverage turbo more (#7795)
* [tests] Use `turbo` for unit tests

* .

* .

* Revert "."

This reverts commit 3d6204fef3fda3c7b4bf08955a186fe806d7c597.

* Add "src/util/constants.ts" to outputs

* .

* Add `@vercel/node-bridge` outputs

* .

* Mac and Windows

* .

* Node 14

* .

* .

* Add templates to CLI output

* Run only selected test files

* Add cross platform testing

* make test paths relative and have minimum per chunk

* add install arg

* update shell

* bump cache key

* use backslashes on windows

* pass tests as arg

* update script name

* update turbo config

* forward turbo args correctly

* dont use backslashes

* chunk integration tests instead

* update env

* separate static-build tests

* ensure unit test turbo cache is saved

* ensure turbo cache is saved for dev/cli

* fix cache key and update timeout

* Increase static-build unit test timeout

* Leverage turbo remote caching instead of actions/cache

* apply suggestions and test chunking itself

* update other ci jobs

* fix test collecting

Co-authored-by: Nathan Rajlich <n@n8.io>
2022-05-18 13:27:20 -07:00
haitrungle
fc2eb1a30d [examples] Fix typo in SolidStart link (#7589) 2022-05-18 10:42:24 -07:00
Sean Massa
ecf194b7c1 [tests] improve platform-based skips (#7802)
Co-authored-by: Steven <steven@ceriously.com>
2022-05-17 12:12:47 -05:00
Steven
c14e5689f1 [tests] Skip broken go test (#7809)
This test was added several years ago, before the monorepo in https://github.com/vercel/vercel/pull/2812 but it was never working correctly due to the way zero config behaves differently than the test with `builds` in vercel.json

We can fix it in a follow up PR
2022-05-16 14:44:09 -04:00
Steven
54dfe747e2 [build-utils] Deprecate Node.js 12.x with warning (#7779)
Node.js 12 reached EOL April 2022 so its time to notify customers.

This PR will warn starting today and then error once the discontinue date is reached in a couple months.
2022-05-16 16:06:39 +00:00
Steven
2afc8db8e7 Publish Stable
- vercel@24.2.3
 - @vercel/static-build@0.25.1
2022-05-13 18:30:15 -04:00
Steven
cc628dd9fb Publish Canary
- vercel@24.2.3-canary.0
 - @vercel/static-build@0.25.1-canary.0
2022-05-13 18:20:41 -04:00
Steven
dfb6ef949b Revert "[static-build] Support subset of Build Output API v2" (#7803)
Revert "[static-build] Support subset of Build Output API v2 (#7690)"

This reverts commit 05243fb6e9.
2022-05-13 18:19:38 -04:00
Sean Massa
cd4799b5d5 [cli] distinguish error messages (#7794)
* distinguish error messages

* use correct response and assertion

* remove check for APIError because getUser does not always throw those
2022-05-13 12:49:36 -05:00
Steven
5e66d4b2cc Publish Stable
- @vercel/build-utils@3.0.1
 - vercel@24.2.2
 - @vercel/client@11.0.2
 - @vercel/go@1.4.2
 - @vercel/node@1.15.2
 - @vercel/python@2.3.2
 - @vercel/redwood@0.8.2
 - @vercel/ruby@1.3.5
 - @vercel/static-build@0.25.0
2022-05-12 17:19:06 -04:00
Ethan Arrowood
44d7473e7c Publish Canary
- @vercel/redwood@0.8.2-canary.2
 - @vercel/static-build@0.24.2-canary.2
2022-05-12 13:48:21 -06:00
Ethan Arrowood
fddec1286c [redwood][static-build] move path logic up so both commands get pnpm7 (#7792)
move path logic up so both commands get pnpm7
2022-05-12 13:47:09 -06:00
Ethan Arrowood
6e5e700e8d Publish Canary
- vercel@24.2.2-canary.2
 - @vercel/node@1.15.2-canary.1
 - @vercel/redwood@0.8.2-canary.1
 - @vercel/static-build@0.24.2-canary.1
2022-05-12 09:04:20 -06:00
Ryan Carniato
b6e8609b83 [examples] Update SolidStart to Build Output API v3 (#7790) 2022-05-11 23:12:53 -07:00
Ethan Arrowood
78b7bd5ec8 [redwood][static-build] add pnpm7 detection logic to builders (#7787)
add pnpm7 detection logic to builders
2022-05-11 19:37:01 -04:00
Steven
4104a45c2d [tests] Fix node tests (#7786)
Since we released Node.js 16, these tests now resolve `engines` differently
2022-05-11 23:36:18 +00:00
Ethan Arrowood
4c20218e05 Publish Canary
- @vercel/build-utils@3.0.1-canary.1
 - vercel@24.2.2-canary.1
 - @vercel/client@11.0.2-canary.0
 - @vercel/go@1.4.2-canary.0
 - @vercel/node@1.15.2-canary.0
 - @vercel/python@2.3.2-canary.0
 - @vercel/redwood@0.8.2-canary.0
 - @vercel/ruby@1.3.5-canary.0
 - @vercel/static-build@0.24.2-canary.0
2022-05-11 15:47:48 -06:00
Ethan Arrowood
02a0004719 [build-utils] Fix pnpm 7 path setting (#7785)
### Related Issues

Fixes pnpm 7 support. Now uses a yarn installed version and drops an unnecessary check for node version.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-05-11 21:37:44 +00:00
Steven
123bffb776 [examples] Pin Ember to 14.x (#7782) 2022-05-10 14:34:46 -04:00
Steven
074535f27c [build-utils] Downgrade remix and solidstart to Node.js 14 (fsapi) (#7781)
These frameworks (remix and solidstart) use the legacy file system api (v2) so they can't support Node.js 16.x so we change engines to pin to Node.js 14.x

I also added the missing `ENABLE_FILE_SYSTEM_API=1` env var to solidstart to match remix.

https://vercel.com/docs/file-system-api/v2
2022-05-10 13:20:07 -04:00
Sean Massa
05243fb6e9 [static-build] Support subset of Build Output API v2 (#7690) 2022-05-09 14:01:26 -07:00
Sean Massa
097725580c [tests] skip flakey test (#7777)
Skips two flakey tests on Mac OS only. The error often looks like:

<img width="1453" alt="Screen Shot 2022-05-09 at 2 26 52 PM" src="https://user-images.githubusercontent.com/41545/167483088-49498f69-5470-4c1a-98f5-96ca811b838b.png">

Created internal tracking card to dig deeper. Skipping this for now will unclog other work.
2022-05-09 21:00:49 +00:00
Andrew Gadzik
4b09c89e7d [build-utils] Fix version mismatch (#7776)
Fixes 3a1eede63b
2022-05-09 14:51:04 -04:00
agadzik
3a1eede63b Publish Canary
- @vercel/build-utils@3.0.1-canary.0
 - vercel@24.2.2-canary.0
2022-05-09 13:51:37 -04:00
agadzik
9cee0dd5d7 BREAKING CHANGE: updating build-utils to version 3 for DetectorFilesystem changes 2022-05-09 13:47:11 -04:00
John Pham
b801c6e593 [cli] Track source of getting decrypted environment variables (#7754)
* Track source of getting decrypted environment variables

* Add source as a query param

* Revert lock file changes

* Change source from strings to type

* Differential between pull and env pull
2022-05-09 10:24:43 -07:00
Andrew Gadzik
505050b923 [build-utils] Add readdir and chdir functions to DetectorFilesystem (#7751)
In order to support various `fs` operations for monorepo detection, the team needs to add two new abstract functions to `DetectorFilesystem` in order to traverse down into children directories

* readdir
* chdir

```ts
interface Stat {
  name: string
  path: string
  type: "file" | "dir"
}

export abstract class DetectorFilesystem {
  ...
  /**
   * Returns a list of Stat objects from the current working directory.
   * 
   * @example
   * 
   * const url = "https://github.com/vercel/front"
   * const fs = new GitDetectorFilesystem(...) // based on url
   *
   * // calls "https://api.github.com/repos/vercel/front/contents" behind the scenes
   * await fs.readdir() => [
   *    { name: "docs", path: "docs", type: "dir" },
   *    { name: "front", path: "front", type: "dir" },
   *    ...,
   *    { name: "package.json", path: "package.json", type: "file" },
   * ]
   */
   protected abstract _readdir(name: string): Promise<Stat[]>;

 /**
   * Changes the current directory to the specified path and returns a new instance of DetectorFilesystem.
   * 
   * @example
   * 
   * my-repo
   * |-- backend
   * |    |-- api-1
   * |    |-- api-2
   * |    |-- package.json // workspaces: ["api-1", "api-2"]
   * |    |-- yarn.lock
   * |-- frontend
   * |    |-- nextjs-app
   * |    |-- gatsby-app
   * |    |-- package.json
   * |    |-- pnpm-workspaces.yaml // packages: ["nextjs-app", "gatsby-app"]
   * 
   * const fs = new (...) // based on "my-repo" as the root
   * const backendFs = fs.chdir("backend")
   * const frontendFs = fs.chdir("frontend")
   * 
   * const backendWorkspaceManager = detectFramework({ fs: backendFs, frameworkList: workspaceManagers }) // "yarn"
   * const frontendWorkspaceManager = detectFramework({ fs: frontendFs, frameworkList: workspaceManagers }) // "pnpm"
   */
   protected abstract _chdir(name: string): DetectorFilesystem
   ...
}
```

### Related Issues

> Related to https://github.com/vercel/vercel/issues/7750

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-05-09 17:12:30 +00:00
Steven
15c7ad241a Publish Stable
- @vercel/build-utils@2.17.0
 - vercel@24.2.1
 - @vercel/client@11.0.1
 - @vercel/frameworks@0.9.0
 - @vercel/go@1.4.1
 - @vercel/node@1.15.1
 - @vercel/python@2.3.1
 - @vercel/redwood@0.8.1
 - @vercel/ruby@1.3.4
 - @vercel/static-build@0.24.1
2022-05-09 10:41:56 -04:00
2406 changed files with 195950 additions and 20275 deletions

View File

@@ -19,6 +19,13 @@ packages/cli/src/util/dev/templates/*.ts
packages/client/tests/fixtures
packages/client/lib
# hydrogen
packages/hydrogen/edge-entry.js
# next
packages/next/test/integration/middleware
packages/next/test/integration/middleware-eval
# node-bridge
packages/node-bridge/bridge.js
packages/node-bridge/launcher.js

4
.gitattributes vendored
View File

@@ -8,3 +8,7 @@ packages/*/test/* linguist-vendored
# Go build fails with Windows line endings.
*.go text eol=lf
go.mod text eol=lf
# Mark certain files as "binary" -- hide diffs
**/test/fixtures/**/git/**/* binary
**/test/fixtures/**/git/**/* linguist-generated

39
.github/CODEOWNERS vendored
View File

@@ -1,29 +1,26 @@
# Documentation
# https://help.github.com/en/articles/about-code-owners
* @TooTallNate
/.github/workflows @AndyBitz @styfle
/packages/frameworks @AndyBitz
/packages/cli/src/commands/build @TooTallNate @styfle @AndyBitz @gdborton @jaredpalmer
/packages/cli/src/commands/dev @TooTallNate @styfle @AndyBitz
/packages/cli/src/util/dev @TooTallNate @styfle @AndyBitz
* @TooTallNate @EndangeredMassa @styfle
/.github/workflows @TooTallNate @EndangeredMassa @styfle @ijjk
/packages/frameworks @TooTallNate @EndangeredMassa @styfle @AndyBitz
/packages/cli/src/commands/domains @javivelasco @mglagola @anatrajkovska
/packages/cli/src/commands/certs @javivelasco @mglagola @anatrajkovska
/packages/cli/src/commands/env @styfle @lucleray
/packages/client @styfle @TooTallNate
/packages/build-utils @styfle @AndyBitz @TooTallNate
/packages/client @TooTallNate @EndangeredMassa @styfle
/packages/build-utils @TooTallNate @EndangeredMassa @styfle @AndyBitz
/packages/middleware @gdborton @javivelasco
/packages/node @styfle @TooTallNate @lucleray
/packages/node-bridge @styfle @TooTallNate @lucleray
/packages/next @Timer @ijjk
/packages/go @styfle @TooTallNate
/packages/python @styfle @TooTallNate
/packages/ruby @styfle @TooTallNate
/packages/static-build @styfle @AndyBitz
/packages/routing-utils @styfle @dav-is @ijjk
/examples @mcsdevv
/packages/node @TooTallNate @EndangeredMassa @styfle
/packages/node-bridge @TooTallNate @EndangeredMassa @styfle @ijjk
/packages/next @TooTallNate @ijjk
/packages/go @TooTallNate @EndangeredMassa @styfle
/packages/python @TooTallNate @EndangeredMassa @styfle
/packages/ruby @TooTallNate @EndangeredMassa @styfle
/packages/static-build @TooTallNate @EndangeredMassa @styfle @AndyBitz
/packages/routing-utils @TooTallNate @EndangeredMassa @styfle @ijjk
/examples @leerob
/examples/create-react-app @Timer
/examples/nextjs @timneutkens @Timer
/examples/hugo @mcsdevv @styfle
/examples/jekyll @mcsdevv @styfle
/examples/zola @mcsdevv @styfle
/examples/nextjs @timneutkens @ijjk @styfle
/examples/hugo @styfle
/examples/jekyll @styfle
/examples/zola @styfle

View File

@@ -23,7 +23,7 @@ Make sure all the tests pass before making changes.
## Verifying your change
Once you are done with your changes (we even suggest doing it along the way), make sure all the test still pass by running:
Once you are done with your changes (we even suggest doing it along the way), make sure all the tests still pass by running:
```
yarn test-unit
@@ -64,7 +64,7 @@ Integration tests create deployments to your Vercel account using the `test` pro
x-now-trace=iad1]
```
In such cases you can visit the URL of the failed deployment and append `/_logs` so see the build error. In the case above, that would be https://test-8ashcdlew.vercel.app/_logs
In such cases, you can visit the URL of the failed deployment and append `/_logs` to see the build error. In the case above, that would be https://test-8ashcdlew.vercel.app/_logs
The logs of this deployment will contain the actual error which may help you to understand what went wrong.
@@ -82,11 +82,11 @@ nodeFileTrace(['path/to/entrypoint.js'], {
.then(e => console.error(e));
```
When you run this script, you'll see all imported files. If anything file is missing, the bug is in [@vercel/nft](https://github.com/vercel/nft) and not the Builder.
When you run this script, you'll see all the imported files. If anything file is missing, the bug is in [@vercel/nft](https://github.com/vercel/nft) and not the Builder.
## Deploy a Builder with existing project
Sometimes you want to test changes to a Builder against an existing project, maybe with `vercel dev` or an actual deployment. You can avoid publishing every Builder change to npm by uploading the Builder as a tarball.
Sometimes you want to test changes to a Builder against an existing project, maybe with `vercel dev` or actual deployment. You can avoid publishing every Builder change to npm by uploading the Builder as a tarball.
1. Change directory to the desired Builder `cd ./packages/node`
2. Run `yarn build` to compile typescript and other build steps

View File

@@ -13,6 +13,5 @@ jobs:
steps:
- uses: styfle/cancel-workflow-action@0.9.1
with:
workflow_id: 849295, 849296, 849297, 849298
workflow_id: test.yml, test-integration-cli.yml, test-unit.yml
access_token: ${{ github.token }}

View File

@@ -16,9 +16,15 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
node: [12]
node: [14]
runs-on: ${{ matrix.os }}
steps:
- name: Conditionally set remote env
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
echo "TURBO_REMOTE_ONLY=true" >> $GITHUB_ENV
echo "TURBO_TEAM=vercel" >> $GITHUB_ENV
echo "TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}" >> $GITHUB_ENV
- uses: actions/setup-go@v2
with:
go-version: '1.13.15'

View File

@@ -1,43 +0,0 @@
name: Dev
on:
push:
branches:
- main
tags:
- '!*'
pull_request:
jobs:
test:
name: Dev
timeout-minutes: 75
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
node: [12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.13.15'
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- uses: actions/checkout@v2
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin main --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/main...HEAD --name-only
- name: Install Hugo
if: matrix.os == '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/
- run: yarn install --network-timeout 1000000
- run: yarn run build
- run: yarn test-integration-dev
env:
VERCEL_TEAM_TOKEN: ${{ secrets.VERCEL_TEAM_TOKEN }}
VERCEL_REGISTRATION_URL: ${{ secrets.VERCEL_REGISTRATION_URL }}

View File

@@ -1,35 +0,0 @@
name: E2E
on:
push:
branches:
- main
tags:
- '!*'
pull_request:
jobs:
test:
name: E2E
timeout-minutes: 120
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.13.15'
- uses: actions/setup-node@v2
with:
node-version: 12
- uses: actions/checkout@v2
with:
fetch-depth: 100
- run: git --version
- run: git fetch origin main --depth=100
- run: git fetch origin ${{ github.ref }} --depth=100
- run: git diff origin/main...HEAD --name-only
- run: yarn install --network-timeout 1000000
- run: yarn run build
- run: yarn test-integration-once
env:
VERCEL_TEAM_TOKEN: ${{ secrets.VERCEL_TEAM_TOKEN }}
VERCEL_REGISTRATION_URL: ${{ secrets.VERCEL_REGISTRATION_URL }}

View File

@@ -16,9 +16,15 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [12]
node: [14]
runs-on: ${{ matrix.os }}
steps:
- name: Conditionally set remote env
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
echo "TURBO_REMOTE_ONLY=true" >> $GITHUB_ENV
echo "TURBO_TEAM=vercel" >> $GITHUB_ENV
echo "TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}" >> $GITHUB_ENV
- uses: actions/setup-go@v2
with:
go-version: '1.13.15'
@@ -35,9 +41,9 @@ jobs:
- run: yarn install --network-timeout 1000000
- run: yarn run build
- run: yarn run lint
if: matrix.os == 'ubuntu-latest' && matrix.node == 12 # only run lint once
if: matrix.os == 'ubuntu-latest' && matrix.node == 14 # only run lint once
- run: yarn run test-unit
- run: yarn workspace vercel run coverage
if: matrix.os == 'ubuntu-latest' && matrix.node == 12 # only run coverage once
if: matrix.os == 'ubuntu-latest' && matrix.node == 14 # only run coverage once
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

90
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: Tests
on:
push:
branches:
- main
tags:
- '!*'
pull_request:
env:
NODE_VERSION: '14'
jobs:
setup:
name: Find Changes
runs-on: ubuntu-latest
outputs:
tests: ${{ steps['set-tests'].outputs['tests'] }}
steps:
- uses: actions/checkout@v2
- run: git --version
- run: git fetch origin main
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn install --network-timeout 1000000
- id: set-tests
run: |
TESTS_ARRAY=$(node utils/chunk-tests.js $SCRIPT_NAME)
echo "Files to test:"
echo "$TESTS_ARRAY"
echo "::set-output name=tests::$TESTS_ARRAY"
test:
timeout-minutes: 120
runs-on: ${{ matrix.runner }}
name: ${{matrix.scriptName}} (${{matrix.packageName}}, ${{matrix.chunkNumber}}, ${{ matrix.runner }})
if: ${{ needs.setup.outputs['tests'] != '[]' }}
needs:
- setup
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.setup.outputs['tests']) }}
steps:
- name: Conditionally set remote env
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
echo "TURBO_REMOTE_ONLY=true" >> $GITHUB_ENV
echo "TURBO_TEAM=vercel" >> $GITHUB_ENV
echo "TURBO_TOKEN=${{ secrets.TURBO_TOKEN }}" >> $GITHUB_ENV
- uses: actions/checkout@v2
with:
fetch-depth: 2
- uses: actions/setup-go@v2
with:
go-version: '1.13.15'
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- 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/
- run: yarn install --network-timeout 1000000
- name: Build ${{matrix.packageName}} and all its dependencies
run: yarn turbo run build --cache-dir=".turbo" --scope=${{matrix.packageName}} --include-dependencies --no-deps
env:
FORCE_COLOR: '1'
- name: Test ${{matrix.packageName}}
run: node_modules/.bin/turbo run test --cache-dir=".turbo" --scope=${{matrix.packageName}} --no-deps -- ${{ join(matrix.testPaths, ' ') }}
shell: bash
env:
VERCEL_TEAM_TOKEN: ${{ secrets.VERCEL_TEAM_TOKEN }}
VERCEL_REGISTRATION_URL: ${{ secrets.VERCEL_REGISTRATION_URL }}
FORCE_COLOR: '1'
conclusion:
needs:
- test
runs-on: ubuntu-latest
name: E2E
steps:
- name: Done
run: echo "Done."

5
.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
# https://prettier.io/docs/en/ignore.html
# ignore these files with an intentional syntax error
packages/cli/test/dev/fixtures/edge-function-error/api/edge-error-syntax.js
packages/cli/test/fixtures/unit/commands/build/node-error/api/typescript.ts

View File

@@ -1,18 +1 @@
*
# general
!utils/
!utils/run.js
!.yarnrc
!yarn.lock
!package.json
!turbo.json
# api
!api/
!api/**
# packages
!packages/
!packages/frameworks
!packages/frameworks/**
packages/*/test/**

View File

@@ -1,5 +1,6 @@
import fs from 'fs/promises';
import { join } from 'path';
import { join, dirname } from 'path';
import execa from 'execa';
import { getExampleList } from '../examples/example-list';
import { mapOldToNew } from '../examples/map-old-to-new';
@@ -40,7 +41,32 @@ async function main() {
JSON.stringify([...existingExamples, ...oldExamples])
);
const { stdout: sha } = await execa('git', ['rev-parse', '--short', 'HEAD'], {
cwd: repoRoot,
});
const tarballsDir = join(pubDir, 'tarballs');
const packagesDir = join(repoRoot, 'packages');
const packages = await fs.readdir(packagesDir);
for (const pkg of packages) {
const fullDir = join(packagesDir, pkg);
const packageJsonRaw = await fs.readFile(
join(fullDir, 'package.json'),
'utf-8'
);
const packageJson = JSON.parse(packageJsonRaw);
const tarballName = `${packageJson.name
.replace('@', '')
.replace('/', '-')}-v${packageJson.version}-${sha.trim()}.tgz`;
const destTarballPath = join(tarballsDir, `${packageJson.name}.tgz`);
await fs.mkdir(dirname(destTarballPath), { recursive: true });
await fs.copyFile(join(fullDir, tarballName), destTarballPath);
}
console.log('Completed building static frontend.');
}
main().catch(console.error);
main().catch(err => {
console.log('error running build:', err);
process.exit(1);
});

View File

@@ -5,12 +5,13 @@
"description": "API for the vercel/vercel repo",
"main": "index.js",
"scripts": {
"vercel-build": "node ../utils/run.js build all"
"//TODO": "We should add this pkg to yarn workspaces",
"vercel-build": "cd .. && yarn install && yarn vercel-build"
},
"dependencies": {
"@sentry/node": "5.11.1",
"got": "10.2.1",
"node-fetch": "2.6.1",
"node-fetch": "2.6.7",
"parse-github-url": "1.0.2",
"tar-fs": "2.0.0",
"unzip-stream": "0.3.0"

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "esnext",
"target": "ES2020",
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,

View File

@@ -0,0 +1,2 @@
# `REACT_APP` prefix is required to expose to client-side
REACT_APP_VERCEL_ANALYTICS_ID=$VERCEL_ANALYTICS_ID

View File

@@ -1,13 +1,13 @@
{
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.3"
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { sendToVercelAnalytics } from './vitals';
ReactDOM.render(
<React.StrictMode>
@@ -11,7 +12,4 @@ ReactDOM.render(
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
reportWebVitals(sendToVercelAnalytics);

View File

@@ -0,0 +1,40 @@
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
return 'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in navigator['connection']
? navigator['connection']['effectiveType']
: '';
}
export function sendToVercelAnalytics(metric) {
const analyticsId = process.env.REACT_APP_VERCEL_ANALYTICS_ID;
if (!analyticsId) {
return;
}
const body = {
dsn: analyticsId,
id: metric.id,
page: window.location.pathname,
href: window.location.href,
event_name: metric.name,
value: metric.value.toString(),
speed: getConnectionSpeed(),
};
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded',
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
});
}

View File

@@ -1484,10 +1484,10 @@
"@svgr/plugin-svgo" "^5.5.0"
loader-utils "^2.0.0"
"@testing-library/dom@^8.0.0":
version "8.11.2"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.2.tgz#fc110c665a066c2287be765e4a35ba8dad737015"
integrity sha512-idsS/cqbYudXcVWngc1PuWNmXs416oBy2g/7Q8QAUREt5Z3MUkAL2XJD7xazLJ6esDfqRDi/ZBxk+OPPXitHRw==
"@testing-library/dom@^8.5.0":
version "8.13.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5"
integrity sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.12.5"
@@ -1498,10 +1498,10 @@
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^5.16.1":
version "5.16.1"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f"
integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw==
"@testing-library/jest-dom@^5.16.4":
version "5.16.4"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd"
integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==
dependencies:
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
@@ -1513,20 +1513,19 @@
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react@^12.1.2":
version "12.1.2"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76"
integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g==
"@testing-library/react@^13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.3.0.tgz#bf298bfbc5589326bbcc8052b211f3bb097a97c5"
integrity sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0"
"@testing-library/dom" "^8.5.0"
"@types/react-dom" "^18.0.0"
"@testing-library/user-event@^13.5.0":
version "13.5.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
dependencies:
"@babel/runtime" "^7.12.5"
"@testing-library/user-event@^14.2.0":
version "14.2.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.2.0.tgz#8293560f8f80a00383d6c755ec3e0b918acb1683"
integrity sha512-+hIlG4nJS6ivZrKnOP7OGsDu9Fxmryj9vCl8x0ZINtTJcCHs2zLsYif5GzuRiBF2ck5GZG2aQr7Msg+EHlnYVQ==
"@tootallnate/once@1":
version "1.1.2"
@@ -1735,6 +1734,11 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.3.tgz#a3c65525b91fca7da00ab1a3ac2b5a2a4afbffbf"
integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w==
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/q@^1.5.1":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
@@ -1750,6 +1754,22 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-dom@^18.0.0":
version "18.0.5"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.5.tgz#330b2d472c22f796e5531446939eacef8378444a"
integrity sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==
dependencies:
"@types/react" "*"
"@types/react@*":
version "18.0.9"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878"
integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -1762,6 +1782,11 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==
"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/serve-index@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
@@ -3175,6 +3200,11 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
csstype@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
damerau-levenshtein@^1.0.7:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -3622,10 +3652,10 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-config-react-app@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz#0fa96d5ec1dfb99c029b1554362ab3fa1c3757df"
integrity sha512-xyymoxtIt1EOsSaGag+/jmcywRuieQoA2JbPCjnw9HukFj9/97aGPoZVFioaotzk1K5Qt9sHO5EutZbkrAXS0g==
eslint-config-react-app@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4"
integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==
dependencies:
"@babel/core" "^7.16.0"
"@babel/eslint-parser" "^7.16.3"
@@ -6841,10 +6871,10 @@ react-app-polyfill@^3.0.0:
regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2"
react-dev-utils@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.0.tgz#4eab12cdb95692a077616770b5988f0adf806526"
integrity sha512-xBQkitdxozPxt1YZ9O1097EJiVpwHr9FoAuEVURCKV0Av8NBERovJauzP7bo1ThvuhZ4shsQ1AJiu4vQpoT1AQ==
react-dev-utils@^12.0.1:
version "12.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"
integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==
dependencies:
"@babel/code-frame" "^7.16.0"
address "^1.1.2"
@@ -6865,25 +6895,24 @@ react-dev-utils@^12.0.0:
open "^8.4.0"
pkg-up "^3.1.0"
prompts "^2.4.2"
react-error-overlay "^6.0.10"
react-error-overlay "^6.0.11"
recursive-readdir "^2.2.2"
shell-quote "^1.7.3"
strip-ansi "^6.0.1"
text-table "^0.2.0"
react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
react-dom@^18.1.0:
version "18.1.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f"
integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler "^0.20.2"
scheduler "^0.22.0"
react-error-overlay@^6.0.10:
version "6.0.10"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
react-error-overlay@^6.0.11:
version "6.0.11"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
react-is@^16.13.1:
version "16.13.1"
@@ -6900,10 +6929,10 @@ react-refresh@^0.11.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-scripts@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.0.tgz#6547a6d7f8b64364ef95273767466cc577cb4b60"
integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg==
react-scripts@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"
integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==
dependencies:
"@babel/core" "^7.16.0"
"@pmmmwh/react-refresh-webpack-plugin" "^0.5.3"
@@ -6921,7 +6950,7 @@ react-scripts@5.0.0:
dotenv "^10.0.0"
dotenv-expand "^5.1.0"
eslint "^8.3.0"
eslint-config-react-app "^7.0.0"
eslint-config-react-app "^7.0.1"
eslint-webpack-plugin "^3.1.1"
file-loader "^6.2.0"
fs-extra "^10.0.0"
@@ -6938,7 +6967,7 @@ react-scripts@5.0.0:
postcss-preset-env "^7.0.1"
prompts "^2.4.2"
react-app-polyfill "^3.0.0"
react-dev-utils "^12.0.0"
react-dev-utils "^12.0.1"
react-refresh "^0.11.0"
resolve "^1.20.0"
resolve-url-loader "^4.0.0"
@@ -6955,13 +6984,12 @@ react-scripts@5.0.0:
optionalDependencies:
fsevents "^2.3.2"
react@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
react@^18.1.0:
version "18.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
readable-stream@^2.0.1:
version "2.3.7"
@@ -7235,13 +7263,12 @@ saxes@^5.0.1:
dependencies:
xmlchars "^2.2.0"
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
scheduler@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8"
integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@2.7.0:
version "2.7.0"
@@ -8156,10 +8183,10 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
web-vitals@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.3.tgz#6dca59f41dbc3fcccdb889da06191b437b18f534"
integrity sha512-+ijpniAzcnQicXaXIN0/eHQAiV/jMt1oHGHTmz7VdAJPPkzzDhmoYPSpLgJTuFtUh+jCjxCoeTZPg7Ic+g8o7w==
web-vitals@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
webidl-conversions@^4.0.2:
version "4.0.2"

View File

@@ -48,6 +48,6 @@
"qunit-dom": "^0.8.4"
},
"engines": {
"node": "8.* || >= 10.*"
"node": "14.x"
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "Shopify Hydrogen",
"image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:0-16",
"settings": {},
"extensions": [
"graphql.vscode-graphql",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
],
"forwardPorts": [3000],
"postCreateCommand": "yarn install",
"postStartCommand": "yarn dev",
"remoteUser": "node",
"features": {
"git": "latest"
}
}

View File

@@ -0,0 +1,8 @@
module.exports = {
extends: ['plugin:hydrogen/recommended', 'plugin:hydrogen/typescript'],
rules: {
'node/no-missing-import': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/naming-convention': 'off',
},
};

79
examples/hydrogen/.gitignore vendored Normal file
View File

@@ -0,0 +1,79 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# Serverless directories
.serverless/
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Vite output
dist
.vercel

View File

@@ -0,0 +1,50 @@
# Hydrogen
[Hydrogen](https://shopify.dev/custom-storefronts/hydrogen) is a React framework and SDK that you can use to build fast and dynamic Shopify custom storefronts.
## Deploy Your Own
Deploy your own Hydrogen project with Vercel.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/vercel/tree/main/examples/hydrogen&template=hydrogen)
_Live Example: https://hydrogen-template.vercel.app_
## Getting started
**Requirements:**
- Node.js version 16.5.0 or higher
- Yarn
To create a new Hydrogen app, run:
```bash
npm init @shopify/hydrogen
```
## Running the dev server
Then `cd` into the new directory and run:
```bash
npm install
npm run dev
```
Remember to update `hydrogen.config.js` with your shop's domain and Storefront API token!
## Building for production
```bash
npm run build
```
## Previewing a production build
To run a local preview of your Hydrogen app in an environment similar to Oxygen, build your Hydrogen app and then run `npm run preview`:
```bash
npm run build
npm run preview
```

View File

@@ -0,0 +1,18 @@
import {defineConfig, CookieSessionStorage} from '@shopify/hydrogen/config';
export default defineConfig({
shopify: {
defaultCountryCode: 'US',
defaultLanguageCode: 'EN',
storeDomain: 'hydrogen-preview.myshopify.com',
storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
storefrontApiVersion: '2022-07',
},
session: CookieSessionStorage('__session', {
path: '/',
httpOnly: true,
secure: import.meta.env.PROD,
sameSite: 'Strict',
maxAge: 60 * 60 * 24 * 30,
}),
});

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hydrogen</title>
<link rel="stylesheet" href="/src/styles/index.css" />
<link rel="preconnect" href="https://cdn.shopify.com" />
<link rel="preconnect" href="https://shop.app/" />
<link rel="preconnect" href="https://hydrogen-preview.myshopify.com/" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/@shopify/hydrogen/entry-client"></script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
{
"name": "hydrogen",
"description": "Demo store template for @shopify/hydrogen",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"dev": "shopify hydrogen dev",
"build": "shopify hydrogen build",
"preview": "shopify hydrogen preview",
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
"lint-ts": "tsc --noEmit",
"test": "WATCH=true vitest",
"test:ci": "yarn build -t node && vitest run"
},
"devDependencies": {
"@shopify/cli": "3.0.27",
"@shopify/cli-hydrogen": "3.0.27",
"@shopify/prettier-config": "^1.1.2",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.2",
"@types/react": "^18.0.14",
"eslint": "^8.18.0",
"eslint-plugin-hydrogen": "^0.12.2",
"playwright": "^1.22.2",
"postcss": "^8.4.14",
"postcss-import": "^14.1.0",
"postcss-preset-env": "^7.6.0",
"prettier": "^2.3.2",
"tailwindcss": "^3.0.24",
"typescript": "^4.7.2",
"vite": "^2.9.0",
"vitest": "^0.15.2"
},
"prettier": "@shopify/prettier-config",
"dependencies": {
"@headlessui/react": "^1.6.4",
"@heroicons/react": "^1.0.6",
"@shopify/hydrogen": "^1.0.2",
"clsx": "^1.1.1",
"graphql-tag": "^2.12.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-use": "^17.4.0",
"title": "^3.4.4",
"typographic-base": "^1.0.4"
},
"author": "nrajlich"
}

View File

@@ -0,0 +1,10 @@
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
'postcss-preset-env': {
features: {'nesting-rules': false},
},
},
};

Binary file not shown.

View File

@@ -0,0 +1,48 @@
import {Suspense} from 'react';
import renderHydrogen from '@shopify/hydrogen/entry-server';
import {
FileRoutes,
type HydrogenRouteProps,
PerformanceMetrics,
PerformanceMetricsDebug,
Route,
Router,
ShopifyAnalytics,
ShopifyProvider,
CartProvider,
} from '@shopify/hydrogen';
import {HeaderFallback} from '~/components';
import type {CountryCode} from '@shopify/hydrogen/storefront-api-types';
import {DefaultSeo, NotFound} from '~/components/index.server';
function App({request}: HydrogenRouteProps) {
const pathname = new URL(request.normalizedUrl).pathname;
const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname);
const countryCode = localeMatch ? (localeMatch[1] as CountryCode) : undefined;
const isHome = pathname === `/${countryCode ? countryCode + '/' : ''}`;
return (
<Suspense fallback={<HeaderFallback isHome={isHome} />}>
<ShopifyProvider countryCode={countryCode}>
<CartProvider countryCode={countryCode}>
<Suspense>
<DefaultSeo />
</Suspense>
<Router>
<FileRoutes
basePath={countryCode ? `/${countryCode}/` : undefined}
/>
<Route path="*" page={<NotFound />} />
</Router>
</CartProvider>
<PerformanceMetrics />
{import.meta.env.DEV && <PerformanceMetricsDebug />}
<ShopifyAnalytics />
</ShopifyProvider>
</Suspense>
);
}
export default renderHydrogen(App);

View File

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
<style>
.stroke {
stroke: #000;
}
.fill {
fill: #000;
}
@media (prefers-color-scheme: dark) {
.stroke {
stroke: #fff;
}
.fill {
fill: #fff;
}
}
</style>
<path
class="stroke"
fill-rule="evenodd"
d="M16.1 16.04 1 8.02 6.16 5.3l5.82 3.09 4.88-2.57-5.82-3.1L16.21 0l15.1 8.02-5.17 2.72-5.5-2.91-4.88 2.57 5.5 2.92-5.16 2.72Z"
/>
<path
class="fill"
fill-rule="evenodd"
d="M16.1 32 1 23.98l5.16-2.72 5.82 3.08 4.88-2.57-5.82-3.08 5.17-2.73 15.1 8.02-5.17 2.72-5.5-2.92-4.88 2.58 5.5 2.92L16.1 32Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 690 B

View File

@@ -0,0 +1,135 @@
import {useCallback, useState, Suspense} from 'react';
import {useLocalization, fetchSync} from '@shopify/hydrogen';
// @ts-expect-error @headlessui/react incompatibility with node16 resolution
import {Listbox} from '@headlessui/react';
import {IconCheck, IconCaret} from '~/components';
import {useMemo} from 'react';
import type {
Country,
CountryCode,
} from '@shopify/hydrogen/storefront-api-types';
/**
* A client component that selects the appropriate country to display for products on a website
*/
export function CountrySelector() {
const [listboxOpen, setListboxOpen] = useState(false);
const {
country: {isoCode},
} = useLocalization();
const currentCountry = useMemo<{name: string; isoCode: CountryCode}>(() => {
const regionNamesInEnglish = new Intl.DisplayNames(['en'], {
type: 'region',
});
return {
name: regionNamesInEnglish.of(isoCode)!,
isoCode: isoCode as CountryCode,
};
}, [isoCode]);
const setCountry = useCallback<(country: Country) => void>(
({isoCode: newIsoCode}) => {
const currentPath = window.location.pathname;
let redirectPath;
if (newIsoCode !== 'US') {
if (currentCountry.isoCode === 'US') {
redirectPath = `/${newIsoCode.toLowerCase()}${currentPath}`;
} else {
redirectPath = `/${newIsoCode.toLowerCase()}${currentPath.substring(
currentPath.indexOf('/', 1),
)}`;
}
} else {
redirectPath = `${currentPath.substring(currentPath.indexOf('/', 1))}`;
}
window.location.href = redirectPath;
},
[currentCountry],
);
return (
<div className="relative">
<Listbox onChange={setCountry}>
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
{({open}) => {
setTimeout(() => setListboxOpen(open));
return (
<>
<Listbox.Button
className={`flex items-center justify-between w-full py-3 px-4 border ${
open ? 'rounded-b md:rounded-t md:rounded-b-none' : 'rounded'
} border-contrast/30 dark:border-white`}
>
<span className="">{currentCountry.name}</span>
<IconCaret direction={open ? 'up' : 'down'} />
</Listbox.Button>
<Listbox.Options
className={`border-t-contrast/30 border-contrast/30 bg-primary dark:bg-contrast absolute bottom-12 z-10 grid
h-48 w-full overflow-y-scroll rounded-t border dark:border-white px-2 py-2
transition-[max-height] duration-150 sm:bottom-auto md:rounded-b md:rounded-t-none
md:border-t-0 md:border-b ${
listboxOpen ? 'max-h-48' : 'max-h-0'
}`}
>
{listboxOpen && (
<Suspense fallback={<div className="p-2">Loading</div>}>
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
<Countries
selectedCountry={currentCountry}
getClassName={(active) => {
return `text-contrast dark:text-primary bg-primary
dark:bg-contrast w-full p-2 transition rounded
flex justify-start items-center text-left cursor-pointer ${
active ? 'bg-primary/10' : null
}`;
}}
/>
</Suspense>
)}
</Listbox.Options>
</>
);
}}
</Listbox>
</div>
);
}
export function Countries({
selectedCountry,
getClassName,
}: {
selectedCountry: Pick<Country, 'isoCode' | 'name'>;
getClassName: (active: boolean) => string;
}) {
const countries: Country[] = fetchSync('/api/countries').json();
return (countries || []).map((country) => {
const isSelected = country.isoCode === selectedCountry.isoCode;
return (
<Listbox.Option key={country.isoCode} value={country}>
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
{({active}) => (
<div
className={`text-contrast dark:text-primary ${getClassName(
active,
)}`}
>
{country.name}
{isSelected ? (
<span className="ml-2">
<IconCheck />
</span>
) : null}
</div>
)}
</Listbox.Option>
);
});
}

View File

@@ -0,0 +1,22 @@
// When making building your custom storefront, you will most likely want to
// use custom fonts as well. These are often implemented without critical
// performance optimizations.
// Below, you'll find the markup needed to optimally render a pair of web fonts
// that we will use on our journal articles. This typeface, IBM Plex,
// can be found at: https://www.ibm.com/plex/, as well as on
// Google Fonts: https://fonts.google.com/specimen/IBM+Plex+Serif. We included
// these locally since youll most likely be using commercially licensed fonts.
// When implementing a custom font, specifying the Unicode range you need,
// and using `font-display: swap` will help you improve your performance.
// For fonts that appear in the critical rendering path, you can speed up
// performance even more by including a <link> tag in your HTML.
// In a production environment, you will likely want to include the below
// markup right in your index.html and index.css files.
import '../styles/custom-font.css';
export function CustomFont() {}

View File

@@ -0,0 +1,37 @@
import {CacheLong, gql, Seo, useShopQuery} from '@shopify/hydrogen';
/**
* A server component that fetches a `shop.name` and sets default values and templates for every page on a website
*/
export function DefaultSeo() {
const {
data: {
shop: {name, description},
},
} = useShopQuery({
query: SHOP_QUERY,
cache: CacheLong(),
preload: '*',
});
return (
// @ts-ignore TODO: Fix types
<Seo
type="defaultSeo"
data={{
title: name,
description,
titleTemplate: `%s · ${name}`,
}}
/>
);
}
const SHOP_QUERY = gql`
query shopInfo {
shop {
name
description
}
}
`;

View File

@@ -0,0 +1,30 @@
export function HeaderFallback({isHome}: {isHome?: boolean}) {
const styles = isHome
? 'bg-primary/80 dark:bg-contrast/60 text-contrast dark:text-primary shadow-darkHeader'
: 'bg-contrast/80 text-primary';
return (
<header
role="banner"
className={`${styles} flex h-nav items-center backdrop-blur-lg z-40 top-0 justify-between w-full leading-none gap-8 px-12 py-8`}
>
<div className="flex space-x-4">
<Box isHome={isHome} />
<Box isHome={isHome} />
<Box isHome={isHome} />
<Box isHome={isHome} />
<Box isHome={isHome} />
</div>
<Box isHome={isHome} wide={true} />
</header>
);
}
function Box({wide, isHome}: {wide?: boolean; isHome?: boolean}) {
return (
<div
className={`h-6 rounded-sm ${wide ? 'w-32' : 'w-16'} ${
isHome ? 'bg-primary/60' : 'bg-primary/20'
}`}
/>
);
}

View File

@@ -0,0 +1,183 @@
import {useState} from 'react';
import {useNavigate} from '@shopify/hydrogen/client';
export function AccountActivateForm({
id,
activationToken,
}: {
id: string;
activationToken: string;
}) {
const navigate = useNavigate();
const [submitError, setSubmitError] = useState<null | string>(null);
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState<null | string>(null);
const [passwordConfirm, setPasswordConfirm] = useState('');
const [passwordConfirmError, setPasswordConfirmError] = useState<
null | string
>(null);
function passwordValidation(
form: HTMLFormElement & {password: HTMLInputElement},
) {
setPasswordError(null);
setPasswordConfirmError(null);
let hasError = false;
if (!form.password.validity.valid) {
hasError = true;
setPasswordError(
form.password.validity.valueMissing
? 'Please enter a password'
: 'Passwords must be at least 6 characters',
);
}
if (!form.passwordConfirm.validity.valid) {
hasError = true;
setPasswordConfirmError(
form.password.validity.valueMissing
? 'Please re-enter a password'
: 'Passwords must be at least 6 characters',
);
}
if (password !== passwordConfirm) {
hasError = true;
setPasswordConfirmError('The two passwords entered did not match.');
}
return hasError;
}
async function onSubmit(
event: React.FormEvent<HTMLFormElement & {password: HTMLInputElement}>,
) {
event.preventDefault();
if (passwordValidation(event.currentTarget)) {
return;
}
const response = await callActivateApi({
id,
activationToken,
password,
});
if (response.error) {
setSubmitError(response.error);
return;
}
navigate('/account');
}
return (
<div className="flex justify-center">
<div className="w-full max-w-md">
<h1 className="text-4xl">Activate Account.</h1>
<p className="mt-4">Create your password to activate your account.</p>
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
{submitError && (
<div className="flex items-center justify-center mb-6 bg-primary/30">
<p className="m-4 text-s text-contrast">{submitError}</p>
</div>
)}
<div className="mb-4">
<input
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary placeholder:text-primary/30 leading-tight focus:shadow-outline ${
passwordError ? ' border-notice' : 'border-primary'
}`}
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="Password"
aria-label="Password"
value={password}
minLength={8}
required
onChange={(event) => {
setPassword(event.target.value);
}}
/>
<p
className={`text-red-500 text-xs ${
!passwordError ? 'invisible' : ''
}`}
>
{passwordError} &nbsp;
</p>
</div>
<div className="mb-4">
<input
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
passwordConfirmError ? ' border-red-500' : 'border-gray-900'
}`}
id="passwordConfirm"
name="passwordConfirm"
type="password"
autoComplete="current-password"
placeholder="Re-enter password"
aria-label="Re-enter password"
value={passwordConfirm}
required
minLength={8}
onChange={(event) => {
setPasswordConfirm(event.target.value);
}}
/>
<p
className={`text-red-500 text-xs ${
!passwordConfirmError ? 'invisible' : ''
}`}
>
{passwordConfirmError} &nbsp;
</p>
</div>
<div className="flex items-center justify-between">
<button
className="block w-full px-4 py-2 text-contrast uppercase bg-gray-900 focus:shadow-outline"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
);
}
async function callActivateApi({
id,
activationToken,
password,
}: {
id: string;
activationToken: string;
password: string;
}) {
try {
const res = await fetch(`/account/activate`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({id, activationToken, password}),
});
if (res.ok) {
return {};
} else {
return res.json();
}
} catch (error: any) {
return {
error: error.toString(),
};
}
}

View File

@@ -0,0 +1,161 @@
import {useState, useMemo, MouseEventHandler} from 'react';
import {Text, Button} from '~/components/elements';
import {Modal} from '../index';
import {AccountAddressEdit, AccountDeleteAddress} from '../index';
export function AccountAddressBook({
addresses,
defaultAddress,
}: {
addresses: any[];
defaultAddress: any;
}) {
const [editingAddress, setEditingAddress] = useState(null);
const [deletingAddress, setDeletingAddress] = useState(null);
const {fullDefaultAddress, addressesWithoutDefault} = useMemo(() => {
const defaultAddressIndex = addresses.findIndex(
(address) => address.id === defaultAddress,
);
return {
addressesWithoutDefault: [
...addresses.slice(0, defaultAddressIndex),
...addresses.slice(defaultAddressIndex + 1, addresses.length),
],
fullDefaultAddress: addresses[defaultAddressIndex],
};
}, [addresses, defaultAddress]);
function close() {
setEditingAddress(null);
setDeletingAddress(null);
}
function editAddress(address: any) {
setEditingAddress(address);
}
return (
<>
{deletingAddress ? (
<Modal close={close}>
<AccountDeleteAddress addressId={deletingAddress} close={close} />
</Modal>
) : null}
{editingAddress ? (
<Modal close={close}>
<AccountAddressEdit
address={editingAddress}
defaultAddress={fullDefaultAddress === editingAddress}
close={close}
/>
</Modal>
) : null}
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
<h3 className="font-bold text-lead">Address Book</h3>
<div>
{!addresses?.length ? (
<Text className="mb-1" width="narrow" as="p" size="copy">
You haven&apos;t saved any addresses yet.
</Text>
) : null}
<div className="w-48">
<Button
className="mt-2 text-sm w-full mb-6"
onClick={() => {
editAddress({
/** empty address */
});
}}
variant="secondary"
>
Add an Address
</Button>
</div>
{addresses?.length ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{fullDefaultAddress ? (
<Address
address={fullDefaultAddress}
defaultAddress
setDeletingAddress={setDeletingAddress.bind(
null,
fullDefaultAddress.originalId,
)}
editAddress={editAddress}
/>
) : null}
{addressesWithoutDefault.map((address) => (
<Address
key={address.id}
address={address}
setDeletingAddress={setDeletingAddress.bind(
null,
address.originalId,
)}
editAddress={editAddress}
/>
))}
</div>
) : null}
</div>
</div>
</>
);
}
function Address({
address,
defaultAddress,
editAddress,
setDeletingAddress,
}: {
address: any;
defaultAddress?: boolean;
editAddress: (address: any) => void;
setDeletingAddress: MouseEventHandler<HTMLButtonElement>;
}) {
return (
<div className="lg:p-8 p-6 border border-gray-200 rounded flex flex-col">
{defaultAddress ? (
<div className="mb-3 flex flex-row">
<span className="px-3 py-1 text-xs font-medium rounded-full bg-primary/20 text-primary/50">
Default
</span>
</div>
) : null}
<ul className="flex-1 flex-row">
{address.firstName || address.lastName ? (
<li>
{(address.firstName && address.firstName + ' ') + address.lastName}
</li>
) : (
<></>
)}
{address.formatted ? (
address.formatted.map((line: string) => <li key={line}>{line}</li>)
) : (
<></>
)}
</ul>
<div className="flex flex-row font-medium mt-6">
<button
onClick={() => {
editAddress(address);
}}
className="text-left underline text-sm"
>
Edit
</button>
<button
onClick={setDeletingAddress}
className="text-left text-primary/50 ml-6 text-sm"
>
Remove
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,337 @@
import {useMemo, useState} from 'react';
import {useRenderServerComponents} from '~/lib/utils';
import {Button, Text} from '~/components';
export function AccountAddressEdit({
address,
defaultAddress,
close,
}: {
address: any;
defaultAddress: boolean;
close: () => void;
}) {
const isNewAddress = useMemo(() => !Object.keys(address).length, [address]);
const [saving, setSaving] = useState(false);
const [submitError, setSubmitError] = useState<null | string>(null);
const [address1, setAddress1] = useState(address?.address1 || '');
const [address2, setAddress2] = useState(address?.address2 || '');
const [firstName, setFirstName] = useState(address?.firstName || '');
const [lastName, setLastName] = useState(address?.lastName || '');
const [company, setCompany] = useState(address?.company || '');
const [country, setCountry] = useState(address?.country || '');
const [province, setProvince] = useState(address?.province || '');
const [city, setCity] = useState(address?.city || '');
const [zip, setZip] = useState(address?.zip || '');
const [phone, setPhone] = useState(address?.phone || '');
const [isDefaultAddress, setIsDefaultAddress] = useState(defaultAddress);
// Necessary for edits to show up on the main page
const renderServerComponents = useRenderServerComponents();
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setSaving(true);
const response = await callUpdateAddressApi({
id: address?.originalId,
firstName,
lastName,
company,
address1,
address2,
country,
province,
city,
zip,
phone,
isDefaultAddress,
});
setSaving(false);
if (response.error) {
setSubmitError(response.error);
return;
}
renderServerComponents();
close();
}
return (
<>
<Text className="mt-4 mb-6" as="h3" size="lead">
{isNewAddress ? 'Add address' : 'Edit address'}
</Text>
<div className="max-w-lg">
<form noValidate onSubmit={onSubmit}>
{submitError && (
<div className="flex items-center justify-center mb-6 bg-red-100 rounded">
<p className="m-4 text-sm text-red-900">{submitError}</p>
</div>
)}
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="firstname"
name="firstname"
required
type="text"
autoComplete="given-name"
placeholder="First name"
aria-label="First name"
value={firstName}
onChange={(event) => {
setFirstName(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="lastname"
name="lastname"
required
type="text"
autoComplete="family-name"
placeholder="Last name"
aria-label="Last name"
value={lastName}
onChange={(event) => {
setLastName(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="company"
name="company"
type="text"
autoComplete="organization"
placeholder="Company"
aria-label="Company"
value={company}
onChange={(event) => {
setCompany(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="street1"
name="street1"
type="text"
autoComplete="address-line1"
placeholder="Address line 1*"
required
aria-label="Address line 1"
value={address1}
onChange={(event) => {
setAddress1(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="address2"
name="address2"
type="text"
autoComplete="address-line2"
placeholder="Addresss line 2"
aria-label="Address line 2"
value={address2}
onChange={(event) => {
setAddress2(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="city"
name="city"
type="text"
required
autoComplete="address-level2"
placeholder="City"
aria-label="City"
value={city}
onChange={(event) => {
setCity(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="state"
name="state"
type="text"
autoComplete="address-level1"
placeholder="State / Province"
required
aria-label="State"
value={province}
onChange={(event) => {
setProvince(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="zip"
name="zip"
type="text"
autoComplete="postal-code"
placeholder="Zip / Postal Code"
required
aria-label="Zip"
value={zip}
onChange={(event) => {
setZip(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="country"
name="country"
type="text"
autoComplete="country-name"
placeholder="Country"
required
aria-label="Country"
value={country}
onChange={(event) => {
setCountry(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="phone"
name="phone"
type="tel"
autoComplete="tel"
placeholder="Phone"
aria-label="Phone"
value={phone}
onChange={(event) => {
setPhone(event.target.value);
}}
/>
</div>
<div className="mt-4">
<input
type="checkbox"
value=""
name="defaultAddress"
id="defaultAddress"
checked={isDefaultAddress}
className="border-gray-500 rounded-sm cursor-pointer border-1"
onChange={() => setIsDefaultAddress(!isDefaultAddress)}
/>
<label
className="inline-block ml-2 text-sm cursor-pointer"
htmlFor="defaultAddress"
>
Set as default address
</label>
</div>
<div className="mt-8">
<Button
className="w-full rounded focus:shadow-outline"
type="submit"
variant="primary"
disabled={saving}
>
Save
</Button>
</div>
<div>
<Button
className="w-full mt-2 rounded focus:shadow-outline"
variant="secondary"
onClick={close}
>
Cancel
</Button>
</div>
</form>
</div>
</>
);
}
export async function callUpdateAddressApi({
id,
firstName,
lastName,
company,
address1,
address2,
country,
province,
city,
phone,
zip,
isDefaultAddress,
}: {
id: string;
firstName: string;
lastName: string;
company: string;
address1: string;
address2: string;
country: string;
province: string;
city: string;
phone: string;
zip: string;
isDefaultAddress: boolean;
}) {
try {
const res = await fetch(
id ? `/account/address/${encodeURIComponent(id)}` : '/account/address',
{
method: id ? 'PATCH' : 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName,
lastName,
company,
address1,
address2,
country,
province,
city,
phone,
zip,
isDefaultAddress,
}),
},
);
if (res.ok) {
return {};
} else {
return res.json();
}
} catch (_e) {
return {
error: 'Error saving address. Please try again.',
};
}
}

View File

@@ -0,0 +1,175 @@
import {useState} from 'react';
import {useNavigate, Link} from '@shopify/hydrogen/client';
import {emailValidation, passwordValidation} from '~/lib/utils';
import {callLoginApi} from './AccountLoginForm.client';
interface FormElements {
email: HTMLInputElement;
password: HTMLInputElement;
}
export function AccountCreateForm() {
const navigate = useNavigate();
const [submitError, setSubmitError] = useState<null | string>(null);
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState<null | string>(null);
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState<null | string>(null);
async function onSubmit(
event: React.FormEvent<HTMLFormElement & FormElements>,
) {
event.preventDefault();
setEmailError(null);
setPasswordError(null);
setSubmitError(null);
const newEmailError = emailValidation(event.currentTarget.email);
if (newEmailError) {
setEmailError(newEmailError);
}
const newPasswordError = passwordValidation(event.currentTarget.password);
if (newPasswordError) {
setPasswordError(newPasswordError);
}
if (newEmailError || newPasswordError) {
return;
}
const accountCreateResponse = await callAccountCreateApi({
email,
password,
});
if (accountCreateResponse.error) {
setSubmitError(accountCreateResponse.error);
return;
}
// this can be avoided if customerCreate mutation returns customerAccessToken
await callLoginApi({
email,
password,
});
navigate('/account');
}
return (
<div className="flex justify-center my-24 px-4">
<div className="max-w-md w-full">
<h1 className="text-4xl">Create an Account.</h1>
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
{submitError && (
<div className="flex items-center justify-center mb-6 bg-zinc-500">
<p className="m-4 text-s text-contrast">{submitError}</p>
</div>
)}
<div className="mb-3">
<input
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
emailError ? ' border-red-500' : 'border-gray-900'
}`}
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="Email address"
aria-label="Email address"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
{!emailError ? (
''
) : (
<p className={`text-red-500 text-xs`}>{emailError} &nbsp;</p>
)}
</div>
<div className="mb-3">
<input
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
passwordError ? ' border-red-500' : 'border-gray-900'
}`}
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="Password"
aria-label="Password"
value={password}
minLength={8}
required
onChange={(event) => {
setPassword(event.target.value);
}}
/>
{!passwordError ? (
''
) : (
<p className={`text-red-500 text-xs`}>{passwordError} &nbsp;</p>
)}
</div>
<div className="flex items-center justify-between">
<button
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
type="submit"
>
Create Account
</button>
</div>
<div className="flex items-center mt-4">
<p className="align-baseline text-sm">
Already have an account? &nbsp;
<Link className="inline underline" to="/account">
Sign in
</Link>
</p>
</div>
</form>
</div>
</div>
);
}
export async function callAccountCreateApi({
email,
password,
firstName,
lastName,
}: {
email: string;
password: string;
firstName?: string;
lastName?: string;
}) {
try {
const res = await fetch(`/account/register`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({email, password, firstName, lastName}),
});
if (res.status === 200) {
return {};
} else {
return res.json();
}
} catch (error: any) {
return {
error: error.toString(),
};
}
}

View File

@@ -0,0 +1,72 @@
import {Text, Button} from '~/components/elements';
import {useRenderServerComponents} from '~/lib/utils';
export function AccountDeleteAddress({
addressId,
close,
}: {
addressId: string;
close: () => void;
}) {
// Necessary for edits to show up on the main page
const renderServerComponents = useRenderServerComponents();
async function deleteAddress(id: string) {
const response = await callDeleteAddressApi(id);
if (response.error) {
alert(response.error);
return;
}
renderServerComponents();
close();
}
return (
<>
<Text className="mb-4" as="h3" size="lead">
Confirm removal
</Text>
<Text as="p">Are you sure you wish to remove this address?</Text>
<div className="mt-6">
<Button
className="text-sm"
onClick={() => {
deleteAddress(addressId);
}}
variant="primary"
width="full"
>
Confirm
</Button>
<Button
className="text-sm mt-2"
onClick={close}
variant="secondary"
width="full"
>
Cancel
</Button>
</div>
</>
);
}
export async function callDeleteAddressApi(id: string) {
try {
const res = await fetch(`/account/address/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
},
});
if (res.ok) {
return {};
} else {
return res.json();
}
} catch (_e) {
return {
error: 'Error removing address. Please try again.',
};
}
}

View File

@@ -0,0 +1,66 @@
import {Seo} from '@shopify/hydrogen';
import {useState} from 'react';
import {Modal} from '../index';
import {AccountDetailsEdit} from './AccountDetailsEdit.client';
export function AccountDetails({
firstName,
lastName,
phone,
email,
}: {
firstName?: string;
lastName?: string;
phone?: string;
email?: string;
}) {
const [isEditing, setIsEditing] = useState(false);
const close = () => setIsEditing(false);
return (
<>
{isEditing ? (
<Modal close={close}>
<Seo type="noindex" data={{title: 'Account details'}} />
<AccountDetailsEdit
firstName={firstName}
lastName={lastName}
phone={phone}
email={email}
close={close}
/>
</Modal>
) : null}
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
<h3 className="font-bold text-lead">Account Details</h3>
<div className="lg:p-8 p-6 border border-gray-200 rounded">
<div className="flex">
<h3 className="font-bold text-base flex-1">Profile & Security</h3>
<button
className="underline text-sm font-normal"
onClick={() => setIsEditing(true)}
>
Edit
</button>
</div>
<div className="mt-4 text-sm text-primary/50">Name</div>
<p className="mt-1">
{firstName || lastName
? (firstName ? firstName + ' ' : '') + lastName
: 'Add name'}{' '}
</p>
<div className="mt-4 text-sm text-primary/50">Contact</div>
<p className="mt-1">{phone ?? 'Add mobile'}</p>
<div className="mt-4 text-sm text-primary/50">Email address</div>
<p className="mt-1">{email}</p>
<div className="mt-4 text-sm text-primary/50">Password</div>
<p className="mt-1">**************</p>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,342 @@
import {useState} from 'react';
import {Text, Button} from '~/components';
import {
emailValidation,
passwordValidation,
useRenderServerComponents,
} from '~/lib/utils';
interface FormElements {
firstName: HTMLInputElement;
lastName: HTMLInputElement;
phone: HTMLInputElement;
email: HTMLInputElement;
currentPassword: HTMLInputElement;
newPassword: HTMLInputElement;
newPassword2: HTMLInputElement;
}
export function AccountDetailsEdit({
firstName: _firstName = '',
lastName: _lastName = '',
phone: _phone = '',
email: _email = '',
close,
}: {
firstName?: string;
lastName?: string;
phone?: string;
email?: string;
close: () => void;
}) {
const [saving, setSaving] = useState(false);
const [firstName, setFirstName] = useState(_firstName);
const [lastName, setLastName] = useState(_lastName);
const [phone, setPhone] = useState(_phone);
const [email, setEmail] = useState(_email);
const [emailError, setEmailError] = useState<null | string>(null);
const [currentPasswordError, setCurrentPasswordError] = useState<
null | string
>(null);
const [newPasswordError, setNewPasswordError] = useState<null | string>(null);
const [newPassword2Error, setNewPassword2Error] = useState<null | string>(
null,
);
const [submitError, setSubmitError] = useState<null | string>(null);
// Necessary for edits to show up on the main page
const renderServerComponents = useRenderServerComponents();
async function onSubmit(
event: React.FormEvent<HTMLFormElement & FormElements>,
) {
event.preventDefault();
setEmailError(null);
setCurrentPasswordError(null);
setNewPasswordError(null);
setNewPassword2Error(null);
const emailError = emailValidation(event.currentTarget.email);
if (emailError) {
setEmailError(emailError);
}
let currentPasswordError, newPasswordError, newPassword2Error;
// Only validate the password fields if the current password has a value
if (event.currentTarget.currentPassword.value) {
currentPasswordError = passwordValidation(
event.currentTarget.currentPassword,
);
if (currentPasswordError) {
setCurrentPasswordError(currentPasswordError);
}
newPasswordError = passwordValidation(event.currentTarget.newPassword);
if (newPasswordError) {
setNewPasswordError(newPasswordError);
}
newPassword2Error =
event.currentTarget.newPassword.value !==
event.currentTarget.newPassword2.value
? 'The two passwords entered did not match'
: null;
if (newPassword2Error) {
setNewPassword2Error(newPassword2Error);
}
}
if (
emailError ||
currentPasswordError ||
newPasswordError ||
newPassword2Error
) {
return;
}
setSaving(true);
const accountUpdateResponse = await callAccountUpdateApi({
email,
newPassword: event.currentTarget.newPassword.value,
currentPassword: event.currentTarget.currentPassword.value,
phone,
firstName,
lastName,
});
setSaving(false);
if (accountUpdateResponse.error) {
setSubmitError(accountUpdateResponse.error);
return;
}
renderServerComponents();
close();
}
return (
<>
<Text className="mt-4 mb-6" as="h3" size="lead">
Update your profile
</Text>
<form noValidate onSubmit={onSubmit}>
{submitError && (
<div className="flex items-center justify-center mb-6 bg-red-100 rounded">
<p className="m-4 text-sm text-red-900">{submitError}</p>
</div>
)}
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="firstname"
name="firstname"
type="text"
autoComplete="given-name"
placeholder="First name"
aria-label="First name"
value={firstName}
onChange={(event) => {
setFirstName(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="lastname"
name="lastname"
type="text"
autoComplete="family-name"
placeholder="Last name"
aria-label="Last name"
value={lastName}
onChange={(event) => {
setLastName(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline border-gray-500 rounded`}
id="phone"
name="phone"
type="tel"
autoComplete="tel"
placeholder="Mobile"
aria-label="Mobile"
value={phone}
onChange={(event) => {
setPhone(event.target.value);
}}
/>
</div>
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline rounded ${
emailError ? ' border-red-500' : 'border-gray-500'
}`}
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="Email address"
aria-label="Email address"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
<p
className={`text-red-500 text-xs ${!emailError ? 'invisible' : ''}`}
>
{emailError} &nbsp;
</p>
</div>
<Text className="mb-6 mt-6" as="h3" size="lead">
Change your password
</Text>
<Password
name="currentPassword"
label="Current password"
passwordError={currentPasswordError}
/>
<Password
name="newPassword"
label="New password"
passwordError={newPasswordError}
/>
<Password
name="newPassword2"
label="Re-enter new password"
passwordError={newPassword2Error}
/>
<Text
size="fine"
color="subtle"
className={`mt-1 ${
currentPasswordError || newPasswordError ? 'text-red-500' : ''
}`}
>
Passwords must be at least 6 characters.
</Text>
{newPassword2Error ? <br /> : null}
<Text
size="fine"
className={`mt-1 text-red-500 ${
newPassword2Error ? '' : 'invisible'
}`}
>
{newPassword2Error} &nbsp;
</Text>
<div className="mt-6">
<Button
className="text-sm mb-2"
variant="primary"
width="full"
type="submit"
disabled={saving}
>
Save
</Button>
</div>
<div className="mb-4">
<Button
type="button"
className="text-sm"
variant="secondary"
width="full"
onClick={close}
>
Cancel
</Button>
</div>
</form>
</>
);
}
function Password({
name,
passwordError,
label,
}: {
name: string;
passwordError: string | null;
label: string;
}) {
const [password, setPassword] = useState('');
return (
<div className="mt-3">
<input
className={`appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline rounded ${
passwordError ? ' border-red-500' : 'border-gray-500'
}`}
id={name}
name={name}
type="password"
autoComplete={
name === 'currentPassword' ? 'current-password' : undefined
}
placeholder={label}
aria-label={label}
value={password}
minLength={8}
required
onChange={(event) => {
setPassword(event.target.value);
}}
/>
</div>
);
}
export async function callAccountUpdateApi({
email,
phone,
firstName,
lastName,
currentPassword,
newPassword,
}: {
email: string;
phone: string;
firstName: string;
lastName: string;
currentPassword: string;
newPassword: string;
}) {
try {
const res = await fetch(`/account`, {
method: 'PATCH',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
phone,
firstName,
lastName,
currentPassword,
newPassword,
}),
});
if (res.ok) {
return {};
} else {
return res.json();
}
} catch (_e) {
return {
error: 'Error saving account. Please try again.',
};
}
}

View File

@@ -0,0 +1,285 @@
import {useState} from 'react';
import {useNavigate, Link} from '@shopify/hydrogen/client';
interface FormElements {
email: HTMLInputElement;
password: HTMLInputElement;
}
export function AccountLoginForm({shopName}: {shopName: string}) {
const navigate = useNavigate();
const [hasSubmitError, setHasSubmitError] = useState(false);
const [showEmailField, setShowEmailField] = useState(true);
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState<null | string>(null);
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState<null | string>(null);
function onSubmit(event: React.FormEvent<HTMLFormElement & FormElements>) {
event.preventDefault();
setEmailError(null);
setHasSubmitError(false);
setPasswordError(null);
if (showEmailField) {
checkEmail(event);
} else {
checkPassword(event);
}
}
function checkEmail(event: React.FormEvent<HTMLFormElement & FormElements>) {
if (event.currentTarget.email.validity.valid) {
setShowEmailField(false);
} else {
setEmailError('Please enter a valid email');
}
}
async function checkPassword(
event: React.FormEvent<HTMLFormElement & FormElements>,
) {
const validity = event.currentTarget.password.validity;
if (validity.valid) {
const response = await callLoginApi({
email,
password,
});
if (response.error) {
setHasSubmitError(true);
resetForm();
} else {
navigate('/account');
}
} else {
setPasswordError(
validity.valueMissing
? 'Please enter a password'
: 'Passwords must be at least 6 characters',
);
}
}
function resetForm() {
setShowEmailField(true);
setEmail('');
setEmailError(null);
setPassword('');
setPasswordError(null);
}
return (
<div className="flex justify-center my-24 px-4">
<div className="max-w-md w-full">
<h1 className="text-4xl">Sign in.</h1>
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
{hasSubmitError && (
<div className="flex items-center justify-center mb-6 bg-zinc-500">
<p className="m-4 text-s text-contrast">
Sorry we did not recognize either your email or password. Please
try to sign in again or create a new account.
</p>
</div>
)}
{showEmailField && (
<EmailField
shopName={shopName}
email={email}
setEmail={setEmail}
emailError={emailError}
/>
)}
{!showEmailField && (
<ValidEmail email={email} resetForm={resetForm} />
)}
{!showEmailField && (
<PasswordField
password={password}
setPassword={setPassword}
passwordError={passwordError}
/>
)}
</form>
</div>
</div>
);
}
export async function callLoginApi({
email,
password,
}: {
email: string;
password: string;
}) {
try {
const res = await fetch(`/account/login`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({email, password}),
});
if (res.ok) {
return {};
} else {
return res.json();
}
} catch (error: any) {
return {
error: error.toString(),
};
}
}
function EmailField({
email,
setEmail,
emailError,
shopName,
}: {
email: string;
setEmail: (email: string) => void;
emailError: null | string;
shopName: string;
}) {
return (
<>
<div className="mb-3">
<input
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
emailError ? ' border-red-500' : 'border-gray-900'
}`}
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="Email address"
aria-label="Email address"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
{!emailError ? (
''
) : (
<p className={`text-red-500 text-xs`}>{emailError} &nbsp;</p>
)}
</div>
<div className="flex items-center justify-between">
<button
className="bg-gray-900 rounded text-contrast py-2 px-4 focus:shadow-outline block w-full"
type="submit"
>
Next
</button>
</div>
<div className="flex items-center mt-8 border-t border-gray-300">
<p className="align-baseline text-sm mt-6">
New to {shopName}? &nbsp;
<Link className="inline underline" to="/account/register">
Create an account
</Link>
</p>
</div>
</>
);
}
function ValidEmail({
email,
resetForm,
}: {
email: string;
resetForm: () => void;
}) {
return (
<div className="mb-3 flex items-center justify-between">
<div>
<p>{email}</p>
<input
className="hidden"
type="text"
autoComplete="username"
value={email}
readOnly
></input>
</div>
<div>
<button
className="inline-block align-baseline text-sm underline"
type="button"
onClick={resetForm}
>
Change email
</button>
</div>
</div>
);
}
function PasswordField({
password,
setPassword,
passwordError,
}: {
password: string;
setPassword: (password: string) => void;
passwordError: null | string;
}) {
return (
<>
<div className="mb-3">
<input
className={`mb-1 appearance-none rounded border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
passwordError ? ' border-red-500' : 'border-gray-900'
}`}
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="Password"
aria-label="Password"
value={password}
minLength={8}
required
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
onChange={(event) => {
setPassword(event.target.value);
}}
/>
{!passwordError ? (
''
) : (
<p className={`text-red-500 text-xs`}> {passwordError} &nbsp;</p>
)}
</div>
<div className="flex items-center justify-between">
<button
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
type="submit"
>
Sign in
</button>
</div>
<div className="flex items-center justify-between mt-4">
<div className="flex-1"></div>
<Link
className="inline-block align-baseline text-sm text-primary/50"
to="/account/recover"
>
Forgot password
</Link>
</div>
</>
);
}

View File

@@ -0,0 +1,38 @@
import type {Order} from '@shopify/hydrogen/storefront-api-types';
import {Button, Text, OrderCard} from '~/components';
export function AccountOrderHistory({orders}: {orders: Order[]}) {
return (
<div className="mt-6">
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
<h2 className="font-bold text-lead">Order History</h2>
{orders?.length ? <Orders orders={orders} /> : <EmptyOrders />}
</div>
</div>
);
}
function EmptyOrders() {
return (
<div>
<Text className="mb-1" size="fine" width="narrow" as="p">
You haven&apos;t placed any orders yet.
</Text>
<div className="w-48">
<Button className="text-sm mt-2 w-full" variant="secondary" to={'/'}>
Start Shopping
</Button>
</div>
</div>
);
}
function Orders({orders}: {orders: Order[]}) {
return (
<ul className="grid-flow-row grid gap-2 gap-y-6 md:gap-4 lg:gap-6 grid-cols-1 false sm:grid-cols-3">
{orders.map((order) => (
<OrderCard order={order} key={order.id} />
))}
</ul>
);
}

View File

@@ -0,0 +1,189 @@
import {useState} from 'react';
import {useNavigate} from '@shopify/hydrogen/client';
interface FormElements {
password: HTMLInputElement;
passwordConfirm: HTMLInputElement;
}
export function AccountPasswordResetForm({
id,
resetToken,
}: {
id: string;
resetToken: string;
}) {
const navigate = useNavigate();
const [submitError, setSubmitError] = useState<string | null>(null);
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState<string | null>(null);
const [passwordConfirm, setPasswordConfirm] = useState('');
const [passwordConfirmError, setPasswordConfirmError] = useState<
string | null
>(null);
function passwordValidation(form: HTMLFormElement & FormElements) {
setPasswordError(null);
setPasswordConfirmError(null);
let hasError = false;
if (!form.password.validity.valid) {
hasError = true;
setPasswordError(
form.password.validity.valueMissing
? 'Please enter a password'
: 'Passwords must be at least 6 characters',
);
}
if (!form.passwordConfirm.validity.valid) {
hasError = true;
setPasswordConfirmError(
form.password.validity.valueMissing
? 'Please re-enter a password'
: 'Passwords must be at least 6 characters',
);
}
if (password !== passwordConfirm) {
hasError = true;
setPasswordConfirmError('The two password entered did not match.');
}
return hasError;
}
async function onSubmit(
event: React.FormEvent<HTMLFormElement & FormElements>,
) {
event.preventDefault();
if (passwordValidation(event.currentTarget)) {
return;
}
const response = await callPasswordResetApi({
id,
resetToken,
password,
});
if (response.error) {
setSubmitError(response.error);
return;
}
navigate('/account');
}
return (
<div className="flex justify-center my-24 px-4">
<div className="max-w-md w-full">
<h1 className="text-4xl">Reset Password.</h1>
<p className="mt-4">Enter a new password for your account.</p>
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
{submitError && (
<div className="flex items-center justify-center mb-6 bg-zinc-500">
<p className="m-4 text-s text-contrast">{submitError}</p>
</div>
)}
<div className="mb-3">
<input
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
passwordError ? ' border-red-500' : 'border-gray-900'
}`}
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="Password"
aria-label="Password"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
value={password}
minLength={8}
required
onChange={(event) => {
setPassword(event.target.value);
}}
/>
<p
className={`text-red-500 text-xs ${
!passwordError ? 'invisible' : ''
}`}
>
{passwordError} &nbsp;
</p>
</div>
<div className="mb-3">
<input
className={`mb-1 appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
passwordConfirmError ? ' border-red-500' : 'border-gray-900'
}`}
id="passwordConfirm"
name="passwordConfirm"
type="password"
autoComplete="current-password"
placeholder="Re-enter password"
aria-label="Re-enter password"
value={passwordConfirm}
required
minLength={8}
onChange={(event) => {
setPasswordConfirm(event.target.value);
}}
/>
<p
className={`text-red-500 text-xs ${
!passwordConfirmError ? 'invisible' : ''
}`}
>
{passwordConfirmError} &nbsp;
</p>
</div>
<div className="flex items-center justify-between">
<button
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
);
}
export async function callPasswordResetApi({
id,
resetToken,
password,
}: {
id: string;
resetToken: string;
password: string;
}) {
try {
const res = await fetch(`/account/reset`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({id, resetToken, password}),
});
if (res.ok) {
return {};
} else {
return res.json();
}
} catch (error: any) {
return {
error: error.toString(),
};
}
}

View File

@@ -0,0 +1,134 @@
import {useState} from 'react';
import {emailValidation} from '~/lib/utils';
interface FormElements {
email: HTMLInputElement;
}
export function AccountRecoverForm() {
const [submitSuccess, setSubmitSuccess] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState<string | null>(null);
async function onSubmit(
event: React.FormEvent<HTMLFormElement & FormElements>,
) {
event.preventDefault();
setEmailError(null);
setSubmitError(null);
const newEmailError = emailValidation(event.currentTarget.email);
if (newEmailError) {
setEmailError(newEmailError);
return;
}
await callAccountRecoverApi({
email,
});
setEmail('');
setSubmitSuccess(true);
}
return (
<div className="flex justify-center my-24 px-4">
<div className="max-w-md w-full">
{submitSuccess ? (
<>
<h1 className="text-4xl">Request Sent.</h1>
<p className="mt-4">
If that email address is in our system, you will receive an email
with instructions about how to reset your password in a few
minutes.
</p>
</>
) : (
<>
<h1 className="text-4xl">Forgot Password.</h1>
<p className="mt-4">
Enter the email address associated with your account to receive a
link to reset your password.
</p>
</>
)}
<form noValidate className="pt-6 pb-8 mt-4 mb-4" onSubmit={onSubmit}>
{submitError && (
<div className="flex items-center justify-center mb-6 bg-zinc-500">
<p className="m-4 text-s text-contrast">{submitError}</p>
</div>
)}
<div className="mb-3">
<input
className={`mb-1 rounded appearance-none border w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline ${
emailError ? ' border-red-500' : 'border-gray-900'
}`}
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="Email address"
aria-label="Email address"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
{!emailError ? (
''
) : (
<p className={`text-red-500 text-xs`}>{emailError} &nbsp;</p>
)}
</div>
<div className="flex items-center justify-between">
<button
className="bg-gray-900 text-contrast rounded py-2 px-4 focus:shadow-outline block w-full"
type="submit"
>
Request Reset Link
</button>
</div>
</form>
</div>
</div>
);
}
export async function callAccountRecoverApi({
email,
password,
firstName,
lastName,
}: {
email: string;
password?: string;
firstName?: string;
lastName?: string;
}) {
try {
const res = await fetch(`/account/recover`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({email, password, firstName, lastName}),
});
if (res.status === 200) {
return {};
} else {
return res.json();
}
} catch (error: any) {
return {
error: error.toString(),
};
}
}

View File

@@ -0,0 +1,11 @@
export {AccountActivateForm} from './AccountActivateForm.client';
export {AccountAddressBook} from './AccountAddressBook.client';
export {AccountAddressEdit} from './AccountAddressEdit.client';
export {AccountCreateForm} from './AccountCreateForm.client';
export {AccountDeleteAddress} from './AccountDeleteAddress.client';
export {AccountDetails} from './AccountDetails.client';
export {AccountDetailsEdit} from './AccountDetailsEdit.client';
export {AccountLoginForm} from './AccountLoginForm.client';
export {AccountOrderHistory} from './AccountOrderHistory.client';
export {AccountPasswordResetForm} from './AccountPasswordResetForm.client';
export {AccountRecoverForm} from './AccountRecoverForm.client';

View File

@@ -0,0 +1,38 @@
import {Image, Link} from '@shopify/hydrogen';
import type {Article} from '@shopify/hydrogen/storefront-api-types';
export function ArticleCard({
blogHandle,
article,
loading,
}: {
blogHandle: string;
article: Article;
loading?: HTMLImageElement['loading'];
}) {
return (
<li key={article.id}>
<Link to={`/${blogHandle}/${article.handle}`}>
{article.image && (
<div className="card-image aspect-[3/2]">
<Image
alt={article.image.altText || article.title}
className="object-cover w-full"
data={article.image}
height={400}
loading={loading}
sizes="(min-width: 768px) 50vw, 100vw"
width={600}
loaderOptions={{
scale: 2,
crop: 'center',
}}
/>
</div>
)}
<h2 className="mt-4 font-medium">{article.title}</h2>
<span className="block mt-1">{article.publishedAt}</span>
</Link>
</li>
);
}

View File

@@ -0,0 +1,36 @@
import {Image, Link} from '@shopify/hydrogen';
import type {Collection} from '@shopify/hydrogen/storefront-api-types';
import {Heading} from '~/components';
export function CollectionCard({
collection,
loading,
}: {
collection: Collection;
loading?: HTMLImageElement['loading'];
}) {
return (
<Link to={`/collections/${collection.handle}`} className="grid gap-4">
<div className="card-image bg-primary/5 aspect-[3/2]">
{collection?.image && (
<Image
alt={`Image of ${collection.title}`}
data={collection.image}
height={400}
sizes="(max-width: 32em) 100vw, 33vw"
width={600}
widths={[400, 500, 600, 700, 800, 900]}
loaderOptions={{
scale: 2,
crop: 'center',
}}
/>
)}
</div>
<Heading as="h3" size="copy">
{collection.title}
</Heading>
</Link>
);
}

View File

@@ -0,0 +1,87 @@
import {Image, Link, flattenConnection} from '@shopify/hydrogen';
import type {
Order,
OrderLineItem,
} from '@shopify/hydrogen/storefront-api-types';
import {Heading, Text} from '~/components';
import {statusMessage} from '~/lib/utils';
export function OrderCard({order}: {order: Order}) {
if (!order?.id) return null;
const legacyOrderId = order!.id!.split('/').pop()!.split('?')[0];
const lineItems = flattenConnection<OrderLineItem>(order?.lineItems);
return (
<li className="grid text-center border rounded">
<Link
className="grid items-center gap-4 p-4 md:gap-6 md:p-6 md:grid-cols-2"
to={`/account/orders/${legacyOrderId}`}
>
{lineItems[0].variant?.image && (
<div className="card-image aspect-square bg-primary/5">
<Image
width={168}
height={168}
widths={[168]}
className="w-full fadeIn cover"
alt={lineItems[0].variant?.image?.altText ?? 'Order image'}
// @ts-expect-error Stock line item variant image type has `url` as optional
data={lineItems[0].variant?.image}
loaderOptions={{scale: 2, crop: 'center'}}
/>
</div>
)}
<div
className={`flex-col justify-center text-left ${
!lineItems[0].variant?.image && 'md:col-span-2'
}`}
>
<Heading as="h3" format size="copy">
{lineItems.length > 1
? `${lineItems[0].title} +${lineItems.length - 1} more`
: lineItems[0].title}
</Heading>
<dl className="grid grid-gap-1">
<dt className="sr-only">Order ID</dt>
<dd>
<Text size="fine" color="subtle">
Order No. {order.orderNumber}
</Text>
</dd>
<dt className="sr-only">Order Date</dt>
<dd>
<Text size="fine" color="subtle">
{new Date(order.processedAt).toDateString()}
</Text>
</dd>
<dt className="sr-only">Fulfillment Status</dt>
<dd className="mt-2">
<span
className={`px-3 py-1 text-xs font-medium rounded-full ${
order.fulfillmentStatus === 'FULFILLED'
? 'bg-green-100 text-green-800'
: 'bg-primary/5 text-primary/50'
}`}
>
<Text size="fine">
{statusMessage(order.fulfillmentStatus)}
</Text>
</span>
</dd>
</dl>
</div>
</Link>
<div className="self-end border-t">
<Link
className="block w-full p-2 text-center"
to={`/account/orders/${legacyOrderId}`}
>
<Text color="subtle" className="ml-3">
View Details
</Text>
</Link>
</div>
</li>
);
}

View File

@@ -0,0 +1,126 @@
import clsx from 'clsx';
import {
flattenConnection,
Image,
Link,
Money,
useMoney,
} from '@shopify/hydrogen';
import {Text} from '~/components';
import {isDiscounted, isNewArrival} from '~/lib/utils';
import {getProductPlaceholder} from '~/lib/placeholders';
import type {
MoneyV2,
Product,
ProductVariant,
ProductVariantConnection,
} from '@shopify/hydrogen/storefront-api-types';
export function ProductCard({
product,
label,
className,
loading,
onClick,
}: {
product: Product;
label?: string;
className?: string;
loading?: HTMLImageElement['loading'];
onClick?: () => void;
}) {
let cardLabel;
const cardData = product?.variants ? product : getProductPlaceholder();
const {
image,
priceV2: price,
compareAtPriceV2: compareAtPrice,
} = flattenConnection<ProductVariant>(
cardData?.variants as ProductVariantConnection,
)[0] || {};
if (label) {
cardLabel = label;
} else if (isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2)) {
cardLabel = 'Sale';
} else if (isNewArrival(product.publishedAt)) {
cardLabel = 'New';
}
const styles = clsx('grid gap-6', className);
return (
<Link onClick={onClick} to={`/products/${product.handle}`}>
<div className={styles}>
<div className="card-image aspect-[4/5] bg-primary/5">
<Text
as="label"
size="fine"
className="absolute top-0 right-0 m-4 text-right text-notice"
>
{cardLabel}
</Text>
{image && (
<Image
className="aspect-[4/5] w-full object-cover fadeIn"
widths={[320]}
sizes="320px"
loaderOptions={{
crop: 'center',
scale: 2,
width: 320,
height: 400,
}}
// @ts-ignore Stock type has `src` as optional
data={image}
alt={image.altText || `Picture of ${product.title}`}
loading={loading}
/>
)}
</div>
<div className="grid gap-1">
<Text
className="w-full overflow-hidden whitespace-nowrap text-ellipsis "
as="h3"
>
{product.title}
</Text>
<div className="flex gap-4">
<Text className="flex gap-4">
<Money withoutTrailingZeros data={price!} />
{isDiscounted(price as MoneyV2, compareAtPrice as MoneyV2) && (
<CompareAtPrice
className={'opacity-50'}
data={compareAtPrice as MoneyV2}
/>
)}
</Text>
</div>
</div>
</div>
</Link>
);
}
function CompareAtPrice({
data,
className,
}: {
data: MoneyV2;
className?: string;
}) {
const {currencyNarrowSymbol, withoutTrailingZerosAndCurrency} =
useMoney(data);
const styles = clsx('strike', className);
return (
<span className={styles}>
{currencyNarrowSymbol}
{withoutTrailingZerosAndCurrency}
</span>
);
}

View File

@@ -0,0 +1 @@
export {CollectionCard} from './CollectionCard.server';

View File

@@ -0,0 +1,3 @@
export {ArticleCard} from './ArticleCard';
export {OrderCard} from './OrderCard.client';
export {ProductCard} from './ProductCard.client';

View File

@@ -0,0 +1,100 @@
import {useRef} from 'react';
import {useScroll} from 'react-use';
import {
useCart,
CartLineProvider,
CartShopPayButton,
Money,
} from '@shopify/hydrogen';
import {Button, Text, CartLineItem, CartEmpty} from '~/components';
export function CartDetails({
layout,
onClose,
}: {
layout: 'drawer' | 'page';
onClose?: () => void;
}) {
const {lines} = useCart();
const scrollRef = useRef(null);
const {y} = useScroll(scrollRef);
if (lines.length === 0) {
return <CartEmpty onClose={onClose} layout={layout} />;
}
const container = {
drawer: 'grid grid-cols-1 h-screen-no-nav grid-rows-[1fr_auto]',
page: 'pb-12 grid md:grid-cols-2 md:items-start gap-8 md:gap-8 lg:gap-12',
};
const content = {
drawer: 'px-6 pb-6 sm-max:pt-2 overflow-auto transition md:px-12',
page: 'flex-grow md:translate-y-4',
};
const summary = {
drawer: 'grid gap-6 p-6 border-t md:px-12',
page: 'sticky top-nav grid gap-6 p-4 md:px-6 md:translate-y-4 bg-primary/5 rounded w-full',
};
return (
<form className={container[layout]}>
<section
ref={scrollRef}
aria-labelledby="cart-contents"
className={`${content[layout]} ${y > 0 ? 'border-t' : ''}`}
>
<ul className="grid gap-6 md:gap-10">
{lines.map((line) => {
return (
<CartLineProvider key={line.id} line={line}>
<CartLineItem />
</CartLineProvider>
);
})}
</ul>
</section>
<section aria-labelledby="summary-heading" className={summary[layout]}>
<h2 id="summary-heading" className="sr-only">
Order summary
</h2>
<OrderSummary />
<CartCheckoutActions />
</section>
</form>
);
}
function CartCheckoutActions() {
const {checkoutUrl} = useCart();
return (
<>
<div className="grid gap-4">
<Button to={checkoutUrl}>Continue to Checkout</Button>
<CartShopPayButton />
</div>
</>
);
}
function OrderSummary() {
const {cost} = useCart();
return (
<>
<dl className="grid">
<div className="flex items-center justify-between font-medium">
<Text as="dt">Subtotal</Text>
<Text as="dd">
{cost?.subtotalAmount?.amount ? (
<Money data={cost?.subtotalAmount} />
) : (
'-'
)}
</Text>
</div>
</dl>
</>
);
}

View File

@@ -0,0 +1,85 @@
import {useRef} from 'react';
import {useScroll} from 'react-use';
import {fetchSync} from '@shopify/hydrogen';
import {Button, Text, ProductCard, Heading, Skeleton} from '~/components';
import type {Product} from '@shopify/hydrogen/storefront-api-types';
import {Suspense} from 'react';
export function CartEmpty({
onClose,
layout = 'drawer',
}: {
onClose?: () => void;
layout?: 'page' | 'drawer';
}) {
const scrollRef = useRef(null);
const {y} = useScroll(scrollRef);
const container = {
drawer: `grid content-start gap-4 px-6 pb-8 transition overflow-y-scroll md:gap-12 md:px-12 h-screen-no-nav md:pb-12 ${
y > 0 ? 'border-t' : ''
}`,
page: `grid pb-12 w-full md:items-start gap-4 md:gap-8 lg:gap-12`,
};
const topProductsContainer = {
drawer: '',
page: 'md:grid-cols-4 sm:grid-col-4',
};
return (
<div ref={scrollRef} className={container[layout]}>
<section className="grid gap-6">
<Text format>
Looks like you haven&rsquo;t added anything yet, let&rsquo;s get you
started!
</Text>
<div>
<Button onClick={onClose}>Continue shopping</Button>
</div>
</section>
<section className="grid gap-8 pt-4">
<Heading format size="copy">
Shop Best Sellers
</Heading>
<div
className={`grid grid-cols-2 gap-x-6 gap-y-8 ${topProductsContainer[layout]}`}
>
<Suspense fallback={<Loading />}>
<TopProducts onClose={onClose} />
</Suspense>
</div>
</section>
</div>
);
}
function TopProducts({onClose}: {onClose?: () => void}) {
const products: Product[] = fetchSync('/api/bestSellers').json();
if (products.length === 0) {
return <Text format>No products found.</Text>;
}
return (
<>
{products.map((product) => (
<ProductCard product={product} key={product.id} onClick={onClose} />
))}
</>
);
}
function Loading() {
return (
<>
{[...new Array(4)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} className="grid gap-2">
<Skeleton className="aspect-[3/4]" />
<Skeleton className="w-32 h-4" />
</div>
))}
</>
);
}

View File

@@ -0,0 +1,103 @@
import {
useCart,
useCartLine,
CartLineQuantityAdjustButton,
CartLinePrice,
CartLineQuantity,
Image,
Link,
} from '@shopify/hydrogen';
import type {Image as ImageType} from '@shopify/hydrogen/storefront-api-types';
import {Heading, IconRemove, Text} from '~/components';
export function CartLineItem() {
const {linesRemove} = useCart();
const {id: lineId, quantity, merchandise} = useCartLine();
return (
<li key={lineId} className="flex gap-4">
<div className="flex-shrink">
<Image
width={112}
height={112}
widths={[112]}
data={merchandise.image as ImageType}
loaderOptions={{
scale: 2,
crop: 'center',
}}
className="object-cover object-center w-24 h-24 border rounded md:w-28 md:h-28"
/>
</div>
<div className="flex justify-between flex-grow">
<div className="grid gap-2">
<Heading as="h3" size="copy">
<Link to={`/products/${merchandise.product.handle}`}>
{merchandise.product.title}
</Link>
</Heading>
<div className="grid pb-2">
{(merchandise?.selectedOptions || []).map((option) => (
<Text color="subtle" key={option.name}>
{option.name}: {option.value}
</Text>
))}
</div>
<div className="flex items-center gap-2">
<div className="flex justify-start text-copy">
<CartLineQuantityAdjust lineId={lineId} quantity={quantity} />
</div>
<button
type="button"
onClick={() => linesRemove([lineId])}
className="flex items-center justify-center w-10 h-10 border rounded"
>
<span className="sr-only">Remove</span>
<IconRemove aria-hidden="true" />
</button>
</div>
</div>
<Text>
<CartLinePrice as="span" />
</Text>
</div>
</li>
);
}
function CartLineQuantityAdjust({
lineId,
quantity,
}: {
lineId: string;
quantity: number;
}) {
return (
<>
<label htmlFor={`quantity-${lineId}`} className="sr-only">
Quantity, {quantity}
</label>
<div className="flex items-center border rounded">
<CartLineQuantityAdjustButton
adjust="decrease"
aria-label="Decrease quantity"
className="w-10 h-10 transition text-primary/50 hover:text-primary disabled:cursor-wait"
>
&#8722;
</CartLineQuantityAdjustButton>
<CartLineQuantity as="div" className="px-2 text-center" />
<CartLineQuantityAdjustButton
adjust="increase"
aria-label="Increase quantity"
className="w-10 h-10 transition text-primary/50 hover:text-primary disabled:cursor-wait"
>
&#43;
</CartLineQuantityAdjustButton>
</div>
</>
);
}

View File

@@ -0,0 +1,3 @@
export {CartDetails} from './CartDetails.client';
export {CartEmpty} from './CartEmpty.client';
export {CartLineItem} from './CartLineItem.client';

View File

@@ -0,0 +1,42 @@
import clsx from 'clsx';
import {Link} from '@shopify/hydrogen';
import {missingClass} from '~/lib/utils';
export function Button({
as = 'button',
className = '',
variant = 'primary',
width = 'auto',
...props
}: {
as?: React.ElementType;
className?: string;
variant?: 'primary' | 'secondary' | 'inline';
width?: 'auto' | 'full';
[key: string]: any;
}) {
const Component = props?.to ? Link : as;
const baseButtonClasses =
'inline-block rounded font-medium text-center py-3 px-6';
const variants = {
primary: `${baseButtonClasses} bg-primary text-contrast`,
secondary: `${baseButtonClasses} border border-primary/10 bg-contrast text-primary`,
inline: 'border-b border-primary/10 leading-none pb-1',
};
const widths = {
auto: 'w-auto',
full: 'w-full',
};
const styles = clsx(
missingClass(className, 'bg-') && variants[variant],
missingClass(className, 'w-') && widths[width],
className,
);
return <Component className={styles} {...props} />;
}

View File

@@ -0,0 +1,44 @@
import clsx from 'clsx';
export function Grid({
as: Component = 'div',
className,
flow = 'row',
gap = 'default',
items = 4,
layout = 'default',
...props
}: {
as?: React.ElementType;
className?: string;
flow?: 'row' | 'col';
gap?: 'default' | 'blog';
items?: number;
layout?: 'default' | 'products' | 'auto' | 'blog';
[key: string]: any;
}) {
const layouts = {
default: `grid-cols-1 ${items === 2 && 'md:grid-cols-2'} ${
items === 3 && 'sm:grid-cols-3'
} ${items > 3 && 'md:grid-cols-3'} ${items >= 4 && 'lg:grid-cols-4'}`,
products: `grid-cols-2 ${items >= 3 && 'md:grid-cols-3'} ${
items >= 4 && 'lg:grid-cols-4'
}`,
auto: 'auto-cols-auto',
blog: `grid-cols-2 pt-24`,
};
const gaps = {
default: 'grid gap-2 gap-y-6 md:gap-4 lg:gap-6',
blog: 'grid gap-6',
};
const flows = {
row: 'grid-flow-row',
col: 'grid-flow-col',
};
const styles = clsx(flows[flow], gaps[gap], layouts[layout], className);
return <Component {...props} className={styles} />;
}

View File

@@ -0,0 +1,45 @@
import clsx from 'clsx';
import {missingClass, formatText} from '~/lib/utils';
export function Heading({
as: Component = 'h2',
children,
className = '',
format,
size = 'heading',
width = 'default',
...props
}: {
as?: React.ElementType;
children: React.ReactNode;
format?: boolean;
size?: 'display' | 'heading' | 'lead' | 'copy';
width?: 'default' | 'narrow' | 'wide';
} & React.HTMLAttributes<HTMLHeadingElement>) {
const sizes = {
display: 'font-bold text-display',
heading: 'font-bold text-heading',
lead: 'font-bold text-lead',
copy: 'font-medium text-copy',
};
const widths = {
default: 'max-w-prose',
narrow: 'max-w-prose-narrow',
wide: 'max-w-prose-wide',
};
const styles = clsx(
missingClass(className, 'whitespace-') && 'whitespace-pre-wrap',
missingClass(className, 'max-w-') && widths[width],
missingClass(className, 'font-') && sizes[size],
className,
);
return (
<Component {...props} className={styles}>
{format ? formatText(children) : children}
</Component>
);
}

View File

@@ -0,0 +1,236 @@
import clsx from 'clsx';
type IconProps = JSX.IntrinsicElements['svg'] & {
direction?: 'up' | 'right' | 'down' | 'left';
};
function Icon({
children,
className,
fill = 'currentColor',
stroke,
...props
}: IconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
{...props}
fill={fill}
stroke={stroke}
className={clsx('w-5 h-5', className)}
>
{children}
</svg>
);
}
export function AccountIcon(props: IconProps) {
return (
<Icon {...props}>
<title>Accounts</title>
<circle cx="20" cy="10.5" r="4.5" strokeWidth="2" />
<path
d="M20 19C13.4375 19 9.5 20.2857 9.5 28H30.5C30.5 20.2857 26.5625 19 20 19Z"
strokeWidth="2"
/>
</Icon>
);
}
export function IconMenu(props: IconProps) {
return (
<Icon {...props} stroke={props.stroke || 'currentColor'}>
<title>Menu</title>
<line x1="3" y1="6.375" x2="17" y2="6.375" strokeWidth="1.25" />
<line x1="3" y1="10.375" x2="17" y2="10.375" strokeWidth="1.25" />
<line x1="3" y1="14.375" x2="17" y2="14.375" strokeWidth="1.25" />
</Icon>
);
}
export function IconClose(props: IconProps) {
return (
<Icon {...props} stroke={props.stroke || 'currentColor'}>
<title>Close</title>
<line
x1="4.44194"
y1="4.30806"
x2="15.7556"
y2="15.6218"
strokeWidth="1.25"
/>
<line
y1="-0.625"
x2="16"
y2="-0.625"
transform="matrix(-0.707107 0.707107 0.707107 0.707107 16 4.75)"
strokeWidth="1.25"
/>
</Icon>
);
}
export function IconArrow({direction = 'right'}: IconProps) {
let rotate;
switch (direction) {
case 'right':
rotate = 'rotate-0';
break;
case 'left':
rotate = 'rotate-180';
break;
case 'up':
rotate = '-rotate-90';
break;
case 'down':
rotate = 'rotate-90';
break;
default:
rotate = 'rotate-0';
}
return (
<Icon className={`w-5 h-5 ${rotate}`}>
<title>Arrow</title>
<path d="M7 3L14 10L7 17" strokeWidth="1.25" />
</Icon>
);
}
export function IconCaret({
direction = 'down',
stroke = 'currentColor',
...props
}: IconProps) {
let rotate;
switch (direction) {
case 'down':
rotate = 'rotate-0';
break;
case 'up':
rotate = 'rotate-180';
break;
case 'left':
rotate = '-rotate-90';
break;
case 'right':
rotate = 'rotate-90';
break;
default:
rotate = 'rotate-0';
}
return (
<Icon
{...props}
className={`w-5 h-5 transition ${rotate}`}
fill="transparent"
stroke={stroke}
>
<title>Caret</title>
<path d="M14 8L10 12L6 8" strokeWidth="1.25" />
</Icon>
);
}
export function IconSelect(props: IconProps) {
return (
<Icon {...props}>
<title>Select</title>
<path d="M7 8.5L10 6.5L13 8.5" strokeWidth="1.25" />
<path d="M13 11.5L10 13.5L7 11.5" strokeWidth="1.25" />
</Icon>
);
}
export function IconBag(props: IconProps) {
return (
<Icon {...props}>
<title>Bag</title>
<path
fillRule="evenodd"
d="M8.125 5a1.875 1.875 0 0 1 3.75 0v.375h-3.75V5Zm-1.25.375V5a3.125 3.125 0 1 1 6.25 0v.375h3.5V15A2.625 2.625 0 0 1 14 17.625H6A2.625 2.625 0 0 1 3.375 15V5.375h3.5ZM4.625 15V6.625h10.75V15c0 .76-.616 1.375-1.375 1.375H6c-.76 0-1.375-.616-1.375-1.375Z"
/>
</Icon>
);
}
export function IconAccount(props: IconProps) {
return (
<Icon {...props}>
<title>Account</title>
<path
fillRule="evenodd"
d="M9.9998 12.625c-1.9141 0-3.6628.698-5.0435 1.8611C3.895 13.2935 3.25 11.7221 3.25 10c0-3.728 3.022-6.75 6.75-6.75 3.7279 0 6.75 3.022 6.75 6.75 0 1.7222-.645 3.2937-1.7065 4.4863-1.3807-1.1632-3.1295-1.8613-5.0437-1.8613ZM10 18c-2.3556 0-4.4734-1.0181-5.9374-2.6382C2.7806 13.9431 2 12.0627 2 10c0-4.4183 3.5817-8 8-8s8 3.5817 8 8-3.5817 8-8 8Zm0-12.5c-1.567 0-2.75 1.394-2.75 3s1.183 3 2.75 3 2.75-1.394 2.75-3-1.183-3-2.75-3Z"
/>
</Icon>
);
}
export function IconHelp(props: IconProps) {
return (
<Icon {...props}>
<title>Help</title>
<path d="M3.375 10a6.625 6.625 0 1 1 13.25 0 6.625 6.625 0 0 1-13.25 0ZM10 2.125a7.875 7.875 0 1 0 0 15.75 7.875 7.875 0 0 0 0-15.75Zm.699 10.507H9.236V14h1.463v-1.368ZM7.675 7.576A3.256 3.256 0 0 0 7.5 8.67h1.245c0-.496.105-.89.316-1.182.218-.299.553-.448 1.005-.448a1 1 0 0 1 .327.065c.124.044.24.113.35.208.108.095.2.223.272.383.08.154.12.34.12.558a1.3 1.3 0 0 1-.076.471c-.044.131-.11.252-.197.361-.08.102-.174.197-.283.285-.102.087-.212.182-.328.284a3.157 3.157 0 0 0-.382.383c-.102.124-.19.27-.262.438a2.476 2.476 0 0 0-.164.591 6.333 6.333 0 0 0-.043.81h1.179c0-.263.021-.485.065-.668a1.65 1.65 0 0 1 .207-.47c.088-.139.19-.263.306-.372.117-.11.244-.223.382-.34l.35-.306c.116-.11.218-.23.305-.361.095-.139.168-.3.219-.482.058-.19.087-.412.087-.667 0-.35-.062-.664-.186-.942a1.881 1.881 0 0 0-.513-.689 2.07 2.07 0 0 0-.753-.427A2.721 2.721 0 0 0 10.12 6c-.4 0-.764.066-1.092.197a2.36 2.36 0 0 0-.83.536c-.225.234-.4.515-.523.843Z" />
</Icon>
);
}
export function IconSearch(props: IconProps) {
return (
<Icon {...props}>
<title>Search</title>
<path
fillRule="evenodd"
d="M13.3 8.52a4.77 4.77 0 1 1-9.55 0 4.77 4.77 0 0 1 9.55 0Zm-.98 4.68a6.02 6.02 0 1 1 .88-.88l4.3 4.3-.89.88-4.3-4.3Z"
/>
</Icon>
);
}
export function IconCheck({
stroke = 'currentColor',
...props
}: React.ComponentProps<typeof Icon>) {
return (
<Icon {...props} fill="transparent" stroke={stroke}>
<title>Check</title>
<circle cx="10" cy="10" r="7.25" strokeWidth="1.25" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="m7.04 10.37 2.42 2.41 3.5-5.56"
/>
</Icon>
);
}
export function IconRemove(props: IconProps) {
return (
<Icon {...props} fill="transparent" stroke={props.stroke || 'currentColor'}>
<title>Remove</title>
<path
d="M4 6H16"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M8.5 9V14" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.5 9V14" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M5.5 6L6 17H14L14.5 6"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 6L8 5C8 4 8.75 3 10 3C11.25 3 12 4 12 5V6"
strokeWidth="1.25"
/>
</Icon>
);
}

View File

@@ -0,0 +1,24 @@
import clsx from 'clsx';
export function Input({
className = '',
type,
variant,
...props
}: {
className?: string;
type?: string;
variant: 'search' | 'minisearch';
[key: string]: any;
}) {
const variants = {
search:
'bg-transparent px-0 py-2 text-heading w-full focus:ring-0 border-x-0 border-t-0 transition border-b-2 border-primary/10 focus:border-primary/90',
minisearch:
'bg-transparent hidden md:inline-block text-left lg:text-right border-b transition border-transparent -mb-px border-x-0 border-t-0 appearance-none px-0 py-1 focus:ring-transparent placeholder:opacity-20 placeholder:text-inherit',
};
const styles = clsx(variants[variant], className);
return <input type={type} {...props} className={styles} />;
}

View File

@@ -0,0 +1,20 @@
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick?: () => void;
}
export function LogoutButton(props: ButtonProps) {
const logout = () => {
fetch('/account/logout', {method: 'POST'}).then(() => {
if (typeof props?.onClick === 'function') {
props.onClick();
}
window.location.href = '/';
});
};
return (
<button className="text-primary/50" {...props} onClick={logout}>
Logout
</button>
);
}

View File

@@ -0,0 +1,62 @@
import clsx from 'clsx';
import {Heading} from '~/components';
import {missingClass} from '~/lib/utils';
export function Section({
as: Component = 'section',
children,
className,
divider = 'none',
display = 'grid',
heading,
padding = 'all',
...props
}: {
as?: React.ElementType;
children?: React.ReactNode;
className?: string;
divider?: 'none' | 'top' | 'bottom' | 'both';
display?: 'grid' | 'flex';
heading?: string;
padding?: 'x' | 'y' | 'swimlane' | 'all';
[key: string]: any;
}) {
const paddings = {
x: 'px-6 md:px-8 lg:px-12',
y: 'py-6 md:py-8 lg:py-12',
swimlane: 'pt-4 md:pt-8 lg:pt-12 md:pb-4 lg:pb-8',
all: 'p-6 md:p-8 lg:p-12',
};
const dividers = {
none: 'border-none',
top: 'border-t border-primary/05',
bottom: 'border-b border-primary/05',
both: 'border-y border-primary/05',
};
const displays = {
flex: 'flex',
grid: 'grid',
};
const styles = clsx(
'w-full gap-4 md:gap-8',
displays[display],
missingClass(className, '\\mp[xy]?-') && paddings[padding],
dividers[divider],
className,
);
return (
<Component {...props} className={styles}>
{heading && (
<Heading size="lead" className={padding === 'y' ? paddings['x'] : ''}>
{heading}
</Heading>
)}
{children}
</Component>
);
}

View File

@@ -0,0 +1,24 @@
import clsx from 'clsx';
/**
* A shared component and Suspense call that's used in `App.server.jsx` to let your app wait for code to load while declaring a loading state
*/
export function Skeleton({
as: Component = 'div',
width,
height,
className,
...props
}: {
as?: React.ElementType;
width?: string;
height?: string;
className?: string;
[key: string]: any;
}) {
const styles = clsx('rounded bg-primary/10', className);
return (
<Component {...props} width={width} height={height} className={styles} />
);
}

View File

@@ -0,0 +1,57 @@
import clsx from 'clsx';
import {missingClass, formatText} from '~/lib/utils';
export function Text({
as: Component = 'span',
className,
color = 'default',
format,
size = 'copy',
width = 'default',
children,
...props
}: {
as?: React.ElementType;
className?: string;
color?: 'default' | 'primary' | 'subtle' | 'notice' | 'contrast';
format?: boolean;
size?: 'lead' | 'copy' | 'fine';
width?: 'default' | 'narrow' | 'wide';
children: React.ReactNode;
[key: string]: any;
}) {
const colors: Record<string, string> = {
default: 'inherit',
primary: 'text-primary/90',
subtle: 'text-primary/50',
notice: 'text-notice',
contrast: 'text-contrast/90',
};
const sizes: Record<string, string> = {
lead: 'text-lead font-medium',
copy: 'text-copy',
fine: 'text-fine subpixel-antialiased',
};
const widths: Record<string, string> = {
default: 'max-w-prose',
narrow: 'max-w-prose-narrow',
wide: 'max-w-prose-wide',
};
const styles = clsx(
missingClass(className, 'max-w-') && widths[width],
missingClass(className, 'whitespace-') && 'whitespace-pre-wrap',
missingClass(className, 'text-') && colors[color],
sizes[size],
className,
);
return (
<Component {...props} className={styles}>
{format ? formatText(children) : children}
</Component>
);
}

View File

@@ -0,0 +1,9 @@
export * from './Icon';
export {Button} from './Button';
export {Grid} from './Grid';
export {Heading} from './Heading';
export {Input} from './Input';
export {LogoutButton} from './LogoutButton.client';
export {Section} from './Section';
export {Skeleton} from './Skeleton';
export {Text} from './Text';

View File

@@ -0,0 +1,18 @@
import {CartDetails} from '~/components/cart';
import {Drawer} from './Drawer.client';
export function CartDrawer({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) {
return (
<Drawer open={isOpen} onClose={onClose} heading="Cart" openFrom="right">
<div className="grid">
<CartDetails layout="drawer" onClose={onClose} />
</div>
</Drawer>
);
}

View File

@@ -0,0 +1,117 @@
import {Fragment, useState} from 'react';
// @ts-expect-error @headlessui/react incompatibility with node16 resolution
import {Dialog, Transition} from '@headlessui/react';
import {Heading, IconClose} from '~/components';
/**
* Drawer component that opens on user click.
* @param heading - string. Shown at the top of the drawer.
* @param open - boolean state. if true opens the drawer.
* @param onClose - function should set the open state.
* @param openFrom - right, left
* @param children - react children node.
*/
function Drawer({
heading,
open,
onClose,
openFrom = 'right',
children,
}: {
heading?: string;
open: boolean;
onClose: () => void;
openFrom: 'right' | 'left';
children: React.ReactNode;
}) {
const offScreen = {
right: 'translate-x-full',
left: '-translate-x-full',
};
return (
<Transition appear show={open} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 left-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0">
<div className="absolute inset-0 overflow-hidden">
<div
className={`fixed inset-y-0 flex max-w-full ${
openFrom === 'right' ? 'right-0' : ''
}`}
>
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-300"
enterFrom={offScreen[openFrom]}
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-300"
leaveFrom="translate-x-0"
leaveTo={offScreen[openFrom]}
>
<Dialog.Panel className="w-screen h-screen max-w-lg text-left align-middle transition-all transform shadow-xl bg-contrast">
<header
className={`sticky top-0 flex items-center px-6 h-nav sm:px-8 md:px-12 ${
heading ? 'justify-between' : 'justify-end'
}`}
>
{heading !== null && (
<Dialog.Title>
<Heading as="span" size="lead" id="cart-contents">
{heading}
</Heading>
</Dialog.Title>
)}
<button
type="button"
className="p-4 -m-4 transition text-primary hover:text-primary/50"
onClick={onClose}
>
<IconClose aria-label="Close panel" />
</button>
</header>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</div>
</Dialog>
</Transition>
);
}
/* Use for associating arialabelledby with the title*/
Drawer.Title = Dialog.Title;
export {Drawer};
export function useDrawer(openDefault = false) {
const [isOpen, setIsOpen] = useState(openDefault);
function openDrawer() {
setIsOpen(true);
}
function closeDrawer() {
setIsOpen(false);
}
return {
isOpen,
openDrawer,
closeDrawer,
};
}

View File

@@ -0,0 +1,46 @@
import {useUrl} from '@shopify/hydrogen';
import {Section, Heading, FooterMenu, CountrySelector} from '~/components';
import type {EnhancedMenu} from '~/lib/utils';
/**
* A server component that specifies the content of the footer on the website
*/
export function Footer({menu}: {menu?: EnhancedMenu}) {
const {pathname} = useUrl();
const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname);
const countryCode = localeMatch ? localeMatch[1] : null;
const isHome = pathname === `/${countryCode ? countryCode + '/' : ''}`;
const itemsCount = menu
? menu?.items?.length + 1 > 4
? 4
: menu?.items?.length + 1
: [];
return (
<Section
divider={isHome ? 'none' : 'top'}
as="footer"
role="contentinfo"
className={`grid min-h-[25rem] items-start grid-flow-row w-full gap-6 py-8 px-6 md:px-8 lg:px-12
border-b md:gap-8 lg:gap-12 grid-cols-1 md:grid-cols-2 lg:grid-cols-${itemsCount}
bg-primary dark:bg-contrast dark:text-primary text-contrast overflow-hidden`}
>
<FooterMenu menu={menu} />
<section className="grid gap-4 w-full md:max-w-[335px] md:ml-auto">
<Heading size="lead" className="cursor-default" as="h3">
Country
</Heading>
<CountrySelector />
</section>
<div
className={`self-end pt-8 opacity-50 md:col-span-2 lg:col-span-${itemsCount}`}
>
&copy; {new Date().getFullYear()} / Shopify, Inc. Hydrogen is an MIT
Licensed Open Source project. This website is carbon&nbsp;neutral.
</div>
</Section>
);
}

View File

@@ -0,0 +1,63 @@
// @ts-expect-error @headlessui/react incompatibility with node16 resolution
import {Disclosure} from '@headlessui/react';
import {Link} from '@shopify/hydrogen';
import {Heading, IconCaret} from '~/components';
import type {EnhancedMenu, EnhancedMenuItem} from '~/lib/utils';
/**
* A server component that specifies the content of the footer on the website
*/
export function FooterMenu({menu}: {menu?: EnhancedMenu}) {
const styles = {
section: 'grid gap-4',
nav: 'grid gap-2 pb-6',
};
return (
<>
{(menu?.items || []).map((item: EnhancedMenuItem) => (
<section key={item.id} className={styles.section}>
<Disclosure>
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
{({open}) => (
<>
<Disclosure.Button className="text-left md:cursor-default">
<Heading className="flex justify-between" size="lead" as="h3">
{item.title}
{item?.items?.length > 0 && (
<span className="md:hidden">
<IconCaret direction={open ? 'up' : 'down'} />
</span>
)}
</Heading>
</Disclosure.Button>
{item?.items?.length > 0 && (
<div
className={`${
open ? `max-h-48 h-fit` : `max-h-0 md:max-h-fit`
} overflow-hidden transition-all duration-300`}
>
<Disclosure.Panel static>
<nav className={styles.nav}>
{item.items.map((subItem) => (
<Link
key={subItem.id}
to={subItem.to}
target={subItem.target}
>
{subItem.title}
</Link>
))}
</nav>
</Disclosure.Panel>
</div>
)}
</>
)}
</Disclosure>
</section>
))}{' '}
</>
);
}

View File

@@ -0,0 +1,230 @@
import {Link, useUrl, useCart} from '@shopify/hydrogen';
import {useWindowScroll} from 'react-use';
import {
Heading,
IconAccount,
IconBag,
IconMenu,
IconSearch,
Input,
} from '~/components';
import {CartDrawer} from './CartDrawer.client';
import {MenuDrawer} from './MenuDrawer.client';
import {useDrawer} from './Drawer.client';
import type {EnhancedMenu} from '~/lib/utils';
/**
* A client component that specifies the content of the header on the website
*/
export function Header({title, menu}: {title: string; menu?: EnhancedMenu}) {
const {pathname} = useUrl();
const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname);
const countryCode = localeMatch ? localeMatch[1] : undefined;
const isHome = pathname === `/${countryCode ? countryCode + '/' : ''}`;
const {
isOpen: isCartOpen,
openDrawer: openCart,
closeDrawer: closeCart,
} = useDrawer();
const {
isOpen: isMenuOpen,
openDrawer: openMenu,
closeDrawer: closeMenu,
} = useDrawer();
return (
<>
<CartDrawer isOpen={isCartOpen} onClose={closeCart} />
<MenuDrawer isOpen={isMenuOpen} onClose={closeMenu} menu={menu!} />
<DesktopHeader
countryCode={countryCode}
isHome={isHome}
title={title}
menu={menu}
openCart={openCart}
/>
<MobileHeader
countryCode={countryCode}
isHome={isHome}
title={title}
openCart={openCart}
openMenu={openMenu}
/>
</>
);
}
function MobileHeader({
countryCode,
title,
isHome,
openCart,
openMenu,
}: {
countryCode?: string | null;
title: string;
isHome: boolean;
openCart: () => void;
openMenu: () => void;
}) {
const {y} = useWindowScroll();
const styles = {
button: 'relative flex items-center justify-center w-8 h-8',
container: `${
isHome
? 'bg-primary/80 dark:bg-contrast/60 text-contrast dark:text-primary shadow-darkHeader'
: 'bg-contrast/80 text-primary'
} ${
y > 50 && !isHome ? 'shadow-lightHeader ' : ''
}flex lg:hidden items-center h-nav sticky backdrop-blur-lg z-40 top-0 justify-between w-full leading-none gap-4 px-4 md:px-8`,
};
return (
<header role="banner" className={styles.container}>
<div className="flex items-center justify-start w-full gap-4">
<button onClick={openMenu} className={styles.button}>
<IconMenu />
</button>
<form
action={`/${countryCode ? countryCode + '/' : ''}search`}
className="items-center gap-2 sm:flex"
>
<button type="submit" className={styles.button}>
<IconSearch />
</button>
<Input
className={
isHome
? 'focus:border-contrast/20 dark:focus:border-primary/20'
: 'focus:border-primary/20'
}
type="search"
variant="minisearch"
placeholder="Search"
name="q"
/>
</form>
</div>
<Link
className="flex items-center self-stretch leading-[3rem] md:leading-[4rem] justify-center flex-grow w-full h-full"
to="/"
>
<Heading className="font-bold text-center" as={isHome ? 'h1' : 'h2'}>
{title}
</Heading>
</Link>
<div className="flex items-center justify-end w-full gap-4">
<Link to={'/account'} className={styles.button}>
<IconAccount />
</Link>
<button onClick={openCart} className={styles.button}>
<IconBag />
<CartBadge dark={isHome} />
</button>
</div>
</header>
);
}
function DesktopHeader({
countryCode,
isHome,
menu,
openCart,
title,
}: {
countryCode?: string | null;
isHome: boolean;
openCart: () => void;
menu?: EnhancedMenu;
title: string;
}) {
const {y} = useWindowScroll();
const styles = {
button:
'relative flex items-center justify-center w-8 h-8 focus:ring-primary/5',
container: `${
isHome
? 'bg-primary/80 dark:bg-contrast/60 text-contrast dark:text-primary shadow-darkHeader'
: 'bg-contrast/80 text-primary'
} ${
y > 50 && !isHome ? 'shadow-lightHeader ' : ''
}hidden h-nav lg:flex items-center sticky transition duration-300 backdrop-blur-lg z-40 top-0 justify-between w-full leading-none gap-8 px-12 py-8`,
};
return (
<header role="banner" className={styles.container}>
<div className="flex gap-12">
<Link className={`font-bold`} to="/">
{title}
</Link>
<nav className="flex gap-8">
{/* Top level menu items */}
{(menu?.items || []).map((item) => (
<Link key={item.id} to={item.to} target={item.target}>
{item.title}
</Link>
))}
</nav>
</div>
<div className="flex items-center gap-1">
<form
action={`/${countryCode ? countryCode + '/' : ''}search`}
className="flex items-center gap-2"
>
<Input
className={
isHome
? 'focus:border-contrast/20 dark:focus:border-primary/20'
: 'focus:border-primary/20'
}
type="search"
variant="minisearch"
placeholder="Search"
name="q"
/>
<button type="submit" className={styles.button}>
<IconSearch />
</button>
</form>
<Link to={'/account'} className={styles.button}>
<IconAccount />
</Link>
<button onClick={openCart} className={styles.button}>
<IconBag />
<CartBadge dark={isHome} />
</button>
</div>
</header>
);
}
function CartBadge({dark}: {dark: boolean}) {
const {totalQuantity} = useCart();
if (totalQuantity < 1) {
return null;
}
return (
<div
className={`${
dark
? 'text-primary bg-contrast dark:text-contrast dark:bg-primary'
: 'text-contrast bg-primary'
} absolute bottom-1 right-1 text-[0.625rem] font-medium subpixel-antialiased h-3 min-w-[0.75rem] flex items-center justify-center leading-none text-center rounded-full w-auto px-[0.125rem] pb-px`}
>
<span>{totalQuantity}</span>
</div>
);
}

View File

@@ -0,0 +1,129 @@
import {Suspense} from 'react';
import {useLocalization, useShopQuery, CacheLong, gql} from '@shopify/hydrogen';
import type {Menu, Shop} from '@shopify/hydrogen/storefront-api-types';
import {Header} from '~/components';
import {Footer} from '~/components/index.server';
import {parseMenu} from '~/lib/utils';
const HEADER_MENU_HANDLE = 'main-menu';
const FOOTER_MENU_HANDLE = 'footer';
const SHOP_NAME_FALLBACK = 'Hydrogen';
/**
* A server component that defines a structure and organization of a page that can be used in different parts of the Hydrogen app
*/
export function Layout({children}: {children: React.ReactNode}) {
return (
<>
<div className="flex flex-col min-h-screen">
<div className="">
<a href="#mainContent" className="sr-only">
Skip to content
</a>
</div>
<Suspense fallback={<Header title={SHOP_NAME_FALLBACK} />}>
<HeaderWithMenu />
</Suspense>
<main role="main" id="mainContent" className="flex-grow">
{children}
</main>
</div>
<Suspense fallback={<Footer />}>
<FooterWithMenu />
</Suspense>
</>
);
}
function HeaderWithMenu() {
const {shopName, headerMenu} = useLayoutQuery();
return <Header title={shopName} menu={headerMenu} />;
}
function FooterWithMenu() {
const {footerMenu} = useLayoutQuery();
return <Footer menu={footerMenu} />;
}
function useLayoutQuery() {
const {
language: {isoCode: languageCode},
} = useLocalization();
const {data} = useShopQuery<{
shop: Shop;
headerMenu: Menu;
footerMenu: Menu;
}>({
query: SHOP_QUERY,
variables: {
language: languageCode,
headerMenuHandle: HEADER_MENU_HANDLE,
footerMenuHandle: FOOTER_MENU_HANDLE,
},
cache: CacheLong(),
preload: '*',
});
const shopName = data ? data.shop.name : SHOP_NAME_FALLBACK;
/*
Modify specific links/routes (optional)
@see: https://shopify.dev/api/storefront/unstable/enums/MenuItemType
e.g here we map:
- /blogs/news -> /news
- /blog/news/blog-post -> /news/blog-post
- /collections/all -> /products
*/
const customPrefixes = {BLOG: '', CATALOG: 'products'};
const headerMenu = data?.headerMenu
? parseMenu(data.headerMenu, customPrefixes)
: undefined;
const footerMenu = data?.footerMenu
? parseMenu(data.footerMenu, customPrefixes)
: undefined;
return {footerMenu, headerMenu, shopName};
}
const SHOP_QUERY = gql`
fragment MenuItem on MenuItem {
id
resourceId
tags
title
type
url
}
query layoutMenus(
$language: LanguageCode
$headerMenuHandle: String!
$footerMenuHandle: String!
) @inContext(language: $language) {
shop {
name
}
headerMenu: menu(handle: $headerMenuHandle) {
id
items {
...MenuItem
items {
...MenuItem
}
}
}
footerMenu: menu(handle: $footerMenuHandle) {
id
items {
...MenuItem
items {
...MenuItem
}
}
}
}
`;

View File

@@ -0,0 +1,43 @@
import {EnhancedMenu} from '~/lib/utils';
import {Text} from '~/components';
import {Drawer} from './Drawer.client';
import {Link} from '@shopify/hydrogen';
export function MenuDrawer({
isOpen,
onClose,
menu,
}: {
isOpen: boolean;
onClose: () => void;
menu: EnhancedMenu;
}) {
return (
<Drawer open={isOpen} onClose={onClose} openFrom="left" heading="Menu">
<div className="grid">
<MenuMobileNav menu={menu} onClose={onClose} />
</div>
</Drawer>
);
}
function MenuMobileNav({
menu,
onClose,
}: {
menu: EnhancedMenu;
onClose: () => void;
}) {
return (
<nav className="grid gap-4 p-6 sm:gap-6 sm:px-12 sm:py-8">
{/* Top level menu items */}
{(menu?.items || []).map((item) => (
<Link key={item.id} to={item.to} target={item.target} onClick={onClose}>
<Text as="span" size="copy">
{item.title}
</Text>
</Link>
))}
</nav>
);
}

View File

@@ -0,0 +1,43 @@
import {IconClose} from '~/components';
export function Modal({
children,
close,
}: {
children: React.ReactNode;
close: () => void;
}) {
return (
<div
className="relative z-50"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
id="modal-bg"
>
<div className="fixed inset-0 transition-opacity bg-opacity-75 bg-primary/40"></div>
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-full p-4 text-center sm:p-0">
<div
className="relative flex-1 px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform rounded shadow-xl bg-contrast sm:my-12 sm:flex-none sm:w-full sm:max-w-sm sm:p-6"
role="button"
onClick={(e) => e.stopPropagation()}
onKeyPress={(e) => e.stopPropagation()}
tabIndex={0}
>
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="p-4 -m-4 transition text-primary hover:text-primary/50"
onClick={close}
>
<IconClose aria-label="Close panel" />
</button>
</div>
{children}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,105 @@
import {
gql,
HydrogenResponse,
useLocalization,
useShopQuery,
} from '@shopify/hydrogen';
import {Suspense} from 'react';
import {PRODUCT_CARD_FRAGMENT} from '~/lib/fragments';
import {Button, FeaturedCollections, PageHeader, Text} from '~/components';
import {ProductSwimlane, Layout} from '~/components/index.server';
import type {
CollectionConnection,
ProductConnection,
} from '@shopify/hydrogen/storefront-api-types';
export function NotFound({
response,
type = 'page',
}: {
response?: HydrogenResponse;
type?: string;
}) {
if (response) {
response.status = 404;
response.statusText = 'Not found';
}
const heading = `Weve lost this ${type}`;
const description = `We couldnt find the ${type} youre looking for. Try checking the URL or heading back to the home page.`;
return (
<Layout>
<PageHeader heading={heading}>
<Text width="narrow" as="p">
{description}
</Text>
<Button width="auto" variant="secondary" to={'/'}>
Take me to the home page
</Button>
</PageHeader>
<Suspense>
<FeaturedSection />
</Suspense>
</Layout>
);
}
function FeaturedSection() {
const {
language: {isoCode: languageCode},
country: {isoCode: countryCode},
} = useLocalization();
const {data} = useShopQuery<{
featuredCollections: CollectionConnection;
featuredProducts: ProductConnection;
}>({
query: NOT_FOUND_QUERY,
variables: {
language: languageCode,
country: countryCode,
},
preload: true,
});
const {featuredCollections, featuredProducts} = data;
return (
<>
{featuredCollections.nodes.length < 2 && (
<FeaturedCollections
title="Popular Collections"
data={featuredCollections.nodes}
/>
)}
<ProductSwimlane data={featuredProducts.nodes} />
</>
);
}
const NOT_FOUND_QUERY = gql`
${PRODUCT_CARD_FRAGMENT}
query homepage($country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
featuredCollections: collections(first: 3, sortKey: UPDATED_AT) {
nodes {
id
title
handle
image {
altText
width
height
url
}
}
}
featuredProducts: products(first: 12) {
nodes {
...ProductCard
}
}
}
`;

View File

@@ -0,0 +1,38 @@
import clsx from 'clsx';
import {Heading} from '~/components';
export function PageHeader({
children,
className,
heading,
variant = 'default',
...props
}: {
children?: React.ReactNode;
className?: string;
heading?: string;
variant?: 'default' | 'blogPost' | 'allCollections';
[key: string]: any;
}) {
const variants: Record<string, string> = {
default: 'grid w-full gap-8 p-6 py-8 md:p-8 lg:p-12 justify-items-start',
blogPost:
'grid md:text-center w-full gap-4 p-6 py-8 md:p-8 lg:p-12 md:justify-items-center',
allCollections:
'flex justify-between items-baseline gap-8 p-6 md:p-8 lg:p-12',
};
const styles = clsx(variants[variant], className);
return (
<header {...props} className={styles}>
{heading && (
<Heading as="h1" width="narrow" size="heading" className="inline-block">
{heading}
</Heading>
)}
{children}
</header>
);
}

View File

@@ -0,0 +1,3 @@
export {Footer} from './Footer.server';
export {Layout} from './Layout.server';
export {NotFound} from './NotFound.server';

View File

@@ -0,0 +1,5 @@
export {Drawer, useDrawer} from './Drawer.client';
export {FooterMenu} from './FooterMenu.client';
export {Header} from './Header.client';
export {Modal} from './Modal.client';
export {PageHeader} from './PageHeader';

View File

@@ -0,0 +1,5 @@
export * from './cards/index.server';
export * from './global/index.server';
export * from './sections/index.server';
export * from './search/index.server';
export {DefaultSeo} from './DefaultSeo.server';

View File

@@ -0,0 +1,10 @@
export * from './account/index';
export * from './cards/index';
export * from './cart/index';
export * from './elements/index';
export * from './global/index';
export * from './product/index';
export * from './sections/index';
export {CountrySelector} from './CountrySelector.client';
export {CustomFont} from './CustomFont.client';
export {HeaderFallback} from './HeaderFallback';

View File

@@ -0,0 +1,54 @@
// @ts-expect-error @headlessui/react incompatibility with node16 resolution
import {Disclosure} from '@headlessui/react';
import {Link} from '@shopify/hydrogen';
import {Text, IconClose} from '~/components';
export function ProductDetail({
title,
content,
learnMore,
}: {
title: string;
content: string;
learnMore?: string;
}) {
return (
<Disclosure key={title} as="div" className="grid w-full gap-2">
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
{({open}) => (
<>
<Disclosure.Button className="text-left">
<div className="flex justify-between">
<Text size="lead" as="h4">
{title}
</Text>
<IconClose
className={`${
open ? '' : 'rotate-[45deg]'
} transition-transform transform-gpu duration-200`}
/>
</div>
</Disclosure.Button>
<Disclosure.Panel className={'pb-4 pt-2 grid gap-2'}>
<div
className="prose dark:prose-invert"
dangerouslySetInnerHTML={{__html: content}}
/>
{learnMore && (
<div className="">
<Link
className="pb-px border-b border-primary/30 text-primary/50"
to={learnMore}
>
Learn more
</Link>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
);
}

View File

@@ -0,0 +1,144 @@
import {useEffect, useCallback, useState} from 'react';
import {
useProductOptions,
isBrowser,
useUrl,
AddToCartButton,
Money,
OptionWithValues,
ShopPayButton,
} from '@shopify/hydrogen';
import {Heading, Text, Button, ProductOptions} from '~/components';
export function ProductForm() {
const {pathname, search} = useUrl();
const [params, setParams] = useState(new URLSearchParams(search));
const {options, setSelectedOption, selectedOptions, selectedVariant} =
useProductOptions();
const isOutOfStock = !selectedVariant?.availableForSale || false;
const isOnSale =
selectedVariant?.priceV2?.amount <
selectedVariant?.compareAtPriceV2?.amount || false;
useEffect(() => {
if (params || !search) return;
setParams(new URLSearchParams(search));
}, [params, search]);
useEffect(() => {
(options as OptionWithValues[]).map(({name, values}) => {
if (!params) return;
const currentValue = params.get(name.toLowerCase()) || null;
if (currentValue) {
const matchedValue = values.filter(
(value) => encodeURIComponent(value.toLowerCase()) === currentValue,
);
setSelectedOption(name, matchedValue[0]);
} else {
params.set(
encodeURIComponent(name.toLowerCase()),
encodeURIComponent(selectedOptions![name]!.toLowerCase()),
),
window.history.replaceState(
null,
'',
`${pathname}?${params.toString()}`,
);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleChange = useCallback(
(name: string, value: string) => {
setSelectedOption(name, value);
if (!params) return;
params.set(
encodeURIComponent(name.toLowerCase()),
encodeURIComponent(value.toLowerCase()),
);
if (isBrowser()) {
window.history.replaceState(
null,
'',
`${pathname}?${params.toString()}`,
);
}
},
[setSelectedOption, params, pathname],
);
return (
<form className="grid gap-10">
{
<div className="grid gap-4">
{(options as OptionWithValues[]).map(({name, values}) => {
if (values.length === 1) {
return null;
}
return (
<div
key={name}
className="flex flex-col flex-wrap mb-4 gap-y-2 last:mb-0"
>
<Heading as="legend" size="lead" className="min-w-[4rem]">
{name}
</Heading>
<div className="flex flex-wrap items-baseline gap-4">
<ProductOptions
name={name}
handleChange={handleChange}
values={values}
/>
</div>
</div>
);
})}
</div>
}
<div className="grid items-stretch gap-4">
<AddToCartButton
variantId={selectedVariant?.id}
quantity={1}
accessibleAddingToCartLabel="Adding item to your cart"
disabled={isOutOfStock}
>
<Button
width="full"
variant={isOutOfStock ? 'secondary' : 'primary'}
as="span"
>
{isOutOfStock ? (
<Text>Sold out</Text>
) : (
<Text
as="span"
className="flex items-center justify-center gap-2"
>
<span>Add to bag</span> <span>·</span>{' '}
<Money
withoutTrailingZeros
data={selectedVariant.priceV2!}
as="span"
/>
{isOnSale && (
<Money
withoutTrailingZeros
data={selectedVariant.compareAtPriceV2!}
as="span"
className="opacity-50 strike"
/>
)}
</Text>
)}
</Button>
</AddToCartButton>
{!isOutOfStock && <ShopPayButton variantIds={[selectedVariant.id!]} />}
</div>
</form>
);
}

View File

@@ -0,0 +1,106 @@
import {MediaFile} from '@shopify/hydrogen/client';
import type {MediaEdge} from '@shopify/hydrogen/storefront-api-types';
import {ATTR_LOADING_EAGER} from '~/lib/const';
/**
* A client component that defines a media gallery for hosting images, 3D models, and videos of products
*/
export function ProductGallery({
media,
className,
}: {
media: MediaEdge['node'][];
className?: string;
}) {
if (!media.length) {
return null;
}
return (
<div
className={`swimlane md:grid-flow-row hiddenScroll md:p-0 md:overflow-x-auto md:grid-cols-2 ${className}`}
>
{media.map((med, i) => {
let mediaProps: Record<string, any> = {};
const isFirst = i === 0;
const isFourth = i === 3;
const isFullWidth = i % 3 === 0;
const data = {
...med,
image: {
// @ts-ignore
...med.image,
altText: med.alt || 'Product image',
},
};
switch (med.mediaContentType) {
case 'IMAGE':
mediaProps = {
width: 800,
widths: [400, 800, 1200, 1600, 2000, 2400],
};
break;
case 'VIDEO':
mediaProps = {
width: '100%',
autoPlay: true,
controls: false,
muted: true,
loop: true,
preload: 'auto',
};
break;
case 'EXTERNAL_VIDEO':
mediaProps = {width: '100%'};
break;
case 'MODEL_3D':
mediaProps = {
width: '100%',
interactionPromptThreshold: '0',
ar: true,
loading: ATTR_LOADING_EAGER,
disableZoom: true,
};
break;
}
if (i === 0 && med.mediaContentType === 'IMAGE') {
mediaProps.loading = ATTR_LOADING_EAGER;
}
const style = [
isFullWidth ? 'md:col-span-2' : 'md:col-span-1',
isFirst || isFourth ? '' : 'md:aspect-[4/5]',
'aspect-square snap-center card-image bg-white dark:bg-contrast/10 w-mobileGallery md:w-full',
].join(' ');
return (
<div
className={style}
// @ts-ignore
key={med.id || med.image.id}
>
<MediaFile
tabIndex="0"
className={`w-full h-full aspect-square fadeIn object-cover`}
data={data}
sizes={
isFullWidth
? '(min-width: 64em) 60vw, (min-width: 48em) 50vw, 90vw'
: '(min-width: 64em) 30vw, (min-width: 48em) 25vw, 90vw'
}
// @ts-ignore
options={{
crop: 'center',
scale: 2,
}}
{...mediaProps}
/>
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,113 @@
import {useState, useRef, useEffect, useCallback} from 'react';
import {Link, flattenConnection} from '@shopify/hydrogen';
import {Button, Grid, ProductCard} from '~/components';
import {getImageLoadingPriority} from '~/lib/const';
import type {Collection, Product} from '@shopify/hydrogen/storefront-api-types';
export function ProductGrid({
url,
collection,
}: {
url: string;
collection: Collection;
}) {
const nextButtonRef = useRef(null);
const initialProducts = collection?.products?.nodes || [];
const {hasNextPage, endCursor} = collection?.products?.pageInfo ?? {};
const [products, setProducts] = useState<Product[]>(initialProducts);
const [cursor, setCursor] = useState(endCursor ?? '');
const [nextPage, setNextPage] = useState(hasNextPage);
const [pending, setPending] = useState(false);
const haveProducts = initialProducts.length > 0;
const fetchProducts = useCallback(async () => {
setPending(true);
const postUrl = new URL(window.location.origin + url);
postUrl.searchParams.set('cursor', cursor);
const response = await fetch(postUrl, {
method: 'POST',
});
const {data} = await response.json();
// ProductGrid can paginate collection, products and search routes
// @ts-ignore TODO: Fix types
const newProducts: Product[] = flattenConnection<Product>(
data?.collection?.products || data?.products || [],
);
const {endCursor, hasNextPage} = data?.collection?.products?.pageInfo ||
data?.products?.pageInfo || {endCursor: '', hasNextPage: false};
setProducts([...products, ...newProducts]);
setCursor(endCursor);
setNextPage(hasNextPage);
setPending(false);
}, [cursor, url, products]);
const handleIntersect = useCallback(
(entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fetchProducts();
}
});
},
[fetchProducts],
);
useEffect(() => {
const observer = new IntersectionObserver(handleIntersect, {
rootMargin: '100%',
});
const nextButton = nextButtonRef.current;
if (nextButton) observer.observe(nextButton);
return () => {
if (nextButton) observer.unobserve(nextButton);
};
}, [nextButtonRef, cursor, handleIntersect]);
if (!haveProducts) {
return (
<>
<p>No products found on this collection</p>
<Link to="/products">
<p className="underline">Browse catalog</p>
</Link>
</>
);
}
return (
<>
<Grid layout="products">
{products.map((product, i) => (
<ProductCard
key={product.id}
product={product}
loading={getImageLoadingPriority(i)}
/>
))}
</Grid>
{nextPage && (
<div
className="flex items-center justify-center mt-6"
ref={nextButtonRef}
>
<Button
variant="secondary"
disabled={pending}
onClick={fetchProducts}
width="full"
>
{pending ? 'Loading...' : 'Load more products'}
</Button>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,141 @@
import {useCallback, useState} from 'react';
// @ts-expect-error @headlessui/react incompatibility with node16 resolution
import {Listbox} from '@headlessui/react';
import {useProductOptions} from '@shopify/hydrogen';
import {Text, IconCheck, IconCaret} from '~/components';
export function ProductOptions({
values,
...props
}: {
values: any[];
[key: string]: any;
} & React.ComponentProps<typeof OptionsGrid>) {
const asDropdown = values.length > 7;
return asDropdown ? (
<OptionsDropdown values={values} {...props} />
) : (
<OptionsGrid values={values} {...props} />
);
}
function OptionsGrid({
values,
name,
handleChange,
}: {
values: string[];
name: string;
handleChange: (name: string, value: string) => void;
}) {
const {selectedOptions} = useProductOptions();
return (
<>
{values.map((value) => {
const checked = selectedOptions![name] === value;
const id = `option-${name}-${value}`;
return (
<Text as="label" key={id} htmlFor={id}>
<input
className="sr-only"
type="radio"
id={id}
name={`option[${name}]`}
value={value}
checked={checked}
onChange={() => handleChange(name, value)}
/>
<div
className={`leading-none py-1 border-b-[1.5px] cursor-pointer transition-all duration-200 ${
checked ? 'border-primary/50' : 'border-primary/0'
}`}
>
{value}
</div>
</Text>
);
})}
</>
);
}
// TODO: De-dupe UI with CountrySelector
function OptionsDropdown({
values,
name,
handleChange,
}: {
values: string[];
name: string;
handleChange: (name: string, value: string) => void;
}) {
const [listboxOpen, setListboxOpen] = useState(false);
const {selectedOptions} = useProductOptions();
const updateSelectedOption = useCallback(
(value: string) => {
handleChange(name, value);
},
[name, handleChange],
);
return (
<div className="relative w-full">
<Listbox onChange={updateSelectedOption} value="">
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
{({open}) => {
setTimeout(() => setListboxOpen(open));
return (
<>
<Listbox.Button
className={`flex items-center justify-between w-full py-3 px-4 border border-primary ${
open ? 'rounded-b md:rounded-t md:rounded-b-none' : 'rounded'
}`}
>
<span>{selectedOptions![name]}</span>
<IconCaret direction={open ? 'up' : 'down'} />
</Listbox.Button>
<Listbox.Options
className={`border-primary bg-contrast absolute bottom-12 z-30 grid
h-48 w-full overflow-y-scroll rounded-t border px-2 py-2 transition-[max-height]
duration-150 sm:bottom-auto md:rounded-b md:rounded-t-none md:border-t-0 md:border-b ${
listboxOpen ? 'max-h-48' : 'max-h-0'
}`}
>
{values.map((value) => {
const isSelected = selectedOptions![name] === value;
const id = `option-${name}-${value}`;
return (
<Listbox.Option key={id} value={value}>
{/* @ts-expect-error @headlessui/react incompatibility with node16 resolution */}
{({active}) => (
<div
className={`text-primary w-full p-2 transition rounded flex justify-start items-center text-left cursor-pointer ${
active ? 'bg-primary/10' : null
}`}
>
{value}
{isSelected ? (
<span className="ml-2">
<IconCheck />
</span>
) : null}
</div>
)}
</Listbox.Option>
);
})}
</Listbox.Options>
</>
);
}}
</Listbox>
</div>
);
}

View File

@@ -0,0 +1,5 @@
export {ProductForm} from './ProductForm.client';
export {ProductGallery} from './ProductGallery.client';
export {ProductGrid} from './ProductGrid.client';
export {ProductDetail} from './ProductDetail.client';
export {ProductOptions} from './ProductOptions.client';

View File

@@ -0,0 +1,65 @@
import {gql, useShopQuery} from '@shopify/hydrogen';
import {PRODUCT_CARD_FRAGMENT} from '~/lib/fragments';
import {FeaturedCollections} from '~/components';
import {ProductSwimlane} from '~/components/index.server';
import {PAGINATION_SIZE} from '~/lib/const';
export function NoResultRecommendations({
country,
language,
}: {
country: string;
language: string;
}) {
const {data} = useShopQuery<any>({
query: SEARCH_NO_RESULTS_QUERY,
variables: {
country,
language,
pageBy: PAGINATION_SIZE,
},
preload: false,
});
return (
<>
<FeaturedCollections
title="Trending Collections"
data={data.featuredCollections.nodes}
/>
<ProductSwimlane
title="Trending Products"
data={data.featuredProducts.nodes}
/>
</>
);
}
const SEARCH_NO_RESULTS_QUERY = gql`
${PRODUCT_CARD_FRAGMENT}
query searchNoResult(
$country: CountryCode
$language: LanguageCode
$pageBy: Int!
) @inContext(country: $country, language: $language) {
featuredCollections: collections(first: 3, sortKey: UPDATED_AT) {
nodes {
id
title
handle
image {
altText
width
height
url
}
}
}
featuredProducts: products(first: $pageBy) {
nodes {
...ProductCard
}
}
}
`;

View File

@@ -0,0 +1,33 @@
import {Heading, Input, PageHeader} from '~/components';
import {Layout} from '~/components/index.server';
export function SearchPage({
searchTerm,
children,
}: {
searchTerm?: string | null;
children: React.ReactNode;
}) {
return (
<Layout>
<PageHeader>
<Heading as="h1" size="copy">
Search
</Heading>
<form className="relative flex w-full text-heading">
<Input
defaultValue={searchTerm}
placeholder="Search…"
type="search"
variant="search"
name="q"
/>
<button className="absolute right-0 py-2" type="submit">
Go
</button>
</form>
</PageHeader>
{children}
</Layout>
);
}

View File

@@ -0,0 +1,2 @@
export {NoResultRecommendations} from './NoResultRecommendations.server';
export {SearchPage} from './SearchPage.server';

View File

@@ -0,0 +1,55 @@
import {Link, Image} from '@shopify/hydrogen';
import type {Collection} from '@shopify/hydrogen/storefront-api-types';
import {Heading, Section, Grid} from '~/components';
export function FeaturedCollections({
data,
title = 'Collections',
...props
}: {
data: Collection[];
title?: string;
[key: string]: any;
}) {
const items = data.filter((item) => item.image).length;
const haveCollections = data.length > 0;
if (!haveCollections) return null;
return (
<Section {...props} heading={title}>
<Grid items={items}>
{data.map((collection) => {
if (!collection?.image) {
return null;
}
// TODO: Refactor to use CollectionCard
return (
<Link key={collection.id} to={`/collections/${collection.handle}`}>
<div className="grid gap-4">
<div className="card-image bg-primary/5 aspect-[3/2]">
{collection?.image && (
<Image
alt={`Image of ${collection.title}`}
data={collection.image}
height={400}
sizes="(max-width: 32em) 100vw, 33vw"
width={600}
widths={[400, 500, 600, 700, 800, 900]}
loaderOptions={{
scale: 2,
crop: 'center',
}}
/>
)}
</div>
<Heading size="copy">{collection.title}</Heading>
</div>
</Link>
);
})}
</Grid>
</Section>
);
}

View File

@@ -0,0 +1,143 @@
import {Image, Link, Video} from '@shopify/hydrogen';
import type {Media} from '@shopify/hydrogen/storefront-api-types';
import {Heading, Text} from '~/components';
interface Metafield {
value: string;
reference?: object;
}
export function Hero({
byline,
cta,
handle,
heading,
height,
loading,
spread,
spreadSecondary,
top,
}: {
byline: Metafield;
cta: Metafield;
handle: string;
heading: Metafield;
height?: 'full';
loading?: 'eager' | 'lazy';
spread: Metafield;
spreadSecondary: Metafield;
top?: boolean;
}) {
return (
<Link to={`/collections/${handle}`}>
<section
className={`relative justify-end flex flex-col w-full ${
top && '-mt-nav'
} ${
height === 'full'
? 'h-screen'
: 'aspect-[4/5] sm:aspect-square md:aspect-[5/4] lg:aspect-[3/2] xl:aspect-[2/1]'
}`}
>
<div className="absolute inset-0 grid flex-grow grid-flow-col pointer-events-none auto-cols-fr -z-10 content-stretch overflow-clip">
{spread?.reference && (
<div className="">
<SpreadMedia
scale={2}
sizes={
spreadSecondary?.reference
? '(min-width: 80em) 700px, (min-width: 48em) 450px, 500px'
: '(min-width: 80em) 1400px, (min-width: 48em) 900px, 500px'
}
widths={
spreadSecondary?.reference
? [500, 450, 700]
: [500, 900, 1400]
}
width={spreadSecondary?.reference ? 375 : 750}
data={spread.reference as Media}
loading={loading}
/>
</div>
)}
{spreadSecondary?.reference && (
<div className="hidden md:block">
<SpreadMedia
sizes="(min-width: 80em) 700, (min-width: 48em) 450, 500"
widths={[450, 700]}
width={375}
data={spreadSecondary.reference}
/>
</div>
)}
</div>
<div className="flex flex-col items-baseline justify-between gap-4 px-6 py-8 sm:px-8 md:px-12 bg-gradient-to-t dark:from-contrast/60 dark:text-primary from-primary/60 text-contrast">
{heading?.value && (
<Heading format as="h2" size="display" className="max-w-md">
{heading.value}
</Heading>
)}
{byline?.value && (
<Text format width="narrow" as="p" size="lead">
{byline.value}
</Text>
)}
{cta?.value && <Text size="lead">{cta.value}</Text>}
</div>
</section>
</Link>
);
}
interface SpreadMediaProps {
data: Media;
loading?: HTMLImageElement['loading'];
scale?: 2 | 3;
sizes: string;
width: number;
widths: number[];
}
function SpreadMedia({
data,
loading,
scale,
sizes,
width,
widths,
}: SpreadMediaProps) {
if (data.mediaContentType === 'VIDEO') {
return (
<Video
previewImageOptions={{scale, src: data.previewImage!.url}}
width={scale! * width}
className="block object-cover w-full h-full"
data={data}
controls={false}
muted
loop
playsInline
autoPlay
/>
);
}
if (data.mediaContentType === 'IMAGE') {
return (
<Image
widths={widths}
sizes={sizes}
alt={data.alt || 'Marketing Banner Image'}
className="block object-cover w-full h-full"
// @ts-ignore
data={data.image}
loading={loading}
width={width}
loaderOptions={{scale, crop: 'center'}}
/>
);
}
return null;
}

View File

@@ -0,0 +1,16 @@
import {Product} from '@shopify/hydrogen/storefront-api-types';
import {ProductCard} from '../cards/ProductCard.client';
export function ProductCards({products}: {products: Product[]}) {
return (
<>
{products.map((product) => (
<ProductCard
product={product}
key={product.id}
className={'snap-start w-80'}
/>
))}
</>
);
}

View File

@@ -0,0 +1,147 @@
import {Suspense, useMemo} from 'react';
import {gql, useShopQuery, useLocalization} from '@shopify/hydrogen';
import {PRODUCT_CARD_FRAGMENT} from '~/lib/fragments';
import {ProductCard, Section} from '~/components';
import type {
Product,
ProductConnection,
} from '@shopify/hydrogen/storefront-api-types';
const mockProducts = new Array(12).fill('');
export function ProductSwimlane({
title = 'Featured Products',
data = mockProducts,
count = 12,
...props
}) {
const productCardsMarkup = useMemo(() => {
// If the data is already provided, there's no need to query it, so we'll just return the data
if (typeof data === 'object') {
return <ProductCards products={data} />;
}
// If the data provided is a productId, we will query the productRecommendations API.
// To make sure we have enough products for the swimlane, we'll combine the results with our top selling products.
if (typeof data === 'string') {
return (
<Suspense>
<RecommendedProducts productId={data} count={count} />
</Suspense>
);
}
// If no data is provided, we'll go and query the top products
return <TopProducts count={count} />;
}, [count, data]);
return (
<Section heading={title} padding="y" {...props}>
<div className="swimlane hiddenScroll md:pb-8 md:scroll-px-8 lg:scroll-px-12 md:px-8 lg:px-12">
{productCardsMarkup}
</div>
</Section>
);
}
function ProductCards({products}: {products: Product[]}) {
return (
<>
{products.map((product) => (
<ProductCard
product={product}
key={product.id}
className={'snap-start w-80'}
/>
))}
</>
);
}
function RecommendedProducts({
productId,
count,
}: {
productId: string;
count: number;
}) {
const {
language: {isoCode: languageCode},
country: {isoCode: countryCode},
} = useLocalization();
const {data: products} = useShopQuery<{
recommended: Product[];
additional: ProductConnection;
}>({
query: RECOMMENDED_PRODUCTS_QUERY,
variables: {
count,
productId,
languageCode,
countryCode,
},
});
const mergedProducts = products.recommended
.concat(products.additional.nodes)
.filter(
(value, index, array) =>
array.findIndex((value2) => value2.id === value.id) === index,
);
const originalProduct = mergedProducts
.map((item) => item.id)
.indexOf(productId);
mergedProducts.splice(originalProduct, 1);
return <ProductCards products={mergedProducts} />;
}
function TopProducts({count}: {count: number}) {
const {
data: {products},
} = useShopQuery({
query: TOP_PRODUCTS_QUERY,
variables: {
count,
},
});
return <ProductCards products={products.nodes} />;
}
const RECOMMENDED_PRODUCTS_QUERY = gql`
${PRODUCT_CARD_FRAGMENT}
query productRecommendations(
$productId: ID!
$count: Int
$countryCode: CountryCode
$languageCode: LanguageCode
) @inContext(country: $countryCode, language: $languageCode) {
recommended: productRecommendations(productId: $productId) {
...ProductCard
}
additional: products(first: $count, sortKey: BEST_SELLING) {
nodes {
...ProductCard
}
}
}
`;
const TOP_PRODUCTS_QUERY = gql`
${PRODUCT_CARD_FRAGMENT}
query topProducts(
$count: Int
$countryCode: CountryCode
$languageCode: LanguageCode
) @inContext(country: $countryCode, language: $languageCode) {
products(first: $count, sortKey: BEST_SELLING) {
nodes {
...ProductCard
}
}
}
`;

View File

@@ -0,0 +1 @@
export {ProductSwimlane} from './ProductSwimlane.server';

View File

@@ -0,0 +1,2 @@
export {FeaturedCollections} from './FeaturedCollections';
export {Hero} from './Hero';

View File

@@ -0,0 +1,10 @@
export const PAGINATION_SIZE = 8;
export const DEFAULT_GRID_IMG_LOAD_EAGER_COUNT = 4;
export const ATTR_LOADING_EAGER = 'eager';
export function getImageLoadingPriority(
index: number,
maxEagerLoadCount = DEFAULT_GRID_IMG_LOAD_EAGER_COUNT,
) {
return index < maxEagerLoadCount ? ATTR_LOADING_EAGER : undefined;
}

View File

@@ -0,0 +1,66 @@
import {gql} from '@shopify/hydrogen';
export const MEDIA_FRAGMENT = gql`
fragment Media on Media {
mediaContentType
alt
previewImage {
url
}
... on MediaImage {
id
image {
url
width
height
}
}
... on Video {
id
sources {
mimeType
url
}
}
... on Model3d {
id
sources {
mimeType
url
}
}
... on ExternalVideo {
id
embedUrl
host
}
}
`;
export const PRODUCT_CARD_FRAGMENT = gql`
fragment ProductCard on Product {
id
title
publishedAt
handle
variants(first: 1) {
nodes {
id
image {
url
altText
width
height
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
}
}
}
`;

Some files were not shown because too many files have changed in this diff Show More