diff --git a/.changeset/wise-turtles-rescue.md b/.changeset/wise-turtles-rescue.md new file mode 100644 index 000000000..a89ef6403 --- /dev/null +++ b/.changeset/wise-turtles-rescue.md @@ -0,0 +1,6 @@ +--- +'@hypermod/fetcher': minor +'@hypermod/cli': minor +--- + +Refactors the module loader in order to support custom npm registries + auth keys. diff --git a/packages/cli/src/list.ts b/packages/cli/src/list.ts index 2b677dd8c..b8ec5aebf 100644 --- a/packages/cli/src/list.ts +++ b/packages/cli/src/list.ts @@ -5,7 +5,7 @@ import { fetchPackages } from './utils/fetch-package'; import { getHypermodPackageName } from './utils/package-names'; export default async function list(packages: string[]) { - const packageManager = new PluginManager(); + const packageManager = new PluginManager() as any; const configs = []; for (const packageName of packages) { diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 1f7efc04d..bfeb954ef 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -3,68 +3,21 @@ import semver from 'semver'; import chalk from 'chalk'; import findUp from 'find-up'; import inquirer from 'inquirer'; -import fs from 'fs-extra'; import { PluginManager, PluginManagerOptions } from 'live-plugin-manager'; -import { installPackage } from '@antfu/install-pkg'; import * as core from '@hypermod/core'; -import { fetchConfigAtPath } from '@hypermod/fetcher'; +import { + type ModuleLoader as MdlLoader, + fetchConfigAtPath, +} from '@hypermod/fetcher'; import { InvalidUserInputError } from './errors'; import { fetchPackages } from './utils/fetch-package'; import { mergeConfigs } from './utils/merge-configs'; import { fetchConfigsForWorkspaces, getPackageJson } from './utils/file-system'; +import ModuleLoader from './utils/module-loader'; import { getConfigPrompt, getMultiConfigPrompt } from './prompt'; -const ExperimentalModuleLoader = () => { - const getInfo = (packageName: string) => { - const entryPath = require.resolve(packageName); - const location = entryPath.split(packageName)[0] + packageName; - const pkgJsonRaw = fs.readFileSync( - path.join(location, 'package.json'), - 'utf8', - ); - const pkgJson = JSON.parse(pkgJsonRaw); - - return { - location, - entryPath, - pkgJson, - }; - }; - - const install = async (packageName: string) => { - await installPackage(packageName, { - cwd: __dirname, - packageManager: 'npm', - additionalArgs: ['--force'], - }); - - const { pkgJson } = getInfo(packageName); - - // Install whitelisted devDependencies - if (pkgJson?.hypermod?.dependencies) { - await Promise.all( - pkgJson.hypermod.dependencies.map((dep: string) => { - const version = pkgJson.devDependencies[dep]; - if (!version) return; - return installPackage(`${dep}@${version}`, { - cwd: __dirname, - packageManager: 'npm', - additionalArgs: ['--force'], - }); - }), - ); - } - }; - - return { - install, - getInfo, - require: (packageName: string) => require(packageName), - }; -}; - export default async function main( paths: string[], flags: Partial, @@ -91,9 +44,12 @@ export default async function main( }; } - const packageManager = flags.experimentalLoader - ? ExperimentalModuleLoader() - : new PluginManager(pluginManagerConfig); + const packageManager: MdlLoader = flags.experimentalLoader + ? ModuleLoader({ + authToken: flags.registryToken, + npmRegistryUrl: flags.registry, + }) + : (new PluginManager(pluginManagerConfig) as unknown as MdlLoader); let transforms: string[] = []; @@ -235,7 +191,6 @@ export default async function main( const { community, remote } = await fetchPackages( pkgName, - // @ts-expect-error Experimental loader packageManager, ); diff --git a/packages/cli/src/utils/fetch-package.ts b/packages/cli/src/utils/fetch-package.ts index 83ffd2ef4..0e304ba87 100644 --- a/packages/cli/src/utils/fetch-package.ts +++ b/packages/cli/src/utils/fetch-package.ts @@ -1,11 +1,11 @@ import ora from 'ora'; import chalk from 'chalk'; -import { PluginManager } from 'live-plugin-manager'; import { fetchPackage, fetchRemotePackage, ConfigMeta, + type ModuleLoader, } from '@hypermod/fetcher'; import { isValidConfig } from '@hypermod/validator'; @@ -13,7 +13,7 @@ import { getHypermodPackageName } from './package-names'; export async function fetchPackages( packageName: string, - packageManager: PluginManager, + packageManager: ModuleLoader, ) { const hypermodPackageName = getHypermodPackageName(packageName); let hypermodPackage: ConfigMeta | undefined; diff --git a/packages/cli/src/utils/module-loader.ts b/packages/cli/src/utils/module-loader.ts new file mode 100644 index 000000000..a6f08784a --- /dev/null +++ b/packages/cli/src/utils/module-loader.ts @@ -0,0 +1,66 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { installPackage } from '@antfu/install-pkg'; + +import { ModuleLoader } from '@hypermod/fetcher'; + +const ModuleLoader = (config: { + npmRegistryUrl?: string; + authToken?: string; +}): ModuleLoader => { + const getInfo = (packageName: string) => { + const entryPath = require.resolve(packageName); + const location = entryPath.split(packageName)[0] + packageName; + const pkgJsonRaw = fs.readFileSync( + path.join(location, 'package.json'), + 'utf8', + ); + const pkgJson = JSON.parse(pkgJsonRaw); + + return { + location, + entryPath, + pkgJson, + }; + }; + + const install = async (packageName: string) => { + await installPackage(packageName, { + cwd: __dirname, + packageManager: 'npm', + additionalArgs: [ + '--force', + // --registry=https://your-custom-registry-url/ --//your-custom-registry-url/:_authToken=YOUR_AUTH_TOKEN + config.npmRegistryUrl ? `--registry=${config.npmRegistryUrl}` : '', + config.authToken + ? `--${config.npmRegistryUrl}/:_authToken=${config.authToken}` + : '', + ], + }); + + const { pkgJson } = getInfo(packageName); + + // Install whitelisted devDependencies + if (pkgJson?.hypermod?.dependencies) { + await Promise.all( + pkgJson.hypermod.dependencies.map((dep: string) => { + const version = pkgJson.devDependencies[dep]; + if (!version) return; + return installPackage(`${dep}@${version}`, { + cwd: __dirname, + packageManager: 'npm', + additionalArgs: ['--force'], + }); + }), + ); + } + }; + + return { + install, + getInfo, + require: (packageName: string) => require(packageName), + }; +}; + +export default ModuleLoader; diff --git a/packages/fetcher/src/index.spec.ts b/packages/fetcher/src/index.spec.ts index 9fd72464b..ca1910325 100644 --- a/packages/fetcher/src/index.spec.ts +++ b/packages/fetcher/src/index.spec.ts @@ -3,9 +3,13 @@ jest.mock('globby'); import fs from 'fs'; import path from 'path'; import globby from 'globby'; -import { PluginManager } from 'live-plugin-manager'; -import { fetchConfig, fetchPackage, fetchRemotePackage } from '.'; +import { + fetchConfig, + fetchPackage, + fetchRemotePackage, + type ModuleLoader, +} from '.'; const mockBasePath = path.join(__dirname, 'path', 'to'); @@ -109,28 +113,23 @@ describe('fetcher', () => { require: jest.fn().mockReturnValue(mockConfig), }; - const configMeta = await fetchPackage( - 'fake-package', - mockPackageManager as unknown as PluginManager, - ); + const configMeta = await fetchPackage('fake-package', mockPackageManager); expect(configMeta!.config).toEqual(mockConfig); expect(configMeta!.filePath).toEqual(mockFilePath); }); it('should throw if fetching fails', async () => { - const mockPackageManager = { + const mockPackageManager: ModuleLoader = { install: jest.fn().mockRejectedValue('Import error'), require: jest.fn().mockReturnValue(mockConfig), + getInfo: jest.fn(), }; expect.assertions(1); await expect( - fetchPackage( - 'fake-package', - mockPackageManager as unknown as PluginManager, - ), + fetchPackage('fake-package', mockPackageManager), ).rejects.toEqual('Import error'); }); }); @@ -150,7 +149,7 @@ describe('fetcher', () => { const configMeta = await fetchRemotePackage( 'fake-package', - mockPackageManager as unknown as PluginManager, + mockPackageManager, ); expect(configMeta!.config).toEqual(mockConfig); @@ -160,17 +159,16 @@ describe('fetcher', () => { }); it('should throw if fetching fails', async () => { - const mockPackageManager = { + const mockPackageManager: ModuleLoader = { install: jest.fn().mockRejectedValue('Import error'), + getInfo: jest.fn(), + require: jest.fn(), }; expect.assertions(1); await expect( - fetchRemotePackage( - 'fake-package', - mockPackageManager as unknown as PluginManager, - ), + fetchRemotePackage('fake-package', mockPackageManager), ).rejects.toEqual('Import error'); }); @@ -185,7 +183,7 @@ describe('fetcher', () => { const configMeta = await fetchRemotePackage( 'fake-package', - mockPackageManager as unknown as PluginManager, + mockPackageManager, ); expect(configMeta!.config).toEqual(mockConfig); @@ -205,42 +203,38 @@ describe('fetcher', () => { Promise.resolve([]), ); - const res = await fetchRemotePackage( - 'fake-package', - mockPackageManager as unknown as PluginManager, - ); + const res = await fetchRemotePackage('fake-package', mockPackageManager); expect(res).toBeUndefined(); }); it('should throw if fetching fails', async () => { - const mockPackageManager = { + const mockPackageManager: ModuleLoader = { install: jest.fn().mockRejectedValue('Import error'), + getInfo: jest.fn(), + require: jest.fn(), }; expect.assertions(1); await expect( - fetchRemotePackage( - 'fake-package', - mockPackageManager as unknown as PluginManager, - ), + fetchRemotePackage('fake-package', mockPackageManager), ).rejects.toEqual('Import error'); }); it('should throw if package source cannot be retrieved', async () => { - const mockPackageManager = { + const mockPackageManager: ModuleLoader = { install: jest.fn(), - getInfo: () => undefined, + getInfo: () => { + throw new Error('Package not found'); + }, + require: jest.fn(), }; expect.assertions(1); await expect( - fetchRemotePackage( - 'fake-package', - mockPackageManager as unknown as PluginManager, - ), + fetchRemotePackage('fake-package', mockPackageManager), ).rejects.toEqual( new Error(`Unable to locate package files for package: 'fake-package'`), ); diff --git a/packages/fetcher/src/index.ts b/packages/fetcher/src/index.ts index 11774d82a..2df16fd11 100644 --- a/packages/fetcher/src/index.ts +++ b/packages/fetcher/src/index.ts @@ -2,10 +2,19 @@ import fs from 'fs'; import path from 'path'; import globby from 'globby'; -import { PluginManager } from 'live-plugin-manager'; import { Config } from '@hypermod/types'; +export interface ModuleLoader { + install: (packageName: string) => Promise; + getInfo: (packageName: string) => { + location: string; + entryPath: string; + pkgJson: any; + }; + require: (packageName: string) => any; +} + // This configuration allows us to require TypeScript config files directly const { DEFAULT_EXTENSIONS } = require('@babel/core'); const presets = []; @@ -109,7 +118,7 @@ export async function fetchConfigAtPath(filePath: string): Promise { export async function fetchPackage( packageName: string, - packageManager: PluginManager, + packageManager: ModuleLoader, ): Promise { await packageManager.install(packageName); const pkg = packageManager.require(packageName); @@ -127,16 +136,23 @@ export async function fetchPackage( export async function fetchRemotePackage( packageName: string, - packageManager: PluginManager, + packageManager: ModuleLoader, ): Promise { if (['javascript', 'typescript'].includes(packageName)) { throw new Error(`'${packageName}' is ignored as a remote package.`); } await packageManager.install(packageName); - const info = packageManager.getInfo(packageName); - if (!info) { + let info; + + try { + info = packageManager.getInfo(packageName); + + if (!info) { + throw new Error(); + } + } catch (error) { throw new Error( `Unable to locate package files for package: '${packageName}'`, );