Skip to content

Commit 867e7e8

Browse files
Merge pull request #5980 from Shopify/skip-auth-for-function-build
Do not require authentication for app function build
2 parents bec79d4 + c2433d9 commit 867e7e8

File tree

3 files changed

+174
-63
lines changed

3 files changed

+174
-63
lines changed
Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import {inFunctionContext, functionFlags} from '../../../services/function/common.js'
1+
import {chooseFunction, functionFlags} from '../../../services/function/common.js'
22
import {buildFunctionExtension} from '../../../services/build/extension.js'
33
import {appFlags} from '../../../flags.js'
4-
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
4+
import AppUnlinkedCommand, {AppUnlinkedCommandOutput} from '../../../utilities/app-unlinked-command.js'
5+
import {localAppContext} from '../../../services/app-context.js'
56
import {globalFlags} from '@shopify/cli-kit/node/cli'
67
import {renderSuccess} from '@shopify/cli-kit/node/ui'
78

8-
export default class FunctionBuild extends AppLinkedCommand {
9+
export default class FunctionBuild extends AppUnlinkedCommand {
910
static summary = 'Compile a function to wasm.'
1011

1112
static descriptionWithMarkdown = `Compiles the function in your current directory to WebAssembly (Wasm) for testing purposes.`
@@ -18,26 +19,25 @@ export default class FunctionBuild extends AppLinkedCommand {
1819
...functionFlags,
1920
}
2021

21-
public async run(): Promise<AppLinkedCommandOutput> {
22+
public async run(): Promise<AppUnlinkedCommandOutput> {
2223
const {flags} = await this.parse(FunctionBuild)
2324

24-
const app = await inFunctionContext({
25-
path: flags.path,
25+
const app = await localAppContext({
26+
directory: flags.path,
2627
userProvidedConfigName: flags.config,
27-
apiKey: flags['client-id'],
28-
reset: flags.reset,
29-
callback: async (app, _, ourFunction) => {
30-
await buildFunctionExtension(ourFunction, {
31-
app,
32-
stdout: process.stdout,
33-
stderr: process.stderr,
34-
useTasks: true,
35-
environment: 'production',
36-
})
37-
renderSuccess({headline: 'Function built successfully.'})
38-
return app
39-
},
4028
})
29+
30+
const ourFunction = await chooseFunction(app, flags.path)
31+
32+
await buildFunctionExtension(ourFunction, {
33+
app,
34+
stdout: process.stdout,
35+
stderr: process.stderr,
36+
useTasks: true,
37+
environment: 'production',
38+
})
39+
renderSuccess({headline: 'Function built successfully.'})
40+
4141
return {app}
4242
}
4343
}

packages/app/src/cli/services/function/common.test.ts

Lines changed: 142 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {getOrGenerateSchemaPath, inFunctionContext} from './common.js'
1+
import {getOrGenerateSchemaPath, inFunctionContext, chooseFunction} from './common.js'
22
import {
33
testAppLinked,
44
testDeveloperPlatformClient,
@@ -42,59 +42,51 @@ beforeEach(async () => {
4242
vi.mocked(isTerminalInteractive).mockReturnValue(true)
4343
})
4444

45-
describe('ensure we are within a function context', () => {
46-
test('runs callback when we are inside a function directory', async () => {
45+
describe('inFunctionContext integration', () => {
46+
test('passes correct parameters to callback when function is found', async () => {
4747
// Given
48-
let ranCallback = false
48+
const callback = vi.fn().mockResolvedValue(app)
4949

5050
// When
5151
await inFunctionContext({
5252
path: joinPath(app.directory, 'extensions/my-function'),
53-
callback: async (_app, _fun) => {
54-
ranCallback = true
55-
return _app
56-
},
53+
callback,
5754
})
5855

5956
// Then
60-
expect(ranCallback).toBe(true)
61-
expect(renderFatalError).not.toHaveBeenCalled()
57+
expect(callback).toHaveBeenCalledWith(
58+
app,
59+
// developerPlatformClient
60+
expect.any(Object),
61+
ourFunction,
62+
// orgId
63+
expect.any(String),
64+
)
6265
})
6366

64-
test('displays function prompt when we are not inside a function directory', async () => {
67+
test('calls linkedAppContext with correct parameters', async () => {
6568
// Given
66-
const callback = vi.fn()
69+
const callback = vi.fn().mockResolvedValue(app)
70+
const path = 'some/path'
71+
const apiKey = 'test-api-key'
72+
const userProvidedConfigName = 'test-config'
6773

6874
// When
6975
await inFunctionContext({
70-
path: 'random/dir',
76+
path,
77+
apiKey,
78+
userProvidedConfigName,
79+
reset: true,
7180
callback,
7281
})
7382

7483
// Then
75-
expect(callback).toHaveBeenCalledOnce()
76-
expect(renderAutocompletePrompt).toHaveBeenCalledOnce()
77-
expect(renderFatalError).not.toHaveBeenCalled()
78-
})
79-
80-
test('displays an error when terminal is not interactive and we are not inside a function directory', async () => {
81-
// Given
82-
let ranCallback = false
83-
vi.mocked(isTerminalInteractive).mockReturnValue(false)
84-
85-
// When
86-
await expect(
87-
inFunctionContext({
88-
path: 'random/dir',
89-
callback: async (_app, _fun) => {
90-
ranCallback = true
91-
return _app
92-
},
93-
}),
94-
).rejects.toThrowError()
95-
96-
// Then
97-
expect(ranCallback).toBe(false)
84+
expect(linkedAppContext).toHaveBeenCalledWith({
85+
directory: path,
86+
clientId: apiKey,
87+
forceRelink: true,
88+
userProvidedConfigName,
89+
})
9890
})
9991
})
10092

@@ -140,3 +132,117 @@ describe('getOrGenerateSchemaPath', () => {
140132
expect(fileExists).toHaveBeenCalledWith(expectedPath)
141133
})
142134
})
135+
136+
describe('chooseFunction', () => {
137+
let app: AppLinkedInterface
138+
let functionExtension1: ExtensionInstance
139+
let functionExtension2: ExtensionInstance
140+
let nonFunctionExtension: ExtensionInstance
141+
142+
beforeEach(async () => {
143+
functionExtension1 = await testFunctionExtension({
144+
dir: '/path/to/app/extensions/function-1',
145+
config: {
146+
name: 'function-1',
147+
type: 'product_discounts',
148+
description: 'Test function 1',
149+
build: {
150+
command: 'echo "hello world"',
151+
watch: ['src/**/*.rs'],
152+
wasm_opt: true,
153+
},
154+
api_version: '2022-07',
155+
configuration_ui: true,
156+
},
157+
})
158+
159+
functionExtension2 = await testFunctionExtension({
160+
dir: '/path/to/app/extensions/function-2',
161+
config: {
162+
name: 'function-2',
163+
type: 'product_discounts',
164+
description: 'Test function 2',
165+
build: {
166+
command: 'echo "hello world"',
167+
watch: ['src/**/*.rs'],
168+
wasm_opt: true,
169+
},
170+
api_version: '2022-07',
171+
configuration_ui: true,
172+
},
173+
})
174+
175+
nonFunctionExtension = {
176+
directory: '/path/to/app/extensions/theme',
177+
isFunctionExtension: false,
178+
handle: 'theme-extension',
179+
} as ExtensionInstance
180+
})
181+
182+
test('returns the function when path matches a function directory', async () => {
183+
// Given
184+
app = testAppLinked({allExtensions: [functionExtension1, functionExtension2, nonFunctionExtension]})
185+
186+
// When
187+
const result = await chooseFunction(app, '/path/to/app/extensions/function-1')
188+
189+
// Then
190+
expect(result).toBe(functionExtension1)
191+
expect(renderAutocompletePrompt).not.toHaveBeenCalled()
192+
})
193+
194+
test('returns the only function when app has single function and path does not match', async () => {
195+
// Given
196+
app = testAppLinked({allExtensions: [functionExtension1, nonFunctionExtension]})
197+
198+
// When
199+
const result = await chooseFunction(app, '/some/other/path')
200+
201+
// Then
202+
expect(result).toBe(functionExtension1)
203+
expect(renderAutocompletePrompt).not.toHaveBeenCalled()
204+
})
205+
206+
test('prompts user to select function when multiple functions exist and path does not match', async () => {
207+
// Given
208+
app = testAppLinked({allExtensions: [functionExtension1, functionExtension2, nonFunctionExtension]})
209+
vi.mocked(isTerminalInteractive).mockReturnValue(true)
210+
vi.mocked(renderAutocompletePrompt).mockResolvedValue(functionExtension2)
211+
212+
// When
213+
const result = await chooseFunction(app, '/some/other/path')
214+
215+
// Then
216+
expect(result).toBe(functionExtension2)
217+
expect(renderAutocompletePrompt).toHaveBeenCalledWith({
218+
message: 'Which function?',
219+
choices: [
220+
{label: functionExtension1.handle, value: functionExtension1},
221+
{label: functionExtension2.handle, value: functionExtension2},
222+
],
223+
})
224+
})
225+
226+
test('throws error when terminal is not interactive and cannot determine function', async () => {
227+
// Given
228+
app = testAppLinked({allExtensions: [functionExtension1, functionExtension2]})
229+
vi.mocked(isTerminalInteractive).mockReturnValue(false)
230+
231+
// When/Then
232+
await expect(chooseFunction(app, '/some/other/path')).rejects.toThrowError(
233+
'Run this command from a function directory or use `--path` to specify a function directory.',
234+
)
235+
expect(renderAutocompletePrompt).not.toHaveBeenCalled()
236+
})
237+
238+
test('filters out non-function extensions', async () => {
239+
// Given
240+
app = testAppLinked({allExtensions: [nonFunctionExtension]})
241+
vi.mocked(isTerminalInteractive).mockReturnValue(false)
242+
243+
// When/Then
244+
await expect(chooseFunction(app, '/some/path')).rejects.toThrowError(
245+
'Run this command from a function directory or use `--path` to specify a function directory.',
246+
)
247+
})
248+
})

packages/app/src/cli/services/function/common.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {AppLinkedInterface} from '../../models/app/app.js'
1+
import {AppInterface, AppLinkedInterface} from '../../models/app/app.js'
22
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
33
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
44
import {generateSchemaService} from '../generate-schema.js'
@@ -47,23 +47,28 @@ export async function inFunctionContext({
4747
userProvidedConfigName,
4848
})
4949

50+
const ourFunction = await chooseFunction(app, path)
51+
return callback(app, developerPlatformClient, ourFunction, organization.id)
52+
}
53+
54+
export async function chooseFunction(app: AppInterface, path: string): Promise<ExtensionInstance<FunctionConfigType>> {
5055
const allFunctions = app.allExtensions.filter(
5156
(ext) => ext.isFunctionExtension,
5257
) as ExtensionInstance<FunctionConfigType>[]
5358
const ourFunction = allFunctions.find((fun) => fun.directory === path)
59+
if (ourFunction) return ourFunction
60+
61+
if (allFunctions.length === 1 && allFunctions[0]) return allFunctions[0]
5462

55-
if (ourFunction) {
56-
return callback(app, developerPlatformClient, ourFunction, organization.id)
57-
} else if (isTerminalInteractive()) {
63+
if (isTerminalInteractive()) {
5864
const selectedFunction = await renderAutocompletePrompt({
5965
message: 'Which function?',
6066
choices: allFunctions.map((shopifyFunction) => ({label: shopifyFunction.handle, value: shopifyFunction})),
6167
})
62-
63-
return callback(app, developerPlatformClient, selectedFunction, organization.id)
64-
} else {
65-
throw new AbortError('Run this command from a function directory or use `--path` to specify a function directory.')
68+
return selectedFunction
6669
}
70+
71+
throw new AbortError('Run this command from a function directory or use `--path` to specify a function directory.')
6772
}
6873

6974
export async function getOrGenerateSchemaPath(

0 commit comments

Comments
 (0)