Skip to content

Commit 0106184

Browse files
authored
Add support for splitting server/client code (#115)
* Experimental code stripping * Early return guards * `@Server`, `@Client` method stripping * Handle void return functions diff * Add imports if required, support `--skipPackages` * Reduce duplicate code output * Auto strip simple `Game.Is*()` calls * Refactoring * Remove old files * Support guard throws * Throw on inverse guards * Nvm support inverse * Macro changes
1 parent eaffca7 commit 0106184

File tree

16 files changed

+819
-37
lines changed

16 files changed

+819
-37
lines changed

src/CLI/commands/build.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { findTsConfigPath, getPackageJson, getTsConfigProjectOptions } from "CLI/util/findTsConfigPath";
22
import { existsSync } from "fs";
3+
import path from "path";
34
import { buildTypes } from "Project/functions/buildTypes";
45
import { cleanup } from "Project/functions/cleanup";
5-
import { compileFiles } from "Project/functions/compileFiles";
6+
import { compileFiles, isPackage } from "Project/functions/compileFiles";
67
import { copyFiles } from "Project/functions/copyFiles";
78
import { copyNodeModules } from "Project/functions/copyInclude";
89
import { createPathTranslator } from "Project/functions/createPathTranslator";
@@ -19,7 +20,7 @@ import { ProjectOptions, TypeScriptConfiguration } from "Shared/types";
1920
import { getRootDirs } from "Shared/util/getRootDirs";
2021
import { hasErrors } from "Shared/util/hasErrors";
2122
import { AirshipBuildState, BUILD_FILE, EDITOR_FILE } from "TSTransformer";
22-
import ts, { TSConfig } from "typescript";
23+
import ts from "typescript";
2324
import yargs from "yargs";
2425

2526
interface BuildFlags {
@@ -43,12 +44,20 @@ export = ts.identity<yargs.CommandModule<{}, BuildFlags & Partial<ProjectOptions
4344
default: ".",
4445
describe: "project path",
4546
},
47+
skipPackages: {
48+
type: "boolean",
49+
alias: "P",
50+
describe: "Compile only the game's code",
51+
boolean: true,
52+
default: false,
53+
},
4654
json: {
4755
hidden: true,
4856
boolean: true,
4957
default: false,
5058
},
5159
publish: {
60+
alias: "D",
5261
hidden: true,
5362
boolean: true,
5463
default: false,
@@ -148,7 +157,15 @@ export = ts.identity<yargs.CommandModule<{}, BuildFlags & Partial<ProjectOptions
148157
pathTranslator,
149158
new Set(getRootDirs(program.getCompilerOptions(), data.projectOptions)),
150159
);
151-
const sourceFiles = getChangedSourceFiles(program);
160+
161+
let sourceFiles = getChangedSourceFiles(program);
162+
163+
if (projectOptions.skipPackages) {
164+
LogService.writeIfVerbose("Filtering out packages from compile");
165+
sourceFiles = sourceFiles.filter(
166+
sourceFile => !isPackage(path.relative(process.cwd(), sourceFile.fileName)),
167+
);
168+
}
152169

153170
if (projectOptions.json) {
154171
jsonReporter("startingCompile", { initial: true, count: sourceFiles.length });

src/Project/functions/compileFiles.ts

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { createTransformerWatcher } from "Project/transformers/createTransformer
1111
import { getPluginConfigs } from "Project/transformers/getPluginConfigs";
1212
import { getCustomPreEmitDiagnostics } from "Project/util/getCustomPreEmitDiagnostics";
1313
import { LogService } from "Shared/classes/LogService";
14-
import { PathTranslator } from "Shared/classes/PathTranslator";
14+
import { PathHint, PathTranslator } from "Shared/classes/PathTranslator";
1515
import { ProjectType } from "Shared/constants";
1616
import { warnings } from "Shared/diagnostics";
1717
import { AirshipBuildFile, ProjectData } from "Shared/types";
@@ -20,6 +20,7 @@ import { benchmarkIfVerbose } from "Shared/util/benchmark";
2020
import {
2121
AirshipBuildState,
2222
BUILD_FILE,
23+
CompliationContext,
2324
EDITOR_FILE,
2425
MultiTransformState,
2526
transformSourceFile,
@@ -46,6 +47,16 @@ function getReverseSymlinkMap(program: ts.Program) {
4647
return result;
4748
}
4849

50+
export function isPackage(relativePath: string) {
51+
return relativePath.startsWith("AirshipPackages" + path.sep);
52+
}
53+
54+
interface FileWriteEntry {
55+
sourceFile: ts.SourceFile;
56+
source: string;
57+
context?: CompliationContext;
58+
}
59+
4960
/**
5061
* 'transpiles' TypeScript project into a logically identical Luau project.
5162
*
@@ -58,7 +69,7 @@ export function compileFiles(
5869
buildState: AirshipBuildState,
5970
sourceFiles: Array<ts.SourceFile>,
6071
): ts.EmitResult {
61-
const asJson = data.projectOptions.json;
72+
const { json: asJson, publish: isPublish } = data.projectOptions;
6273
const compilerOptions = program.getCompilerOptions();
6374

6475
const watch = compilerOptions.watch ?? false;
@@ -92,7 +103,7 @@ export function compileFiles(
92103

93104
LogService.writeLineIfVerbose(`Now running TypeScript compiler:`);
94105

95-
const fileWriteQueue = new Array<{ sourceFile: ts.SourceFile; source: string }>();
106+
const fileWriteQueue = new Array<FileWriteEntry>();
96107
const fileMetadataWriteQueue = new Map<ts.SourceFile, string>();
97108

98109
const progressMaxLength = `${sourceFiles.length}/${sourceFiles.length}`.length;
@@ -175,11 +186,24 @@ export function compileFiles(
175186
});
176187
}
177188

189+
if (isPublish) {
190+
const sharedDirectory = pathTranslator.getOutDir(PathHint.Shared);
191+
if (fs.pathExistsSync(sharedDirectory)) fs.removeSync(sharedDirectory);
192+
193+
const clientDirectory = pathTranslator.getOutDir(PathHint.Client);
194+
if (fs.pathExistsSync(clientDirectory)) fs.removeSync(clientDirectory);
195+
196+
const serverDirectory = pathTranslator.getOutDir(PathHint.Server);
197+
if (fs.pathExistsSync(serverDirectory)) fs.removeSync(serverDirectory);
198+
}
199+
178200
for (let i = 0; i < sourceFiles.length; i++) {
179201
const sourceFile = proxyProgram.getSourceFile(sourceFiles[i].fileName);
180202
assert(sourceFile);
181203
const progress = `${i + 1}/${sourceFiles.length}`.padStart(progressMaxLength);
182-
benchmarkIfVerbose(`${progress} compile ${path.relative(process.cwd(), sourceFile.fileName)}`, () => {
204+
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
205+
206+
benchmarkIfVerbose(`${progress} compile ${relativePath}`, () => {
183207
DiagnosticService.addDiagnostics(ts.getPreEmitDiagnostics(proxyProgram, sourceFile));
184208
DiagnosticService.addDiagnostics(getCustomPreEmitDiagnostics(data, sourceFile));
185209
if (DiagnosticService.hasErrors()) return;
@@ -200,12 +224,37 @@ export function compileFiles(
200224
sourceFile,
201225
);
202226

203-
const luauAST = transformSourceFile(transformState, sourceFile);
204-
if (DiagnosticService.hasErrors()) return;
227+
if (isPublish && !isPackage(relativePath)) {
228+
const serverWriteEntry = transformState.useContext(CompliationContext.Server, context => {
229+
const luauAST = transformSourceFile(transformState, sourceFile);
230+
if (DiagnosticService.hasErrors()) return;
231+
const source = renderAST(luauAST);
232+
return { sourceFile, source, context } satisfies FileWriteEntry;
233+
});
234+
235+
if (DiagnosticService.hasErrors() || !serverWriteEntry) return;
205236

206-
const source = renderAST(luauAST);
237+
const clientWriteEntry = transformState.useContext(CompliationContext.Client, context => {
238+
const luauAST = transformSourceFile(transformState, sourceFile);
239+
if (DiagnosticService.hasErrors()) return;
240+
const source = renderAST(luauAST);
241+
return { sourceFile, source, context } satisfies FileWriteEntry;
242+
});
207243

208-
fileWriteQueue.push({ sourceFile, source });
244+
if (!clientWriteEntry) return;
245+
246+
if (clientWriteEntry.source !== serverWriteEntry.source) {
247+
fileWriteQueue.push(clientWriteEntry);
248+
fileWriteQueue.push(serverWriteEntry);
249+
} else {
250+
fileWriteQueue.push({ ...serverWriteEntry, context: CompliationContext.Shared });
251+
}
252+
} else {
253+
const luauAST = transformSourceFile(transformState, sourceFile);
254+
if (DiagnosticService.hasErrors()) return;
255+
const source = renderAST(luauAST);
256+
fileWriteQueue.push({ sourceFile, source });
257+
}
209258

210259
const airshipBehaviours = transformState.airshipBehaviours;
211260

@@ -231,7 +280,8 @@ export function compileFiles(
231280
const airshipBehaviourMetadata = behaviour.metadata;
232281

233282
if (airshipBehaviourMetadata) {
234-
assert(!fileMetadataWriteQueue.has(sourceFile));
283+
if (fileMetadataWriteQueue.has(sourceFile)) continue;
284+
235285
fileMetadataWriteQueue.set(sourceFile, JSON.stringify(airshipBehaviourMetadata, null, "\t"));
236286
}
237287

@@ -263,8 +313,23 @@ export function compileFiles(
263313
let writeCount = 0;
264314
let metadataCount = 0;
265315

266-
for (const { sourceFile, source } of fileWriteQueue) {
267-
const outPath = pathTranslator.getOutputPath(sourceFile.fileName);
316+
for (const { sourceFile, source, context } of fileWriteQueue) {
317+
let pathHint: PathHint | undefined;
318+
if (context !== undefined) {
319+
switch (context) {
320+
case CompliationContext.Client:
321+
pathHint = PathHint.Client;
322+
break;
323+
case CompliationContext.Server:
324+
pathHint = PathHint.Server;
325+
break;
326+
case CompliationContext.Shared:
327+
pathHint = PathHint.Shared;
328+
break;
329+
}
330+
}
331+
332+
const outPath = pathTranslator.getOutputPath(sourceFile.fileName, pathHint);
268333
const hasMetadata = fileMetadataWriteQueue.has(sourceFile);
269334
const metadataPathOutPath = outPath + ".json~";
270335

src/Shared/classes/PathTranslator.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ class PathInfo {
3737
}
3838
}
3939

40+
export enum PathHint {
41+
None,
42+
Server,
43+
Client,
44+
Shared,
45+
}
46+
47+
type PathInfoDelegate = (pathInfo: PathInfo) => string;
48+
4049
export class PathTranslator {
4150
constructor(
4251
public readonly rootDir: string,
@@ -46,7 +55,7 @@ export class PathTranslator {
4655
public readonly projectOptions: ProjectOptions,
4756
) {}
4857

49-
private makeRelativeFactory(from = this.rootDir, to = this.outDir) {
58+
private makeRelativeFactory(from = this.rootDir, to = this.outDir): PathInfoDelegate {
5059
return (pathInfo: PathInfo) => path.join(to, path.relative(from, pathInfo.join()));
5160
}
5261

@@ -63,14 +72,37 @@ export class PathTranslator {
6372
// return unityPath;
6473
// }
6574

75+
public getOutDir(pathHint = PathHint.None) {
76+
if (pathHint === PathHint.Server) {
77+
return path.resolve(this.outDir, "../dist/server");
78+
} else if (pathHint === PathHint.Client) {
79+
return path.resolve(this.outDir, "../dist/client");
80+
} else if (pathHint === PathHint.Shared) {
81+
return path.resolve(this.outDir, "../dist/shared");
82+
} else {
83+
return this.outDir;
84+
}
85+
}
86+
6687
/**
6788
* Maps an input path to an output path
6889
* - `.tsx?` && !`.d.tsx?` -> `.lua`
6990
* - `index` -> `init`
7091
* - `src/*` -> `out/*`
7192
*/
72-
public getOutputPath(filePath: string) {
73-
const makeRelative = this.makeRelativeFactory();
93+
public getOutputPath(filePath: string, pathHint = PathHint.None) {
94+
let makeRelative: PathInfoDelegate;
95+
96+
if (pathHint === PathHint.Server) {
97+
makeRelative = this.makeRelativeFactory(undefined, path.resolve(this.outDir, "../dist/server"));
98+
} else if (pathHint === PathHint.Client) {
99+
makeRelative = this.makeRelativeFactory(undefined, path.resolve(this.outDir, "../dist/client"));
100+
} else if (pathHint === PathHint.Shared) {
101+
makeRelative = this.makeRelativeFactory(undefined, path.resolve(this.outDir, "../dist/shared"));
102+
} else {
103+
makeRelative = this.makeRelativeFactory();
104+
}
105+
74106
filePath = path.join(filePath);
75107

76108
const pathInfo = PathInfo.from(filePath);

src/Shared/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const DEFAULT_PROJECT_OPTIONS: ProjectOptions = {
5252
usePolling: false,
5353
json: false,
5454
verbose: false,
55+
skipPackages: false,
5556
noInclude: false,
5657
logTruthyChanges: false,
5758
incremental: undefined,

src/Shared/diagnostics.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,31 +244,41 @@ export const errors = {
244244
];
245245
}),
246246

247+
invalidServerMacroUse: error("invalid"),
248+
247249
requiredComponentTypeParameterRequired: errorWithContext((className: string) => {
248250
return [
249251
`@RequireComponent decorator on class '${className}' requires at least one type parameter`,
250-
suggestion("Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour"),
252+
suggestion(
253+
"Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour",
254+
),
251255
];
252256
}),
253257

254258
requiredComponentArgumentRequired: errorWithContext((className: string) => {
255259
return [
256260
`@RequireComponent decorator on class '${className}' requires at least one type parameter`,
257-
suggestion("Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour"),
261+
suggestion(
262+
"Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour",
263+
),
258264
];
259265
}),
260266

261267
requiredComponentInvalidType: errorWithContext((className: string, typeName: string) => {
262268
return [
263269
`@RequireComponent decorator on class '${className}' received invalid component type '${typeName}'`,
264-
suggestion("Component type must be a Unity component or AirshipBehaviour. Try @RequireComponent<ValidComponentType>()"),
270+
suggestion(
271+
"Component type must be a Unity component or AirshipBehaviour. Try @RequireComponent<ValidComponentType>()",
272+
),
265273
];
266274
}),
267275

268276
requiredComponentInvalidArgument: errorWithContext((className: string, argumentType: string) => {
269277
return [
270278
`@RequireComponent decorator on class '${className}' received invalid argument of type '${argumentType}'`,
271-
suggestion("Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour"),
279+
suggestion(
280+
"Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour",
281+
),
272282
];
273283
}),
274284

@@ -308,6 +318,13 @@ export const errors = {
308318
];
309319
}),
310320

321+
directiveServerInvalid: error(
322+
"$SERVER is a directive macro and can only be used as a top-level condition in an if statement - e.g. if ($SERVER)",
323+
),
324+
directiveClientInvalid: error(
325+
"$CLIENT is a directive macro and can only be used as a top-level condition in an if statement - e.g. if ($CLIENT)",
326+
),
327+
311328
flameworkIdNoType: errorWithContext(() => {
312329
return [
313330
"Macro Flamework.id<T> requires a type argument at T",

src/Shared/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface ProjectOptions {
2828
watch: boolean;
2929
json: boolean;
3030
publish: boolean;
31+
skipPackages: boolean;
3132
writeOnlyChanged: boolean;
3233
optimizedLoops: boolean;
3334
allowCommentDirectives: boolean;

0 commit comments

Comments
 (0)