Skip to content

Commit

Permalink
Initial support for globs in dependency paths
Browse files Browse the repository at this point in the history
  • Loading branch information
aomarks committed Sep 9, 2024
1 parent 1232b54 commit 0aa80c7
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 16 deletions.
77 changes: 68 additions & 9 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
NamedAstNode,
ValueTypes,
} from './util/ast.js';
import {glob} from './util/glob.js';
import type {PackageJson, ScriptSyntaxInfo} from './util/package-json.js';

export interface AnalyzeResult {
Expand Down Expand Up @@ -147,11 +148,18 @@ export class Analyzer {
readonly #relevantConfigFilePaths = new Set<string>();
readonly #agent: Agent;
readonly #logger: Logger | undefined;
readonly #script: ScriptReference | undefined;

constructor(agent: Agent, logger?: Logger, filesystem?: FileSystem) {
constructor(
agent: Agent,
logger?: Logger,
filesystem?: FileSystem,
script?: ScriptReference,
) {
this.#agent = agent;
this.#logger = logger;
this.#packageJsonReader = new CachingPackageJsonReader(filesystem);
this.#script = script;
}

/**
Expand Down Expand Up @@ -785,7 +793,7 @@ export class Analyzer {
}

const unresolved = specifierResult.value;
const result = this.#resolveDependency(
const result = await this.#resolveDependency(
unresolved,
placeholder,
packageJson.jsonFile,
Expand Down Expand Up @@ -1705,24 +1713,24 @@ export class Analyzer {
*
* Note this can return 0, 1, or >1 script references.
*/
#resolveDependency(
async #resolveDependency(
dependency: JsonAstNode<string>,
context: ScriptReference,
referencingFile: JsonFile,
): Result<Array<ScriptReference>, Failure> {
): Promise<Result<Array<ScriptReference>, Failure>> {
// TODO(aomarks) Implement $WORKSPACES syntax.
if (dependency.value.startsWith('.')) {
// TODO(aomarks) It is technically valid for an npm script to start with a
// ".". We should support that edge case with backslash escaping.
const result = this.#resolveCrossPackageDependency(
const result = await this.#resolveCrossPackageDependency(
dependency,
context,
referencingFile,
);
if (!result.ok) {
return result;
}
return {ok: true, value: [result.value]};
return {ok: true, value: result.value};
}
return {
ok: true,
Expand All @@ -1734,11 +1742,11 @@ export class Analyzer {
* Resolve a cross-package dependency (e.g. "../other-package:build").
* Cross-package dependencies always start with a ".".
*/
#resolveCrossPackageDependency(
async #resolveCrossPackageDependency(
dependency: JsonAstNode<string>,
context: ScriptReference,
referencingFile: JsonFile,
): Result<ScriptReference, Failure> {
): Promise<Result<ScriptReference[], Failure>> {
// TODO(aomarks) On some file systems, it is valid to have a ":" in a file
// path. We should support that edge case with backslash escaping.
const firstColonIdx = dependency.value.indexOf(':');
Expand Down Expand Up @@ -1786,6 +1794,36 @@ export class Analyzer {
};
}
const relativePackageDir = dependency.value.slice(0, firstColonIdx);
if (relativePackageDir.includes('*')) {
// Execute the glob pattern.
const packageDirs = await this.#globPackageDirs(relativePackageDir);
console.log({packageDirs});
if (packageDirs.length === 0) {
return {
ok: false,
error: {
type: 'failure',
reason: 'invalid-config-syntax',
script: context,
diagnostic: {
severity: 'error',
message: `No packages matched the glob pattern "${relativePackageDir}"`,
location: {
file: referencingFile,
range: {offset: dependency.offset, length: dependency.length},
},
},
},
};
}
return {
ok: true,
value: packageDirs.map((packageDir) => ({
packageDir,
name: scriptName,
})),
};
}
const absolutePackageDir = pathlib.resolve(
context.packageDir,
relativePackageDir,
Expand All @@ -1812,9 +1850,30 @@ export class Analyzer {
}
return {
ok: true,
value: {packageDir: absolutePackageDir, name: scriptName},
value: [{packageDir: absolutePackageDir, name: scriptName}],
};
}

async #globPackageDirs(pattern: string): Promise<string[]> {
if (this.#script === undefined) {
// TODO(aomarks) Make it required to constructor.
throw new Error('Internal error: script must be set to do a glob');
}
const results = await glob(
[
(pattern.endsWith('/') ? pattern : pattern + '/') + 'package.json',
...DEFAULT_EXCLUDE_PATHS,
],
{
cwd: this.#script.packageDir,
followSymlinks: true,
includeDirectories: false,
expandDirectories: false,
throwIfOutsideCwd: false,
},
);
return results.map((entry) => pathlib.dirname(entry.path));
}
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ const run = async (options: Options): Promise<Result<void, Failure[]>> => {
await watcher.watch();
return {ok: true, value: undefined};
} else {
const analyzer = new Analyzer(options.agent, logger);
const analyzer = new Analyzer(
options.agent,
logger,
undefined,
options.script,
);
const {config} = await analyzer.analyze(options.script, options.extraArgs);
if (!config.ok) {
return config;
Expand Down
77 changes: 75 additions & 2 deletions src/test/analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as pathlib from 'path';
import {suite} from 'uvu';
import * as assert from 'uvu/assert';
import {
Expand Down Expand Up @@ -349,6 +350,75 @@ test(
}),
);

test.only(
'dependency package paths are expanded as globs',
rigTest(async ({rig}) => {
await rig.write({
'package.json': {
scripts: {
main: 'wireit',
},
wireit: {
main: {
command: 'true',
dependencies: ['./packages/*:build'],
},
},
},
'packages/foo/package.json': {
wireit: {
build: {command: 'true'},
test: {command: 'true'},
},
},
'packages/bar/package.json': {
wireit: {
build: {command: 'true'},
test: {command: 'true'},
},
},
'packages/baz/potato.json': {
wireit: {
build: {command: 'true'},
test: {command: 'true'},
},
},
});

const analyzer = new Analyzer('npm', undefined, undefined, {
packageDir: rig.temp,
name: 'main',
});
const result = await analyzer.analyze(
{
packageDir: rig.temp,
name: 'main',
},
[],
);
if (!result.config.ok) {
console.log(result.config.error);
throw new Error('Not ok');
}

const build = result.config.value;
assert.equal(build.dependencies?.length, 2);
const [bar, foo] = build.dependencies.sort((a, b) =>
a.config.packageDir.localeCompare(b.config.packageDir),
);
assert.equal(
bar?.config.packageDir,
pathlib.join(rig.temp, 'packages/bar'),
);
assert.equal(bar?.config.name, 'build');
assert.equal(
foo?.config.packageDir,
pathlib.join(rig.temp, 'packages/foo'),
);
assert.equal(foo?.config.name, 'build');
}),
);

test(
'dependency script name globs are expanded',
rigTest(async ({rig}) => {
Expand All @@ -372,7 +442,10 @@ test(
},
});

const analyzer = new Analyzer('npm');
const analyzer = new Analyzer('npm', undefined, undefined, {
packageDir: rig.temp,
name: 'main',
});
const result = await analyzer.analyze(
{
packageDir: rig.temp,
Expand Down Expand Up @@ -658,7 +731,7 @@ const cases: Array<[string, ParsedDependency | DiagnosticWithoutFile]> = [
] as const;

for (const [dependency, valueOrError] of cases) {
test.only(dependency, () => {
test(dependency, () => {
const actual = parseDependency(dependency);
const expected =
'severity' in valueOrError
Expand Down
8 changes: 4 additions & 4 deletions src/test/gc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@

import {suite} from 'uvu';
import * as assert from 'uvu/assert';
import {rigTest} from './util/rig-test.js';
import {Analyzer} from '../analyzer.js';
import {registerExecutionConstructorHook} from '../execution/base.js';
import {
Executor,
registerExecutorConstructorHook,
ServiceMap,
} from '../executor.js';
import {Analyzer} from '../analyzer.js';
import {DefaultLogger} from '../logging/default-logger.js';
import {WorkerPool} from '../util/worker-pool.js';
import {registerExecutionConstructorHook} from '../execution/base.js';
import {Console} from '../logging/logger.js';
import {WorkerPool} from '../util/worker-pool.js';
import {rigTest} from './util/rig-test.js';

const test = suite<object>();

Expand Down

0 comments on commit 0aa80c7

Please sign in to comment.