Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
9 changes: 9 additions & 0 deletions .yarn/plugins/@yarnpkg/plugin-backstage.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-disable */
Copy link
Contributor

@Parkreiner Parkreiner Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camilaibs I'm getting ready to start the review in earnest, but I did have two questions about the changes being made to the repo.

From my understanding, Backstage introduced this Yarn plugin to make it simpler for Backstage users to ensure that their various dependencies are in sync with the Backstage mainline versions. And also, this got added around the middle of last year (probably around the time development slowed for this plugin the first time around). A key part of that is specifying Backstage versions like this:

"@backstage/core-compat-api": "backstage:^",
  1. Is there anything that needs to change from the plugin authoring standpoint to make sure that we're a little bit more insulated from Backstage version changes and breaking API changes?
    • We're looking into reports from some potential customers, and they've said the current version of the plugin (based on 1.22) is breaking for later versions of Backstage (at least 1.43). We're still gathering information from them, but they've made it sound that it was the upgrade process that broke things. I assume the NFS helps with this a lot, but is there anything else we can do to maximize compatibility? I don't want to force consumers to update their Backstage deployment to continue using the plugin, when I don't know what red tape various companies have to deal with
  2. With the updates that have been made so far, it looks like they've all been centered on updating the dev page, as well as getting the plugin moved to NFS. Does the PR still need to update the packages directory to keep those in sync with the new Backstage version?

//prettier-ignore
module.exports = {
name: "@yarnpkg/plugin-backstage",
factory: function (require) {
"use strict";var plugin=(()=>{var F=Object.create;var v=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,I=Object.prototype.hasOwnProperty;var p=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var J=(e,t)=>{for(var r in t)v(e,r,{get:t[r],enumerable:!0})},S=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of N(t))!I.call(e,o)&&o!==r&&v(e,o,{get:()=>t[o],enumerable:!(n=_(t,o))||n.enumerable});return e};var L=(e,t,r)=>(r=e!=null?F(G(e)):{},S(t||!e||!e.__esModule?v(r,"default",{value:e,enumerable:!0}):r,e)),z=e=>S(v({},"__esModule",{value:!0}),e);var ae={};J(ae,{default:()=>se});var P=p("@yarnpkg/core");var w=p("@yarnpkg/core");var W=L(p("assert")),j=p("semver"),y=p("@yarnpkg/fslib");var u=L(p("fs")),g=p("path");function A(e,t){let r=e;for(let n=0;n<1e3;n++){let o=(0,g.resolve)(r,"package.json");if(u.default.existsSync(o)&&t(o))return r;let i=(0,g.dirname)(r);if(i===r)return;r=i}throw new Error(`Iteration limit reached when searching for root package.json at ${e}`)}function K(e){let t=A(e,()=>!0);if(!t)throw new Error(`No package.json found while searching for package root of ${e}`);return t}function Y(e){if(!u.default.existsSync((0,g.resolve)(e,"src")))throw new Error("Tried to access monorepo package root dir outside of Backstage repository");return(0,g.resolve)(e,"../..")}function b(e){let t=K(e),r=u.default.realpathSync(process.cwd()).replace(/^[a-z]:/,s=>s.toLocaleUpperCase("en-US")),n="",o=()=>(n||(n=Y(t)),n),a="",i=()=>(a||(a=A(r,s=>{try{let m=u.default.readFileSync(s,"utf8");return!!JSON.parse(m).workspaces}catch(m){throw new Error(`Failed to parse package.json file while searching for root, ${m}`)}})??r),a);return{ownDir:t,get ownRoot(){return o()},targetDir:r,get targetRoot(){return i()},resolveOwn:(...s)=>(0,g.resolve)(t,...s),resolveOwnRoot:(...s)=>(0,g.resolve)(o(),...s),resolveTarget:(...s)=>(0,g.resolve)(r,...s),resolveTargetRoot:(...s)=>(0,g.resolve)(i(),...s)}}var x="backstage.json";var V=e=>{let t=!1,r;return()=>(t||(r=e(),t=!0),r)};var h=p("@yarnpkg/fslib");var C=()=>h.npath.toPortablePath(b(h.npath.fromPortablePath(h.ppath.cwd())).targetRoot);var k=V(()=>{let e=y.ppath.join(C(),x),t=null;try{t=(0,j.valid)(y.xfs.readJsonSync(e).version),(0,W.default)(t!==null)}catch{throw new Error("Valid version string not found in backstage.json")}return t});var d=p("@yarnpkg/core");var q="https://versions.backstage.io",Q="https://raw.githubusercontent.com/backstage/versions/main";function X(e,t){return new Promise((r,n)=>{let o=setTimeout(()=>{t.aborted||r()},e);t.addEventListener("abort",()=>{clearTimeout(o),n(new Error("Aborted"))})})}async function Z(e,t,r){let n=new AbortController,o=new AbortController,a=e(n.signal).then(s=>(o.abort(),s)),i=X(r,o.signal).then(()=>t(o.signal)).then(s=>(n.abort(),s));return Promise.any([a,i]).catch(()=>a)}async function D(e){let t=encodeURIComponent(e.version),r=e.fetch??fetch,n=e.versionsBaseUrl??q,o=e.gitHubRawBaseUrl??Q,a=await Z(i=>r(`${n}/v1/releases/${t}/manifest.json`,{signal:i}),i=>r(`${o}/v1/releases/${t}/manifest.json`,{signal:i}),500);if(a.status===404)throw new Error(`No release found for ${e.version} version`);if(a.status!==200)throw new Error(`Unexpected response status ${a.status} when fetching release from ${a.url}.`);return a.json()}var c="backstage:";var f=async(e,t)=>{let r=d.structUtils.stringifyIdent(e),n=d.structUtils.parseRange(e.range);if(n.protocol!==c)throw new Error(`Unsupported version protocol in version range "${e.range}" for package ${r}`);if(n.selector!=="^")throw new Error(`Unexpected version selector "${n.selector}" for package ${r}`);let o=k(),i=(await D({version:o,fetch:async s=>{let m=await d.httpUtils.get(s,{configuration:t,jsonResponse:!0});return{status:200,url:s,json:()=>m}}})).packages.find(s=>s.name===r);if(!i)throw new Error(`Package ${r} not found in manifest for Backstage v${o}. This means the specified package is not included in this Backstage release. This may imply the package has been replaced with an alternative - please review the documentation for the package. If you need to continue using this package, it will be necessary to switch to manually managing its version.`);return i.version};var ee=e=>w.structUtils.parseRange(e).protocol===c,te=(e,t,r)=>e!=="dependencies"?e:r.manifest.ensureDependencyMeta(w.structUtils.makeDescriptor(t,"unknown")).optional?"optionalDependencies":e,B=async(e,t)=>{for(let r of["dependencies","devDependencies"]){let n=Array.from(e.manifest.getForScope(r).values()).filter(o=>o.range.startsWith(c));for(let o of n){let a=w.structUtils.stringifyIdent(o);if(w.structUtils.parseRange(o.range).selector!=="^")throw new Error(`Unexpected version range "${o.range}" for dependency on "${a}"`);let s=te(r,o,e);t[s][a]=`^${await f(o,e.project.configuration)}`}}if(["dependencies","devDependencies","optionalDependencies"].some(r=>Object.values(t[r]??{}).some(ee)))throw new Error(`Failed to replace all "backstage:" ranges in manifest for ${t.name}`)};var O=p("@yarnpkg/core");var $=async(e,t)=>{let r=O.structUtils.parseRange(e.range);if(r.protocol!==c)return e;if(r.selector!=="^")throw new Error(`Invalid backstage: version range found: ${e.range}`);return O.structUtils.bindDescriptor(e,{backstage:k(),npm:await f(e,t.configuration)})};var H=p("@yarnpkg/core");var U=async(e,t,r,n)=>{let o=H.structUtils.parseRange(r.range);if(r.scope==="backstage"&&o.protocol!==c){let a=r.range;try{r.range=`${c}^`,await f(r,e.project.configuration),console.info(`Setting ${r.scope}/${r.name} to ${c}^`)}catch{r.range=a}}};var M=p("@yarnpkg/core");var E=async(e,t,r,n)=>{let o=M.structUtils.parseRange(n.range);n.scope==="backstage"&&o.protocol!==c&&console.warn(`${n.name} should be set to "${c}^" instead of "${n.range}". Make sure this change is intentional and not a mistake.`)};var l=p("@yarnpkg/core"),T=p("@yarnpkg/plugin-npm");var R=class e{static protocol=c;supportsDescriptor=t=>t.range.startsWith(e.protocol);async getCandidates(t,r,n){let o=l.structUtils.parseRange(t.range).params?.npm;if(!o||Array.isArray(o))throw new Error(`Missing npm parameter on backstage: range "${t.range}"`);return new T.NpmSemverResolver().getCandidates(l.structUtils.makeDescriptor(t,`npm:^${o}`),r,n)}getResolutionDependencies(t){let r=l.structUtils.parseRange(t.range).params?.npm;if(!r)throw new Error(`Missing npm parameter on backstage: range "${t.range}".`);return{[l.structUtils.stringifyIdent(t)]:l.structUtils.makeDescriptor(t,`npm:^${r}`)}}async getSatisfying(t,r,n,o){let a=t,i=l.structUtils.parseRange(a.range);if(i.protocol===c){let s=i.params?.npm;a=l.structUtils.makeDescriptor(t,`npm:^${s}`)}return new T.NpmSemverResolver().getSatisfying(a,r,n,o)}bindDescriptor=t=>t;supportsLocator=()=>!1;shouldPersistResolution=()=>{throw new Error("Unreachable: BackstageNpmResolver should never persist resolution as it uses npm: protocol")};resolve=async()=>{throw new Error("Unreachable: BackstageNpmResolver should never resolve as it uses npm: protocol")}};var re="\x1B[31;1m",oe="\x1B[0m";P.semverUtils.satisfiesWithPrereleases(P.YarnVersion,"^4.1.1")||(console.error(),console.error(`${re}Unsupported yarn version${oe}: The Backstage yarn plugin only works with yarn ^4.1.1. Please upgrade yarn, or remove this plugin with "yarn plugin remove @yarnpkg/plugin-backstage".`),console.error());var ne={hooks:{afterWorkspaceDependencyAddition:U,afterWorkspaceDependencyReplacement:E,reduceDependency:$,beforeWorkspacePacking:B},resolvers:[R]},se=ne;return z(ae);})();
return plugin;
}
};
6 changes: 6 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
nodeLinker: node-modules

plugins:
- checksum: 8af7b3f2d7d19cacc7a3712f871efcb6208ba283a1f532260b0cba80c2cb66ed772b207b5ba41b8c5d64dd8d5e0c0e15bbb445bd14afac491712965211ba027c
path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs
spec: "https://versions.backstage.io/v1/tags/main/yarn-plugin"
2 changes: 1 addition & 1 deletion backstage.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "1.22.1"
"version": "1.42.5"
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,6 @@
"*.{json,md}": [
"prettier --write"
]
}
},
"packageManager": "[email protected]"
}
273 changes: 273 additions & 0 deletions plugins/backstage-plugin-coder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ the Dev Container.
X-Custom-Source: backstage
```

### Old Frontend System

3. Add the `CoderProvider` to the application:

```tsx
Expand Down Expand Up @@ -117,6 +119,277 @@ the Dev Container.
);
```

### New Frontend System

Follow these steps to detect and configure the Coder plugin if you'd like to use it in an application that supports the new Backstage frontend system.

#### Package detection

Once you install the `@coder/backstage-plugin-coder` package using your preferred package manager, you have to choose how the package should be detected by the app. The package can be automatically discovered when the feature discovery config is set, or it can be manually enabled via code (for more granular package customization cases, such as extension overrides).

<table>
<tr>
<td>Via config</td>
<td>Via code</td>
</tr>
<tr>
<td>
<pre lang="yaml">
<code>
# app-config.yaml
app:
# Enable package discovery for all plugins
packages: 'all'
---
app:
# Enable package discovery only for Coder
packages:
include:
- '@coder/backstage-plugin-coder'
</code>
</pre>
</td>
<td>
<pre lang="javascript">
<code>
// packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-defaults';
import coderPlugin from '@coder/backstage-plugin-coder/alpha';
//...
const app = createApp({
// ...
features: [
//...
coderPlugin,
],
});

//...
</code>
</pre>
</td>
</tr>
</table>

#### Extension configurations

Currently, the plugin installs 4 extensions: 2 APIs (url sync and client wrapper), 1 App root wrapper (the coder provider), and 1 Entity page card (the overview workspaces card).

To be able to connect to your Coder organization it is mandotory that you set the Coder provider configuration in the `app-config.yaml` file:

```yml
# app-config.yaml
app:
extensions:
# Defining the Coder provider app config
- 'app-root-wrapper:coder':
config:
# (required)
appConfig:
deployment:
# Replace with your Coder deployment access URL
accessUrl: 'https://dev.coder.app'
# Set the default template (and parameters) for
# catalog items. Individual properties can be overridden
# by a repo's catalog-info.yaml file
workspaces:
defaultTemplateName: 'devcontainers'
defaultMode: 'manual'
# This property defines which parameters in your Coder
# workspace templates are used to store repository links
repoUrlParamKeys: ['custom_repo', 'repo_url']
params:
repo: 'custom'
region: 'eu-helsinki'
```

The Coder plugin also installs the `workspaces` card in the Catalog entity overview tab by default. No code is needed to see it on the screen, but there are a few optional customizations that can be set via the `app-config.yaml` file:

```yml
# app-config.yaml
app:
extensions:
- 'entity-card:coder':
config:
# (optional) determine in which Catalog overview tab area the card will be shown
# defaults to "content", but can be changed to "info" or "summary"
type: 'summary'
# (optional) determines whether to show the card or not
# the card is always shown, but you can add a filter for example to show it only for
# entities of kind component
filter:
kind: 'component'
# (optional) define a default filter for the workspaces search
# defaults to "owner:me"
defaultQueryFilter: 'owner:guest'
# (optional) whether to read entity metadata from catalog-info.yaml
readEntityData: true
```

#### Extension overrides

If you want to use your own custom version of the Workspaces Card component, override the default Workspaces Card component as follows:

```tsx
// packages/app/src/plugins/coder/index.tsx
import React, { useState } from 'react';
import { compatWrapper } from '@backstage/core-compat-api';
import coderPlugin from '@coder/backstage-plugin-coder/alpha';
// ...

export default coderPlugin.withOverrides({
extensions: [
// Get the default workspaces card and override its component loader
coderPlugin.getExtension('entity-card:coder').override({
params: {
async loader() {
const { CoderWorkspacesCard } = await import(
'@coder/backstage-plugin-coder'
);
function Component() {
const [searchText, setSearchText] = useState('owner:me');
// The "compatWrapper" is needed because CoderWorkspacesCard is still using legacy frontend system utilities
// such as the AppContext
return compatWrapper(
<CoderWorkspacesCard
queryFilter={searchText}
onFilterChange={newSearchText => setSearchText(newSearchText)}
/>,
);
}
return <Component />;
},
},
}),
],
});

// packages/app/src/App.tsx
// ...
import coderPluginWithOverrides from './plugins/coder';

// ...

const app = createApp({
features: [
// ...
coderPluginWithOverrides,
],
});
```

Additionally, if you don't want a global Coder provider installed and would rather create your own page to view Coder information, here's an example of how you can do that:

```tsx
// packages/app/src/plugins/coder/index.tsx
import { PageBlueprint } from '@backstage/frontend-plugin-api';
import coderPlugin from '@coder/backstage-plugin-coder/alpha';
// ...

function CustomCoderWorkspacesPage() {
// Your page code goes here
}

// In this case there is no Coder page to override as the Coder plugin do not provide a page extension by default
// We have to create a brand new page extension
const customCoderWorspacesPage = PageBlueprint.makeWithOverrides({
// You can decide if you want to keep config in the "app-config.yaml" file, or define it in the code instead.
// In this example we decided to define a config schema for the page so the config value continue be set in the "app-config.yaml" file
config: {
schema: {
fallbackAuthUiMode: z => z
.union([
z.literal('restrained'),
z.literal('assertive'),
z.literal('hidden')
])
.optional(),
appConfig: z => z.object({
deployment: z.object({
accessUrl: z.string(),
}),
workspaces: z.object({
defaultMode: z
.union([z.literal('manual'), z.literal('auto')])
.optional(),
defaultTemplateName: z.string().optional(),
params: z.record(z.string(), z.string().optional()).optional(),
repoUrlParamKeys: z.tuple([z.string()]).rest(z.string()),
}),
}),
}
},
factory(originalFactory, context) {
// Getting the appConfig defined in the "app-config.yaml" file
const appConfig = context.config.appConfig;
return originalFactory({
path: '/coder',
async loader() {
const { CoderProvider } = await import('@coder/backstage-plugin-coder');
// The "compatWrapper" is needed because CoderProvider is still using legacy frontend system utilities
return compatWrapper(
<CoderProvider appConfig={appConfig}>
<CustomCoderWorkspacesPage />
</CoderProvider>
);
}
});
},
});

export default coderPlugin.withOverrides({
extensions: [
customCoderWorspacesPage,
],
});

// packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-defaults';
import coderPluginWithOverrides from './plugins/coder';
// ...

const app = createApp({
features: [
// ...
coderPluginWithOverrides,
],
});
```

Now we just need to update the configs in the `app-config.yaml` file:

```diff
# app-config.yaml
app:
extensions:
# ...
- - 'app-root-wrapper:coder':
+ # As the provider extension was disabled, the app config is now passed to the new page extension
+ - 'page:coder':
config:
# (required)
appConfig:
deployment:
# Replace with your Coder deployment access URL
accessUrl: 'https://dev.coder.app'
# Set the default template (and parameters) for
# catalog items. Individual properties can be overridden
# by a repo's catalog-info.yaml file
workspaces:
defaultTemplateName: 'devcontainers'
defaultMode: 'manual'
# This property defines which parameters in your Coder
# workspace templates are used to store repository links
repoUrlParamKeys: ['custom_repo', 'repo_url']
params:
repo: 'custom'
region: 'eu-helsinki'
+ # Disabling the default provider extension as we are not using it anymore
+ - app-root-wrapper:coder: false
+ # Also disabling the default entity card as we now have a page for it
+ - entity-card:coder: false
```

### `catalog-info.yaml` files

In addition to the above, you can define additional properties on your specific repo's `catalog-info.yaml` file.
Expand Down
69 changes: 0 additions & 69 deletions plugins/backstage-plugin-coder/dev/DevPage.tsx

This file was deleted.

Loading
Loading