Skip to content
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
87ff9fe
Configure CLI build pipeline for npm publishing
ivan-ottinger Mar 3, 2026
81d1bcd
Address Copilot review feedback
ivan-ottinger Mar 3, 2026
773f2ee
Remove redundant web WASM pruning (removed upstream in wordpress-play…
ivan-ottinger Mar 5, 2026
2cf25ef
Remove node_modules pruning from postinstall script
ivan-ottinger Mar 5, 2026
7707212
Extract shared Vite config into vite.config.base.ts
ivan-ottinger Mar 5, 2026
fb7be26
Remove @studio/common deps from dependencies (bundled by Vite)
ivan-ottinger Mar 5, 2026
57bfb0e
Rename package from @automattic/studio-cli to @automattic/wp-studio
ivan-ottinger Mar 6, 2026
535a343
Simplify externalDeps filter (no-op @studio/common exception removed)
ivan-ottinger Mar 6, 2026
760b4b2
Fail loudly when patch-package fails during postinstall
ivan-ottinger Mar 6, 2026
0786acf
Use Rollup banner for shebang to preserve sourcemap accuracy
ivan-ottinger Mar 6, 2026
2ba7ede
Deduplicate Node builtin externals in npm Vite config
ivan-ottinger Mar 6, 2026
81c92d1
Sync CLI package version with Studio app version
ivan-ottinger Mar 10, 2026
07ae336
Remove source maps from npm package
ivan-ottinger Mar 12, 2026
bccdb50
Simplify postinstall to inline patch-package command
ivan-ottinger Mar 12, 2026
bdce54e
Remove yargs locale copying (no longer needed since yargs is external…
ivan-ottinger Mar 12, 2026
e55cc43
Use defineConfig() in base Vite config for consistency
ivan-ottinger Mar 12, 2026
d39b8b4
Clarify why @php-wasm/@wp-playground regex externalization is needed
ivan-ottinger Mar 12, 2026
2da2890
Move __STUDIO_CLI_VERSION__ define to shared base config
ivan-ottinger Mar 12, 2026
8455b4d
Update dependencies: remove unused @wp-playground/common, add zod and…
ivan-ottinger Mar 12, 2026
6eacd2d
Update package-lock.json to match package.json
ivan-ottinger Mar 12, 2026
e060e40
Merge remote-tracking branch 'origin/trunk' into stu-1362-cli-npm-bui…
ivan-ottinger Mar 12, 2026
45c5bb1
Restore postinstall script for npm consumers
ivan-ottinger Mar 12, 2026
b677102
Fix postinstall workspace detection by checking for patched packages
ivan-ottinger Mar 12, 2026
bcb1fa4
Add directly imported @php-wasm and @wp-playground packages to depend…
ivan-ottinger Mar 12, 2026
8301ec6
Merge remote-tracking branch 'origin/trunk' into stu-1362-cli-npm-bui…
ivan-ottinger Mar 12, 2026
6b756c5
Separate bump stat handling between CLI and app
fredrikekelund Mar 12, 2026
9bbd8bb
Only enable CLI telemetry in production builds
fredrikekelund Mar 12, 2026
1d02ed6
Type-safe wrappers in `apps/cli`
fredrikekelund Mar 12, 2026
fd74c4d
Fix bad imports
fredrikekelund Mar 12, 2026
dbcb0ee
Cleanup
fredrikekelund Mar 12, 2026
8c11f7f
Fix tests
fredrikekelund Mar 12, 2026
d902e60
Naming
fredrikekelund Mar 12, 2026
0b3b46a
Fix locking regression
fredrikekelund Mar 12, 2026
81e1f66
Only copy node_modules when building CLI for prod
fredrikekelund Mar 13, 2026
508ef54
Fix package-lock diff
fredrikekelund Mar 13, 2026
b140592
Upgrade Playground packages
fredrikekelund Mar 13, 2026
479e8b9
Merge branch 'trunk' into stu-1362-cli-npm-build-pipeline
fredrikekelund Mar 13, 2026
0328355
Fixup
fredrikekelund Mar 13, 2026
2684826
Even more fixups
fredrikekelund Mar 13, 2026
be9c5e7
Added README and LICENSE
fredrikekelund Mar 13, 2026
cfb3213
Update version
fredrikekelund Mar 13, 2026
7e77f61
`npm pkg fix` and remove `publishConfig`
fredrikekelund Mar 13, 2026
1396884
Add a warning in the README
fredrikekelund Mar 13, 2026
2515bf2
Install atomically, fix tree-shaking, upgrade version
fredrikekelund Mar 13, 2026
66eab06
Move ora dependency
fredrikekelund Mar 13, 2026
ddb03c8
Version
fredrikekelund Mar 13, 2026
3193be8
Define external modules the same way in every build
fredrikekelund Mar 16, 2026
6908bec
Dynamic command imports to avoid barrel file issue
fredrikekelund Mar 16, 2026
03e99d0
Merge branch 'stu-1362-cli-npm-build-pipeline' into stu-1365-standalo…
fredrikekelund Mar 16, 2026
2edd6dd
Fix circular dependency issue
fredrikekelund Mar 16, 2026
197a919
Fix
fredrikekelund Mar 16, 2026
d097274
Merge branch 'trunk' into stu-1365-standalone-cli-telemetry
fredrikekelund Mar 17, 2026
888ae05
More permissive `lastBumpStats` zod schema
fredrikekelund Mar 17, 2026
7b0a05a
Review suggestion
fredrikekelund Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ WordPress Studio - Electron desktop app for managing local WordPress sites. Buil

## Build & Distribution

**Build**: CLI (`vite build --config apps/cli/vite.config.ts`) → Electron (`electron-vite build --config apps/studio/electron.vite.config.ts`) → Package (`electron-forge make --config apps/studio/forge.config.ts`)
**Build**: CLI (`vite build --config apps/cli/vite.config.dev.ts`) → Electron (`electron-vite build --config apps/studio/electron.vite.config.ts`) → Package (`electron-forge make --config apps/studio/forge.config.ts`)
**Platforms**: macOS (x64/ARM64 DMG), Windows (x64/ARM64 MSIX), Linux (DEB)
**Bundling**: Rollup (main), Vite (renderer with code splitting), ASAR (resources)

Expand Down
4 changes: 3 additions & 1 deletion apps/cli/globals.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
declare const __ENABLE_STUDIO_AI__: boolean;
declare const __ENABLE_AGENT_SUITE__: boolean;
declare const __ENABLE_CLI_TELEMETRY__: boolean;
declare const __ENABLE_STUDIO_AI__: boolean;
declare const __IS_PACKAGED_FOR_NPM__: boolean;
declare const __STUDIO_CLI_VERSION__: string;
40 changes: 18 additions & 22 deletions apps/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import path from 'node:path';
import {
bumpAggregatedUniqueStat,
AppdataProvider,
LastBumpStatsData,
} from '@studio/common/lib/bump-stat';
import { suppressPunycodeWarning } from '@studio/common/lib/suppress-punycode-warning';
import { StatsGroup, StatsMetric } from '@studio/common/types/stats';
import { __ } from '@wordpress/i18n';
import yargs from 'yargs';
import { readAppdata, lockAppdata, unlockAppdata, saveAppdata } from 'cli/lib/appdata';
import { bumpAggregatedUniqueStat, getPlatformMetric } from 'cli/lib/bump-stat';
import { loadTranslations } from 'cli/lib/i18n';
import { StatsGroup, StatsMetric } from 'cli/lib/types/bump-stats';
import { untildify } from 'cli/lib/utils';
import { StudioArgv } from 'cli/types';

const version = __STUDIO_CLI_VERSION__;

suppressPunycodeWarning();

const cliAppdataProvider: AppdataProvider< LastBumpStatsData > = {
load: readAppdata,
lock: lockAppdata,
unlock: unlockAppdata,
save: async ( data ) => {
// Cast is safe: data comes from readAppdata() which returns the full UserData type.
// The lock/unlock is already handled by the caller (updateLastBump in /common/lib/bump-stat.ts)
// eslint-disable-next-line studio/require-lock-before-save
await saveAppdata( data as never );
},
};

async function main() {
const yargsLocale = await loadTranslations();

Expand All @@ -52,14 +35,27 @@ async function main() {
},
} )
.middleware( async ( argv ) => {
if ( ! argv.avoidTelemetry ) {
if ( __ENABLE_CLI_TELEMETRY__ && ! argv.avoidTelemetry ) {
try {
await bumpAggregatedUniqueStat(
StatsGroup.STUDIO_CLI_USAGE_UNIQUE,
StatsMetric.SUCCESS,
'weekly',
cliAppdataProvider
'weekly'
);

if ( __IS_PACKAGED_FOR_NPM__ ) {
await bumpAggregatedUniqueStat(
StatsGroup.STUDIO_CLI_WEEKLY_UNIQUE_NPM,
getPlatformMetric(),
'weekly'
);
} else {
await bumpAggregatedUniqueStat(
StatsGroup.STUDIO_CLI_WEEKLY_UNIQUE_APP,
getPlatformMetric(),
'weekly'
);
}
} catch ( error ) {
console.error( 'Failed to bump stat:', error );
}
Expand Down
6 changes: 2 additions & 4 deletions apps/cli/lib/appdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { lockFileAsync, unlockFileAsync } from '@studio/common/lib/lockfile';
import { getAuthenticationUrl } from '@studio/common/lib/oauth';
import { siteDetailsSchema } from '@studio/common/lib/site-events';
import { snapshotSchema } from '@studio/common/types/snapshot';
import { StatsMetric } from '@studio/common/types/stats';
import { __, sprintf } from '@wordpress/i18n';
import { readFile, writeFile } from 'atomically';
import { z } from 'zod';
import { validateAccessToken } from 'cli/lib/api';
import { StatsMetric } from 'cli/lib/types/bump-stats';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import here, it should be safe to remove.

import { LoggerError } from 'cli/logger';
import type { AiProviderId } from 'cli/ai/providers';

Expand Down Expand Up @@ -44,9 +44,7 @@ const userDataSchema = z
} )
.loose()
.optional(),
lastBumpStats: z
.record( z.string(), z.partialRecord( z.enum( StatsMetric ), z.number() ) )
.optional(),
lastBumpStats: z.record( z.string(), z.record( z.string(), z.number() ) ).optional(),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I previously advocated for the stricter schema here (#2394 (review)), but it actually causes more trouble than it fixes, given the other changes in this PR. Strictness in runtime types makes sense (i.e., for the function arguments to `bumpStat), but strictness in the appdata schema introduces the risk of runtime exceptions if the config file contains unknown bump stats. That isn't really needed, since we only use this object to check when we last bumped a particular stat.

Anyway, with this change, it's safe to land this PR to trunk even before the config file PR train (#2807 etc) has landed.

betaFeatures: betaFeaturesSchema.optional(),
} )
.loose();
Expand Down
50 changes: 50 additions & 0 deletions apps/cli/lib/bump-stat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
__bumpAggregatedUniqueStat,
__bumpStat,
AggregateInterval,
LastBumpStatsProvider,
} from '@studio/common/lib/bump-stat';
import { lockAppdata, readAppdata, saveAppdata, unlockAppdata } from 'cli/lib/appdata';
import { StatsGroup, StatsMetric } from 'cli/lib/types/bump-stats';

const lastBumpStatsProvider: LastBumpStatsProvider = {
load: async () => {
const { lastBumpStats } = await readAppdata();
return lastBumpStats ?? {};
},
lock: lockAppdata,
unlock: unlockAppdata,
save: async ( lastBumpStats ) => {
const appdata = await readAppdata();
appdata.lastBumpStats = lastBumpStats;
// Locking is handled in `@studio/common/lib/bump-stat`
// eslint-disable-next-line studio/require-lock-before-save
await saveAppdata( appdata );
},
};

export function bumpStat( group: StatsGroup, stat: StatsMetric, bumpInDev = false ) {
return __bumpStat( group, stat, bumpInDev );
}

export async function bumpAggregatedUniqueStat(
group: StatsGroup,
stat: StatsMetric,
aggregateBy: AggregateInterval,
bumpInDev = false
) {
return __bumpAggregatedUniqueStat( group, stat, aggregateBy, lastBumpStatsProvider, bumpInDev );
}

export function getPlatformMetric(): StatsMetric {
switch ( process.platform ) {
case 'darwin':
return StatsMetric.DARWIN;
case 'linux':
return StatsMetric.LINUX;
case 'win32':
return StatsMetric.WINDOWS;
default:
return StatsMetric.UNKNOWN_PLATFORM;
}
}
2 changes: 1 addition & 1 deletion apps/cli/lib/tests/appdata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import { arePathsEqual } from '@studio/common/lib/fs-utils';
import { StatsMetric } from '@studio/common/types/stats';
import { readFile, writeFile } from 'atomically';
import { vi } from 'vitest';
import {
Expand All @@ -12,6 +11,7 @@ import {
lockAppdata,
unlockAppdata,
} from 'cli/lib/appdata';
import { StatsMetric } from 'cli/lib/types/bump-stats';

vi.mock( 'fs', () => ( {
default: {
Expand Down
15 changes: 15 additions & 0 deletions apps/cli/lib/types/bump-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum StatsGroup {
STUDIO_CLI_USAGE_UNIQUE = 'studio-cli-usage-unique',
STUDIO_CLI_WEEKLY_UNIQUE_NPM = 'studio-cli-weekly-unq-npm',
STUDIO_CLI_WEEKLY_UNIQUE_APP = 'studio-cli-weekly-unq-app',
}

export enum StatsMetric {
SUCCESS = 'success',
FAILURE = 'failure',
// Platforms
DARWIN = 'darwin',
LINUX = 'linux',
WINDOWS = 'win32',
UNKNOWN_PLATFORM = 'unknown-platform',
}
7 changes: 4 additions & 3 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@
"zod": "^4.3.6"
},
"scripts": {
"build": "vite build --config ./vite.config.ts",
"build": "vite build --config ./vite.config.dev.ts",
"build:prod": "vite build --config ./vite.config.prod.ts",
"build:npm": "vite build --config ./vite.config.npm.ts",
"install:bundle": "npm install --omit=dev --no-package-lock --no-progress --install-links --no-workspaces && patch-package && node ../../scripts/remove-fs-ext-other-platform-binaries.mjs",
"package": "npm run install:bundle && npm run build",
"watch": "vite build --config ./vite.config.ts --watch",
"package": "npm run install:bundle && npm run build:prod",
"watch": "vite build --config ./vite.config.dev.ts --watch",
"prepublishOnly": "npm run build:npm",
"postinstall": "node scripts/postinstall-npm.mjs"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/vite.config.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ export const baseConfig = defineConfig( {
mainFields: [ 'main' ],
},
define: {
__STUDIO_CLI_VERSION__: JSON.stringify( packageJson.version ),
__ENABLE_STUDIO_AI__: true,
__ENABLE_AGENT_SUITE__: true,
__STUDIO_CLI_VERSION__: JSON.stringify( packageJson.version ),
},
} );
12 changes: 12 additions & 0 deletions apps/cli/vite.config.dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig, mergeConfig } from 'vite';
import { baseConfig } from './vite.config.base';

export default mergeConfig(
baseConfig,
defineConfig( {
define: {
__IS_PACKAGED_FOR_NPM__: false,
__ENABLE_CLI_TELEMETRY__: false,
},
} )
);
4 changes: 3 additions & 1 deletion apps/cli/vite.config.npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export default mergeConfig(
},
},
define: {
__ENABLE_STUDIO_AI__: false,
__ENABLE_AGENT_SUITE__: false,
__ENABLE_CLI_TELEMETRY__: true,
__ENABLE_STUDIO_AI__: false,
__IS_PACKAGED_FOR_NPM__: true,
},
} )
);
7 changes: 5 additions & 2 deletions apps/cli/vite.config.ts → apps/cli/vite.config.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { resolve } from 'path';
import { sync as globSync } from 'glob';
import { defineConfig, mergeConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { baseConfig } from './vite.config.base';
import devConfig from './vite.config.dev';

const cliNodeModulesPath = resolve( __dirname, 'node_modules' );
const distCliNodeModulesPath = resolve( __dirname, 'dist/cli/node_modules' );

export default mergeConfig(
baseConfig,
devConfig,
defineConfig( {
plugins: [
...( existsSync( cliNodeModulesPath )
Expand Down Expand Up @@ -43,5 +43,8 @@ export default mergeConfig(
]
: [] ),
],
define: {
__ENABLE_CLI_TELEMETRY__: true,
},
} )
);
42 changes: 12 additions & 30 deletions apps/studio/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,7 @@ import path from 'path';
import { pathToFileURL } from 'url';
import * as Sentry from '@sentry/electron/main';
import { PROTOCOL_PREFIX } from '@studio/common/constants';
import {
bumpStat,
bumpAggregatedUniqueStat,
AppdataProvider,
LastBumpStatsData,
} from '@studio/common/lib/bump-stat';
import { suppressPunycodeWarning } from '@studio/common/lib/suppress-punycode-warning';
import { StatsGroup } from '@studio/common/types/stats';
import { __, _n, sprintf } from '@wordpress/i18n';
import {
installExtension,
Expand All @@ -33,7 +26,12 @@ import {
hasActiveSyncOperations,
hasUploadingPushOperations,
} from 'src/lib/active-sync-operations';
import { getPlatformMetric } from 'src/lib/bump-stats/lib';
import {
bumpStat,
bumpAggregatedUniqueStat,
getPlatformMetric,
StatsGroup,
} from 'src/lib/bump-stats';
import { handleDeeplink } from 'src/lib/deeplink';
import { getUserLocaleWithFallback } from 'src/lib/locale-node';
import { getSentryReleaseInfo } from 'src/lib/sentry-release';
Expand Down Expand Up @@ -89,20 +87,6 @@ if ( ! process.env.IS_DEV_BUILD ) {

suppressPunycodeWarning();

const appAppdataProvider: AppdataProvider< LastBumpStatsData > = {
load: loadUserData,
lock: lockAppdata,
unlock: unlockAppdata,
save: async ( data ) => {
// Cast is safe: data comes from loadUserData() which returns the full UserData type.
// The lock/unlock is already handled by the caller (updateLastBump in /common/lib/bump-stat.ts)
// eslint-disable-next-line studio/require-lock-before-save
await saveUserData( data as never );
},
};

// Handle creating/removing shortcuts on Windows when installing/uninstalling.

const isInInstaller = require( 'electron-squirrel-startup' );

// Ensure we're the only instance of the app running
Expand Down Expand Up @@ -345,24 +329,22 @@ async function appBoot() {
const userData = await loadUserData();
// Bump stats for the first time the app runs - this is when no lastBumpStats are available
if ( ! userData.lastBumpStats ) {
bumpStat( StatsGroup.STUDIO_APP_LAUNCH, getPlatformMetric( process.platform ) );
bumpStat( StatsGroup.STUDIO_APP_LAUNCH, getPlatformMetric() );
}

// Bump a stat on each app launch, approximates total app launches
bumpStat( StatsGroup.STUDIO_APP_LAUNCH_TOTAL, getPlatformMetric( process.platform ) );
bumpStat( StatsGroup.STUDIO_APP_LAUNCH_TOTAL, getPlatformMetric() );
// Bump stat for unique weekly app launch, approximates weekly active users
bumpAggregatedUniqueStat(
StatsGroup.STUDIO_APP_LAUNCH_UNIQUE,
getPlatformMetric( process.platform ),
'weekly',
appAppdataProvider
getPlatformMetric(),
'weekly'
).catch( ( err ) => Sentry.captureException( err ) );
// Bump stat for unique monthly app launch, approximates monthly active users
bumpAggregatedUniqueStat(
StatsGroup.STUDIO_APP_LAUNCH_UNIQUE_MONTHLY,
getPlatformMetric( process.platform ),
'monthly',
appAppdataProvider
getPlatformMetric(),
'monthly'
).catch( ( err ) => Sentry.captureException( err ) );

await updateWindowsCliVersionedPathIfNeeded();
Expand Down
10 changes: 7 additions & 3 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import os from 'os';
import nodePath from 'path';
import * as Sentry from '@sentry/electron/main';
import { validateBlueprintData } from '@studio/common/lib/blueprint-validation';
import { bumpStat } from '@studio/common/lib/bump-stat';
import { parseCliError, errorMessageContains } from '@studio/common/lib/cli-error';
import {
calculateDirectorySizeForArchive,
Expand All @@ -37,12 +36,17 @@ import { portFinder } from '@studio/common/lib/port-finder';
import { sanitizeFolderName } from '@studio/common/lib/sanitize-folder-name';
import { isWordPressDevVersion } from '@studio/common/lib/wordpress-version-utils';
import { Snapshot } from '@studio/common/types/snapshot';
import { StatsGroup, StatsMetric } from '@studio/common/types/stats';
import { __, sprintf, LocaleData, defaultI18n } from '@wordpress/i18n';
import { MACOS_TRAFFIC_LIGHT_POSITION, MAIN_MIN_WIDTH, SIDEBAR_WIDTH } from 'src/constants';
import { sendIpcEventToRendererWithWindow } from 'src/ipc-utils';
import { getBetaFeatures as getBetaFeaturesFromLib } from 'src/lib/beta-features';
import { getImporterMetric, getBlueprintMetric } from 'src/lib/bump-stats/lib';
import {
bumpStat,
getImporterMetric,
getBlueprintMetric,
StatsGroup,
StatsMetric,
} from 'src/lib/bump-stats';
import {
openCertificate as openCertificateDialog,
isRootCATrusted,
Expand Down
Loading
Loading