Skip to content

Commit f85c77f

Browse files
JasonVMotido64
andauthored
feat(yarn-plugin-external-workspaces): Add a yarn plugin for adding external workspace support (#3504)
* add initial yarn plugin files * update yarn lock file * changes to make the plugin work in the test repo * split core utilities into a shared package * add capability to load the module from the repo environment * change logging signature and add tags for unexpected functions * lock file changes * dedupe packages using --strategy highest * remove unused dependencies * docs(changeset): Create a yarn-plugin to handle external workspaces and a shared package for the behavior * update readme files for external-workspaces plugin * update to use non-yarn based bundle command * update to use new bundling logic * rename produced bundle * merge external-workspaces package into tools-workspaces * move external-workspaces code into the tools-workspaces package * finish switch to using tools-workspaces * rework workspace includes to improve tree-shaking * reorganize tools-workspaces to separate the package manager implementations * fix last part of tree shaking for the bundle * add output workspaces functionality * turn on workspaces output * update to non-test format * committing intermediate changes in case reversion is needed * working external workspaces plugin with usage in repo * remove old changeset * clean up old external-workspaces package * docs(changeset): Add yarn plugin package for handling external-workspaces with supporting code in tools-workspaces * update readme for tools-workspaces * remove unused dependency * readme updates * update lockfile after semver cleanup * update tools-workspaces/external with contained settings * update plugin with fetcher * update repo with fetcher based approach * in progress changes * commit in-flight changes to update PR with current * update to shared tracker between the fetcher and resolver * plugin rework to handle all cases * consolidate code in plugin directory * more cleanup of workspaces plugin * knip and update-readme results * cleanup types and comments * revert tools-workspaces changes and fix lockfile * re-enable plugin for repo * add self-installer, produce typescript output as well, and update README * remove unused files * add lockfile changes for the bin entry in yarn-plugin-external-workspaces * Apply suggestions from code review Co-authored-by: Tommy Nguyen <[email protected]> * code review feedback * remove unused package.json entries and unused function --------- Co-authored-by: Tommy Nguyen <[email protected]>
1 parent 11fe202 commit f85c77f

26 files changed

+1653
-7
lines changed

.changeset/ninety-pumpkins-lick.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rnx-kit/yarn-plugin-external-workspaces": minor
3+
---
4+
5+
Add yarn plugin package for handling external-workspaces with supporting code in
6+
tools-workspaces
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* eslint-disable */
2+
//prettier-ignore
3+
module.exports = {
4+
name: "@rnx-kit/yarn-plugin-external-workspaces",
5+
factory: function (require) {
6+
"use strict";var plugin=(()=>{var gt=Object.create;var b=Object.defineProperty;var kt=Object.getOwnPropertyDescriptor;var mt=Object.getOwnPropertyNames;var yt=Object.getPrototypeOf,Pt=Object.prototype.hasOwnProperty;var c=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(t,e)=>(typeof require<"u"?require:t)[e]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+r+'" is not supported')});var vt=(r,t)=>{for(var e in t)b(r,e,{get:t[e],enumerable:!0})},G=(r,t,e,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of mt(t))!Pt.call(r,n)&&n!==e&&b(r,n,{get:()=>t[n],enumerable:!(o=kt(t,n))||o.enumerable});return r};var S=(r,t,e)=>(e=r!=null?gt(yt(r)):{},G(t||!r||!r.__esModule?b(e,"default",{value:r,enumerable:!0}):e,r)),wt=r=>G(b({},"__esModule",{value:!0}),r);var Wt={};vt(Wt,{default:()=>Ft});var C=c("@yarnpkg/core"),m=c("@yarnpkg/fslib"),I=S(c("fs"));var q="externalWorkspacesProvider",V="externalWorkspacesOutputPath",z="externalWorkspacesOutputOnlyOnCommand",X={[q]:{description:"Relative path to a .json file of shape WorkspaceOutputJson or a .js file that exports a function of type DefinitionFinder as the default export",type:C.SettingsType.STRING,default:null},[V]:{description:"Relative path to a .json file where workspace info should be recorded. If a directory is provided the file will pick up the name from the root package.json",type:C.SettingsType.STRING,default:null},[z]:{description:"Suppress writing out the workspaces on install and only write them out when the command is invoked",type:C.SettingsType.BOOLEAN,default:!1}};function Y(r){return m.npath.toPortablePath(typeof r=="string"?r:"")}function y(r){let t=Y(r.get(q)),e=Y(r.get(V)),o=!!r.get(z);return{provider:t,outputPath:e,outputOnlyOnCommand:o}}function Q(r){if(!I.default.existsSync(r))throw new Error(`Unable to find external workspaces definition file ${r}`);let t=JSON.parse(I.default.readFileSync(r,"utf8"))?.generated||{},e=typeof t.generated=="object"?t.generated:{},{repoPath:o="",workspaces:n={}}=e,s=m.npath.toPortablePath(o);return a=>{let i=n[a];return i?{path:m.ppath.join(s,m.npath.toPortablePath(i))}:null}}function Z(r){if(!I.default.existsSync(r))throw new Error(`Unable to find external workspaces definition file ${r}`);let t=c(r).default;if(typeof t!="function")throw new Error(`External workspaces definition file ${r} does not export a function as default`);return t}var x=c("@yarnpkg/fslib");var v=c("@yarnpkg/core"),p=c("@yarnpkg/fslib"),at=S(c("fs"));var M=c("@yarnpkg/core");function tt(r){let t=r.indexOf(":");return t!==-1?{protocol:r.slice(0,t+1),version:r.slice(t+1)}:{protocol:"",version:r}}function et(r,t){if(r.range.startsWith(t))return r;let{version:e}=tt(r.range);return M.structUtils.makeDescriptor(r,`${t}${e}`)}function rt(r,t){let{protocol:e,version:o}=tt(r.reference);return e===t?r:M.structUtils.makeLocator(r,`${t}${o}`)}var P="external:",W="fallback:",ot="npm:",nt=[P,W,ot],xt=nt.map(r=>r.slice(0,-1)+"Dependency"),st=[W,P,ot],Ot=st.map(r=>r.slice(0,-1)+"Dependency"),F=class{constructor(t,e,o,n){this.packages=[null,null,null];this.localIndex=0;this.remoteIndex=1;this.fallbackIndex=2;this.name=t,this.localPath=o,this.isLocal=!!o,this.prettyName=e,this.trace=n,this.protocols=this.isLocal?nt:st,this.dependentKeys=this.isLocal?xt:Ot,this.isLocal||(this.localIndex=1,this.remoteIndex=0)}getResolutionDependencies(t,e){let o=this.indexFromResolverType(e)+1;return{[this.dependentKeys[o]]:this.transformDescriptor(t,o)}}async getCandidates(t,e,o,n){let s=this.indexFromResolverType(n),[a,i]=this.getNextDependencies(e,s),l=o.resolver,h=this.transformDescriptor(t,s+1),u=await l.getCandidates(h,a,o),k=u.length>0;return k&&i&&u.push(i),(s===0||u.length===0)&&this.trace(`${this.prettyName}: getCandidates: (${n}) found ${u.length} locators from ${k?"child":"package"}`),this.transformLocators(u,this.resolverLocatorIndex(n))}async getSatisfying(t,e,o,n,s){let a=this.indexFromResolverType(s),i=a+1,[l]=this.getNextDependencies(e,a),h=this.transformDescriptor(t,i),u=this.transformLocators(o,i),f=await n.resolver.getSatisfying(h,l,u,n),D=this.resolverLocatorIndex(s);return D!==i&&(f.locators=this.transformLocators(f.locators,D)),(a===0||f.locators.length===0)&&this.trace(`${this.prettyName}: getSatisfying (${s}) found ${f.locators.length} locators from ${o.length} inputs`),f}toFallbackLocator(t){return this.transformLocator(t,this.fallbackIndex)}toLeadDescriptor(t){return this.transformDescriptor(t,0)}indexFromResolverType(t){return t==="local"?this.localIndex:this.remoteIndex}resolverLocatorIndex(t){return t==="local"?this.localIndex:this.fallbackIndex}transformDescriptor(t,e){return et(t,this.protocols[e])}transformLocator(t,e){return rt(t,this.protocols[e])}transformLocators(t,e){return t.map(o=>this.transformLocator(o,e))}getNextDependencies(t,e){let o=e+1,n=t[this.dependentKeys[o]];n&&this.packages[o]===null&&(this.packages[o]=n);let s=this.packages[o],a={};return s&&(a[this.dependentKeys[o]]=s),[a,s]}};var _=r=>null,Lt=p.npath.toPortablePath("package.json"),Rt=p.npath.toPortablePath(""),J=class{constructor(t){this.trace=_;this.workspaceMap=new Map;this.workspaceByIdent=new Map;this.notExternal=new Set;this.npmPackageByIdent=new Map;this.resolver=null;this.fetcher=null;this.findPackage=_;this.pathOffset=p.npath.toPortablePath("");this.trace=_,this.report=o=>console.log(o),this.root=t.cwd,this.project=t;let{provider:e}=y(t.configuration);e&&(e.endsWith(".json")?(this.pathOffset=this.findConfigPathOffset(e),this.findPackage=Q(e)):(e.endsWith(".js")||e.endsWith(".cjs"))&&(this.pathOffset=this.findConfigPathOffset(e),this.findPackage=Z(e)))}findConfigPathOffset(t){let e=p.ppath.dirname(t);return p.ppath.relative(this.root,e)}tryNameLookup(t){return this.workspaceMap.get(t)||null}createWorkspace(t,e){let o=v.structUtils.prettyIdent(this.project.configuration,v.structUtils.parseIdent(t));return new F(t,o,e,this.trace)}tryIdentLoad(t){let e=v.structUtils.stringifyIdent(t),o=this.tryNameLookup(e);if(o)return this.workspaceByIdent.set(t.identHash,o),o;let n=this.findPackage(e);if(n){let s=p.npath.toPortablePath(n.path||"");return s&&(p.ppath.isAbsolute(s)||(s=p.ppath.join(this.root,this.pathOffset,s),at.default.existsSync(p.ppath.join(s,Lt))||(s=Rt))),o=this.createWorkspace(e,s),this.trace(`Loaded external workspace ${o.prettyName} of type ${n.path?"LOCAL":"REMOTE"}`),this.workspaceMap.set(e,o),this.workspaceByIdent.set(t.identHash,o),o}return this.notExternal.add(t.identHash),null}setFallbackPackage(t,e){this.npmPackageByIdent.set(t.identHash,e)}getFallbackPackage(t){return this.npmPackageByIdent.get(t.identHash)}tryByDescriptor(t){return this.workspaceByIdent.has(t.identHash)?this.workspaceByIdent.get(t.identHash):this.notExternal.has(t.identHash)||this.project.tryWorkspaceByDescriptor(t)?null:this.tryIdentLoad(t)}findByDescriptor(t){let e=this.tryByDescriptor(t);if(!e)throw new Error(`Cannot find workspace for descriptor ${v.structUtils.stringifyDescriptor(t)}`);return e}tryByLocator(t){return this.workspaceByIdent.has(t.identHash)?this.workspaceByIdent.get(t.identHash):this.notExternal.has(t.identHash)||this.project.tryWorkspaceByLocator(t)?null:this.tryIdentLoad(t)}findByLocator(t){let e=this.tryByLocator(t);if(!e)throw new Error(`Cannot find workspace for locator ${v.structUtils.stringifyLocator(t)}`);return e}getResolver(){return this.resolver??=this.project.configuration.makeResolver(),this.resolver}getFetcher(){return this.fetcher??=this.project.configuration.makeFetcher(),this.fetcher}},U=null;function w(r){return U||(U=new J(r)),U}var E=class r{constructor(){this.tracker=null}static{this.protocol=P}ensureTracker(t){return this.tracker||(this.tracker=w(t.project)),this.tracker}supports(t,e){return t.reference.startsWith(r.protocol)}getLocalPath(t,e){return this.ensureTracker(e).findByLocator(t).localPath||null}async fetch(t,e){let o=this.ensureTracker(e),n=o.findByLocator(t);if(n.localPath){let s=x.ppath.resolve(o.root,n.localPath);return{packageFs:new x.CwdFS(x.PortablePath.root),prefixPath:s,localPath:s}}return await this.fetchFallback(t,e,n)}async fetchFallback(t,e,o){let n=o.toFallbackLocator(t),a=await e.fetcher.fetch(n,e);return delete a.checksum,a}};var pt=c("@yarnpkg/fslib");var ct=c("@yarnpkg/cli"),L=c("@yarnpkg/core"),d=c("@yarnpkg/fslib"),g=c("clipanion"),O=S(c("fs"));var it="1.0.0",$=class extends ct.BaseCommand{constructor(){super(...arguments);this.target=g.Option.String("--target","",{description:"The path to the file to output the workspaces to"});this.checkOnly=g.Option.Boolean("--check-only",!1,{description:"Check if the workspaces have changed without writing the file"});this.includePrivate=g.Option.Boolean("--include-private",!1,{description:"Include private workspaces in the output"})}static{this.paths=[["external-workspaces","output"]]}static{this.usage=g.Command.Usage({category:"External Workspaces",description:"Output current workspace information to a json file",details:`
7+
This command will output the current set of workspaces to a json file. The file will not be modified if the workspaces have not changed.
8+
9+
The path to the .json file can optionally have a set of keys appended to the end as a path. This will write the workspaces to a subpath of
10+
the file while maintaining the other contents of the file.
11+
`,examples:[["Output workspaces with settings from package.json","$0 external-workspaces output"],["Output workspaces to target","$0 external-workspaces output --target ./path/to/file.json"],["Output workspaces to target with a subpath","$0 external-workspaces output --target ./path/to/file.json/key1/key2"],["Check if workspaces have changed","$0 external-workspaces output --target ./path/to/file.json --check-only"]]})}async execute(){let{quiet:e,stdout:o}=this.context,n=e?()=>null:h=>o.write(`${h}
12+
`),s=await L.Configuration.find(this.context.cwd,this.context.plugins),a=y(s),{project:i}=await L.Project.find(s,this.context.cwd),l=this.target||a.outputPath;if(!l)throw new g.UsageError("No output path specified in configuration or command. Use --target to specify a path");await H(i,d.npath.toPortablePath(l),this.checkOnly,n)}};function H(r,t,e,o){let n=t.endsWith(".json"),s=n?d.ppath.dirname(t):t,a=n?d.ppath.basename(t):Ct(r);!e&&!O.default.existsSync(s)&&O.default.mkdirSync(s,{recursive:!0,mode:493});let i=d.npath.join(d.npath.fromPortablePath(s),a),l={};r.workspacesByIdent.forEach(T=>{let{name:K,private:ft}=T.manifest;if(K&&!ft){let dt=L.structUtils.stringifyIdent(K);l[dt]=d.ppath.relative(r.cwd,T.cwd)}});let h=d.ppath.relative(s,r.cwd),u={repoPath:h,version:it,workspaces:bt(l)},k=O.default.existsSync(i)?JSON.parse(O.default.readFileSync(i,"utf8")):{},f=k.generated||{},D=f.repoPath||"",A=Tt(f.workspaces||{},l),ut=h!==D||f.version!==it;if(A||ut)if(e)Dt(i,D,h,A,o);else{k.generated=u;let T=JSON.stringify(k,null,2);O.default.writeFileSync(i,T),o(`Updated workspaces in ${i}`)}}function Dt(r,t,e,o,n){if(n(`Updates needed for ${r}:`),t!==e&&n(`Repo path has changed from ${t} to ${e}`),o)for(let s in o){let a=String(o[s]).padEnd(6," ");n(`${a} - ${s}`)}}function Tt(r,t){let e={},o=!1;for(let n in t)r[n]?r[n]!==t[n]&&(e[n]="update",o=!0):(e[n]="add",o=!0);for(let n in r)t[n]||(e[n]="remove",o=!0);return o?e:null}function bt(r){let t=Object.keys(r).sort(),e={};for(let o of t)e[o]=r[o];return e}function Ct(r){let e=r.workspacesByCwd.get(r.cwd)?.manifest.name;return e?`${e.scope?`@${e.scope}-${e.name}`:e.name}-workspaces.json`:"workspaces.json"}function lt(r,t){let e=y(r.configuration);if(!e.outputOnlyOnCommand&&e.outputPath){let o=n=>t.report.reportInfo(null,n);H(r,pt.npath.toPortablePath(e.outputPath),!1,o)}}async function ht(r,t,e,o,n){return w(t).tryByDescriptor(r)?.toLeadDescriptor(r)??r}var R=c("@yarnpkg/core");var j=class{constructor(t,e){this.tracker=null;this.resolverType=t,this.protocol=e}ensureTracker(t){return this.tracker||(this.tracker=w(t.project)),this.tracker}getResolutionDependencies(t,e){return this.ensureTracker(e).findByDescriptor(t).getResolutionDependencies(t,this.resolverType)}supportsDescriptor(t,e){return t.range.startsWith(this.protocol)}supportsLocator(t,e){return t.reference.startsWith(this.protocol)}shouldPersistResolution(t,e){return!1}bindDescriptor(t,e,o){return t}async getCandidates(t,e,o){return await this.ensureTracker(o).findByDescriptor(t).getCandidates(t,e,o,this.resolverType)}async getSatisfying(t,e,o,n){return await this.ensureTracker(n).findByDescriptor(t).getSatisfying(t,e,o,n,this.resolverType)}async resolve(t,e){let o=this.ensureTracker(e),n=`UNEXPECTED: resolve called for ${R.structUtils.stringifyLocator(t)} in ${this.resolverType} resolver`;throw o.trace(n),new Error(n)}},B=class r extends j{static{this.protocol=W}constructor(){super("remote",r.protocol)}},N=class r extends j{static{this.protocol=P}constructor(){super("local",r.protocol)}async resolve(t,e){return{...new R.Manifest,...t,version:"0.0.0",languageName:e.project.configuration.get("defaultLanguageName"),linkType:R.LinkType.SOFT}}};var It={configuration:X,fetchers:[E],resolvers:[N,B],hooks:{afterAllInstalled:lt,reduceDependency:ht},commands:[$]},Ft=It;return wt(Wt);})();
13+
return plugin;
14+
}
15+
};
16+
//# sourceMappingURL=external-workspaces.cjs.map

.yarnrc.yml

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
compressionLevel: 0
2+
3+
dynamicPackageExtensions: ./scripts/dependencies.config.js
4+
25
enableGlobalCache: false
6+
37
enableScripts: false
8+
49
enableTelemetry: false
10+
11+
externalWorkspacesOutputPath: test-repos
12+
513
globalFolder: .yarn/berry # Workaround for 'EXDEV: cross-device link not permitted' errors on GHA
14+
615
logFilters:
716
- code: YN0007 # X must be built because it never has been before or the last one failed
817
level: discard
@@ -12,8 +21,11 @@ logFilters:
1221
level: discard
1322
- code: YN0069 # This rule seems redundant when applied on the original package
1423
level: error
24+
1525
nodeLinker: pnpm
26+
1627
npmRegistryServer: "https://registry.npmjs.org"
28+
1729
packageExtensions:
1830
"@fluentui/utilities@*":
1931
peerDependenciesMeta:
@@ -38,10 +50,14 @@ packageExtensions:
3850
# https://github.com/facebook/react-native/pull/47308
3951
"@react-native-community/cli-platform-android": ^15.0.0
4052
"@react-native-community/cli-platform-ios": ^15.0.0
53+
4154
plugins:
4255
- path: .yarn/plugins/@yarnpkg/plugin-compat.cjs
4356
spec: "@yarnpkg/plugin-compat"
4457
- path: incubator/yarn-plugin-dynamic-extensions/index.js
45-
dynamicPackageExtensions: ./scripts/dependencies.config.js
58+
- path: .yarn/plugins/@rnx-kit/yarn-plugin-external-workspaces.cjs
59+
spec: incubator/yarn-plugin-external-workspaces/dist/external-workspaces.cjs
60+
4661
tsEnableAutoTypes: false
62+
4763
yarnPath: .yarn/releases/yarn-4.6.0.cjs

0 commit comments

Comments
 (0)