Skip to content

Commit cf460e5

Browse files
committed
basic template writing and .gitignore
1 parent b36fe16 commit cf460e5

File tree

13 files changed

+202
-23
lines changed

13 files changed

+202
-23
lines changed

lib/file/template.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import cxxUmbrella from "../../templates/cxx/umbrella.h.mustache";
2+
import executableMainSwift from "../../templates/executable/main.swift.mustache";
3+
import gitignore from "../../templates/git/.gitignore.mustache";
4+
import libraryMainSwift from "../../templates/library/main.swift.mustache";
5+
import testCaseSwift from "../../templates/test/testCase.swift.mustache";
6+
7+
import Mustache from "mustache";
8+
9+
export type Template =
10+
| { template: "gitignore"; props: {} } // Doesn't need to be run through Mustache but this makes this easier
11+
| { template: "cxx/umbrella"; props: { targetName: string } }
12+
| { template: "swift/executable/main"; props: { targetName: string } }
13+
| { template: "swift/library/main"; props: { targetName: string } }
14+
| { template: "swift/plugin/main"; props: { targetName: string } }
15+
| { template: "swift/supporting/main"; props: { targetName: string } }
16+
| { template: "swift/test/testCase"; props: { targetName: string } };
17+
18+
// Use imports so we don't have to mess around with files on disk, and they all get bundled into the distribution.
19+
const getTemplateContents = (template: Template) => {
20+
switch (template.template) {
21+
case "gitignore":
22+
return gitignore;
23+
case "cxx/umbrella":
24+
return cxxUmbrella;
25+
case "swift/executable/main":
26+
return executableMainSwift;
27+
case "swift/library/main":
28+
return libraryMainSwift;
29+
case "swift/plugin/main":
30+
return "";
31+
case "swift/supporting/main":
32+
return "";
33+
case "swift/test/testCase":
34+
return testCaseSwift;
35+
}
36+
};
37+
38+
export const evaluateTemplate = (template: Template) => {
39+
const contents = getTemplateContents(template);
40+
const { props } = template;
41+
return Mustache.render(contents, props);
42+
};

lib/package/create.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,32 @@ import path from "path";
44
import { packageFile } from ".";
55
import { type CliOptions } from "../cli";
66
import { type Config } from "../config";
7+
import { evaluateTemplate } from "../file/template";
78
import { formatDirectoryTree, type Node } from "../format/directory";
89
import { writeSwiftFile } from "../swift/file";
910
import { canWrite, exists } from "../util/fs";
1011
import { makePackageDescription } from "./description";
1112
import { type Target } from "./target";
1213

13-
const writeTarget = async (config: Config, target: Target) => {
14-
fs.promises.mkdir(path.join(config.projectDir, "Sources", target.name), {
15-
recursive: true,
16-
});
14+
const writeTarget = async (config: Config, target: Target, cli: CliOptions) => {
15+
const targetBase = path.join(config.projectDir, "Sources", target.name);
16+
17+
if (!cli.dryRun) {
18+
fs.promises.mkdir(targetBase, {
19+
recursive: true,
20+
});
21+
}
22+
23+
await Promise.all(
24+
target.files.map(async (file) => {
25+
const contents = evaluateTemplate(file.template);
26+
const filePath = path.join(targetBase, file.path);
27+
const { dir } = path.parse(filePath);
28+
29+
await fs.promises.mkdir(dir, { recursive: true });
30+
return await fs.promises.writeFile(filePath, contents);
31+
})
32+
);
1733
};
1834

1935
const relativePath = (pathInTarget: string, target: Target) => {
@@ -27,6 +43,7 @@ const packageFiles = (config: Config, targets: Target[]) => {
2743
target.files.map((file) => relativePath(file.path, target))
2844
),
2945
"Package.swift",
46+
".gitignore",
3047
],
3148
targetPaths: targets.map((target) => relativePath("/", target)),
3249
};
@@ -118,9 +135,15 @@ export const createPackage = async (props: {
118135
path.join(config.projectDir, "Package.swift"),
119136
writeSwiftFile(file)
120137
);
121-
await Promise.all(targets.map((target) => writeTarget(config, target)));
138+
139+
await fs.promises.writeFile(
140+
path.join(config.projectDir, ".gitignore"),
141+
evaluateTemplate({ template: "gitignore", props: {} })
142+
);
122143
}
123144

145+
await Promise.all(targets.map((target) => writeTarget(config, target, cli)));
146+
124147
console.log(
125148
formatDirectoryTree(
126149
makeDirectoryStructure(config, packageFiles(config, targets))

lib/package/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const productValue = (
2424
}
2525
};
2626

27+
/**
28+
* General purpose AST for Package.swift. This should be easy to modify for different versions
29+
* of Swift or if this format changes.
30+
*/
2731
export const packageFile = (description: PackageDescription): SwiftFile => {
2832
return {
2933
headerComment: `swift-tools-version: ${description.toolsVersion}`,

lib/package/target.ts

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import path from "path";
22
import { Config, LanguageOptions } from "../config";
3+
import { type Template } from "../file/template";
4+
import { ProductType } from "../swift/types";
35

46
export type TargetFile = {
57
path: string; // Relative path of the file within the target's folder
6-
template: string; // Relative path to the template in `templates`
8+
template: Template;
79
};
810
type TargetRole = "main" | "supporting" | "test";
911
export type TargetLanguage = "swift" | "cfamily";
@@ -41,19 +43,35 @@ const makeFiles = (
4143
language: TargetLanguage,
4244
config: Config,
4345
templates: {
44-
swiftTemplate: string;
45-
cxxTemplates: { header: string; implementation: string };
46+
swiftTemplate: Template;
47+
cxxTemplates: { header?: Template; implementation?: Template };
4648
}
4749
) => {
4850
const { swiftTemplate, cxxTemplates } = templates;
4951
switch (language) {
5052
case "cfamily": {
5153
const { header, implementation } = cxxTemplates;
5254
const headerPath = cIncludePath(config.language) || "include";
53-
return [
54-
{ path: path.join(headerPath, `${name}.h`), template: header },
55-
{ path: `${name}.m`, template: implementation },
56-
];
55+
const headerFile =
56+
header != null
57+
? { path: path.join(headerPath, `${name}.h`), template: header }
58+
: null;
59+
const implementationFile =
60+
implementation != null
61+
? {
62+
path: path.join(headerPath, `${name}.m`),
63+
template: implementation,
64+
}
65+
: null;
66+
67+
var files: TargetFile[] = [];
68+
if (headerFile != null) {
69+
files.push(headerFile);
70+
}
71+
if (implementationFile != null) {
72+
files.push(implementationFile);
73+
}
74+
return files;
5775
}
5876
case "swift":
5977
return [{ path: `${name}.swift`, template: swiftTemplate }];
@@ -62,6 +80,7 @@ const makeFiles = (
6280

6381
const makeMainTarget = (
6482
mainName: string,
83+
productType: ProductType,
6584
language: TargetLanguage,
6685
dependencies: Target[],
6786
config: Config
@@ -71,8 +90,11 @@ const makeMainTarget = (
7190
language,
7291
dependencies,
7392
files: makeFiles(mainName, language, config, {
74-
swiftTemplate: "",
75-
cxxTemplates: { header: "", implementation: "" },
93+
swiftTemplate: {
94+
template: `swift/${productType}/main`,
95+
props: { targetName: mainName },
96+
},
97+
cxxTemplates: {},
7698
}),
7799
});
78100

@@ -87,8 +109,11 @@ const makeSupportingTarget = (
87109
language,
88110
dependencies,
89111
files: makeFiles(name, language, config, {
90-
swiftTemplate: "",
91-
cxxTemplates: { header: "", implementation: "" },
112+
swiftTemplate: {
113+
template: "swift/supporting/main",
114+
props: { targetName: name },
115+
},
116+
cxxTemplates: {},
92117
}),
93118
});
94119

@@ -98,25 +123,29 @@ const makeTestTarget = (mainTarget: Target, config: Config): Target => ({
98123
language: mainTarget.language,
99124
dependencies: [mainTarget],
100125
files: makeFiles(`${mainTarget.name}Tests`, mainTarget.language, config, {
101-
swiftTemplate: "",
102-
cxxTemplates: { header: "", implementation: "" },
126+
swiftTemplate: {
127+
template: "swift/test/testCase",
128+
props: { targetName: mainTarget.name },
129+
},
130+
cxxTemplates: {},
103131
}),
104132
});
105133

106134
/* Public */
107135

108136
export const makeTargets = (config: Config): Target[] => {
109137
const targets: Target[] = [];
138+
const { productType, language } = config;
110139

111140
const mainName = mainTargetName(config);
112141
let mainTarget: Target;
113-
switch (config.language.type) {
142+
switch (language.type) {
114143
case "cfamily":
115-
mainTarget = makeMainTarget(mainName, "cfamily", [], config);
144+
mainTarget = makeMainTarget(mainName, productType, "cfamily", [], config);
116145
targets.push(mainTarget);
117146
break;
118147
case "swift":
119-
mainTarget = makeMainTarget(mainName, "swift", [], config);
148+
mainTarget = makeMainTarget(mainName, productType, "swift", [], config);
120149
targets.push(mainTarget);
121150
break;
122151
case "mixed":
@@ -126,7 +155,13 @@ export const makeTargets = (config: Config): Target[] => {
126155
[],
127156
config
128157
);
129-
mainTarget = makeMainTarget(mainName, "swift", [objCxx], config);
158+
mainTarget = makeMainTarget(
159+
mainName,
160+
productType,
161+
"swift",
162+
[objCxx],
163+
config
164+
);
130165
targets.push(mainTarget);
131166
targets.push(objCxx);
132167
break;

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "An interactive and more fully-featured alternative to swift package init",
55
"main": "index.js",
66
"scripts": {
7-
"dev": "esbuild index.ts --bundle --outdir=dist --platform=node --watch",
7+
"dev": "esbuild index.ts --bundle --outdir=dist --platform=node --watch --loader:.mustache=text",
88
"test": "jest",
99
"unit": "NODE_OPTIONS='--experimental-vm-modules' jest \"test/unit\""
1010
},
@@ -26,6 +26,7 @@
2626
"devDependencies": {
2727
"@types/jest": "^29.5.1",
2828
"@types/lodash": "^4.14.194",
29+
"@types/mustache": "^4.2.2",
2930
"@types/node": "^18.16.2",
3031
"@types/prompts": "2.0.1",
3132
"@types/tmp": "^0.2.3",

templates/cxx/umbrella.h.mustache

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//
2+
// {{targetName}}.h
3+
//
4+
5+
// This is your target's umbrella header. Import public-facing headers here.
6+
//
7+
// #import "MyHeader.h"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@main
2+
public struct {{targetName}} {
3+
public static func main() {
4+
print("Hello from Swift 🤘")
5+
exit(0)
6+
}
7+
}

templates/git/.gitignore.mustache

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/config/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc
10+

templates/library/main.swift.mustache

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public struct {{targetName}} {
2+
public init() {}
3+
}

templates/test/TestCase.m.mustache

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#import <XCTest/XCTest.h>
2+
3+
@interface {{targetName}}Tests : XCTestCase
4+
@end
5+
6+
@implementation MyLibraryTests
7+
8+
- (void)setUp {
9+
[super setUp];
10+
11+
// Peform shared test setup here.
12+
}
13+
14+
- (void)tearDown {
15+
// Peform shared test teardown here.
16+
17+
[super tearDown];
18+
}
19+
20+
- (void)testExample {
21+
NSString *input = @"Hello, MyLibrary!";
22+
NSString *expectedOutput = @"Processed: Hello, MyLibrary!";
23+
24+
NSString *actualOutput = [self.myLibrary processString:input];
25+
XCTAssertEqualObjects(expectedOutput, actualOutput, @"MyLibrary should correctly process strings");
26+
}
27+
28+
@end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import XCTest
2+
@testable import {{targetName}}
3+
4+
final class libraryTests: XCTestCase {
5+
func testExample() throws {
6+
XCTAssertEqual(library().text, "Hello, World!")
7+
}
8+
}

types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ declare module "*.yml" {
22
const content: { [key: string]: any };
33
export default content;
44
}
5+
declare module "*.mustache" {
6+
const content: string;
7+
export default content;
8+
}

0 commit comments

Comments
 (0)