Merge branch 'master' into canon-token-updates

This commit is contained in:
Charles de Dreuille
2025-01-16 13:59:20 +00:00
126 changed files with 3171 additions and 408 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli-node': patch
---
Added `type` field to `BackstagePackageJson` type.
+18
View File
@@ -0,0 +1,18 @@
---
'@backstage/cli': minor
---
**BREAKING**: Add support for native ESM in Node.js code. This changes the behavior of dynamic import expressions in Node.js code. Typically this can be fixed by replacing `import(...)` with `require(...)`, with an `as typeof import(...)` cast if needed for types. This is because dynamic imports will no longer be transformed to `require(...)` calls, but instead be left as-is. This in turn allows you to load ESM modules from CommonJS code using `import(...)`.
This change adds support for the following in Node.js packages, across type checking, package builds, runtime transforms and Jest tests:
- Dynamic imports that load ESM modules from CommonJS code.
- Both `.mjs` and `.mts` files as explicit ESM files, as well as `.cjs` and `.cts` as explicit CommonJS files.
- Support for the `"type": "module"` field in `package.json` to indicate that the package is an ESM package.
There are a few caveats to be aware of:
- To enable support for native ESM in tests, you need to run the tests with the `--experimental-vm-modules` flag enabled, typically via `NODE_OPTIONS='--experimental-vm-modules'`.
- Declaring a package as `"type": "module"` in `package.json` is supported, but in tests it will cause all local transitive dependencies to also be treated as ESM, regardless of whether they declare `"type": "module"` or not.
- Node.js has an [ESM interoperability layer with CommonJS](https://nodejs.org/docs/latest-v22.x/api/esm.html#interoperability-with-commonjs) that allows for imports from ESM to identify named exports in CommonJS packages. This interoperability layer is **only** enabled when importing packages with a `.cts` or `.cjs` extension. This is because the interoperability layer is not fully compatible with the NPM ecosystem, and would break package if it was enabled for `.js` files.
- Dynamic imports of CommonJS packages will vary in shape depending on the runtime, i.e. test vs local development, etc. It is therefore recommended to avoid dynamic imports of CommonJS packages and instead use `require`, or to use the explicit CommonJS extensions as mentioned above. If you do need to dynamically import CommonJS packages, avoid using `default` exports, as the shape of them vary across different environments and you would otherwise need to manually unwrap the import based on the shape of the module object.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-notifications-backend': patch
'@backstage/plugin-notifications': patch
---
added topic filter for notifications
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/backend-test-utils': patch
'@backstage/backend-defaults': patch
'@backstage/config-loader': patch
---
Internal refactor to use explicit `require` for lazy-loading dependency.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Sync feature installation compatibility logic with `@backstage/backend-app-api`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Cleanup `refresh_state_references` for entity processors and providers that are no longer in control of a `refresh_state` row for entity
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Internal refactor to support native ESM.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-catalog': patch
---
Modified the logic for generating the location URL by encoding the entity property values with `encodeURIComponent`. This enhancement improves the safety and reliability of the URL.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-dynamic-feature-service': patch
---
Make sure changes are successfully tracked before starting up scanner.
+1 -1
View File
@@ -202,7 +202,7 @@ jobs:
env:
CI: true
NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot
NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules
INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }}
INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }}
INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }}
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
env:
CI: true
NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot
NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules
INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }}
INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }}
INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }}
+4
View File
@@ -9,11 +9,15 @@
"watch-css": "node scripts/watch-css.js"
},
"dependencies": {
"@codemirror/lang-sass": "^6.0.2",
"@lezer/highlight": "^1.2.1",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.1.4",
"@storybook/react": "^8.4.7",
"@types/mdx": "^2.0.13",
"@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7",
"next": "14.2.23",
"react": "^18",
"react-dom": "^18",
+34
View File
@@ -35,3 +35,37 @@ iframe {
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
.cm-editor {
transition: background-color 0.2s ease-in-out;
}
.ͼ2 .cm-gutters {
background-color: var(--canon-surface-2);
border-right: 1px solid var(--canon-border-base);
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
}
.ͼ1 .cm-lineNumbers .cm-gutterElement {
padding-right: 8px;
padding-left: 8px;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 34px;
}
.cm-line {
font-size: 0.875rem;
}
.ͼ1 .cm-line {
padding-left: 12px;
padding-top: 1px;
padding-bottom: 1px;
}
.cm-focused {
outline: none !important;
}
+3 -1
View File
@@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import { Sidebar } from '../components/Sidebar';
import { Toolbar } from '@/components/Toolbar';
import { Providers } from './providers';
import { CustomTheme } from '@/components/CustomTheme';
import styles from './page.module.css';
import './globals.css';
@@ -13,6 +13,7 @@ import '/public/backstage.css';
export const metadata: Metadata = {
title: 'Canon',
description: 'UI library for Backstage',
metadataBase: new URL('https://canon.backstage.io'),
};
export default function RootLayout({
@@ -35,6 +36,7 @@ export default function RootLayout({
<Toolbar />
{children}
</div>
<CustomTheme />
</div>
</Providers>
</body>
@@ -0,0 +1,147 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { sass } from '@codemirror/lang-sass';
import styles from './styles.module.css';
import { usePlayground } from '@/utils/playground-context';
import { AnimatePresence, motion } from 'framer-motion';
import { Icon } from '../../../../packages/canon';
import { createTheme } from '@uiw/codemirror-themes';
import { tags as t } from '@lezer/highlight';
const defaultTheme = `:root {
--canon-accent: #000;
}`;
const myTheme = createTheme({
theme: 'light',
settings: {
background: 'var(--canon-surface-1)',
backgroundImage: '',
foreground: '#6182B8',
caret: '#5d00ff',
selection: '#036dd626',
selectionMatch: '#036dd626',
lineHighlight: '#8a91991a',
gutterBackground: '#fff',
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.comment, color: '#787b8099' },
{ tag: t.variableName, color: '#0080ff' },
{ tag: [t.string, t.special(t.brace)], color: '#6182B8' },
{ tag: t.number, color: '#6182B8' },
{ tag: t.bool, color: '#6182B8' },
{ tag: t.null, color: '#6182B8' },
{ tag: t.keyword, color: '#6182B8' },
{ tag: t.operator, color: '#6182B8' },
{ tag: t.className, color: '#6182B8' },
{ tag: t.definition(t.typeName), color: '#6182B8' },
{ tag: t.typeName, color: '#6182B8' },
{ tag: t.angleBracket, color: '#6182B8' },
{ tag: t.tagName, color: '#6182B8' },
{ tag: t.attributeName, color: '#6182B8' },
],
});
export const CustomTheme = () => {
const [isClient, setIsClient] = useState(false);
const [open, setOpen] = useState(true);
const [customTheme, setCustomTheme] = useState<string | undefined>(undefined);
const { selectedThemeName } = usePlayground();
const [savedMessage, setSavedMessage] = useState<string>('Save');
const updateStyleElement = (theme: string) => {
let styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'custom-theme-style';
document.head.appendChild(styleElement);
}
styleElement.textContent = theme;
};
useEffect(() => {
if (selectedThemeName === 'custom') {
let storedTheme = localStorage.getItem('customThemeCss');
if (!storedTheme) {
storedTheme = defaultTheme;
localStorage.setItem('customThemeCss', storedTheme);
}
setCustomTheme(storedTheme);
updateStyleElement(storedTheme);
} else {
const styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
if (styleElement) {
styleElement.remove();
}
}
}, [selectedThemeName]);
useEffect(() => {
setIsClient(true);
}, []);
const handleSave = () => {
if (customTheme) {
localStorage.setItem('customThemeCss', customTheme);
updateStyleElement(customTheme);
setSavedMessage('Saved!');
setTimeout(() => setSavedMessage('Save'), 1000);
}
};
const handleChange = useCallback((val: string) => {
setCustomTheme(val);
}, []);
if (isClient === false) return null;
return (
<AnimatePresence>
{selectedThemeName === 'custom' && (
<motion.div
className={`${styles.container} ${open ? styles.open : ''}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
<div className={styles.header}>
<div className={styles.headerLeft}>Custom Theme</div>
<div className={styles.headerRight}>
{open && (
<button className={styles.buttonSave} onClick={handleSave}>
{savedMessage}
</button>
)}
<button
className={styles.buttonClose}
onClick={() => setOpen(!open)}
>
<Icon name={open ? 'chevronDown' : 'chevronUp'} />
</button>
</div>
</div>
<div className={styles.editorContainer}>
<CodeMirror
value={customTheme}
height="300px"
extensions={[sass()]}
onChange={handleChange}
className={styles.editor}
basicSetup={{ foldGutter: false }}
theme={myTheme}
/>
</div>
</motion.div>
)}
</AnimatePresence>
);
};
@@ -0,0 +1 @@
export { CustomTheme } from './customTheme';
@@ -0,0 +1,77 @@
.container {
position: fixed;
bottom: 16px;
right: 16px;
width: 240px;
height: 47px;
background-color: var(--canon-surface-1);
border-radius: 0.375rem;
border: 1px solid var(--canon-border-base);
display: flex;
flex-direction: column;
overflow: hidden;
transition-property: background-color, border-color, height, width;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
}
.open {
width: 36%;
height: 348px;
}
.editor {
flex: 1;
}
.editorContainer {
overflow: hidden;
}
.header {
height: 46px;
flex-shrink: 0;
border-bottom: 1px solid var(--canon-border-base);
background-color: var(--canon-surface-1);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px 0 16px;
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
}
.headerLeft {
font-size: 0.875rem;
}
.headerRight {
display: flex;
gap: 8px;
}
.buttonSave {
all: unset;
height: 28px;
padding: 0 8px;
color: #fff;
background-color: #000;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.75rem;
}
.buttonClose {
all: unset;
height: 28px;
padding: 0 8px;
color: #fff;
background-color: var(--canon-surface-2);
color: var(--canon-text-primary);
transition: background-color 0.2s ease-in-out;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
}
@@ -35,15 +35,11 @@
position: relative;
}
.menu a {
display: flex;
text-decoration: none;
height: 28px;
align-items: center;
}
.section {
width: 100%;
display: flex;
flex-direction: column;
gap: 2px;
}
.sectionTitle {
@@ -54,12 +50,14 @@
}
.line {
text-decoration: none;
align-items: center;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 28px;
height: 26px;
padding: 0 12px;
border-radius: 4px;
transition: background-color 0.2s ease-in-out;
+1 -1
View File
@@ -2,7 +2,7 @@
import Link from 'next/link';
import { components, overview, layoutComponents, theme } from '@/utils/data';
import { Box } from '../../../../packages/canon/src/components/Box';
import { Box } from '../../../../packages/canon';
import { motion } from 'framer-motion';
import styles from './Sidebar.module.css';
import { usePathname } from 'next/navigation';
@@ -43,7 +43,7 @@
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: canvas;
background-color: var(--canon-surface-1);
color: var(--color-gray-900);
border: 1px solid var(--canon-border-base);
padding-inline: 0.25rem;
@@ -6,8 +6,9 @@ import { Icon } from '@backstage/canon';
import { usePlayground } from '@/utils/playground-context';
const themes = [
{ name: 'Backstage Legacy', value: 'legacy' },
{ name: 'Backstage Default', value: 'default' },
{ name: 'Backstage Legacy', value: 'legacy' },
{ name: 'Custom theme', value: 'custom' },
];
export const ThemeNameSelector = () => {
+14 -9
View File
@@ -7,16 +7,19 @@ import React, {
} from 'react';
import { components } from './data';
type Theme = 'light' | 'dark';
type ThemeName = 'legacy' | 'default' | 'custom';
// Create a context with an empty array as the default value
const PlaygroundContext = createContext<{
selectedScreenSizes: string[];
setSelectedScreenSizes: (screenSizes: string[]) => void;
selectedComponents: string[];
setSelectedComponents: (components: string[]) => void;
selectedTheme: string | null;
setSelectedTheme: (theme: string) => void;
selectedThemeName: string;
setSelectedThemeName: (themeName: string) => void;
selectedTheme: Theme;
setSelectedTheme: (theme: Theme) => void;
selectedThemeName: ThemeName;
setSelectedThemeName: (themeName: ThemeName) => void;
}>({
selectedScreenSizes: [],
setSelectedScreenSizes: () => {},
@@ -37,12 +40,14 @@ export const PlaygroundProvider = ({ children }: { children: ReactNode }) => {
const [selectedComponents, setSelectedComponents] = useState<string[]>(
components.map(component => component.slug),
);
const [selectedTheme, setSelectedTheme] = useState<string | null>(() => {
return isBrowser ? localStorage.getItem('theme') : 'light';
});
const [selectedThemeName, setSelectedThemeName] = useState<string>(() => {
const [selectedTheme, setSelectedTheme] = useState<Theme>(() => {
return isBrowser
? localStorage.getItem('theme-name') || 'default'
? (localStorage.getItem('theme') as Theme) || 'light'
: 'light';
});
const [selectedThemeName, setSelectedThemeName] = useState<ThemeName>(() => {
return isBrowser
? (localStorage.getItem('theme-name') as ThemeName) || 'default'
: 'default';
});
+288
View File
@@ -5,6 +5,133 @@ __metadata:
version: 6
cacheKey: 8
"@babel/runtime@npm:^7.18.6":
version: 7.26.0
resolution: "@babel/runtime@npm:7.26.0"
dependencies:
regenerator-runtime: ^0.14.0
checksum: c8e2c0504ab271b3467a261a8f119bf2603eb857a0d71e37791f4e3fae00f681365073cc79f141ddaa90c6077c60ba56448004ad5429d07ac73532be9f7cf28a
languageName: node
linkType: hard
"@codemirror/autocomplete@npm:^6.0.0":
version: 6.18.4
resolution: "@codemirror/autocomplete@npm:6.18.4"
dependencies:
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.17.0
"@lezer/common": ^1.0.0
checksum: 4216f45a17f6cfd8d33df53f940396f7d3707662570bf3a79d8d333f926e273a265fac13c362e29e3fa57ccdf444f1a047862f5f56c672cfc669c87ee975858f
languageName: node
linkType: hard
"@codemirror/commands@npm:^6.0.0, @codemirror/commands@npm:^6.1.0":
version: 6.8.0
resolution: "@codemirror/commands@npm:6.8.0"
dependencies:
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.4.0
"@codemirror/view": ^6.27.0
"@lezer/common": ^1.1.0
checksum: 7d819bab4830ec7b8c5dffdec4b035dfa664bfd1d2675e639e08a459df65f45be111e1b8b569b1a8a3253d5980cf2ecf4394d8a13509996cca1b65cc16d47a4e
languageName: node
linkType: hard
"@codemirror/lang-css@npm:^6.2.0":
version: 6.3.1
resolution: "@codemirror/lang-css@npm:6.3.1"
dependencies:
"@codemirror/autocomplete": ^6.0.0
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@lezer/common": ^1.0.2
"@lezer/css": ^1.1.7
checksum: ed175d75d75bc0a059d1e60b3dcd8464d570da14fc97388439943c9c43e1e9146e37b83fe2ccaad9cd387420b7b411ea1d24ede78ecd1f2045a38acbb4dd36bc
languageName: node
linkType: hard
"@codemirror/lang-sass@npm:^6.0.2":
version: 6.0.2
resolution: "@codemirror/lang-sass@npm:6.0.2"
dependencies:
"@codemirror/lang-css": ^6.2.0
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@lezer/common": ^1.0.2
"@lezer/sass": ^1.0.0
checksum: e7665aaab70476a952522b143fd7bd59f6c025746cbf7b542f6965f94eecac483b4afd03f6da98aaa1572e379194309b241c5264eff05c681c637aa26651b9ab
languageName: node
linkType: hard
"@codemirror/language@npm:^6.0.0":
version: 6.10.8
resolution: "@codemirror/language@npm:6.10.8"
dependencies:
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.23.0
"@lezer/common": ^1.1.0
"@lezer/highlight": ^1.0.0
"@lezer/lr": ^1.0.0
style-mod: ^4.0.0
checksum: 679b69d69faa94f028f996a7005d0c6c2a2e4cd7a7a2614f615c23d7b642c31fc1837915248e864cb1ad59a2f032d1a7a8ef486b5f9904e5f6fbe6f7d2882c38
languageName: node
linkType: hard
"@codemirror/lint@npm:^6.0.0":
version: 6.8.4
resolution: "@codemirror/lint@npm:6.8.4"
dependencies:
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.35.0
crelt: ^1.0.5
checksum: 640e3dd44eb167d952eb5c5b8518919ba46e164aa3471776342f7f9361e676b4627a76a9f01d51b22127b97413f2bc9b8c60299d8dfdd5fc8ad0225d42de7669
languageName: node
linkType: hard
"@codemirror/search@npm:^6.0.0":
version: 6.5.8
resolution: "@codemirror/search@npm:6.5.8"
dependencies:
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
crelt: ^1.0.5
checksum: 0f9633037492a7b647b606c30255ea42c4327319e643be7ea3aa2913ed8e4aa662589f457e376636521c7d4d1215fae0e8939f127db9c0790b19ae3b654c3bc4
languageName: node
linkType: hard
"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.1.1, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0":
version: 6.5.1
resolution: "@codemirror/state@npm:6.5.1"
dependencies:
"@marijn/find-cluster-break": ^1.0.0
checksum: b7d6de9a87d5b55dadfadaeb6e1c991e0a91845d3cdd0bd953391f05363fcbaf21de79a4eec2816ab8c3e31293faeca82cc2bb729d080779df94b14e28ae0d8a
languageName: node
linkType: hard
"@codemirror/theme-one-dark@npm:^6.0.0":
version: 6.1.2
resolution: "@codemirror/theme-one-dark@npm:6.1.2"
dependencies:
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
"@lezer/highlight": ^1.0.0
checksum: 29bc09f79534115f62658caf3d0db527fe347d058b69a8c7f580ae636827377aadd0606fd0d83dbab8d6f3b0a5df53d3253c619341b5fb93d2c8291a8efb9556
languageName: node
linkType: hard
"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.35.0":
version: 6.36.2
resolution: "@codemirror/view@npm:6.36.2"
dependencies:
"@codemirror/state": ^6.5.0
style-mod: ^4.1.0
w3c-keyname: ^2.2.4
checksum: a58c64b623ddc65bb864917297f3b37f8e95280deec442024c43a9513b26352c829665c5d98e4dfcae104e8ecdfdb774d94a395a29da98a919c83482d2c14152
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.24.2":
version: 0.24.2
resolution: "@esbuild/aix-ppc64@npm:0.24.2"
@@ -261,6 +388,60 @@ __metadata:
languageName: node
linkType: hard
"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0":
version: 1.2.3
resolution: "@lezer/common@npm:1.2.3"
checksum: 9b5f52d949adae69d077f56c0b1c2295923108c3dfb241dd9f17654ff708f3eab81ff9fa7f0d0e4a668eabdcb9d961c73e75caca87c966ca1436e30e49130fcb
languageName: node
linkType: hard
"@lezer/css@npm:^1.1.7":
version: 1.1.9
resolution: "@lezer/css@npm:1.1.9"
dependencies:
"@lezer/common": ^1.2.0
"@lezer/highlight": ^1.0.0
"@lezer/lr": ^1.0.0
checksum: 25c63475061a3c9f87961a7f85c5f547f14fb7e81b0864675d2206999a874a0559d676145c74c6ccde39519dbc8aa33e216265f5366d08060507b6c9e875fe0f
languageName: node
linkType: hard
"@lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.2.1":
version: 1.2.1
resolution: "@lezer/highlight@npm:1.2.1"
dependencies:
"@lezer/common": ^1.0.0
checksum: a8822d7e37f79ff64669eb2df4a9f9d16580e88f2b276a646092e19a9bdccac304e92510e200e35869a8b1f6c27eba5972c508d347a277e9b722d582ab7a23d5
languageName: node
linkType: hard
"@lezer/lr@npm:^1.0.0":
version: 1.4.2
resolution: "@lezer/lr@npm:1.4.2"
dependencies:
"@lezer/common": ^1.0.0
checksum: 94318ad046c7dfcc8d37e26cb85b99623c39aef60aa51ec2abb30928e7a649f38fa5520f34bd5b356f1db11b6991999589f039e87c8949b0f163be3764f029d8
languageName: node
linkType: hard
"@lezer/sass@npm:^1.0.0":
version: 1.0.7
resolution: "@lezer/sass@npm:1.0.7"
dependencies:
"@lezer/common": ^1.2.0
"@lezer/highlight": ^1.0.0
"@lezer/lr": ^1.0.0
checksum: 32d37aa5d3143033f0b886c33a293b1d28b7eecdf2cb979cf7b4cbba0587be227dfed246c27116060ac9b2b2c0fe4b09df4b672690dd0593e876529583e7ef2a
languageName: node
linkType: hard
"@marijn/find-cluster-break@npm:^1.0.0":
version: 1.0.2
resolution: "@marijn/find-cluster-break@npm:1.0.2"
checksum: 0d836de25e04d58325813401ef3c2d34caf040da985a5935fcbc9d84e7b47a21bdb15f57d70c2bf0960bd29ed3dbbb1afd00cdd0fc4fafbee7fd0ffe7d508ae1
languageName: node
linkType: hard
"@mdx-js/loader@npm:^3.1.0":
version: 3.1.0
resolution: "@mdx-js/loader@npm:3.1.0"
@@ -904,6 +1085,66 @@ __metadata:
languageName: node
linkType: hard
"@uiw/codemirror-extensions-basic-setup@npm:4.23.7":
version: 4.23.7
resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.23.7"
dependencies:
"@codemirror/autocomplete": ^6.0.0
"@codemirror/commands": ^6.0.0
"@codemirror/language": ^6.0.0
"@codemirror/lint": ^6.0.0
"@codemirror/search": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
peerDependencies:
"@codemirror/autocomplete": ">=6.0.0"
"@codemirror/commands": ">=6.0.0"
"@codemirror/language": ">=6.0.0"
"@codemirror/lint": ">=6.0.0"
"@codemirror/search": ">=6.0.0"
"@codemirror/state": ">=6.0.0"
"@codemirror/view": ">=6.0.0"
checksum: 656c5f78db4c6216a9a7eeb65455f52b002b7a5426bb1b05f11b896d5dba9887c7b9ee5aee9fe369d6c7b3c69128ee842cb5c8fe029cd07c2e48ec9358039a30
languageName: node
linkType: hard
"@uiw/codemirror-themes@npm:^4.23.7":
version: 4.23.7
resolution: "@uiw/codemirror-themes@npm:4.23.7"
dependencies:
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
peerDependencies:
"@codemirror/language": ">=6.0.0"
"@codemirror/state": ">=6.0.0"
"@codemirror/view": ">=6.0.0"
checksum: 8c2a9a7ee8df2de3f09ad1f18979c4b407f7dd03e1f6ddfe47837b9e4769b566a70259ec8c398473b6b6db79cdb8809d4acc2f2ed7851743e13ff95308f52e33
languageName: node
linkType: hard
"@uiw/react-codemirror@npm:^4.23.7":
version: 4.23.7
resolution: "@uiw/react-codemirror@npm:4.23.7"
dependencies:
"@babel/runtime": ^7.18.6
"@codemirror/commands": ^6.1.0
"@codemirror/state": ^6.1.1
"@codemirror/theme-one-dark": ^6.0.0
"@uiw/codemirror-extensions-basic-setup": 4.23.7
codemirror: ^6.0.0
peerDependencies:
"@babel/runtime": ">=7.11.0"
"@codemirror/state": ">=6.0.0"
"@codemirror/theme-one-dark": ">=6.0.0"
"@codemirror/view": ">=6.0.0"
codemirror: ">=6.0.0"
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 5f6318b290f479c4718129578bdccbbdf64e3b588fdd79e3d80224779219eae7e249d40e4f88c864ed6ae40ea93036042313e983a022349563698593a2bf2e6c
languageName: node
linkType: hard
"@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.2.0":
version: 1.2.1
resolution: "@ungap/structured-clone@npm:1.2.1"
@@ -1254,6 +1495,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "canon-docs@workspace:."
dependencies:
"@codemirror/lang-sass": ^6.0.2
"@lezer/highlight": ^1.2.1
"@mdx-js/loader": ^3.1.0
"@mdx-js/react": ^3.1.0
"@next/mdx": ^15.1.4
@@ -1262,6 +1505,8 @@ __metadata:
"@types/node": ^20
"@types/react": ^18
"@types/react-dom": ^18
"@uiw/codemirror-themes": ^4.23.7
"@uiw/react-codemirror": ^4.23.7
concurrently: ^9.1.2
eslint: ^8
eslint-config-next: 14.2.23
@@ -1339,6 +1584,21 @@ __metadata:
languageName: node
linkType: hard
"codemirror@npm:^6.0.0":
version: 6.0.1
resolution: "codemirror@npm:6.0.1"
dependencies:
"@codemirror/autocomplete": ^6.0.0
"@codemirror/commands": ^6.0.0
"@codemirror/language": ^6.0.0
"@codemirror/lint": ^6.0.0
"@codemirror/search": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
checksum: 1a78f7077ac5801bdbff162aa0c61bf2b974603c7e9a477198c3ce50c789af674a061d7c293c58b73807eda345c2b5228c38ad2aabb9319d552d5486f785cbef
languageName: node
linkType: hard
"collapse-white-space@npm:^2.0.0":
version: 2.1.0
resolution: "collapse-white-space@npm:2.1.0"
@@ -1394,6 +1654,13 @@ __metadata:
languageName: node
linkType: hard
"crelt@npm:^1.0.5":
version: 1.0.6
resolution: "crelt@npm:1.0.6"
checksum: dad842093371ad702afbc0531bfca2b0a8dd920b23a42f26e66dabbed9aad9acd5b9030496359545ef3937c3aced0fd4ac39f7a2d280a23ddf9eb7fdcb94a69f
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
@@ -4380,6 +4647,13 @@ __metadata:
languageName: node
linkType: hard
"regenerator-runtime@npm:^0.14.0":
version: 0.14.1
resolution: "regenerator-runtime@npm:0.14.1"
checksum: 9f57c93277b5585d3c83b0cf76be47b473ae8c6d9142a46ce8b0291a04bb2cf902059f0f8445dcabb3fb7378e5fe4bb4ea1e008876343d42e46d3b484534ce38
languageName: node
linkType: hard
"regex-recursion@npm:^5.1.1":
version: 5.1.1
resolution: "regex-recursion@npm:5.1.1"
@@ -4971,6 +5245,13 @@ __metadata:
languageName: node
linkType: hard
"style-mod@npm:^4.0.0, style-mod@npm:^4.1.0":
version: 4.1.2
resolution: "style-mod@npm:4.1.2"
checksum: 7c5c3e82747f9bcf5f288d8d07f50848e4630fe5ff7bfe4d94cc87d6b6a2588227cbf21b4c792ac6406e5852293300a75e710714479a5c59a06af677f0825ef8
languageName: node
linkType: hard
"style-to-object@npm:^1.0.0":
version: 1.0.8
resolution: "style-to-object@npm:1.0.8"
@@ -5331,6 +5612,13 @@ __metadata:
languageName: node
linkType: hard
"w3c-keyname@npm:^2.2.4":
version: 2.2.8
resolution: "w3c-keyname@npm:2.2.8"
checksum: 95bafa4c04fa2f685a86ca1000069c1ec43ace1f8776c10f226a73296caeddd83f893db885c2c220ebeb6c52d424e3b54d7c0c1e963bbf204038ff1a944fbb07
languageName: node
linkType: hard
"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
version: 1.1.1
resolution: "which-boxed-primitive@npm:1.1.1"
+34 -23
View File
@@ -483,29 +483,40 @@ of the build system, including the bundling, tests, builds, and type checking.
Loaders are always selected based on the file extension. The following is a list
of all supported file extensions:
| Extension | Exports | Purpose |
| --------- | ------------- | ------------------ |
| `.ts` | Script Module | TypeScript |
| `.tsx` | Script Module | TypeScript and XML |
| `.js` | Script Module | JavaScript |
| `.jsx` | Script Module | JavaScript and XML |
| `.mjs` | Script Module | ECMAScript Module |
| `.cjs` | Script Module | CommonJS Module |
| `.json` | JSON Data | JSON Data |
| `.yml` | JSON Data | YAML Data |
| `.yaml` | JSON Data | YAML Data |
| `.css` | classes | Style sheet |
| `.eot` | URL Path | Font |
| `.ttf` | URL Path | Font |
| `.woff2` | URL Path | Font |
| `.woff` | URL Path | Font |
| `.bmp` | URL Path | Image |
| `.gif` | URL Path | Image |
| `.jpeg` | URL Path | Image |
| `.jpg` | URL Path | Image |
| `.png` | URL Path | Image |
| `.svg` | URL Path | Image |
| `.md` | URL Path | Markdown File |
| Extension | Exports | Purpose |
| --------- | ------------- | ---------------------------- |
| `.ts` | Script Module | TypeScript |
| `.tsx` | Script Module | TypeScript and XML |
| `.mts` | Script Module | ECMAScript Module TypeScript |
| `.cts` | Script Module | CommonJS TypeScript |
| `.js` | Script Module | JavaScript |
| `.jsx` | Script Module | JavaScript and XML |
| `.mjs` | Script Module | ECMAScript Module |
| `.cjs` | Script Module | CommonJS Module |
| `.json` | JSON Data | JSON Data |
| `.yml` | JSON Data | YAML Data |
| `.yaml` | JSON Data | YAML Data |
| `.css` | classes | Style sheet |
| `.eot` | URL Path | Font |
| `.ttf` | URL Path | Font |
| `.woff2` | URL Path | Font |
| `.woff` | URL Path | Font |
| `.bmp` | URL Path | Image |
| `.gif` | URL Path | Image |
| `.jpeg` | URL Path | Image |
| `.jpg` | URL Path | Image |
| `.png` | URL Path | Image |
| `.svg` | URL Path | Image |
| `.md` | URL Path | Markdown File |
## ECMAScript Modules
The Backstage tooling supports [ECMAScript modules (ESM)](https://nodejs.org/docs/latest-v22.x/api/esm.html) in Node.js packages. This includes support for all the script module file extensions listed above during local development, in built packages, in tests, and during type checking. [Dynamic imports](https://nodejs.org/docs/latest-v22.x/api/esm.html#import-expressions) can be used to load ESM-only packages from CommonJS and vice versa. There are however a couple of limitations to be aware of:
- To enable support for native ESM in tests, you need to run the tests with the `--experimental-vm-modules` flag enabled, typically via `NODE_OPTIONS='--experimental-vm-modules'`.
- Declaring a package as `"type": "module"` in `package.json` is supported, but in tests it will cause all local transitive dependencies to also be treated as ESM, regardless of whether they declare `"type": "module"` or not.
- Node.js has an [ESM interoperability layer with CommonJS](https://nodejs.org/docs/latest-v22.x/api/esm.html#interoperability-with-commonjs) that allows for imports from ESM to identify named exports in CommonJS packages. This interoperability layer is **only** enabled when importing packages with a `.cts` or `.cjs` extension. This is because the interoperability layer is not fully compatible with the NPM ecosystem, and would break package if it was enabled for `.js` files.
- Dynamic imports of CommonJS packages will vary in shape depending on the runtime, i.e. test vs local development, etc. It is therefore recommended to avoid dynamic imports of CommonJS packages and instead use `require`, or to use the explicit CommonJS extensions as mentioned above. If you do need to dynamically import CommonJS packages, avoid using `default` exports, as the shape of them vary across different environments and you would otherwise need to manually unwrap the import based on the shape of the module object.
## Jest Configuration
+4 -4
View File
@@ -41,16 +41,16 @@
"snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false",
"snyk:test:package": "yarn snyk:test --include",
"start": "yarn workspace example-app start",
"start-backend": "yarn workspace example-backend start",
"start-backend:legacy": "yarn workspace example-backend-legacy start",
"start:lighthouse": "yarn workspaces foreach -A --include example-backend --include example-app --parallel --jobs unlimited -v -i run start",
"start:microsite": "cd microsite/ && yarn start",
"start:next": "yarn workspace example-app-next start",
"start-backend": "yarn workspace example-backend start",
"start-backend:legacy": "yarn workspace example-backend-legacy start",
"storybook": "yarn ./storybook run storybook",
"techdocs-cli": "node scripts/techdocs-cli.js",
"techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js",
"test": "NODE_OPTIONS=--no-node-snapshot backstage-cli repo test",
"test:all": "NODE_OPTIONS=--no-node-snapshot backstage-cli repo test --coverage",
"test": "NODE_OPTIONS='--no-node-snapshot --experimental-vm-modules' backstage-cli repo test",
"test:all": "NODE_OPTIONS='--no-node-snapshot --experimental-vm-modules' backstage-cli repo test --coverage",
"test:e2e": "NODE_OPTIONS=--no-node-snapshot playwright test",
"tsc": "tsc",
"tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false"
@@ -103,7 +103,7 @@ export async function buildPgDatabaseConfig(
Connector: CloudSqlConnector,
IpAddressTypes,
AuthTypes,
} = await import('@google-cloud/cloud-sql-connector');
} = require('@google-cloud/cloud-sql-connector') as typeof import('@google-cloud/cloud-sql-connector');
const connector = new CloudSqlConnector();
const clientOpts = await connector.getOptions({
instanceConnectionName: config.connection.instance,
@@ -68,7 +68,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
preferAlpha: options.preferAlpha,
});
const scannedPlugins = (await scanner.scanRoot()).packages;
scanner.trackChanges();
await scanner.trackChanges();
const moduleLoader =
options.moduleLoader ||
new CommonJSModuleLoader({ logger: options.logger });
+2 -1
View File
@@ -59,7 +59,8 @@ export async function startMemcachedContainer(
image: string,
): Promise<Instance> {
// Lazy-load to avoid side-effect of importing testcontainers
const { GenericContainer } = await import('testcontainers');
const { GenericContainer } =
require('testcontainers') as typeof import('testcontainers');
const container = await new GenericContainer(image)
.withExposedPorts(11211)
+2 -1
View File
@@ -57,7 +57,8 @@ export async function connectToExternalRedis(
export async function startRedisContainer(image: string): Promise<Instance> {
// Lazy-load to avoid side-effect of importing testcontainers
const { GenericContainer } = await import('testcontainers');
const { GenericContainer } =
require('testcontainers') as typeof import('testcontainers');
const container = await new GenericContainer(image)
.withExposedPorts(6379)
@@ -72,7 +72,8 @@ export async function startMysqlContainer(image: string): Promise<{
const password = uuid();
// Lazy-load to avoid side-effect of importing testcontainers
const { GenericContainer } = await import('testcontainers');
const { GenericContainer } =
require('testcontainers') as typeof import('testcontainers');
const container = await new GenericContainer(image)
.withExposedPorts(3306)
@@ -72,7 +72,8 @@ export async function startPostgresContainer(image: string): Promise<{
const password = uuid();
// Lazy-load to avoid side-effect of importing testcontainers
const { GenericContainer } = await import('testcontainers');
const { GenericContainer } =
require('testcontainers') as typeof import('testcontainers');
const container = await new GenericContainer(image)
.withExposedPorts(5432)
@@ -210,10 +210,19 @@ function isPromise<T>(value: unknown | Promise<T>): value is Promise<T> {
);
}
// Same as in the backend-app-api, handles double defaults from dynamic imports
function unwrapFeature(
feature: BackendFeature | (() => BackendFeature),
feature: BackendFeature | { default: BackendFeature },
): BackendFeature {
return typeof feature === 'function' ? feature() : feature;
if ('$$type' in feature) {
return feature;
}
if ('default' in feature) {
return feature.default;
}
return feature;
}
const backendInstancesToCleanUp = new Array<Backend>();
+2
View File
@@ -72,6 +72,8 @@ export interface BackstagePackageJson {
[key: string]: string;
};
// (undocumented)
type?: 'module' | 'commonjs';
// (undocumented)
types?: string;
// (undocumented)
typesVersions?: Record<string, Record<string, string[]>>;
@@ -43,6 +43,8 @@ export interface BackstagePackageJson {
// that the package bundles all of its dependencies in its build output.
bundled?: boolean;
type?: 'module' | 'commonjs';
backstage?: {
role?: PackageRole;
moved?: string;
+98 -62
View File
@@ -31,6 +31,14 @@ const FRONTEND_ROLES = [
'frontend-plugin-module',
];
const NODE_ROLES = [
'backend',
'cli',
'node-library',
'backend-plugin',
'backend-plugin-module',
];
const envOptions = {
oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS),
};
@@ -130,11 +138,97 @@ const transformIgnorePattern = [
].join('|');
// Provides additional config that's based on the role of the target package
function getRoleConfig(role) {
function getRoleConfig(role, pkgJson) {
// Only Node.js package roles support native ESM modules, frontend and common
// packages are always transpiled to CommonJS.
const moduleOpts = NODE_ROLES.includes(role)
? {
module: {
ignoreDynamic: true,
exportInteropAnnotation: true,
},
}
: undefined;
const transform = {
'\\.(mjs|cjs|js)$': [
require.resolve('./jestSwcTransform'),
{
...moduleOpts,
jsc: {
parser: {
syntax: 'ecmascript',
},
},
},
],
'\\.jsx$': [
require.resolve('./jestSwcTransform'),
{
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
],
'\\.(mts|cts|ts)$': [
require.resolve('./jestSwcTransform'),
{
...moduleOpts,
jsc: {
parser: {
syntax: 'typescript',
},
},
},
],
'\\.tsx$': [
require.resolve('./jestSwcTransform'),
{
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
],
'\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$':
require.resolve('./jestFileTransform.js'),
'\\.(yaml)$': require.resolve('./jestYamlTransform'),
};
if (FRONTEND_ROLES.includes(role)) {
return { testEnvironment: require.resolve('jest-environment-jsdom') };
return {
testEnvironment: require.resolve('jest-environment-jsdom'),
transform,
};
}
return { testEnvironment: require.resolve('jest-environment-node') };
return {
testEnvironment: require.resolve('jest-environment-node'),
moduleFileExtensions: [...SRC_EXTS, 'json', 'node'],
// Jest doesn't let us dynamically detect type=module per transformed file,
// so we have to assume that if the entry point is ESM, all TS files are
// ESM.
//
// This means you can't switch a package to type=module until all of its
// monorepo dependencies are also type=module or does not contain any .ts
// files.
extensionsToTreatAsEsm:
pkgJson.type === 'module' ? ['.ts', '.mts'] : ['.mts'],
transform,
};
}
async function getProjectConfig(targetPath, extraConfig, extraOptions) {
@@ -160,64 +254,6 @@ async function getProjectConfig(targetPath, extraConfig, extraOptions) {
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
},
transform: {
'\\.(mjs|cjs|js)$': [
require.resolve('./jestSwcTransform'),
{
jsc: {
parser: {
syntax: 'ecmascript',
},
},
},
],
'\\.jsx$': [
require.resolve('./jestSwcTransform'),
{
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
],
'\\.ts$': [
require.resolve('./jestSwcTransform'),
{
jsc: {
parser: {
syntax: 'typescript',
},
},
},
],
'\\.tsx$': [
require.resolve('./jestSwcTransform'),
{
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
},
},
],
'\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$':
require.resolve('./jestFileTransform.js'),
'\\.(yaml)$': require.resolve('./jestYamlTransform'),
},
// A bit more opinionated
testMatch: [`**/*.test.{${SRC_EXTS.join(',')}}`],
@@ -226,7 +262,7 @@ async function getProjectConfig(targetPath, extraConfig, extraOptions) {
: require.resolve('./jestCachingModuleLoader'),
transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
...getRoleConfig(pkgJson.backstage?.role),
...getRoleConfig(pkgJson.backstage?.role, pkgJson),
};
options.setupFilesAfterEnv = options.setupFilesAfterEnv || [];
@@ -19,6 +19,11 @@ const { default: JestRuntime } = require('jest-runtime');
const scriptTransformCache = new Map();
module.exports = class CachingJestRuntime extends JestRuntime {
constructor(config, ...restAgs) {
super(config, ...restAgs);
this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts');
}
// This may or may not be a good idea. Theoretically I don't know why this would impact
// test correctness and flakiness, but it seems like it may introduce flakiness and strange failures.
// It does seem to speed up test execution by a fair amount though.
@@ -33,4 +38,13 @@ module.exports = class CachingJestRuntime extends JestRuntime {
}
return script;
}
// Unfortunately we need to use this unstable API to make sure that .js files
// are only loaded as modules where ESM is supported, i.e. Node.js packages.
unstable_shouldLoadAsEsm(path, ...restArgs) {
if (!this.allowLoadAsEsm) {
return false;
}
return super.unstable_shouldLoadAsEsm(path, ...restArgs);
}
};
+6
View File
@@ -23,10 +23,16 @@ function createTransformer(config) {
...config,
});
const process = (source, filePath, jestOptions) => {
// Skip transformation of .js files without ESM syntax, we never transform from CJS to ESM
if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) {
return { code: source };
}
// Skip transformation of .mjs files, they should only be used if ESM support is available
if (filePath.endsWith('.mjs')) {
return { code: source };
}
return swcTransformer.process(source, filePath, jestOptions);
};
+10 -1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
const { pathToFileURL } = require('url');
const { transformSync } = require('@swc/core');
const { addHook } = require('pirates');
const { Module } = require('module');
@@ -55,7 +56,10 @@ addHook(
const transformed = transformSync(code, {
filename,
sourceMaps: 'inline',
module: { type: 'commonjs' },
module: {
type: 'commonjs',
ignoreDynamic: true,
},
jsc: {
target: 'es2022',
parser: {
@@ -76,3 +80,8 @@ addHook(
},
{ extensions: ['.js', '.cjs'], ignoreNodeModules: true },
);
// Register module hooks, used by "type": "module" in package.json, .mjs and
// .mts files, as well as dynamic import(...)s, although dynamic imports will be
// handled be the CommonJS hooks in this file if what it points to is CommonJS.
Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename));
+282
View File
@@ -0,0 +1,282 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { dirname, extname, resolve as resolvePath } from 'path';
import { fileURLToPath } from 'url';
import { transformFile } from '@swc/core';
import { isBuiltin } from 'node:module';
import { readFile } from 'fs/promises';
import { existsSync } from 'fs';
// @ts-check
// No explicit file extension, no type in package.json
const DEFAULT_MODULE_FORMAT = 'commonjs';
// Source file extensions to look for when using bundle resolution strategy
const SRC_EXTS = ['.ts', '.js'];
const TS_EXTS = ['.ts', '.mts', '.cts'];
const moduleTypeTable = {
'.mjs': 'module',
'.mts': 'module',
'.cjs': 'commonjs',
'.cts': 'commonjs',
'.ts': undefined,
'.js': undefined,
};
/** @type {import('module').ResolveHook} */
export async function resolve(specifier, context, nextResolve) {
// Built-in modules are handled by the default resolver
if (isBuiltin(specifier)) {
return nextResolve(specifier, context);
}
const ext = extname(specifier);
// Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below.
if (ext === '.json' && !context.importAttributes?.type) {
const jsonResult = await nextResolve(specifier, context);
return {
...jsonResult,
format: 'commonjs',
importAttributes: { type: 'json' },
};
}
// Anything else with an explicit extension is handled by the default
// resolver, except that we help determine the module type where needed.
if (ext !== '') {
return withDetectedModuleType(await nextResolve(specifier, context));
}
// Other external modules are handled by the default resolver, but again we
// help determine the module type where needed.
if (!specifier.startsWith('.')) {
return withDetectedModuleType(await nextResolve(specifier, context));
}
// The rest of this function handles the case of resolving imports that do not
// specify any extension and might point to a directory with an `index.*`
// file. We resolve those using the same logic as most JS bundlers would, with
// the addition of checking if there's an explicit module format listed in the
// closest `package.json` file.
//
// We use a bundle resolution strategy in order to keep code consistent across
// Backstage codebases that contains code both for Web and Node.js, and to
// support packages with common code that can be used in both environments.
try {
// This is expected to throw, but in the event that this module specifier is
// supported we prefer to use the default resolver.
return await nextResolve(specifier, context);
} catch (error) {
if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`;
const resolved = await resolveWithoutExt(spec, context, nextResolve);
if (resolved) {
return withDetectedModuleType(resolved);
}
} else if (error.code === 'ERR_MODULE_NOT_FOUND') {
const resolved = await resolveWithoutExt(specifier, context, nextResolve);
if (resolved) {
return withDetectedModuleType(resolved);
}
}
// Unexpected error or no resolution found
throw error;
}
}
/**
* Populates the `format` field in the resolved object based on the closest `package.json` file.
*
* @param {import('module').ResolveFnOutput} resolved
* @returns {Promise<import('module').ResolveFnOutput>}
*/
async function withDetectedModuleType(resolved) {
// Already has an explicit format
if (resolved.format) {
return resolved;
}
// Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default.
if (resolved.format === null) {
return { ...resolved, format: DEFAULT_MODULE_FORMAT };
}
const ext = extname(resolved.url);
const explicitFormat = moduleTypeTable[ext];
if (explicitFormat) {
return {
...resolved,
format: explicitFormat,
};
}
// TODO(Rugvip): Afaik this should never happen and we can remove this check, but want it here for a little while to verify.
if (ext === '.js') {
throw new Error('Unexpected .js file without explicit format');
}
// TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring
const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url));
if (!packageJsonPath) {
return resolved;
}
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
return {
...resolved,
format: packageJson.type ?? DEFAULT_MODULE_FORMAT,
};
}
/**
* Find the closest package.json file from the given path.
*
* TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable.
* @param {string} startPath
* @returns {Promise<string | undefined>}
*/
async function findPackageJSON(startPath) {
let path = startPath;
// Some confidence check to avoid infinite loop
for (let i = 0; i < 1000; i++) {
const packagePath = resolvePath(path, 'package.json');
if (existsSync(packagePath)) {
return packagePath;
}
const newPath = dirname(path);
if (newPath === path) {
return undefined;
}
path = newPath;
}
throw new Error(
`Iteration limit reached when searching for package.json at ${startPath}`,
);
}
/** @type {import('module').ResolveHook} */
async function resolveWithoutExt(specifier, context, nextResolve) {
for (const tryExt of SRC_EXTS) {
try {
const resolved = await nextResolve(specifier + tryExt, {
...context,
format: 'commonjs',
});
return {
...resolved,
format: moduleTypeTable[tryExt] ?? resolved.format,
};
} catch {
/* ignore */
}
}
return undefined;
}
/** @type {import('module').LoadHook} */
export async function load(url, context, nextLoad) {
// Non-file URLs are handled by the default loader
if (!url.startsWith('file://')) {
return nextLoad(url, context);
}
// JSON files loaded as CommonJS are handled by this custom loader, because
// the default one doesn't work. For JSON loading to work we'd need the
// synchronous hooks that aren't supported yet, or avoid using the CommonJS
// compatibility.
if (
context.format === 'commonjs' &&
context.importAttributes?.type === 'json'
) {
try {
// TODO(Rugvip): Make sure this is valid JSON
const content = await readFile(fileURLToPath(url), 'utf8');
return {
source: `module.exports = (${content})`,
format: 'commonjs',
shortCircuit: true,
};
} catch {
// Let the default loader generate the error
return nextLoad(url, context);
}
}
const ext = extname(url);
// Non-TS files are handled by the default loader
if (!TS_EXTS.includes(ext)) {
return nextLoad(url, context);
}
const format = context.format ?? DEFAULT_MODULE_FORMAT;
// We have two choices at this point, we can either transform CommonJS files
// and return the transformed source code, or let the default loader handle
// them. If we transform them ourselves we will enter CommonJS compatibility
// mode in the new module system in Node.js, this effectively means all
// CommonJS loaded via `require` calls from this point will all be treated as
// if it was loaded via `import` calls from modules.
//
// The CommonJS compatibility layer will try to identify named exports and
// make them available directly, which is convenient as it avoids things like
// `import(...).then(m => m.default.foo)`, allowing you to instead write
// `import(...).then(m => m.foo)`. The compatibility layer doesn't always work
// all that well though, and can lead to module loading issues in many cases,
// especially for older code.
// This `if` block opts-out of using CommonJS compatibility mode by default,
// and instead leaves it to our existing loader to transform CommonJS. We do
// however use compatibility mode for the more explicit .cts file extension,
// allows for a way to opt-in to the new behavior.
//
// TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead
if (format === 'commonjs' && ext !== '.cts') {
return nextLoad(url, { ...context, format });
}
const transformed = await transformFile(fileURLToPath(url), {
sourceMaps: 'inline',
module: {
type: format === 'module' ? 'es6' : 'commonjs',
ignoreDynamic: true,
// This helps the Node.js CommonJS compat layer identify named exports.
exportInteropAnnotation: true,
},
jsc: {
target: 'es2022',
parser: {
syntax: 'typescript',
},
},
});
return {
...context,
shortCircuit: true,
source: transformed.code,
format,
responseURL: url,
};
}
+1
View File
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"allowJs": true,
"declaration": true,
"declarationMap": false,
+1 -1
View File
@@ -24,6 +24,6 @@ import chalk from 'chalk';
),
);
const initializer = new CliInitializer();
initializer.add(import('./modules/config/alpha').then(m => m.default));
initializer.add(import('./modules/config/alpha'));
await initializer.run();
})();
+24 -34
View File
@@ -43,7 +43,7 @@ export function registerRepoCommand(program: Command) {
'--minify',
'Minify the generated code. Does not apply to app package (app is minified by default).',
)
.action(lazy(() => import('./repo/build').then(m => m.command)));
.action(lazy(() => import('./repo/build'), 'command'));
command
.command('lint')
@@ -70,7 +70,7 @@ export function registerRepoCommand(program: Command) {
'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
)
.option('--fix', 'Attempt to automatically fix violations')
.action(lazy(() => import('./repo/lint').then(m => m.command)));
.action(lazy(() => import('./repo/lint'), 'command'));
command
.command('fix')
@@ -83,20 +83,18 @@ export function registerRepoCommand(program: Command) {
'--check',
'Fail if any packages would have been changed by the command',
)
.action(lazy(() => import('./repo/fix').then(m => m.command)));
.action(lazy(() => import('./repo/fix'), 'command'));
command
.command('clean')
.description('Delete cache and output directories')
.action(lazy(() => import('./repo/clean').then(m => m.command)));
.action(lazy(() => import('./repo/clean'), 'command'));
command
.command('list-deprecations')
.description('List deprecations')
.option('--json', 'Output as JSON')
.action(
lazy(() => import('./repo/list-deprecations').then(m => m.command)),
);
.action(lazy(() => import('./repo/list-deprecations'), 'command'));
command
.command('test')
@@ -118,7 +116,7 @@ export function registerRepoCommand(program: Command) {
'Show help for Jest CLI options, which are passed through',
)
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
.action(lazy(() => import('./repo/test').then(m => m.command)));
.action(lazy(() => import('./repo/test'), 'command'));
}
export function registerScriptCommand(program: Command) {
@@ -139,7 +137,7 @@ export function registerScriptCommand(program: Command) {
)
.option('--require <path>', 'Add a --require argument to the node process')
.option('--link <path>', 'Link an external workspace for module resolution')
.action(lazy(() => import('./start').then(m => m.command)));
.action(lazy(() => import('./start'), 'command'));
command
.command('build')
@@ -163,7 +161,7 @@ export function registerScriptCommand(program: Command) {
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
Array<string>(),
)
.action(lazy(() => import('./build').then(m => m.command)));
.action(lazy(() => import('./build'), 'command'));
command
.command('lint [directories...]')
@@ -182,29 +180,29 @@ export function registerScriptCommand(program: Command) {
'Fail if more than this number of warnings. -1 allows warnings. (default: 0)',
)
.description('Lint a package')
.action(lazy(() => import('./lint').then(m => m.default)));
.action(lazy(() => import('./lint'), 'default'));
command
.command('test')
.allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
.helpOption(', --backstage-cli-help') // Let Jest handle help
.description('Run tests, forwarding args to Jest, defaulting to watch mode')
.action(lazy(() => import('./test').then(m => m.default)));
.action(lazy(() => import('./test'), 'default'));
command
.command('clean')
.description('Delete cache directories')
.action(lazy(() => import('./clean/clean').then(m => m.default)));
.action(lazy(() => import('./clean/clean'), 'default'));
command
.command('prepack')
.description('Prepares a package for packaging before publishing')
.action(lazy(() => import('./pack').then(m => m.pre)));
.action(lazy(() => import('./pack'), 'pre'));
command
.command('postpack')
.description('Restores the changes made by the prepack command')
.action(lazy(() => import('./pack').then(m => m.post)));
.action(lazy(() => import('./pack'), 'post'));
}
export function registerMigrateCommand(program: Command) {
@@ -215,39 +213,31 @@ export function registerMigrateCommand(program: Command) {
command
.command('package-roles')
.description(`Add package role field to packages that don't have it`)
.action(lazy(() => import('./migrate/packageRole').then(m => m.default)));
.action(lazy(() => import('./migrate/packageRole'), 'default'));
command
.command('package-scripts')
.description('Set package scripts according to each package role')
.action(
lazy(() => import('./migrate/packageScripts').then(m => m.command)),
);
.action(lazy(() => import('./migrate/packageScripts'), 'command'));
command
.command('package-exports')
.description('Synchronize package subpath export definitions')
.action(
lazy(() => import('./migrate/packageExports').then(m => m.command)),
);
.action(lazy(() => import('./migrate/packageExports'), 'command'));
command
.command('package-lint-configs')
.description(
'Migrates all packages to use @backstage/cli/config/eslint-factory',
)
.action(
lazy(() => import('./migrate/packageLintConfigs').then(m => m.command)),
);
.action(lazy(() => import('./migrate/packageLintConfigs'), 'command'));
command
.command('react-router-deps')
.description(
'Migrates the react-router dependencies for all packages to be peer dependencies',
)
.action(
lazy(() => import('./migrate/reactRouterDeps').then(m => m.command)),
);
.action(lazy(() => import('./migrate/reactRouterDeps'), 'command'));
}
export function registerCommands(program: Command) {
@@ -281,7 +271,7 @@ export function registerCommands(program: Command) {
'The license to use for any new packages (default: Apache-2.0)',
)
.option('--no-private', 'Do not mark new packages as private')
.action(lazy(() => import('./new/new').then(m => m.default)));
.action(lazy(() => import('./new/new'), 'default'));
registerConfigCommands(program);
registerRepoCommand(program);
@@ -302,7 +292,7 @@ export function registerCommands(program: Command) {
.option('--skip-install', 'Skips yarn install step')
.option('--skip-migrate', 'Skips migration of any moved packages')
.description('Bump Backstage packages to the latest versions')
.action(lazy(() => import('./versions/bump').then(m => m.default)));
.action(lazy(() => import('./versions/bump'), 'default'));
program
.command('versions:migrate')
@@ -317,7 +307,7 @@ export function registerCommands(program: Command) {
.description(
'Migrate any plugins that have been moved to the @backstage-community namespace automatically',
)
.action(lazy(() => import('./versions/migrate').then(m => m.default)));
.action(lazy(() => import('./versions/migrate'), 'default'));
program
.command('build-workspace <workspace-dir> [packages...]')
@@ -334,17 +324,17 @@ export function registerCommands(program: Command) {
'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)',
)
.description('Builds a temporary dist workspace from the provided packages')
.action(lazy(() => import('./buildWorkspace').then(m => m.default)));
.action(lazy(() => import('./buildWorkspace'), 'default'));
program
.command('create-github-app <github-org>')
.description('Create new GitHub App in your organization.')
.action(lazy(() => import('./create-github-app').then(m => m.default)));
.action(lazy(() => import('./create-github-app'), 'default'));
program
.command('info')
.description('Show helpful information for debugging and reporting bugs')
.action(lazy(() => import('./info').then(m => m.default)));
.action(lazy(() => import('./info'), 'default'));
// Notifications for removed commands
program
+84 -8
View File
@@ -16,7 +16,11 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { relative as relativePath, resolve as resolvePath } from 'path';
import {
extname,
relative as relativePath,
resolve as resolvePath,
} from 'path';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import postcss from 'rollup-plugin-postcss';
@@ -29,6 +33,7 @@ import {
RollupOptions,
OutputOptions,
WarningHandlerWithDefault,
OutputPlugin,
} from 'rollup';
import { forwardFileImports } from './plugins';
@@ -40,6 +45,12 @@ import { readEntryPoints } from '../entryPoints';
const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx'];
const MODULE_EXTS = ['.mjs', '.mts'];
const COMMONJS_EXTS = ['.cjs', '.cts'];
const MOD_EXT = '.mjs';
const CJS_EXT = '.cjs';
const CJS_JS_EXT = '.cjs.js';
function isFileImport(source: string) {
if (source.startsWith('.')) {
return true;
@@ -68,6 +79,39 @@ function buildInternalImportPattern(options: BuildOptions) {
return new RegExp(`^(?:${names.join('|')})(?:$|/)`);
}
// This Rollup output plugin enables support for mixed CommonJS and ESM output.
// It does it be filtering out the unwanted output files that don't match the
// input file format, allowing the rollup configuration to have overlapping
// output configurations for different formats.
function multiOutputFormat(): OutputPlugin {
return {
name: 'backstage-multi-output-format',
generateBundle(opts, bundle) {
const filter: (name: string) => boolean =
opts.format === 'cjs'
? s => s.endsWith(MOD_EXT)
: s => !s.endsWith(MOD_EXT);
// Delete any files that don't match the current output format
for (const name in bundle) {
if (filter(name)) {
delete bundle[name];
delete bundle[`${name}.map`];
}
}
},
renderDynamicImport(opts) {
if (opts.format === 'cjs') {
return {
left: 'import(',
right: ')',
};
}
return undefined;
},
};
}
export async function makeRollupConfigs(
options: BuildOptions,
): Promise<RollupOptions[]> {
@@ -120,18 +164,47 @@ export async function makeRollupConfigs(
const rewriteNodeModules = (name: string) =>
name.replaceAll('node_modules', 'node_modules_dist');
// For CommonJS we build both CommonJS and ESM output. Each of these outputs
// can output both .cjs and .mjs files. The files from each of these outputs
// will overlap, but we trim away files where the format doesn't match the
// file extensions. That way we are left with a combination of .cjs and .mjs
// files where the module format in the file matches the file extension.
if (options.outputs.has(Output.cjs)) {
output.push({
const defaultExt = targetPkg.type === 'module' ? MOD_EXT : CJS_JS_EXT;
const outputOpts: OutputOptions = {
dir: distDir,
entryFileNames: chunkInfo =>
`${rewriteNodeModules(chunkInfo.name)}.cjs.js`,
chunkFileNames: `cjs/[name]-[hash].cjs.js`,
format: 'commonjs',
interop: 'compat',
entryFileNames(chunkInfo) {
const cleanName = rewriteNodeModules(chunkInfo.name);
const inputId = chunkInfo.facadeModuleId;
if (!inputId) {
return cleanName + defaultExt;
}
const inputExt = extname(inputId);
if (MODULE_EXTS.includes(inputExt)) {
return cleanName + MOD_EXT;
}
if (COMMONJS_EXTS.includes(inputExt)) {
return cleanName + CJS_EXT;
}
return cleanName + defaultExt;
},
sourcemap: true,
preserveModules: true,
preserveModulesRoot: `${targetDir}/src`,
interop: 'compat',
exports: 'named',
plugins: [multiOutputFormat()],
};
output.push({
...outputOpts,
format: 'cjs',
});
output.push({
...outputOpts,
format: 'module',
});
}
if (options.outputs.has(Output.esm)) {
@@ -160,7 +233,10 @@ export async function makeRollupConfigs(
// All module imports are always marked as external
external,
plugins: [
resolve({ mainFields }),
resolve({
mainFields,
extensions: SCRIPT_EXTS,
}),
commonjs({
include: /node_modules/,
exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/],
+1 -1
View File
@@ -107,7 +107,7 @@ export const buildPackage = async (options: BuildOptions) => {
const rollupConfigs = await makeRollupConfigs(options);
await fs.remove(paths.resolveTarget('dist'));
await fs.remove(resolvePath(options.targetDir ?? paths.targetDir, 'dist'));
const buildTasks = rollupConfigs.map(rollupBuild);
+15 -3
View File
@@ -17,13 +17,25 @@
import { assertError } from '@backstage/errors';
import { exitWithError } from '../lib/errors';
type ActionFunc = (...args: any[]) => Promise<void>;
type ActionExports<TModule extends object> = {
[KName in keyof TModule as TModule[KName] extends ActionFunc
? KName
: never]: TModule[KName];
};
// Wraps an action function so that it always exits and handles errors
export function lazy(
getActionFunc: () => Promise<(...args: any[]) => Promise<void>>,
export function lazy<TModule extends object>(
moduleLoader: () => Promise<TModule>,
exportName: keyof ActionExports<TModule>,
): (...args: any[]) => Promise<never> {
return async (...args: any[]) => {
try {
const actionFunc = await getActionFunc();
const mod = await moduleLoader();
const actualModule = (
mod as unknown as { default: ActionExports<TModule> }
).default;
const actionFunc = actualModule[exportName] as ActionFunc;
await actionFunc(...args);
process.exit(0);
+1 -1
View File
@@ -32,7 +32,7 @@ export default createCliPlugin({
'Only include the schema that applies to the given package',
)
.description('Browse the configuration reference documentation')
.action(lazy(() => import('./commands/docs').then(m => m.default)));
.action(lazy(() => import('./commands/docs'), 'default'));
await defaultCommand.parseAsync(args, { from: 'user' });
},
+4 -4
View File
@@ -32,7 +32,7 @@ export function registerCommands(program: Command) {
'Only include the schema that applies to the given package',
)
.description('Browse the configuration reference documentation')
.action(lazy(() => import('./commands/docs').then(m => m.default)));
.action(lazy(() => import('./commands/docs'), 'default'));
program
.command('config:print')
@@ -49,7 +49,7 @@ export function registerCommands(program: Command) {
)
.option(...configOption)
.description('Print the app configuration for the current package')
.action(lazy(() => import('./commands/print').then(m => m.default)));
.action(lazy(() => import('./commands/print'), 'default'));
program
.command('config:check')
@@ -68,7 +68,7 @@ export function registerCommands(program: Command) {
.description(
'Validate that the given configuration loads and matches schema',
)
.action(lazy(() => import('./commands/validate').then(m => m.default)));
.action(lazy(() => import('./commands/validate'), 'default'));
program
.command('config:schema')
@@ -83,5 +83,5 @@ export function registerCommands(program: Command) {
.option('--merge', 'Print the config schemas merged', true)
.option('--no-merge', 'Print the config schemas not merged')
.description('Print configuration schema')
.action(lazy(() => import('./commands/schema').then(m => m.default)));
.action(lazy(() => import('./commands/schema'), 'default'));
}
@@ -0,0 +1,2 @@
!node_modules
dist
@@ -0,0 +1 @@
module.exports = 'a'
@@ -0,0 +1 @@
exports.value = 'a'
@@ -0,0 +1 @@
export default 'b'
@@ -0,0 +1 @@
export const value = 'b'
@@ -0,0 +1 @@
module.exports = 'c'
@@ -0,0 +1 @@
exports.value = 'c'
@@ -0,0 +1,15 @@
export const namedA: string
export const namedB: string
export const namedC: string
export const defaultA: string
export const defaultB: string
export const defaultC: string
export namespace dyn {
export const namedA: Promise<string>
export const namedB: Promise<string>
export const namedC: Promise<string>
export const defaultA: Promise<string>
export const defaultB: Promise<string>
export const defaultC: Promise<string>
}
@@ -0,0 +1,14 @@
exports.namedA = require('./a-named').value;
// exports.namedB = require('./b-named.mjs').value;
exports.namedC = require('./c-named.cjs').value;
exports.defaultA = require('./a-default');
// exports.defaultB = require('./b-default.mjs').default;
exports.defaultC = require('./c-default.cjs');
exports.dyn = {
namedA: import('./a-named').then(m => m.value),
namedB: import('./b-named.mjs').then(m => m.value),
namedC: import('./c-named.cjs').then(m => m.value),
defaultA: import('./a-default').then(m => m.default),
defaultB: import('./b-default.mjs').then(m => m.default),
defaultC: import('./c-default.cjs').then(m => m.default),
}
@@ -0,0 +1,14 @@
{
"name": "dep-commonjs",
"type": "commonjs",
"exports": {
".": "./main.js"
},
"typesVersions": {
"*": {
"*": [
"main.d.ts"
]
}
}
}
@@ -0,0 +1 @@
module.exports = 'a'
@@ -0,0 +1 @@
exports.value = 'a'
@@ -0,0 +1 @@
export default 'b'
@@ -0,0 +1 @@
export const value = 'b'
@@ -0,0 +1 @@
module.exports = 'c'
@@ -0,0 +1 @@
exports.value = 'c'
@@ -0,0 +1,15 @@
export const namedA: string
export const namedB: string
export const namedC: string
export const defaultA: string
export const defaultB: string
export const defaultC: string
export namespace dyn {
export const namedA: Promise<string>
export const namedB: Promise<string>
export const namedC: Promise<string>
export const defaultA: Promise<string>
export const defaultB: Promise<string>
export const defaultC: Promise<string>
}
@@ -0,0 +1,14 @@
exports.namedA = require('./a-named').value;
// exports.namedB = require('./b-named.mjs').value;
exports.namedC = require('./c-named.cjs').value;
exports.defaultA = require('./a-default');
// exports.defaultB = require('./b-default.mjs').default;
exports.defaultC = require('./c-default.cjs');
exports.dyn = {
namedA: import('./a-named').then(m => m.value),
namedB: import('./b-named.mjs').then(m => m.value),
namedC: import('./c-named.cjs').then(m => m.value),
defaultA: import('./a-default').then(m => m.default),
defaultB: import('./b-default.mjs').then(m => m.default),
defaultC: import('./c-default.cjs').then(m => m.default),
}
@@ -0,0 +1,13 @@
{
"name": "dep-default",
"exports": {
".": "./main.js"
},
"typesVersions": {
"*": {
"*": [
"main.d.ts"
]
}
}
}
@@ -0,0 +1 @@
export default 'a'
@@ -0,0 +1 @@
export const value = 'a'
@@ -0,0 +1 @@
export default 'b'
@@ -0,0 +1 @@
export const value = 'b'
@@ -0,0 +1 @@
module.exports = 'c'
@@ -0,0 +1 @@
exports.value = 'c'
@@ -0,0 +1,15 @@
export const namedA: string
export const namedB: string
export const namedC: string
export const defaultA: string
export const defaultB: string
export const defaultC: string
export namespace dyn {
export const namedA: Promise<string>
export const namedB: Promise<string>
export const namedC: Promise<string>
export const defaultA: Promise<string>
export const defaultB: Promise<string>
export const defaultC: Promise<string>
}
@@ -0,0 +1,14 @@
export { value as namedA } from './a-named'
export { value as namedB } from './b-named.mjs'
export { value as namedC } from './c-named.cjs'
export { default as defaultA } from './a-default'
export { default as defaultB } from './b-default.mjs'
export { default as defaultC } from './c-default.cjs'
export const dyn = {
namedA: import('./a-named').then(m => m.value),
namedB: import('./b-named.mjs').then(m => m.value),
namedC: import('./c-named.cjs').then(m => m.value),
defaultA: import('./a-default').then(m => m.default),
defaultB: import('./b-default.mjs').then(m => m.default),
defaultC: import('./c-default.cjs').then(m => m.default),
}
@@ -0,0 +1,14 @@
{
"name": "dep-module",
"type": "module",
"exports": {
".": "./main.js"
},
"typesVersions": {
"*": {
"*": [
"main.d.ts"
]
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'b';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'b';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'c';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'c';
@@ -0,0 +1,71 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as depCommonJs from 'dep-commonjs';
// import * as depModule from 'dep-module';
import * as depDefault from 'dep-default';
import { value as namedA } from './a-named';
// import { value as namedB } from './b-named.mts';
import { value as namedC } from './c-named.cts';
import { default as defaultA } from './a-default';
// import { default as defaultB } from './b-default.mts';
import { default as defaultC } from './c-default.cts';
async function resolveAll(obj: object): Promise<unknown> {
const val = await obj;
if (typeof val !== 'object' || val === null) {
return val;
}
if (Array.isArray(val)) {
return await Promise.all(val.map(resolveAll));
}
return Object.fromEntries(
await Promise.all(
Object.entries(obj).map(async ([key, value]) => [
key,
await resolveAll(await value),
]),
),
);
}
export const values = resolveAll({
depCommonJs,
// depModule,
depDefault,
dynCommonJs: import('dep-commonjs'),
dynModule: import('dep-module'),
dynDefault: import('dep-default'),
dep: {
namedA,
// namedB,
namedC,
defaultA,
// defaultB,
defaultC,
},
dyn: {
// @ts-expect-error Default exports from CommonJS are not well supported
namedA: import('./a-named').then(m => m.default.value),
namedB: import('./b-named.mts').then(m => m.value),
namedC: import('./c-named.cts').then(m => m.value),
// @ts-expect-error Default exports from CommonJS are not well supported
defaultA: import('./a-default').then(m => m.default.default),
defaultB: import('./b-default.mts').then(m => m.default),
// @ts-expect-error Default exports from CommonJS are not well supported
defaultC: import('./c-default.cts').then(m => m.default.default),
},
});
@@ -0,0 +1,8 @@
{
"name": "pkg-commonjs",
"type": "commonjs",
"exports": {
".": "./main.ts",
"./print": "./print.ts"
}
}
@@ -0,0 +1,19 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { values } from './main';
values.then(obj => console.log(JSON.stringify(obj, null, 2)));
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'b';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'b';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'c';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'c';
@@ -0,0 +1,71 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as depCommonJs from 'dep-commonjs';
// import * as depModule from 'dep-module';
import * as depDefault from 'dep-default';
import { value as namedA } from './a-named';
// import { value as namedB } from './b-named.mts';
import { value as namedC } from './c-named.cts';
import { default as defaultA } from './a-default';
// import { default as defaultB } from './b-default.mts';
import { default as defaultC } from './c-default.cts';
async function resolveAll(obj: object): Promise<unknown> {
const val = await obj;
if (typeof val !== 'object' || val === null) {
return val;
}
if (Array.isArray(val)) {
return await Promise.all(val.map(resolveAll));
}
return Object.fromEntries(
await Promise.all(
Object.entries(obj).map(async ([key, value]) => [
key,
await resolveAll(await value),
]),
),
);
}
export const values = resolveAll({
depCommonJs,
// depModule,
depDefault,
dynCommonJs: import('dep-commonjs'),
dynModule: import('dep-module'),
dynDefault: import('dep-default'),
dep: {
namedA,
// namedB,
namedC,
defaultA,
// defaultB,
defaultC,
},
dyn: {
// @ts-expect-error Default exports from CommonJS are not well supported
namedA: import('./a-named').then(m => m.default.value),
namedB: import('./b-named.mts').then(m => m.value),
namedC: import('./c-named.cts').then(m => m.value),
// @ts-expect-error Default exports from CommonJS are not well supported
defaultA: import('./a-default').then(m => m.default.default),
defaultB: import('./b-default.mts').then(m => m.default),
// @ts-expect-error Default exports from CommonJS are not well supported
defaultC: import('./c-default.cts').then(m => m.default.default),
},
});
@@ -0,0 +1,7 @@
{
"name": "pkg-default",
"exports": {
".": "./main.ts",
"./print": "./print.ts"
}
}
@@ -0,0 +1,19 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { values } from './main';
values.then(obj => console.log(JSON.stringify(obj, null, 2)));
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'a';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'b';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const value = 'b';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default 'c';

Some files were not shown because too many files have changed in this diff Show More