1 // Package jspacker is a javascript packer for javascript projects 2 package jspacker 3 4 import ( 5 "fmt" 6 "net/url" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 12 "vimagination.zapto.org/javascript" 13 "vimagination.zapto.org/javascript/scope" 14 "vimagination.zapto.org/javascript/walk" 15 "vimagination.zapto.org/parser" 16 ) 17 18 type config struct { 19 filesToDo []string 20 filesDone map[string]*dependency 21 loader func(string) (*javascript.Module, error) 22 bare bool 23 parseDynamic bool 24 nextID uint 25 exportAllFrom [][2]*dependency 26 statementList []javascript.StatementListItem 27 dependency 28 } 29 30 const ( 31 jsSuffix = ".js" 32 tsSuffix = ".ts" 33 ) 34 35 // OSLoad is the default loader for Package, with the base set to CWD 36 func OSLoad(base string) func(string) (*javascript.Module, error) { 37 return func(urlPath string) (*javascript.Module, error) { 38 var ( 39 f *os.File 40 err error 41 ) 42 ts := strings.HasSuffix(base, tsSuffix) 43 for _, loader := range [...]func() (*os.File, error){ 44 func() (*os.File, error) { // Assume that any TS file will be more up-to-date by default 45 if strings.HasSuffix(urlPath, jsSuffix) { 46 ts = true 47 return os.Open(filepath.Join(base, filepath.FromSlash(urlPath[:len(urlPath)-3]+tsSuffix))) 48 } 49 return nil, nil 50 }, 51 func() (*os.File, error) { // Normal 52 f, err := os.Open(filepath.Join(base, filepath.FromSlash(urlPath))) 53 if err == nil { 54 ts = strings.HasSuffix(urlPath, tsSuffix) 55 } 56 return f, err 57 }, 58 func() (*os.File, error) { // As URL 59 if u, err := url.Parse(urlPath); err == nil && u.Path != urlPath { 60 f, err := os.Open(filepath.Join(base, filepath.FromSlash(u.Path))) 61 if err == nil { 62 ts = strings.HasSuffix(urlPath, tsSuffix) 63 } 64 return f, err 65 } 66 return nil, nil 67 }, 68 func() (*os.File, error) { // Add TS extension 69 if !strings.HasSuffix(urlPath, tsSuffix) { 70 ts = true 71 return os.Open(filepath.Join(base, filepath.FromSlash(urlPath+tsSuffix))) 72 } 73 return nil, nil 74 }, 75 func() (*os.File, error) { // Add JS extension 76 if !strings.HasSuffix(urlPath, jsSuffix) { 77 return os.Open(filepath.Join(base, filepath.FromSlash(urlPath+jsSuffix))) 78 } 79 return nil, nil 80 }, 81 } { 82 fb, errr := loader() 83 if fb != nil { 84 f = fb 85 break 86 } else if err == nil { 87 err = errr 88 } 89 } 90 if f == nil { 91 return nil, fmt.Errorf("error opening file (%s): %w", urlPath, err) 92 } 93 rt := parser.NewReaderTokeniser(f) 94 var tks javascript.Tokeniser = &rt 95 if ts { 96 tks = javascript.AsTypescript(&rt) 97 } 98 m, err := javascript.ParseModule(tks) 99 f.Close() 100 if err != nil { 101 return nil, fmt.Errorf("error parsing file (%s): %w", urlPath, err) 102 } 103 return m, nil 104 } 105 } 106 107 // Package packages up multiple javascript modules into a single file, renaming 108 // bindings to simulate imports 109 func Package(opts ...Option) (*javascript.Script, error) { 110 c := config{ 111 statementList: make([]javascript.StatementListItem, 2), 112 filesDone: make(map[string]*dependency), 113 dependency: dependency{ 114 requires: make(map[string]*dependency), 115 }, 116 } 117 if c.loader == nil { 118 base, err := os.Getwd() 119 if err != nil { 120 return nil, fmt.Errorf("error getting current working directory: %w", err) 121 } 122 c.loader = OSLoad(base) 123 } 124 c.config = &c 125 for _, o := range opts { 126 o(&c) 127 } 128 if len(c.filesToDo) == 0 { 129 return nil, ErrNoFiles 130 } 131 c.statementList[1].Declaration = &javascript.Declaration{ 132 LexicalDeclaration: &javascript.LexicalDeclaration{ 133 LetOrConst: javascript.Const, 134 BindingList: []javascript.LexicalBinding{ 135 { 136 BindingIdentifier: jToken("o"), 137 Initializer: &javascript.AssignmentExpression{ 138 ConditionalExpression: javascript.WrapConditional(javascript.MemberExpression{ 139 MemberExpression: &javascript.MemberExpression{ 140 PrimaryExpression: &javascript.PrimaryExpression{ 141 IdentifierReference: jToken("location"), 142 }, 143 }, 144 IdentifierName: jToken("origin"), 145 }), 146 }, 147 }, 148 }, 149 }, 150 } 151 for _, url := range c.filesToDo { 152 if !strings.HasPrefix(url, "/") { 153 return nil, fmt.Errorf("%w: %s", ErrInvalidURL, url) 154 } 155 if _, err := c.dependency.addImport(c.dependency.RelTo(url)); err != nil { 156 return nil, err 157 } 158 } 159 for changed := true; changed; { 160 changed = false 161 for _, eaf := range c.exportAllFrom { 162 for export := range eaf[1].exports { 163 if export == "default" { 164 continue 165 } 166 if _, ok := eaf[0].exports[export]; !ok { 167 eaf[0].exports[export] = &importBinding{ 168 dependency: eaf[1], 169 binding: export, 170 } 171 changed = true 172 } 173 } 174 } 175 } 176 if err := c.dependency.resolveImports(); err != nil { 177 return nil, err 178 } 179 if err := c.makeLoader(); err != nil { 180 return nil, err 181 } 182 if len(c.statementList[1].Declaration.LexicalDeclaration.BindingList) == 1 { 183 c.statementList[1] = c.statementList[0] 184 c.statementList = c.statementList[1:] 185 } 186 return &javascript.Script{ 187 StatementList: c.statementList, 188 }, nil 189 } 190 191 // Plugin converts a single javascript module to make use of the processed 192 // exports from package 193 func Plugin(m *javascript.Module, url string) (*javascript.Script, error) { 194 if !strings.HasPrefix(url, "/") { 195 return nil, ErrInvalidURL 196 } 197 var ( 198 imports = uint(0) 199 importURLs = make(map[string]string) 200 importBindings = make(importBindingMap) 201 importObjectBindings []javascript.BindingElement 202 importURLsArrayE []javascript.ArrayElement 203 importURLsArray []javascript.Argument 204 statementList = make([]javascript.StatementListItem, 1, len(m.ModuleListItems)) 205 d = dependency{ 206 url: url, 207 prefix: "_", 208 } 209 ) 210 scope, err := scope.ModuleScope(m, nil) 211 if err != nil { 212 return nil, err 213 } 214 for _, li := range m.ModuleListItems { 215 if li.ImportDeclaration != nil { 216 id := li.ImportDeclaration 217 durl, _ := javascript.Unquote(id.ModuleSpecifier.Data) 218 iurl := d.RelTo(durl) 219 ib, ok := importURLs[iurl] 220 if !ok { 221 imports++ 222 ib = id2String(imports) 223 importURLs[iurl] = ib 224 ae := javascript.Argument{ 225 AssignmentExpression: javascript.AssignmentExpression{ 226 ConditionalExpression: javascript.WrapConditional(&javascript.PrimaryExpression{ 227 Literal: jToken(strconv.Quote(iurl)), 228 }), 229 }, 230 } 231 importURLsArray = append(importURLsArray, ae) 232 importURLsArrayE = append(importURLsArrayE, javascript.ArrayElement{ 233 AssignmentExpression: ae.AssignmentExpression, 234 }) 235 importObjectBindings = append(importObjectBindings, javascript.BindingElement{ 236 SingleNameBinding: jToken(ib), 237 }) 238 } 239 if id.ImportClause != nil { 240 if id.NameSpaceImport != nil { 241 for _, binding := range scope.Bindings[li.ImportDeclaration.NameSpaceImport.Data] { 242 binding.Data = ib 243 } 244 } 245 if id.ImportedDefaultBinding != nil { 246 importBindings[id.ImportedDefaultBinding.Data] = javascript.MemberExpression{ 247 MemberExpression: &javascript.MemberExpression{ 248 PrimaryExpression: &javascript.PrimaryExpression{ 249 IdentifierReference: jToken(ib), 250 }, 251 }, 252 IdentifierName: jToken("default"), 253 } 254 } 255 if id.NamedImports != nil { 256 for _, is := range id.NamedImports.ImportList { 257 tk := is.ImportedBinding 258 if is.IdentifierName != nil { 259 tk = is.IdentifierName 260 } 261 importBindings[is.ImportedBinding.Data] = javascript.MemberExpression{ 262 MemberExpression: &javascript.MemberExpression{ 263 PrimaryExpression: &javascript.PrimaryExpression{ 264 IdentifierReference: jToken(ib), 265 }, 266 }, 267 IdentifierName: tk, 268 } 269 } 270 } 271 } 272 } else if li.StatementListItem != nil { 273 statementList = append(statementList, *li.StatementListItem) 274 } else if li.ExportDeclaration != nil { 275 ed := li.ExportDeclaration 276 if ed.VariableStatement != nil { 277 statementList = append(statementList, javascript.StatementListItem{ 278 Statement: &javascript.Statement{ 279 VariableStatement: ed.VariableStatement, 280 }, 281 }) 282 } else if ed.Declaration != nil { 283 statementList = append(statementList, javascript.StatementListItem{ 284 Declaration: ed.Declaration, 285 }) 286 } else if ed.DefaultFunction != nil { 287 if ed.DefaultFunction.BindingIdentifier != nil { 288 statementList = append(statementList, javascript.StatementListItem{ 289 Declaration: &javascript.Declaration{ 290 FunctionDeclaration: ed.DefaultFunction, 291 }, 292 }) 293 } 294 } else if ed.DefaultClass != nil { 295 if ed.DefaultClass.BindingIdentifier != nil { 296 statementList = append(statementList, javascript.StatementListItem{ 297 Declaration: &javascript.Declaration{ 298 ClassDeclaration: ed.DefaultClass, 299 }, 300 }) 301 } 302 } else if ed.DefaultAssignmentExpression != nil { 303 statementList = append(statementList, javascript.StatementListItem{ 304 Statement: &javascript.Statement{ 305 ExpressionStatement: &javascript.Expression{ 306 Expressions: []javascript.AssignmentExpression{ 307 *ed.DefaultAssignmentExpression, 308 }, 309 }, 310 }, 311 }) 312 } 313 } 314 } 315 d.processBindings(scope) 316 if imports == 0 { 317 statementList = statementList[1:] 318 } else if imports == 1 { 319 statementList[0] = javascript.StatementListItem{ 320 Declaration: &javascript.Declaration{ 321 LexicalDeclaration: &javascript.LexicalDeclaration{ 322 LetOrConst: javascript.Const, 323 BindingList: []javascript.LexicalBinding{ 324 { 325 BindingIdentifier: importObjectBindings[0].SingleNameBinding, 326 Initializer: &javascript.AssignmentExpression{ 327 ConditionalExpression: javascript.WrapConditional(&javascript.UnaryExpression{ 328 UnaryOperators: []javascript.UnaryOperator{javascript.UnaryAwait}, 329 UpdateExpression: javascript.UpdateExpression{ 330 LeftHandSideExpression: &javascript.LeftHandSideExpression{ 331 CallExpression: &javascript.CallExpression{ 332 MemberExpression: &javascript.MemberExpression{ 333 PrimaryExpression: &javascript.PrimaryExpression{ 334 IdentifierReference: jToken("include"), 335 }, 336 }, 337 Arguments: &javascript.Arguments{ 338 ArgumentList: importURLsArray, 339 }, 340 }, 341 }, 342 }, 343 }), 344 }, 345 }, 346 }, 347 }, 348 }, 349 } 350 } else { 351 statementList[0] = javascript.StatementListItem{ 352 Declaration: &javascript.Declaration{ 353 LexicalDeclaration: &javascript.LexicalDeclaration{ 354 LetOrConst: javascript.Const, 355 BindingList: []javascript.LexicalBinding{ 356 { 357 ArrayBindingPattern: &javascript.ArrayBindingPattern{ 358 BindingElementList: importObjectBindings, 359 }, 360 Initializer: &javascript.AssignmentExpression{ 361 ConditionalExpression: javascript.WrapConditional(&javascript.UnaryExpression{ 362 UnaryOperators: []javascript.UnaryOperator{javascript.UnaryAwait}, 363 UpdateExpression: javascript.UpdateExpression{ 364 LeftHandSideExpression: &javascript.LeftHandSideExpression{ 365 CallExpression: &javascript.CallExpression{ 366 MemberExpression: &javascript.MemberExpression{ 367 MemberExpression: &javascript.MemberExpression{ 368 PrimaryExpression: &javascript.PrimaryExpression{ 369 IdentifierReference: jToken("Promise"), 370 }, 371 }, 372 IdentifierName: jToken("all"), 373 }, 374 Arguments: &javascript.Arguments{ 375 ArgumentList: []javascript.Argument{ 376 { 377 AssignmentExpression: javascript.AssignmentExpression{ 378 ConditionalExpression: javascript.WrapConditional(&javascript.CallExpression{ 379 MemberExpression: &javascript.MemberExpression{ 380 MemberExpression: &javascript.MemberExpression{ 381 PrimaryExpression: &javascript.PrimaryExpression{ 382 ArrayLiteral: &javascript.ArrayLiteral{ 383 ElementList: importURLsArrayE, 384 }, 385 }, 386 }, 387 IdentifierName: jToken("map"), 388 }, 389 Arguments: &javascript.Arguments{ 390 ArgumentList: []javascript.Argument{ 391 { 392 AssignmentExpression: javascript.AssignmentExpression{ 393 ConditionalExpression: javascript.WrapConditional(&javascript.PrimaryExpression{ 394 IdentifierReference: jToken("include"), 395 }), 396 }, 397 }, 398 }, 399 }, 400 }), 401 }, 402 }, 403 }, 404 }, 405 }, 406 }, 407 }, 408 }), 409 }, 410 }, 411 }, 412 }, 413 }, 414 } 415 } 416 s := &javascript.Script{ 417 StatementList: statementList, 418 } 419 walk.Walk(s, &d) 420 walk.Walk(s, importBindings) 421 return s, nil 422 } 423 424 type importBindingMap map[string]javascript.MemberExpression 425 426 func (i importBindingMap) Handle(t javascript.Type) error { 427 if me, ok := t.(*javascript.MemberExpression); ok && me.PrimaryExpression != nil && me.PrimaryExpression.IdentifierReference != nil { 428 if nme, ok := i[me.PrimaryExpression.IdentifierReference.Data]; ok { 429 *me = nme 430 return nil 431 } 432 } 433 return walk.Walk(t, i) 434 } 435