diff --git a/.gitignore b/.gitignore index a5199c5671..a6c33bbdb6 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ storybook-static/ node_modules/ jspm_packages/ +!packages/cli/src/__mocks__/**/node_modules + # Optional npm cache directory .npm diff --git a/apps/site/package.json b/apps/site/package.json index c380f50253..0fd6e3f824 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -1,6 +1,6 @@ { "name": "site", - "version": "3.0.77", + "version": "3.0.88", "author": "Emerson Laurentino @emersonlaurentino", "license": "MIT", "private": true, @@ -10,9 +10,9 @@ "start": "next start" }, "dependencies": { - "@faststore/api": "^3.0.76", - "@faststore/sdk": "^3.0.68", - "@faststore/ui": "^3.0.77", + "@faststore/api": "^3.0.88", + "@faststore/sdk": "^3.0.88", + "@faststore/ui": "^3.0.88", "next": "13.0.7", "nextra": "^2.8.0", "nextra-theme-docs": "^2.8.0", @@ -21,7 +21,7 @@ "sass": "^1.57.1" }, "devDependencies": { - "@faststore/eslint-config": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", "@types/node": "^18.11.16", "eslint": "7.32.0", "react-docgen-typescript": "^2.2.2", diff --git a/apps/site/pages/components/molecules/input-field.mdx b/apps/site/pages/components/molecules/input-field.mdx index a9c8cf8e3e..12c4b4f187 100644 --- a/apps/site/pages/components/molecules/input-field.mdx +++ b/apps/site/pages/components/molecules/input-field.mdx @@ -205,13 +205,19 @@ export const propsInputField = [ #### Label - + - + + + +#### Button + + + ### Variants diff --git a/apps/site/pages/components/molecules/order-summary.mdx b/apps/site/pages/components/molecules/order-summary.mdx index 9bc0e655ef..bb0c831ba9 100644 --- a/apps/site/pages/components/molecules/order-summary.mdx +++ b/apps/site/pages/components/molecules/order-summary.mdx @@ -114,6 +114,10 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c token="--fs-order-summary-margin-bottom" value="var(--fs-spacing-2)" /> + + diff --git a/apps/site/pages/components/molecules/toast.mdx b/apps/site/pages/components/molecules/toast.mdx index 1480bd4efd..11624dcc69 100644 --- a/apps/site/pages/components/molecules/toast.mdx +++ b/apps/site/pages/components/molecules/toast.mdx @@ -315,7 +315,7 @@ export const propsToast = [ token="--fs-toast-title-weight" value="var(--fs-text-weight-bold)" /> - + diff --git a/apps/site/pages/components/organisms/payment-methods.mdx b/apps/site/pages/components/organisms/payment-methods.mdx index 14c552e8b1..c94eb2c530 100644 --- a/apps/site/pages/components/organisms/payment-methods.mdx +++ b/apps/site/pages/components/organisms/payment-methods.mdx @@ -84,14 +84,16 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c ```tsx export const flags = [ - { icon: { icon: 'Visa' }, alt: 'Visa' }, + { icon: { icon: 'Amex' }, alt: 'Amex' }, + { icon: { icon: 'ApplePay' }, alt: 'ApplePay' }, + { icon: { icon: 'EloCard' }, alt: 'Elo Card' }, { icon: { icon: 'Diners' }, alt: 'Diners Club' }, + { icon: { icon: 'GooglePay' }, alt: 'GooglePay' }, { icon: { icon: 'Mastercard' }, alt: 'Mastercard' }, - { icon: { icon: 'EloCard' }, alt: 'Elo Card' }, { icon: { icon: 'PayPal' }, alt: 'PayPal' }, + { icon: { icon: 'Pix' }, alt: 'Pix' }, { icon: { icon: 'Stripe' }, alt: 'Stripe' }, - { icon: { icon: 'GooglePay' }, alt: 'GooglePay' }, - { icon: { icon: 'ApplePay' }, alt: 'ApplePay' }, + { icon: { icon: 'Visa' }, alt: 'Visa' }, ] Payment Methods} flagList={flags} /> @@ -101,14 +103,16 @@ Follow the instructions in the [Importing FastStore UI component styles](/docs/c export const flags = [ - { icon: { icon: 'Visa' }, alt: 'Visa' }, + { icon: { icon: 'Amex' }, alt: 'Amex' }, + { icon: { icon: 'ApplePay' }, alt: 'ApplePay' }, + { icon: { icon: 'EloCard' }, alt: 'Elo Card' }, { icon: { icon: 'Diners' }, alt: 'Diners Club' }, + { icon: { icon: 'GooglePay' }, alt: 'GooglePay' }, { icon: { icon: 'Mastercard' }, alt: 'Mastercard' }, - { icon: { icon: 'EloCard' }, alt: 'Elo Card' }, { icon: { icon: 'PayPal' }, alt: 'PayPal' }, + { icon: { icon: 'Pix' }, alt: 'Pix' }, { icon: { icon: 'Stripe' }, alt: 'Stripe' }, - { icon: { icon: 'GooglePay' }, alt: 'GooglePay' }, - { icon: { icon: 'ApplePay' }, alt: 'ApplePay' }, + { icon: { icon: 'Visa' }, alt: 'Visa' }, ] --- @@ -177,6 +181,11 @@ export const propsFlag = [ token="--fs-payment-methods-flag-height" value="var(--fs-spacing-4)" /> + + + @@ -45,41 +47,25 @@ export const desktopList = [ { size: 14, token: 'var(--fs-text-size-1)' }, { size: 16, token: 'var(--fs-text-size-2)' }, { size: 20, token: 'var(--fs-text-size-3)' }, - { size: 25, token: 'var(--fs-text-size-4)' }, - { size: 31, token: 'var(--fs-text-size-5)' }, - { size: 39, token: 'var(--fs-text-size-6)' }, - { size: 48, token: 'var(--fs-text-size-7)' }, - { size: 61, token: 'var(--fs-text-size-8)' }, + { size: 24, token: 'var(--fs-text-size-4)' }, + { size: 28, token: 'var(--fs-text-size-5)' }, + { size: 32, token: 'var(--fs-text-size-6)' }, + { size: 40, token: 'var(--fs-text-size-7)' }, + { size: 48, token: 'var(--fs-text-size-8)' }, + { size: 56, token: 'var(--fs-text-size-9)' }, ] export const mobileList = [ { size: 12, token: 'var(--fs-text-size-0)' }, { size: 14, token: 'var(--fs-text-size-1)' }, { size: 16, token: 'var(--fs-text-size-2)' }, - { - size: 18, - token: 'var(--fs-text-size-3)', - }, - { - size: 20, - token: 'var(--fs-text-size-4)', - }, - { - size: 23, - token: 'var(--fs-text-size-5)', - }, - { - size: 26, - token: 'var(--fs-text-size-6)', - }, - { - size: 29, - token: 'var(--fs-text-size-7)', - }, - { - size: 33, - token: 'var(--fs-text-size-8)', - }, + { size: 18, token: 'var(--fs-text-size-3)' }, + { size: 20, token: 'var(--fs-text-size-4)' }, + { size: 22, token: 'var(--fs-text-size-5)' }, + { size: 24, token: 'var(--fs-text-size-6)' }, + { size: 28, token: 'var(--fs-text-size-7)' }, + { size: 32, token: 'var(--fs-text-size-8)' }, + { size: 36, token: 'var(--fs-text-size-9)' }, ] ### Mobile Scale @@ -92,35 +78,39 @@ export const mobileList = [ - - + + + @@ -131,8 +121,8 @@ export const mobileList = [ ### Sizes - - + + tag for your font-family of choice */} ); @@ -202,7 +192,7 @@ export default WebFonts; // -------------------------------------------------------- // Typography (Branding Core) // -------------------------------------------------------- - --fs-text-face-body: 'Lato', -apple-system, system-ui, BlinkMacSystemFont, sans-serif; + --fs-text-face-body: 'Inter', -apple-system, system-ui, BlinkMacSystemFont, sans-serif; } } ``` diff --git a/apps/site/pages/docs/icons.mdx b/apps/site/pages/docs/icons.mdx index 119fe89457..854ab681d5 100644 --- a/apps/site/pages/docs/icons.mdx +++ b/apps/site/pages/docs/icons.mdx @@ -85,14 +85,16 @@ You can find more details about the `Icon` component [here](/components/atoms/ic ### Payment Flags - } name="Visa" /> + } name="Amex" /> + } name="ApplePay" /> + } name="EloCard" /> } name="Diners" /> + } name="GooglePay" /> } name="Mastercard" /> - } name="EloCard" /> } name="PayPal" /> + } name="Pix" /> } name="Stripe" /> - } name="GooglePay" /> - } name="ApplePay" /> + } name="Visa" /> ### Social diff --git a/apps/site/public/icons.svg b/apps/site/public/icons.svg index 14ae64a260..50a10851c5 100644 --- a/apps/site/public/icons.svg +++ b/apps/site/public/icons.svg @@ -1,65 +1,129 @@ - + + + + + + + + + + + + + + + + + - + + - + - + + - + + + + - + + + - + + + - - + + + + + + + + + + - + + - - - + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lerna.json b/lerna.json index 8e466899e9..bb7b77df1c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "3.0.77", + "version": "3.0.88", "npmClient": "yarn", "command": { "publish": { diff --git a/packages/api/package.json b/packages/api/package.json index 2e0f0a4d82..00f07030ee 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/api", - "version": "3.0.76", + "version": "3.0.88", "license": "MIT", "main": "dist/cjs/src/index.js", "typings": "dist/esm/src/index.d.ts", @@ -51,8 +51,8 @@ }, "devDependencies": { "@envelop/core": "^2.6.0", - "@faststore/eslint-config": "^3.0.68", - "@faststore/shared": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", + "@faststore/shared": "^3.0.88", "@graphql-codegen/cli": "2.2.0", "@graphql-codegen/typescript": "2.2.2", "@parcel/watcher": "^2.4.0", diff --git a/packages/api/src/platforms/vtex/utils/skuVariants.ts b/packages/api/src/platforms/vtex/utils/skuVariants.ts index b397198322..c2deb5cdf3 100644 --- a/packages/api/src/platforms/vtex/utils/skuVariants.ts +++ b/packages/api/src/platforms/vtex/utils/skuVariants.ts @@ -132,7 +132,13 @@ function sortVariants(variantsByName: SkuVariantsByName) { const sortedVariants = variantsByName for (const variantProperty in variantsByName) { - variantsByName[variantProperty].sort((a, b) => compare(a.value, b.value)) + const areAllNumbers = variantsByName[variantProperty].every( + (option: any) => !Number.isNaN(Number(option.value)) + ) + + // Preserve Admin's variants order for cases variants are strings + areAllNumbers && + variantsByName[variantProperty].sort((a, b) => compare(a.value, b.value)) } return sortedVariants @@ -182,7 +188,10 @@ export function getFormattedVariations( previouslySeenPropertyValues.add(nameValueIdentifier) - const variantImageToUse = findSkuVariantImage(variant.images, dominantVariantName) + const variantImageToUse = findSkuVariantImage( + variant.images, + dominantVariantName + ) const formattedVariant = { src: variantImageToUse.imageUrl, @@ -209,7 +218,10 @@ export function getFormattedVariations( previouslySeenPropertyValues.add(nameValueIdentifier) - const variantImageToUse = findSkuVariantImage(variant.images, variationProperty.name) + const variantImageToUse = findSkuVariantImage( + variant.images, + variationProperty.name + ) const formattedVariant = { src: variantImageToUse.imageUrl, diff --git a/packages/api/test/vtex.skuVariants.test.ts b/packages/api/test/vtex.skuVariants.test.ts index 220cdb0666..13614658d4 100644 --- a/packages/api/test/vtex.skuVariants.test.ts +++ b/packages/api/test/vtex.skuVariants.test.ts @@ -110,18 +110,18 @@ describe('getFormattedVariations', () => { it('should return all value options for dominantVariant and only possible combinations for others', () => { const expected = { Color: [ - { - src: 'https://storecomponents.vtexassets.com/arquivos/ids/155559/pink-sku-variation.png?v=637087508159070000', - alt: 'skuvariation', - label: 'Color: Green', - value: 'Green', - }, { src: 'https://storecomponents.vtexassets.com/arquivos/ids/155560/pink-cool-sku-variation.png?v=637063239809000000', alt: 'skuvariation', label: 'Color: Red', value: 'Red', }, + { + src: 'https://storecomponents.vtexassets.com/arquivos/ids/155559/pink-sku-variation.png?v=637087508159070000', + alt: 'skuvariation', + label: 'Color: Green', + value: 'Green', + }, { src: 'https://storecomponents.vtexassets.com/arquivos/ids/155561/white-sku-variation.png?v=637087507771770000', alt: 'skuvariation', diff --git a/packages/cli/README.md b/packages/cli/README.md index 6fa0df56e0..e15ef58818 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -30,7 +30,7 @@ $ npm install -g @faststore/cli $ faststore COMMAND running command... $ faststore (--version) -@faststore/cli/3.0.71 linux-x64 node-v18.20.3 +@faststore/cli/3.0.88 linux-x64 node-v18.20.4 $ faststore --help [COMMAND] USAGE $ faststore COMMAND @@ -41,55 +41,68 @@ USAGE ## Commands -* [`faststore build`](#faststore-build) -* [`faststore cms-sync`](#faststore-cms-sync) -* [`faststore dev`](#faststore-dev) -* [`faststore generate-graphql`](#faststore-generate-graphql) +* [`faststore build [PATH]`](#faststore-build-path) +* [`faststore cms-sync [PATH]`](#faststore-cms-sync-path) +* [`faststore dev [PATH]`](#faststore-dev-path) +* [`faststore generate-graphql [PATH]`](#faststore-generate-graphql-path) * [`faststore help [COMMAND]`](#faststore-help-command) -* [`faststore start`](#faststore-start) -* [`faststore test`](#faststore-test) +* [`faststore init [PATH]`](#faststore-init-path) +* [`faststore start [PATH]`](#faststore-start-path) +* [`faststore test [PATH]`](#faststore-test-path) -## `faststore build` +## `faststore build [PATH]` ``` USAGE - $ faststore build + $ faststore build [PATH] + +ARGUMENTS + PATH The path where the FastStore being built is. Defaults to cwd. ``` -_See code: [dist/commands/build.ts](https://github.com/vtex/faststore/blob/v3.0.71/dist/commands/build.ts)_ +_See code: [dist/commands/build.ts](https://github.com/vtex/faststore/blob/v3.0.88/dist/commands/build.ts)_ -## `faststore cms-sync` +## `faststore cms-sync [PATH]` ``` USAGE - $ faststore cms-sync [-d] + $ faststore cms-sync [PATH] [-d] + +ARGUMENTS + PATH The path where the FastStore being synched with the CMS is. Defaults to cwd. FLAGS -d, --dry-run ``` -_See code: [dist/commands/cms-sync.ts](https://github.com/vtex/faststore/blob/v3.0.71/dist/commands/cms-sync.ts)_ +_See code: [dist/commands/cms-sync.ts](https://github.com/vtex/faststore/blob/v3.0.88/dist/commands/cms-sync.ts)_ -## `faststore dev` +## `faststore dev [PATH]` ``` USAGE - $ faststore dev + $ faststore dev [PATH] + +ARGUMENTS + PATH The path where the FastStore being run is. Defaults to cwd. ``` -_See code: [dist/commands/dev.ts](https://github.com/vtex/faststore/blob/v3.0.71/dist/commands/dev.ts)_ +_See code: [dist/commands/dev.ts](https://github.com/vtex/faststore/blob/v3.0.88/dist/commands/dev.ts)_ -## `faststore generate-graphql` +## `faststore generate-graphql [PATH]` ``` USAGE - $ faststore generate-graphql [-d] + $ faststore generate-graphql [PATH] [-d] + +ARGUMENTS + PATH The path where the FastStore GraphQL customization is. Defaults to cwd. FLAGS -d, --debug ``` -_See code: [dist/commands/generate-graphql.ts](https://github.com/vtex/faststore/blob/v3.0.71/dist/commands/generate-graphql.ts)_ +_See code: [dist/commands/generate-graphql.ts](https://github.com/vtex/faststore/blob/v3.0.88/dist/commands/generate-graphql.ts)_ ## `faststore help [COMMAND]` @@ -111,21 +124,47 @@ DESCRIPTION _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.22/src/commands/help.ts)_ -## `faststore start` +## `faststore init [PATH]` + +Creates a discovery folder based on the starter.store template. + +``` +USAGE + $ faststore init [PATH] + +ARGUMENTS + PATH The path where the Discovery folder will be created. Defaults to ./discovery. + +DESCRIPTION + Creates a discovery folder based on the starter.store template. + +EXAMPLES + $ yarn faststore init discovery +``` + +_See code: [dist/commands/init.ts](https://github.com/vtex/faststore/blob/v3.0.88/dist/commands/init.ts)_ + +## `faststore start [PATH]` ``` USAGE - $ faststore start + $ faststore start [PATH] + +ARGUMENTS + PATH The path where the FastStore being run is. Defaults to cwd. ``` -_See code: [dist/commands/start.ts](https://github.com/vtex/faststore/blob/v3.0.71/dist/commands/start.ts)_ +_See code: [dist/commands/start.ts](https://github.com/vtex/faststore/blob/v3.0.88/dist/commands/start.ts)_ -## `faststore test` +## `faststore test [PATH]` ``` USAGE - $ faststore test + $ faststore test [PATH] + +ARGUMENTS + PATH The path where the FastStore being tested is. Defaults to cwd. ``` -_See code: [dist/commands/test.ts](https://github.com/vtex/faststore/blob/v3.0.71/dist/commands/test.ts)_ +_See code: [dist/commands/test.ts](https://github.com/vtex/faststore/blob/v3.0.88/dist/commands/test.ts)_ diff --git a/packages/cli/package.json b/packages/cli/package.json index de8c310ebf..c33cc03c93 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/cli", - "version": "3.0.71", + "version": "3.0.88", "description": "FastStore CLI", "author": "Emerson Laurentino @emersonlaurentino", "bin": { @@ -17,19 +17,24 @@ "/oclif.manifest.json" ], "dependencies": { + "@antfu/ni": "^0.21.12", + "@faststore/core": "^3.0.88", + "@inquirer/prompts": "^5.1.2", "@oclif/core": "^1.16.4", "@oclif/plugin-help": "^5", "@oclif/plugin-not-found": "^2.3.3", "chalk": "~4.1.2", "chokidar": "^3.5.3", + "degit": "^2.8.4", "dotenv": "^16.4.5", "fs-extra": "^10.1.0", "path": "^0.12.7" }, "devDependencies": { - "@faststore/eslint-config": "^3.0.68", - "@faststore/shared": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", + "@faststore/shared": "^3.0.88", "@types/chai": "^4", + "@types/degit": "^2.8.6", "@types/fs-extra": "^9.0.13", "@types/node": "^16.11.63", "chai": "^4", @@ -60,8 +65,8 @@ "lint": "eslint src/**/*.ts", "test": "jest src/**/*.test.ts", "postpack": "shx rm -f oclif.manifest.json", - "posttest": "yarn lint", - "prepack": "yarn build && oclif manifest && oclif readme", + "posttest": "na run lint", + "prepack": "na run && oclif manifest && oclif readme", "version": "oclif readme && git add README.md" }, "bugs": "https://github.com/vtex/faststore/issues", diff --git a/packages/cli/src/__mocks__/monorepo/discovery/.gitkeep b/packages/cli/src/__mocks__/monorepo/discovery/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli/src/__mocks__/monorepo/node_modules/@faststore/core/.gitkeep b/packages/cli/src/__mocks__/monorepo/node_modules/@faststore/core/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli/src/__mocks__/store/node_modules/@faststore/core/.gitkeep b/packages/cli/src/__mocks__/store/node_modules/@faststore/core/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index d3500bf934..519a9124ba 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -3,25 +3,42 @@ import chalk from 'chalk' import { spawnSync } from 'child_process' import { existsSync } from 'fs' import { copySync, moveSync, readdirSync, removeSync } from 'fs-extra' -import { tmpDir, userDir } from '../utils/directory' +import { withBasePath } from '../utils/directory' import { generate } from '../utils/generate' +import { getPreferredPackageManager } from '../utils/commands' export default class Build extends Command { + + static args = [ + { + name: 'path', + description: 'The path where the FastStore being built is. Defaults to cwd.', + } + ] + async run() { - await generate({ setup: true }) + const { args } = await this.parse(Build) + + const basePath = args.path ?? process.cwd() + + const { tmpDir } = withBasePath(basePath) - const yarnBuildResult = spawnSync(`yarn build`, { + await generate({ setup: true, basePath }) + + const packageManager = getPreferredPackageManager() + + const buildResult = spawnSync(`${packageManager} run build`, { shell: true, cwd: tmpDir, stdio: 'inherit', }) - if (yarnBuildResult.status && yarnBuildResult.status !== 0) { - process.exit(yarnBuildResult.status) + if (buildResult.status && buildResult.status !== 0) { + process.exit(buildResult.status) } - await normalizeStandaloneBuildDir() - await copyResources() + await normalizeStandaloneBuildDir(basePath) + await copyResources(basePath) } } @@ -42,7 +59,9 @@ async function copyResource(from: string, to: string) { } } -async function normalizeStandaloneBuildDir() { +async function normalizeStandaloneBuildDir(basePath: string) { + const { tmpDir } = withBasePath(basePath) + // Fix Next.js v13+ standalone build output directory if (existsSync(`${tmpDir}/.next/standalone/.faststore`)) { const standaloneBuildFiles = readdirSync( @@ -62,7 +81,9 @@ async function normalizeStandaloneBuildDir() { } } -async function copyResources() { +async function copyResources(basePath: string) { + const { tmpDir, userDir } = withBasePath(basePath) + await copyResource(`${tmpDir}/.next`, `${userDir}/.next`) await copyResource(`${tmpDir}/lighthouserc.js`, `${userDir}/lighthouserc.js`) await copyResource(`${tmpDir}/public`, `${userDir}/public`) diff --git a/packages/cli/src/commands/cms-sync.ts b/packages/cli/src/commands/cms-sync.ts index 8e8c244aad..648a0ac166 100644 --- a/packages/cli/src/commands/cms-sync.ts +++ b/packages/cli/src/commands/cms-sync.ts @@ -1,6 +1,6 @@ import { Command, Flags } from '@oclif/core' import { spawn } from 'child_process' -import { tmpDir } from '../utils/directory' +import { withBasePath } from '../utils/directory' import { generate } from '../utils/generate' import { mergeCMSFiles } from '../utils/hcms' @@ -9,11 +9,22 @@ export default class CmsSync extends Command { ['dry-run']: Flags.boolean({ char: 'd' }), } + static args = [ + { + name: 'path', + description: 'The path where the FastStore being synched with the CMS is. Defaults to cwd.', + } + ] + + async run() { - const { flags } = await this.parse(CmsSync) + const { flags, args } = await this.parse(CmsSync) + + const basePath = args.path ?? process.cwd() + const { tmpDir } = withBasePath(basePath) - await generate({ setup: true }) - await mergeCMSFiles() + await generate({ setup: true, basePath }) + await mergeCMSFiles(basePath) if (flags['dry-run']) { return diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index a68bf39b0b..0ad36dd264 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -1,12 +1,16 @@ import { Command } from '@oclif/core'; import { spawn } from 'child_process'; +import chalk from 'chalk'; import chokidar from 'chokidar'; import dotenv from 'dotenv'; -import { readFileSync } from 'fs'; +import { readFileSync, cpSync } from 'fs'; import path from 'path'; -import { getRoot, tmpDir } from '../utils/directory'; +import { withBasePath } from '../utils/directory'; import { generate } from '../utils/generate'; +import { getPreferredPackageManager } from '../utils/commands'; +import { runCommandSync } from '../utils/runCommandSync'; + /** * Taken from toolbelt @@ -31,10 +35,28 @@ const defaultIgnored = [ const devAbortController = new AbortController() -async function storeDev() { - const envVars = dotenv.parse(readFileSync(path.join(getRoot(), 'vtex.env'))) +async function storeDev(rootDir: string, tmpDir: string, coreDir: string) { + const envVars = dotenv.parse(readFileSync(path.join(rootDir, 'vtex.env'))) + + const packageManager = getPreferredPackageManager() + + runCommandSync({ + cmd: `${packageManager} predev`, + errorMessage: + 'GraphQL was not optimized and TS files were not updated. Changes in the GraphQL layer did not take effect', + throws: 'error', + debug: true, + cwd: tmpDir, + }) - const devProcess = spawn('yarn dev', { + const { success } = copyGenerated(path.join(tmpDir, '@generated'), path.join(coreDir, '@generated')) + + if (!success) { + console.log(`${chalk.yellow('warn')} - Failed to copy @generated schema back to node_modules, autocomplete and DX might be impacted.`) + console.log(`Attempted to copy from ${path.join(tmpDir, '@generated')} to ${path.join(coreDir, '@generated')}`) + } + + const devProcess = spawn(`${packageManager} dev-only`, { shell: true, cwd: tmpDir, signal: devAbortController.signal, @@ -50,12 +72,32 @@ async function storeDev() { }) } +function copyGenerated(from: string, to: string) { + try { + cpSync(from, to, { recursive: true, force: true }) + + return { success: true } + } catch (err) { + return { success: false } + } +} + export default class Dev extends Command { + static args = [ + { + name: 'path', + description: 'The path where the FastStore being run is. Defaults to cwd.', + } + ] + async run() { - const queueChange = (/* path: string, remove: boolean */) => { - // getContentFromPath(path, remove) + const { args } = await this.parse(Dev) + const basePath = args.path ?? process.cwd() - generate() + const { getRoot, tmpDir, coreDir } = withBasePath(basePath) + + const queueChange = (/* path: string, remove: boolean */) => { + generate({ basePath }) } const watcher = chokidar.watch([...defaultPatterns], { @@ -74,9 +116,9 @@ export default class Dev extends Command { watcher.close() }) - await generate({ setup: true }) + await generate({ setup: true, basePath }) - storeDev() + storeDev(getRoot(), tmpDir, coreDir) return await new Promise((resolve, reject) => { watcher diff --git a/packages/cli/src/commands/generate-graphql.ts b/packages/cli/src/commands/generate-graphql.ts index 4bc0dc1f66..8a5257bc80 100644 --- a/packages/cli/src/commands/generate-graphql.ts +++ b/packages/cli/src/commands/generate-graphql.ts @@ -1,9 +1,10 @@ +import chalk from 'chalk' import { Command, Flags } from '@oclif/core' import { existsSync } from 'fs-extra' -import chalk from 'chalk' -import { coreDir, tmpDir } from '../utils/directory' +import { withBasePath } from '../utils/directory' import { runCommandSync } from '../utils/runCommandSync' +import { getPreferredPackageManager } from '../utils/commands' export default class GenerateGraphql extends Command { static flags = { @@ -11,12 +12,24 @@ export default class GenerateGraphql extends Command { core: Flags.boolean({ char: 'c', hidden: true }), } + static args = [ + { + name: 'path', + description: 'The path where the FastStore GraphQL customization is. Defaults to cwd.', + } + ] + async run() { - const { flags } = await this.parse(GenerateGraphql) + const { flags, args } = await this.parse(GenerateGraphql) + + const basePath = args.path ?? process.cwd() + const { tmpDir, coreDir } = withBasePath(basePath) const debug = flags.debug ?? false const isCore = flags.core ?? false + const packageManager = getPreferredPackageManager() + if (!isCore && !existsSync(tmpDir)) { console.log( `${chalk.red( @@ -28,16 +41,15 @@ export default class GenerateGraphql extends Command { } runCommandSync({ - cmd: 'yarn generate:schema', - errorMessage: - "Failed to run 'yarn generate:schema'. Please check your setup.", + cmd: `${packageManager} run generate:schema`, + errorMessage: `Failed to run '${packageManager} generate:schema'. Please check your setup.`, throws: 'error', debug, cwd: isCore ? undefined : tmpDir, }) runCommandSync({ - cmd: 'yarn generate:codegen', + cmd: `${packageManager} run generate:codegen`, errorMessage: 'GraphQL was not optimized and TS files were not updated. Changes in the GraphQL layer did not take effect', throws: 'error', @@ -46,19 +58,19 @@ export default class GenerateGraphql extends Command { }) runCommandSync({ - cmd: 'yarn format:generated', + cmd: `${packageManager} run format:generated`, errorMessage: - "Failed to format generated files. 'yarn format:generated' thrown errors", + `Failed to format generated files. '${packageManager} format:generated' thrown errors`, throws: 'warning', debug, cwd: isCore ? undefined : tmpDir, }) - // yarn generate:copy-back expects the DESTINATION var to be present so it can copy the files to the correct directory + // The command generate:copy-back expects the DESTINATION var to be present so it can copy the files to the correct directory runCommandSync({ - cmd: `DESTINATION=${coreDir} yarn generate:copy-back`, + cmd: `DESTINATION=${coreDir} ${packageManager} run generate:copy-back`, errorMessage: - "Failed to copy back typings files. 'yarn generate:copy-back' thrown errors", + `Failed to copy back typings files. '${packageManager} generate:copy-back' thrown errors`, throws: 'warning', debug, cwd: isCore ? undefined : tmpDir, diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 0000000000..7082c00087 --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -0,0 +1,47 @@ +import fs from 'node:fs' +import degit from 'degit' +import { Command } from '@oclif/core' +import { confirm } from '@inquirer/prompts' + +export default class Init extends Command { + static args = [ + { + name: 'path', + description: + 'The path where the Discovery folder will be created. Defaults to ./discovery.', + }, + ] + + static description = + 'Creates a discovery folder based on the starter.store template.' + + static examples = ['$ yarn faststore init discovery'] + + async run() { + const { args } = await this.parse(Init) + + const discoveryPath = args.path ?? './discovery' + const discoveryFolderExists = fs.existsSync(discoveryPath) + + if (discoveryFolderExists) { + const confirmOverride = await confirm({ + message: `It looks like you already have a discovery folder named "${discoveryPath}" in your store. Do you want to override it?`, + }) + + if (!confirmOverride) + return this.log('🛑 Interrupted initializing discovery') + } + + const discoveryEmitter = degit('vtex-sites/starter.store', { + force: true, + }) + + this.log('Pulling starter.store template...') + + discoveryEmitter.clone(discoveryPath).then(() => { + this.log( + `Discovery created successfully! You can find it at ${discoveryPath}` + ) + }) + } +} diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 8b8d941896..beb85f1d78 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1,17 +1,30 @@ import { Command } from '@oclif/core' import { spawn } from 'child_process' import { existsSync } from 'fs-extra' -import { tmpDir } from '../utils/directory' +import { withBasePath } from '../utils/directory' +import { getPreferredPackageManager } from '../utils/commands' export default class Start extends Command { + static args = [ + { + name: 'path', + description: 'The path where the FastStore being run is. Defaults to cwd.', + } + ] + async run() { + const { args } = await this.parse(Start) + const basePath = args.path ?? process.cwd() + const { tmpDir } = withBasePath(basePath) + const packageManager = getPreferredPackageManager() + if (!existsSync(tmpDir)) { throw Error( 'The ".faststore" directory could not be found. If you are trying to serve your store, run "faststore build" first.' ) } - return spawn(`yarn serve`, { + return spawn(`${packageManager} run serve`, { shell: true, cwd: tmpDir, stdio: 'inherit', diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index defe3607b5..b0e148c2a2 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -3,7 +3,8 @@ import { spawn } from 'child_process' import chokidar from 'chokidar' import { generate } from '../utils/generate' -import { getRoot, tmpDir } from '../utils/directory' +import { withBasePath } from '../utils/directory' +import { getPreferredPackageManager } from '../utils/commands' /** * Taken from toolbelt @@ -28,8 +29,10 @@ const defaultIgnored = [ const testAbortController = new AbortController() -async function storeTest() { - const testProcess = spawn('yarn test:e2e', { +async function storeTest(tmpDir: string) { + const packageManager = getPreferredPackageManager() + + const testProcess = spawn(`${packageManager} run test:e2e`, { shell: true, cwd: tmpDir, signal: testAbortController.signal, @@ -42,7 +45,18 @@ async function storeTest() { } export default class Test extends Command { + static args = [ + { + name: 'path', + description: 'The path where the FastStore being tested is. Defaults to cwd.', + } + ] + async run() { + const { args } = await this.parse(Test) + const basePath = args.path ?? process.cwd() + const { getRoot, tmpDir } = withBasePath(basePath) + const watcher = chokidar.watch([...defaultPatterns], { atomic: stabilityThreshold, awaitWriteFinish: { @@ -59,9 +73,9 @@ export default class Test extends Command { watcher.close() }) - await generate({ setup: true }) + await generate({ setup: true, basePath }) - storeTest() + storeTest(tmpDir) return await new Promise((resolve, reject) => { watcher diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0237950aa4..c551ea6e21 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1 +1,17 @@ export { run } from '@oclif/core' + +import { default as Init } from './commands/init' +import { default as Dev } from './commands/dev' +import { default as Build } from './commands/build' +import { default as Serve } from './commands/start' +import { default as CmsSync } from './commands/cms-sync' +import { default as Test } from './commands/test' + +export const commands = { + init: Init, + dev: Dev, + build: Build, + serve: Serve, + 'cms-sync': CmsSync, + test: Test, +} diff --git a/packages/cli/src/utils/commands.ts b/packages/cli/src/utils/commands.ts new file mode 100644 index 0000000000..478e644dee --- /dev/null +++ b/packages/cli/src/utils/commands.ts @@ -0,0 +1,8 @@ +import { spawnSync } from "node:child_process" + +// Retrieves the package manager based on the developer lockfile, using `ni`. +export function getPreferredPackageManager() { + const agent = spawnSync("na", ['\?'], { encoding: 'utf8' }).stdout.trim() + + return agent +} diff --git a/packages/cli/src/utils/contentFromPath.ts b/packages/cli/src/utils/contentFromPath.ts deleted file mode 100644 index f85e35446b..0000000000 --- a/packages/cli/src/utils/contentFromPath.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { resolve as resolvePath, sep } from 'path' -import { Readable } from 'stream' -import { readFileSync } from 'fs-extra' -import { getRoot } from './directory' - -export interface ContentFromPath { - path: string | null - content: string | Readable | Buffer | NodeJS.ReadableStream -} - -const getContentFromPath = (path: string, remove?: boolean): ContentFromPath => { - const content = remove - ? '' - : readFileSync(resolvePath(getRoot(), path)).toString('base64') - - return { - content, - path: path.split(sep).join('/'), - } -} - -export { getContentFromPath } \ No newline at end of file diff --git a/packages/cli/src/utils/directory.test.ts b/packages/cli/src/utils/directory.test.ts new file mode 100644 index 0000000000..718955efdb --- /dev/null +++ b/packages/cli/src/utils/directory.test.ts @@ -0,0 +1,201 @@ +import path from "path" +import { withBasePath } from "./directory" + +const pathsToMatch = (expected: string, desired: string) => { + const expectedResolved = path.resolve(expected) + const desiredResolved = path.resolve(desired) + + return expectedResolved === desiredResolved +} + +describe('withBasePath as the current dir `.`', () => { + const basePath = '.' + + describe('tmpDir', () => { + it('is the basePath + .faststore', () => { + const { tmpDir: tmpDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpDirWithBase, './.faststore')).toBe(true) + }) + }) + + describe('userDir', () => { + it("returns the directory of the starter's package.json", () => { + const { userSrcDir: userSrcDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userSrcDirWithBase, './src')).toBe(true) + }) + }) + + describe('tmpCustomizationsSrcDir', () => { + it('returns the directory of customizations directory on the tmp dir', () => { + const { tmpCustomizationsSrcDir: tmpCustomizationsSrcDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpCustomizationsSrcDirWithBase, './.faststore/src/customizations/src')).toBe(true) + }) + }) + + describe('userThemesFileDir', () => { + it("returns the directory of the starter's theme directory", () => { + const { userThemesFileDir: userThemesFileDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userThemesFileDirWithBase, './src/themes')).toBe(true) + }) + }) + + describe('tmpThemesCustomizationsFile', () => { + it('returns the path of the theme file on the .faststore dir', () => { + const { tmpThemesCustomizationsFile: tmpThemesCustomizationsFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpThemesCustomizationsFileWithBase, './.faststore/src/customizations/src/themes/index.scss')).toBe(true) + }) + }) + + describe('tmpCMSDir', () => { + it('returns the path of the CMS dir on the .faststore dir', () => { + const { tmpCMSDir: tmpCMSDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpCMSDirWithBase, './.faststore/cms/faststore')).toBe(true) + }) + }) + + describe('userCMSDir', () => { + it('returns the path of the CMS dir on the user dir', () => { + const { userCMSDir: userCMSDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userCMSDirWithBase, './cms/faststore')).toBe(true) + }) + }) + + describe('tmpCMSWebhookUrlsFile', () => { + it('returns the path of the CMS webhooks file on the .faststore dir', () => { + const { tmpCMSWebhookUrlsFile: tmpCMSWebhookUrlsFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpCMSWebhookUrlsFileWithBase, './.faststore/cms-webhook-urls.json')).toBe(true) + }) + }) + + describe('userStoreConfigFile', () => { + it('returns the path of the user faststore.config file', () => { + const { userStoreConfigFile: userStoreConfigFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userStoreConfigFileWithBase, './faststore.config.js')).toBe(true) + }) + }) + + describe('tmpStoreConfigFile', () => { + it('returns the path of the faststore.config file in the customizations dir', () => { + const { tmpStoreConfigFile: tmpStoreConfigFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpStoreConfigFileWithBase, './.faststore/src/customizations/faststore.config.js')).toBe(true) + }) + }) +}) + +describe('withBasePath as an arbitrary dir', () => { + const basePath = path.join(__dirname, '..', '__mocks__', 'store') + + describe('coreDir', () => { + it('is the faststoreDir + core', () => { + const { coreDir: coreDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(coreDirWithBase, './src/__mocks__/store/node_modules/@faststore/core')).toBe(true) + }) + + describe('when is in a monorepo', () => { + it('can look at its parent until it reaches the monorepo directory', () => { + const { coreDir: coreDirWithBase } = withBasePath(path.join(__dirname, '..', '__mocks__', 'monorepo', 'discovery')) + + expect(pathsToMatch(coreDirWithBase, './src/__mocks__/monorepo/node_modules/@faststore/core')).toBe(true) + }) + }) + }) + + describe('tmpDir', () => { + it('is the basePath + .faststore', () => { + const { tmpDir: tmpDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpDirWithBase, './src/__mocks__/store/.faststore')).toBe(true) + }) + }) + + describe('userDir', () => { + it("returns the directory of the starter's package.json", () => { + const { userSrcDir: userSrcDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userSrcDirWithBase, './src/__mocks__/store/src')).toBe(true) + }) + }) + + describe('tmpCustomizationsSrcDir', () => { + it('returns the directory of customizations directory on the tmp dir', () => { + const { tmpCustomizationsSrcDir: tmpCustomizationsSrcDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpCustomizationsSrcDirWithBase, './src/__mocks__/store/.faststore/src/customizations/src')).toBe(true) + }) + }) + + describe('userThemesFileDir', () => { + it("returns the directory of the starter's theme directory", () => { + const { userThemesFileDir: userThemesFileDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userThemesFileDirWithBase, './src/__mocks__/store/src/themes')).toBe(true) + }) + }) + + describe('tmpThemesCustomizationsFile', () => { + it('returns the path of the theme file on the .faststore dir', () => { + const { tmpThemesCustomizationsFile: tmpThemesCustomizationsFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpThemesCustomizationsFileWithBase, './src/__mocks__/store/.faststore/src/customizations/src/themes/index.scss')).toBe(true) + }) + }) + + describe('tmpCMSDir', () => { + it('returns the path of the CMS dir on the .faststore dir', () => { + const { tmpCMSDir: tmpCMSDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpCMSDirWithBase, './src/__mocks__/store/.faststore/cms/faststore')).toBe(true) + }) + }) + + describe('userCMSDir', () => { + it('returns the path of the CMS dir on the user dir', () => { + const { userCMSDir: userCMSDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userCMSDirWithBase, './src/__mocks__/store/cms/faststore')).toBe(true) + }) + }) + + describe('coreCMSDir', () => { + it('returns the path of the CMS dir on @faststore/core package', () => { + const { coreCMSDir: coreCMSDirWithBase } = withBasePath(basePath) + + expect(pathsToMatch(coreCMSDirWithBase, './src/__mocks__/store/node_modules/@faststore/core/cms/faststore')).toBe(true) + }) + }) + + describe('tmpCMSWebhookUrlsFile', () => { + it('returns the path of the CMS webhooks file on the .faststore dir', () => { + const { tmpCMSWebhookUrlsFile: tmpCMSWebhookUrlsFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpCMSWebhookUrlsFileWithBase, './src/__mocks__/store/.faststore/cms-webhook-urls.json')).toBe(true) + }) + }) + + describe('userStoreConfigFile', () => { + it('returns the path of the user faststore.config file', () => { + const { userStoreConfigFile: userStoreConfigFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(userStoreConfigFileWithBase, './src/__mocks__/store/faststore.config.js')).toBe(true) + }) + }) + + describe('tmpStoreConfigFile', () => { + it('returns the path of the faststore.config file in the customizations dir', () => { + const { tmpStoreConfigFile: tmpStoreConfigFileWithBase } = withBasePath(basePath) + + expect(pathsToMatch(tmpStoreConfigFileWithBase, './src/__mocks__/store/.faststore/src/customizations/faststore.config.js')).toBe(true) + }) + }) +}) diff --git a/packages/cli/src/utils/directory.ts b/packages/cli/src/utils/directory.ts index ed251b484d..f331c8ccf1 100644 --- a/packages/cli/src/utils/directory.ts +++ b/packages/cli/src/utils/directory.ts @@ -1,74 +1,68 @@ import path from 'path' +import fs from 'node:fs' -// build folder -export const tmpFolderName = '.faststore' +export const withBasePath = (basepath: string) => { + const tmpFolderName = '.faststore' -// always returns the root of the project, AKA the starter root or @faststore/core dir root when running in the monorepo -export const getRoot = () => { - if (process.env.OCLIF_COMPILATION) { - return '' - } - - if(process.cwd().endsWith(tmpFolderName)) { - // if the current working directory is the build folder (tmp folder), return the starter root - // this makes sure the semantics of the starter root are consistent with the directories declared below - return path.join(process.cwd(), '..') - } + const getRoot = () => { + if (process.env.OCLIF_COMPILATION) { + return '' + } - return process.cwd() -} + if (basepath.endsWith(tmpFolderName)) { + // if the current working directory is the build folder (tmp folder), return the starter root + // this makes sure the semantics of the starter root are consistent with the directories declared below + return path.join(basepath, '..') + } -// starter root -export const userDir = getRoot() + return basepath + } -// node_modules folder for faststorer packages -export const faststoreDir = path.join(userDir, 'node_modules', '@faststore') + /* + * This will loop from the basepath until the process.cwd() looking for node_modules/@faststore/core + * + * If it reaches process.cwd() (or /, as a safeguard), without finding it, it will throw an exception + */ + const getCorePackagePath = () => { + const coreFromNodeModules = path.join('node_modules', '@faststore', 'core') + const resolvedCwd = path.resolve(process.cwd()) -// build folder dir -export const tmpDir = path.join(userDir, tmpFolderName) + const parents: string[] = [] -// node_modules folder for @faststore/core -export const coreFolderName = 'core' -export const coreDir = path.join(faststoreDir, coreFolderName) + let attemptedPath + do { + attemptedPath = path.join(basepath, ...parents, coreFromNodeModules) -// starter src/ folder -export const srcFolderName = 'src' -export const userSrcDir = path.join(userDir, srcFolderName) + if (fs.existsSync(attemptedPath)) { + return attemptedPath + } -// build folder's folder to which starter files should always be copied -export const customizationsFolderName = 'customizations' -// build folder's root folder for starter files -export const tmpCustomizationsDir = path.join(tmpDir, 'src', customizationsFolderName) -// build folder's starter src files -export const tmpCustomizationsSrcDir = path.join(tmpCustomizationsDir, srcFolderName) + parents.push('..') + } while (path.resolve(attemptedPath) !== resolvedCwd || path.resolve(attemptedPath) !== '/') -// starter's folder for themes -export const userThemesFileDir = path.join(userSrcDir, 'themes') -// build folder's dir for theme -export const tmpThemesCustomizationsFileDir = path.join(tmpCustomizationsSrcDir, 'themes', 'index.scss') + throw `Could not find @node_modules on ${basepath} or any of its parents until ${attemptedPath}` + } -// path segment of cms files for faststore -export const cmsFolderName = path.join('cms', 'faststore') -// build folder's cms folder -export const tmpCMSDir = path.join(tmpDir, cmsFolderName) -// node_modules folder for @faststore/core's cms folder -export const coreCMSDir = path.join(coreDir, cmsFolderName) -// starter folder's cms folder -export const userCMSDir = path.join(userDir, cmsFolderName) + const tmpDir = path.join(getRoot(), tmpFolderName) + const userSrcDir = path.join(getRoot(), 'src') -// file name for faststore configs -export const configFileName = 'faststore.config.js' -// starter's config file dir -export const userStoreConfigFileDir = path.join(userDir, configFileName) -// build folder's config file dir -export const tmpStoreConfigFileDir = path.join(tmpCustomizationsDir, configFileName) + return { + getRoot, + userDir: getRoot(), + userSrcDir, + userThemesFileDir: path.join(userSrcDir, 'themes'), + userCMSDir: path.join(getRoot(), 'cms', 'faststore'), + userStoreConfigFile: path.join(getRoot(), 'faststore.config.js'), -// starter's node_modules -export const userNodeModulesDir = path.join(userDir, 'node_modules') -// build folder's node_modules -export const tmpNodeModulesDir = path.join(tmpDir, 'node_modules') + tmpFolderName, + tmpDir, + tmpCustomizationsSrcDir: path.join(tmpDir, 'src', 'customizations', 'src'), + tmpThemesCustomizationsFile: path.join(tmpDir, 'src', 'customizations', 'src', 'themes', 'index.scss'), + tmpCMSDir: path.join(tmpDir, 'cms', 'faststore'), + tmpCMSWebhookUrlsFile: path.join(tmpDir, 'cms-webhook-urls.json'), + tmpStoreConfigFile: path.join(tmpDir, 'src', 'customizations', 'faststore.config.js'), -// cms webhook config file name -export const cmsWebhookUrlsFileName = 'cms-webhook-urls.json' -// build folder's dir for webhook config -export const tmpCmsWebhookUrlsFileDir = path.join(tmpDir, cmsWebhookUrlsFileName) + coreDir: getCorePackagePath(), + coreCMSDir: path.join(getCorePackagePath(), 'cms', 'faststore'), + } +} diff --git a/packages/cli/src/utils/generate.ts b/packages/cli/src/utils/generate.ts index fe6bf9682a..a10075ac81 100644 --- a/packages/cli/src/utils/generate.ts +++ b/packages/cli/src/utils/generate.ts @@ -11,28 +11,19 @@ import { } from 'fs-extra' import path from 'path' -import { - coreDir, - tmpCmsWebhookUrlsFileDir, - tmpCustomizationsSrcDir, - tmpDir, - tmpFolderName, - tmpStoreConfigFileDir, - tmpThemesCustomizationsFileDir, - userDir, - userSrcDir, - userStoreConfigFileDir, - userThemesFileDir, -} from './directory' +import { withBasePath } from './directory' interface GenerateOptions { setup?: boolean + basePath: string } // package.json is copied manually after filtering its content const ignorePaths = ['package.json', 'node_modules', 'cypress.config.ts'] -function createTmpFolder() { +function createTmpFolder(basePath: string) { + const { tmpDir, tmpFolderName } = withBasePath(basePath) + try { if (existsSync(tmpDir)) { removeSync(tmpDir) @@ -54,7 +45,9 @@ function createTmpFolder() { * where sometimes the package.json from the .faststore folder * took precedence over @faststore/core's package.json. */ -function filterAndCopyPackageJson() { +function filterAndCopyPackageJson(basePath: string) { + const { coreDir, tmpDir } = withBasePath(basePath) + const corePackageJsonPath = path.join(coreDir, 'package.json') const corePackageJsonFile = readFileSync(corePackageJsonPath, 'utf8') @@ -67,7 +60,9 @@ function filterAndCopyPackageJson() { }) } -function copyCoreFiles() { +function copyCoreFiles(basePath: string) { + const { coreDir, tmpDir } = withBasePath(basePath) + try { copySync(coreDir, tmpDir, { filter(src) { @@ -80,7 +75,7 @@ function copyCoreFiles() { }, }) - filterAndCopyPackageJson() + filterAndCopyPackageJson(basePath) console.log(`${chalk.green('success')} - Core files copied`) } catch (e) { @@ -88,7 +83,9 @@ function copyCoreFiles() { } } -function copyPublicFiles() { +function copyPublicFiles(basePath: string) { + const { userDir, tmpDir } = withBasePath(basePath) + const allowList = ['json', 'txt', 'xml', 'ico', 'public'] try { if (existsSync(`${userDir}/public`)) { @@ -107,7 +104,9 @@ function copyPublicFiles() { } } -async function copyCypressFiles() { +async function copyCypressFiles(basePath: string) { + const { userDir, userStoreConfigFile, tmpDir } = withBasePath(basePath) + try { // Cypress 9.x config file if (existsSync(`${userDir}/cypress.json`)) { @@ -119,7 +118,7 @@ async function copyCypressFiles() { copySync(`${userDir}/cypress.config.ts`, `${tmpDir}/cypress.config.ts`) } - const userStoreConfig = await import(userStoreConfigFileDir) + const userStoreConfig = await import(path.resolve(userStoreConfigFile)) // Copy custom Cypress folder and files if ( @@ -146,14 +145,16 @@ async function copyCypressFiles() { } } -function copyUserStarterToCustomizations() { +function copyUserStarterToCustomizations(basePath: string) { + const { userSrcDir, tmpCustomizationsSrcDir, userStoreConfigFile, tmpStoreConfigFile } = withBasePath(basePath) + try { if (existsSync(userSrcDir) && readdirSync(userSrcDir).length > 0) { copySync(userSrcDir, tmpCustomizationsSrcDir) } - if (existsSync(userStoreConfigFileDir)) { - copySync(userStoreConfigFileDir, tmpStoreConfigFileDir) + if (existsSync(userStoreConfigFile)) { + copySync(userStoreConfigFile, tmpStoreConfigFile) } console.log(`${chalk.green('success')} - Starter files copied`) @@ -162,8 +163,9 @@ function copyUserStarterToCustomizations() { } } -async function createCmsWebhookUrlsJsonFile() { - const userStoreConfig = await import(userStoreConfigFileDir) +async function createCmsWebhookUrlsJsonFile(basePath: string) { + const { userStoreConfigFile, tmpCMSWebhookUrlsFile } = withBasePath(basePath) + const userStoreConfig = await import(path.resolve(userStoreConfigFile)) if ( userStoreConfig?.vtexHeadlessCms && @@ -173,7 +175,7 @@ async function createCmsWebhookUrlsJsonFile() { try { writeJsonSync( - tmpCmsWebhookUrlsFileDir, + tmpCMSWebhookUrlsFile, { urls: webhookUrls }, { spaces: 2 } ) @@ -186,8 +188,9 @@ async function createCmsWebhookUrlsJsonFile() { } } -async function copyTheme() { - const storeConfig = await import(userStoreConfigFileDir) +async function copyTheme(basePath: string) { + const { userStoreConfigFile, userThemesFileDir, tmpThemesCustomizationsFile } = withBasePath(basePath) + const storeConfig = await import(path.resolve(userStoreConfigFile)) if (storeConfig.theme) { const customTheme = path.join( userThemesFileDir, @@ -195,10 +198,9 @@ async function copyTheme() { ) if (existsSync(customTheme)) { try { - copyFileSync(customTheme, tmpThemesCustomizationsFileDir) + copyFileSync(customTheme, tmpThemesCustomizationsFile) console.log( - `${chalk.green('success')} - ${ - storeConfig.theme + `${chalk.green('success')} - ${storeConfig.theme } theme has been applied` ) } catch (err) { @@ -206,10 +208,8 @@ async function copyTheme() { } } else { console.info( - `${chalk.blue('info')} - The ${ - storeConfig.theme - } theme was added to the config file but the ${ - storeConfig.theme + `${chalk.blue('info')} - The ${storeConfig.theme + } theme was added to the config file but the ${storeConfig.theme }.scss file does not exist in the themes folder. Read more: https://www.faststore.dev/docs/themes/overview` ) } @@ -225,24 +225,24 @@ async function copyTheme() { } } -export async function generate(options?: GenerateOptions) { - const { setup = false } = options ?? {} +export async function generate(options: GenerateOptions) { + const { basePath, setup = false } = options let setupPromise: Promise | null = null if (setup) { setupPromise = Promise.all([ - createTmpFolder(), - copyCoreFiles(), - copyCypressFiles(), - copyPublicFiles(), + createTmpFolder(basePath), + copyCoreFiles(basePath), + copyCypressFiles(basePath), + copyPublicFiles(basePath), ]) } await Promise.all([ setupPromise, - copyUserStarterToCustomizations(), - copyTheme(), - createCmsWebhookUrlsJsonFile(), + copyUserStarterToCustomizations(basePath), + copyTheme(basePath), + createCmsWebhookUrlsJsonFile(basePath), ]) } diff --git a/packages/cli/src/utils/hcms.test.ts b/packages/cli/src/utils/hcms.test.ts index 64e18a03c5..b3e9aaf0ad 100644 --- a/packages/cli/src/utils/hcms.test.ts +++ b/packages/cli/src/utils/hcms.test.ts @@ -15,7 +15,7 @@ import { splitCustomDefinitions, mergeCMSFile, } from './hcms' -import { tmpCMSDir } from './directory' +import { withBasePath } from './directory' jest.mock('fs-extra', () => ({ readFileSync: jest.fn(), @@ -26,11 +26,12 @@ jest.mock('fs-extra', () => ({ describe('mergeCMSFile', () => { it("should create a resulting file that contains all core definitions if a custom definitions file doesn't exist", async () => { const { readFileSync, existsSync, writeFileSync } = require('fs-extra') + const { tmpCMSDir } = withBasePath('.') existsSync.mockReturnValueOnce(false) readFileSync.mockReturnValueOnce(JSON.stringify(coreContentTypes)) - await mergeCMSFile('content-types.json') + await mergeCMSFile('content-types.json', '.') expect(writeFileSync).toHaveBeenCalledWith( path.join(tmpCMSDir, 'content-types.json'), @@ -40,7 +41,7 @@ describe('mergeCMSFile', () => { existsSync.mockReturnValueOnce(false) readFileSync.mockReturnValueOnce(JSON.stringify(coreSections)) - await mergeCMSFile('sections.json') + await mergeCMSFile('sections.json', '.') expect(writeFileSync).toHaveBeenCalledWith( path.join(tmpCMSDir, 'sections.json'), diff --git a/packages/cli/src/utils/hcms.ts b/packages/cli/src/utils/hcms.ts index 8777c11ba8..469c8aef5d 100644 --- a/packages/cli/src/utils/hcms.ts +++ b/packages/cli/src/utils/hcms.ts @@ -3,7 +3,7 @@ import chalk from 'chalk' import { CliUx } from '@oclif/core' import { readFileSync, existsSync, writeFileSync } from 'fs-extra' -import { userCMSDir, coreCMSDir, tmpCMSDir } from './directory' +import { withBasePath } from './directory' export interface ContentTypeOrSectionDefinition { id?: string @@ -96,8 +96,7 @@ async function confirmUserChoice( fileName: string ) { const goAhead = await CliUx.ux.confirm( - `You are about to override default ${ - fileName.split('.')[0] + `You are about to override default ${fileName.split('.')[0] }:\n\n${duplicates .map((definition) => definition.id || definition.name) .join('\n')}\n\nAre you sure? [yes/no]` @@ -110,7 +109,9 @@ async function confirmUserChoice( return } -export async function mergeCMSFile(fileName: string) { +export async function mergeCMSFile(fileName: string, basePath: string) { + const { coreCMSDir, userCMSDir, tmpCMSDir } = withBasePath(basePath) + const coreFilePath = path.join(coreCMSDir, fileName) const customFilePath = path.join(userCMSDir, fileName) @@ -172,10 +173,10 @@ export async function mergeCMSFile(fileName: string) { } } -export async function mergeCMSFiles() { +export async function mergeCMSFiles(basePath: string) { try { - await mergeCMSFile('content-types.json') - await mergeCMSFile('sections.json') + await mergeCMSFile('content-types.json', basePath) + await mergeCMSFile('sections.json', basePath) } catch (err) { console.error(`${chalk.red('error')} - ${err}`) } diff --git a/packages/components/package.json b/packages/components/package.json index f17df36344..d3751b9316 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/components", - "version": "3.0.77", + "version": "3.0.88", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "typings": "dist/esm/index.d.ts", @@ -35,8 +35,8 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@faststore/eslint-config": "^3.0.68", - "@faststore/shared": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", + "@faststore/shared": "^3.0.88", "@testing-library/react": "^14.3.0", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", diff --git a/packages/components/src/molecules/OrderSummary/OrderSummary.tsx b/packages/components/src/molecules/OrderSummary/OrderSummary.tsx index 872471b9ad..cf72d86b83 100644 --- a/packages/components/src/molecules/OrderSummary/OrderSummary.tsx +++ b/packages/components/src/molecules/OrderSummary/OrderSummary.tsx @@ -5,24 +5,24 @@ import { Label, List } from '../../' export interface OrderSummaryProps extends HTMLAttributes { /** - * ID to find this component in testing tools (e.g.: cypress, + * ID to find this component in testing tools (e.g., cypress, * testing-library, and jest). */ testId?: string /** - * Label for the subtotal value of the order. Will only show if subtotalValue is provided. + * Label for the subtotal value of the order. It will only show if subtotalValue is provided. */ subtotalLabel?: string /** - * Subtotal value of the order. If provided, subtotal label and value will be shown. + * Subtotal value of the order. If provided, a subtotal label and value will be shown. */ subtotalValue?: string /** - * Label for the discount value for the order. Will only show if discountValue is provided. + * Label for the discount value for the order. It will only show if discountValue is provided. */ discountLabel?: string /** - * Discount value for the order. If provided, discount label and value will be shown. + * Discount value for the order. If provided, a discount label and value will be shown. */ discountValue?: string /** @@ -34,11 +34,11 @@ export interface OrderSummaryProps extends HTMLAttributes { */ totalValue?: string /** - * Enables to include taxes status. + * Specifies whether the displayed price should include taxes. */ includeTaxes?: boolean /** - * Label to determine if the price is with taxes included. + * Label to determine if the price includes taxes. */ includeTaxesLabel?: string } diff --git a/packages/components/src/molecules/ProductCard/ProductCardContent.tsx b/packages/components/src/molecules/ProductCard/ProductCardContent.tsx index 9e9869f19a..5cffe72bf0 100644 --- a/packages/components/src/molecules/ProductCard/ProductCardContent.tsx +++ b/packages/components/src/molecules/ProductCard/ProductCardContent.tsx @@ -26,7 +26,7 @@ export interface ProductCardContentProps extends HTMLAttributes { */ title: string /** - * Props for the link from ProductCard component. + * Props for the link from the ProductCard component. */ linkProps?: Partial> /** @@ -34,7 +34,7 @@ export interface ProductCardContentProps extends HTMLAttributes { */ price?: PriceDefinition /** - * Enables a outOfStock status. + * Enables an outOfStock status. */ outOfStock?: boolean /** @@ -42,7 +42,7 @@ export interface ProductCardContentProps extends HTMLAttributes { */ outOfStockLabel?: string /** - * Specifies Rating Value of the product. + * Specifies the Rating Value of the product. */ ratingValue?: number /** @@ -54,15 +54,15 @@ export interface ProductCardContentProps extends HTMLAttributes { */ showDiscountBadge?: boolean /** - * Callback function when button is clicked. + * Callback function when the button is clicked. */ onButtonClick?: () => void /** - * Enables to include taxes status. + * Specifies whether the displayed price should include taxes. */ includeTaxes?: boolean /** - * Label to determine if the price is with taxes included. + * Label to determine if the price includes taxes. */ includeTaxesLabel?: string } diff --git a/packages/components/src/molecules/QuantitySelector/QuantitySelector.tsx b/packages/components/src/molecules/QuantitySelector/QuantitySelector.tsx index 8ceccf2771..7253d68cfa 100644 --- a/packages/components/src/molecules/QuantitySelector/QuantitySelector.tsx +++ b/packages/components/src/molecules/QuantitySelector/QuantitySelector.tsx @@ -22,13 +22,13 @@ export interface QuantitySelectorProps { */ initial?: number /** - * Controls by how many units the value advances - */ + * Controls by how many units the value advances + */ unitMultiplier?: number - /** - * Controls wheter you use or not the unitMultiplier - */ - useUnitMultiplier?: boolean + /** + * Controls wheter you use or not the unitMultiplier + */ + useUnitMultiplier?: boolean /** * Specifies that the whole quantity selector component should be disabled. */ @@ -37,6 +37,10 @@ export interface QuantitySelectorProps { * Event emitted when value is changed */ onChange?: (value: number) => void + /** + * Event emitted when value is out of the min and max bounds + */ + onValidateBlur?: (min: number, maxValue: number, quantity: number) => void } const QuantitySelector = ({ @@ -47,21 +51,24 @@ const QuantitySelector = ({ initial, disabled = false, onChange, + onValidateBlur, testId = 'fs-quantity-selector', ...otherProps }: QuantitySelectorProps) => { const [quantity, setQuantity] = useState(initial ?? min) - const [multipliedUnit, setMultipliedUnit] = useState(quantity * unitMultiplier) + const [multipliedUnit, setMultipliedUnit] = useState( + quantity * unitMultiplier + ) const roundUpQuantityIfNeeded = (quantity: number) => { - if(!useUnitMultiplier){ + if (!useUnitMultiplier) { return quantity } - return Math.ceil(quantity / unitMultiplier) * unitMultiplier; - } + return Math.ceil(quantity / unitMultiplier) * unitMultiplier + } const isLeftDisabled = quantity === min - const isRightDisabled = quantity === max + const isRightDisabled = quantity === max const changeQuantity = (increaseValue: number) => { const quantityValue = validateQuantityBounds(quantity + increaseValue) @@ -70,7 +77,7 @@ const QuantitySelector = ({ setQuantity(quantityValue) setMultipliedUnit(quantityValue * unitMultiplier) } - + const increase = () => changeQuantity(1) const decrease = () => changeQuantity(-1) @@ -78,39 +85,37 @@ const QuantitySelector = ({ function validateQuantityBounds(n: number): number { const maxValue = min ? Math.max(n, min) : n - return max ? Math.min(maxValue, useUnitMultiplier ? max * unitMultiplier : max) : maxValue + return max + ? Math.min(maxValue, useUnitMultiplier ? max * unitMultiplier : max) + : maxValue } function validateBlur() { - const roundedQuantity = roundUpQuantityIfNeeded(quantity) + const quantityValue = validateQuantityBounds(quantity) + const roundedQuantity = roundUpQuantityIfNeeded(quantityValue) - setQuantity(() => { - setMultipliedUnit(roundedQuantity) - onChange?.(roundedQuantity / unitMultiplier) - - return roundedQuantity / unitMultiplier - }) - } - - function validateInput(e: React.FormEvent) { - const val = e.currentTarget.value + const maxValue = max ?? (min ? Math.max(quantity, min) : quantity) + const isOutOfBounds = quantity > maxValue || quantity < min + if (isOutOfBounds) { + onValidateBlur?.(min, maxValue, roundedQuantity) + } - if (!Number.isNaN(Number(val))) { - setQuantity(() => { - const quantityValue = validateQuantityBounds(Number(val)) - setMultipliedUnit(quantityValue) - onChange?.(quantityValue) + setQuantity(() => { + setMultipliedUnit(roundedQuantity) + onChange?.(roundedQuantity / unitMultiplier) - return quantityValue - }) - } + return roundedQuantity / unitMultiplier + }) } - useEffect(() => { initial && setQuantity(initial) }, [initial]) + const changeInputValue = (e: React.ChangeEvent) => { + setQuantity(Number(e.currentTarget.value)) + } + return (
diff --git a/packages/components/src/organisms/PaymentMethods/PaymentMethods.tsx b/packages/components/src/organisms/PaymentMethods/PaymentMethods.tsx index f7d2ed8510..5f5977b631 100644 --- a/packages/components/src/organisms/PaymentMethods/PaymentMethods.tsx +++ b/packages/components/src/organisms/PaymentMethods/PaymentMethods.tsx @@ -60,7 +60,7 @@ const PaymentMethods = forwardRef( data-fs-payment-methods-flag key={`fs-payment-method-${index}-${text}`} > - + {text && } ))} diff --git a/packages/core/README.md b/packages/core/README.md index 18680027a0..491cadb3e2 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -14,6 +14,8 @@ This starter ships the main FastStore configuration files to get your store up a 1. **Install dependencies** +> PS: you can install dependencies using the package manager of your choosing. In this guide, we'll be using `yarn` as an example. + Install dependencies with yarn ```shell diff --git a/packages/core/package.json b/packages/core/package.json index 97c86bda79..4ea517501b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/core", - "version": "3.0.77", + "version": "3.0.88", "license": "MIT", "repository": "vtex/faststore", "browserslist": "supports es6-module and not dead", @@ -13,17 +13,19 @@ "scripts": { "generate:schema": "tsx src/server/generator/generateGraphQLSchemaFile.ts", "generate:codegen": "graphql-codegen", - "generate:copy-back": "copyfiles \"@generated/**/*\" $DESTINATION", - "generate": "faststore generate-graphql -c -d", - "build": "yarn partytown & yarn generate && next build", - "dev": "yarn partytown & yarn generate && next dev", + "format:generated": "prettier --write \"@generated/**/*.{ts,js,tsx,jsx,json}\" --loglevel error", + "generate": "na run generate:schema && na run generate:codegen && na run format:generated", + "prebuild": "na run partytown && na run generate", + "build": "next build", + "predev": "na run partytown && na run generate", + "dev": "next dev", + "dev-only": "next dev", "clean": "rm -r .next", "serve": "next start", "test:e2e": "cypress open", "test": "jest", "lhci": "lhci autorun", "format": "prettier --write \"**/*.{ts,js,tsx,jsx,json}\"", - "format:generated": "prettier --write \"@generated/**/*.{ts,js,tsx,jsx,json}\" --loglevel error", "lint": "next lint", "stylelint": "stylelint \"**/*.scss\"", "stylelint:fix": "stylelint \"**/*.scss\" --fix", @@ -35,16 +37,17 @@ }, "sideEffects": false, "dependencies": { + "@antfu/ni": "^0.21.12", "@builder.io/partytown": "^0.6.1", "@envelop/core": "^1.2.0", "@envelop/graphql-jit": "^1.1.1", "@envelop/parser-cache": "^2.2.0", "@envelop/validation-cache": "^2.2.0", - "@faststore/api": "^3.0.76", - "@faststore/components": "^3.0.77", - "@faststore/graphql-utils": "^3.0.68", - "@faststore/sdk": "^3.0.68", - "@faststore/ui": "^3.0.77", + "@faststore/api": "^3.0.88", + "@faststore/components": "^3.0.88", + "@faststore/graphql-utils": "^3.0.88", + "@faststore/sdk": "^3.0.88", + "@faststore/ui": "^3.0.88", "@graphql-codegen/cli": "^5.0.2", "@graphql-codegen/client-preset": "^4.2.6", "@graphql-codegen/typescript": "^4.0.7", @@ -60,7 +63,6 @@ "@vtex/prettier-config": "1.0.0", "autoprefixer": "^10.4.0", "chalk": "^5.2.0", - "copyfiles": "^2.4.1", "css-loader": "^6.7.1", "deepmerge": "^4.3.1", "draftjs-to-html": "^0.9.1", @@ -86,8 +88,7 @@ "devDependencies": { "@cypress/code-coverage": "^3.12.1", "@envelop/testing": "^6.0.0", - "@faststore/cli": "^3.0.71", - "@faststore/eslint-config": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", "@faststore/lighthouse": "^1.12.32", "@lhci/cli": "^0.9.0", "@testing-library/cypress": "^10.0.1", diff --git a/packages/core/postinstall.js b/packages/core/postinstall.js index 90bd383d32..875d8b3138 100644 --- a/packages/core/postinstall.js +++ b/packages/core/postinstall.js @@ -1,6 +1,3 @@ -if ( - process.cwd().includes('node_modules') || - process.cwd().includes('.faststore') -) { +if (__dirname.includes('node_modules') || __dirname.includes('.faststore')) { process.exitCode = 1 } diff --git a/packages/core/public/icons.svg b/packages/core/public/icons.svg index a05940cd63..f0c616e74d 100644 --- a/packages/core/public/icons.svg +++ b/packages/core/public/icons.svg @@ -1,64 +1,128 @@ - + + + + + + + + + + + + + + + + + - + + - + - + + - + + + + - + + + - + + + - - + + + + + + + + + + - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/src/components/ui/ProductDetails/ProductDetailsSettings.tsx b/packages/core/src/components/ui/ProductDetails/ProductDetailsSettings.tsx index ebd7a87e64..97d248ce19 100644 --- a/packages/core/src/components/ui/ProductDetails/ProductDetailsSettings.tsx +++ b/packages/core/src/components/ui/ProductDetails/ProductDetailsSettings.tsx @@ -9,6 +9,7 @@ import { useFormattedPrice } from 'src/sdk/product/useFormattedPrice' import Selectors from 'src/components/ui/SkuSelector' import AddToCartLoadingSkeleton from './AddToCartLoadingSkeleton' +import { Icon as UIIcon, useUI } from '@faststore/ui' import { useOverrideComponents } from 'src/sdk/overrides/OverrideContext' import { Label as UILabel } from '@faststore/ui' @@ -49,6 +50,8 @@ function ProductDetailsSettings({ __experimentalNotAvailableButton: NotAvailableButton, } = useOverrideComponents<'ProductDetails'>() + const { pushToast } = useUI() + const { id, sku, @@ -157,6 +160,21 @@ function ProductDetailsSettings({ // Dynamic props shouldn't be overridable // This decision can be reviewed later if needed onChange={setQuantity} + // TODO: we should get the Toast values from the hCMS + onValidateBlur={( + min: number, + maxValue: number, + quantity: number + ) => { + pushToast({ + title: 'Invalid quantity!', + message: `The quantity you entered is outside the range of ${min} to ${maxValue}. The quantity was set to ${quantity}.`, + status: 'INFO', + icon: ( + + ), + }) + }} /> )} diff --git a/packages/core/src/fonts/WebFonts.tsx b/packages/core/src/fonts/WebFonts.tsx index 4fdff57f81..f4bc0d041c 100644 --- a/packages/core/src/fonts/WebFonts.tsx +++ b/packages/core/src/fonts/WebFonts.tsx @@ -6,7 +6,7 @@ function WebFonts() { {/* Add a tag for your font-family of choice */} {/* */} ) diff --git a/packages/core/src/sdk/analytics/platform/vtex/search.ts b/packages/core/src/sdk/analytics/platform/vtex/search.ts index 2c8f6444a1..57ec3e79f3 100644 --- a/packages/core/src/sdk/analytics/platform/vtex/search.ts +++ b/packages/core/src/sdk/analytics/platform/vtex/search.ts @@ -1,5 +1,5 @@ /** - * More info at: https://www.notion.so/vtexhandbook/Event-API-Documentation-48eee26730cf4d7f80f8fd7262231f84 + * More info at: https://developers.vtex.com/docs/api-reference/intelligent-search-events-api-headless */ import type { AnalyticsEvent } from '@faststore/sdk' import type { @@ -15,33 +15,35 @@ const ONE_YEAR_S = 365 * 24 * 3600 const randomUUID = () => typeof crypto.randomUUID === 'function' - ? crypto.randomUUID() + ? crypto.randomUUID().replaceAll('-', '') : (Math.random() * 1e6).toFixed(0) -const createCookie = (key: string, expiresSecond: number) => { +const createOrRefreshCookie = (key: string, expiresSecond: number) => { // Setting the domain attribute specifies which host can receive it; we need it to make the cookies available on the `secure` subdomain. // Although https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie mentioned leading dot (.) is not needed and ignored. I couldn't set the cookies without it. - const urlDomain = `.${new URL(config.storeUrl).hostname}` + const urlDomain = + process.env.NODE_ENV === 'development' + ? '.localhost' + : `.${new URL(config.storeUrl).hostname}` return () => { - const isExpired = getCookie(key) === undefined + let currentValue = getCookie(key) + const isExpired = currentValue === undefined if (isExpired) { - const value = randomUUID() - - document.cookie = `${key}=${value}; max-age=${expiresSecond}; domain=${urlDomain}; path=/;` - // Setting the `path=/` makes the cookie accessible on any path of the domain/subdomain - - return value + currentValue = randomUUID() } - return getCookie(key) + // Setting the `path=/` makes the cookie accessible on any path of the domain/subdomain + document.cookie = `${key}=${currentValue}; max-age=${expiresSecond}; domain=${urlDomain}; path=/;` + + return currentValue } } const user = { - anonymous: createCookie('vtex-faststore-anonymous', ONE_YEAR_S), - session: createCookie('vtex-faststore-session', THIRTY_MINUTES_S), + anonymous: createOrRefreshCookie('vtex-search-anonymous', ONE_YEAR_S), + session: createOrRefreshCookie('vtex-search-session', THIRTY_MINUTES_S), } type SearchEvent = diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index cf2580ff97..080b3486dc 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/eslint-config", - "version": "3.0.68", + "version": "3.0.88", "main": "index.js", "license": "MIT", "repository": "vtex/faststore", diff --git a/packages/graphql-utils/package.json b/packages/graphql-utils/package.json index 5de5a579d2..8f14121e28 100644 --- a/packages/graphql-utils/package.json +++ b/packages/graphql-utils/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/graphql-utils", - "version": "3.0.68", + "version": "3.0.88", "description": "GraphQL utilities", "repository": { "type": "git", @@ -21,8 +21,8 @@ "graphql": "^15.6.1" }, "devDependencies": { - "@faststore/eslint-config": "^3.0.68", - "@faststore/shared": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", + "@faststore/shared": "^3.0.88", "cross-env": "^7.0.2", "eslint": "7.32.0", "typescript": "^4.2.4" diff --git a/packages/lighthouse/package.json b/packages/lighthouse/package.json index 06201785aa..06cc0b3278 100644 --- a/packages/lighthouse/package.json +++ b/packages/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/lighthouse", - "version": "3.0.68", + "version": "3.0.88", "author": "Emerson Laurentino", "license": "MIT", "repository": { @@ -24,8 +24,8 @@ "lint": "eslint src/**/*.ts" }, "devDependencies": { - "@faststore/eslint-config": "^3.0.68", - "@faststore/shared": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", + "@faststore/shared": "^3.0.88", "eslint": "7.32.0", "typescript": "^4.2.4" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 869d48a1fb..61ceccc3a5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/sdk", - "version": "3.0.68", + "version": "3.0.88", "description": "Hooks for creating your next component library", "license": "MIT", "repository": { @@ -37,7 +37,7 @@ "react": "^18.2.0" }, "devDependencies": { - "@faststore/shared": "^3.0.68", + "@faststore/shared": "^3.0.88", "@size-limit/preset-small-lib": "^7.0.8", "@testing-library/react-hooks": "^8.0.1", "fake-indexeddb": "^3.1.3", diff --git a/packages/sdk/src/search/serializer.ts b/packages/sdk/src/search/serializer.ts index 43861571eb..7d6a5f7c8e 100644 --- a/packages/sdk/src/search/serializer.ts +++ b/packages/sdk/src/search/serializer.ts @@ -3,6 +3,24 @@ import { isSearchSort, setFacet } from './facets' import { initialize } from './useSearchState' import type { SearchSort, State } from '../types' +function getPassThroughSearchParams( + params: URLSearchParams, + denyList: string[] +) { + const passthroughParams = new URLSearchParams() + const denySet = new Set(denyList) + + const entriesArray = Array.from(params.entries()) + + for (const [key, value] of entriesArray) { + if (!denySet.has(key)) { + passthroughParams.append(key, value) + } + } + + return passthroughParams +} + export const parse = ({ pathname, searchParams }: URL): State => { const state = initialize({ base: pathname, @@ -28,5 +46,13 @@ export const parse = ({ pathname, searchParams }: URL): State => { } } + state.passThrough = getPassThroughSearchParams(searchParams, [ + 'q', + 'sort', + 'page', + 'facets', + ...facets, + ]) + return state } diff --git a/packages/sdk/src/search/useSearchState.ts b/packages/sdk/src/search/useSearchState.ts index f2dd122337..c9db5cdba0 100644 --- a/packages/sdk/src/search/useSearchState.ts +++ b/packages/sdk/src/search/useSearchState.ts @@ -9,12 +9,14 @@ export const initialize = ({ term = null, base = '/', page = 0, + passThrough = new URLSearchParams() }: Partial | undefined = {}) => ({ sort, selectedFacets, term, base, page, + passThrough, }) const equals = (s1: State, s2: State) => format(s1).href === format(s2).href diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 4942d42bb6..190dbcb3ef 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -30,4 +30,8 @@ export interface State { * @description current pagination cursor */ page: number + /** + * @description params from other sources to preserve when building URLs + */ + passThrough: URLSearchParams } diff --git a/packages/sdk/src/utils/format.ts b/packages/sdk/src/utils/format.ts index 86c7c065db..0bb0bed01f 100644 --- a/packages/sdk/src/utils/format.ts +++ b/packages/sdk/src/utils/format.ts @@ -2,7 +2,7 @@ import type { State } from '../types' const format = (params: State): URL => { const url = new URL(params.base, 'http://localhost') - const { page, selectedFacets, sort, term } = params + const { page, selectedFacets, sort, term, passThrough } = params if (term !== null) { url.searchParams.set('q', term) @@ -22,6 +22,12 @@ const format = (params: State): URL => { url.searchParams.set('sort', sort) url.searchParams.set('page', page.toString()) +if (passThrough) { + for (const [key, value] of passThrough.entries()) { + url.searchParams.append(key, value) + } + } + return url } diff --git a/packages/sdk/test/search/serializer.test.ts b/packages/sdk/test/search/serializer.test.ts index d62b7d86c3..efeadfecef 100644 --- a/packages/sdk/test/search/serializer.test.ts +++ b/packages/sdk/test/search/serializer.test.ts @@ -90,6 +90,7 @@ test('Search State Serializer: Basic parsing', async () => { sort: 'score_desc', term: 'Hello World', page: 0, + passThrough: new URLSearchParams() }) expect( @@ -104,6 +105,7 @@ test('Search State Serializer: Basic parsing', async () => { sort: 'score_desc', term: 'Hello World', page: 1, + passThrough: new URLSearchParams() }) expect( @@ -116,5 +118,56 @@ test('Search State Serializer: Basic parsing', async () => { sort: 'score_desc', term: 'Hello World', page: 10, + passThrough: new URLSearchParams() }) }) + +test('Search State Serializer: Passthrough param parsing', async () => { + expect( + parseSearchState( + new URL( + 'http://localhost/pt-br/sale?q=Hello+World&&sort=score_desc&price=10%3A100&page=0&facets=price&foo=bar' + ) + ) + ).toEqual({ + base: '/pt-br/sale', + selectedFacets: [ + { + key: 'price', + value: '10:100', + }, + ], + sort: 'score_desc', + term: 'Hello World', + page: 0, + passThrough: new URLSearchParams({ foo: 'bar' }) + }) + + expect( + parseSearchState( + new URL( + 'http://localhost/pt-br/sale?q=Hello+World&sort=score_desc&page=1&foo=bar' + ) + ) + ).toEqual({ + base: '/pt-br/sale', + selectedFacets: [], + sort: 'score_desc', + term: 'Hello World', + page: 1, + passThrough: new URLSearchParams({ foo: 'bar' }) + }) + + expect( + parseSearchState( + new URL('http://localhost?q=Hello+World&sort=score_desc&page=10&foo=bar') + ) + ).toEqual({ + base: '/', + selectedFacets: [], + sort: 'score_desc', + term: 'Hello World', + page: 10, + passThrough: new URLSearchParams({ foo: 'bar' }) + }) +}) \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json index 6118b96d52..4892a7c318 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/shared", - "version": "3.0.68", + "version": "3.0.88", "private": true, "files": [ "tsconfig.json" diff --git a/packages/ui/package.json b/packages/ui/package.json index c8e8f7095e..c08a7dc5ce 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/ui", - "version": "3.0.77", + "version": "3.0.88", "description": "A lightweight, framework agnostic component library for React", "author": "emersonlaurentino", "license": "MIT", @@ -48,7 +48,7 @@ } ], "dependencies": { - "@faststore/components": "^3.0.77", + "@faststore/components": "^3.0.88", "include-media": "^1.4.10", "modern-normalize": "^1.1.0", "react-swipeable": "^7.0.0", @@ -59,8 +59,8 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@faststore/eslint-config": "^3.0.68", - "@faststore/shared": "^3.0.68", + "@faststore/eslint-config": "^3.0.88", + "@faststore/shared": "^3.0.88", "@size-limit/preset-small-lib": "^7.0.8", "@types/tabbable": "^3.1.1", "babel-loader": "^8.2.5", diff --git a/packages/ui/src/components/molecules/InputField/styles.scss b/packages/ui/src/components/molecules/InputField/styles.scss index a3940a08dd..a0f5b90efa 100644 --- a/packages/ui/src/components/molecules/InputField/styles.scss +++ b/packages/ui/src/components/molecules/InputField/styles.scss @@ -18,6 +18,9 @@ --fs-input-field-label-color : var(--fs-color-text-light); --fs-input-field-label-size : var(--fs-text-size-tiny); + // Button + --fs-input-field-button-height : var(--fs-control-tap-size); + // Error --fs-input-field-error-message-size : var(--fs-text-size-legend); --fs-input-field-error-message-line-height : 1.1; @@ -41,17 +44,18 @@ display: flex; flex-flow: column; - input, - label { + [data-fs-input], + [data-fs-label] { transition: var(--fs-input-field-transition-property) var(--fs-input-field-transition-timing) var(--fs-input-field-transition-function); } - input { + [data-fs-input] { + --fs-input-padding: var(--fs-input-field-padding); padding: var(--fs-input-field-padding); color: var(--fs-input-field-color); &:placeholder-shown + label { - top: calc(1.5 * (var(--fs-input-field-size) / 2)); + top: calc(50% - (var(--fs-input-field-size) / 2)); overflow: hidden; } @@ -76,11 +80,11 @@ } } - label { + [data-fs-label] { position: absolute; padding: var(--fs-input-field-label-padding); font-size: var(--fs-input-field-size); - line-height: 1.5; + line-height: var(--fs-input-field-size); color: var(--fs-input-field-label-color); } @@ -89,11 +93,12 @@ // -------------------------------------------------------- &[data-fs-input-field-error="true"] { - input { + [data-fs-input] { border-color: var(--fs-input-field-error-border-color); - - @include input-focus-ring($outline: #{var(--fs-input-field-error-focus-ring)}, - $border: #{var(--fs-input-field-error-border-color)}); + @include input-focus-ring( + $outline: #{var(--fs-input-field-error-focus-ring)}, + $border: #{var(--fs-input-field-error-border-color)} + ); &:hover:not(:disabled):not(:focus-visible):not(:focus) { border-color: var(--fs-input-field-error-border-color); @@ -112,14 +117,13 @@ &[data-fs-input-field-actionable="true"] { min-width: rem(250px); - input { - padding-right: var(--fs-spacing-13); - } + [data-fs-input] { padding-right: var(--fs-spacing-13); } [data-fs-button] { position: absolute; top: 0; right: 0; + height: var(--fs-input-field-button-height); [data-fs-button-wrapper]::before { position: absolute; diff --git a/packages/ui/src/components/molecules/OrderSummary/styles.scss b/packages/ui/src/components/molecules/OrderSummary/styles.scss index 618f4635cf..1e04684a9c 100644 --- a/packages/ui/src/components/molecules/OrderSummary/styles.scss +++ b/packages/ui/src/components/molecules/OrderSummary/styles.scss @@ -6,6 +6,7 @@ // Default properties --fs-order-summary-padding : var(--fs-spacing-3); --fs-order-summary-margin-bottom : var(--fs-spacing-2); + --fs-order-summary-row-gap : 0; --fs-order-summary-discount-text-color : var(--fs-color-success-text); @@ -20,7 +21,10 @@ // -------------------------------------------------------- // Structural Styles // -------------------------------------------------------- + display: flex; + flex-direction: column; padding: var(--fs-order-summary-padding); + row-gap: var(--fs-order-summary-row-gap); li { display: flex; diff --git a/packages/ui/src/components/molecules/ProductCard/styles.scss b/packages/ui/src/components/molecules/ProductCard/styles.scss index c194b643d3..e968b01be2 100644 --- a/packages/ui/src/components/molecules/ProductCard/styles.scss +++ b/packages/ui/src/components/molecules/ProductCard/styles.scss @@ -39,7 +39,8 @@ --fs-product-card-price-size : var(--fs-text-size-base); // Out Of Stock - --fs-product-card-out-of-stock-bkg-color : var(--fs-color-disabled-bkg); + --fs-product-card-out-of-stock-bkg-color : transparent; + --fs-product-card-out-of-stock-border-color: var(--fs-color-neutral-1); --fs-product-card-out-of-stock-img-opacity : .5; // Taxes label @@ -168,7 +169,8 @@ // -------------------------------------------------------- &[data-fs-product-card="out-of-stock"] { - --fs-product-card-bkg-color : var(--fs-product-card-out-of-stock-bkg-color); + --fs-product-card-bkg-color : var(--fs-product-card-out-of-stock-bkg-color); + --fs-product-card-border-color : var(--fs-product-card-out-of-stock-border-color); [data-fs-product-card-image] { opacity: var(--fs-product-card-out-of-stock-img-opacity); } diff --git a/packages/ui/src/components/molecules/QuantitySelector/styles.scss b/packages/ui/src/components/molecules/QuantitySelector/styles.scss index 49a8c87033..55dc72e09c 100644 --- a/packages/ui/src/components/molecules/QuantitySelector/styles.scss +++ b/packages/ui/src/components/molecules/QuantitySelector/styles.scss @@ -42,7 +42,6 @@ justify-content: center; width: var(--fs-qty-selector-width); height: var(--fs-qty-selector-height); - border-radius: var(--fs-qty-selector-border-radius); box-shadow: var(--fs-qty-selector-shadow); [data-quantity-selector-input] { @@ -53,6 +52,7 @@ color: var(--fs-qty-selector-text-color); text-align: center; border: var(--fs-qty-selector-border-width) solid var(--fs-qty-selector-border-color); + border-radius: var(--fs-qty-selector-border-radius); } [data-quantity-selector-button] { diff --git a/packages/ui/src/components/molecules/Toast/styles.scss b/packages/ui/src/components/molecules/Toast/styles.scss index 96703c233d..2dd22bfe19 100644 --- a/packages/ui/src/components/molecules/Toast/styles.scss +++ b/packages/ui/src/components/molecules/Toast/styles.scss @@ -32,7 +32,7 @@ // Title --fs-toast-title-size : var(--fs-text-size-body); --fs-toast-title-weight : var(--fs-text-weight-bold); - --fs-toast-title-line-height : var(--fs-scale); + --fs-toast-title-line-height : 1.2; --fs-toast-title-margin-left : var(--fs-spacing-3); // Message diff --git a/packages/ui/src/components/organisms/PaymentMethods/styles.scss b/packages/ui/src/components/organisms/PaymentMethods/styles.scss index 2aa787e578..a5376d0ea1 100644 --- a/packages/ui/src/components/organisms/PaymentMethods/styles.scss +++ b/packages/ui/src/components/organisms/PaymentMethods/styles.scss @@ -13,6 +13,7 @@ // Flag --fs-payment-methods-flag-width : var(--fs-spacing-5); --fs-payment-methods-flag-height : var(--fs-spacing-4); + --fs-payment-methods-flag-bkg-color : var(--fs-color-neutral-0); --fs-payment-methods-flag-border-width : var(--fs-border-width); --fs-payment-methods-flag-border-color : var(--fs-color-neutral-3); --fs-payment-methods-flag-border-radius : var(--fs-border-radius-small); @@ -44,21 +45,20 @@ justify-content: space-between; margin-top: var(--fs-payment-methods-flags-margin-top); - li { - display: flex; - place-content: center; - } - @include media("