Skip to content
Open
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
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"activationEvents": [
"onLanguage:yaml",
"workspaceContains:melos.yaml",
"workspaceContains:pubspec.yaml",
"onCommand:melos.bootstrap",
"onCommand:melos.clean",
"onCommand:melos.runScript",
Expand Down Expand Up @@ -100,6 +101,16 @@
"command": "melos.bootstrap",
"when": "resourceFilename == melos.yaml",
"group": "navigation"
},
{
"command": "melos.showPackageGraph",
"when": "resourceFilename == pubspec.yaml",
"group": "navigation"
},
{
"command": "melos.bootstrap",
"when": "resourceFilename == pubspec.yaml",
"group": "navigation"
}
]
}
Expand Down
58 changes: 46 additions & 12 deletions src/code-lenses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,75 @@ import { MelosRunScriptCommandArgs } from './commands'
import { debug } from './logging'
import { vscodeRangeFromNode } from './utils/yaml-utils'
import {
melosYamlFile,
MelosWorkspaceConfig,
parseMelosWorkspaceConfig,
parsePubspecWithMelosConfig,
pubspecYamlFile,
} from './workspace-config'

export function registerMelosYamlCodeLenseProvider(
context: vscode.ExtensionContext
) {
const provider = new MelosCodeLensProvider()

context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
{ language: 'yaml', pattern: '**/melos.yaml' },
new MelosYamlCodeLenseProvider()
provider
)
)

context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
{ language: 'yaml', pattern: '**/pubspec.yaml' },
provider
)
)
}

class MelosYamlCodeLenseProvider implements vscode.CodeLensProvider {
class MelosCodeLensProvider implements vscode.CodeLensProvider {
async provideCodeLenses(document: vscode.TextDocument) {
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
const melosConfig = parseMelosWorkspaceConfig(document.getText())
if (!workspaceFolder) {
return []
}

const melosConfig = this.resolveConfig(document, workspaceFolder)
if (!melosConfig) {
return []
}

return [
...this.buildRunScriptCodeLenses(melosConfig, workspaceFolder, document),
]
return this.buildRunScriptCodeLenses(melosConfig, workspaceFolder, document)
}

private resolveConfig(
document: vscode.TextDocument,
workspaceFolder: vscode.WorkspaceFolder
): MelosWorkspaceConfig | null {
const fileName = document.uri.path.split('/').pop()

if (fileName === melosYamlFile) {
return parseMelosWorkspaceConfig(document.getText())
}

if (fileName === pubspecYamlFile) {
// Only provide lenses for the root pubspec.yaml, not package-level files.
const rootUri = vscode.Uri.joinPath(workspaceFolder.uri, pubspecYamlFile)
if (document.uri.toString() !== rootUri.toString()) {
return null
}
return parsePubspecWithMelosConfig(document.getText())
}

return null
}

private buildRunScriptCodeLenses(
melosConfig: MelosWorkspaceConfig,
workspaceFolder: vscode.WorkspaceFolder | undefined,
workspaceFolder: vscode.WorkspaceFolder,
document: vscode.TextDocument
) {
if (!workspaceFolder) {
// We need a workspace folder to run scripts.
return []
}

debug(
`Providing 'Run script' CodeLenses in '${workspaceFolder.name}' folder`,
melosConfig.scripts.map((script) => script.name.value)
Expand Down
4 changes: 3 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ function runScriptCommandHandler(context: vscode.ExtensionContext) {

const melosConfig = await loadMelosWorkspaceConfig(context, workspaceFolder)
if (!melosConfig) {
vscode.window.showErrorMessage('No melos.yaml file found.')
vscode.window.showErrorMessage(
'No Melos configuration found. Add a melos.yaml file or a melos: key in pubspec.yaml.'
)
return
}

Expand Down
63 changes: 59 additions & 4 deletions src/melos-workspace.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as vscode from 'vscode'
import { debug, info } from './logging'
import { fileExists } from './utils/fs-utils'
import { fileExists, readOptionalFile } from './utils/fs-utils'
import { isOpenWorkspaceFolder } from './utils/vscode-utils'
import { melosYamlFile } from './workspace-config'
import {
isDartWorkspacePubspec,
melosYamlFile,
pubspecYamlFile,
} from './workspace-config'

/**
* API for the currently opened Melos workspaces.
Expand Down Expand Up @@ -86,6 +90,46 @@ export async function initMelosWorkspaces(context: vscode.ExtensionContext) {
}
})
)

// Watch root pubspec.yaml files for creation/deletion (Dart workspace, Melos 7+).
// Content changes are not watched here since they fire on every package save.
const pubspecYamlWatcher = vscode.workspace.createFileSystemWatcher(
`**/${pubspecYamlFile}`,
false,
true,
false
)
context.subscriptions.push(pubspecYamlWatcher)

context.subscriptions.push(
pubspecYamlWatcher.onDidCreate(async (uri) => {
debug(`pubspec.yaml onDidCreate: ${uri}`)

const folder = vscode.workspace.getWorkspaceFolder(uri)
if (folder) {
const pubspecAtRoot = vscode.Uri.joinPath(folder.uri, pubspecYamlFile)
if (
uri.toString() === pubspecAtRoot.toString() &&
(await isMelosWorkspace(folder))
) {
addMelosWorkspace(folder)
}
}
})
)

context.subscriptions.push(
pubspecYamlWatcher.onDidDelete(async (uri) => {
debug(`pubspec.yaml onDidDelete: ${uri}`)

const folder = vscode.workspace.getWorkspaceFolder(uri)
if (folder) {
if (!(await isMelosWorkspace(folder))) {
removeMelosWorkspace(folder)
}
}
})
)
}

const workspaceFolders: vscode.WorkspaceFolder[] = []
Expand Down Expand Up @@ -122,8 +166,19 @@ function removeMelosWorkspace(folder: vscode.WorkspaceFolder) {
})
}

export async function isMelosWorkspace(folder: vscode.WorkspaceFolder) {
return fileExists(vscode.Uri.joinPath(folder.uri, melosYamlFile))
export async function isMelosWorkspace(
folder: vscode.WorkspaceFolder
): Promise<boolean> {
// Old-style: melos.yaml at workspace root (Melos <7)
if (await fileExists(vscode.Uri.joinPath(folder.uri, melosYamlFile))) {
return true
}

// New-style: pubspec.yaml with `workspace:` key (Dart workspace, Melos 7+)
const pubspecContent = await readOptionalFile(
vscode.Uri.joinPath(folder.uri, pubspecYamlFile)
)
return pubspecContent !== null && isDartWorkspacePubspec(pubspecContent.toString())
}

async function getMelosWorkspaceFolders() {
Expand Down
126 changes: 125 additions & 1 deletion src/test/suite/workspace-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as assert from 'assert'
import { parseMelosWorkspaceConfig } from '../../workspace-config'
import {
isDartWorkspacePubspec,
parseMelosWorkspaceConfig,
parsePubspecWithMelosConfig,
} from '../../workspace-config'

suite('MelosWorkspaceConfig', () => {
suite('parseMelosWorkspaceConfig', () => {
Expand Down Expand Up @@ -181,5 +185,125 @@ scripts:
flutter: undefined,
})
})

test('sets configSource to melos.yaml', () => {
const config = parseMelosWorkspaceConfig(`scripts:\n a: echo hi\n`)
assert.strictEqual(config.configSource, 'melos.yaml')
})
})
})

suite('isDartWorkspacePubspec', () => {
test('returns true when workspace: key is present', () => {
assert.strictEqual(
isDartWorkspacePubspec(`
name: my_workspace
environment:
sdk: '>=3.6.0 <4.0.0'
workspace:
- packages/a
- packages/b
`),
true
)
})

test('returns false when workspace: key is absent', () => {
assert.strictEqual(
isDartWorkspacePubspec(`
name: my_package
environment:
sdk: '>=3.0.0 <4.0.0'
`),
false
)
})

test('returns false for empty content', () => {
assert.strictEqual(isDartWorkspacePubspec(''), false)
})
})

suite('parsePubspecWithMelosConfig', () => {
test('returns null when melos: key is absent', () => {
const config = parsePubspecWithMelosConfig(`
name: my_workspace
workspace:
- packages/a
`)
assert.strictEqual(config, null)
})

test('parses scripts from melos: key', () => {
const config = parsePubspecWithMelosConfig(`
name: my_workspace
workspace:
- packages/a
melos:
scripts:
build: flutter build apk
test: flutter test
`)

assert.ok(config)
assert.strictEqual(config.configSource, 'pubspec.yaml')
assert.strictEqual(config.scripts.length, 2)

const build = config.scripts.find((s) => s.name.value === 'build')!
assert.strictEqual(build.name.value, 'build')
assert.strictEqual(build.run?.value, 'flutter build apk')

const test = config.scripts.find((s) => s.name.value === 'test')!
assert.strictEqual(test.name.value, 'test')
assert.strictEqual(test.run?.value, 'flutter test')
})

test('parses melos exec script from pubspec.yaml', () => {
const config = parsePubspecWithMelosConfig(`
name: my_workspace
workspace:
- packages/a
melos:
scripts:
analyze: melos exec -- dart analyze
`)

assert.ok(config)
assert.deepStrictEqual(config.scripts[0].run?.melosExec, {
options: [],
command: 'dart analyze',
})
})

test('returns empty scripts when melos: has no scripts key', () => {
const config = parsePubspecWithMelosConfig(`
name: my_workspace
workspace:
- packages/a
melos:
command:
bootstrap:
usePubspecOverrides: true
`)

assert.ok(config)
assert.strictEqual(config.scripts.length, 0)
})

test('preserves yaml node positions for CodeLens', () => {
const config = parsePubspecWithMelosConfig(
`name: my_workspace
workspace:
- packages/a
melos:
scripts:
build: flutter build apk
`
)

assert.ok(config)
assert.strictEqual(config.scripts.length, 1)
// The node position must be non-null to allow CodeLens rendering
assert.ok(config.scripts[0].name.yamlNode)
})
})
Loading