Skip to content

Commit

Permalink
Bundle create-hydrogen package to enhance installation speed (#2184)
Browse files Browse the repository at this point in the history
* Bundle create-app

* Support checking create-app versioning

* Refactor check cli version

* Remove remaining references to LOCAL_DEV

* Fix bundled code and copy assets

* Changesets

* Use CLI source instead of dist

* Add integration test

* Ensure create-hydrogen tests run after build

* Remove cli-kit usage from create-app

* Higher timeout for integration test

* Avoid snapshot mismatches on CI

* Avoid snapshot mismatches on CI

* Changesets
  • Loading branch information
frandiox authored Jun 7, 2024
1 parent 4337200 commit 10a419b
Show file tree
Hide file tree
Showing 17 changed files with 375 additions and 285 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-melons-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-hydrogen': patch
---

Fix CLI upgrade notification when running from a globla process.
5 changes: 5 additions & 0 deletions .changeset/wet-apricots-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/create-hydrogen': major
---

The code is now bundled to enhance installation speed.
10 changes: 8 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 7 additions & 9 deletions packages/cli/src/commands/hydrogen/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -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 () => {
Expand Down
45 changes: 4 additions & 41 deletions packages/cli/src/commands/hydrogen/init.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
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 {
commonFlags,
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<string, string>;

Expand Down Expand Up @@ -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);
Expand Down
125 changes: 125 additions & 0 deletions packages/cli/src/lib/check-cli-version.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
94 changes: 94 additions & 0 deletions packages/cli/src/lib/check-cli-version.ts
Original file line number Diff line number Diff line change
@@ -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};
};
}
3 changes: 2 additions & 1 deletion packages/cli/src/lib/check-lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 10a419b

Please sign in to comment.