Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ jobs:
> If the versions don't match, you might have multiple global instances installed.

> Use `which shopify` to find out which one you are running and uninstall it."
comment_package_manager: 'npm'
comment_command_flags: '--@shopify:registry=https://registry.npmjs.org'
build_script: "node bin/update-cli-kit-version.js && pnpm nx run-many --target=bundle --all --skip-nx-cache --output-style=stream && pnpm refresh-manifests"
env:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ jobs:
- name: Build
run: pnpm nx run-many --all --skip-nx-cache --target=build --output-style=stream
- name: Install Playwright Chromium
run: npx playwright install chromium
run: pnpm exec playwright install chromium
working-directory: packages/e2e
- name: Rebuild node-pty
run: pnpm rebuild node-pty
Expand All @@ -247,7 +247,7 @@ jobs:
E2E_STORE_FQDN: ${{ secrets.E2E_STORE_FQDN }}
E2E_SECONDARY_CLIENT_ID: ${{ secrets.E2E_SECONDARY_CLIENT_ID }}
E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }}
run: npx playwright test
run: pnpm exec playwright test
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
Expand Down
2 changes: 1 addition & 1 deletion bin/create-notification-pr.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async function createPR() {
'**Please update the release highlights before merging.**',
'',
'### How to test',
'- `npx http-server ~/src/github.com/Shopify/static-cdn-assets`',
'- `pnpx http-server ~/src/github.com/Shopify/static-cdn-assets`',
'- `SHOPIFY_CLI_NOTIFICATIONS_URL=http://127.0.0.1:8080/static-24h/cli/notifications.json shopify version`',
"- You may need to clear the CLI cache with `shopify cache clear` and run the command twice to see the notification (it's fetched in the background).",
'',
Expand Down
10 changes: 5 additions & 5 deletions bin/docs/build-dev-docs.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
echo "STARTING"
COMPILE_DOCS="npx tsc --project bin/docs/tsconfig.docs.json --moduleResolution node --target esNext && npx generate-docs --overridePath ./bin/docs/typeOverride.json --input ./docs-shopify.dev/commands --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/commands/**/*.doc.js docs-shopify.dev/commands/*.doc.js"
COMPILE_STATIC_PAGES="npx tsc docs-shopify.dev/static/*.doc.ts --moduleResolution node --target esNext && npx generate-docs --isLandingPage --input ./docs-shopify.dev/static --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/static/*.doc.js"
COMPILE_CATEGORY_PAGES="npx tsc docs-shopify.dev/categories/*.doc.ts --moduleResolution node --target esNext && generate-docs --isCategoryPage --input ./docs-shopify.dev/categories --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/categories/*.doc.js"
COMPILE_DOCS="pnpx tsc --project bin/docs/tsconfig.docs.json --moduleResolution node --target esNext && pnpx generate-docs --overridePath ./bin/docs/typeOverride.json --input ./docs-shopify.dev/commands --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/commands/**/*.doc.js docs-shopify.dev/commands/*.doc.js"
COMPILE_STATIC_PAGES="pnpx tsc docs-shopify.dev/static/*.doc.ts --moduleResolution node --target esNext && pnpx generate-docs --isLandingPage --input ./docs-shopify.dev/static --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/static/*.doc.js"
COMPILE_CATEGORY_PAGES="pnpx tsc docs-shopify.dev/categories/*.doc.ts --moduleResolution node --target esNext && pnpx generate-docs --isCategoryPage --input ./docs-shopify.dev/categories --output ./docs-shopify.dev/generated && rm -rf docs-shopify.dev/categories/*.doc.js"

if [ "$1" = "isTest" ];
then
COMPILE_DOCS="npx tsc --project bin/docs/tsconfig.docs.json --moduleResolution node --target esNext && npx generate-docs --overridePath ./bin/docs/typeOverride.json --input ./docs-shopify.dev/commands --output ./docs-shopify.dev/static/temp && rm -rf docs-shopify.dev/commands/**/*.doc.js docs-shopify.dev/commands/*.doc.js"
COMPILE_STATIC_PAGES="npx tsc docs-shopify.dev/static/*.doc.ts --moduleResolution node --target esNext && npx generate-docs --isLandingPage --input ./docs-shopify.dev/static/docs-shopify.dev --output ./docs-shopify.dev/static/temp && rm -rf docs-shopify.dev/static/*.doc.js"
COMPILE_DOCS="pnpx tsc --project bin/docs/tsconfig.docs.json --moduleResolution node --target esNext && pnpx generate-docs --overridePath ./bin/docs/typeOverride.json --input ./docs-shopify.dev/commands --output ./docs-shopify.dev/static/temp && rm -rf docs-shopify.dev/commands/**/*.doc.js docs-shopify.dev/commands/*.doc.js"
COMPILE_STATIC_PAGES="pnpx tsc docs-shopify.dev/static/*.doc.ts --moduleResolution node --target esNext && pnpx generate-docs --isLandingPage --input ./docs-shopify.dev/static/docs-shopify.dev --output ./docs-shopify.dev/static/temp && rm -rf docs-shopify.dev/static/*.doc.js"
fi

echo $1
Expand Down
2 changes: 1 addition & 1 deletion dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ up:
meet: 'true'
- custom:
name: 'Install Playwright Chromium'
met?: cd packages/e2e && npx playwright install chromium
met?: cd packages/e2e && pnpx playwright install chromium
meet: 'true'
- custom:
name: 'Rebuild node-pty'
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"bundle-for-release": "nx run-many --target=bundle --all --skip-nx-cache",
"changeset-manifests": "changeset version && pnpm install --no-frozen-lockfile && pnpm refresh-manifests && pnpm refresh-readme && pnpm refresh-code-documentation && bin/update-cli-kit-version.js",
"clean": "nx run-many --target=clean --all --skip-nx-cache && nx reset",
"create-app": "nx build create-app && node packages/create-app/bin/dev.js --package-manager npm",
"create-app": "nx build create-app && node packages/create-app/bin/dev.js --package-manager pnpm",
"deploy-experimental": "node bin/deploy-experimental.js",
"graph": "nx graph",
"graphql-codegen:get-graphql-schemas": "bin/get-graphql-schemas.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ dev = "npm run dev"

// package.json (needed by the loader)
await writeFile(joinPath(dir, 'package.json'), JSON.stringify({name: 'test-app', dependencies: {}}))
// Pin npm: getPackageManager walks up to ancestors if no lockfile is found
await writeFile(joinPath(dir, 'package-lock.json'), '')
}

// Load specifications once — this is expensive (loads all extension specs from disk)
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/services/app/config/use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('use', () => {
}
writeFileSync(joinPath(tmp, 'package.json'), '{}')
writeFileSync(joinPath(tmp, 'shopify.app.toml'), '')
writeFileSync(joinPath(tmp, 'package-lock.json'), '')

// When
await use(options)
Expand Down
37 changes: 37 additions & 0 deletions packages/app/src/cli/services/function/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ import {
} from './binaries.js'
import {testApp, testFunctionExtension} from '../../models/app/app.test-data.js'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {getPackageManager} from '@shopify/cli-kit/node/node-package-manager'
import {exec} from '@shopify/cli-kit/node/system'
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
import {inTemporaryDirectory, mkdir, readFileSync, writeFile, removeFile} from '@shopify/cli-kit/node/fs'
import {build as esBuild} from 'esbuild'

vi.mock('@shopify/cli-kit/node/fs')
vi.mock('@shopify/cli-kit/node/system')
vi.mock('@shopify/cli-kit/node/node-package-manager', async (importOriginal) => {
const actual = await importOriginal<typeof import('@shopify/cli-kit/node/node-package-manager')>()
return {
...actual,
getPackageManager: vi.fn(),
}
})

vi.mock('./binaries.js', async (importOriginal) => {
const actual: any = await importOriginal()
Expand Down Expand Up @@ -76,6 +84,7 @@ beforeEach(async () => {
stderr = {write: vi.fn()}
stdout = {write: vi.fn()}
signal = vi.fn()
vi.mocked(getPackageManager).mockResolvedValue('npm')
})

describe('buildGraphqlTypes', () => {
Expand All @@ -95,6 +104,34 @@ describe('buildGraphqlTypes', () => {
})
})

test('generate types uses pnpm exec when package manager is pnpm', {timeout: 20000}, async () => {
vi.mocked(getPackageManager).mockResolvedValueOnce('pnpm')
const ourFunction = await testFunctionExtension({entryPath: 'src/index.js'})

const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})

await expect(got).resolves.toBeUndefined()
expect(exec).toHaveBeenCalledWith('pnpm', ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], {
cwd: ourFunction.directory,
stderr,
signal,
})
})

test('generate types uses yarn without exec subcommand when package manager is yarn', {timeout: 20000}, async () => {
vi.mocked(getPackageManager).mockResolvedValueOnce('yarn')
const ourFunction = await testFunctionExtension({entryPath: 'src/index.js'})

const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})

await expect(got).resolves.toBeUndefined()
expect(exec).toHaveBeenCalledWith('yarn', ['--', 'graphql-code-generator', '--config', 'package.json'], {
cwd: ourFunction.directory,
stderr,
signal,
})
})

test('errors if function is not a JS function and no typegen_command', async () => {
// Given
const ourFunction = await testFunctionExtension()
Expand Down
8 changes: 7 additions & 1 deletion packages/app/src/cli/services/function/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {renderTasks} from '@shopify/cli-kit/node/ui'
import {pickBy} from '@shopify/cli-kit/common/object'
import {runWithTimer} from '@shopify/cli-kit/node/metadata'
import {AbortError} from '@shopify/cli-kit/node/error'
import {getPackageManager} from '@shopify/cli-kit/node/node-package-manager'
import {Writable} from 'stream'

export const PREFERRED_FUNCTION_NPM_PACKAGE_MAJOR_VERSION = '2'
Expand Down Expand Up @@ -144,7 +145,12 @@ export async function buildGraphqlTypes(
}

return runWithTimer('cmd_all_timing_network_ms')(async () => {
return exec('npm', ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], {
const packageManager = await getPackageManager(fun.directory)
const execArgs = ['--', 'graphql-code-generator', '--config', 'package.json']
if (packageManager !== 'yarn') {
execArgs.unshift('exec')
}
return exec(packageManager, execArgs, {
cwd: fun.directory,
stderr: options.stderr,
signal: options.signal,
Expand Down
17 changes: 16 additions & 1 deletion packages/cli-kit/src/public/node/node-package-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
checkForCachedNewVersion,
inferPackageManager,
PackageManager,
npmLockfile,
} from './node-package-manager.js'
import {captureOutput, exec} from './system.js'
import {inTemporaryDirectory, mkdir, touchFile, writeFile} from './fs.js'
Expand Down Expand Up @@ -845,8 +846,9 @@ describe('writePackageJSON', () => {
describe('getPackageManager', () => {
test('finds if npm is being used', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given
// Given — pin NPM in the temp project
await writePackageJSON(tmpDir, {name: 'mock name'})
await writeFile(joinPath(tmpDir, npmLockfile), '')

// Then
const packageManager = await getPackageManager(tmpDir)
Expand Down Expand Up @@ -878,6 +880,19 @@ describe('getPackageManager', () => {
})
})

test('finds pnpm from a nested workspace package when the lockfile is only at the repo root', async () => {
await inTemporaryDirectory(async (tmpDir) => {
await writePackageJSON(tmpDir, {name: 'root'})
await writeFile(joinPath(tmpDir, 'pnpm-lock.yaml'), '')
const nested = joinPath(tmpDir, 'extensions', 'cart-transformer')
await mkdir(nested)
await writePackageJSON(nested, {name: 'cart-transformer'})

const packageManager = await getPackageManager(nested)
expect(packageManager).toEqual('pnpm')
})
})

test('falls back to packageManagerFromUserAgent when no package.json is found', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given — no package.json in tmpDir, stub user agent to yarn
Expand Down
26 changes: 17 additions & 9 deletions packages/cli-kit/src/public/node/node-package-manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {AbortError, BugError} from './error.js'
import {AbortController, AbortSignal} from './abort.js'
import {exec} from './system.js'
import {fileExists, readFile, writeFile, findPathUp, glob} from './fs.js'
import {fileExists, readFile, writeFile, findPathUp, glob, fileExistsSync} from './fs.js'
import {dirname, joinPath} from './path.js'
import {runWithTimer} from './metadata.js'
import {inferPackageManagerForGlobalCLI} from './is-global.js'
Expand Down Expand Up @@ -111,21 +111,29 @@ export function packageManagerFromUserAgent(env = process.env): PackageManager {

/**
* Returns the dependency manager used in a directory.
* Walks upward from `fromDirectory` so workspace packages (e.g. `extensions/my-fn/package.json`)
* still resolve to the repo root lockfile (`pnpm-lock.yaml`).
* If no lockfile is found, it falls back to the package manager from the user agent.
* If the package manager from the user agent is unknown, it returns 'npm'.
* @param fromDirectory - The starting directory
* @returns The dependency manager
*/
export async function getPackageManager(fromDirectory: string): Promise<PackageManager> {
const packageJsonPath = await findPathUp('package.json', {cwd: fromDirectory, type: 'file'})
if (!packageJsonPath) {
return packageManagerFromUserAgent()
let current = fromDirectory
outputDebug(outputContent`Looking for a lockfile in ${outputToken.path(current)}...`)
while (true) {
if (fileExistsSync(joinPath(current, yarnLockfile))) return 'yarn'
if (fileExistsSync(joinPath(current, pnpmLockfile))) return 'pnpm'
if (fileExistsSync(joinPath(current, bunLockfile))) return 'bun'
if (fileExistsSync(joinPath(current, npmLockfile))) return 'npm'
const parent = dirname(current)
if (parent === current) break
current = parent
}

const directory = dirname(packageJsonPath)
outputDebug(outputContent`Obtaining the dependency manager in directory ${outputToken.path(directory)}...`)
const pm: PackageManager = packageManagerFromUserAgent()
if (pm !== 'unknown') return pm

if (await fileExists(joinPath(directory, yarnLockfile))) return 'yarn'
if (await fileExists(joinPath(directory, pnpmLockfile))) return 'pnpm'
if (await fileExists(joinPath(directory, bunLockfile))) return 'bun'
return 'npm'
}

Expand Down
9 changes: 6 additions & 3 deletions packages/e2e/scripts/cleanup-test-apps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Deletes all test apps from the dev dashboard via browser automation.
* Run: npx tsx packages/e2e/scripts/cleanup-test-apps.ts
* Run: pnpx tsx packages/e2e/scripts/cleanup-test-apps.ts
*
* Pass --dry-run to list apps without deleting.
* Pass --filter <pattern> to only delete apps matching the pattern.
Expand Down Expand Up @@ -128,15 +128,18 @@ async function main() {
await page.waitForTimeout(2000)

// Check for 500 error and retry
const pageText = await page.textContent('body') ?? ''
const pageText = (await page.textContent('body')) ?? ''
if (pageText.includes('500') || pageText.includes('Internal Server Error')) {
console.log('Got 500 error, retrying...')
await page.reload({waitUntil: 'domcontentloaded'})
await page.waitForTimeout(3000)
}

// Check for org selection page
const orgLink = page.locator('a, button').filter({hasText: /core-build|cli-e2e/i}).first()
const orgLink = page
.locator('a, button')
.filter({hasText: /core-build|cli-e2e/i})
.first()
if (await orgLink.isVisible({timeout: 3000}).catch(() => false)) {
console.log('Org selection detected, clicking...')
await orgLink.click()
Expand Down
47 changes: 24 additions & 23 deletions packages/e2e/scripts/create-test-apps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Creates test apps in the authenticated org and prints their client IDs.
* Run: npx tsx packages/e2e/scripts/create-test-apps.ts
* Run: pnpx tsx packages/e2e/scripts/create-test-apps.ts
*/

import * as fs from 'fs'
Expand Down Expand Up @@ -39,7 +39,7 @@ if (!email || !password) {
}

const baseEnv: Record<string, string> = {
...process.env as Record<string, string>,
...(process.env as Record<string, string>),
NODE_OPTIONS: '',
SHOPIFY_RUN_AS_USER: '0',
FORCE_COLOR: '0',
Expand Down Expand Up @@ -86,16 +86,16 @@ async function createAppInteractive(tmpDir: string, appName: string): Promise<st
fs.mkdirSync(appDir)

const nodePty = await import('node-pty')
const pty = nodePty.spawn('node', [
createAppPath,
'--name', appName,
'--path', appDir,
'--template', 'none',
'--package-manager', 'npm',
'--local',
], {
name: 'xterm-color', cols: 120, rows: 30, env: baseEnv,
})
const pty = nodePty.spawn(
'node',
[createAppPath, '--name', appName, '--path', appDir, '--template', 'none', '--package-manager', 'pnpm', '--local'],
{
name: 'xterm-color',
cols: 120,
rows: 30,
env: baseEnv,
},
)

let output = ''
pty.onData((data: string) => {
Expand All @@ -104,11 +104,7 @@ async function createAppInteractive(tmpDir: string, appName: string): Promise<st
})

// Answer each interactive prompt as it appears
const prompts = [
'Which organization',
'Create this project as a new app',
'App name',
]
const prompts = ['Which organization', 'Create this project as a new app', 'App name']
for (const prompt of prompts) {
try {
await waitForText(() => output, prompt, 60_000)
Expand All @@ -130,9 +126,7 @@ async function createAppInteractive(tmpDir: string, appName: string): Promise<st

// Find the app dir and extract client_id
const entries = fs.readdirSync(appDir, {withFileTypes: true})
const created = entries.find(
(e) => e.isDirectory() && fs.existsSync(path.join(appDir, e.name, 'shopify.app.toml')),
)
const created = entries.find((e) => e.isDirectory() && fs.existsSync(path.join(appDir, e.name, 'shopify.app.toml')))
if (!created) throw new Error(`No app directory found in ${appDir}`)

const tomlPath = path.join(appDir, created.name, 'shopify.app.toml')
Expand All @@ -147,11 +141,16 @@ async function oauthLogin() {
const nodePty = await import('node-pty')
const spawnEnv = {...baseEnv, BROWSER: 'none'}
const pty = nodePty.spawn('node', [cliPath, 'auth', 'login'], {
name: 'xterm-color', cols: 120, rows: 30, env: spawnEnv,
name: 'xterm-color',
cols: 120,
rows: 30,
env: spawnEnv,
})

let output = ''
pty.onData((data: string) => { output += data })
pty.onData((data: string) => {
output += data
})

await waitForText(() => output, 'Press any key to open the login page', 30_000)
pty.write(' ')
Expand All @@ -169,7 +168,9 @@ async function oauthLogin() {
await completeLogin(page, urlMatch[0], email!, password!)

await waitForText(() => output, 'Logged in', 60_000)
try { pty.kill() } catch {}
try {
pty.kill()
} catch {}
await browser.close()
}

Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/setup/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function createApp(ctx: {
const name = ctx.name ?? 'e2e-test-app'
const template = ctx.template ?? 'reactRouter'
const packageManager =
ctx.packageManager ?? (process.env.E2E_PACKAGE_MANAGER as 'npm' | 'yarn' | 'pnpm' | 'bun') ?? 'npm'
ctx.packageManager ?? (process.env.E2E_PACKAGE_MANAGER as 'npm' | 'yarn' | 'pnpm' | 'bun') ?? 'pnpm'

const args = [
'--name',
Expand Down
Loading