@@ -14,14 +14,20 @@ import {
1414} from './util/package-json-reader.js' ;
1515import { IS_WINDOWS } from './util/windows.js' ;
1616
17+ import { parseDependency } from './analysis/dependency-parser.js' ;
1718import type { Agent } from './cli-options.js' ;
1819import type {
1920 ScriptConfig ,
2021 ScriptReference ,
2122 ScriptReferenceString ,
2223} from './config.js' ;
23- import type { Diagnostic , MessageLocation , Result } from './error.js' ;
24- import type { Cycle , DependencyOnMissingPackageJson , Failure } from './event.js' ;
24+ import type { Diagnostic , MessageLocation , Range , Result } from './error.js' ;
25+ import type {
26+ Cycle ,
27+ DependencyOnMissingPackageJson ,
28+ Failure ,
29+ InvalidConfigSyntax ,
30+ } from './event.js' ;
2531import { Logger } from './logging/logger.js' ;
2632import type {
2733 ArrayNode ,
@@ -1708,109 +1714,115 @@ export class Analyzer {
17081714 context : ScriptReference ,
17091715 referencingFile : JsonFile ,
17101716 ) : Result < Array < ScriptReference > , Failure > {
1711- // TODO(aomarks) Implement $WORKSPACES syntax.
1712- if ( dependency . value . startsWith ( '.' ) ) {
1713- // TODO(aomarks) It is technically valid for an npm script to start with a
1714- // ".". We should support that edge case with backslash escaping.
1715- const result = this . #resolveCrossPackageDependency(
1716- dependency ,
1717- context ,
1718- referencingFile ,
1719- ) ;
1720- if ( ! result . ok ) {
1721- return result ;
1722- }
1723- return { ok : true , value : [ result . value ] } ;
1724- }
1725- return {
1726- ok : true ,
1727- value : [ { packageDir : context . packageDir , name : dependency . value } ] ,
1728- } ;
1729- }
1730-
1731- /**
1732- * Resolve a cross-package dependency (e.g. "../other-package:build").
1733- * Cross-package dependencies always start with a ".".
1734- */
1735- #resolveCrossPackageDependency(
1736- dependency : JsonAstNode < string > ,
1737- context : ScriptReference ,
1738- referencingFile : JsonFile ,
1739- ) : Result < ScriptReference , Failure > {
1740- // TODO(aomarks) On some file systems, it is valid to have a ":" in a file
1741- // path. We should support that edge case with backslash escaping.
1742- const firstColonIdx = dependency . value . indexOf ( ':' ) ;
1743- if ( firstColonIdx === - 1 ) {
1717+ const parsed = parseDependency ( dependency . value ) ;
1718+ if ( ! parsed . ok ) {
17441719 return {
17451720 ok : false ,
17461721 error : {
17471722 type : 'failure' ,
17481723 reason : 'invalid-config-syntax' ,
17491724 script : context ,
17501725 diagnostic : {
1751- severity : 'error' ,
1752- message :
1753- `Cross-package dependency must use syntax ` +
1754- `"<relative-path>:<script-name>", ` +
1755- `but there's no ":" character in "${ dependency . value } ".` ,
1726+ ...parsed . error ,
17561727 location : {
1728+ // The parser doesn't know about the file, add that to the
1729+ // diagnostic.
17571730 file : referencingFile ,
1758- range : { offset : dependency . offset , length : dependency . length } ,
1731+ range : {
1732+ offset :
1733+ dependency . offset + 1 + parsed . error . location . range . offset ,
1734+ length : parsed . error . location . range . length ,
1735+ } ,
17591736 } ,
17601737 } ,
17611738 } ,
17621739 } ;
17631740 }
1764- const scriptName = dependency . value . slice ( firstColonIdx + 1 ) ;
1765- if ( ! scriptName ) {
1766- return {
1767- ok : false ,
1768- error : {
1769- type : 'failure' ,
1770- reason : 'invalid-config-syntax' ,
1771- script : context ,
1772- diagnostic : {
1773- severity : 'error' ,
1774- message :
1775- `Cross-package dependency must use syntax ` +
1776- `"<relative-path>:<script-name>", ` +
1777- `but there's no script name in "${ dependency . value } ".` ,
1778- location : {
1779- file : referencingFile ,
1780- range : { offset : dependency . offset , length : dependency . length } ,
1781- } ,
1782- } ,
1783- } ,
1784- } ;
1741+
1742+ const { package : pkg , script, inverted} = parsed . value ;
1743+
1744+ if ( inverted ) {
1745+ // TODO(aomarks) Support inversion.
1746+ return invalidSyntaxError (
1747+ 'Dependency inversion operator "!" is not yet supported' ,
1748+ context ,
1749+ referencingFile ,
1750+ { offset : dependency . offset , length : 1 } ,
1751+ ) ;
17851752 }
1786- const relativePackageDir = dependency . value . slice ( 0 , firstColonIdx ) ;
1787- const absolutePackageDir = pathlib . resolve (
1788- context . packageDir ,
1789- relativePackageDir ,
1790- ) ;
1791- if ( absolutePackageDir === context . packageDir ) {
1792- return {
1793- ok : false ,
1794- error : {
1795- type : 'failure' ,
1796- reason : 'invalid-config-syntax' ,
1797- script : context ,
1798- diagnostic : {
1799- severity : 'error' ,
1800- message :
1801- `Cross-package dependency "${ dependency . value } " ` +
1802- `resolved to the same package.` ,
1803- location : {
1804- file : referencingFile ,
1805- range : { offset : dependency . offset , length : dependency . length } ,
1806- } ,
1753+
1754+ let packageDir : string ;
1755+ if ( pkg . kind === 'this' ) {
1756+ packageDir = context . packageDir ;
1757+ } else if ( pkg . kind === 'path' ) {
1758+ packageDir = pathlib . resolve ( context . packageDir , pkg . path ) ;
1759+ if ( packageDir === context . packageDir ) {
1760+ return invalidSyntaxError (
1761+ `Cross-package dependency "${ dependency . value } " ` +
1762+ `resolved to the same package.` ,
1763+ context ,
1764+ referencingFile ,
1765+ {
1766+ offset : dependency . offset + 1 + pkg . range . offset ,
1767+ length : pkg . range . length ,
18071768 } ,
1769+ ) ;
1770+ }
1771+ } else if ( pkg . kind === 'npm' ) {
1772+ // TODO(aomarks) Support npm resolution.
1773+ return invalidSyntaxError (
1774+ 'NPM packages are not yet supported' ,
1775+ context ,
1776+ referencingFile ,
1777+ {
1778+ offset : dependency . offset + 1 + pkg . range . offset ,
1779+ length : pkg . range . length ,
18081780 } ,
1809- } ;
1781+ ) ;
1782+ } else if ( pkg . kind === 'dependencies' ) {
1783+ // TODO(aomarks) Support "dependencies".
1784+ return invalidSyntaxError (
1785+ `"dependencies" is not yet supported` ,
1786+ context ,
1787+ referencingFile ,
1788+ {
1789+ offset : dependency . offset + 1 + pkg . range . offset ,
1790+ length : pkg . range . length ,
1791+ } ,
1792+ ) ;
1793+ } else if ( pkg . kind === 'workspaces' ) {
1794+ // TODO(aomarks) Support "workspacess".
1795+ return invalidSyntaxError (
1796+ `"workspaces" is not yet supported` ,
1797+ context ,
1798+ referencingFile ,
1799+ {
1800+ offset : dependency . offset + 1 + pkg . range . offset ,
1801+ length : pkg . range . length ,
1802+ } ,
1803+ ) ;
1804+ } else {
1805+ pkg satisfies never ;
1806+ throw new Error (
1807+ `Unexpected parsed package format: ${ JSON . stringify ( pkg ) } ` ,
1808+ ) ;
1809+ }
1810+
1811+ let scriptName : string ;
1812+ if ( script . kind === 'name' ) {
1813+ scriptName = script . name ;
1814+ } else if ( script . kind === 'this' ) {
1815+ scriptName = context . name ;
1816+ } else {
1817+ script satisfies never ;
1818+ throw new Error (
1819+ `Unexpected parsed script format: ${ JSON . stringify ( script ) } ` ,
1820+ ) ;
18101821 }
1822+
18111823 return {
18121824 ok : true ,
1813- value : { packageDir : absolutePackageDir , name : scriptName } ,
1825+ value : [ { packageDir, name : scriptName } ] ,
18141826 } ;
18151827 }
18161828}
@@ -1967,3 +1979,22 @@ export const failUnlessKeyValue = (
19671979 }
19681980 return { ok : true , value : [ rawName , rawValue ] } ;
19691981} ;
1982+
1983+ const invalidSyntaxError = (
1984+ message : string ,
1985+ script : ScriptReference ,
1986+ file : JsonFile ,
1987+ range : Range ,
1988+ ) : { ok : false ; error : InvalidConfigSyntax } => ( {
1989+ ok : false ,
1990+ error : {
1991+ type : 'failure' ,
1992+ reason : 'invalid-config-syntax' ,
1993+ script,
1994+ diagnostic : {
1995+ message,
1996+ severity : 'error' ,
1997+ location : { file, range} ,
1998+ } ,
1999+ } ,
2000+ } ) ;
0 commit comments