-
Notifications
You must be signed in to change notification settings - Fork 125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Themes] Theme Console - Handle the REPL lifecycle #4224
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
68fd049
Initiate REPL Loop
jamesmengo 3f64f7b
Add welcome message to liquid console
jamesmengo b5e70ea
Add shutdown handling when errors occur
jamesmengo 04d70ba
refactor: Clean up DevServerSession initialization
jamesmengo 1ec35cf
feat: Add delimitering warning when liquid delimiters are used
jamesmengo 781c141
Move REPL loop into its own module
jamesmengo 709e4a8
Add support for URL flag
jamesmengo 45304b8
Print results in cyan
jamesmengo a20f04b
Handle JSON Error
jamesmengo 8bf6b6e
Refator - Cleanup
jamesmengo 31faf44
Extract + Test Presentation Module
jamesmengo 8e3179d
Move evaluator into repl folder
jamesmengo 57638df
Update error handling output and add tests
jamesmengo fe300f8
Use readline to read input for the REPL loop
jamesmengo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import {handleInput} from './repl.js' | ||
import {DevServerSession} from './theme-environment/types.js' | ||
import {evaluate} from './repl/evaluater.js' | ||
import {presentValue} from './repl/presenter.js' | ||
import {describe, expect, test, vi} from 'vitest' | ||
import {consoleWarn} from '@shopify/cli-kit/node/output' | ||
import {createInterface} from 'readline' | ||
|
||
vi.mock('@shopify/cli-kit/node/output') | ||
vi.mock('./repl/evaluater.js') | ||
vi.mock('./repl/presenter.js') | ||
|
||
describe('handleInput', () => { | ||
const themeSesssion: DevServerSession = { | ||
storefrontPassword: 'password', | ||
token: 'token', | ||
expiresAt: new Date(), | ||
storeFqdn: 'store.myshopify.com', | ||
storefrontToken: 'storefrontToken', | ||
} | ||
const themeId = '123' | ||
const url = '/' | ||
const rl = createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
}) | ||
|
||
test('should call consoleWarn and prompt readline if input has delimiter', async () => { | ||
// Given | ||
const inputValue = '{{ collections.first }}' | ||
|
||
// When | ||
await handleInput(inputValue, themeSesssion, themeId, url, rl) | ||
|
||
// Then | ||
expect(consoleWarn).toHaveBeenCalled() | ||
}) | ||
|
||
test('should call evaluate, presentValue, and prompt readline if input is valid', async () => { | ||
// Given | ||
const inputValue = 'collections.first' | ||
|
||
// When | ||
await handleInput(inputValue, themeSesssion, themeId, url, rl) | ||
|
||
// Then | ||
expect(consoleWarn).not.toHaveBeenCalled() | ||
expect(evaluate).toHaveBeenCalled() | ||
expect(presentValue).toHaveBeenCalled() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import {DevServerSession} from './theme-environment/types.js' | ||
import {evaluate} from './repl/evaluater.js' | ||
import {presentValue} from './repl/presenter.js' | ||
import {AbortSilentError} from '@shopify/cli-kit/node/error' | ||
import {consoleWarn, outputContent, outputDebug, outputInfo, outputToken} from '@shopify/cli-kit/node/output' | ||
import {createInterface, Interface} from 'readline' | ||
|
||
export async function replLoop(themeSession: DevServerSession, themeId: string, url: string) { | ||
try { | ||
if (process.stdin.isTTY) { | ||
// We want to indicate that we're still using stdin, so that the process | ||
// doesn't exit early. | ||
process.stdin.ref() | ||
} | ||
|
||
const rl = createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
}) | ||
|
||
rl.on('line', (input) => { | ||
// eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
handleInput(input, themeSession, themeId, url, rl) | ||
}) | ||
rl.prompt() | ||
} catch (error) { | ||
shutdownReplSession(error) | ||
throw new AbortSilentError() | ||
} | ||
} | ||
|
||
export async function handleInput( | ||
inputValue: string, | ||
themeSession: DevServerSession, | ||
themeId: string, | ||
url: string, | ||
rl: Interface, | ||
) { | ||
if (hasDelimiter(inputValue)) { | ||
consoleWarn( | ||
"Liquid Console doesn't support Liquid delimiters such as '{{ ... }}' or '{% ... %}'.\nPlease use 'collections.first' instead of '{{ collections.first }}'.", | ||
) | ||
return rl.prompt() | ||
} | ||
const evaluatedValue = await evaluate(themeSession, inputValue, themeId, url) | ||
presentValue(evaluatedValue) | ||
rl.prompt() | ||
} | ||
|
||
function shutdownReplSession(error: unknown) { | ||
if (error instanceof Error) { | ||
outputInfo(outputContent`${outputToken.errorText(`Shopify Liquid console error: ${error.message}`)}`) | ||
outputDebug(error.stack || 'Error backtrace not found') | ||
} | ||
} | ||
|
||
function hasDelimiter(input: string): boolean { | ||
return /\{\{|\}\}|\{%|%\}/.test(input) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import {render} from '../theme-environment/storefront-renderer.js' | ||
import {DevServerSession} from '../theme-environment/types.js' | ||
import {outputContent, outputDebug, outputInfo, outputToken} from '@shopify/cli-kit/node/output' | ||
|
||
export async function evaluate( | ||
themeSession: DevServerSession, | ||
snippet: string, | ||
themeId: string, | ||
url: string, | ||
): Promise<string | undefined> { | ||
const result = await evaluateResult(themeSession, themeId, snippet, url) | ||
try { | ||
return sanitizeResult(result) | ||
// eslint-disable-next-line no-catch-all/no-catch-all, @typescript-eslint/no-explicit-any | ||
} catch (error: any) { | ||
outputInfo(outputContent`${outputToken.errorText(error.message)}`) | ||
outputDebug(error.stack || 'Error backtrace not found') | ||
} | ||
} | ||
|
||
async function evaluateResult(themeSession: DevServerSession, themeId: string, snippet: string, url: string) { | ||
outputDebug(`Evaluating snippet - ${snippet}`) | ||
const response = await render(themeSession, { | ||
path: url, | ||
query: [], | ||
themeId, | ||
cookies: '', | ||
sectionId: 'announcement-bar', | ||
headers: {}, | ||
replaceTemplates: { | ||
'sections/announcement-bar.liquid': `{{ ${snippet} | json }}`, | ||
}, | ||
}) | ||
|
||
return response.text() | ||
} | ||
|
||
function sanitizeResult(result: string) { | ||
const regex = />([^<]+)</ | ||
const match = result.match(regex) | ||
|
||
if (match && match[1]) { | ||
return JSON.parse(match[1]) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import {presentValue} from './presenter.js' | ||
import {consoleWarn, outputContent, outputInfo, outputToken} from '@shopify/cli-kit/node/output' | ||
import {describe, expect, test, vi} from 'vitest' | ||
|
||
vi.mock('@shopify/cli-kit/node/output') | ||
|
||
describe('presentValue', () => { | ||
test('should print a warning message if value has a JSON error', () => { | ||
// Given | ||
const value = {error: 'json not allowed for this object'} | ||
|
||
// When | ||
presentValue(value) | ||
|
||
// Then | ||
expect(consoleWarn).toHaveBeenCalledWith( | ||
"Object can't be printed, but you can access its fields. Read more at https://shopify.dev/docs/api/liquid.", | ||
) | ||
expect(outputInfo).not.toHaveBeenCalled() | ||
}) | ||
|
||
test('should print "null" if value is undefined', () => { | ||
// Given | ||
const value = undefined | ||
|
||
// When | ||
presentValue(value) | ||
|
||
// Then | ||
expect(outputInfo).toHaveBeenCalledWith(outputContent`${outputToken.cyan('null')}`) | ||
expect(consoleWarn).not.toHaveBeenCalled() | ||
}) | ||
|
||
// if null | ||
test('should print "null" if value is null', () => { | ||
// Given | ||
const value = null | ||
|
||
// When | ||
presentValue(value) | ||
|
||
// Then | ||
expect(outputInfo).toHaveBeenCalledWith(outputContent`${outputToken.cyan('null')}`) | ||
expect(consoleWarn).not.toHaveBeenCalled() | ||
}) | ||
|
||
test('should print the formatted output if value is not undefined, null, or has a JSON error', () => { | ||
// Given | ||
const value = {foo: 'bar'} | ||
const formattedOutput = JSON.stringify(value, null, 2) | ||
|
||
// When | ||
presentValue(value) | ||
|
||
// Then | ||
expect(outputInfo).toHaveBeenCalledWith(outputContent`${outputToken.cyan(formattedOutput)}`) | ||
expect(consoleWarn).not.toHaveBeenCalled() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import {consoleWarn, outputContent, outputInfo, outputToken} from '@shopify/cli-kit/node/output' | ||
|
||
export function presentValue(value?: unknown) { | ||
if (hasJsonError(value)) { | ||
consoleWarn( | ||
"Object can't be printed, but you can access its fields. Read more at https://shopify.dev/docs/api/liquid.", | ||
) | ||
return | ||
} | ||
|
||
if (value === undefined || value === null) { | ||
renderValue('null') | ||
return | ||
} | ||
|
||
const formattedOutput = JSON.stringify(value, null, 2) | ||
renderValue(formattedOutput) | ||
} | ||
|
||
function hasJsonError(output: unknown): boolean { | ||
switch (typeof output) { | ||
case 'object': | ||
if (Array.isArray(output)) { | ||
return hasJsonError(output[0]) | ||
} else if (output !== null) { | ||
const errorOutput = output as {error?: string} | ||
return errorOutput.error?.includes('json not allowed for this object') ?? false | ||
} | ||
return false | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
function renderValue(value: string) { | ||
return outputInfo(outputContent`${outputToken.cyan(value)}`) | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This matches the Ruby implementation, but I wonder if we could be a bit more specific; something like this:
/^\s*(\{\{|{%)/
, this way we may avoid from failing in some specific valid use cases :)