2025-06-30 18:19:15 +08:00
const fs = require ( "fs" ) ;
const path = require ( "path" ) ;
2022-07-06 17:50:52 +08:00
const { execSync } = require ( "child_process" ) ;
2021-07-06 00:59:35 +08:00
2025-06-30 18:19:15 +08:00
const updateChangelog = require ( "./updateChangelog" ) ;
// skipping utils for now, as it has independent release process
const PACKAGES = [ "common" , "math" , "element" , "excalidraw" ] ;
const PACKAGES _DIR = path . resolve ( _ _dirname , "../packages" ) ;
/ * *
* Returns the arguments for the release script .
*
* Usage examples :
* - yarn release -- help - > prints this help message
* - yarn release - > publishes ` @excalidraw ` packages with "test" tag and "-[hash]" version suffix
* - yarn release -- tag = test - > same as above
* - yarn release -- tag = next - > publishes ` @excalidraw ` packages with "next" tag and version "-[hash]" suffix
* - yarn release -- tag = next -- non - interactive - > skips interactive prompts ( runs on CI / CD ) , otherwise same as above
* - yarn release -- tag = latest -- version = 0.19 . 0 - > publishes ` @excalidraw ` packages with "latest" tag and version "0.19.0" & prepares changelog for the release
*
* @ returns [ tag , version , nonInteractive ]
* /
const getArguments = ( ) => {
let tag = "test" ;
let version = "" ;
let nonInteractive = false ;
for ( const argument of process . argv . slice ( 2 ) ) {
if ( /--help/ . test ( argument ) ) {
console . info ( ` Available arguments:
-- tag = < tag > - > ( optional ) "test" ( default ) , "next" for auto release , "latest" for stable release
-- version = < version > - > ( optional ) for "next" and "test" , ( required ) for "latest" i . e . "0.19.0"
-- non - interactive - > ( optional ) disables interactive prompts ` );
console . info ( ` \n Usage examples:
- yarn release - > publishes \ ` @excalidraw \` packages with "test" tag and "-[hash]" version suffix
- yarn release -- tag = test - > same as above
- yarn release -- tag = next - > publishes \ ` @excalidraw \` packages with "next" tag and version "-[hash]" suffix
- yarn release -- tag = next -- non - interactive - > skips interactive prompts ( runs on CI / CD ) , otherwise same as above
- yarn release -- tag = latest -- version = 0.19 . 0 - > publishes \ ` @excalidraw \` packages with "latest" tag and version "0.19.0" & prepares changelog for the release ` ) ;
process . exit ( 0 ) ;
}
if ( /--tag=/ . test ( argument ) ) {
tag = argument . split ( "=" ) [ 1 ] ;
}
if ( /--version=/ . test ( argument ) ) {
version = argument . split ( "=" ) [ 1 ] ;
}
if ( /--non-interactive/ . test ( argument ) ) {
nonInteractive = true ;
}
}
if ( tag !== "latest" && tag !== "next" && tag !== "test" ) {
console . error ( ` Unsupported tag " ${ tag } ", use "latest", "next" or "test". ` ) ;
process . exit ( 1 ) ;
}
if ( tag === "latest" && ! version ) {
console . error ( "Pass the version to make the latest stable release!" ) ;
process . exit ( 1 ) ;
}
if ( ! version ) {
// set the next version based on the excalidraw package version + commit hash
const excalidrawPackageVersion = require ( getPackageJsonPath (
"excalidraw" ,
) ) . version ;
const hash = getShortCommitHash ( ) ;
if ( ! excalidrawPackageVersion . includes ( hash ) ) {
version = ` ${ excalidrawPackageVersion } - ${ hash } ` ;
} else {
// ensuring idempotency
version = excalidrawPackageVersion ;
}
}
console . info ( ` Running with tag " ${ tag } " and version " ${ version } "... ` ) ;
return [ tag , version , nonInteractive ] ;
} ;
const validatePackageName = ( packageName ) => {
if ( ! PACKAGES . includes ( packageName ) ) {
console . error ( ` Package " ${ packageName } " not found! ` ) ;
2021-07-06 00:59:35 +08:00
process . exit ( 1 ) ;
}
} ;
2025-06-30 18:19:15 +08:00
const getPackageJsonPath = ( packageName ) => {
validatePackageName ( packageName ) ;
return path . resolve ( PACKAGES _DIR , packageName , "package.json" ) ;
2022-07-06 17:50:52 +08:00
} ;
2025-06-30 18:19:15 +08:00
const updatePackageJsons = ( nextVersion ) => {
const packageJsons = new Map ( ) ;
for ( const packageName of PACKAGES ) {
const pkg = require ( getPackageJsonPath ( packageName ) ) ;
pkg . version = nextVersion ;
if ( pkg . dependencies ) {
for ( const dependencyName of PACKAGES ) {
if ( ! pkg . dependencies [ ` @excalidraw/ ${ dependencyName } ` ] ) {
continue ;
}
pkg . dependencies [ ` @excalidraw/ ${ dependencyName } ` ] = nextVersion ;
}
}
packageJsons . set ( packageName , ` ${ JSON . stringify ( pkg , null , 2 ) } \n ` ) ;
}
// modify once, to avoid inconsistent state
for ( const packageName of PACKAGES ) {
const content = packageJsons . get ( packageName ) ;
fs . writeFileSync ( getPackageJsonPath ( packageName ) , content , "utf-8" ) ;
}
} ;
const getShortCommitHash = ( ) => {
return execSync ( "git rev-parse --short HEAD" ) . toString ( ) . trim ( ) ;
} ;
const askToCommit = ( tag , nextVersion ) => {
if ( tag !== "latest" ) {
return Promise . resolve ( ) ;
}
return new Promise ( ( resolve ) => {
const rl = require ( "readline" ) . createInterface ( {
input : process . stdin ,
output : process . stdout ,
} ) ;
rl . question (
2025-08-07 21:38:58 +08:00
"Would you like to commit these changes to git? (Y/n): " ,
2025-06-30 18:19:15 +08:00
( answer ) => {
rl . close ( ) ;
if ( answer . toLowerCase ( ) === "y" ) {
execSync ( ` git add -u ` ) ;
execSync (
` git commit -m "chore: release @excalidraw/excalidraw@ ${ nextVersion } 🎉" ` ,
) ;
} else {
console . warn (
"Skipping commit. Don't forget to commit manually later!" ,
) ;
}
resolve ( ) ;
} ,
) ;
} ) ;
} ;
const buildPackages = ( ) => {
console . info ( "Running yarn install..." ) ;
execSync ( ` yarn --frozen-lockfile ` , { stdio : "inherit" } ) ;
console . info ( "Removing existing build artifacts..." ) ;
execSync ( ` yarn rm:build ` , { stdio : "inherit" } ) ;
for ( const packageName of PACKAGES ) {
console . info ( ` Building "@excalidraw/ ${ packageName } "... ` ) ;
execSync ( ` yarn run build:esm ` , {
cwd : path . resolve ( PACKAGES _DIR , packageName ) ,
stdio : "inherit" ,
} ) ;
}
} ;
const askToPublish = ( tag , version ) => {
return new Promise ( ( resolve ) => {
const rl = require ( "readline" ) . createInterface ( {
input : process . stdin ,
output : process . stdout ,
} ) ;
rl . question (
2025-08-07 21:38:58 +08:00
"Would you like to publish these changes to npm? (Y/n): " ,
2025-06-30 18:19:15 +08:00
( answer ) => {
rl . close ( ) ;
if ( answer . toLowerCase ( ) === "y" ) {
publishPackages ( tag , version ) ;
} else {
console . info ( "Skipping publish." ) ;
}
resolve ( ) ;
} ,
) ;
} ) ;
} ;
const publishPackages = ( tag , version ) => {
for ( const packageName of PACKAGES ) {
execSync ( ` yarn publish --tag ${ tag } ` , {
cwd : path . resolve ( PACKAGES _DIR , packageName ) ,
stdio : "inherit" ,
} ) ;
console . info (
` Published "@excalidraw/ ${ packageName } @ ${ tag } " with version " ${ version } "! 🎉 ` ,
) ;
}
} ;
/** main */
( async ( ) => {
const [ tag , version , nonInteractive ] = getArguments ( ) ;
buildPackages ( ) ;
if ( tag === "latest" ) {
await updateChangelog ( version ) ;
}
updatePackageJsons ( version ) ;
if ( nonInteractive ) {
publishPackages ( tag , version ) ;
} else {
await askToCommit ( tag , version ) ;
await askToPublish ( tag , version ) ;
}
} ) ( ) ;