diff --git a/packages/cli/src/commands/build/buildBackend.ts b/packages/cli/src/commands/build/buildBackend.ts index d7c4f72dbe..7e61f9ae85 100644 --- a/packages/cli/src/commands/build/buildBackend.ts +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -37,6 +37,7 @@ export async function buildBackend(options: BuildBackendOptions) { // We build the target package without generating type declarations. await buildPackage({ targetDir: options.targetDir, + packageJson: pkg, outputs: new Set([Output.cjs]), }); diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index a6bf87da82..d5602a4008 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -130,6 +130,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { return { targetDir: pkg.dir, + packageJson: pkg.packageJson, outputs, logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, minify: buildOptions.minify, diff --git a/packages/cli/src/lib/builder/config.test.ts b/packages/cli/src/lib/builder/config.test.ts index a2e99da73b..733c35fb55 100644 --- a/packages/cli/src/lib/builder/config.test.ts +++ b/packages/cli/src/lib/builder/config.test.ts @@ -24,6 +24,11 @@ describe('makeRollupConfigs', () => { const [config] = await makeRollupConfigs({ outputs: new Set([Output.cjs]), + packageJson: { + name: 'test', + version: '0.0.0', + main: './src/index.ts', + }, }); const external = config.external as Exclude< ExternalOption, diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 1cfaf15bfc..ecd755d05b 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -31,6 +31,10 @@ import { forwardFileImports } from './plugins'; import { BuildOptions, Output } from './types'; import { paths } from '../paths'; import { svgrTemplate } from '../svgrTemplate'; +import { ExtendedPackageJSON } from '../monorepo'; +import { readEntryPoints } from '../monorepo/entryPoints'; + +const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx']; function isFileImport(source: string) { if (source.startsWith('.')) { @@ -50,6 +54,13 @@ export async function makeRollupConfigs( ): Promise { const configs = new Array(); const targetDir = options.targetDir ?? paths.targetDir; + + let targetPkg = options.packageJson; + if (!targetPkg) { + const packagePath = resolvePath(targetDir, 'package.json'); + targetPkg = (await fs.readJson(packagePath)) as ExtendedPackageJSON; + } + const onwarn = ({ code, message }: RollupWarning) => { if (code === 'EMPTY_BUNDLE') { return; // We don't care about this one @@ -62,111 +73,118 @@ export async function makeRollupConfigs( }; const distDir = resolvePath(targetDir, 'dist'); + const entryPoints = readEntryPoints(targetPkg); - if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) { - const output = new Array(); - const mainFields = ['module', 'main']; - - if (options.outputs.has(Output.cjs)) { - output.push({ - dir: distDir, - entryFileNames: 'index.cjs.js', - chunkFileNames: 'cjs/[name]-[hash].cjs.js', - format: 'commonjs', - sourcemap: true, - }); - } - if (options.outputs.has(Output.esm)) { - output.push({ - dir: distDir, - entryFileNames: 'index.esm.js', - chunkFileNames: 'esm/[name]-[hash].esm.js', - format: 'module', - sourcemap: true, - }); - // Assume we're building for the browser if ESM output is included - mainFields.unshift('browser'); + for (const { path, name, ext } of entryPoints) { + if (!SCRIPT_EXTS.includes(ext)) { + continue; } - configs.push({ - input: resolvePath(targetDir, 'src/index.ts'), - output, - onwarn, - preserveEntrySignatures: 'strict', - // All module imports are always marked as external - external: (source, importer, isResolved) => - Boolean(importer && !isResolved && !isFileImport(source)), - plugins: [ - resolve({ mainFields }), - commonjs({ - include: /node_modules/, - exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/], - }), - postcss(), - forwardFileImports({ - exclude: /\.icon\.svg$/, - include: [ - /\.svg$/, - /\.png$/, - /\.gif$/, - /\.jpg$/, - /\.jpeg$/, - /\.eot$/, - /\.woff$/, - /\.woff2$/, - /\.ttf$/, - ], - }), - json(), - yaml(), - svgr({ - include: /\.icon\.svg$/, - template: svgrTemplate, - }), - esbuild({ - target: 'es2019', - minify: options.minify, - }), - ], - }); - } + if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) { + const output = new Array(); + const mainFields = ['module', 'main']; - if (options.outputs.has(Output.types) && !options.useApiExtractor) { - const typesInput = paths.resolveTargetRoot( - 'dist-types', - relativePath(paths.targetRoot, targetDir), - 'src/index.d.ts', - ); + if (options.outputs.has(Output.cjs)) { + output.push({ + dir: distDir, + entryFileNames: `${name}.cjs.js`, + chunkFileNames: `cjs/${name}/[name]-[hash].cjs.js`, + format: 'commonjs', + sourcemap: true, + }); + } + if (options.outputs.has(Output.esm)) { + output.push({ + dir: distDir, + entryFileNames: `${name}.esm.js`, + chunkFileNames: `esm/${name}/[name]-[hash].esm.js`, + format: 'module', + sourcemap: true, + }); + // Assume we're building for the browser if ESM output is included + mainFields.unshift('browser'); + } - const declarationsExist = await fs.pathExists(typesInput); - if (!declarationsExist) { - const path = relativePath(targetDir, typesInput); - throw new Error( - `No declaration files found at ${path}, be sure to run ${chalk.bgRed.white( - 'yarn tsc', - )} to generate .d.ts files before packaging`, + configs.push({ + input: resolvePath(targetDir, path), + output, + onwarn, + preserveEntrySignatures: 'strict', + // All module imports are always marked as external + external: (source, importer, isResolved) => + Boolean(importer && !isResolved && !isFileImport(source)), + plugins: [ + resolve({ mainFields }), + commonjs({ + include: /node_modules/, + exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/], + }), + postcss(), + forwardFileImports({ + exclude: /\.icon\.svg$/, + include: [ + /\.svg$/, + /\.png$/, + /\.gif$/, + /\.jpg$/, + /\.jpeg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], + }), + json(), + yaml(), + svgr({ + include: /\.icon\.svg$/, + template: svgrTemplate, + }), + esbuild({ + target: 'es2019', + minify: options.minify, + }), + ], + }); + } + + if (options.outputs.has(Output.types) && !options.useApiExtractor) { + const typesInput = paths.resolveTargetRoot( + 'dist-types', + relativePath(paths.targetRoot, targetDir), + path.replace(/\.ts$/, '.d.ts'), ); - } - configs.push({ - input: typesInput, - output: { - file: resolvePath(distDir, 'index.d.ts'), - format: 'es', - }, - external: [ - /\.css$/, - /\.scss$/, - /\.sass$/, - /\.svg$/, - /\.eot$/, - /\.woff$/, - /\.woff2$/, - /\.ttf$/, - ], - onwarn, - plugins: [dts()], - }); + const declarationsExist = await fs.pathExists(typesInput); + if (!declarationsExist) { + const declarationPath = relativePath(targetDir, typesInput); + throw new Error( + `No declaration files found at ${declarationPath}, be sure to run ${chalk.bgRed.white( + 'yarn tsc', + )} to generate .d.ts files before packaging`, + ); + } + + configs.push({ + input: typesInput, + output: { + file: resolvePath(distDir, `${name}.d.ts`), + format: 'es', + }, + external: [ + /\.css$/, + /\.scss$/, + /\.sass$/, + /\.svg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], + onwarn, + plugins: [dts()], + }); + } } return configs; diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index f43d84cd92..de7e9b10eb 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ExtendedPackageJSON } from '../monorepo'; + export enum Output { esm, cjs, @@ -23,6 +25,7 @@ export enum Output { export type BuildOptions = { logPrefix?: string; targetDir?: string; + packageJson?: ExtendedPackageJSON; outputs: Set; minify?: boolean; useApiExtractor?: boolean; diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index d34ede41ae..421d9aa25b 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -25,6 +25,9 @@ import { JsonValue } from '@backstage/types'; type PackageJSON = Package['packageJson']; export interface ExtendedPackageJSON extends PackageJSON { + main?: string; + module?: string; + scripts?: { [key: string]: string; }; diff --git a/packages/cli/src/lib/monorepo/entryPoints.ts b/packages/cli/src/lib/monorepo/entryPoints.ts new file mode 100644 index 0000000000..8a8bbd27f9 --- /dev/null +++ b/packages/cli/src/lib/monorepo/entryPoints.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2023 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 { extname } from 'path'; +import { ExtendedPackageJSON } from './PackageGraph'; + +export interface EntryPoint { + mount: string; + path: string; + name: string; + ext: string; +} + +function parseEntryPoint(mount: string, path: string): EntryPoint { + let name = mount; + if (name === '.') { + name = 'index'; + } else if (name.startsWith('./')) { + name = name.slice(2); + } + if (name.includes('/')) { + throw new Error(`Mount point '${mount}' may not contain multiple slashes`); + } + + return { mount, path, name, ext: extname(path) }; +} + +export function readEntryPoints(pkg: ExtendedPackageJSON): Array { + const exp = pkg.exports; + if (typeof exp === 'string') { + return [parseEntryPoint('.', exp)]; + } else if (exp && typeof exp === 'object' && !Array.isArray(exp)) { + const entryPoints = new Array<{ + mount: string; + path: string; + name: string; + ext: string; + }>(); + + for (const mount of Object.keys(exp)) { + const path = exp[mount]; + if (typeof path !== 'string') { + throw new Error( + `Exports field value must be a string, got '${JSON.stringify(path)}'`, + ); + } + + entryPoints.push(parseEntryPoint(mount, path)); + } + + return entryPoints; + } + + const main = pkg.main || pkg.module; + if (main) { + return [parseEntryPoint('.', main)]; + } + + return []; +} diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts index 5451a873f7..28b5526fc9 100644 --- a/packages/cli/src/lib/packager/createDistWorkspace.ts +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -178,6 +178,7 @@ export async function createDistWorkspace( if (outputs.size > 0) { standardBuilds.push({ targetDir: pkg.dir, + packageJson: pkg.packageJson, outputs: outputs, logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, // No need to detect these for the backend builds, we assume no minification or types