Skip to content

Commit

Permalink
Automatically inject variables in runtime (#861)
Browse files Browse the repository at this point in the history
* Move environment variable pull into a separate utility

We're about to be using this same query in the `dev` command so it makes
sense to colocate the logic.

* Allow mini-oxygen to be explicitly passed env variables

* Add combinedEnvironmentVariables function

This will be used in the `dev` command to combine local and remote
environment variables to pass to mini-oxygen.

* Automatically inject environment variables in dev

* Only autoload .env if no injection is happening
  • Loading branch information
graygilmore authored May 19, 2023
1 parent ba54a3b commit a8d5fef
Show file tree
Hide file tree
Showing 11 changed files with 625 additions and 228 deletions.
5 changes: 5 additions & 0 deletions .changeset/hungry-spiders-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-hydrogen': patch
---

Update dev command to automatically injected environment variables from a linked Hydrogen storefront
2 changes: 1 addition & 1 deletion packages/cli/oclif.manifest.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/cli/src/commands/hydrogen/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {startMiniOxygen} from '../../lib/mini-oxygen.js';
import {checkHydrogenVersion} from '../../lib/check-version.js';
import {addVirtualRoutes} from '../../lib/virtual-routes.js';
import {spawnCodegenProcess} from '../../lib/codegen.js';
import {combinedEnvironmentVariables} from '../../lib/combined-environment-variables.js';
import {getConfig} from '../../lib/shopify-config.js';

const LOG_INITIAL_BUILD = '\n🏁 Initial build';
const LOG_REBUILDING = '🧱 Rebuilding...';
Expand Down Expand Up @@ -41,6 +43,7 @@ export default class Dev extends Command {
env: 'SHOPIFY_HYDROGEN_FLAG_DISABLE_VIRTUAL_ROUTES',
default: false,
}),
shop: commonFlags.shop,
debug: Flags.boolean({
description: 'Attaches a Node inspector',
env: 'SHOPIFY_HYDROGEN_FLAG_DEBUG',
Expand All @@ -67,13 +70,15 @@ async function runDev({
codegen = false,
codegenConfigPath,
disableVirtualRoutes,
shop,
debug = false,
}: {
port?: number;
path?: string;
codegen?: boolean;
codegenConfigPath?: string;
disableVirtualRoutes?: boolean;
shop?: string;
debug?: false;
}) {
if (!process.env.NODE_ENV) process.env.NODE_ENV = 'development';
Expand Down Expand Up @@ -102,6 +107,11 @@ async function runDev({

const serverBundleExists = () => fileExists(buildPathWorkerFile);

const hasLinkedStorefront = !!(await getConfig(root))?.storefront?.id;
const environmentVariables = hasLinkedStorefront
? await combinedEnvironmentVariables({root, shop})
: undefined;

let miniOxygenStarted = false;
async function safeStartMiniOxygen() {
if (miniOxygenStarted) return;
Expand All @@ -112,6 +122,7 @@ async function runDev({
watch: true,
buildPathWorkerFile,
buildPathClient,
environmentVariables,
});

miniOxygenStarted = true;
Expand Down
155 changes: 20 additions & 135 deletions packages/cli/src/commands/hydrogen/env/pull.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@ import {
import {joinPath} from '@shopify/cli-kit/node/path';
import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui';

import {
PullVariablesQuery,
PullVariablesSchema,
} from '../../../lib/graphql/admin/pull-variables.js';
import {getAdminSession} from '../../../lib/admin-session.js';
import {adminRequest} from '../../../lib/graphql.js';
import {pullRemoteEnvironmentVariables} from '../../../lib/pull-environment-variables.js';
import {getConfig} from '../../../lib/shopify-config.js';
import {linkStorefront} from '../link.js';

Expand All @@ -33,15 +29,7 @@ vi.mock('@shopify/cli-kit/node/ui', async () => {
vi.mock('../link.js');
vi.mock('../../../lib/admin-session.js');
vi.mock('../../../lib/shopify-config.js');
vi.mock('../../../lib/graphql.js', async () => {
const original = await vi.importActual<
typeof import('../../../lib/graphql.js')
>('../../../lib/graphql.js');
return {
...original,
adminRequest: vi.fn(),
};
});
vi.mock('../../../lib/pull-environment-variables.js');
vi.mock('../../../lib/shop.js', () => ({
getHydrogenShop: () => 'my-shop',
}));
Expand All @@ -60,43 +48,34 @@ describe('pullVariables', () => {
title: 'Existing Link',
},
});
vi.mocked(adminRequest<PullVariablesSchema>).mockResolvedValue({
hydrogenStorefront: {
id: 'gid://shopify/HydrogenStorefront/1',
environmentVariables: [
{
id: 'gid://shopify/HydrogenStorefrontEnvironmentVariable/1',
key: 'PUBLIC_API_TOKEN',
value: 'abc123',
isSecret: false,
},
{
id: 'gid://shopify/HydrogenStorefrontEnvironmentVariable/1',
key: 'PRIVATE_API_TOKEN',
value: '',
isSecret: true,
},
],
vi.mocked(pullRemoteEnvironmentVariables).mockResolvedValue([
{
id: 'gid://shopify/HydrogenStorefrontEnvironmentVariable/1',
key: 'PUBLIC_API_TOKEN',
value: 'abc123',
isSecret: false,
},
});
{
id: 'gid://shopify/HydrogenStorefrontEnvironmentVariable/2',
key: 'PRIVATE_API_TOKEN',
value: '',
isSecret: true,
},
]);
});

afterEach(() => {
vi.resetAllMocks();
mockAndCaptureOutput().clear();
});

it('makes a GraphQL call to fetch environment variables', async () => {
it('calls pullRemoteEnvironmentVariables', async () => {
await inTemporaryDirectory(async (tmpDir) => {
await pullVariables({path: tmpDir});

expect(adminRequest).toHaveBeenCalledWith(
PullVariablesQuery,
ADMIN_SESSION,
{
id: 'gid://shopify/HydrogenStorefront/2',
},
);
expect(pullRemoteEnvironmentVariables).toHaveBeenCalledWith({
root: tmpDir,
});
});
});

Expand All @@ -116,21 +95,7 @@ describe('pullVariables', () => {
});
});

it('warns if there are any variables marked as secret', async () => {
vi.mocked(adminRequest<PullVariablesSchema>).mockResolvedValue({
hydrogenStorefront: {
id: 'gid://shopify/HydrogenStorefront/1',
environmentVariables: [
{
id: 'gid://shopify/HydrogenStorefrontEnvironmentVariable/1',
key: 'PRIVATE_API_TOKEN',
value: '',
isSecret: true,
},
],
},
});

it('warns about secret environment variables', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const outputMock = mockAndCaptureOutput();

Expand Down Expand Up @@ -176,84 +141,4 @@ describe('pullVariables', () => {
});
});
});

describe('when there are no environment variables to update', () => {
beforeEach(() => {
vi.mocked(adminRequest<PullVariablesSchema>).mockResolvedValue({
hydrogenStorefront: {
id: 'gid://shopify/HydrogenStorefront/1',
environmentVariables: [],
},
});
});

it('renders a message', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const outputMock = mockAndCaptureOutput();

await pullVariables({path: tmpDir});

expect(outputMock.info()).toMatch(
/No Preview environment variables found\./,
);
});
});
});

describe('when there is no linked storefront', () => {
beforeEach(() => {
vi.mocked(getConfig).mockResolvedValue({
storefront: undefined,
});
});

it('renders an error message', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const outputMock = mockAndCaptureOutput();

await pullVariables({path: tmpDir});

expect(outputMock.error()).toMatch(
/No linked Hydrogen storefront on my-shop/,
);
});
});

it('prompts the user to create a link', async () => {
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);

await inTemporaryDirectory(async (tmpDir) => {
await pullVariables({path: tmpDir});

expect(renderConfirmationPrompt).toHaveBeenCalledWith({
message: expect.stringMatching(/Run .*npx shopify hydrogen link.*\?/),
});

expect(linkStorefront).toHaveBeenCalledWith({
path: tmpDir,
silent: true,
});
});
});
});

describe('when there is no matching storefront in the shop', () => {
beforeEach(() => {
vi.mocked(adminRequest<PullVariablesSchema>).mockResolvedValue({
hydrogenStorefront: null,
});
});

it('renders an error message', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const outputMock = mockAndCaptureOutput();

await pullVariables({path: tmpDir});

expect(outputMock.error()).toMatch(
/Couldnt find Hydrogen storefront\./,
);
});
});
});
});
104 changes: 14 additions & 90 deletions packages/cli/src/commands/hydrogen/env/pull.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
import {Flags} from '@oclif/core';
import Command from '@shopify/cli-kit/node/base-command';
import {
renderConfirmationPrompt,
renderFatalError,
} from '@shopify/cli-kit/node/ui';
import {
outputContent,
outputInfo,
outputSuccess,
outputToken,
outputWarn,
} from '@shopify/cli-kit/node/output';
import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui';
import {outputSuccess, outputWarn} from '@shopify/cli-kit/node/output';
import {fileExists, writeFile} from '@shopify/cli-kit/node/fs';
import {resolvePath} from '@shopify/cli-kit/node/path';

import {linkStorefront} from '../link.js';
import {adminRequest, parseGid} from '../../../lib/graphql.js';
import {commonFlags} from '../../../lib/flags.js';
import {getHydrogenShop} from '../../../lib/shop.js';
import {getAdminSession} from '../../../lib/admin-session.js';
import {
PullVariablesQuery,
PullVariablesSchema,
} from '../../../lib/graphql/admin/pull-variables.js';
import {pullRemoteEnvironmentVariables} from '../../../lib/pull-environment-variables.js';
import {getConfig} from '../../../lib/shopify-config.js';
import {hydrogenStorefrontsUrl} from '../../../lib/admin-urls.js';

export default class Pull extends Command {
static description =
Expand All @@ -51,77 +34,14 @@ interface Flags {
}

export async function pullVariables({force, path, shop: flagShop}: Flags) {
const shop = await getHydrogenShop({path, shop: flagShop});
const adminSession = await getAdminSession(shop);
const actualPath = path ?? process.cwd();
let configStorefront = (await getConfig(actualPath)).storefront;

if (!configStorefront?.id) {
renderFatalError({
name: 'NoLinkedStorefrontError',
type: 0,
message: `No linked Hydrogen storefront on ${adminSession.storeFqdn}`,
tryMessage:
outputContent`To pull environment variables, link this project to a Hydrogen storefront. To select a storefront to link, run ${outputToken.genericShellCommand(
`npx shopify hydrogen link`,
)}.`.value,
});

const runLink = await renderConfirmationPrompt({
message: outputContent`Run ${outputToken.genericShellCommand(
`npx shopify hydrogen link`,
)}?`.value,
});

if (!runLink) {
return;
}

await linkStorefront({force, path, shop: flagShop, silent: true});
}

configStorefront = (await getConfig(actualPath)).storefront;
const environmentVariables = await pullRemoteEnvironmentVariables({
root: actualPath,
flagShop,
});

if (!configStorefront) {
return;
}

outputInfo(
`Fetching Preview environment variables from ${configStorefront.title}...`,
);
const result: PullVariablesSchema = await adminRequest(
PullVariablesQuery,
adminSession,
{
id: configStorefront.id,
},
);

const hydrogenStorefront = result.hydrogenStorefront;

if (!hydrogenStorefront) {
renderFatalError({
name: 'NoStorefrontError',
type: 0,
message: outputContent`${outputToken.errorText(
'Couldn’t find Hydrogen storefront.',
)}`.value,
tryMessage: outputContent`Couldn’t find ${
configStorefront.title
} (ID: ${parseGid(configStorefront.id)}) on ${
adminSession.storeFqdn
}. Check that the storefront exists and run ${outputToken.genericShellCommand(
`npx shopify hydrogen link`,
)} to link this project to it.\n\n${outputToken.link(
'Hydrogen Storefronts Admin',
hydrogenStorefrontsUrl(adminSession),
)}`.value,
});
return;
}

if (!hydrogenStorefront.environmentVariables.length) {
outputInfo(`No Preview environment variables found.`);
if (!environmentVariables.length) {
return;
}

Expand All @@ -140,7 +60,7 @@ export async function pullVariables({force, path, shop: flagShop}: Flags) {

let hasSecretVariables = false;
const contents =
hydrogenStorefront.environmentVariables
environmentVariables
.map(({key, value, isSecret}) => {
let line = `${key}="${value}"`;

Expand All @@ -155,8 +75,12 @@ export async function pullVariables({force, path, shop: flagShop}: Flags) {
.join('\n') + '\n';

if (hasSecretVariables) {
const {storefront: configStorefront} = await getConfig(actualPath);

outputWarn(
`${configStorefront.title} contains environment variables marked as secret, \
`${
configStorefront!.title
} contains environment variables marked as secret, \
so their values weren’t pulled.`,
);
}
Expand Down
Loading

0 comments on commit a8d5fef

Please sign in to comment.