Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion integration-tests/packtory/publish.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable import/max-dependencies -- needed */
import path from 'node:path';
import test from 'ava';
import npmFetch from 'npm-registry-fetch';
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async function createTemporaryDirectory(): Promise<string> {
}

async function createRegistryServer(storageDirectory: string): Promise<Server> {
// @ts-expect-error
// @ts-expect-error -- ok in this case
const server = (await runServer({ ...configuration, storage: storageDirectory })) as Server;
return server;
}
Expand Down
6,112 changes: 2,586 additions & 3,526 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"@schema-hub/zod-error-formatter": "0.0.10",
"@topcli/spinner": "3.0.0",
"@ts-morph/common": "0.28.0",
"@types/common-tags": "1.8.4",
"cmd-ts": "0.14.2",
"common-tags": "1.8.2",
"effect": "2.2.5",
"kleur": "4.1.5",
"libnpmpublish": "11.1.1",
Expand All @@ -60,10 +62,10 @@
},
"devDependencies": {
"@ava/typescript": "6.0.0",
"@enormora/eslint-config-ava": "0.0.7",
"@enormora/eslint-config-base": "0.0.7",
"@enormora/eslint-config-node": "0.0.7",
"@enormora/eslint-config-typescript": "0.0.7",
"@enormora/eslint-config-ava": "0.0.24",
"@enormora/eslint-config-base": "0.0.23",
"@enormora/eslint-config-node": "0.0.20",
"@enormora/eslint-config-typescript": "0.0.26",
"@types/libnpmpublish": "9.0.1",
"@types/node": "22.18.6",
"@types/npm-registry-fetch": "8.0.8",
Expand All @@ -72,15 +74,15 @@
"@types/ssri": "7.1.5",
"@types/tar-stream": "3.1.4",
"ava": "6.4.1",
"eslint": "8.57.1",
"eslint": "9.36.0",
"get-port": "7.1.0",
"prettier": "3.3.3",
"prettier": "3.6.2",
"sinon": "21.0.0",
"type-fest": "5.0.1",
"typescript": "5.9.2",
"verdaccio": "6.1.6"
},
"engines": {
"node": "^20 || ^22"
"node": "^22 || ^24"
}
}
36 changes: 12 additions & 24 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@ Tired of restrictive monorepo conventions? Fed up with complex workspace setups?

Say goodbye to:

- 🔗 Cumbersome workspaces
- 📦 Dependency linking during development
- 🙅‍♂️ Manual file selection (e.g. via `.npmignore` or `files`)
- 📄 Shipping unnecessary files (e.g. build configs, tests)
- 🔄 Manual versioning
- 🔗 Cumbersome workspaces
- 📦 Dependency linking during development
- 🙅‍♂️ Manual file selection (e.g. via `.npmignore` or `files`)
- 📄 Shipping unnecessary files (e.g. build configs, tests)
- 🔄 Manual versioning

## 🌟 **Introducing packtory: Your Code Organization and Packaging Game Changer** 🌟

**Key Features:**

- **Organize with Freedom**: Manage your monorepo without confining conventions or workspace limitations. packtory simplifies it, just like a single codebase.
- **Effortless Dependency Bundling**: Forget manual dependency linking. packtory automatically detects and bundles dependencies, freeing you to focus on your code.
- **Clean and Efficient Packaging**: Package only essential files, excluding devDependencies, CI configurations, and tests. Keep your npm package clean and efficient.
- **Revolutionary Automatic Versioning**: Choose manual versioning or let packtory handle it. In automatic mode, it calculates versions intelligently, ensuring reproducibility without complexity.
- **Seamless CI Pipeline Integration**: Easily integrate packtory into your CI pipelines for automatic publishing with every commit. No more intricate checks to decide what to publish.
- **Organize with Freedom**: Manage your monorepo without confining conventions or workspace limitations. packtory simplifies it, just like a single codebase.
- **Effortless Dependency Bundling**: Forget manual dependency linking. packtory automatically detects and bundles dependencies, freeing you to focus on your code.
- **Clean and Efficient Packaging**: Package only essential files, excluding devDependencies, CI configurations, and tests. Keep your npm package clean and efficient.
- **Revolutionary Automatic Versioning**: Choose manual versioning or let packtory handle it. In automatic mode, it calculates versions intelligently, ensuring reproducibility without complexity.
- **Seamless CI Pipeline Integration**: Easily integrate packtory into your CI pipelines for automatic publishing with every commit. No more intricate checks to decide what to publish.

## Quick Start

Expand Down Expand Up @@ -85,8 +85,8 @@ For more details about the CLI application have a look at the [full documentatio

Packtory guarantees minimal packages with:

- No published `devDependencies`
- No unnecessary files, including CI configurations
- No published `devDependencies`
- No unnecessary files, including CI configurations

**How Bundling Works:**

Expand All @@ -102,7 +102,6 @@ Packtory guarantees minimal packages with:
Packtory supports two versioning modes:

1. **Automatic Versioning (Default) 🔄:**

- Fetch the version details of the latest information available from the registry.
- Download and extract the tarball of the latest version in-memory.
- Compare the contents of all files from the downloaded tarball with the contents of all files resolved from the bundler:
Expand All @@ -120,57 +119,46 @@ This explanation provides a comprehensive overview of the bundling and publishin
The configuration for `packtory` is an object with the following properties:

1. **`registrySettings`** (Required):

- An object with at least a required `token` for authentication.
- Optionally, you can provide a custom `registryUrl` for non-default registries.

2. **`commonPackageSettings`** (Optional):

- Defines settings that can be shared for all packages.
- Allowed settings: `sourcesFolder`, `mainPackageJson`, `includeSourceMapFiles`, `additionalFiles`, `additionalPackageJsonAttributes`.

3. **`packages`** (Required, Array):

- An array of per-package configurations.
- Each per-package configuration has the following settings:

- **`name`** (Required, String):

- Must be unique; the name of the package.

- **`sourcesFolder`** (Required):

- The absolute path to the base folder of the source files.
- All other file paths are resolved relative to this path.

- **`mainPackageJson`** (Required):

- The parsed content of the project's `package.json`.
- Needed to obtain version numbers of third-party dependencies.

- **`entryPoints`** (Required, Array of Objects):

- An array of entry points with the following shape: `{ js: 'file.js', declarationFile: 'file.d.ts' }`.
- The `js` property is required, while `declarationFile` is optional.

- **`includeSourceMapFiles`** (Optional, Boolean, Default: `false`):

- If `true`, the bundler will look for and include source map files in the final package.

- **`additionalFiles`** (Optional, Array of File Descriptions):

- An array to add additional files to the package that are not automatically resolved.
- Example: `{ sourceFilePath: 'LICENSE', targetFilePath: 'LICENSE' }`.
- If defined in both per-package and common settings, they are merged.

- **`additionalPackageJsonAttributes`** (Optional, Object):

- An object to be merged directly into the generated `package.json`.
- Useful for setting meta properties like `description` or `keywords`.
- If defined in both per-package and common settings, they are merged.

- **`bundleDependencies`** (Optional, Array of Strings):

- An array of package names to mark as dependencies, allowing the bundler to substitute import statements accordingly.

- **`bundlePeerDependencies`** (Optional, Array of Strings):
Expand Down
1 change: 1 addition & 0 deletions source/artifacts/artifacts-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Overrides = {
readonly tarballBuilder?: { readonly build?: SinonSpy };
};

// eslint-disable-next-line complexity -- needs to be refactored
function artifactsBuilderFactory(overrides: Overrides = {}): ArtifactsBuilder {
const {
readFile = fake(),
Expand Down
6 changes: 3 additions & 3 deletions source/artifacts/artifacts-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export type TarballArtifact = {
};

export type ArtifactsBuilder = {
collectContents(bundle: VersionedBundleWithManifest, prefix?: string): readonly FileDescription[];
buildTarball(bundle: VersionedBundleWithManifest): Promise<TarballArtifact>;
buildFolder(bundle: VersionedBundleWithManifest, targetFolder: string): Promise<void>;
collectContents: (bundle: VersionedBundleWithManifest, prefix?: string) => readonly FileDescription[];
buildTarball: (bundle: VersionedBundleWithManifest) => Promise<TarballArtifact>;
buildFolder: (bundle: VersionedBundleWithManifest, targetFolder: string) => Promise<void>;
};

export function createArtifactsBuilder(artifactsBuilderDependencies: ArtifactsBuilderDependencies): ArtifactsBuilder {
Expand Down
1 change: 1 addition & 0 deletions source/bundle-emitter/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Overrides = {
readonly fetchTarball?: SinonSpy;
};

// eslint-disable-next-line complexity -- needs to be refactored
function emitterFactory(overrides: Overrides = {}): BundleEmitter {
const {
buildTarball = fake.resolves({}),
Expand Down
6 changes: 3 additions & 3 deletions source/bundle-emitter/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ type BundlePublishedCheckResult = {
};

export type BundleEmitter = {
publish(options: PublishOptions): Promise<void>;
determineCurrentVersion(options: CurrentVersionLookupOptions): Promise<Maybe<string>>;
checkBundleAlreadyPublished(options: PublishOptions): Promise<BundlePublishedCheckResult>;
publish: (options: PublishOptions) => Promise<void>;
determineCurrentVersion: (options: CurrentVersionLookupOptions) => Promise<Maybe<string>>;
checkBundleAlreadyPublished: (options: PublishOptions) => Promise<BundlePublishedCheckResult>;
};

export function createBundleEmitter(dependencies: BundleEmitterDependencies): BundleEmitter {
Expand Down
8 changes: 4 additions & 4 deletions source/bundle-emitter/registry-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ test('fetchLatestVersion() throws and propagates the error when npmFetch throws

test('fetchLatestVersion() throws and propagates the error when npmFetch throws with a fetch-error and the status code is not 404 nor 403', async (t) => {
const error = new Error('fetch-error');
// @ts-expect-error
// @ts-expect-error -- ok in this case
error.statusCode = 500;
const npmFetchJson = fake.rejects(error);
const registryClient = registryClientFactory({ npmFetchJson });
Expand Down Expand Up @@ -186,7 +186,7 @@ test('fetchLatestVersion() throws when npmFetch resolves with inconsistent data'

test('fetchLatestVersion() returns nothing when npmFetch throws a fetch error with status code 404', async (t) => {
const error = new Error('fetch-error');
// @ts-expect-error
// @ts-expect-error -- ok in this case
error.statusCode = 404;
const npmFetchJson = fake.rejects(error);
const registryClient = registryClientFactory({ npmFetchJson });
Expand All @@ -197,7 +197,7 @@ test('fetchLatestVersion() returns nothing when npmFetch throws a fetch error wi

test('fetchLatestVersion() returns nothing when npmFetch throws a fetch error with status code 403', async (t) => {
const error = new Error('fetch-error');
// @ts-expect-error
// @ts-expect-error -- ok in this case
error.statusCode = 403;
const npmFetchJson = fake.rejects(error);
const registryClient = registryClientFactory({ npmFetchJson });
Expand Down Expand Up @@ -239,7 +239,7 @@ test('fetchTarball() returns the buffer of the fetched tarball', async (t) => {

test('fetchTarball() throws when npmFetch throws a fetch error with status code 404', async (t) => {
const error = new Error('fetch-error');
// @ts-expect-error
// @ts-expect-error -- ok in this case
error.statusCode = 404;
const npmFetch = fake.rejects(error) as FakeNpmFetch;
const registryClient = registryClientFactory({ npmFetch });
Expand Down
7 changes: 4 additions & 3 deletions source/bundle-emitter/registry-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export type RegistryClientDependencies = {
};

export type RegistryClient = {
fetchLatestVersion(packageName: string, config: RegistrySettings): Promise<Maybe<PackageVersionDetails>>;
publishPackage(manifest: Readonly<BundlePackageJson>, tarData: Buffer, config: RegistrySettings): Promise<void>;
fetchTarball(tarballUrl: string, shasum: string): Promise<Buffer>;
fetchLatestVersion: (packageName: string, config: RegistrySettings) => Promise<Maybe<PackageVersionDetails>>;
publishPackage: (manifest: Readonly<BundlePackageJson>, tarData: Buffer, config: RegistrySettings) => Promise<void>;
fetchTarball: (tarballUrl: string, shasum: string) => Promise<Buffer>;
};

type FetchError = {
Expand Down Expand Up @@ -112,6 +112,7 @@ export function createRegistryClient(dependencies: Readonly<RegistryClientDepend
},

async publishPackage(manifest, tarData, registrySettings) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ok in this case
await publish(manifest as unknown as PublishManifest, tarData, {
defaultTag: 'latest',
forceAuth: {
Expand Down
1 change: 1 addition & 0 deletions source/command-line-interface/config-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ test('returns the value of the config property when it exists and buildConfig do
});

test('returns the value of the config property when it exists and buildConfig exists', async (t) => {
// eslint-disable-next-line @typescript-eslint/no-empty-function -- ok in this case
const importModule = fake.resolves({ config: 'the-value', buildConfig() {} });
const configLoader = configLoaderFactory({ importModule });

Expand Down
4 changes: 2 additions & 2 deletions source/command-line-interface/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { has, type ReadonlyRecord } from 'effect/ReadonlyRecord';

export type ConfigLoaderDependencies = {
readonly currentWorkingDirectory: string;
importModule(modulePath: string): Promise<unknown>;
importModule: (modulePath: string) => Promise<unknown>;
};

export type ConfigLoader = {
load(): Promise<unknown>;
load: () => Promise<unknown>;
};

type UnknownFunction = (...args: unknown[]) => unknown;
Expand Down
1 change: 1 addition & 0 deletions source/command-line-interface/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Overrides = {
};
};

// eslint-disable-next-line complexity -- needs to be refactored
function runnerFactory(overrides: Overrides = {}): CommandLineInterfaceRunner {
const {
buildAndPublishAll = fake.resolves(undefined),
Expand Down
5 changes: 3 additions & 2 deletions source/command-line-interface/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export type CommandLineInterfaceRunnerDependencies = {
readonly progressBroadcaster: ProgressBroadcastConsumer;
readonly spinnerRenderer: TerminalSpinnerRenderer;
readonly configLoader: ConfigLoader;
log(message: string): void;
log: (message: string) => void;
};

export type CommandLineInterfaceRunner = {
run(programArguments: readonly string[]): Promise<number>;
run: (programArguments: readonly string[]) => Promise<number>;
};

const errorSymbol = kleur.bold().red('✖');
Expand Down Expand Up @@ -137,6 +137,7 @@ export function createCommandLineInterfaceRunner(
});

const program = binary(baseCommand);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ok in this case
await run(program, programArguments as string[]);

return exitCode;
Expand Down
8 changes: 4 additions & 4 deletions source/command-line-interface/terminal-spinner-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { Spinner } from '@topcli/spinner';
type Status = 'failure' | 'success';

export type TerminalSpinnerRenderer = {
add(id: string, label: string, message: string): void;
updateMessage(id: string, message: string): void;
stop(id: string, status: Status, message: string): void;
stopAll(): void;
add: (id: string, label: string, message: string) => void;
updateMessage: (id: string, message: string) => void;
stop: (id: string, status: Status, message: string) => void;
stopAll: () => void;
};

export type TerminalSpinnerRendererDependencies = {
Expand Down
1 change: 1 addition & 0 deletions source/config/package-json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ test('additional attributes: validation fails when version is given', checkValid

test('additional attributes: validation fails when forbidden value is given', checkValidationFailure, {
schema: additionalPackageJsonAttributesSchema,
// eslint-disable-next-line @typescript-eslint/no-empty-function -- ok in this case
data: { foo: () => {} },
expectedMessages: [
'at foo: invalid value: expected one of string, number, boolean, null, array or record, but got function'
Expand Down
12 changes: 6 additions & 6 deletions source/dependency-scanner/dependency-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ export function mergeDependencyFiles(
type DependencyGraphVisitor = (node: Readonly<DependencyNode>) => void;

export type DependencyGraph = {
addDependency(filePath: string, data: DependencyGraphNodeData): void;
connect(fromFilePath: string, toFilePath: string): void;
hasConnection(fromFilePath: string, toFilePath: string): boolean;
walk(startFilePath: string, visitor: DependencyGraphVisitor): void;
isKnown(filePath: string): boolean;
flatten(startFilePath: string): Readonly<DependencyFiles>;
addDependency: (filePath: string, data: DependencyGraphNodeData) => void;
connect: (fromFilePath: string, toFilePath: string) => void;
hasConnection: (fromFilePath: string, toFilePath: string) => boolean;
walk: (startFilePath: string, visitor: DependencyGraphVisitor) => void;
isKnown: (filePath: string) => boolean;
flatten: (startFilePath: string) => Readonly<DependencyFiles>;
};

export function createDependencyGraph(): DependencyGraph {
Expand Down
4 changes: 2 additions & 2 deletions source/dependency-scanner/scanner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Maybe from 'true-myth/maybe';
import { Maybe } from 'true-myth/maybe';
import { uniqueList } from '../list/unique-list.js';
import type { SourceMapFileLocator } from './source-map-file-locator.js';
import type { ModuleResolution, TypescriptProjectAnalyzer, TypescriptProject } from './typescript-project-analyzer.js';
Expand Down Expand Up @@ -55,7 +55,7 @@ export type DependencyScannerDependencies = {
};

export type DependencyScanner = {
scan(entryPointFile: string, folder: string, options?: Partial<ScanOptions>): Promise<DependencyGraph>;
scan: (entryPointFile: string, folder: string, options?: Partial<ScanOptions>) => Promise<DependencyGraph>;
};

export function createDependencyScanner(
Expand Down
3 changes: 2 additions & 1 deletion source/dependency-scanner/source-file-references.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isBuiltin } from 'node:module';
import Maybe, { first } from 'true-myth/maybe';
import { Maybe, first } from 'true-myth/maybe';
import {
ts,
Node as ASTNode,
Expand All @@ -18,6 +18,7 @@ function getReferencedSourceFileFromSymbol(symbol: TSSymbol | undefined): Readon

return first(declarations).andThen((firstDeclaration) => {
if (firstDeclaration.getKind() === SyntaxKind.SourceFile) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ok in this case
return Maybe.just(firstDeclaration as SourceFile);
}

Expand Down
2 changes: 1 addition & 1 deletion source/dependency-scanner/source-map-file-locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type SourceMapFileLocatorDependencies = {
};

export type SourceMapFileLocator = {
locate(sourceFile: string): Promise<Maybe<string>>;
locate: (sourceFile: string) => Promise<Maybe<string>>;
};

const sourceMappingUrlPattern = /^\/\/# sourceMappingURL=(?<url>.+)$/m;
Expand Down
Loading