@@ -1131,75 +1131,90 @@ private struct AsyncClosureReturnTypeCollector: BridgeSkeletonVisitor {
11311131/// - `module`: Read from an external ECMAScript module named by a bare specifier
11321132/// that the JavaScript host resolves (e.g. `node:path`, `lodash/fp`).
11331133///
1134- /// The JSON encoding keeps `.global` as `"global"` and a snippet as its plain path
1135- /// string; only `.module` uses a tagged object. That keeps checked-in skeletons
1136- /// stable and avoids colliding with the `"global"` sentinel, since `global` is
1137- /// itself a valid package name.
1134+ /// `.global` encodes as the string `"global"`; the other two encode as tagged
1135+ /// objects that name their kind, so the JSON mirrors the Swift cases and is
1136+ /// readable without knowing that a `/`-prefixed string means one thing and any
1137+ /// other string means another. A plain string is never used for a specifier,
1138+ /// which also avoids colliding with the `"global"` sentinel — `global` is itself
1139+ /// a valid npm package name.
11381140public enum JSImportFrom : Codable , Equatable , Sendable {
11391141 case global
11401142 case snippet( String )
11411143 case module( String )
11421144
11431145 private enum CodingKeys : String , CodingKey {
1144- case kind, specifier
1146+ case kind, path , specifier
11451147 }
11461148
11471149 public init ( from decoder: any Decoder ) throws {
11481150 if let container = try ? decoder. singleValueContainer ( ) , let value = try ? container. decode ( String . self) {
1149- if value == " global " {
1150- self = . global
1151- return
1152- }
1153- guard value. hasPrefix ( " / " ) , !value. split ( separator: " / " ) . contains ( " .. " ) else {
1151+ guard value == " global " else {
11541152 throw DecodingError . dataCorruptedError (
11551153 in: container,
1156- debugDescription: " Unknown import origin ' \( value) '. Expected \" global \" or a rooted snippet path ."
1154+ debugDescription: " Unknown import origin ' \( value) '. Expected \" global \" . "
11571155 )
11581156 }
1159- self = . snippet ( value )
1157+ self = . global
11601158 return
11611159 }
11621160 let container = try decoder. container ( keyedBy: CodingKeys . self)
11631161 let kind = try container. decode ( String . self, forKey: . kind)
1164- guard kind == " module " else {
1162+ switch kind {
1163+ case " snippet " :
1164+ let path = try container. decode ( String . self, forKey: . path)
1165+ // A snippet names a file we resolve inside the Swift target, so it must be
1166+ // rooted there and must not traverse out of it.
1167+ guard path. hasPrefix ( " / " ) , !path. split ( separator: " / " ) . contains ( " .. " ) else {
1168+ throw DecodingError . dataCorruptedError (
1169+ forKey: . path,
1170+ in: container,
1171+ debugDescription: " Snippet path ' \( path) ' must start with '/' and must not contain '..'. "
1172+ )
1173+ }
1174+ self = . snippet( path)
1175+ case " module " :
1176+ let specifier = try container. decode ( String . self, forKey: . specifier)
1177+ // A bare specifier is resolved by the JavaScript host, so almost anything is
1178+ // legal, but the shapes that can never work are rejected here as well as at
1179+ // parse time: an empty specifier, a relative one, and a rooted path.
1180+ guard !specifier. isEmpty else {
1181+ throw DecodingError . dataCorruptedError (
1182+ forKey: . specifier,
1183+ in: container,
1184+ debugDescription: " Module specifier must not be empty. "
1185+ )
1186+ }
1187+ guard !specifier. hasPrefix ( " . " ) , !specifier. hasPrefix ( " / " ) else {
1188+ throw DecodingError . dataCorruptedError (
1189+ forKey: . specifier,
1190+ in: container,
1191+ debugDescription: " Module specifier ' \( specifier) ' must not be a path. Use a snippet instead. "
1192+ )
1193+ }
1194+ self = . module( specifier)
1195+ default :
11651196 throw DecodingError . dataCorruptedError (
11661197 forKey: . kind,
11671198 in: container,
1168- debugDescription: " Unknown import origin kind ' \( kind) '. Expected \" module \" . "
1169- )
1170- }
1171- let specifier = try container. decode ( String . self, forKey: . specifier)
1172- // A bare specifier is resolved by the JavaScript host, so almost anything is
1173- // legal, but the shapes that can never work are rejected here as well as at
1174- // parse time: an empty specifier, a relative one, and a rooted path (which is
1175- // a snippet and must be encoded as a plain string instead).
1176- guard !specifier. isEmpty else {
1177- throw DecodingError . dataCorruptedError (
1178- forKey: . specifier,
1179- in: container,
1180- debugDescription: " Module specifier must not be empty. "
1181- )
1182- }
1183- guard !specifier. hasPrefix ( " . " ) , !specifier. hasPrefix ( " / " ) else {
1184- throw DecodingError . dataCorruptedError (
1185- forKey: . specifier,
1186- in: container,
1187- debugDescription:
1188- " Module specifier ' \( specifier) ' must not be a path. Rooted snippet paths are encoded as a plain string. "
1199+ debugDescription: " Unknown import origin kind ' \( kind) '. Expected \" snippet \" or \" module \" . "
11891200 )
11901201 }
1191- self = . module( specifier)
11921202 }
11931203
11941204 public func encode( to encoder: any Encoder ) throws {
1195- guard let specifier = moduleSpecifier else {
1205+ switch self {
1206+ case . global:
11961207 var container = encoder. singleValueContainer ( )
1197- try container. encode ( snippetPath ?? " global " )
1198- return
1208+ try container. encode ( " global " )
1209+ case . snippet( let path) :
1210+ var container = encoder. container ( keyedBy: CodingKeys . self)
1211+ try container. encode ( " snippet " , forKey: . kind)
1212+ try container. encode ( path, forKey: . path)
1213+ case . module( let specifier) :
1214+ var container = encoder. container ( keyedBy: CodingKeys . self)
1215+ try container. encode ( " module " , forKey: . kind)
1216+ try container. encode ( specifier, forKey: . specifier)
11991217 }
1200- var container = encoder. container ( keyedBy: CodingKeys . self)
1201- try container. encode ( " module " , forKey: . kind)
1202- try container. encode ( specifier, forKey: . specifier)
12031218 }
12041219
12051220 /// The path of a target-local JavaScript file, rooted at the Swift target directory.
0 commit comments