diff --git a/.changeset/fifty-melons-film.md b/.changeset/fifty-melons-film.md new file mode 100644 index 0000000000..a6a8c62621 --- /dev/null +++ b/.changeset/fifty-melons-film.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-hydrogen': patch +--- + +Fix CLI upgrade notification when running from a globla process. diff --git a/.changeset/wet-apricots-fix.md b/.changeset/wet-apricots-fix.md new file mode 100644 index 0000000000..7424b2b9a5 --- /dev/null +++ b/.changeset/wet-apricots-fix.md @@ -0,0 +1,5 @@ +--- +'@shopify/create-hydrogen': major +--- + +The code is now bundled to enhance installation speed. diff --git a/package-lock.json b/package-lock.json index d51971c1cc..c3463a2abc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31359,10 +31359,14 @@ "version": "4.3.10", "license": "MIT", "dependencies": { - "@shopify/cli-hydrogen": "^8.1.0" + "@ast-grep/napi": "0.11.0" }, "bin": { "create-hydrogen": "dist/create-app.js" + }, + "devDependencies": { + "@shopify/cli-hydrogen": "^8.1.0", + "tempy": "3.0.0" } }, "packages/hydrogen": { @@ -39678,7 +39682,9 @@ "@shopify/create-hydrogen": { "version": "file:packages/create-hydrogen", "requires": { - "@shopify/cli-hydrogen": "^8.1.0" + "@ast-grep/napi": "0.11.0", + "@shopify/cli-hydrogen": "^8.1.0", + "tempy": "3.0.0" } }, "@shopify/generate-docs": { diff --git a/package.json b/package.json index 7b8057fea2..c3294539cb 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "build:all": "npm run build:pkg && npm run build:templates && npm run build:examples", "ci:checks": "turbo run lint test format:check typecheck", "dev": "npm run dev:pkg", - "dev:pkg": "cross-env LOCAL_DEV=true turbo dev --parallel --filter=./packages/*", - "dev:app": "cd templates/skeleton && cross-env LOCAL_DEV=true npm run dev --", + "dev:pkg": "turbo dev --parallel --filter=./packages/*", + "dev:app": "cd templates/skeleton && npm run dev --", "docs:build": "turbo run build-docs", "docs:preview": "turbo run preview-docs", "lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx ./packages", diff --git a/packages/cli/src/commands/hydrogen/init.test.ts b/packages/cli/src/commands/hydrogen/init.test.ts index b50459103e..fb20aee113 100644 --- a/packages/cli/src/commands/hydrogen/init.test.ts +++ b/packages/cli/src/commands/hydrogen/init.test.ts @@ -5,12 +5,12 @@ import {exec} from '@shopify/cli-kit/node/system'; import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'; import {fileExists, readFile, removeFile} from '@shopify/cli-kit/node/fs'; import {temporaryDirectory} from 'tempy'; -import {checkHydrogenVersion} from '../../lib/check-version.js'; +import {checkCurrentCLIVersion} from '../../lib/check-cli-version.js'; import {runCheckRoutes} from './check.js'; import {runCodegen} from './codegen.js'; import {setupTemplate} from '../../lib/onboarding/index.js'; -vi.mock('../../lib/check-version.js'); +vi.mock('../../lib/check-cli-version.js'); vi.mock('../../lib/onboarding/index.js', async () => { const original = await vi.importActual< @@ -32,21 +32,19 @@ describe('init', () => { outputMock.clear(); }); - it('checks Hydrogen version', async () => { + it('checks Hydrogen CLI version', async () => { const showUpgradeMock = vi.fn((param?: string) => ({ currentVersion: '1.0.0', newVersion: '1.0.1', })); - vi.mocked(checkHydrogenVersion).mockResolvedValueOnce(showUpgradeMock); + vi.mocked(checkCurrentCLIVersion).mockResolvedValueOnce(showUpgradeMock); vi.mocked(setupTemplate).mockResolvedValueOnce(undefined); - const project = await runInit(); + const project = await runInit({packageManager: 'pnpm'}); expect(project).toBeFalsy(); - expect(checkHydrogenVersion).toHaveBeenCalledOnce(); - expect(showUpgradeMock).toHaveBeenCalledWith( - expect.stringContaining('npm create @shopify/hydrogen@latest'), - ); + expect(checkCurrentCLIVersion).toHaveBeenCalledOnce(); + expect(showUpgradeMock).toHaveBeenCalledWith('pnpm'); }); it('scaffolds Quickstart project with expected values', async () => { diff --git a/packages/cli/src/commands/hydrogen/init.ts b/packages/cli/src/commands/hydrogen/init.ts index 425afa9b6e..c1f0762beb 100644 --- a/packages/cli/src/commands/hydrogen/init.ts +++ b/packages/cli/src/commands/hydrogen/init.ts @@ -1,9 +1,4 @@ import Command from '@shopify/cli-kit/node/base-command'; -import {fileURLToPath} from 'node:url'; -import { - packageManager, - packageManagerFromUserAgent, -} from '@shopify/cli-kit/node/node-package-manager'; import {Flags} from '@oclif/core'; import {AbortError} from '@shopify/cli-kit/node/error'; import { @@ -11,16 +6,11 @@ import { parseProcessFlags, flagsToCamelObject, } from '../../lib/flags.js'; -import {checkHydrogenVersion} from '../../lib/check-version.js'; +import {checkCurrentCLIVersion} from '../../lib/check-cli-version.js'; import {I18N_CHOICES, type I18nChoice} from '../../lib/setups/i18n/index.js'; -import {execAsync, supressNodeExperimentalWarnings} from '../../lib/process.js'; +import {supressNodeExperimentalWarnings} from '../../lib/process.js'; import {setupTemplate, type InitOptions} from '../../lib/onboarding/index.js'; import {LANGUAGES} from '../../lib/onboarding/common.js'; -import { - currentProcessIsGlobal, - inferPackageManagerForGlobalCLI, -} from '@shopify/cli-kit/node/is-global'; -import {getPkgJsonPath} from '../../lib/build.js'; const FLAG_MAP = {f: 'force'} as Record; @@ -121,36 +111,9 @@ export async function runInit( options.shortcut ??= true; } - // Check if we are running the command using the h2 alias - const npmPrefix = (await execAsync('npm prefix -s')).stdout.trim(); - const isH2 = process.argv[1]?.startsWith(npmPrefix); - - // If the current process is global (shopify hydrogen init) we need to check for @shopify/cli version - // The process could report to be global when using the h2 alias, so we need to check for that - const isGlobal = currentProcessIsGlobal() && !isH2; - - const showUpgrade = await checkHydrogenVersion( - // Resolving the CLI package from a local directory might fail because - // this code could be run from a global dependency (e.g. on `npm create`). - // Therefore, pass the known path to the package.json directly from here: - await getPkgJsonPath(), - isGlobal ? 'cli' : 'cliHydrogen', - ); - + const showUpgrade = await checkCurrentCLIVersion(); if (showUpgrade) { - let packageManager = - options.packageManager ?? packageManagerFromUserAgent(); - if (packageManager === 'unknown' || !packageManager) { - packageManager = inferPackageManagerForGlobalCLI(); - } - const globalInstallCommand = - packageManager === 'yarn' - ? `yarn global add @shopify/cli` - : `${packageManager} install -g @shopify/cli`; - const globalMessage = `Please install the latest Shopify CLI version with \`${globalInstallCommand}\` and try again.`; - const localMessage = `Please use the latest version with \`${packageManager} create @shopify/hydrogen@latest\``; - const message = isGlobal ? globalMessage : localMessage; - showUpgrade(message); + showUpgrade(options.packageManager); } return setupTemplate(options); diff --git a/packages/cli/src/lib/check-cli-version.test.ts b/packages/cli/src/lib/check-cli-version.test.ts new file mode 100644 index 0000000000..5cabd90fb1 --- /dev/null +++ b/packages/cli/src/lib/check-cli-version.test.ts @@ -0,0 +1,125 @@ +import { + checkCurrentCLIVersion, + UPGRADABLE_CLI_NAMES, +} from './check-cli-version.js'; +import {afterEach, beforeEach, describe, it, expect, vi} from 'vitest'; +import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'; +import { + checkForNewVersion, + findUpAndReadPackageJson, +} from '@shopify/cli-kit/node/node-package-manager'; + +vi.mock('@shopify/cli-kit/node/node-package-manager', () => { + return { + checkForNewVersion: vi.fn(), + packageManagerFromUserAgent: vi.fn(() => 'npm'), + findUpAndReadPackageJson: vi.fn(() => + Promise.resolve({ + path: '', + content: { + name: UPGRADABLE_CLI_NAMES.cliHydrogen, + version: '8.0.0', + }, + }), + ), + }; +}); + +describe('checkHydrogenVersion()', () => { + const outputMock = mockAndCaptureOutput(); + + afterEach(() => { + vi.restoreAllMocks(); + outputMock.clear(); + }); + + describe('when a current version is available', () => { + it('calls checkForNewVersion', async () => { + await checkCurrentCLIVersion(); + + expect(checkForNewVersion).toHaveBeenCalledWith( + UPGRADABLE_CLI_NAMES.cliHydrogen, + expect.stringMatching(/\d{1,2}\.\d{1,2}\.\d{1,2}/), + ); + }); + + describe('and it is up to date', () => { + beforeEach(() => { + vi.mocked(checkForNewVersion).mockResolvedValue(undefined); + }); + + it('returns undefined', async () => { + expect(await checkCurrentCLIVersion()).toBe(undefined); + }); + }); + + describe('and it is using @next or @exprimental', () => { + it('returns undefined', async () => { + vi.mocked(checkForNewVersion).mockResolvedValue('8.0.0'); + vi.mocked(findUpAndReadPackageJson).mockResolvedValueOnce({ + path: '', + content: { + name: UPGRADABLE_CLI_NAMES.cliHydrogen, + version: '0.0.0-next-a188915-20230713115118', + }, + }); + + expect(await checkCurrentCLIVersion()).toBe(undefined); + + vi.mocked(findUpAndReadPackageJson).mockResolvedValueOnce({ + path: '', + content: { + name: UPGRADABLE_CLI_NAMES.cliHydrogen, + version: '0.0.0-experimental-a188915-20230713115118', + }, + }); + + expect(await checkCurrentCLIVersion()).toBe(undefined); + }); + }); + + describe('and a new version is available', () => { + beforeEach(() => { + vi.mocked(checkForNewVersion).mockResolvedValue('9.0.0'); + }); + + it('returns a function that prints the upgrade', async () => { + const showUpgrade = await checkCurrentCLIVersion(); + expect(showUpgrade).toBeInstanceOf(Function); + + showUpgrade!(); + + expect(outputMock.info()).toMatch( + / info .+ Upgrade available .+ Version 9.0.0.+ running v8.0.0.+`npm create @shopify\/hydrogen@latest`/is, + ); + }); + + it('outputs a message to the user with the new version', async () => { + const showUpgrade = await checkCurrentCLIVersion(); + const {currentVersion, newVersion} = showUpgrade!(); + + expect(outputMock.info()).toMatch( + new RegExp( + ` info .+ Upgrade available .+ Version ${newVersion.replaceAll( + '.', + '\\.', + )}.+ running v${currentVersion.replaceAll('.', '\\.')}`, + 'is', + ), + ); + }); + }); + }); + + describe('when no current version can be found', () => { + beforeEach(() => { + vi.mocked(findUpAndReadPackageJson).mockRejectedValue(new Error()); + }); + + it('returns undefined and does not call checkForNewVersion', async () => { + expect(await checkCurrentCLIVersion()).toBe(undefined); + + expect(checkForNewVersion).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/cli/src/lib/check-cli-version.ts b/packages/cli/src/lib/check-cli-version.ts new file mode 100644 index 0000000000..ea5d561166 --- /dev/null +++ b/packages/cli/src/lib/check-cli-version.ts @@ -0,0 +1,94 @@ +import {fileURLToPath} from 'node:url'; +import { + type PackageManager, + checkForNewVersion, + findUpAndReadPackageJson, + packageManagerFromUserAgent, +} from '@shopify/cli-kit/node/node-package-manager'; +import {renderInfo} from '@shopify/cli-kit/node/ui'; +import {} from '@shopify/cli-kit/node/path'; +import {isHydrogenMonorepo} from './build.js'; +import {inferPackageManagerForGlobalCLI} from '@shopify/cli-kit/node/is-global'; + +export const UPGRADABLE_CLI_NAMES = { + cli: '@shopify/cli', + cliHydrogen: '@shopify/cli-hydrogen', + createApp: '@shopify/create-hydrogen', +} as const; + +/** + * Checks if a new version of the current package is available. + * @returns A function to show the update information if any update is available. + */ +export async function checkCurrentCLIVersion() { + if (isHydrogenMonorepo && !process.env.SHOPIFY_UNIT_TEST) return; + + const {content: pkgJson} = await findUpAndReadPackageJson( + fileURLToPath(import.meta.url), + ).catch(() => ({content: undefined})); + + const pkgName = pkgJson?.name; + const currentVersion = pkgJson?.version; + + if ( + !pkgName || + !currentVersion || + !Object.values(UPGRADABLE_CLI_NAMES).some((name) => name === pkgName) || + currentVersion.includes('next') || + currentVersion.includes('experimental') + ) { + return; + } + + const newVersionAvailable = await checkForNewVersion(pkgName, currentVersion); + + if (!newVersionAvailable) return; + + const reference = [ + { + link: { + label: 'Hydrogen releases', + url: 'https://github.com/Shopify/hydrogen/releases', + }, + }, + ]; + + if (pkgName === UPGRADABLE_CLI_NAMES.cli) { + reference.push({ + link: { + label: 'Global CLI reference', + url: 'https://shopify.dev/docs/api/shopify-cli/', + }, + }); + } + + return (packageManager?: PackageManager) => { + packageManager ??= packageManagerFromUserAgent(); + if (packageManager === 'unknown' || !packageManager) { + packageManager = inferPackageManagerForGlobalCLI(); + } + if (packageManager === 'unknown') { + packageManager = 'npm'; + } + + const installMessage = + pkgName === UPGRADABLE_CLI_NAMES.cli + ? `Please install the latest Shopify CLI version with \`${ + packageManager === 'yarn' + ? `yarn global add ${UPGRADABLE_CLI_NAMES.cli}` + : `${packageManager} install -g ${UPGRADABLE_CLI_NAMES.cli}` + }\` and try again.` + : `Please use the latest version with \`${packageManager} create @shopify/hydrogen@latest\``; + + renderInfo({ + headline: 'Upgrade available', + body: + `Version ${newVersionAvailable} of ${pkgName} is now available.\n` + + `You are currently running v${currentVersion}.\n\n` + + installMessage, + reference, + }); + + return {currentVersion, newVersion: newVersionAvailable}; + }; +} diff --git a/packages/cli/src/lib/check-lockfile.ts b/packages/cli/src/lib/check-lockfile.ts index ecb41c38b2..a9d167d26e 100644 --- a/packages/cli/src/lib/check-lockfile.ts +++ b/packages/cli/src/lib/check-lockfile.ts @@ -4,6 +4,7 @@ import {checkIfIgnoredInGitRepository} from '@shopify/cli-kit/node/git'; import {renderWarning} from '@shopify/cli-kit/node/ui'; import {AbortError} from '@shopify/cli-kit/node/error'; import {packageManagers, type PackageManager} from './package-managers.js'; +import {isHydrogenMonorepo} from './build.js'; function missingLockfileWarning(shouldExit: boolean) { const headline = 'No lockfile found'; @@ -73,7 +74,7 @@ export async function checkLockfileStatus( directory: string, shouldExit = false, ) { - if (process.env.LOCAL_DEV) return; + if (isHydrogenMonorepo && !process.env.SHOPIFY_UNIT_TEST) return; const foundPackageManagers: PackageManager[] = []; for (const packageManager of packageManagers) { diff --git a/packages/cli/src/lib/check-version.test.ts b/packages/cli/src/lib/check-version.test.ts deleted file mode 100644 index f8f1f19d5c..0000000000 --- a/packages/cli/src/lib/check-version.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import {checkHydrogenVersion} from './check-version.js'; -import {afterEach, beforeEach, describe, it, expect, vi} from 'vitest'; -import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'; -import {checkForNewVersion} from '@shopify/cli-kit/node/node-package-manager'; -import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'; - -vi.mock('@shopify/cli-kit/node/node-package-manager', () => { - return { - checkForNewVersion: vi.fn(), - }; -}); - -const requireMock = vi.fn(); -vi.mock('node:module', async () => { - const {createRequire} = await vi.importActual( - 'node:module', - ); - - return { - createRequire: (url: string) => { - const actualRequire = createRequire(url); - requireMock.mockImplementation((mod: string) => actualRequire(mod)); - const require = requireMock as unknown as typeof actualRequire; - require.resolve = actualRequire.resolve.bind(actualRequire); - - return require; - }, - }; -}); - -describe('checkHydrogenVersion()', () => { - const outputMock = mockAndCaptureOutput(); - - afterEach(() => { - vi.restoreAllMocks(); - outputMock.clear(); - }); - - describe('when a current version is available', () => { - it('calls checkForNewVersion', async () => { - await checkHydrogenVersion('dir'); - - expect(checkForNewVersion).toHaveBeenCalledWith( - '@shopify/hydrogen', - // Calver - expect.stringMatching(/20\d{2}\.\d{1,2}\.\d{1,3}/), - ); - }); - - describe('and it is up to date', () => { - beforeEach(() => { - vi.mocked(checkForNewVersion).mockResolvedValue(undefined); - }); - - it('returns undefined', async () => { - expect(await checkHydrogenVersion('dir')).toBe(undefined); - }); - }); - - describe('and it is using @next', () => { - it('returns undefined', async () => { - vi.mocked(checkForNewVersion).mockResolvedValue('2023.1.5'); - vi.mocked(requireMock).mockReturnValueOnce({ - version: '0.0.0-next-a188915-20230713115118', - }); - - expect(await checkHydrogenVersion('dir')).toBe(undefined); - }); - }); - - describe('and a new version is available', () => { - beforeEach(() => { - vi.mocked(checkForNewVersion).mockResolvedValue('2023.1.5'); - }); - - it('returns a function', async () => { - expect(await checkHydrogenVersion('dir')).toBeInstanceOf(Function); - }); - - it('outputs a message to the user with the new version', async () => { - const showUpgrade = await checkHydrogenVersion('dir'); - const {currentVersion, newVersion} = showUpgrade!(); - - expect(outputMock.info()).toMatch( - new RegExp( - ` info .+ Upgrade available .+ Version ${newVersion.replaceAll( - '.', - '\\.', - )}.+ running v${currentVersion.replaceAll('.', '\\.')}`, - 'is', - ), - ); - }); - }); - }); - - describe('when checking the global cli version', () => { - it('uses the CLI_KIT_VERSION', async () => { - await checkHydrogenVersion('dir', 'cli'); - - expect(checkForNewVersion).toHaveBeenCalledWith( - '@shopify/cli', - CLI_KIT_VERSION, - ); - }); - }); - - describe('when no current version can be found', () => { - it('returns undefined and does not call checkForNewVersion', async () => { - expect(await checkHydrogenVersion('/fake-absolute-dir')).toBe(undefined); - - expect(checkForNewVersion).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/cli/src/lib/check-version.ts b/packages/cli/src/lib/check-version.ts deleted file mode 100644 index ddc8cfa449..0000000000 --- a/packages/cli/src/lib/check-version.ts +++ /dev/null @@ -1,103 +0,0 @@ -import path from 'node:path'; -import {createRequire} from 'node:module'; -import {checkForNewVersion} from '@shopify/cli-kit/node/node-package-manager'; -import {renderInfo} from '@shopify/cli-kit/node/ui'; -import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'; - -const PACKAGE_NAMES = { - main: '@shopify/hydrogen', - cliHydrogen: '@shopify/cli-hydrogen', - cli: '@shopify/cli', -} as const; - -/** - * - * @param resolveFrom Path to a directory to resolve from, or directly the path to a package.json file. - * @param pkgKey Package to check for updates. - * @returns A function to show the update information if any update is available. - */ -export async function checkHydrogenVersion( - resolveFrom: string, - pkgKey: keyof typeof PACKAGE_NAMES = 'main', -) { - if (process.env.LOCAL_DEV) return; - const pkgName = PACKAGE_NAMES[pkgKey]; - - let currentVersion: string | undefined; - if (pkgName === PACKAGE_NAMES.cli) { - currentVersion = CLI_KIT_VERSION; - } else { - currentVersion = getCurrentVersionFromPackageJson(pkgName, resolveFrom); - } - - if (!currentVersion || currentVersion.includes('next')) return; - - const newVersionAvailable = await checkForNewVersion(pkgName, currentVersion); - - if (!newVersionAvailable) return; - - const references = [ - { - link: { - label: 'Hydrogen releases', - url: 'https://github.com/Shopify/hydrogen/releases', - }, - }, - ]; - - if (pkgName === PACKAGE_NAMES.cli) { - references.push({ - link: { - label: 'Global CLI reference', - url: 'https://shopify.dev/docs/api/shopify-cli/', - }, - }); - } - - return (extraMessage = '') => { - renderInfo({ - headline: 'Upgrade available', - body: - `Version ${newVersionAvailable} of ${pkgName} is now available.\n` + - `You are currently running v${currentVersion}.` + - (extraMessage ? '\n\n' : '') + - extraMessage, - reference: references, - }); - - return {currentVersion, newVersion: newVersionAvailable}; - }; -} - -function getCurrentVersionFromPackageJson( - pkgName: string, - resolveFrom: string, -) { - const require = createRequire(import.meta.url); - const pkgJsonPath = resolveFrom.endsWith('package.json') - ? locateDependency(require, resolveFrom) - : locateDependency( - require, - path.join(pkgName, 'package.json'), - resolveFrom, - ); - - if (!pkgJsonPath) return; - - const currentVersion = require(pkgJsonPath).version as string; - return currentVersion; -} - -function locateDependency( - require: NodeRequire, - nameToResolve: string, - resolveFrom?: string, -) { - try { - return require.resolve(nameToResolve, { - paths: [resolveFrom ?? process.cwd()], - }); - } catch { - return; - } -} diff --git a/packages/cli/src/lib/template-downloader.ts b/packages/cli/src/lib/template-downloader.ts index 934d1f217c..2c529302cb 100644 --- a/packages/cli/src/lib/template-downloader.ts +++ b/packages/cli/src/lib/template-downloader.ts @@ -8,7 +8,11 @@ import {parseGitHubRepositoryURL} from '@shopify/cli-kit/node/github'; import {mkdir, fileExists, rmdir} from '@shopify/cli-kit/node/fs'; import {AbortError} from '@shopify/cli-kit/node/error'; import {AbortSignal} from '@shopify/cli-kit/node/abort'; -import {getAssetsDir, getSkeletonSourceDir} from './build.js'; +import { + getAssetsDir, + getSkeletonSourceDir, + isHydrogenMonorepo, +} from './build.js'; import {joinPath} from '@shopify/cli-kit/node/path'; import {downloadGitRepository} from '@shopify/cli-kit/node/git'; @@ -78,7 +82,7 @@ async function downloadMonorepoTarball( export async function downloadMonorepoTemplates({ signal, }: {signal?: AbortSignal} = {}) { - if (process.env.LOCAL_DEV) { + if (isHydrogenMonorepo && process.env.FORCE_TEMPLATES_SOURCE !== 'remote') { const templatesDir = path.dirname(getSkeletonSourceDir()); return { version: 'local', diff --git a/packages/create-hydrogen/integration.test.ts b/packages/create-hydrogen/integration.test.ts new file mode 100644 index 0000000000..a893d3005d --- /dev/null +++ b/packages/create-hydrogen/integration.test.ts @@ -0,0 +1,69 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; +import {createRequire} from 'node:module'; +import {execa} from 'execa'; +import {temporaryDirectoryTask} from 'tempy'; +import {describe, it, expect} from 'vitest'; + +describe('create-hydrogen', () => { + it('creates a quickstart project using the compiled files', async () => { + const packageJson = createRequire(import.meta.url)('./package.json'); + const bin = path.resolve(packageJson.bin); + + expect(bin).toMatch(/\bdist\/.*\.m?js$/); + + await expect( + fs.stat(bin).catch(() => false), + `It looks like there are no compiled files for create-hydrogen in ${bin}.` + + `Please build the project before running the tests`, + ).resolves.toBeTruthy(); + + await temporaryDirectoryTask(async (tmpDir) => { + const processPromise = execa('node', [ + bin, + '--quickstart', + '--no-install-deps', + '--no-shortcut', + '--path', + tmpDir, + ]); + + await expect(processPromise, 'create-app process').resolves.toBeTruthy(); + + await expect( + fs.stat(path.resolve(tmpDir, 'package.json')).catch(() => false), + ).resolves.toBeTruthy(); + + // Replace the temporary directory with a placeholder to avoid snapshot noise. + // The directory can wrap to a new line, so we can't use a simple string replace. + const output = (await processPromise).stdout + .replace(/^.*╭/ims, '╭') + .replace(/Run `.*$/s, 'Run ``'); + + expect(output).toMatchInlineSnapshot(` + "╭─ success ────────────────────────────────────────────────────────────────────╮ + │ │ + │ Storefront setup complete! │ + │ │ + │ Shopify: Mock.shop │ + │ Language: JavaScript │ + │ Routes: │ + │ • Home (/ & /:catchAll) │ + │ • Page (/pages/:handle) │ + │ • Cart (/cart/* & /discount/*) │ + │ • Products (/products/:handle) │ + │ • Collections (/collections/*) │ + │ • Policies (/policies & /policies/:handle) │ + │ • Blogs (/blogs/*) │ + │ • Account (/account/*) │ + │ • Search (/api/predictive-search & /search) │ + │ • Robots (/robots.txt) │ + │ • Sitemap (/sitemap.xml) │ + │ │ + │ Next steps │ + │ │ + │ • Run \`\`" + `); + }); + }); +}); diff --git a/packages/create-hydrogen/package.json b/packages/create-hydrogen/package.json index f7951967ab..27771090ec 100644 --- a/packages/create-hydrogen/package.json +++ b/packages/create-hydrogen/package.json @@ -9,11 +9,16 @@ "type": "module", "scripts": { "build": "tsup --clean", - "dev": "tsup --watch", + "dev": "tsup --watch src --watch ../cli/src", + "test": "vitest run --test-timeout=60000", "typecheck": "tsc --noEmit" }, "dependencies": { - "@shopify/cli-hydrogen": "^8.1.0" + "@ast-grep/napi": "0.11.0" + }, + "devDependencies": { + "@shopify/cli-hydrogen": "^8.1.0", + "tempy": "3.0.0" }, "bin": "dist/create-app.js", "files": [ diff --git a/packages/create-hydrogen/tsconfig.json b/packages/create-hydrogen/tsconfig.json index dcb7ad5398..c4bbacee81 100644 --- a/packages/create-hydrogen/tsconfig.json +++ b/packages/create-hydrogen/tsconfig.json @@ -1,12 +1,18 @@ { "extends": "../../tsconfig.json", - "include": ["src"], + "include": ["src", "*.test.ts"], "exclude": ["dist", "__tests__", "node_modules", ".turbo"], "compilerOptions": { + "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "rootDir": "src", "noUncheckedIndexedAccess": true, - "outDir": "dist" + "outDir": "dist", + "types": ["node"], + "paths": { + // Ensure we use the source code of the CLI, + // not the compiled version from `dist`. + "@shopify/cli-hydrogen/*": ["../cli/src/*"] + } } } diff --git a/packages/create-hydrogen/tsup.config.ts b/packages/create-hydrogen/tsup.config.ts index f18defe787..e5e9fbd023 100644 --- a/packages/create-hydrogen/tsup.config.ts +++ b/packages/create-hydrogen/tsup.config.ts @@ -1,13 +1,37 @@ +import {createRequire} from 'node:module'; +import fs from 'node:fs/promises'; import {defineConfig} from 'tsup'; export default defineConfig({ - entry: ['src/**/*.ts'], + entry: ['src/create-app.ts'], outDir: 'dist', format: 'esm', - minify: false, - bundle: false, - splitting: true, - treeshake: true, + clean: true, sourcemap: false, dts: false, + minify: true, + splitting: true, // Async/await breaks without splitting + + // -- Bundle: + bundle: true, + external: [ + '@ast-grep/napi', // Required binary + '@parcel/watcher', // Not used but but needs to resolve it + 'react-devtools-core', // Not used but breaks the build otherwise + ], + // Needed for some CJS dependencies: + shims: true, + banner: { + js: "import { createRequire as __createRequire } from 'module';globalThis.require = __createRequire(import.meta.url);", + }, + async onSuccess() { + // Copy assets to the dist folder + await fs.cp('../cli/dist/assets', './dist/assets', {recursive: true}); + + // This WASM file is used in a dependency, copy it over: + await fs.copyFile( + createRequire(import.meta.url).resolve('yoga-wasm-web/dist/yoga.wasm'), + './dist/yoga.wasm', + ); + }, }); diff --git a/turbo.json b/turbo.json index bed6c8ffa6..f2e4471794 100644 --- a/turbo.json +++ b/turbo.json @@ -39,6 +39,9 @@ "@shopify/create-hydrogen#build": { "dependsOn": ["@shopify/cli-hydrogen#build"] }, + "@shopify/create-hydrogen#test": { + "dependsOn": ["@shopify/create-hydrogen#build"] + }, "@shopify/remix-oxygen#build": { "dependsOn": ["@shopify/hydrogen#build"] },