feat(twenty-sdk): include app readme in published package (#22431)
## Context When running `yarn twenty app:publish`, no README was included in the npm-published app package. Closes twentyhq/core-team-issues#2632 ## What changed - Added `copy-readme-to-output.ts`, which finds the app's root readme file (matched case-insensitively, preferring the markdown variant, mirroring how npm ranks README candidates) and copies it into the build output directory (`.twenty/output/`). - Wired `copyReadmeToOutput` into `buildApplication` — the shared build path used by `publish`, `build`, and `dev` — so the readme is present when `npm publish`/`npm pack` runs from the output directory. npm only ships a README when the file lives in the package root, which for published apps is `.twenty/output/`. The readme is not tracked in the manifest checksums; it is a pure npm packaging artifact, so it is only copied into the output directory and does not affect app installation/validation. ## Tests - Added unit tests for `findReadmeFileName` (case-insensitivity, markdown preference, ignoring unrelated files) and `copyReadmeToOutput` (copies the readme into the output dir; no-ops when the app has no readme). https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL --- _Generated by [Claude Code](https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22431?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { OUTPUT_DIR } from 'twenty-shared/application';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
copyReadmeToOutput,
|
||||
findReadmeFileName,
|
||||
} from '@/cli/utilities/build/common/copy-readme-to-output';
|
||||
import { pathExists } from '@/cli/utilities/file/fs-utils';
|
||||
|
||||
describe('findReadmeFileName', () => {
|
||||
it('should match readme files regardless of case', () => {
|
||||
expect(findReadmeFileName(['README.md'])).toBe('README.md');
|
||||
expect(findReadmeFileName(['readme.md'])).toBe('readme.md');
|
||||
expect(findReadmeFileName(['Readme.txt'])).toBe('Readme.txt');
|
||||
expect(findReadmeFileName(['README'])).toBe('README');
|
||||
});
|
||||
|
||||
it('should prefer the markdown readme over other variants', () => {
|
||||
expect(findReadmeFileName(['README.txt', 'README.md'])).toBe('README.md');
|
||||
});
|
||||
|
||||
it('should ignore unrelated files', () => {
|
||||
expect(
|
||||
findReadmeFileName(['index.ts', 'package.json', 'readme-notes.md']),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyReadmeToOutput', () => {
|
||||
let appPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
appPath = await mkdtemp(join(tmpdir(), 'readme-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(appPath, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should copy the readme into the output directory', async () => {
|
||||
await writeFile(join(appPath, 'README.md'), '# My App', 'utf-8');
|
||||
|
||||
await copyReadmeToOutput(appPath);
|
||||
|
||||
const copiedReadme = await readFile(
|
||||
join(appPath, OUTPUT_DIR, 'README.md'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
expect(copiedReadme).toBe('# My App');
|
||||
});
|
||||
|
||||
it('should do nothing when the app has no readme', async () => {
|
||||
await writeFile(join(appPath, 'package.json'), '{}', 'utf-8');
|
||||
|
||||
await copyReadmeToOutput(appPath);
|
||||
|
||||
expect(await pathExists(join(appPath, OUTPUT_DIR))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { copyReadmeToOutput } from '@/cli/utilities/build/common/copy-readme-to-output';
|
||||
import { type GeneratedAsset } from '@/cli/utilities/build/cover/generated-asset.type';
|
||||
import { esbuildOneShotBuild } from '@/cli/utilities/build/common/esbuild-one-shot-build';
|
||||
import { LOGIC_FUNCTION_EXTERNAL_MODULES } from '@/cli/utilities/build/common/esbuild-watcher';
|
||||
@@ -153,6 +154,8 @@ export const buildApplication = async (
|
||||
});
|
||||
}
|
||||
|
||||
await copyReadmeToOutput(options.appPath);
|
||||
|
||||
return { builtFileInfos };
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { OUTPUT_DIR } from 'twenty-shared/application';
|
||||
|
||||
import { copy, ensureDir } from '@/cli/utilities/file/fs-utils';
|
||||
|
||||
// npm only ships a README when the file lives in the package root, so the
|
||||
// app's readme has to be copied into the build output that gets published.
|
||||
const README_FILE_NAME_REGEX = /^readme(\.[^.]+)?$/i;
|
||||
|
||||
export const findReadmeFileName = (
|
||||
entries: string[],
|
||||
): string | undefined => {
|
||||
const readmeFileNames = entries.filter((entry) =>
|
||||
README_FILE_NAME_REGEX.test(entry),
|
||||
);
|
||||
|
||||
// Prefer markdown, matching how npm ranks README candidates.
|
||||
return (
|
||||
readmeFileNames.find((entry) => /\.md$/i.test(entry)) ?? readmeFileNames[0]
|
||||
);
|
||||
};
|
||||
|
||||
export const copyReadmeToOutput = async (appPath: string): Promise<void> => {
|
||||
const readmeFileName = findReadmeFileName(await readdir(appPath));
|
||||
|
||||
if (readmeFileName === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outputDir = join(appPath, OUTPUT_DIR);
|
||||
|
||||
await ensureDir(outputDir);
|
||||
await copy(join(appPath, readmeFileName), join(outputDir, readmeFileName));
|
||||
};
|
||||
Reference in New Issue
Block a user