Skip to content

Commit 0e6e812

Browse files
Merge branch 'trunk' into fix/move-sites-up
2 parents ecee4ef + 14e9fc1 commit 0e6e812

129 files changed

Lines changed: 10897 additions & 1148 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/studio/e2e/blueprints.test.ts‎

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,8 @@ test.describe( 'Blueprints', () => {
5959
// Upload blueprint file
6060
await modal.selectBlueprintFile( blueprintPath );
6161

62-
// Wait for file to be processed and continue button to be enabled
63-
await expect( modal.continueButton ).toBeEnabled( { timeout: 5000 } );
64-
await modal.continueButton.click();
62+
// Wait for the create form to appear (file upload navigates directly to it)
63+
await expect( modal.siteNameInput ).toBeVisible( { timeout: 5000 } );
6564

6665
// Fill in site name
6766
await modal.siteNameInput.fill( siteName );
@@ -95,9 +94,8 @@ test.describe( 'Blueprints', () => {
9594
// Upload blueprint file
9695
await modal.selectBlueprintFile( blueprintPath );
9796

98-
// Wait for file to be processed and continue button to be enabled
99-
await expect( modal.continueButton ).toBeEnabled( { timeout: 5000 } );
100-
await modal.continueButton.click();
97+
// Wait for the create form to appear (file upload navigates directly to it)
98+
await expect( modal.siteNameInput ).toBeVisible( { timeout: 5000 } );
10199

102100
// Fill in site name
103101
await modal.siteNameInput.fill( siteName );
@@ -133,9 +131,8 @@ test.describe( 'Blueprints', () => {
133131
// Upload blueprint file
134132
await modal.selectBlueprintFile( blueprintPath );
135133

136-
// Wait for file to be processed and continue button to be enabled
137-
await expect( modal.continueButton ).toBeEnabled( { timeout: 5000 } );
138-
await modal.continueButton.click();
134+
// Wait for the create form to appear (file upload navigates directly to it)
135+
await expect( modal.siteNameInput ).toBeVisible( { timeout: 5000 } );
139136

140137
// Fill in site name
141138
await modal.siteNameInput.fill( siteName );
@@ -169,9 +166,8 @@ test.describe( 'Blueprints', () => {
169166
// Upload blueprint file
170167
await modal.selectBlueprintFile( blueprintPath );
171168

172-
// Wait for file to be processed and continue button to be enabled
173-
await expect( modal.continueButton ).toBeEnabled( { timeout: 5000 } );
174-
await modal.continueButton.click();
169+
// Wait for the create form to appear (file upload navigates directly to it)
170+
await expect( modal.siteNameInput ).toBeVisible( { timeout: 5000 } );
175171

176172
// Fill in site name
177173
await modal.siteNameInput.fill( siteName );
@@ -208,9 +204,8 @@ test.describe( 'Blueprints', () => {
208204
// Upload blueprint file
209205
await modal.selectBlueprintFile( blueprintPath );
210206

211-
// Wait for file to be processed and continue button to be enabled
212-
await expect( modal.continueButton ).toBeEnabled( { timeout: 5000 } );
213-
await modal.continueButton.click();
207+
// Wait for the create form to appear (file upload navigates directly to it)
208+
await expect( modal.siteNameInput ).toBeVisible( { timeout: 5000 } );
214209

215210
// Fill in site name
216211
await modal.siteNameInput.fill( siteName );
@@ -247,9 +242,8 @@ test.describe( 'Blueprints', () => {
247242
// Upload blueprint file
248243
await modal.selectBlueprintFile( blueprintPath );
249244

250-
// Wait for file to be processed and continue button to be enabled
251-
await expect( modal.continueButton ).toBeEnabled( { timeout: 5000 } );
252-
await modal.continueButton.click();
245+
// Wait for the create form to appear (file upload navigates directly to it)
246+
await expect( modal.siteNameInput ).toBeVisible( { timeout: 5000 } );
253247

254248
// Fill in site name
255249
await modal.siteNameInput.fill( siteName );

‎apps/studio/e2e/localization.test.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,22 @@ test.describe( 'Localization', () => {
127127

128128
// Click "Create a site" option using data-testid
129129
await expect( addSiteModal.createSiteButton ).toBeVisible();
130-
// Button contains both title and description, so we check it contains the title
131-
await expect( addSiteModal.createSiteButton ).toContainText( 'サイトを作成' );
132130
await addSiteModal.createSiteButton.click();
133131

132+
// Select "Empty site" and continue to the create form.
133+
// "Empty site" is untranslated so the English text works in all locales.
134+
// Use force:true because the app-drag-region overlay intercepts pointer events.
135+
const emptySiteBtn = session.mainWindow.getByRole( 'button', { name: /Empty site/ } );
136+
await expect( emptySiteBtn ).toBeVisible( { timeout: 5000 } );
137+
await emptySiteBtn.click( { force: true } );
138+
await addSiteModal.continueButton.click();
139+
134140
// Fill in site name using data-testid
135141
await expect( addSiteModal.siteNameInput ).toBeVisible( { timeout: 5000 } );
136142
await addSiteModal.siteNameInput.fill( siteName );
137143

138144
// Click "Add site" button using data-testid (wait for it to be enabled)
139145
await expect( addSiteModal.addSiteButton ).toBeVisible();
140-
await expect( addSiteModal.addSiteButton ).toContainText( 'サイトを追加' );
141146
await expect( addSiteModal.addSiteButton ).toBeEnabled();
142147
await addSiteModal.addSiteButton.click();
143148

‎apps/studio/e2e/page-objects/add-site-modal.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default class AddSiteModal {
1414
}
1515

1616
get blueprintButton() {
17-
return this.page.locator( 'button:has-text("Start from a Blueprint")' ).first();
17+
return this.page.getByTestId( 'create-site-option-button' );
1818
}
1919

2020
get importButton() {

‎apps/studio/e2e/page-objects/onboarding.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export default class Onboarding {
2222
const modal = new AddSiteModal( this.page );
2323
await modal.createSiteButton.click();
2424

25+
const emptySiteButton = this.page.getByRole( 'button', { name: /Empty site/ } );
26+
if ( await emptySiteButton.isVisible( { timeout: 2000 } ).catch( () => false ) ) {
27+
await emptySiteButton.click();
28+
await modal.continueButton.click();
29+
}
30+
2531
if ( customSiteName ) {
2632
await modal.siteNameInput.fill( customSiteName );
2733
}

‎apps/studio/forge.config.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,14 @@ const config: ForgeConfig = {
101101
desktopTemplate: path.join( __dirname, 'installers', 'desktop.ejs' ),
102102
// libcap2-bin: ships `setcap`, used by postinst to grant the bundled
103103
// node CAP_NET_BIND_SERVICE so the proxy can bind ports 80/443.
104-
// policykit-1: pkexec backend used by @vscode/sudo-prompt for hosts-file writes.
104+
// pkexec | policykit-1: provides `pkexec`, used by @vscode/sudo-prompt for
105+
// hosts-file writes. `policykit-1` is the legacy package name (Ubuntu 24.04
106+
// and older Debian); on Debian trixie / Kali rolling polkit was split and
107+
// `pkexec` ships as its own binary package. The alternative makes the
108+
// dependency resolvable on both.
105109
// ca-certificates: ships `update-ca-certificates` and the system trust bundle.
106110
// libnss3-tools: ships `certutil`, used to import the CA into per-user NSS DBs.
107-
depends: [ 'libcap2-bin', 'policykit-1', 'ca-certificates', 'libnss3-tools' ],
111+
depends: [ 'libcap2-bin', 'pkexec | policykit-1', 'ca-certificates', 'libnss3-tools' ],
108112
scripts: {
109113
postinst: path.join( __dirname, 'installers', 'linux', 'postinst.sh' ),
110114
postrm: path.join( __dirname, 'installers', 'linux', 'postrm.sh' ),

‎apps/studio/src/components/tests/app.test.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { wordpressVersionsApi } from 'src/stores/wordpress-versions-api';
1616
import { wpcomApi, wpcomPublicApi } from 'src/stores/wpcom-api';
1717

1818
vi.mock( 'src/index.css', () => ( {} ) );
19+
vi.mock( 'src/components/dot-grid', () => ( {
20+
DotGrid: () => null,
21+
} ) );
1922
vi.mock( 'src/stores/onboarding-slice', async () => {
2023
const actual = await vi.importActual( 'src/stores/onboarding-slice' );
2124
return {

‎apps/studio/src/ipc-handlers.ts‎

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
isRootCATrusted,
7676
trustRootCA,
7777
} from 'src/lib/certificate-manager';
78+
import { download } from 'src/lib/download';
7879
import { simplifyErrorForDisplay } from 'src/lib/error-formatting';
7980
import { buildFeatureFlags } from 'src/lib/feature-flags';
8081
import { getImageData } from 'src/lib/get-image-data';
@@ -186,7 +187,13 @@ export { getDefaultSiteDirectory, saveDefaultSiteDirectory };
186187

187188
export { importSite, exportSite } from 'src/modules/import-export/lib/ipc-handlers';
188189

189-
export { getUserDeskConfig, saveUserDeskConfig } from 'src/modules/desks/lib/ipc-handlers';
190+
export {
191+
getSiteDeskConfig,
192+
getUserDeskConfig,
193+
saveSiteDeskConfig,
194+
saveUserDeskConfig,
195+
} from 'src/modules/desks/lib/ipc-handlers';
196+
export { fetchSiteRest as fetchSiteRestApi } from 'src/lib/wordpress-rest-api';
190197

191198
export {
192199
studioCodeSendMessage,
@@ -678,6 +685,16 @@ export async function createSite(
678685
const metric = getBlueprintMetric( blueprint?.slug );
679686
bumpStat( StatsGroup.STUDIO_SITE_CREATE, metric );
680687

688+
// If the blueprint has a bundle_url (API blueprints with bundled resources like zips),
689+
// download and extract the bundle so bundled resources can be resolved locally.
690+
let bundleTempDir: string | undefined;
691+
let blueprintFilePath = blueprint?.filePath;
692+
if ( blueprint?.bundle_url && ! blueprintFilePath ) {
693+
const result = await downloadAndExtractBlueprintBundle( blueprint.bundle_url );
694+
bundleTempDir = result.tempDir;
695+
blueprintFilePath = result.blueprintJsonPath;
696+
}
697+
681698
try {
682699
const { server } = await SiteServer.create(
683700
{
@@ -689,7 +706,7 @@ export async function createSite(
689706
enableHttps,
690707
siteId,
691708
blueprint: blueprint?.blueprint,
692-
originalBlueprintPath: blueprint?.filePath,
709+
originalBlueprintPath: blueprintFilePath,
693710
adminUsername,
694711
adminPassword,
695712
adminEmail,
@@ -748,7 +765,9 @@ export async function createSite(
748765

749766
throw error;
750767
} finally {
751-
if ( blueprint?.filePath ) {
768+
if ( bundleTempDir ) {
769+
await removeBlueprintTempDir( bundleTempDir ).catch( () => {} );
770+
} else if ( blueprint?.filePath ) {
752771
const blueprintDir = nodePath.dirname( nodePath.resolve( blueprint.filePath ) );
753772
await removeBlueprintTempDir( blueprintDir ).catch( () => {} );
754773
}
@@ -2035,6 +2054,59 @@ export async function listLocalFileTree(
20352054
}
20362055
}
20372056

2057+
/**
2058+
* Downloads a blueprint bundle zip from a URL, extracts it to a temp directory,
2059+
* and returns the path to the extracted blueprint.json.
2060+
* Used for API blueprints that reference bundled resources (e.g. theme zips, WXR files).
2061+
*/
2062+
async function downloadAndExtractBlueprintBundle( bundleUrl: string ): Promise< {
2063+
blueprintJsonPath: string;
2064+
tempDir: string;
2065+
} > {
2066+
const tempDir = await fsPromises.mkdtemp(
2067+
nodePath.join( os.tmpdir(), 'studio-blueprint-bundle-' )
2068+
);
2069+
const tempZipPath = nodePath.join( tempDir, 'bundle.zip' );
2070+
2071+
try {
2072+
await download( bundleUrl, tempZipPath );
2073+
await extractZip( tempZipPath, tempDir );
2074+
await fsPromises.unlink( tempZipPath ).catch( () => {} );
2075+
2076+
// Find blueprint.json in the extracted contents
2077+
let blueprintJsonPath = nodePath.join( tempDir, 'blueprint.json' );
2078+
try {
2079+
await fsPromises.access( blueprintJsonPath );
2080+
} catch {
2081+
// Some zips have a single root directory — check one level deeper
2082+
const files = await fsPromises.readdir( tempDir );
2083+
for ( const file of files ) {
2084+
const nestedPath = nodePath.join( tempDir, file, 'blueprint.json' );
2085+
try {
2086+
await fsPromises.access( nestedPath );
2087+
blueprintJsonPath = nestedPath;
2088+
break;
2089+
} catch {
2090+
// continue checking
2091+
}
2092+
}
2093+
}
2094+
2095+
try {
2096+
await fsPromises.access( blueprintJsonPath );
2097+
} catch {
2098+
throw new Error(
2099+
'No blueprint.json found in the downloaded bundle. Ensure the bundle zip contains a blueprint.json.'
2100+
);
2101+
}
2102+
2103+
return { blueprintJsonPath, tempDir };
2104+
} catch ( error ) {
2105+
await fsPromises.rm( tempDir, { recursive: true, force: true } ).catch( () => {} );
2106+
throw error;
2107+
}
2108+
}
2109+
20382110
export async function validateBlueprint(
20392111
_event: IpcMainInvokeEvent,
20402112
blueprintJson: Blueprint[ 'blueprint' ]

‎apps/studio/src/ipc-types.d.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ type IpcApi = {
9999

100100
interface FeatureFlags {
101101
enableBlueprints: boolean;
102-
enableBlueprintsGallery: boolean;
103102
enableStudioCodeUi: boolean;
104103
}
105104

‎apps/studio/src/lib/feature-flags.ts‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@ export const FEATURE_FLAGS: Record< keyof FeatureFlags, FeatureFlagDefinition >
1212
flag: 'enableBlueprints',
1313
default: true,
1414
},
15-
enableBlueprintsGallery: {
16-
label: 'Enable Blueprints Gallery',
17-
env: 'ENABLE_BLUEPRINTS_GALLERY',
18-
flag: 'enableBlueprintsGallery',
19-
default: false,
20-
},
2115
enableStudioCodeUi: {
2216
label: 'Enable Studio Code UI',
2317
env: 'ENABLE_STUDIO_CODE_UI',

0 commit comments

Comments
 (0)