Skip to content

Commit 2a0b75d

Browse files
committed
fix(windows): stage C# sources as an exact mirror and don't ship them
- App_Resources/Windows and each plugin's platforms/windows are restaged from scratch, and staged folders of uninstalled plugins are removed: C# sources there are compiled into the app, so deleted files must not keep compiling. - Generated plugin.props/plugin.targets no longer copy C# sources and MSBuild files to the app output. - Tests for staging plugin and App_Resources C# sources.
1 parent 3c839a5 commit 2a0b75d

2 files changed

Lines changed: 210 additions & 2 deletions

File tree

‎lib/services/windows-project-service.ts‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,24 @@ export class WindowsProjectService
621621
}
622622
}
623623

624+
// Drop the staged files of plugins that are no longer installed (their C# sources would
625+
// otherwise still be compiled into the app). Scoped plugins stage under plugins/@scope/name.
626+
const installedNames = new Set(installedPlugins.map((p: IPluginData) => p.name));
627+
for (const entry of fs.readdirSync(pluginsDir, { withFileTypes: true })) {
628+
if (!entry.isDirectory()) continue;
629+
const names = entry.name.startsWith("@")
630+
? fs
631+
.readdirSync(path.join(pluginsDir, entry.name), { withFileTypes: true })
632+
.filter((e) => e.isDirectory())
633+
.map((e) => `${entry.name}/${e.name}`)
634+
: [entry.name];
635+
for (const name of names) {
636+
if (!installedNames.has(name)) {
637+
this.$fs.deleteDirectory(path.join(pluginsDir, ...name.split("/")));
638+
}
639+
}
640+
}
641+
624642
// Write aggregate imports that the project csproj imports via "plugins\Plugins.props"
625643
const aggregatePropsPath = path.join(pluginsDir, "Plugins.props");
626644
const aggregateTargetsPath = path.join(pluginsDir, "Plugins.targets");
@@ -700,12 +718,17 @@ export class WindowsProjectService
700718
projectData.projectName,
701719
);
702720

703-
// Copy App_Resources/Windows -> platforms/windows/<Project>/App_Resources/Windows
721+
// Copy App_Resources/Windows -> platforms/windows/<Project>/App_Resources/Windows. The copy is
722+
// an exact mirror: C# sources under it are compiled into the app, so a file deleted from
723+
// App_Resources must not linger here.
704724
const destAppResourcesWindows = path.join(
705725
platformAppDir,
706726
"App_Resources",
707727
"Windows",
708728
);
729+
if (this.$fs.exists(destAppResourcesWindows)) {
730+
this.$fs.deleteDirectory(destAppResourcesWindows);
731+
}
709732
this.$fs.ensureDirectoryExists(destAppResourcesWindows);
710733

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

9871015
// recursively copy native files (exclude JS/TS/JSON)
@@ -1028,14 +1056,19 @@ export class WindowsProjectService
10281056
);
10291057
}
10301058

1059+
// Build inputs rather than files the app ships: the plugin's C# sources (staged here,
1060+
// under the app project, so the SDK's default Compile glob builds them into the app the way
1061+
// a plugin's Java/Kotlin or Objective-C/Swift sources are on Android/iOS) and MSBuild files.
1062+
const buildInputExtensions = [".cs", ".csproj", ".props", ".targets"];
10311063
const collectStagedFiles = (root: string): string[] => {
10321064
const out: string[] = [];
10331065
if (!this.$fs.exists(root)) return out;
10341066
const walk = (dir: string) => {
10351067
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
10361068
const full = path.join(dir, e.name);
10371069
if (e.isDirectory()) walk(full);
1038-
else out.push(path.relative(root, full).split(path.sep).join("\\"));
1070+
else if (!buildInputExtensions.includes(path.extname(e.name).toLowerCase()))
1071+
out.push(path.relative(root, full).split(path.sep).join("\\"));
10391072
}
10401073
};
10411074
walk(root);
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import * as fs from "fs";
2+
import * as os from "os";
3+
import * as path from "path";
4+
import { assert } from "chai";
5+
import { WindowsProjectService } from "../../lib/services/windows-project-service";
6+
import { injector } from "../../lib/common/yok";
7+
import * as stubs from "../stubs";
8+
9+
// Native sources on Windows: C# files under App_Resources/Windows and a plugin's
10+
// platforms/windows are staged into the platform project, where the template's SDK-style csproj
11+
// compiles them into the app (the counterpart of App_Resources/Android/src, App_Resources/iOS/src
12+
// and plugins' platforms/android|ios sources).
13+
14+
const projectName = "TestApp";
15+
16+
// The subset of IFileSystem the Windows project service uses, backed by the real file system.
17+
const realFs = <any>{
18+
exists: (p: string) => fs.existsSync(p),
19+
ensureDirectoryExists: (p: string) => fs.mkdirSync(p, { recursive: true }),
20+
copyFile: (from: string, to: string) => {
21+
fs.mkdirSync(path.dirname(to), { recursive: true });
22+
fs.copyFileSync(from, to);
23+
},
24+
writeFile: (p: string, content: string) => {
25+
fs.mkdirSync(path.dirname(p), { recursive: true });
26+
fs.writeFileSync(p, content);
27+
},
28+
writeJson: (p: string, value: any) => fs.writeFileSync(p, JSON.stringify(value)),
29+
deleteDirectory: (p: string) => fs.rmSync(p, { recursive: true, force: true }),
30+
};
31+
32+
function write(file: string, content = "") {
33+
fs.mkdirSync(path.dirname(file), { recursive: true });
34+
fs.writeFileSync(file, content);
35+
}
36+
37+
describe("WindowsProjectService native sources", () => {
38+
let root: string;
39+
let projectDir: string;
40+
let projectData: any;
41+
let service: WindowsProjectService;
42+
let appProjectDir: string;
43+
44+
beforeEach(() => {
45+
root = fs.mkdtempSync(path.join(os.tmpdir(), "ns-windows-project-"));
46+
projectDir = path.join(root, "app");
47+
fs.mkdirSync(projectDir);
48+
projectData = {
49+
projectDir,
50+
projectName,
51+
projectId: "org.nativescript.testapp",
52+
projectIdentifiers: { windows: "org.nativescript.testapp" },
53+
platformsDir: path.join(projectDir, "platforms"),
54+
nsConfig: {},
55+
};
56+
const projectDataService = <any>{
57+
getRuntimePackage: () => ({ name: "@nativescript/windows", version: "0.1.0" }),
58+
};
59+
service = new WindowsProjectService(
60+
realFs,
61+
projectDataService,
62+
<any>{},
63+
new stubs.LoggerStub(),
64+
<any>{},
65+
<any>{},
66+
);
67+
appProjectDir = path.join(projectData.platformsDir, "windows", projectName);
68+
});
69+
70+
afterEach(() => {
71+
fs.rmSync(root, { recursive: true, force: true });
72+
});
73+
74+
function plugin(name: string, files: Record<string, string>) {
75+
const fullPath = path.join(projectDir, "node_modules", ...name.split("/"));
76+
for (const [rel, content] of Object.entries(files)) {
77+
write(path.join(fullPath, "platforms", "windows", rel), content);
78+
}
79+
return <any>{ name, fullPath, nativescript: {} };
80+
}
81+
82+
describe("preparePluginNativeCode", () => {
83+
it("stages C# sources into the app project, where the csproj compiles them", async () => {
84+
const p = plugin("test-plugin", {
85+
"src/Greeter.cs": "namespace TestPlugin { public class Greeter {} }",
86+
"src/index.js": "module.exports = {};",
87+
"lib/Native.dll": "dll",
88+
});
89+
await service.preparePluginNativeCode(p, projectData);
90+
91+
const stage = path.join(appProjectDir, "plugins", "test-plugin");
92+
assert.isTrue(fs.existsSync(path.join(stage, "src", "Greeter.cs")));
93+
assert.isTrue(fs.existsSync(path.join(stage, "lib", "Native.dll")));
94+
assert.isFalse(fs.existsSync(path.join(stage, "src", "index.js")));
95+
});
96+
97+
it("does not ship C# sources with the app", async () => {
98+
const p = plugin("test-plugin", {
99+
"src/Greeter.cs": "namespace TestPlugin { public class Greeter {} }",
100+
"lib/Native.dll": "dll",
101+
});
102+
await service.preparePluginNativeCode(p, projectData);
103+
104+
const stage = path.join(appProjectDir, "plugins", "test-plugin");
105+
const props = fs.readFileSync(path.join(stage, "plugin.props"), "utf8");
106+
const targets = fs.readFileSync(path.join(stage, "plugin.targets"), "utf8");
107+
assert.include(props, "lib\\Native.dll");
108+
assert.notInclude(props, "Greeter.cs");
109+
assert.include(targets, "lib\\Native.dll");
110+
assert.notInclude(targets, "Greeter.cs");
111+
});
112+
113+
it("stages scoped plugins under plugins/@scope/name", async () => {
114+
const p = plugin("@acme/native", { "src/Acme.cs": "namespace Acme { public class A {} }" });
115+
await service.preparePluginNativeCode(p, projectData);
116+
assert.isTrue(fs.existsSync(path.join(appProjectDir, "plugins", "@acme", "native", "src", "Acme.cs")));
117+
});
118+
119+
it("drops sources a newer plugin version no longer has", async () => {
120+
const p = plugin("test-plugin", { "src/Old.cs": "class Old {}", "src/Kept.cs": "class Kept {}" });
121+
await service.preparePluginNativeCode(p, projectData);
122+
fs.rmSync(path.join(p.fullPath, "platforms", "windows", "src", "Old.cs"));
123+
await service.preparePluginNativeCode(p, projectData);
124+
125+
const stage = path.join(appProjectDir, "plugins", "test-plugin", "src");
126+
assert.isFalse(fs.existsSync(path.join(stage, "Old.cs")));
127+
assert.isTrue(fs.existsSync(path.join(stage, "Kept.cs")));
128+
});
129+
});
130+
131+
describe("prepareAppResources", () => {
132+
it("mirrors App_Resources/Windows, C# sources included", () => {
133+
write(path.join(projectDir, "App_Resources", "Windows", "src", "Fixtures.cs"), "class Fixtures {}");
134+
write(path.join(projectDir, "App_Resources", "Windows", "app.csproj"), "<Project />");
135+
service.prepareAppResources(projectData);
136+
137+
const dest = path.join(appProjectDir, "App_Resources", "Windows");
138+
assert.isTrue(fs.existsSync(path.join(dest, "src", "Fixtures.cs")));
139+
assert.isTrue(fs.existsSync(path.join(dest, "app.csproj")));
140+
});
141+
142+
it("removes C# sources deleted from App_Resources/Windows", () => {
143+
const src = path.join(projectDir, "App_Resources", "Windows", "src");
144+
write(path.join(src, "Gone.cs"), "class Gone {}");
145+
write(path.join(src, "Stays.cs"), "class Stays {}");
146+
service.prepareAppResources(projectData);
147+
fs.rmSync(path.join(src, "Gone.cs"));
148+
service.prepareAppResources(projectData);
149+
150+
const dest = path.join(appProjectDir, "App_Resources", "Windows", "src");
151+
assert.isFalse(fs.existsSync(path.join(dest, "Gone.cs")));
152+
assert.isTrue(fs.existsSync(path.join(dest, "Stays.cs")));
153+
});
154+
});
155+
156+
describe("prepareProject", () => {
157+
it("removes the staged sources of uninstalled plugins", async () => {
158+
const kept = plugin("kept-plugin", { "src/Kept.cs": "class Kept {}" });
159+
const scoped = plugin("@acme/kept", { "src/Acme.cs": "class Acme {}" });
160+
write(path.join(appProjectDir, "plugins", "removed-plugin", "src", "Removed.cs"), "class Removed {}");
161+
write(path.join(appProjectDir, "plugins", "@acme", "removed", "src", "Gone.cs"), "class Gone {}");
162+
163+
injector.register("pluginsService", { getAllInstalledPlugins: async () => [kept, scoped] });
164+
await service.prepareProject(projectData, {});
165+
166+
const plugins = path.join(appProjectDir, "plugins");
167+
assert.isTrue(fs.existsSync(path.join(plugins, "kept-plugin", "src", "Kept.cs")));
168+
assert.isTrue(fs.existsSync(path.join(plugins, "@acme", "kept", "src", "Acme.cs")));
169+
assert.isFalse(fs.existsSync(path.join(plugins, "removed-plugin")));
170+
assert.isFalse(fs.existsSync(path.join(plugins, "@acme", "removed")));
171+
const aggregate = fs.readFileSync(path.join(plugins, "Plugins.props"), "utf8");
172+
assert.include(aggregate, "kept-plugin\\plugin.props");
173+
});
174+
});
175+
});

0 commit comments

Comments
 (0)