Skip to content
Open
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
37 changes: 35 additions & 2 deletions lib/services/windows-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,24 @@ export class WindowsProjectService
}
}

// Drop the staged files of plugins that are no longer installed (their C# sources would
// otherwise still be compiled into the app). Scoped plugins stage under plugins/@scope/name.
const installedNames = new Set(installedPlugins.map((p: IPluginData) => p.name));
for (const entry of fs.readdirSync(pluginsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const names = entry.name.startsWith("@")
? fs
.readdirSync(path.join(pluginsDir, entry.name), { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => `${entry.name}/${e.name}`)
: [entry.name];
for (const name of names) {
if (!installedNames.has(name)) {
this.$fs.deleteDirectory(path.join(pluginsDir, ...name.split("/")));
}
}
}

// Write aggregate imports that the project csproj imports via "plugins\Plugins.props"
const aggregatePropsPath = path.join(pluginsDir, "Plugins.props");
const aggregateTargetsPath = path.join(pluginsDir, "Plugins.targets");
Expand Down Expand Up @@ -700,12 +718,17 @@ export class WindowsProjectService
projectData.projectName,
);

// Copy App_Resources/Windows -> platforms/windows/<Project>/App_Resources/Windows
// Copy App_Resources/Windows -> platforms/windows/<Project>/App_Resources/Windows. The copy is
// an exact mirror: C# sources under it are compiled into the app, so a file deleted from
// App_Resources must not linger here.
const destAppResourcesWindows = path.join(
platformAppDir,
"App_Resources",
"Windows",
);
if (this.$fs.exists(destAppResourcesWindows)) {
this.$fs.deleteDirectory(destAppResourcesWindows);
}
this.$fs.ensureDirectoryExists(destAppResourcesWindows);

const copyRecursive = (srcDir: string, destDir: string) => {
Expand Down Expand Up @@ -982,6 +1005,11 @@ export class WindowsProjectService
projectData.projectName,
);
const pluginStageDir = path.join(appProjectDir, "plugins", pluginData.name);
// Restage from scratch: the plugin's C# sources are compiled into the app, so files a newer
// plugin version dropped must not linger.
if (this.$fs.exists(pluginStageDir)) {
this.$fs.deleteDirectory(pluginStageDir);
}
this.$fs.ensureDirectoryExists(pluginStageDir);

// recursively copy native files (exclude JS/TS/JSON)
Expand Down Expand Up @@ -1028,14 +1056,19 @@ export class WindowsProjectService
);
}

// Build inputs rather than files the app ships: the plugin's C# sources (staged here,
// under the app project, so the SDK's default Compile glob builds them into the app the way
// a plugin's Java/Kotlin or Objective-C/Swift sources are on Android/iOS) and MSBuild files.
const buildInputExtensions = [".cs", ".csproj", ".props", ".targets"];
const collectStagedFiles = (root: string): string[] => {
const out: string[] = [];
if (!this.$fs.exists(root)) return out;
const walk = (dir: string) => {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) walk(full);
else out.push(path.relative(root, full).split(path.sep).join("\\"));
else if (!buildInputExtensions.includes(path.extname(e.name).toLowerCase()))
out.push(path.relative(root, full).split(path.sep).join("\\"));
}
};
walk(root);
Expand Down
175 changes: 175 additions & 0 deletions test/services/windows-project-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { assert } from "chai";
import { WindowsProjectService } from "../../lib/services/windows-project-service";
import { injector } from "../../lib/common/yok";
import * as stubs from "../stubs";

// Native sources on Windows: C# files under App_Resources/Windows and a plugin's
// platforms/windows are staged into the platform project, where the template's SDK-style csproj
// compiles them into the app (the counterpart of App_Resources/Android/src, App_Resources/iOS/src
// and plugins' platforms/android|ios sources).

const projectName = "TestApp";

// The subset of IFileSystem the Windows project service uses, backed by the real file system.
const realFs = <any>{
exists: (p: string) => fs.existsSync(p),
ensureDirectoryExists: (p: string) => fs.mkdirSync(p, { recursive: true }),
copyFile: (from: string, to: string) => {
fs.mkdirSync(path.dirname(to), { recursive: true });
fs.copyFileSync(from, to);
},
writeFile: (p: string, content: string) => {
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, content);
},
writeJson: (p: string, value: any) => fs.writeFileSync(p, JSON.stringify(value)),
deleteDirectory: (p: string) => fs.rmSync(p, { recursive: true, force: true }),
};

function write(file: string, content = "") {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
}

describe("WindowsProjectService native sources", () => {
let root: string;
let projectDir: string;
let projectData: any;
let service: WindowsProjectService;
let appProjectDir: string;

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), "ns-windows-project-"));
projectDir = path.join(root, "app");
fs.mkdirSync(projectDir);
projectData = {
projectDir,
projectName,
projectId: "org.nativescript.testapp",
projectIdentifiers: { windows: "org.nativescript.testapp" },
platformsDir: path.join(projectDir, "platforms"),
nsConfig: {},
};
const projectDataService = <any>{
getRuntimePackage: () => ({ name: "@nativescript/windows", version: "0.1.0" }),
};
service = new WindowsProjectService(
realFs,
projectDataService,
<any>{},
new stubs.LoggerStub(),
<any>{},
<any>{},
);
appProjectDir = path.join(projectData.platformsDir, "windows", projectName);
});

afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});

function plugin(name: string, files: Record<string, string>) {
const fullPath = path.join(projectDir, "node_modules", ...name.split("/"));
for (const [rel, content] of Object.entries(files)) {
write(path.join(fullPath, "platforms", "windows", rel), content);
}
return <any>{ name, fullPath, nativescript: {} };
}

describe("preparePluginNativeCode", () => {
it("stages C# sources into the app project, where the csproj compiles them", async () => {
const p = plugin("test-plugin", {
"src/Greeter.cs": "namespace TestPlugin { public class Greeter {} }",
"src/index.js": "module.exports = {};",
"lib/Native.dll": "dll",
});
await service.preparePluginNativeCode(p, projectData);

const stage = path.join(appProjectDir, "plugins", "test-plugin");
assert.isTrue(fs.existsSync(path.join(stage, "src", "Greeter.cs")));
assert.isTrue(fs.existsSync(path.join(stage, "lib", "Native.dll")));
assert.isFalse(fs.existsSync(path.join(stage, "src", "index.js")));
});

it("does not ship C# sources with the app", async () => {
const p = plugin("test-plugin", {
"src/Greeter.cs": "namespace TestPlugin { public class Greeter {} }",
"lib/Native.dll": "dll",
});
await service.preparePluginNativeCode(p, projectData);

const stage = path.join(appProjectDir, "plugins", "test-plugin");
const props = fs.readFileSync(path.join(stage, "plugin.props"), "utf8");
const targets = fs.readFileSync(path.join(stage, "plugin.targets"), "utf8");
assert.include(props, "lib\\Native.dll");
assert.notInclude(props, "Greeter.cs");
assert.include(targets, "lib\\Native.dll");
assert.notInclude(targets, "Greeter.cs");
});

it("stages scoped plugins under plugins/@scope/name", async () => {
const p = plugin("@acme/native", { "src/Acme.cs": "namespace Acme { public class A {} }" });
await service.preparePluginNativeCode(p, projectData);
assert.isTrue(fs.existsSync(path.join(appProjectDir, "plugins", "@acme", "native", "src", "Acme.cs")));
});

it("drops sources a newer plugin version no longer has", async () => {
const p = plugin("test-plugin", { "src/Old.cs": "class Old {}", "src/Kept.cs": "class Kept {}" });
await service.preparePluginNativeCode(p, projectData);
fs.rmSync(path.join(p.fullPath, "platforms", "windows", "src", "Old.cs"));
await service.preparePluginNativeCode(p, projectData);

const stage = path.join(appProjectDir, "plugins", "test-plugin", "src");
assert.isFalse(fs.existsSync(path.join(stage, "Old.cs")));
assert.isTrue(fs.existsSync(path.join(stage, "Kept.cs")));
});
});

describe("prepareAppResources", () => {
it("mirrors App_Resources/Windows, C# sources included", () => {
write(path.join(projectDir, "App_Resources", "Windows", "src", "Fixtures.cs"), "class Fixtures {}");
write(path.join(projectDir, "App_Resources", "Windows", "app.csproj"), "<Project />");
service.prepareAppResources(projectData);

const dest = path.join(appProjectDir, "App_Resources", "Windows");
assert.isTrue(fs.existsSync(path.join(dest, "src", "Fixtures.cs")));
assert.isTrue(fs.existsSync(path.join(dest, "app.csproj")));
});

it("removes C# sources deleted from App_Resources/Windows", () => {
const src = path.join(projectDir, "App_Resources", "Windows", "src");
write(path.join(src, "Gone.cs"), "class Gone {}");
write(path.join(src, "Stays.cs"), "class Stays {}");
service.prepareAppResources(projectData);
fs.rmSync(path.join(src, "Gone.cs"));
service.prepareAppResources(projectData);

const dest = path.join(appProjectDir, "App_Resources", "Windows", "src");
assert.isFalse(fs.existsSync(path.join(dest, "Gone.cs")));
assert.isTrue(fs.existsSync(path.join(dest, "Stays.cs")));
});
});

describe("prepareProject", () => {
it("removes the staged sources of uninstalled plugins", async () => {
const kept = plugin("kept-plugin", { "src/Kept.cs": "class Kept {}" });
const scoped = plugin("@acme/kept", { "src/Acme.cs": "class Acme {}" });
write(path.join(appProjectDir, "plugins", "removed-plugin", "src", "Removed.cs"), "class Removed {}");
write(path.join(appProjectDir, "plugins", "@acme", "removed", "src", "Gone.cs"), "class Gone {}");

injector.register("pluginsService", { getAllInstalledPlugins: async () => [kept, scoped] });
await service.prepareProject(projectData, {});

const plugins = path.join(appProjectDir, "plugins");
assert.isTrue(fs.existsSync(path.join(plugins, "kept-plugin", "src", "Kept.cs")));
assert.isTrue(fs.existsSync(path.join(plugins, "@acme", "kept", "src", "Acme.cs")));
assert.isFalse(fs.existsSync(path.join(plugins, "removed-plugin")));
assert.isFalse(fs.existsSync(path.join(plugins, "@acme", "removed")));
const aggregate = fs.readFileSync(path.join(plugins, "Plugins.props"), "utf8");
assert.include(aggregate, "kept-plugin\\plugin.props");
});
});
});
Loading