@@ -244,4 +244,198 @@ defmodule Phoenix.LiveView.HTMLEngine do
244
244
defp current_otp_app do
245
245
Application . get_env ( :logger , :compile_time_application )
246
246
end
247
+
248
+ @ impl true
249
+ def token_preprocess ( tokens , opts ) do
250
+ file = Keyword . fetch! ( opts , :file )
251
+ caller = Keyword . fetch! ( opts , :caller )
252
+ module = caller . module
253
+
254
+ { hooks , tokens , _module } = process_hooks ( tokens , { % { } , [ ] , module } )
255
+ hooks = write_hooks_and_manifest ( hooks , file )
256
+
257
+ if hooks == % { } do
258
+ Enum . reverse ( tokens )
259
+ else
260
+ # when a <script type="text/phx-hook" name="..." > is found, we generate the hook name
261
+ # based on its content. Then, we need to rewrite the phx-hook="..." attribute of all
262
+ # other tags to match the generated hook name.
263
+ # This is expensive, as we traverse all tags and attributes,
264
+ # but we only do it if a script hook is present.
265
+ rewrite_hook_names ( hooks , tokens )
266
+ end
267
+ end
268
+
269
+ defp process_hooks (
270
+ [
271
+ { :tag , "script" , attrs , meta } = start ,
272
+ { :text , text , _ } = content ,
273
+ { :close , :tag , "script" , _ } = end_ | rest
274
+ ] ,
275
+ { hooks , tokens_acc , module }
276
+ ) do
277
+ str_attrs = for { name , { :string , value , _ } , _ } <- attrs , into: % { } , do: { name , value }
278
+
279
+ case str_attrs do
280
+ # keep runtime hooks
281
+ % { "type" => "text/phx-hook" , "name" => name , "bundle" => "runtime" } ->
282
+ # keep bundle="runtime" hooks in DOM
283
+ # TODO: handle in JS
284
+ process_hooks ( rest , { hooks , [ end_ , content , start | tokens_acc ] , module } )
285
+
286
+ % { "type" => "text/phx-hook" , "name" => name , "bundle" => "current_otp_app" } ->
287
+ # only consider bundle="current_otp_app" hooks if they are part of the current otp_app
288
+ if current_otp_app ( ) == Application . get_application ( module ) do
289
+ hooks = Map . put ( hooks , name , % { content: text , attrs: str_attrs , meta: meta } )
290
+ process_hooks ( rest , { hooks , [ end_ , content , start | tokens_acc ] , module } )
291
+ else
292
+ process_hooks ( rest , { hooks , tokens_acc , module } )
293
+ end
294
+
295
+ % { "type" => "text/phx-hook" , "name" => name } ->
296
+ # by default, hooks with no hook-type are extracted, no matter where they're from
297
+ hooks = Map . put ( hooks , name , % { content: text , attrs: str_attrs , meta: meta } )
298
+ process_hooks ( rest , { hooks , tokens_acc , module } )
299
+
300
+ % { "type" => "text/phx-hook" } ->
301
+ # TODO: nice error message
302
+ raise ArgumentError ,
303
+ "scripts with type=\" text/phx-hook\" must have a compile-time string \" name\" attribute"
304
+
305
+ _ ->
306
+ process_hooks ( rest , { hooks , [ end_ , content , start | tokens_acc ] , module } )
307
+ end
308
+ end
309
+
310
+ defp process_hooks ( [ { :tag , "script" , attrs , _meta } = start | rest ] , { hooks , tokens_acc , module } ) do
311
+ if Enum . find ( attrs , match? ( { "type" , { :string , "text/phx-hook" , _ } , _ } , attrs ) ) do
312
+ # TODO: nice error message
313
+ raise ArgumentError ,
314
+ "scripts with type=\" text/phx-hook\" must not contain any interpolation!"
315
+ else
316
+ process_hooks ( rest , { hooks , [ start | tokens_acc ] , module } )
317
+ end
318
+ end
319
+
320
+ defp process_hooks ( [ token | rest ] , { hooks , tokens_acc , module } ) ,
321
+ do: process_hooks ( rest , { hooks , [ token | tokens_acc ] , module } )
322
+
323
+ defp process_hooks ( [ ] , acc ) , do: acc
324
+
325
+ defp rewrite_hook_names ( hooks , tokens ) do
326
+ for token <- tokens , reduce: [ ] do
327
+ acc ->
328
+ case token do
329
+ { :tag , name , attrs , meta } ->
330
+ [ { :tag , name , rewrite_hook_attrs ( hooks , attrs ) , meta } | acc ]
331
+
332
+ { :local_component , name , attrs , meta } ->
333
+ [ { :local_component , name , rewrite_hook_attrs ( hooks , attrs ) , meta } | acc ]
334
+
335
+ { :remote_component , name , attrs , meta } ->
336
+ [ { :remote_component , name , rewrite_hook_attrs ( hooks , attrs ) , meta } | acc ]
337
+
338
+ other ->
339
+ [ other | acc ]
340
+ end
341
+ end
342
+ end
343
+
344
+ defp rewrite_hook_attrs ( hooks , attrs ) do
345
+ Enum . map ( attrs , fn
346
+ { "phx-hook" , { :string , name , meta1 } , meta2 } ->
347
+ if is_map_key ( hooks , name ) do
348
+ { "phx-hook" , { :string , hooks [ name ] . name , meta1 } , meta2 }
349
+ else
350
+ { "phx-hook" , { :string , name , meta1 } , meta2 }
351
+ end
352
+
353
+ { attr , value , meta } ->
354
+ { attr , value , meta }
355
+ end )
356
+ end
357
+
358
+ defp write_hooks_and_manifest ( hooks , file ) do
359
+ for { name , % { content: raw_content , attrs: attrs , meta: meta } = hook } <- hooks ,
360
+ attrs [ "hook-type" ] == nil or attrs [ "hook-type" ] == "default" ,
361
+ into: % { } do
362
+ line = meta [ :line ]
363
+ col = meta [ :column ]
364
+
365
+ script_content =
366
+ "// #{ Path . relative_to_cwd ( file ) } :#{ line } :#{ col } \n " <> raw_content
367
+
368
+ dir = "assets/js/hooks"
369
+ manifest_path = Path . join ( dir , "index.js" )
370
+
371
+ js_filename = hashed_script_name ( file ) <> "_#{ line } _#{ col } "
372
+ js_path = Path . join ( dir , js_filename <> ".js" )
373
+
374
+ File . mkdir_p! ( dir )
375
+ File . write! ( js_path , script_content )
376
+
377
+ if ! File . exists? ( manifest_path ) do
378
+ File . write! ( manifest_path , """
379
+ let hooks = {}
380
+ export default hooks
381
+ """ )
382
+ end
383
+
384
+ manifest = File . read! ( manifest_path )
385
+
386
+ File . open ( manifest_path , [ :append ] , fn file ->
387
+ if ! String . contains? ( manifest , js_filename ) do
388
+ IO . puts ( "Add hook to #{ manifest_path } " )
389
+
390
+ IO . binwrite (
391
+ file ,
392
+ ~s| \n import hook_#{ js_filename } from "./#{ js_filename } "; hooks["#{ js_filename } "] = hook_#{ js_filename } ;|
393
+ )
394
+ end
395
+ end )
396
+
397
+ IO . puts ( "Write hook to #{ js_path } " )
398
+
399
+ { name , Map . put ( hook , :name , js_filename ) }
400
+ end
401
+ end
402
+
403
+ defp hashed_script_name ( file ) do
404
+ :md5 |> :crypto . hash ( file ) |> Base . encode16 ( )
405
+ end
406
+
407
+ def prune_hooks ( file ) do
408
+ hashed_name = hashed_script_name ( file )
409
+ hooks_dir = Path . expand ( "assets/js/hooks" , File . cwd! ( ) )
410
+ manifest_path = Path . join ( hooks_dir , "index.js" )
411
+
412
+ case File . ls ( hooks_dir ) do
413
+ { :ok , hooks } ->
414
+ for hook_basename <- hooks do
415
+ case String . split ( hook_basename , "_" ) do
416
+ [ ^ hashed_name | _ ] ->
417
+ File . rm! ( IO . inspect ( Path . join ( hooks_dir , hook_basename ) , label: "Pruning" ) )
418
+
419
+ if File . exists? ( manifest_path ) do
420
+ new_file =
421
+ manifest_path
422
+ |> File . stream! ( )
423
+ |> Enum . filter ( fn line -> ! String . contains? ( line , hashed_name ) end )
424
+ |> Enum . join ( "" )
425
+ |> String . trim ( )
426
+
427
+ File . write! ( manifest_path , new_file )
428
+ end
429
+
430
+ _ ->
431
+ :noop
432
+ end
433
+ end
434
+
435
+ _ ->
436
+ :noop
437
+ end
438
+
439
+ nil
440
+ end
247
441
end
0 commit comments