Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-bobcats-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/cli': patch
---

Improved CLI install speed by bundling the CLI into a dependency-free package.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
!tsconfig.build.json
!tsconfig.json
!package-lock.json
!README.md
!LICENSE.md
!packages/*
!scripts/local-pack.sh
1 change: 1 addition & 0 deletions .github/workflows/performance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
with:
node-version: 24
cache: npm
- uses: oven-sh/setup-bun@v2
- name: Install Dependencies
run: npm ci
- name: Install External
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- name: Install Dependencies
run: npm ci

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Create Release Pull Request or Publish to npm
id: changesets
uses: RomanHotsiy/changesets-action@v1
Expand Down Expand Up @@ -200,6 +203,9 @@ jobs:
cache: npm
registry-url: https://registry.npmjs.org

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Update package versions
run: |
TIMESTAMP=$(date +%s)
Expand Down Expand Up @@ -255,4 +261,6 @@ jobs:
sleep 10

cd ../cli
npm publish --tag snapshot
npm run prepare:publish-dir
npm publish ./.publish --tag snapshot
npm run clean:publish-dir
1 change: 1 addition & 0 deletions .github/workflows/smoke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
with:
node-version: 24
cache: npm
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
- name: Prepare Smoke
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
continue-on-error: true # Do not fail if there is an error during reporting
uses: davelosert/vitest-coverage-report-action@v2

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: E2E Tests
run: npm run e2e

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ output/
*.tsbuildinfo
*.tgz
redoc-static.html
packages/cli/README.md
packages/cli/.publish/
.env
packages/respect-core/src/modules/runtime-expressions/abnf-parser.js
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ RUN apk add --no-cache jq git && \
npm run prepare && \
npm run pack:prepare && \
npm install --global redocly-cli.tgz && \
cp packages/cli/src/commands/build-docs/template.hbs \
/usr/local/lib/node_modules/@redocly/cli/lib/commands/build-docs/ && \
# Clean up to reduce image size
npm cache clean --force && rm -rf /build

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"scripts": {
"test": "npm run compile && npm run typecheck && npm run unit && npm run e2e",
"unit": "VITEST_SUITE=unit vitest run",
"e2e": "VITEST_SUITE=e2e vitest run",
"e2e": "npm run compile:cli:bundle && VITEST_SUITE=e2e vitest run",
"smoke:rebilly": "VITEST_SUITE=smoke-rebilly vitest run",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
Expand All @@ -24,8 +24,9 @@
"prepare": "husky && npm run compile",
"cli": "node --import tsx packages/cli/src/index.ts",
"precli": "npm run compile",
"release": "changeset publish",
"release": "node ./scripts/release-publish.mjs",
"pack:prepare": "./scripts/local-pack.sh",
"compile:cli:bundle": "npm --workspace packages/cli run compile",
Comment thread
tatomyr marked this conversation as resolved.
"respect:parser:generate": "pegjs --format es --output packages/respect-core/lib/modules/runtime-expressions/abnf-parser.js packages/respect-core/src/modules/runtime-expressions/abnf-parser.pegjs && cp packages/respect-core/lib/modules/runtime-expressions/abnf-parser.js packages/respect-core/src/modules/runtime-expressions/abnf-parser.js",
"build-docs:copy-assets": "cp packages/cli/src/commands/build-docs/template.hbs packages/cli/lib/commands/build-docs/template.hbs ",
"json-server": "json-server --watch tests/e2e/respect/local-json-server/fake-db.json --port 3000 --host 0.0.0.0"
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
},
"engineStrict": true,
"scripts": {
"compile": "tsc",
"copy-assets": "cp src/commands/build-docs/template.hbs lib/commands/build-docs/template.hbs ",
"prepack": "npm run copy-assets",
"prepublishOnly": "npm run copy-assets && cp ../../README.md ."
"compile": "bun build src/index.ts --target node --define process.env.NODE_ENV=process.env.NODE_ENV --outfile lib/index.js",
"prepare:publish-dir": "node ./scripts/prepare-publish-dir.mjs",
"clean:publish-dir": "node ./scripts/clean-publish-dir.mjs"
Comment thread
tatomyr marked this conversation as resolved.
Outdated
},
"publishConfig": {
"directory": ".publish"
},
"repository": {
"type": "git",
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/scripts/clean-publish-dir.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { rmSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const packageDir = path.resolve(scriptDir, '..');
const publishDir = path.join(packageDir, '.publish');

rmSync(publishDir, { recursive: true, force: true });
108 changes: 108 additions & 0 deletions packages/cli/scripts/prepare-publish-dir.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { spawnSync } from 'node:child_process';
import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const packageDir = path.resolve(scriptDir, '..');
const rootDir = path.resolve(packageDir, '..', '..');
const publishDir = path.join(packageDir, '.publish');

const packageJsonPath = path.join(packageDir, 'package.json');
const readmeSourcePath = path.join(rootDir, 'README.md');
const licenseSourcePath = path.join(rootDir, 'LICENSE.md');

const bunBuildArgs = [
'build',
'src/index.ts',
'--target',
'node',
'--define',
'process.env.NODE_ENV=process.env.NODE_ENV',
'--outfile',
'lib/index.js',
];

const compileResult = compileWithBun();

if (compileResult.error) {
throw compileResult.error;
}

if (compileResult.status !== 0) {
process.exit(compileResult.status ?? 1);
}

rmSync(publishDir, { recursive: true, force: true });
mkdirSync(publishDir, { recursive: true });

const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const binEntrypoint = packageJson.bin?.redocly ?? 'bin/cli.js';
const binTargetPath = path.join(publishDir, binEntrypoint);

mkdirSync(path.dirname(binTargetPath), { recursive: true });
mkdirSync(path.join(publishDir, 'lib'), { recursive: true });

copyFileSync(path.join(packageDir, binEntrypoint), binTargetPath);
copyFileSync(path.join(packageDir, 'lib/index.js'), path.join(publishDir, 'lib/index.js'));
copyFileSync(readmeSourcePath, path.join(publishDir, 'README.md'));
copyFileSync(licenseSourcePath, path.join(publishDir, 'LICENSE'));

const publishPackageJson = compactObject({
name: packageJson.name,
version: packageJson.version,
description: packageJson.description,
license: packageJson.license,
type: packageJson.type,
repository: packageJson.repository,
homepage: packageJson.homepage,
keywords: packageJson.keywords,
contributors: packageJson.contributors,
engines: packageJson.engines,
engineStrict: packageJson.engineStrict,
bin: {
redocly: binEntrypoint,
openapi: binEntrypoint,
},
files: [binEntrypoint, 'lib/index.js', 'README.md', 'LICENSE'],
});

writeFileSync(
path.join(publishDir, 'package.json'),
`${JSON.stringify(publishPackageJson, null, 2)}\n`
);

function compactObject(value) {
if (Array.isArray(value)) {
return value.filter((item) => item !== undefined);
}

if (!value || typeof value !== 'object') {
return value;
}

const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined);
return Object.fromEntries(entries);
Comment thread
tatomyr marked this conversation as resolved.
Outdated
}

function compileWithBun() {
const directBun = spawnSync('bun', bunBuildArgs, {
cwd: packageDir,
stdio: 'inherit',
});

if (!directBun.error) {
return directBun;
}

if (directBun.error.code !== 'ENOENT') {
throw directBun.error;
}

// Fallback for environments where Bun is not preinstalled (e.g. some CI/docker jobs).
console.warn('Bun binary not found on PATH; falling back to `npx bun@1.3.10`.');
return spawnSync('npx', ['--yes', 'bun@1.3.10', ...bunBuildArgs], {
cwd: packageDir,
stdio: 'inherit',
});
}
4 changes: 1 addition & 3 deletions packages/cli/src/commands/build-docs/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { isAbsoluteUrl, logger } from '@redocly/openapi-core';
import { writeFileSync, mkdirSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { performance } from 'node:perf_hooks';
import { default as redoc } from 'redoc';

import packageJson from '../../../package.json' with { type: 'json' };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It probably makes more sense to import it from utils/package.js.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

solved

import { exitWithError } from '../../utils/error.js';
import { getExecutionTime, getFallbackApisOrExit } from '../../utils/miscellaneous.js';
import type { CommandArgs } from '../../wrapper.js';
import type { BuildDocsArgv } from './types.js';
import { getObjectOrJSON, getPageHTML } from './utils.js';

const packageJson = createRequire(import.meta.url ?? __dirname)('../../../package.json');

export const handlerBuildCommand = async ({
argv,
config,
Expand Down
44 changes: 42 additions & 2 deletions packages/cli/src/commands/build-docs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ import type { BuildDocsOptions } from './types.js';
const __internalDirname = import.meta.url
? path.dirname(url.fileURLToPath(import.meta.url))
: __dirname;
const DEFAULT_TEMPLATE_FILE_NAME = path.join(__internalDirname, './template.hbs');
const DEFAULT_TEMPLATE_SOURCE = `<!DOCTYPE html>
<html>

<head>
<meta charset="utf8" />
<title>{{title}}</title>
<!-- needed for adaptive design -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 0;
margin: 0;
}
</style>
{{{redocHead}}}
{{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
</head>

<body>
{{{redocHTML}}}
</body>

</html>
`;

export function getObjectOrJSON(
openapiOptions: string | Record<string, unknown>,
Expand Down Expand Up @@ -75,8 +100,10 @@ export async function getPageHTML(
? templateFileName
: redocOptions?.htmlTemplate
? path.resolve(configPath ? path.dirname(configPath) : '', redocOptions.htmlTemplate)
: path.join(__internalDirname, './template.hbs');
const template = handlebars.compile(readFileSync(templateFileName).toString());
: DEFAULT_TEMPLATE_FILE_NAME;

const templateSource = resolveTemplateSource(templateFileName);
const template = handlebars.compile(templateSource);
return template({
redocHTML: `
<div id="redoc">${html || ''}</div>
Expand All @@ -96,6 +123,19 @@ export async function getPageHTML(
});
}

function resolveTemplateSource(templateFileName: string): string {
if (templateFileName !== DEFAULT_TEMPLATE_FILE_NAME) {
return readFileSync(templateFileName, 'utf-8');
}

try {
return readFileSync(templateFileName, 'utf-8');
} catch {
// Bun bundling may not include template.hbs as a real file; fallback to the embedded template.
return DEFAULT_TEMPLATE_SOURCE;
}
}

export function sanitizeJSONString(str: string): string {
return escapeClosingScriptTag(escapeUnicode(str));
}
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/utils/package.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { createRequire } from 'node:module';

const packageJson = createRequire(import.meta.url ?? __dirname)('../../package.json');
import packageJson from '../../package.json' with { type: 'json' };
Comment thread
tatomyr marked this conversation as resolved.

export const { version, name, engines } = packageJson;
8 changes: 3 additions & 5 deletions scripts/local-pack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Backup package.json files
cp packages/core/package.json packages/core/package.json.bak
cp packages/respect-core/package.json packages/respect-core/package.json.bak
cp packages/cli/package.json packages/cli/package.json.bak

# Build and pack core package
cd packages/core
Expand All @@ -20,13 +19,12 @@ cd ../../

# Update and pack cli package
cd packages/cli
jq '.dependencies["@redocly/openapi-core"] = "./openapi-core.tgz"' package.json > tmp.json && mv tmp.json package.json
jq '.dependencies["@redocly/respect-core"] = "./respect-core.tgz"' package.json > tmp.json && mv tmp.json package.json
cli=$(npm pack | tail -n 1)
npm run prepare:publish-dir
cli=$(npm pack ./.publish | tail -n 1)
npm run clean:publish-dir
mv $cli ../../redocly-cli.tgz
cd ../../

# Restore original package.json files
mv packages/core/package.json.bak packages/core/package.json
mv packages/respect-core/package.json.bak packages/respect-core/package.json
mv packages/cli/package.json.bak packages/cli/package.json
Loading
Loading