diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5f8f095..4e72d99fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Removed pilet-related options in debug settings when running `piral debug` (#670) - Improved internal navigation Blazor pilets using `piral-blazor` - Updated dependencies +- Updated `piral publish` command to work exclusively for emulator websites - Added special entry point to emulator website when accessed online (#654) - Added feed selection view for remote emulator website (#654) diff --git a/src/tooling/piral-cli/src/api.ts b/src/tooling/piral-cli/src/api.ts index ed1da59fa..f37587c46 100644 --- a/src/tooling/piral-cli/src/api.ts +++ b/src/tooling/piral-cli/src/api.ts @@ -1,7 +1,6 @@ import { log, installPatch } from './common'; import { commands } from './commands'; import { setBundler } from './bundler'; -import { setReleaseProvider } from './release'; import { addPiletRule, addPiralRule } from './rules'; import { ToolCommand, @@ -13,7 +12,6 @@ import { PiletRuleContext, PackagePatcher, BundlerDefinition, - ReleaseProvider, } from './types'; function findAll(commandName: string, cb: (command: ToolCommand, index: number) => void) { @@ -157,18 +155,3 @@ export function withBundler(name: string, actions: BundlerDefinition) { return this; } - -export function withReleaseProvider(name: string, action: ReleaseProvider) { - if (typeof name !== 'string') { - log('apiReleaseProviderInvalid_0207', 'providerName'); - } else if (typeof action !== 'object') { - log('apiReleaseProviderInvalid_0207', 'provider'); - } else { - setReleaseProvider({ - name, - action, - }); - } - - return this; -} diff --git a/src/tooling/piral-cli/src/apps/build-pilet.ts b/src/tooling/piral-cli/src/apps/build-pilet.ts index 45d335c58..9ea2f54b1 100644 --- a/src/tooling/piral-cli/src/apps/build-pilet.ts +++ b/src/tooling/piral-cli/src/apps/build-pilet.ts @@ -27,6 +27,7 @@ import { validateSharedDependencies, defaultSchemaVersion, flattenExternals, + triggerBuildPilet, } from '../common'; interface PiletData { @@ -217,66 +218,23 @@ export async function buildPilet(baseDir = process.cwd(), options: BuildPiletOpt } const pilets = await concurrentWorkers(allEntries, concurrency, async (entryModule) => { - const targetDir = dirname(entryModule); - const { peerDependencies, peerModules, root, apps, piletPackage, ignored, importmap, schema } = - await retrievePiletData(targetDir, app); - const schemaVersion = originalSchemaVersion || schema || config.schemaVersion || defaultSchemaVersion; - const piralInstances = apps.map((m) => m.appPackage.name); - const externals = combinePiletExternals(piralInstances, peerDependencies, peerModules, importmap); - const dest = resolve(root, target); - const outDir = dirname(dest); - const outFile = basename(dest); - - validateSharedDependencies(importmap); - - if (fresh) { - progress('Removing output directory ...'); - await removeDirectory(outDir); - } - - logInfo('Bundle pilet ...'); - - await hooks.beforeBuild?.({ root, outDir, importmap, entryModule, schemaVersion, piletPackage }); - - await callPiletBuild( - { - root, - piralInstances, - optimizeModules, - sourceMaps, - watch, - contentHash, - minify, - externals, - targetDir, - importmap, - outFile, - outDir, - entryModule: `./${relative(root, entryModule)}`, - logLevel, - version: schemaVersion, - ignored, - _, - }, + const { piletPackage, root, outDir, apps, outFile, dest } = await triggerBuildPilet({ + _, + app, bundlerName, - ); - - await hooks.afterBuild?.({ root, outDir, importmap, entryModule, schemaVersion, piletPackage }); - - if (declaration) { - await hooks.beforeDeclaration?.({ root, outDir, entryModule, piletPackage }); - await createPiletDeclaration( - piletPackage.name, - piralInstances, - root, - entryModule, - externals, - outDir, - ForceOverwrite.yes, - logLevel, - ); - await hooks.afterDeclaration?.({ root, outDir, entryModule, piletPackage }); - } + contentHash, + entryModule, + fresh, + logLevel, + minify, + optimizeModules, + originalSchemaVersion, + sourceMaps, + target, + watch, + hooks, + declaration, + }); logDone(`Pilet "${piletPackage.name}" built successfully!`); diff --git a/src/tooling/piral-cli/src/apps/build-piral.ts b/src/tooling/piral-cli/src/apps/build-piral.ts index 060793103..bf7fb7346 100644 --- a/src/tooling/piral-cli/src/apps/build-piral.ts +++ b/src/tooling/piral-cli/src/apps/build-piral.ts @@ -1,34 +1,26 @@ import { join, resolve } from 'path'; -import { callPiralBuild } from '../bundler'; import { LogLevels, PiralBuildType } from '../types'; import { retrievePiletsInfo, retrievePiralRoot, removeDirectory, - logDone, checkCliCompatibility, progress, setLogLevel, logReset, - createEmulatorSources, - log, - logInfo, - runScript, - packageEmulator, normalizePublicUrl, getDestination, validateSharedDependencies, - flattenExternals, - createEmulatorWebsite, + allName, + emulatorPackageName, + emulatorName, + emulatorWebsiteName, + emulatorSourcesName, + releaseName, + triggerBuildEmulator, + triggerBuildShell, } from '../common'; -const allName = 'all'; -const releaseName = 'release'; -const emulatorName = 'emulator'; -const emulatorPackageName = 'package'; -const emulatorSourcesName = 'sources'; -const emulatorWebsiteName = 'website'; - export interface BuildPiralOptions { /** * The location of the piral @@ -130,18 +122,6 @@ export const buildPiralDefaults: BuildPiralOptions = { optimizeModules: false, }; -async function runLifecycle(root: string, scripts: Record, type: string) { - const script = scripts?.[type]; - - if (script) { - log('generalDebug_0003', `Running "${type}" ("${script}") ...`); - await runScript(script, root); - log('generalDebug_0003', `Finished running "${type}".`); - } else { - log('generalDebug_0003', `No script for "${type}" found ...`); - } -} - export async function buildPiral(baseDir = process.cwd(), options: BuildPiralOptions = {}) { const { entry = buildPiralDefaults.entry, @@ -193,139 +173,56 @@ export async function buildPiral(baseDir = process.cwd(), options: BuildPiralOpt // only applies to an explicit emulator target (e.g., "emulator-website") or to "all" / "emulator" with the setting from the piral.json if ([emulatorSourcesName, emulatorPackageName, emulatorWebsiteName].includes(emulatorType)) { - const emulatorPublicUrl = '/'; const targetDir = useSubdir ? join(dest.outDir, emulatorName) : dest.outDir; - const appDir = emulatorType !== emulatorWebsiteName ? join(targetDir, 'app') : targetDir; - progress('Starting emulator build ...'); - - // since we create this anyway let's just pretend we want to have it clean! - await removeDirectory(targetDir); - - await hooks.beforeBuild?.({ root, publicUrl: emulatorPublicUrl, externals, entryFiles, targetDir, piralInstances }); - logInfo(`Bundle ${emulatorName} ...`); - const { - dir: outDir, - name: outFile, - hash, - } = await callPiralBuild( - { - root, - piralInstances, - emulator: true, - standalone: false, - optimizeModules, - sourceMaps, - watch, - contentHash, - minify: false, - externals: flattenExternals(externals), - publicUrl: emulatorPublicUrl, - entryFiles, - logLevel, - ignored, - outDir: appDir, - outFile: dest.outFile, - _, - }, - bundlerName, - ); - - await hooks.afterBuild?.({ + await triggerBuildEmulator({ root, - publicUrl: emulatorPublicUrl, + logLevel, + bundlerName, + emulatorType, + hooks, + targetDir, + ignored, externals, entryFiles, - targetDir, piralInstances, - hash, - outDir, - outFile, + optimizeModules, + sourceMaps, + watch, + scripts, + contentHash, + outFile: dest.outFile, + _, }); - await runLifecycle(root, scripts, 'piral:postbuild'); - await runLifecycle(root, scripts, `piral:postbuild-${emulatorName}`); - - await hooks.beforeEmulator?.({ root, externals, targetDir, outDir }); - - let rootDir = root; - - switch (emulatorType) { - case emulatorPackageName: - rootDir = await createEmulatorSources(root, externals, outDir, outFile, logLevel); - await hooks.beforePackage?.({ root, externals, targetDir, outDir, rootDir }); - await packageEmulator(rootDir); - await hooks.afterPackage?.({ root, externals, targetDir, outDir, rootDir }); - break; - case emulatorSourcesName: - rootDir = await createEmulatorSources(root, externals, outDir, outFile, logLevel); - logDone(`Emulator package sources available in "${rootDir}".`); - break; - case emulatorWebsiteName: - rootDir = await createEmulatorWebsite(root, externals, outDir, outFile, logLevel); - logDone(`Emulator website available in "${rootDir}".`); - break; - } - - await hooks.afterEmulator?.({ root, externals, targetDir, outDir, rootDir }); logReset(); } // either 'release' or 'all' if (type === releaseName || type === allName) { const targetDir = useSubdir ? join(dest.outDir, releaseName) : dest.outDir; - progress('Starting release build ...'); - - // since we create this anyway let's just pretend we want to have it clean! - await removeDirectory(targetDir); - - logInfo(`Bundle ${releaseName} ...`); - await hooks.beforeBuild?.({ root, publicUrl, externals, entryFiles, targetDir, piralInstances }); - - const { - dir: outDir, - name: outFile, - hash, - } = await callPiralBuild( - { - root, - piralInstances, - emulator: false, - standalone: false, - optimizeModules, - sourceMaps, - watch, - contentHash, - minify, - externals: flattenExternals(externals), - publicUrl, - outFile: dest.outFile, - outDir: targetDir, - entryFiles, - logLevel, - ignored, - _, - }, + await triggerBuildShell({ + targetDir, + logLevel, bundlerName, - ); - - await hooks.afterBuild?.({ - root, - publicUrl, + contentHash, externals, + ignored, + minify, + optimizeModules, + publicUrl, + outFile: dest.outFile, + root, + sourceMaps, + watch, + hooks, entryFiles, - targetDir, piralInstances, - outDir, - outFile, - hash, + scripts, + _, }); - await runLifecycle(root, scripts, 'piral:postbuild'); - await runLifecycle(root, scripts, `piral:postbuild-${releaseName}`); - - logDone(`Files for publication available in "${outDir}".`); logReset(); } diff --git a/src/tooling/piral-cli/src/apps/publish-pilet.ts b/src/tooling/piral-cli/src/apps/publish-pilet.ts index fb068a657..886d2d8dd 100644 --- a/src/tooling/piral-cli/src/apps/publish-pilet.ts +++ b/src/tooling/piral-cli/src/apps/publish-pilet.ts @@ -1,6 +1,5 @@ -import { relative, dirname, basename, resolve, isAbsolute } from 'path'; -import { callPiletBuild } from '../bundler'; -import { LogLevels, PiletSchemaVersion, PiletPublishSource, PiletPublishScheme } from '../types'; +import { relative, dirname, basename, resolve } from 'path'; +import { LogLevels, PiletSchemaVersion, PiletPublishSource, PublishScheme } from '../types'; import { postFile, readBinary, @@ -16,11 +15,7 @@ import { findNpmTarball, downloadFile, matchAnyPilet, - retrievePiletData, - removeDirectory, - logInfo, - combinePiletExternals, - defaultSchemaVersion, + triggerBuildPilet, } from '../common'; export interface PublishPiletOptions { @@ -92,12 +87,22 @@ export interface PublishPiletOptions { /** * Sets the authorization scheme to use. */ - mode?: PiletPublishScheme; + mode?: PublishScheme; /** * Additional arguments for a specific bundler. */ _?: Record; + + /** + * Hooks to be triggered at various stages. + */ + hooks?: { + beforeBuild?(e: any): Promise; + afterBuild?(e: any): Promise; + beforeDeclaration?(e: any): Promise; + afterDeclaration?(e: any): Promise; + }; } export const publishPiletDefaults: PublishPiletOptions = { @@ -114,11 +119,6 @@ export const publishPiletDefaults: PublishPiletOptions = { interactive: false, }; -function isSubDir(parent: string, dir: string) { - const rel = relative(parent, dir); - return rel && !rel.startsWith('..') && !isAbsolute(rel); -} - async function getFiles( baseDir: string, sources: Array, @@ -129,6 +129,7 @@ async function getFiles( bundlerName: string, _?: Record, ca?: Buffer, + hooks?: PublishPiletOptions['hooks'], ): Promise> { if (fresh) { log('generalDebug_0003', 'Detected "--fresh". Trying to resolve the package.json.'); @@ -140,54 +141,25 @@ async function getFiles( return await Promise.all( allEntries.map(async (entryModule) => { - const targetDir = dirname(entryModule); - progress('Triggering pilet build ...'); - const { root, piletPackage, importmap, peerDependencies, peerModules, apps, schema } = await retrievePiletData( - targetDir, - ); - const schemaVersion = originalSchemaVersion || schema || config.schemaVersion || defaultSchemaVersion; - const piralInstances = apps.map((m) => m.appPackage.name); - const defaultOutput = 'dist/index.js'; - const { main = defaultOutput, name = 'pilet' } = piletPackage; - const propDest = resolve(root, main); - const propDestDir = dirname(propDest); - log('generalDebug_0003', `Pilet "${name}" is supposed to generate artifact in "${propDest}".`); - const usePropDest = propDestDir !== root && propDestDir !== targetDir && isSubDir(root, propDest); - const dest = usePropDest ? propDest : resolve(root, defaultOutput); - log('generalDebug_0003', `Pilet "${name}" is generating artifact in "${dest}".`); - const outDir = dirname(dest); - const outFile = basename(dest); - const externals = combinePiletExternals(piralInstances, peerDependencies, peerModules, importmap); - log('generalDebug_0003', `Pilet "${name}" uses externals: ${externals.join(', ')}.`); - - progress('Removing output directory ...'); - await removeDirectory(outDir); - - logInfo('Bundle pilet ...'); - await callPiletBuild( - { - root, - piralInstances, - optimizeModules: false, - sourceMaps: true, - watch: false, - contentHash: true, - minify: true, - externals, - targetDir, - importmap, - outFile, - outDir, - entryModule: `./${relative(root, entryModule)}`, - logLevel, - version: schemaVersion, - ignored: [], - _, - }, - bundlerName, - ); + const { root, piletPackage } = await triggerBuildPilet({ + _, + bundlerName, + entryModule, + fresh, + logLevel, + originalSchemaVersion, + watch: false, + optimizeModules: false, + sourceMaps: true, + declaration: true, + contentHash: true, + minify: true, + hooks, + }); + + const name = piletPackage.name; log('generalDebug_0003', `Pilet "${name}" built successfully!`); progress('Triggering pilet pack ...'); @@ -237,6 +209,7 @@ export async function publishPilet(baseDir = process.cwd(), options: PublishPile mode = publishPiletDefaults.mode, interactive = publishPiletDefaults.interactive, _ = {}, + hooks = {}, bundlerName, } = options; const fullBase = resolve(process.cwd(), baseDir); @@ -259,7 +232,7 @@ export async function publishPilet(baseDir = process.cwd(), options: PublishPile log('generalDebug_0003', 'Getting the tgz files ...'); const sources = Array.isArray(source) ? source : [source]; - const files = await getFiles(fullBase, sources, from, fresh, schemaVersion, logLevel, bundlerName, _, ca); + const files = await getFiles(fullBase, sources, from, fresh, schemaVersion, logLevel, bundlerName, _, ca, hooks); const successfulUploads: Array = []; log('generalDebug_0003', 'Received available tgz files.'); @@ -275,7 +248,7 @@ export async function publishPilet(baseDir = process.cwd(), options: PublishPile const content = await readBinary(fullBase, fileName); if (content) { - progress(`Publishing "%s" ...`, file, url); + progress(`Publishing "%s" to "%s" ...`, file, url); const result = await postFile(url, mode, apiKey, content, fields, headers, ca, interactive); if (result.success) { diff --git a/src/tooling/piral-cli/src/apps/publish-piral.ts b/src/tooling/piral-cli/src/apps/publish-piral.ts index 9bcc939af..0d17c42b6 100644 --- a/src/tooling/piral-cli/src/apps/publish-piral.ts +++ b/src/tooling/piral-cli/src/apps/publish-piral.ts @@ -1,16 +1,26 @@ -import { resolve } from 'path'; -import { publishArtifacts } from '../release'; -import { LogLevels, PiralBuildType } from '../types'; +import { basename, dirname, resolve } from 'path'; +import { LogLevels, PublishScheme } from '../types'; import { setLogLevel, progress, checkExists, fail, logDone, - logReset, - publishNpmPackage, - matchFiles, log, + config, + readBinary, + emulatorName, + emulatorJson, + publishWebsiteEmulator, + matchFiles, + readJson, + triggerBuildEmulator, + logReset, + emulatorWebsiteName, + retrievePiralRoot, + emulatorPackageName, + retrievePiletsInfo, + validateSharedDependencies, } from '../common'; export interface PublishPiralOptions { @@ -19,15 +29,36 @@ export interface PublishPiralOptions { */ source?: string; + /** + * Sets the URL of the feed service to deploy to. + */ + url?: string; + + /** + * Sets the API key to use. + */ + apiKey?: string; + /** * Sets the log level to use (1-5). */ logLevel?: LogLevels; /** - * The options to supply for the provider. + * Specifies if the Piral instance should be built before publishing. + * If yes, then the tarball is created from fresh build artifacts. + */ + fresh?: boolean; + + /** + * Defines a custom certificate for the feed service. */ - opts?: Record; + cert?: string; + + /** + * Places additional headers that should be posted to the feed service. + */ + headers?: Record; /** * Defines if authorization tokens can be retrieved interactively. @@ -35,104 +66,143 @@ export interface PublishPiralOptions { interactive?: boolean; /** - * The provider to use for publishing the release artifacts. + * Sets the bundler to use for building, if any specific. */ - provider?: string; + bundlerName?: string; /** - * The type of publish. + * Sets the authorization scheme to use. */ - type?: PiralBuildType; + mode?: PublishScheme; + + /** + * Additional arguments for a specific bundler. + */ + _?: Record; + + /** + * Hooks to be triggered at various stages. + */ + hooks?: { + beforeEmulator?(e: any): Promise; + afterEmulator?(e: any): Promise; + beforePackage?(e: any): Promise; + afterPackage?(e: any): Promise; + }; } export const publishPiralDefaults: PublishPiralOptions = { source: './dist', logLevel: LogLevels.info, - type: 'all', - provider: 'none', - opts: {}, + url: undefined, interactive: false, + apiKey: undefined, + fresh: false, + cert: undefined, + mode: 'basic', + headers: {}, }; -async function publishEmulator( - baseDir: string, - source: string, - args: Record = {}, - interactive = false, -) { - const type = 'emulator'; - const directory = resolve(baseDir, source, type); - const exists = await checkExists(directory); - - if (!exists) { - fail('publishDirectoryMissing_0110', directory); - } - - const files = await matchFiles(directory, '*.tgz'); - log('generalDebug_0003', `Found ${files.length} in "${directory}": ${files.join(', ')}`); - - if (files.length !== 1) { - fail('publishEmulatorFilesUnexpected_0111', directory); - } - - const [file] = files; - const flags = Object.keys(args).reduce((p, c) => { - p.push(`--${c}`, args[c]); - return p; - }, [] as Array); - - await publishNpmPackage(directory, file, flags, interactive); -} - -async function publishRelease( - baseDir: string, - source: string, - providerName: string, - args: Record = {}, - interactive = false, -) { - const type = 'release'; - const directory = resolve(baseDir, source, type); - const exists = await checkExists(directory); - - if (!exists) { - fail('publishDirectoryMissing_0110', directory); - } - - const files = await matchFiles(directory, '**/*'); - log('generalDebug_0003', `Found ${files.length} in "${directory}": ${files.join(', ')}`); - await publishArtifacts(providerName, directory, files, args, interactive); -} - export async function publishPiral(baseDir = process.cwd(), options: PublishPiralOptions = {}) { const { source = publishPiralDefaults.source, - type = publishPiralDefaults.type, logLevel = publishPiralDefaults.logLevel, - opts = publishPiralDefaults.opts, - provider = publishPiralDefaults.provider, interactive = publishPiralDefaults.interactive, + fresh = publishPiralDefaults.fresh, + url = config.url ?? publishPiralDefaults.url, + apiKey = config.apiKeys?.[url] ?? config.apiKey ?? publishPiralDefaults.apiKey, + cert = config.cert ?? publishPiralDefaults.cert, + headers = publishPiralDefaults.headers, + mode = publishPiralDefaults.mode, + _ = {}, + hooks = {}, + bundlerName, } = options; const fullBase = resolve(process.cwd(), baseDir); setLogLevel(logLevel); + progress('Reading configuration ...'); - if (type === 'emulator-sources') { - fail('publishEmulatorSourcesInvalid_0114'); + if (!url) { + fail('missingPiletFeedUrl_0060'); } - progress('Reading configuration ...'); + log('generalDebug_0003', 'Checking if certificate exists.'); + let ca: Buffer = undefined; - if (type !== 'release') { - progress('Publishing emulator package ...'); - await publishEmulator(fullBase, source, opts, interactive); - logDone(`Successfully published emulator.`); - logReset(); + if (await checkExists(cert)) { + const dir = dirname(cert); + const file = basename(cert); + log('generalDebug_0003', `Reading certificate file "${file}" from "${dir}".`); + ca = await readBinary(dir, file); + } + + log('generalDebug_0003', 'Getting the files ...'); + const entryFiles = await retrievePiralRoot(fullBase, './'); + const { + name, + root, + ignored, + externals, + scripts, + emulator = emulatorPackageName, + } = await retrievePiletsInfo(entryFiles); + + if (emulator !== emulatorWebsiteName) { + fail('generalError_0002', `Currently only the "${emulatorWebsiteName}" option is supported.`); } + + const emulatorDir = resolve(fullBase, source, emulatorName); + + if (fresh) { + const piralInstances = [name]; + + validateSharedDependencies(externals); + + await triggerBuildEmulator({ + root, + logLevel, + bundlerName, + emulatorType: emulatorWebsiteName, + hooks, + targetDir: emulatorDir, + ignored, + externals, + entryFiles, + piralInstances, + optimizeModules: true, + sourceMaps: true, + watch: false, + scripts, + contentHash: true, + outFile: 'index.html', + _, + }); - if (type !== 'emulator') { - progress('Publishing release files ...'); - await publishRelease(fullBase, source, provider, opts, interactive); - logDone(`Successfully published release.`); logReset(); } + + const { version } = await readJson(emulatorDir, emulatorJson); + + if (!version) { + fail('missingEmulatorWebsite_0130', emulatorDir); + } + + log('generalInfo_0000', `Using feed service "${url}".`); + + const files = await matchFiles(emulatorDir, '**/*'); + + progress(`Publishing emulator to "%s" ...`, url); + const result = await publishWebsiteEmulator(version, url, apiKey, mode, emulatorDir, files, interactive, headers, ca); + + if (!result.success) { + fail('failedUploading_0064'); + } + + if (result.response) { + log('httpPostResponse_0067', result); + } + + progress(`Published successfully!`); + + logDone(`Emulator published successfully!`); } diff --git a/src/tooling/piral-cli/src/commands.ts b/src/tooling/piral-cli/src/commands.ts index 7f8df828e..eed43a346 100644 --- a/src/tooling/piral-cli/src/commands.ts +++ b/src/tooling/piral-cli/src/commands.ts @@ -24,7 +24,7 @@ import { PiletPublishSource, PiletSchemaVersion, PiletBuildType, - PiletPublishScheme, + PublishScheme, SourceLanguage, } from './types'; @@ -196,25 +196,39 @@ const allCommands: Array> = [ alias: ['release-piral', 'release'], description: 'Publishes Piral instance build artifacts.', arguments: ['[source]'], - flags(argv) { + // "any" due to https://github.com/microsoft/TypeScript/issues/28663 [artifical N = 50] + flags(argv: any) { return argv .positional('source', { type: 'string', describe: 'Sets the previously used output directory to publish.', default: apps.publishPiralDefaults.source, }) + .string('url') + .describe('url', 'Sets the explicit URL where to publish the emulator to.') + .default('url', apps.publishPiralDefaults.url) + .string('api-key') + .describe('api-key', 'Sets the potential API key to send to the service.') + .default('api-key', apps.publishPiralDefaults.apiKey) + .string('ca-cert') + .describe('ca-cert', 'Sets a custom certificate authority to use, if any.') + .default('ca-cert', apps.publishPiralDefaults.cert) .number('log-level') .describe('log-level', 'Sets the log level to use (1-5).') .default('log-level', apps.publishPiralDefaults.logLevel) - .choices('type', piralBuildTypeKeys) - .describe('type', 'Selects the target type to publish. "all" publishes all target types.') - .default('type', apps.publishPiralDefaults.type) - .choices('provider', availableReleaseProviders) - .describe('provider', 'Sets the provider for publishing the release assets.') - .default('provider', apps.publishPiralDefaults.provider) - .option('opts', undefined) - .describe('opts', 'Sets the options to forward to the chosen provider.') - .default('opts', apps.publishPiralDefaults.opts) + .boolean('fresh') + .describe('fresh', 'Performs a fresh build of the emulator website.') + .default('fresh', apps.publishPiralDefaults.fresh) + .choices('mode', publishModeKeys) + .describe('mode', 'Sets the authorization mode to use.') + .default('mode', apps.publishPiralDefaults.mode) + .alias('mode', 'auth-mode') + .choices('bundler', availableBundlers) + .describe('bundler', 'Sets the bundler to use.') + .default('bundler', availableBundlers[0]) + .option('headers', undefined) + .describe('headers', 'Sets additional headers to be included in the feed service request.') + .default('headers', apps.publishPiralDefaults.headers) .boolean('interactive') .describe('interactive', 'Defines if authorization tokens can be retrieved interactively.') .default('interactive', apps.publishPiralDefaults.interactive) @@ -226,10 +240,15 @@ const allCommands: Array> = [ return apps.publishPiral(args.base as string, { source: args.source as string, logLevel: args['log-level'] as LogLevels, - type: args.type as PiralBuildType, - provider: args.provider as string, - opts: args.opts as Record, + apiKey: args['api-key'] as string, + cert: args['ca-cert'] as string, + url: args.url as string, interactive: args.interactive as boolean, + mode: args.mode as PublishScheme, + bundlerName: args.bundler as string, + fresh: args.fresh as boolean, + headers: args.headers as Record, + _: args, }); }, }, @@ -671,7 +690,7 @@ const allCommands: Array> = [ schemaVersion: args.schema as PiletSchemaVersion, fields: args.fields as Record, headers: args.headers as Record, - mode: args.mode as PiletPublishScheme, + mode: args.mode as PublishScheme, interactive: args.interactive as boolean, _: args, }); diff --git a/src/tooling/piral-cli/src/common/constants.ts b/src/tooling/piral-cli/src/common/constants.ts index c16619814..f3125768f 100644 --- a/src/tooling/piral-cli/src/common/constants.ts +++ b/src/tooling/piral-cli/src/common/constants.ts @@ -6,6 +6,13 @@ export const piletJson = 'pilet.json'; export const filesOnceTar = 'files_once'; export const piralBaseRoot = 'piral-base/package.json'; export const defaultSchemaVersion = 'v2'; +export const allName = 'all'; +export const releaseName = 'release'; +export const emulatorJson = 'emulator.json'; +export const emulatorName = 'emulator'; +export const emulatorPackageName = 'package'; +export const emulatorSourcesName = 'sources'; +export const emulatorWebsiteName = 'website'; export const frameworkLibs = ['piral' as const, 'piral-core' as const, 'piral-base' as const]; export const piletJsonSchemaUrl = 'https://docs.piral.io/schemas/pilet-v0.json'; export const piralJsonSchemaUrl = 'https://docs.piral.io/schemas/piral-v0.json'; diff --git a/src/tooling/piral-cli/src/common/emulator.ts b/src/tooling/piral-cli/src/common/emulator.ts index 9799addea..520e3c1bd 100644 --- a/src/tooling/piral-cli/src/common/emulator.ts +++ b/src/tooling/piral-cli/src/common/emulator.ts @@ -1,7 +1,7 @@ import { join, resolve, relative, basename } from 'path'; import { findDependencyVersion, copyScaffoldingFiles, isValidDependency, flattenExternals } from './package'; import { createPiralStubIndexIfNotExists } from './template'; -import { filesTar, filesOnceTar, packageJson, piralJson } from './constants'; +import { filesTar, filesOnceTar, packageJson, piralJson, emulatorJson } from './constants'; import { cliVersion } from './info'; import { createNpmPackage } from './npm'; import { createPiralDeclaration } from './declaration'; @@ -238,7 +238,7 @@ export async function createEmulatorWebsite( }, }; - await writeJson(targetDir, 'emulator.json', data, true); + await writeJson(targetDir, emulatorJson, data, true); // generate the associated index.d.ts await createPiralDeclaration(sourceDir, piralPkg.app ?? `./src/index.html`, targetDir, ForceOverwrite.yes, logLevel); diff --git a/src/tooling/piral-cli/src/common/http.ts b/src/tooling/piral-cli/src/common/http.ts index 6cff16335..34273f68a 100644 --- a/src/tooling/piral-cli/src/common/http.ts +++ b/src/tooling/piral-cli/src/common/http.ts @@ -7,7 +7,7 @@ import { log } from './log'; import { standardHeaders } from './info'; import { getTokenInteractively } from './interactive'; import { axios, FormData } from '../external'; -import { PiletPublishScheme } from '../types'; +import { PublishScheme } from '../types'; function getMessage(body: string | { message?: string }) { if (typeof body === 'string') { @@ -67,7 +67,7 @@ export type FormDataObj = Record = {}, @@ -192,7 +192,7 @@ export function postForm( export function postFile( target: string, - scheme: PiletPublishScheme, + scheme: PublishScheme, key: string, file: Buffer, customFields: Record = {}, diff --git a/src/tooling/piral-cli/src/common/index.ts b/src/tooling/piral-cli/src/common/index.ts index e103b5c91..5ed9c2e87 100644 --- a/src/tooling/piral-cli/src/common/index.ts +++ b/src/tooling/piral-cli/src/common/index.ts @@ -25,8 +25,11 @@ export * from './package'; export * from './parallel'; export * from './patcher'; export * from './patches'; +export * from './pilet'; +export * from './piral'; export * from './platform'; export * from './port'; +export * from './release'; export * from './rules'; export * from './scaffold'; export * from './scripts'; diff --git a/src/tooling/piral-cli/src/common/interactive.ts b/src/tooling/piral-cli/src/common/interactive.ts index 5f7071bcb..bf2413251 100644 --- a/src/tooling/piral-cli/src/common/interactive.ts +++ b/src/tooling/piral-cli/src/common/interactive.ts @@ -3,7 +3,7 @@ import { openBrowserAt } from './browser'; import { standardHeaders } from './info'; import { logSuspend, logInfo } from './log'; import { axios, inquirer } from '../external'; -import { PiletPublishScheme } from '../types'; +import { PublishScheme } from '../types'; export function promptSelect(message: string, values: Array, defaultValue: string): Promise { const questions = [ @@ -30,7 +30,7 @@ export function promptConfirm(message: string, defaultValue: boolean): Promise answers.q); } -type TokenResult = Promise<{ mode: PiletPublishScheme; token: string }>; +type TokenResult = Promise<{ mode: PublishScheme; token: string }>; const tokenRetrievers: Record = {}; diff --git a/src/tooling/piral-cli/src/common/pilet.ts b/src/tooling/piral-cli/src/common/pilet.ts new file mode 100644 index 000000000..07f804236 --- /dev/null +++ b/src/tooling/piral-cli/src/common/pilet.ts @@ -0,0 +1,140 @@ +import { basename, dirname, isAbsolute, relative, resolve } from 'path'; +import { config } from './config'; +import { removeDirectory } from './io'; +import { ForceOverwrite } from './enums'; +import { logInfo, progress } from './log'; +import { callPiletBuild } from '../bundler'; +import { defaultSchemaVersion } from './constants'; +import { createPiletDeclaration } from './declaration'; +import { combinePiletExternals, retrievePiletData, validateSharedDependencies } from './package'; +import { LogLevels, PiletSchemaVersion } from '../types'; + +const defaultOutput = 'dist/index.js'; + +function isSubDir(parent: string, dir: string) { + const rel = relative(parent, dir); + return rel && !rel.startsWith('..') && !isAbsolute(rel); +} + +function getTarget(root: string, main: string, source: string, target?: string) { + if (typeof target === 'undefined') { + const propDest = resolve(root, main); + const propDestDir = dirname(propDest); + const usePropDest = propDestDir !== root && propDestDir !== source && isSubDir(root, propDest); + return usePropDest ? propDest : resolve(root, defaultOutput); + } + + return resolve(root, target); +} + +export interface BuildPiletOptions { + entryModule: string; + app?: string; + originalSchemaVersion: PiletSchemaVersion; + target?: string; + fresh: boolean; + logLevel: LogLevels; + bundlerName: string; + optimizeModules: boolean; + sourceMaps: boolean; + watch: boolean; + contentHash: boolean; + minify: boolean; + declaration: boolean; + hooks?: { + beforeBuild?(e: any): Promise; + afterBuild?(e: any): Promise; + beforeDeclaration?(e: any): Promise; + afterDeclaration?(e: any): Promise; + }; + _: Record; +} + +export async function triggerBuildPilet({ + target, + app, + originalSchemaVersion, + fresh, + entryModule, + logLevel, + optimizeModules, + sourceMaps, + watch, + contentHash, + declaration, + minify, + hooks, + bundlerName, + _, +}: BuildPiletOptions) { + const targetDir = dirname(entryModule); + const { peerDependencies, peerModules, root, apps, piletPackage, ignored, importmap, schema } = + await retrievePiletData(targetDir, app); + const schemaVersion = originalSchemaVersion || schema || config.schemaVersion || defaultSchemaVersion; + const piralInstances = apps.map((m) => m.appPackage.name); + const externals = combinePiletExternals(piralInstances, peerDependencies, peerModules, importmap); + const { main = defaultOutput, name = 'pilet' } = piletPackage; + const dest = getTarget(root, main, targetDir, target); + const outDir = dirname(dest); + const outFile = basename(dest); + + validateSharedDependencies(importmap); + + if (fresh) { + progress('Removing output directory ...'); + await removeDirectory(outDir); + } + + logInfo('Bundle pilet ...'); + + await hooks.beforeBuild?.({ root, outDir, importmap, entryModule, schemaVersion, piletPackage }); + + await callPiletBuild( + { + root, + piralInstances, + optimizeModules, + sourceMaps, + watch, + contentHash, + minify, + externals, + targetDir, + importmap, + outFile, + outDir, + entryModule: `./${relative(root, entryModule)}`, + logLevel, + version: schemaVersion, + ignored, + _, + }, + bundlerName, + ); + + await hooks.afterBuild?.({ root, outDir, importmap, entryModule, schemaVersion, piletPackage }); + + if (declaration) { + await hooks.beforeDeclaration?.({ root, outDir, entryModule, piletPackage }); + await createPiletDeclaration( + name, + piralInstances, + root, + entryModule, + externals, + outDir, + ForceOverwrite.yes, + logLevel, + ); + await hooks.afterDeclaration?.({ root, outDir, entryModule, piletPackage }); + } + + return { + piletPackage, + root, + outDir, + apps, + outFile, + dest, + }; +} diff --git a/src/tooling/piral-cli/src/common/piral.ts b/src/tooling/piral-cli/src/common/piral.ts new file mode 100644 index 000000000..2e55c5283 --- /dev/null +++ b/src/tooling/piral-cli/src/common/piral.ts @@ -0,0 +1,227 @@ +import { join } from 'path'; +import { runScript } from './scripts'; +import { removeDirectory } from './io'; +import { flattenExternals } from './package'; +import { log, logDone, logInfo, progress } from './log'; +import { createEmulatorSources, createEmulatorWebsite, packageEmulator } from './emulator'; +import { emulatorName, emulatorPackageName, emulatorSourcesName, emulatorWebsiteName, releaseName } from './constants'; +import { callPiralBuild } from '../bundler'; +import { LogLevels, SharedDependency } from '../types'; + +async function runLifecycle(root: string, scripts: Record, type: string) { + const script = scripts?.[type]; + + if (script) { + log('generalDebug_0003', `Running "${type}" ("${script}") ...`); + await runScript(script, root); + log('generalDebug_0003', `Finished running "${type}".`); + } else { + log('generalDebug_0003', `No script for "${type}" found ...`); + } +} + +export interface BaseBuildPiralOptions { + root: string; + targetDir: string; + logLevel: LogLevels; + bundlerName: string; + externals: Array; + ignored: Array; + outFile: string; + entryFiles: string; + optimizeModules: boolean; + sourceMaps: boolean; + watch: boolean; + contentHash: boolean; + piralInstances: Array; + scripts?: Record; + hooks?: { + beforeBuild?(e: any): Promise; + afterBuild?(e: any): Promise; + beforeEmulator?(e: any): Promise; + afterEmulator?(e: any): Promise; + beforePackage?(e: any): Promise; + afterPackage?(e: any): Promise; + }; + _: Record; +} + +export interface BuildEmulatorOptions extends BaseBuildPiralOptions { + emulatorType: string; +} + +export async function triggerBuildEmulator({ + root, + logLevel, + externals, + emulatorType, + bundlerName, + optimizeModules, + sourceMaps, + watch, + ignored, + contentHash, + targetDir, + outFile, + scripts, + entryFiles, + piralInstances, + hooks, + _, +}: BuildEmulatorOptions) { + progress('Starting emulator build ...'); + + const emulatorPublicUrl = '/'; + const appDir = emulatorType !== emulatorWebsiteName ? join(targetDir, 'app') : targetDir; + + // since we create this anyway let's just pretend we want to have it clean! + await removeDirectory(targetDir); + + await hooks.beforeBuild?.({ root, publicUrl: emulatorPublicUrl, externals, entryFiles, targetDir, piralInstances }); + + logInfo(`Bundle ${emulatorName} ...`); + + const { + dir: outDir, + name, + hash, + } = await callPiralBuild( + { + root, + piralInstances, + emulator: true, + standalone: false, + optimizeModules, + sourceMaps, + watch, + contentHash, + minify: false, + externals: flattenExternals(externals), + publicUrl: emulatorPublicUrl, + entryFiles, + logLevel, + ignored, + outDir: appDir, + outFile, + _, + }, + bundlerName, + ); + + await hooks.afterBuild?.({ + root, + publicUrl: emulatorPublicUrl, + externals, + entryFiles, + targetDir, + piralInstances, + hash, + outDir, + outFile: name, + }); + + await runLifecycle(root, scripts, 'piral:postbuild'); + await runLifecycle(root, scripts, `piral:postbuild-${emulatorName}`); + + await hooks.beforeEmulator?.({ root, externals, targetDir, outDir }); + + let rootDir = root; + + switch (emulatorType) { + case emulatorPackageName: + rootDir = await createEmulatorSources(root, externals, outDir, outFile, logLevel); + await hooks.beforePackage?.({ root, externals, targetDir, outDir, rootDir }); + await packageEmulator(rootDir); + await hooks.afterPackage?.({ root, externals, targetDir, outDir, rootDir }); + break; + case emulatorSourcesName: + rootDir = await createEmulatorSources(root, externals, outDir, outFile, logLevel); + logDone(`Emulator package sources available in "${rootDir}".`); + break; + case emulatorWebsiteName: + rootDir = await createEmulatorWebsite(root, externals, outDir, outFile, logLevel); + logDone(`Emulator website available in "${rootDir}".`); + break; + } + + await hooks.afterEmulator?.({ root, externals, targetDir, outDir, rootDir }); +} + +export interface BuildShellOptions extends BaseBuildPiralOptions { + minify: boolean; + publicUrl: string; +} + +export async function triggerBuildShell({ + root, + targetDir, + bundlerName, + minify, + optimizeModules, + entryFiles, + piralInstances, + sourceMaps, + logLevel, + ignored, + watch, + outFile, + publicUrl, + contentHash, + externals, + hooks, + scripts, + _, +}: BuildShellOptions) { + progress('Starting release build ...'); + + // since we create this anyway let's just pretend we want to have it clean! + await removeDirectory(targetDir); + + logInfo(`Bundle ${releaseName} ...`); + + await hooks.beforeBuild?.({ root, publicUrl, externals, entryFiles, targetDir, piralInstances }); + + const { + dir: outDir, + name, + hash, + } = await callPiralBuild( + { + root, + piralInstances, + emulator: false, + standalone: false, + optimizeModules, + sourceMaps, + watch, + contentHash, + minify, + externals: flattenExternals(externals), + publicUrl, + outFile, + outDir: targetDir, + entryFiles, + logLevel, + ignored, + _, + }, + bundlerName, + ); + + await hooks.afterBuild?.({ + root, + publicUrl, + externals, + entryFiles, + targetDir, + piralInstances, + outDir, + outFile: name, + hash, + }); + + await runLifecycle(root, scripts, 'piral:postbuild'); + await runLifecycle(root, scripts, `piral:postbuild-${releaseName}`); + + logDone(`Files for publication available in "${outDir}".`); +} diff --git a/src/tooling/piral-cli/src/common/release.ts b/src/tooling/piral-cli/src/common/release.ts new file mode 100644 index 000000000..f1c8c7b55 --- /dev/null +++ b/src/tooling/piral-cli/src/common/release.ts @@ -0,0 +1,62 @@ +import { basename, dirname, relative, resolve } from 'path'; +import { fail, log } from './log'; +import { publishNpmPackage } from './npm'; +import { FormDataObj, postForm } from './http'; +import { checkExists, matchFiles, readBinary } from './io'; +import { PublishScheme } from '../types'; + +export async function publishPackageEmulator( + baseDir: string, + source: string, + args: Record = {}, + interactive = false, +) { + const type = 'emulator'; + const directory = resolve(baseDir, source, type); + const exists = await checkExists(directory); + + if (!exists) { + fail('publishDirectoryMissing_0110', directory); + } + + const files = await matchFiles(directory, '*.tgz'); + log('generalDebug_0003', `Found ${files.length} in "${directory}": ${files.join(', ')}`); + + if (files.length !== 1) { + fail('publishEmulatorFilesUnexpected_0111', directory); + } + + const [file] = files; + const flags = Object.keys(args).reduce((p, c) => { + p.push(`--${c}`, args[c]); + return p; + }, [] as Array); + + await publishNpmPackage(directory, file, flags, interactive); +} + +export async function publishWebsiteEmulator( + version: string, + url: string, + apiKey: string, + mode: PublishScheme, + directory: string, + files: Array, + interactive: boolean, + headers?: Record, + ca?: Buffer, +) { + const data: FormDataObj = { + version, + type: 'custom', + }; + + for (const file of files) { + const relPath = relative(directory, file); + const fileName = basename(file); + const content = await readBinary(dirname(file), fileName); + data[relPath] = [content, fileName]; + } + + return await postForm(url, mode, apiKey, data, headers, ca, interactive); +} diff --git a/src/tooling/piral-cli/src/helpers.ts b/src/tooling/piral-cli/src/helpers.ts index f5c492b5f..21c8310ca 100644 --- a/src/tooling/piral-cli/src/helpers.ts +++ b/src/tooling/piral-cli/src/helpers.ts @@ -7,12 +7,12 @@ import type { PiletPublishSource, PiralBuildType, PiletBuildType, - PiletPublishScheme, + PublishScheme, SourceLanguage, } from './types'; export const schemaKeys: Array = ['v0', 'v1', 'v2', 'v3', 'mf', 'none']; -export const publishModeKeys: Array = ['none', 'basic', 'bearer', 'digest']; +export const publishModeKeys: Array = ['none', 'basic', 'bearer', 'digest']; export const fromKeys: Array = ['local', 'remote', 'npm']; export const piralBuildTypeKeys: Array = [ 'all', diff --git a/src/tooling/piral-cli/src/messages.ts b/src/tooling/piral-cli/src/messages.ts index 0f618cefc..241846afb 100644 --- a/src/tooling/piral-cli/src/messages.ts +++ b/src/tooling/piral-cli/src/messages.ts @@ -2585,6 +2585,28 @@ export function requiredEmulatorAssetDownloadSkipped_0123(url: string): QuickMes return [LogLevels.warning, '0123', `Could not download asset file at "${url}".`]; } +/** + * @kind Error + * + * @summary + * The emulator.json and associated files could not be found in the source directory. + * + * @abstract + * Only an emulator website can be published using `piral publish`. Other artifacts such as + * standard release artifacts or the package emulator (tgz) need to be published using other + * mechanisms such as `npm publish`. + * + * If no emulator website exists you can either build one using the `--fresh` flag with + * `piral publish` (i.e., `piral publish --fresh`) or preparing the build using `piral build` + * with the `--type emulator-website` flag. + * + * @see + * - [Emulator](https://docs.piral.io/concepts/T01-emulator) + */ +export function missingEmulatorWebsite_0130(path: string): QuickMessage { + return [LogLevels.error, '0130', `Could not find the files for an emulator website at "${path}".`]; +} + /** * @kind Warning * diff --git a/src/tooling/piral-cli/src/release.ts b/src/tooling/piral-cli/src/release.ts deleted file mode 100644 index 60a0ac80d..000000000 --- a/src/tooling/piral-cli/src/release.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { basename, dirname, relative, resolve } from 'path'; -import { copy, fail, findFile, FormDataObj, postForm, readBinary } from './common'; -import { availableReleaseProviders } from './helpers'; -import { ReleaseProvider } from './types'; - -async function getVersion(directory: string) { - const data = await findFile(directory, 'package.json'); - - if (!data) { - fail('packageJsonNotFound_0020'); - } - - const { version } = require(data); - return version; -} - -export interface QualifiedReleaseProvider { - name: string; - action: ReleaseProvider; -} - -const providers: Record = { - none() { - return Promise.resolve(); - }, - async xcopy(_, files, args) { - const { target } = args; - - if (!target) { - fail('publishXcopyMissingTarget_0112'); - } - - await Promise.all(files.map(async (file) => copy(file, resolve(target, basename(file))))); - }, - async feed(directory, files, args, interactive) { - const { url, apiKey, scheme = 'basic', version = await getVersion(directory) } = args; - - if (!url) { - fail('publishFeedMissingUrl_0115'); - } - - if (!version) { - fail('publishFeedMissingVersion_0116'); - } - - const data: FormDataObj = { - version, - type: 'custom', - }; - - for (const file of files) { - const relPath = relative(directory, file); - const fileName = basename(file); - const content = await readBinary(dirname(file), fileName); - data[relPath] = [content, fileName]; - } - - await postForm(url, scheme as any, apiKey, data, {}, undefined, interactive); - }, -}; - -function findReleaseProvider(providerName: string) { - const provider = providers[providerName]; - - if (typeof provider !== 'function') { - fail('publishProviderMissing_0113', providerName, availableReleaseProviders); - } - - return provider; -} - -availableReleaseProviders.push(...Object.keys(providers)); - -export function setReleaseProvider(provider: QualifiedReleaseProvider) { - providers[provider.name] = provider.action; - - if (!availableReleaseProviders.includes(provider.name)) { - availableReleaseProviders.push(provider.name); - } -} - -export function publishArtifacts( - providerName: string, - directory: string, - files: Array, - args: Record, - interactive: boolean, -) { - const runRelease = findReleaseProvider(providerName); - return runRelease(directory, files, args, interactive); -} diff --git a/src/tooling/piral-cli/src/types/public.ts b/src/tooling/piral-cli/src/types/public.ts index 8fbb48c17..c974af052 100644 --- a/src/tooling/piral-cli/src/types/public.ts +++ b/src/tooling/piral-cli/src/types/public.ts @@ -1,5 +1,5 @@ import { Argv, Arguments } from 'yargs'; -import { RuleRunner, PiletRuleContext, PiralRuleContext, LogLevels, SharedDependency, ReleaseProvider } from './common'; +import { RuleRunner, PiletRuleContext, PiralRuleContext, LogLevels, SharedDependency } from './common'; export interface ToolCommandRunner { (args: Arguments): void | Promise; @@ -44,7 +44,6 @@ export interface CliPluginApi { withPiletRule(ruleName: string, runner: RuleRunner): CliPluginApi; withPatcher(packageName: string, patch: PackagePatcher): CliPluginApi; withBundler(bundlerName: string, bundler: BundlerDefinition): CliPluginApi; - withReleaseProvider(providerName: string, provider: ReleaseProvider): CliPluginApi; } export interface CliPlugin { @@ -242,7 +241,7 @@ export type AuthConfig = HeaderAuthConfig | HttpAuthConfig; export type SourceLanguage = 'js' | 'ts'; -export type PiletPublishScheme = 'none' | 'digest' | 'bearer' | 'basic'; +export type PublishScheme = 'none' | 'digest' | 'bearer' | 'basic'; export type PiletPublishSource = 'local' | 'npm' | 'remote';