diff --git a/src/cli/env.loader.ts b/src/cli/env.loader.ts index eb40017e..758ad8fa 100644 --- a/src/cli/env.loader.ts +++ b/src/cli/env.loader.ts @@ -11,11 +11,14 @@ export const loadEnv = (): JunoCliEnv => { const envContainerUrl = containerUrl ?? (mode === 'development' ? 'http://127.0.0.1:5987' : undefined); + const ci = process.env.CI === 'true'; + return { mode: mode ?? 'production', containerUrl: envContainerUrl, console: loadEnvConsole({args, mode}), - config: loadEnvConfig({mode}) + config: loadEnvConfig({mode}), + ci }; }; diff --git a/src/constants/dev.constants.ts b/src/constants/dev.constants.ts index 1211d338..067c2c1a 100644 --- a/src/constants/dev.constants.ts +++ b/src/constants/dev.constants.ts @@ -1,5 +1,8 @@ import {join} from 'node:path'; +export const SATELLITE_PROJECT_NAME = 'satellite'; +export const SPUTNIK_PROJECT_NAME = 'sputnik'; + export const DEVELOPER_PROJECT_SRC_PATH = join(process.cwd(), 'src'); export const DEVELOPER_PROJECT_SATELLITE_PATH = join(DEVELOPER_PROJECT_SRC_PATH, 'satellite'); export const DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH = join( @@ -46,4 +49,7 @@ export const PACKAGE_JSON_PATH = join(process.cwd(), 'package.json'); export const SPUTNIK_INDEX_MJS = 'sputnik.index.mjs'; export const DEPLOY_SPUTNIK_PATH = join(DEPLOY_LOCAL_REPLICA_PATH, SPUTNIK_INDEX_MJS); +export const JUNO_ACTION_SPUTNIK_PATH = '/juno/src/sputnik'; +export const SPUTNIK_CARGO_TOML = join(JUNO_ACTION_SPUTNIK_PATH, CARGO_TOML); + export const SATELLITE_OUTPUT = join(DEPLOY_LOCAL_REPLICA_PATH, 'satellite.wasm'); diff --git a/src/constants/help.constants.ts b/src/constants/help.constants.ts index 935d173f..361f2a3c 100644 --- a/src/constants/help.constants.ts +++ b/src/constants/help.constants.ts @@ -33,7 +33,8 @@ export const FUNCTIONS_EJECT_DESCRIPTION = export const FUNCTIONS_BUILD_NOTES = `- If no language is provided, the CLI attempts to determine the appropriate build. - Language can be shortened to ${magenta('rs')} for Rust, ${magenta('ts')} for TypeScript and ${magenta('mjs')} for JavaScript. -- The path option maps to ${magenta('--manifest-path')} for Rust (Cargo) or to the source file for TypeScript and JavaScript (e.g. ${magenta('index.ts')} or ${magenta('index.mjs')}). +- Use ${magenta('--cargo-path')} to specify a specific crate path. For Rust builds, this maps to ${magenta('--manifest-path')} for ${magenta('cargo build')}. For TypeScript and JavaScript, it points to the Rust crate (commonly "Sputnik") that imports the functions. +- An optional ${magenta('--source-path')} to specify the source file for TypeScript and JavaScript (e.g. ${magenta('index.ts')} or ${magenta('index.mjs')}). - The watch option rebuilds when source files change, with a default debounce delay of 10 seconds; optionally, pass a delay in milliseconds.`; export const CHANGES_LIST_DESCRIPTION = 'List all submitted or applied changes.'; @@ -50,5 +51,8 @@ export const OPTIONS_UPGRADE = `${yellow('--clear-chunks')} Clear any pre export const OPTIONS_URL = `${yellow('-m, --mode')} Set env mode. For example production or a custom string. Default is production. ${yellow('--container-url')} Override a custom container URL. If not provided, defaults to production or the local container in development mode. ${yellow('--console-url')} Specify a custom URL to access the developer Console.`; +export const OPTIONS_BUILD = `${yellow('-l, --lang')} Specify the language for building the serverless functions: ${magenta('rust')}, ${magenta('typescript')} or ${magenta('javascript')}. + ${yellow('--cargo-path')} Path to the Rust manifest. + ${yellow('--source-path')} Optional path to the TypeScript or JavaScript entry file.`; export const NOTE_KEEP_STAGED = `The option ${yellow('--keep-staged')} only applies when ${yellow('--no-apply')} is NOT used (i.e. the change is applied immediately).`; diff --git a/src/help/dev.start.help.ts b/src/help/dev.start.help.ts index fc2bda42..c19eb116 100644 --- a/src/help/dev.start.help.ts +++ b/src/help/dev.start.help.ts @@ -2,7 +2,8 @@ import {cyan, green, magenta, yellow} from 'kleur'; import { DEV_START_DESCRIPTION, FUNCTIONS_BUILD_NOTES, - OPTION_HELP + OPTION_HELP, + OPTIONS_BUILD } from '../constants/help.constants'; import {helpOutput} from './common.help'; import {TITLE} from './help'; @@ -10,8 +11,7 @@ import {TITLE} from './help'; const usage = `Usage: ${green('juno')} ${cyan('dev')} ${magenta('start')} ${yellow('[options]')} Options: - ${yellow('-l, --lang')} Language used when watching for file changes: ${magenta('rust')}, ${magenta('typescript')} or ${magenta('javascript')}. - ${yellow('-p, --path')} Path to the source file or manifest used when watching. + ${OPTIONS_BUILD} ${yellow('-w, --watch')} Rebuild your functions automatically when source files change. ${OPTION_HELP} diff --git a/src/help/functions.build.help.ts b/src/help/functions.build.help.ts index 06a9ffe3..64d08d9e 100644 --- a/src/help/functions.build.help.ts +++ b/src/help/functions.build.help.ts @@ -2,7 +2,8 @@ import {cyan, green, magenta, yellow} from 'kleur'; import { FUNCTIONS_BUILD_DESCRIPTION, FUNCTIONS_BUILD_NOTES, - OPTION_HELP + OPTION_HELP, + OPTIONS_BUILD } from '../constants/help.constants'; import {helpOutput} from './common.help'; import {TITLE} from './help'; @@ -10,8 +11,7 @@ import {TITLE} from './help'; const usage = `Usage: ${green('juno')} ${cyan('functions')} ${magenta('build')} ${yellow('[options]')} Options: - ${yellow('-l, --lang')} Specify the language for building the serverless functions: ${magenta('rust')}, ${magenta('typescript')} or ${magenta('javascript')}. - ${yellow('-p, --path')} Path to the source to bundle. + ${OPTIONS_BUILD} ${yellow('-w, --watch')} Rebuild your functions automatically when source files change. ${OPTION_HELP} diff --git a/src/services/functions/build/build.javascript.ts b/src/services/functions/build/build.javascript.ts index fba6a38e..4ca0f65f 100644 --- a/src/services/functions/build/build.javascript.ts +++ b/src/services/functions/build/build.javascript.ts @@ -1,8 +1,7 @@ -import {isEmptyString, notEmptyString} from '@dfinity/utils'; -import {buildEsm, execute, formatBytes, type PackageJson} from '@junobuild/cli-tools'; +import {notEmptyString} from '@dfinity/utils'; +import {buildEsm, execute, formatBytes} from '@junobuild/cli-tools'; import type {Metafile} from 'esbuild'; import {green, magenta, red, yellow} from 'kleur'; -import {existsSync} from 'node:fs'; import {mkdir} from 'node:fs/promises'; import {join} from 'node:path'; import { @@ -10,30 +9,29 @@ import { DEPLOY_SPUTNIK_PATH, DEVELOPER_PROJECT_SATELLITE_PATH, INDEX_MJS, - INDEX_TS, - PACKAGE_JSON_PATH + INDEX_TS } from '../../../constants/dev.constants'; -import type {BuildArgs, BuildLang} from '../../../types/build'; +import type {BuildArgs, BuildLang, BuildMetadata} from '../../../types/build'; import {formatTime} from '../../../utils/format.utils'; -import {readPackageJson} from '../../../utils/pkg.utils'; import {detectPackageManager} from '../../../utils/pm.utils'; import {confirmAndExit} from '../../../utils/prompt.utils'; +import {prepareJavaScriptBuildMetadata} from './build.metadata.services'; export const buildTypeScript = async ({ - path, + paths, exitOnError -}: Pick = {}) => { - await build({lang: 'ts', path, exitOnError}); +}: Pick = {}) => { + await build({lang: 'ts', paths, exitOnError}); }; export const buildJavaScript = async ({ - path, + paths, exitOnError -}: Pick = {}) => { - await build({lang: 'mjs', path, exitOnError}); +}: Pick = {}) => { + await build({lang: 'mjs', paths, exitOnError}); }; -type BuildArgsTsJs = {lang: Omit} & Pick; +type BuildArgsTsJs = {lang: Omit} & Pick; const build = async ({exitOnError, ...params}: BuildArgsTsJs) => { await installEsbuild(); @@ -41,7 +39,7 @@ const build = async ({exitOnError, ...params}: BuildArgsTsJs) => { await createTargetDir(); try { - const metadata = await prepareMetadata(); + const metadata = await prepareJavaScriptBuildMetadata(); const buildResult = await buildWithEsbuild({params, metadata}); @@ -59,14 +57,14 @@ interface BuildResult { } const buildWithEsbuild = async ({ - params: {lang, path}, + params: {lang, paths}, metadata }: { params: Omit; metadata: BuildMetadata; }): Promise => { const infile = - path ?? join(DEVELOPER_PROJECT_SATELLITE_PATH, lang === 'mjs' ? INDEX_MJS : INDEX_TS); + paths?.source ?? join(DEVELOPER_PROJECT_SATELLITE_PATH, lang === 'mjs' ? INDEX_MJS : INDEX_TS); // We pass the package information as metadata so the Docker container can read it and embed it into the `juno:package` custom section of the WASM’s public metadata. const banner = { @@ -137,35 +135,6 @@ const hasEsbuild = async (): Promise => { } }; -type BuildMetadata = Omit | undefined; - -const prepareMetadata = async (): Promise => { - if (!existsSync(PACKAGE_JSON_PATH)) { - // No package.json therefore no metadata to pass to the build in the container. - return undefined; - } - - try { - const {juno, version, name} = await readPackageJson(); - - if (isEmptyString(juno?.functions?.version) && isEmptyString(version)) { - // No version detected therefore no metadata to the build in the container. - return undefined; - } - - const functionsVersion = juno?.functions?.version; - - return { - ...(notEmptyString(name) && {name}), - ...(notEmptyString(version) && {version}), - ...(notEmptyString(functionsVersion) && {juno}) - }; - } catch (err: unknown) { - console.log(red('⚠️ Could not read build metadata from package.json.')); - throw err; - } -}; - const printResults = ({ metadata, buildResult diff --git a/src/services/functions/build/build.metadata.services.ts b/src/services/functions/build/build.metadata.services.ts new file mode 100644 index 00000000..086c50f0 --- /dev/null +++ b/src/services/functions/build/build.metadata.services.ts @@ -0,0 +1,113 @@ +import {isEmptyString, isNullish, notEmptyString} from '@dfinity/utils'; +import type {JunoPackage} from '@junobuild/config'; +import {red} from 'kleur'; +import {existsSync} from 'node:fs'; +import {writeFile} from 'node:fs/promises'; +import { + JUNO_PACKAGE_JSON_PATH, + PACKAGE_JSON_PATH, + SATELLITE_PROJECT_NAME +} from '../../../constants/dev.constants'; +import {type BuildMetadata, type BuildType} from '../../../types/build'; +import {readPackageJson} from '../../../utils/pkg.utils'; + +export const prepareJunoPkgForSatellite = async ({buildType}: {buildType: BuildType}) => { + // We do not write a juno.package.json for legacy build + if (buildType.build === 'legacy') { + return; + } + + const {version, satelliteVersion} = buildType; + + const pkg: JunoPackage = { + version, + name: SATELLITE_PROJECT_NAME, + dependencies: { + '@junobuild/satellite': satelliteVersion + } + }; + + await writeFile(JUNO_PACKAGE_JSON_PATH, JSON.stringify(pkg, null, 2), 'utf-8'); +}; + +export const prepareJunoPkgForSputnik = async ({ + buildType +}: { + buildType: BuildType; +}): Promise<{success: 'ok' | 'skip'} | {error: string}> => { + // We do not write a juno.package.json for legacy build + if (buildType.build === 'legacy') { + return {success: 'skip'}; + } + + const metadata = await prepareJavaScriptDevMetadata(); + + if ('error' in metadata) { + return {error: metadata.error}; + } + + const {satelliteVersion, sputnikVersion} = buildType; + + if (isNullish(sputnikVersion) || isEmptyString(sputnikVersion)) { + return {error: `⚠️ Cannot resolve the Sputnik "version" in Cargo metadata. Aborting build!`}; + } + + const pkg: JunoPackage = { + ...metadata, + dependencies: { + '@junobuild/satellite': satelliteVersion, + '@junobuild/sputnik': sputnikVersion + } + }; + + await writeFile(JUNO_PACKAGE_JSON_PATH, JSON.stringify(pkg, null, 2), 'utf-8'); + + return {success: 'ok'}; +}; + +export const prepareJavaScriptDevMetadata = async (): Promise => { + const metadata = await prepareJavaScriptBuildMetadata(); + + const pkgName = metadata?.juno?.functions?.name ?? metadata?.name; + const pkgVersion = metadata?.juno?.functions?.version ?? metadata?.version; + + if (isNullish(pkgName) || isEmptyString(pkgName)) { + return {error: `⚠️ Missing "name" in package metadata. Aborting build!`}; + } + + if (isNullish(pkgVersion) || isEmptyString(pkgVersion)) { + return {error: `⚠️ Missing "version" in package metadata. Aborting build!`}; + } + + return { + name: pkgName, + version: pkgVersion + }; +}; + +export const prepareJavaScriptBuildMetadata = async (): Promise => { + if (!existsSync(PACKAGE_JSON_PATH)) { + // No package.json therefore no metadata to pass to the build in the container. + return undefined; + } + + try { + const {juno, version, name} = await readPackageJson(); + + if (isEmptyString(juno?.functions?.version) && isEmptyString(version)) { + // No version detected therefore no metadata to the build in the container. + return undefined; + } + + const functionsVersion = juno?.functions?.version; + + return { + ...(notEmptyString(name) && {name}), + ...(notEmptyString(version) && {version}), + ...(notEmptyString(functionsVersion) && {juno}) + }; + } catch (err: unknown) { + console.log(red('⚠️ Could not read build metadata from package.json.')); + throw err; + } +}; diff --git a/src/services/functions/build/build.rust.services.ts b/src/services/functions/build/build.rust.services.ts index f3c5b60f..8141bc96 100644 --- a/src/services/functions/build/build.rust.services.ts +++ b/src/services/functions/build/build.rust.services.ts @@ -1,9 +1,8 @@ import {isEmptyString, isNullish, nonNullish} from '@dfinity/utils'; import {execute, formatBytes, gzipFile, spawn} from '@junobuild/cli-tools'; -import {type JunoPackage} from '@junobuild/config'; import {generateApi} from '@junobuild/did-tools'; import {magenta, red, yellow} from 'kleur'; -import {existsSync} from 'node:fs'; +import {existsSync, renameSync} from 'node:fs'; import {lstat, mkdir, readFile, rename, writeFile} from 'node:fs/promises'; import {join, relative} from 'node:path'; import ora, {type Ora} from 'ora'; @@ -11,14 +10,17 @@ import {compare, minVersion, satisfies} from 'semver'; import {detectJunoDevConfigType} from '../../../configs/juno.dev.config'; import { DEPLOY_LOCAL_REPLICA_PATH, + DEPLOY_SPUTNIK_PATH, DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH, DEVELOPER_PROJECT_SATELLITE_PATH, IC_WASM_MIN_VERSION, JUNO_PACKAGE_JSON_PATH, SATELLITE_OUTPUT, + SATELLITE_PROJECT_NAME, + SPUTNIK_PROJECT_NAME, TARGET_PATH } from '../../../constants/dev.constants'; -import type {BuildArgs} from '../../../types/build'; +import type {BuildArgs, BuildType} from '../../../types/build'; import {readSatelliteDid} from '../../../utils/did.utils'; import { checkCargoBinInstalled, @@ -28,11 +30,12 @@ import { import {formatTime} from '../../../utils/format.utils'; import {readPackageJson} from '../../../utils/pkg.utils'; import {confirmAndExit} from '../../../utils/prompt.utils'; +import {prepareJunoPkgForSatellite, prepareJunoPkgForSputnik} from './build.metadata.services'; -const CARGO_RELEASE_DIR = join(process.cwd(), 'target', 'wasm32-unknown-unknown', 'release'); -const SATELLITE_PROJECT_NAME = 'satellite'; - -export const buildRust = async ({path}: Pick = {}) => { +export const buildRust = async ({ + paths, + target +}: Pick & {target?: 'wasm32-unknown-unknown' | 'wasm32-wasip1'} = {}) => { const {valid: validRust} = await checkRustVersion(); if (validRust === 'error' || !validRust) { @@ -57,18 +60,34 @@ export const buildRust = async ({path}: Pick = {}) => { return; } + const {valid: validWasi2ic} = target === 'wasm32-wasip1' ? await checkWasi2ic() : {valid: true}; + + if (!validWasi2ic) { + return; + } + const defaultProjectArgs = ['-p', SATELLITE_PROJECT_NAME]; + const cargoTarget = target ?? 'wasm32-unknown-unknown'; + const cargoReleaseDir = join(process.cwd(), 'target'); + const cargoOutputWasm = join(cargoReleaseDir, 'satellite.wasm'); + const args = [ 'build', '--target', - 'wasm32-unknown-unknown', - ...(nonNullish(path) ? ['--manifest-path', path] : defaultProjectArgs), + cargoTarget, + ...(nonNullish(paths?.cargo) ? ['--manifest-path', paths.cargo] : defaultProjectArgs), '--release', - ...(existsSync('Cargo.lock') ? ['--locked'] : []) + ...(existsSync('Cargo.lock') ? ['--locked'] : []), + '--target-dir', + cargoReleaseDir ]; - const env = {...process.env, RUSTFLAGS: '--cfg getrandom_backend="custom"'}; + const env = { + ...process.env, + RUSTFLAGS: '--cfg getrandom_backend="custom"', + ...(target === 'wasm32-wasip1' && {DEV_SCRIPT_PATH: DEPLOY_SPUTNIK_PATH}) + }; await execute({ command: 'cargo', @@ -82,20 +101,46 @@ export const buildRust = async ({path}: Pick = {}) => { }).start(); try { - const buildType = await extractBuildType({path}); + const buildType = await extractBuildType({paths}); if ('error' in buildType) { console.log(red(buildType.error)); return; } - await prepareJunoPkg({buildType}); + switch (target) { + case 'wasm32-wasip1': { + spinner.text = 'Converting WASI to IC...'; + + // The output of the Sputnik build is sputnik.wasm but, the developer and tools is expecting using satellite.wasm + renameSync( + join(cargoReleaseDir, 'wasm32-wasip1', 'release', `${SPUTNIK_PROJECT_NAME}.wasm`), + cargoOutputWasm + ); - await did(); + await wasi2ic({cargoOutputWasm}); + + const result = await prepareJunoPkgForSputnik({buildType}); + + if ('error' in result) { + console.log(red(result.error)); + return {result: 'error'}; + } + + break; + } + default: { + await prepareJunoPkgForSatellite({buildType}); + } + } + + spinner.text = 'Generating DID...'; + + await did({cargoOutputWasm}); await didc(); await api(); - await icWasm({buildType}); + await icWasm({buildType, cargoOutputWasm}); spinner.text = 'Compressing...'; @@ -117,11 +162,11 @@ const SATELLITE_CUSTOM_DID_FILE = join(DEVELOPER_PROJECT_SATELLITE_PATH, EXTENSI const AUTO_GENERATED = `// This file was automatically generated by the Juno CLI. // Any modifications may be overwritten.`; -const did = async () => { +const did = async ({cargoOutputWasm}: {cargoOutputWasm: string}) => { let candid = ''; await spawn({ command: 'candid-extractor', - args: [join(CARGO_RELEASE_DIR, 'satellite.wasm')], + args: [cargoOutputWasm], stdout: (o) => (candid += o) }); @@ -234,33 +279,12 @@ const api = async () => { }); }; -type BuildType = {build: 'legacy'} | {build: 'modern'; version: string; satelliteVersion: string}; - -const prepareJunoPkg = async ({buildType}: {buildType: BuildType}) => { - // We do not write a juno.package.json for legacy build - if (buildType.build === 'legacy') { - return; - } - - const {version, satelliteVersion} = buildType; - - const pkg: JunoPackage = { - version, - name: SATELLITE_PROJECT_NAME, - dependencies: { - '@junobuild/satellite': satelliteVersion - } - }; - - await writeFile(JUNO_PACKAGE_JSON_PATH, JSON.stringify(pkg, null, 2), 'utf-8'); -}; - -const extractBuildType = async ({path}: Pick = {}): Promise< +const extractBuildType = async ({paths}: Pick = {}): Promise< BuildType | {error: string} > => { await mkdir(TARGET_PATH, {recursive: true}); - const manifestArgs = nonNullish(path) ? ['--manifest-path', path] : []; + const manifestArgs = nonNullish(paths?.cargo) ? ['--manifest-path', paths.cargo] : []; let output = ''; await spawn({ @@ -338,22 +362,29 @@ const extractBuildType = async ({path}: Pick = {}): Promise< return {build: 'legacy'}; } - return {build: 'modern', version, satelliteVersion}; + const sputnikPkg = (metadata?.packages ?? []).find((pkg) => pkg?.name === SPUTNIK_PROJECT_NAME); + + return { + build: 'modern', + version, + satelliteVersion, + ...(nonNullish(sputnikPkg) && {sputnikVersion: sputnikPkg.version}) + }; }; -const icWasm = async ({buildType}: {buildType: BuildType}) => { +const icWasm = async ({ + buildType, + cargoOutputWasm +}: { + buildType: BuildType; + cargoOutputWasm: string; +}) => { await mkdir(DEPLOY_LOCAL_REPLICA_PATH, {recursive: true}); // Remove unused functions and debug info. await spawn({ command: 'ic-wasm', - args: [ - join(CARGO_RELEASE_DIR, 'satellite.wasm'), - '-o', - SATELLITE_OUTPUT, - 'shrink', - '--keep-name-section' - ] + args: [cargoOutputWasm, '-o', SATELLITE_OUTPUT, 'shrink', '--keep-name-section'] }); // Adds the content of satellite.did to the `icp:public candid:service` custom section of the public metadata in the wasm @@ -517,3 +548,36 @@ const checkJunoDidc = async (): Promise<{valid: boolean}> => { return {valid: true}; }; + +const wasi2ic = async ({cargoOutputWasm}: {cargoOutputWasm: string}) => { + await execute({ + command: 'wasi2ic', + args: [cargoOutputWasm, cargoOutputWasm, '--quiet'] + }); +}; + +const checkWasi2ic = async (): Promise<{valid: boolean}> => { + const {valid} = await checkCargoBinInstalled({ + command: 'wasi2ic', + args: ['--version'] + }); + + if (valid === false) { + return {valid}; + } + + if (valid === 'error') { + await confirmAndExit( + `The ${magenta( + 'wasi2ic' + )} polyfill tool is required to replaces the specific function calls with their corresponding polyfill implementations for the Internet Computer. Would you like to install it?` + ); + + await execute({ + command: 'cargo', + args: ['install', 'wasi2ic'] + }); + } + + return {valid: true}; +}; diff --git a/src/services/functions/build/build.services.ts b/src/services/functions/build/build.services.ts index 1b01a600..c7dd7d2f 100644 --- a/src/services/functions/build/build.services.ts +++ b/src/services/functions/build/build.services.ts @@ -7,8 +7,10 @@ import { DEVELOPER_PROJECT_SATELLITE_CARGO_TOML, DEVELOPER_PROJECT_SATELLITE_INDEX_MJS, DEVELOPER_PROJECT_SATELLITE_INDEX_TS, - DEVELOPER_PROJECT_SATELLITE_PATH + DEVELOPER_PROJECT_SATELLITE_PATH, + SPUTNIK_CARGO_TOML } from '../../../constants/dev.constants'; +import {ENV} from '../../../env'; import {SMALL_TITLE} from '../../../help/help'; import {type BuildArgs} from '../../../types/build'; import {buildArgs} from '../../../utils/build.utils'; @@ -26,41 +28,44 @@ export const build = async (args?: string[]) => { await executeBuild(params); }; -const executeBuild = async ({lang, path, exitOnError}: Omit) => { +const executeBuild = async ({lang, paths, exitOnError}: Omit) => { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (lang) { case 'rs': - await buildRust({path}); + await buildRust({paths}); return; case 'ts': - await buildTypeScript({path, exitOnError}); + await executeSputnikBuild({paths, exitOnError, buildFn: buildTypeScript}); return; case 'mjs': - await buildJavaScript({path, exitOnError}); + await executeSputnikBuild({paths, exitOnError, buildFn: buildJavaScript}); return; } const isPathToml = - nonNullish(path) && basename(path) === basename(DEVELOPER_PROJECT_SATELLITE_CARGO_TOML); + nonNullish(paths?.cargo) && + basename(paths.cargo) === basename(DEVELOPER_PROJECT_SATELLITE_CARGO_TOML); if (isPathToml) { - await buildRust({path}); + await buildRust({paths}); return; } const isPathTypeScript = - nonNullish(path) && extname(path) === extname(DEVELOPER_PROJECT_SATELLITE_INDEX_TS); + nonNullish(paths?.source) && + extname(paths.source) === extname(DEVELOPER_PROJECT_SATELLITE_INDEX_TS); if (isPathTypeScript) { - await buildTypeScript({path, exitOnError}); + await executeSputnikBuild({paths, exitOnError, buildFn: buildTypeScript}); return; } const isPathJavaScript = - nonNullish(path) && extname(path) === extname(DEVELOPER_PROJECT_SATELLITE_INDEX_MJS); + nonNullish(paths?.source) && + extname(paths.source) === extname(DEVELOPER_PROJECT_SATELLITE_INDEX_MJS); if (isPathJavaScript) { - await buildJavaScript({path, exitOnError}); + await executeSputnikBuild({paths, exitOnError, buildFn: buildJavaScript}); return; } @@ -70,12 +75,26 @@ const executeBuild = async ({lang, path, exitOnError}: Omit) } if (existsSync(DEVELOPER_PROJECT_SATELLITE_INDEX_TS)) { - await buildTypeScript({exitOnError}); + await executeSputnikBuild({ + paths: { + ...paths, + source: DEVELOPER_PROJECT_SATELLITE_INDEX_TS + }, + exitOnError, + buildFn: buildTypeScript + }); return; } if (existsSync(DEVELOPER_PROJECT_SATELLITE_INDEX_MJS)) { - await buildJavaScript({exitOnError}); + await executeSputnikBuild({ + paths: { + ...paths, + source: DEVELOPER_PROJECT_SATELLITE_INDEX_MJS + }, + exitOnError, + buildFn: buildJavaScript + }); return; } @@ -86,10 +105,31 @@ const executeBuild = async ({lang, path, exitOnError}: Omit) ); }; -export const watchBuild = ({watch, path, ...params}: BuildArgs) => { +const executeSputnikBuild = async ({ + paths, + exitOnError, + buildFn +}: Omit & { + buildFn: (args: Pick) => Promise; +}) => { + await buildFn({paths, exitOnError}); + + const withToolchain = nonNullish(paths?.cargo) || ENV.ci; + + if (withToolchain) { + const rustPaths = { + ...paths, + cargo: paths?.cargo ?? SPUTNIK_CARGO_TOML + }; + + await buildRust({paths: rustPaths, target: 'wasm32-wasip1'}); + } +}; + +export const watchBuild = ({watch, paths, ...params}: BuildArgs) => { const doBuild = async () => { console.log(`\n⏱ Rebuilding serverless functions...`); - await executeBuild({path, exitOnError: false, ...params}); + await executeBuild({paths, exitOnError: false, ...params}); }; const DEFAULT_TIMEOUT = 10_000; @@ -102,7 +142,11 @@ export const watchBuild = ({watch, path, ...params}: BuildArgs) => { debounceBuild(); }; - const watchPath = nonNullish(path) ? dirname(path) : DEVELOPER_PROJECT_SATELLITE_PATH; + const watchPath = nonNullish(paths?.source) + ? dirname(paths.source) + : nonNullish(paths?.cargo) + ? dirname(paths.cargo) + : DEVELOPER_PROJECT_SATELLITE_PATH; console.log(SMALL_TITLE); console.log('👀 Watching for file changes'); diff --git a/src/types/build.ts b/src/types/build.ts index f4905525..2901518a 100644 --- a/src/types/build.ts +++ b/src/types/build.ts @@ -1,8 +1,26 @@ +import type {PackageJson} from '@junobuild/cli-tools'; + export type BuildLang = 'ts' | 'mjs' | 'rs'; +export interface BuildPaths { + cargo?: string | undefined; + source?: string | undefined; +} + export interface BuildArgs { lang?: BuildLang; - path?: string | undefined; + paths?: BuildPaths; watch?: boolean | string; exitOnError?: boolean; } + +export type BuildType = + | {build: 'legacy'} + | { + build: 'modern'; + version: string; + satelliteVersion: string; + sputnikVersion?: string | undefined; + }; + +export type BuildMetadata = Omit | undefined; diff --git a/src/types/cli.env.ts b/src/types/cli.env.ts index 260a3500..7988c1fa 100644 --- a/src/types/cli.env.ts +++ b/src/types/cli.env.ts @@ -4,6 +4,7 @@ export type JunoCliEnv = JunoConfigEnv & { containerUrl: string | undefined; console: JunoConsole; config: JunoCliConfig; + ci: boolean; }; export interface JunoConsole { diff --git a/src/utils/build.utils.ts b/src/utils/build.utils.ts index 77bc6641..86edc17b 100644 --- a/src/utils/build.utils.ts +++ b/src/utils/build.utils.ts @@ -1,8 +1,10 @@ +import {notEmptyString} from '@dfinity/utils'; import {hasArgs, nextArg} from '@junobuild/cli-tools'; import type {BuildArgs} from '../types/build'; export const buildArgs = (args?: string[]): BuildArgs => { - const path = nextArg({args, option: '-p'}) ?? nextArg({args, option: '--path'}); + const cargoPath = nextArg({args, option: '--cargo-path'}); + const sourcePath = nextArg({args, option: '--source-path'}); const {lang} = buildLang(args); @@ -10,7 +12,9 @@ export const buildArgs = (args?: string[]): BuildArgs => { const watchValue = nextArg({args, option: '-w'}) ?? nextArg({args, option: '--watch'}); return { - path, + ...((notEmptyString(cargoPath) || notEmptyString(sourcePath)) && { + paths: {cargo: cargoPath, source: sourcePath} + }), lang, watch: watchValue ?? watch };