Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"build:publish": "tsc --build tsconfig.publish.json && npm run copyfiles",
"build:watch": "npm run build && tsc --watch",
"clean": "node -e \"fs.rmSync('lib', { recursive: true, force: true }); fs.rmSync('dev', { recursive: true, force: true });\"",
"copyfiles": "node -e \"const fs = require('fs'); fs.mkdirSync('./lib', {recursive:true}); fs.copyFileSync('./src/dynamicImport.js', './lib/dynamicImport.js')\"",
"copyfiles": "node -e \"const fs = require('fs'); fs.copyFileSync('./src/dynamicImport.js', './lib/dynamicImport.js'); fs.cpSync('./src/firebase_studio', './lib/firebase_studio', {recursive: true, filter: (src) => fs.statSync(src).isDirectory() || src.endsWith('.md') || src.endsWith('.js')});\"",
"format": "npm run format:ts && npm run format:other",
"format:other": "npm run lint:other -- --write",
"format:ts": "npm run lint:ts -- --fix --quiet",
Expand Down
21 changes: 16 additions & 5 deletions src/commands/studio-export.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Command } from "../command";
import { logger } from "../logger";
import { Options } from "../options";
import { migrate } from "../firebase_studio/migrate";
import * as path from "path";
import * as experiments from "../experiments";
import { FirebaseError } from "../error";

export const command = new Command("studio:export")
.description("export Firebase Studio apps to continue development locally")
.action(() => {
export const command = new Command("studio:export <path>")
.description(
"Bootstrap Firebase Studio apps for migration to Antigravity. Run on the unzipped folder from the Firebase Studio download.",
)
.option("--no-start-agy", "skip starting the Antigravity IDE after migration")
.action(async (exportPath: string, options: Options) => {
experiments.assertEnabled("studioexport", "export Studio apps");
logger.info("Exporting Studio apps to Antigravity...");
// TODO: implement export logic
if (!exportPath) {
throw new FirebaseError("Must specify a path for migration.", { exit: 1 });
}
const rootPath = path.resolve(exportPath);
logger.info(`Exporting Studio apps from ${rootPath} to Antigravity...`);
await migrate(rootPath, { noStartAgy: !options.startAgy });
});
127 changes: 127 additions & 0 deletions src/firebase_studio/migrate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { expect } from "chai";
import * as fs from "fs/promises";
import * as path from "path";
import * as sinon from "sinon";
import { migrate } from "./migrate";
import * as apphosting from "../gcp/apphosting";
import * as prompt from "../prompt";

describe("migrate", () => {
let sandbox: sinon.SinonSandbox;
const testRoot = "/test/root";

beforeEach(() => {
sandbox = sinon.createSandbox();
});

afterEach(() => {
sandbox.restore();
});

describe("migrate", () => {
it("should perform a full migration successfully", async () => {
// Stub global fetch
const fetchStub = sandbox.stub(global, "fetch");

// Mock GitHub API for skills listing
fetchStub
.withArgs("https://api.github.com/repos/firebase/agent-skills/contents/skills")
.resolves({

Check warning on line 29 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Response | undefined`
ok: true,
json: async () => [
{
name: "test-skill",
type: "dir",
url: "https://api.github.com/repos/firebase/agent-skills/contents/skills/test-skill",
},
],
} as any);

Check warning on line 38 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The repository style guide advises against using any as an escape hatch. This type assertion is used multiple times in this test file (e.g., lines 35, 42, 49, 52, 85), which bypasses type safety.

Please define proper interfaces/types or use type guards. For mocking fetch responses, you could use as unknown as Response or define a partial mock object that satisfies the Response interface.

References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)


// Mock GitHub API for specific skill content
fetchStub
.withArgs("https://api.github.com/repos/firebase/agent-skills/contents/skills/test-skill")
.resolves({

Check warning on line 43 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Response | undefined`
ok: true,
json: async () => [],
} as any);

Check warning on line 46 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

// Mock GitHub API for Genkit skill content
fetchStub
.withArgs(
"https://api.github.com/repos/genkit-ai/skills/contents/skills/developing-genkit-js?ref=main",
)
.resolves({

Check warning on line 53 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Response | undefined`
ok: true,
json: async () => [],
} as any);

Check warning on line 56 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

// Mock filesystem
sandbox.stub(fs, "readFile").callsFake(async (p: any) => {

Check warning on line 59 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
const pStr = p.toString();

Check warning on line 60 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 60 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

Check warning on line 60 in src/firebase_studio/migrate.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
if (pStr.endsWith("metadata.json")) {
return JSON.stringify({ projectId: "test-project", appName: "Test App" });
}
if (pStr.endsWith("readme_template.md")) {
return "# ${appName}\nExport Date: ${exportDate}\n${blueprintContent}";
}
if (pStr.endsWith("system_instructions_template.md")) {
return "Project: ${appName}";
}
if (pStr.endsWith("startup_workflow.md")) {
return "Step 1: Build";
}
if (pStr.endsWith(".firebaserc")) {
return JSON.stringify({ projects: { default: "test-project" } });
}
if (pStr.endsWith("blueprint.md")) {
return "# **App Name**: Test App\nSome blueprint content";
}
throw new Error(`Unexpected readFile: ${pStr}`);
});

sandbox.stub(fs, "writeFile").resolves();
sandbox.stub(fs, "mkdir").resolves();
sandbox.stub(fs, "unlink").resolves();
sandbox.stub(fs, "readdir").resolves([]);
sandbox.stub(fs, "access").rejects({ code: "ENOENT" });

// Mock App Hosting backends
sandbox.stub(apphosting, "listBackends").resolves({
backends: [
{
name: "projects/test-project/locations/us-central1/backends/studio",
uri: "example.com",
servingLocality: "GLOBAL_ACCESS",
labels: {},
createTime: "",
updateTime: "",
},
] as any[],
unreachable: [],
});

// Mock prompt
sandbox.stub(prompt, "confirm").resolves(false);

// Mock execSync
const childProcess = require("child_process");
sandbox.stub(childProcess, "execSync").returns(Buffer.from("1.0.0"));

await migrate(testRoot);

// Verify key files were written
const writeStub = fs.writeFile as sinon.SinonStub;

expect(writeStub.calledWith(path.join(testRoot, ".firebaserc"), sinon.match(/test-project/)))
.to.be.true;
expect(
writeStub.calledWith(
path.join(testRoot, "firebase.json"),
sinon.match(/"backendId": "studio"/),
),
).to.be.true;
expect(writeStub.calledWith(path.join(testRoot, "README.md"), sinon.match(/Test App/))).to.be
.true;
});
});
});
Loading
Loading