Skip to content
Draft
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
10 changes: 10 additions & 0 deletions packages/cli-kit/src/public/node/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
collectedLogs,
clearCollectedLogs,
LogLevel,
debugMessageSink,
outputDebug,
outputWhereAppropriate,
outputToken,
Expand Down Expand Up @@ -31,6 +32,15 @@ beforeEach(() => {
})

describe('Output helpers', () => {
test('delegates debug events only to outputDebug', () => {
isUnitTestMock.mockReturnValue(true)

debugMessageSink({level: 'debug', message: 'debug'})
debugMessageSink({level: 'info', message: 'info'})
debugMessageSink({level: 'warning', message: 'warning'})

expect(collectedLogs.debug).toEqual(['debug'])
})
test('can format dependency manager commands with flags', () => {
expect(outputToken.packagejsonScript('yarn', 'dev', '--reset').value).toEqual('yarn dev --reset')
expect(outputToken.packagejsonScript('npm', 'dev', '--reset').value).toEqual('npm run dev -- --reset')
Expand Down
14 changes: 14 additions & 0 deletions packages/cli-kit/src/public/node/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ export class TokenizedString {

export type OutputMessage = string | TokenizedString

interface DiagnosticEvent {
level: 'debug' | 'info' | 'warning'
message: string
}

/**
* Adapts a debug-level diagnostic event to the CLI's existing debug output behavior.
*
* @param event - Diagnostic event to adapt.
*/
export function debugMessageSink(event: DiagnosticEvent): void {
if (event.level === 'debug') outputDebug(event.message)
}

export const outputToken = {
raw(value: string): RawContentToken {
return new RawContentToken(value)
Expand Down
1 change: 0 additions & 1 deletion packages/cli-kit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@
"rootDir": "src",
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
},
"references": []
}
40 changes: 40 additions & 0 deletions packages/diagnostics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@shopify/diagnostics",
"version": "4.5.0",
"packageManager": "pnpm@10.11.1",
"private": true,
"description": "Dependency-free synchronous diagnostic primitives",
"license": "MIT",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": ["/dist"],
"scripts": {
"build": "nx build",
"clean": "nx clean",
"lint": "nx lint",
"lint:fix": "nx lint:fix",
"vitest": "vitest",
"type-check": "nx type-check"
},
"eslintConfig": {
"extends": [
"../../.eslintrc.cjs"
]
},
"devDependencies": {
"@vitest/coverage-istanbul": "^3.2.7"
},
"engines": {
"node": ">=22.12.0"
},
"os": [
"darwin",
"linux",
"win32"
]
}
46 changes: 46 additions & 0 deletions packages/diagnostics/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@shopify/diagnostics",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/diagnostics/src",
"projectType": "library",
"tags": ["npm:private"],
"targets": {
"clean": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm rimraf dist/",
"cwd": "packages/diagnostics"
}
},
"build": {
"executor": "nx:run-commands",
"outputs": ["{projectRoot}/dist"],
"inputs": ["{projectRoot}/src/**/*", "{projectRoot}/package.json"],
"options": {
"command": "pnpm tsc -b ./tsconfig.build.json",
"cwd": "packages/diagnostics"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm eslint 'src/**/*.ts'",
"cwd": "packages/diagnostics"
}
},
"lint:fix": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm eslint 'src/**/*.ts' --fix",
"cwd": "packages/diagnostics"
}
},
"type-check": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm tsc --noEmit",
"cwd": "packages/diagnostics"
}
}
}
}
19 changes: 19 additions & 0 deletions packages/diagnostics/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {createSyncDiagnosticChannel} from './index.js'
import {describe, expect, test} from 'vitest'

describe('createSyncDiagnosticChannel', () => {
test('delivers synchronously in registration order and isolates observer failures', () => {
const calls: string[] = []
const channel = createSyncDiagnosticChannel<{level: 'debug'; message: string; value: string}>(
() => {
calls.push('first')
throw new Error('failure')
},
() => calls.push('second'),
)

channel.emit({level: 'debug', message: 'message', value: 'value'})

expect(calls).toEqual(['first', 'second'])
})
})
56 changes: 56 additions & 0 deletions packages/diagnostics/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* eslint-disable no-catch-all/no-catch-all */

/**
* Data-only, synchronous diagnostic primitives for the event-sink PoC.
*
* This module intentionally has no imports. Domain packages can depend on this leaf without
* introducing terminal, renderer, or domain dependencies.
*/
export type DiagnosticLevel = 'debug' | 'info' | 'warning'

/**
* Data emitted by command execution to describe a diagnostic.
*
* Domain-specific events can extend this shape with a discriminant and structured fields. The
* event carries no terminal or renderer objects, so adapters can render, suppress, record, or
* route it without changing the execution code.
*/
export interface DiagnosticEvent {
readonly level: DiagnosticLevel
readonly message: string
}

/** Receives one diagnostic from an execution channel. */
export type DiagnosticObserver<TEvent extends DiagnosticEvent = DiagnosticEvent> = (event: TEvent) => void

/**
* Delivers diagnostics from execution to one or more observers.
*
* This PoC delivers events synchronously. Async delivery can remain a compatible future extension
* without making execution depend on an async channel API today.
*/
export interface SyncDiagnosticChannel<TEvent extends DiagnosticEvent = DiagnosticEvent> {
emit(event: TEvent): void
}

/**
* Creates a synchronous channel that calls observers in registration order.
*
* Diagnostic observers are advisory. A failing observer is isolated so it cannot change the
* command result or prevent later observers from receiving the event.
*/
export function createSyncDiagnosticChannel<TEvent extends DiagnosticEvent = DiagnosticEvent>(
...observers: DiagnosticObserver<TEvent>[]
): SyncDiagnosticChannel<TEvent> {
return {
emit(event) {
for (const observer of observers) {
try {
observer(event)
} catch {
// Optional diagnostic observers must not change the command result.
}
}
},
}
}
4 changes: 4 additions & 0 deletions packages/diagnostics/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["**/*.test.ts"]
}
11 changes: 11 additions & 0 deletions packages/diagnostics/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../configurations/tsconfig.json",
"include": ["./src/**/*.ts"],
"exclude": ["./dist"],
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
},
"references": []
}
1 change: 1 addition & 0 deletions packages/store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@graphql-typed-document-node/core": "3.2.0",
"@oclif/core": "4.8.3",
"@shopify/cli-kit": "4.5.0",
"@shopify/diagnostics": "4.5.0",
"@shopify/organizations": "4.5.0"
},
"devDependencies": {
Expand Down
7 changes: 6 additions & 1 deletion packages/store/src/cli/commands/store/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {getStoreInfo} from '../../services/store/info/index.js'
import {renderStoreInfoResult} from '../../services/store/info/result.js'
import StoreCommand from '../../utilities/store-command.js'
import {storeFlags} from '../../flags.js'
import {createSyncDiagnosticChannel} from '@shopify/diagnostics'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {debugMessageSink} from '@shopify/cli-kit/node/output'

export default class StoreInfo extends StoreCommand {
static summary = 'Surface metadata about a Shopify store.'
Expand All @@ -29,7 +31,10 @@ Use \`--json\` for machine-readable output.`
public async run(): Promise<void> {
const {flags} = await this.parse(StoreInfo)

const result = await getStoreInfo({store: flags.store})
const result = await getStoreInfo({
store: flags.store,
context: {diagnostics: createSyncDiagnosticChannel(debugMessageSink)},
})

renderStoreInfoResult(result, flags.json ? 'json' : 'text')
}
Expand Down
Loading
Loading