Skip to content

Commit 84243a4

Browse files
Merge pull request #457 from t089/platform-arg
Add `--platform` option to `package js`
2 parents 6310307 + 617110e commit 84243a4

File tree

15 files changed

+216
-100
lines changed

15 files changed

+216
-100
lines changed

Plugins/PackageToJS/Sources/PackageToJS.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import Foundation
22

33
struct PackageToJS {
44
struct PackageOptions {
5+
enum Platform: String, CaseIterable {
6+
case browser
7+
case node
8+
}
9+
510
/// Path to the output directory
611
var outputPath: String?
712
/// The build configuration to use (default: debug)
813
var configuration: String?
914
/// Name of the package (default: lowercased Package.swift name)
1015
var packageName: String?
16+
/// Target platform for the generated JavaScript (default: browser)
17+
var defaultPlatform: Platform = .browser
1118
/// Whether to explain the build plan (default: false)
1219
var explain: Bool = false
1320
/// Whether to print verbose output
@@ -717,6 +724,8 @@ struct PackagingPlanner {
717724
"USE_WASI_CDN": options.useCDN,
718725
"HAS_BRIDGE": exportedSkeletons.count > 0 || importedSkeletons.count > 0,
719726
"HAS_IMPORTS": importedSkeletons.count > 0,
727+
"TARGET_DEFAULT_PLATFORM_NODE": options.defaultPlatform == .node,
728+
"TARGET_DEFAULT_PLATFORM_BROWSER": options.defaultPlatform == .browser,
720729
]
721730
let constantSubstitutions: [String: String] = [
722731
"PACKAGE_TO_JS_MODULE_PATH": wasmFilename,

Plugins/PackageToJS/Sources/PackageToJSPlugin.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ struct PackageToJSPlugin: CommandPlugin {
175175
}
176176

177177
var extractor = ArgumentExtractor(arguments)
178-
let buildOptions = PackageToJS.BuildOptions.parse(from: &extractor)
178+
let buildOptions = try PackageToJS.BuildOptions.parse(from: &extractor)
179179

180180
if extractor.remainingArguments.count > 0 {
181181
printStderr(
@@ -239,7 +239,7 @@ struct PackageToJSPlugin: CommandPlugin {
239239
}
240240

241241
var extractor = ArgumentExtractor(arguments)
242-
let testOptions = PackageToJS.TestOptions.parse(from: &extractor)
242+
let testOptions = try PackageToJS.TestOptions.parse(from: &extractor)
243243

244244
if extractor.remainingArguments.count > 0 {
245245
printStderr(
@@ -440,12 +440,28 @@ private func printStderr(_ message: String) {
440440

441441
// MARK: - Options parsing
442442

443+
extension ArgumentExtractor {
444+
mutating func extractPlatformOption(named name: String) throws -> PackageToJS.PackageOptions.Platform {
445+
guard let stringValue = self.extractOption(named: name).last else {
446+
return .browser
447+
}
448+
449+
guard let platform = PackageToJS.PackageOptions.Platform(rawValue: stringValue) else {
450+
throw PackageToJSError(
451+
"Invalid platform: \(stringValue), expected one of \(PackageToJS.PackageOptions.Platform.allCases.map(\.rawValue).joined(separator: ", "))"
452+
)
453+
}
454+
return platform
455+
}
456+
}
457+
443458
extension PackageToJS.PackageOptions {
444-
static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.PackageOptions {
459+
static func parse(from extractor: inout ArgumentExtractor) throws -> PackageToJS.PackageOptions {
445460
let outputPath = extractor.extractOption(named: "output").last
446461
let configuration: String? =
447462
(extractor.extractOption(named: "configuration") + extractor.extractSingleDashOption(named: "c")).last
448463
let packageName = extractor.extractOption(named: "package-name").last
464+
let defaultPlatform = try extractor.extractPlatformOption(named: "default-platform")
449465
let explain = extractor.extractFlag(named: "explain")
450466
let useCDN = extractor.extractFlag(named: "use-cdn")
451467
let verbose = extractor.extractFlag(named: "verbose")
@@ -454,6 +470,7 @@ extension PackageToJS.PackageOptions {
454470
outputPath: outputPath,
455471
configuration: configuration,
456472
packageName: packageName,
473+
defaultPlatform: defaultPlatform,
457474
explain: explain != 0,
458475
verbose: verbose != 0,
459476
useCDN: useCDN != 0,
@@ -466,6 +483,7 @@ extension PackageToJS.PackageOptions {
466483
--output <path> Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
467484
-c, --configuration <name> The build configuration to use (values: debug, release; default: debug)
468485
--package-name <name> Name of the package (default: lowercased Package.swift name)
486+
--platform <name> Target platform for generated JavaScript (values: \(PackageToJS.PackageOptions.Platform.allCases.map(\.rawValue).joined(separator: ", ")); default: \(PackageToJS.PackageOptions.Platform.browser))
469487
--use-cdn Whether to use CDN for dependency packages
470488
--enable-code-coverage Whether to enable code coverage collection
471489
--explain Whether to explain the build plan
@@ -475,7 +493,7 @@ extension PackageToJS.PackageOptions {
475493
}
476494

477495
extension PackageToJS.BuildOptions {
478-
static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.BuildOptions {
496+
static func parse(from extractor: inout ArgumentExtractor) throws -> PackageToJS.BuildOptions {
479497
let product = extractor.extractOption(named: "product").last
480498
let noOptimize = extractor.extractFlag(named: "no-optimize")
481499
let rawDebugInfoFormat = extractor.extractOption(named: "debug-info-format").last
@@ -488,7 +506,7 @@ extension PackageToJS.BuildOptions {
488506
}
489507
debugInfoFormat = format
490508
}
491-
let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor)
509+
let packageOptions = try PackageToJS.PackageOptions.parse(from: &extractor)
492510
return PackageToJS.BuildOptions(
493511
product: product,
494512
noOptimize: noOptimize != 0,
@@ -526,15 +544,15 @@ extension PackageToJS.BuildOptions {
526544
}
527545

528546
extension PackageToJS.TestOptions {
529-
static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.TestOptions {
547+
static func parse(from extractor: inout ArgumentExtractor) throws -> PackageToJS.TestOptions {
530548
let buildOnly = extractor.extractFlag(named: "build-only")
531549
let listTests = extractor.extractFlag(named: "list-tests")
532550
let filter = extractor.extractOption(named: "filter")
533551
let prelude = extractor.extractOption(named: "prelude").last
534552
let environment = extractor.extractOption(named: "environment").last
535553
let inspect = extractor.extractFlag(named: "inspect")
536554
let extraNodeArguments = extractor.extractSingleDashOption(named: "Xnode")
537-
let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor)
555+
let packageOptions = try PackageToJS.PackageOptions.parse(from: &extractor)
538556
var options = PackageToJS.TestOptions(
539557
buildOnly: buildOnly != 0,
540558
listTests: listTests != 0,

Plugins/PackageToJS/Templates/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { Exports, Imports, ModuleSource } from './instantiate.js'
22

33
export type Options = {
4+
/* #if TARGET_DEFAULT_PLATFORM_BROWSER */
45
/**
56
* The WebAssembly module to instantiate
67
*
78
* If not provided, the module will be fetched from the default path.
89
*/
910
module?: ModuleSource
11+
/* #endif */
1012
/* #if HAS_IMPORTS */
1113
/**
1214
* The imports to use for the module

Plugins/PackageToJS/Templates/index.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
// @ts-check
22
import { instantiate } from './instantiate.js';
3-
import { defaultBrowserSetup /* #if USE_SHARED_MEMORY */, createDefaultWorkerFactory /* #endif */} from './platforms/browser.js';
3+
/* #if TARGET_DEFAULT_PLATFORM_NODE */
4+
import { defaultNodeSetup /* #if USE_SHARED_MEMORY */, createDefaultWorkerFactory as createDefaultWorkerFactoryForNode /* #endif */} from './platforms/node.js';
5+
/* #else */
6+
import { defaultBrowserSetup /* #if USE_SHARED_MEMORY */, createDefaultWorkerFactory as createDefaultWorkerFactoryForBrowser /* #endif */} from './platforms/browser.js';
7+
/* #endif */
48

9+
/* #if TARGET_DEFAULT_PLATFORM_NODE */
510
/** @type {import('./index.d').init} */
6-
export async function init(_options) {
11+
async function initNode(_options) {
12+
/** @type {import('./platforms/node.d.ts').DefaultNodeSetupOptions} */
13+
const options = {
14+
...(_options || {}),
15+
/* #if USE_SHARED_MEMORY */
16+
spawnWorker: createDefaultWorkerFactoryForNode(),
17+
/* #endif */
18+
};
19+
const instantiateOptions = await defaultNodeSetup(options);
20+
return await instantiate(instantiateOptions);
21+
}
22+
23+
/* #else */
24+
25+
/** @type {import('./index.d').init} */
26+
async function initBrowser(_options) {
727
/** @type {import('./index.d').Options} */
828
const options = _options || {
929
/* #if HAS_IMPORTS */
@@ -21,8 +41,19 @@ export async function init(_options) {
2141
getImports: () => options.getImports(),
2242
/* #endif */
2343
/* #if USE_SHARED_MEMORY */
24-
spawnWorker: createDefaultWorkerFactory()
44+
spawnWorker: createDefaultWorkerFactoryForBrowser()
2545
/* #endif */
2646
})
2747
return await instantiate(instantiateOptions);
2848
}
49+
50+
/* #endif */
51+
52+
/** @type {import('./index.d').init} */
53+
export async function init(options) {
54+
/* #if TARGET_DEFAULT_PLATFORM_NODE */
55+
return initNode(options);
56+
/* #else */
57+
return initBrowser(options);
58+
/* #endif */
59+
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { InstantiateOptions } from "../instantiate.js"
22
import type { Worker } from "node:worker_threads"
33

4-
export function defaultNodeSetup(options: {
4+
export type DefaultNodeSetupOptions = {
55
/* #if IS_WASI */
66
args?: string[],
77
/* #endif */
88
onExit?: (code: number) => void,
99
/* #if USE_SHARED_MEMORY */
1010
spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker,
1111
/* #endif */
12-
}): Promise<InstantiateOptions>
12+
}
1313

14-
export function createDefaultWorkerFactory(preludeScript: string): (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker
14+
export function defaultNodeSetup(options: DefaultNodeSetupOptions): Promise<InstantiateOptions>
15+
16+
export function createDefaultWorkerFactory(preludeScript?: string): (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker

Plugins/PackageToJS/Templates/platforms/node.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,17 @@ export async function defaultNodeSetup(options) {
131131
new PreopenDirectory("/", rootFs),
132132
], { debug: false })
133133
const pkgDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
134-
const module = await WebAssembly.compile(await readFile(path.join(pkgDir, MODULE_PATH)))
134+
const module = await WebAssembly.compile(new Uint8Array(await readFile(path.join(pkgDir, MODULE_PATH))))
135135
/* #if USE_SHARED_MEMORY */
136136
const memory = new WebAssembly.Memory(MEMORY_TYPE);
137137
const threadChannel = new DefaultNodeThreadRegistry(options.spawnWorker)
138138
/* #endif */
139139

140140
return {
141141
module,
142+
/* #if HAS_IMPORTS */
142143
getImports() { return {} },
144+
/* #endif */
143145
/* #if IS_WASI */
144146
wasi: Object.assign(wasi, {
145147
setInstance(instance) {

Plugins/PackageToJS/Templates/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"compilerOptions": {
33
"module": "esnext",
4+
"lib": ["es2017", "dom"],
45
"noEmit": true,
56
"allowJs": true,
67
"skipLibCheck": true,

Plugins/PackageToJS/Tests/TemplatesTests.swift

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,66 @@ import Foundation
33
@testable import PackageToJS
44

55
@Suite struct TemplatesTests {
6-
static let templatesPath = URL(fileURLWithPath: #filePath)
6+
static let pluginPackagePath = URL(fileURLWithPath: #filePath)
77
.deletingLastPathComponent()
88
.deletingLastPathComponent()
9+
static let templatesPath =
10+
pluginPackagePath
911
.appendingPathComponent("Templates")
12+
static let localTemporaryDirectory =
13+
pluginPackagePath
14+
.appendingPathComponent("Tests")
15+
.appendingPathComponent("TemporaryDirectory")
1016

1117
/// `npx tsc -p Templates/tsconfig.json`
12-
@Test func tscCheck() throws {
13-
let tsc = Process()
14-
tsc.executableURL = try which("npx")
15-
tsc.arguments = ["tsc", "-p", Self.templatesPath.appending(path: "tsconfig.json").path]
16-
try tsc.run()
17-
tsc.waitUntilExit()
18-
#expect(tsc.terminationStatus == 0)
18+
/// Test both node and browser platform variants
19+
@Test(arguments: ["node", "browser"])
20+
func tscCheck(platform: String) throws {
21+
// Use a local temporary directory to place instantiated templates so that
22+
// they can reference repo-root node_modules packages like @types/node.
23+
try FileManager.default.createDirectory(
24+
at: Self.localTemporaryDirectory,
25+
withIntermediateDirectories: true,
26+
attributes: nil
27+
)
28+
try withTemporaryDirectory(prefixDirectory: Self.localTemporaryDirectory) { tempDir, _ in
29+
let destination = tempDir.appending(path: Self.templatesPath.lastPathComponent)
30+
// Copy entire templates folder to temp location
31+
try FileManager.default.copyItem(at: Self.templatesPath, to: destination)
32+
33+
// Setup preprocessing conditions
34+
let conditions: [String: Bool] = [
35+
"USE_SHARED_MEMORY": false,
36+
"IS_WASI": true,
37+
"USE_WASI_CDN": false,
38+
"HAS_BRIDGE": false,
39+
"HAS_IMPORTS": false,
40+
"TARGET_DEFAULT_PLATFORM_NODE": platform == "node",
41+
"TARGET_DEFAULT_PLATFORM_BROWSER": platform == "browser",
42+
]
43+
let preprocessOptions = PreprocessOptions(conditions: conditions, substitutions: [:])
44+
45+
// Preprocess all JS and TS files in-place
46+
let enumerator = FileManager.default.enumerator(at: destination, includingPropertiesForKeys: nil)
47+
while let fileURL = enumerator?.nextObject() as? URL {
48+
guard !fileURL.hasDirectoryPath,
49+
fileURL.pathExtension == "js" || fileURL.pathExtension == "ts"
50+
else {
51+
continue
52+
}
53+
54+
let content = try String(contentsOf: fileURL, encoding: .utf8)
55+
let preprocessed = try preprocess(source: content, file: fileURL.path, options: preprocessOptions)
56+
try preprocessed.write(to: fileURL, atomically: true, encoding: .utf8)
57+
}
58+
59+
// Run TypeScript on the preprocessed files
60+
let tsc = Process()
61+
tsc.executableURL = try which("npx")
62+
tsc.arguments = ["tsc", "-p", destination.appending(path: "tsconfig.json").path]
63+
try tsc.run()
64+
tsc.waitUntilExit()
65+
#expect(tsc.terminationStatus == 0)
66+
}
1967
}
2068
}

Plugins/PackageToJS/Tests/TemporaryDirectory.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ struct MakeTemporaryDirectoryError: Error {
44
let error: CInt
55
}
66

7-
internal func withTemporaryDirectory<T>(body: (URL, _ retain: inout Bool) throws -> T) throws -> T {
7+
internal func withTemporaryDirectory<T>(
8+
prefixDirectory: URL = FileManager.default.temporaryDirectory,
9+
body: (URL, _ retain: inout Bool) throws -> T
10+
) throws -> T {
811
// Create a temporary directory using mkdtemp
9-
var template = FileManager.default.temporaryDirectory.appendingPathComponent("PackageToJSTests.XXXXXX").path
12+
var template = prefixDirectory.appendingPathComponent("PackageToJSTests.XXXXXX").path
1013
return try template.withUTF8 { template in
1114
let copy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: template.count + 1)
1215
template.copyBytes(to: copy)

0 commit comments

Comments
 (0)