Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F79269206
CelerityResourceMapGenerator.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sun, Aug 25, 14:11
Size
11 KB
Mime Type
text/x-php
Expires
Tue, Aug 27, 14:11 (2 d)
Engine
blob
Format
Raw Data
Handle
20168223
Attached To
rPH Phabricator
CelerityResourceMapGenerator.php
View Options
<?php
final
class
CelerityResourceMapGenerator
extends
Phobject
{
private
$debug
=
false
;
private
$resources
;
private
$nameMap
=
array
();
private
$symbolMap
=
array
();
private
$requiresMap
=
array
();
private
$packageMap
=
array
();
public
function
__construct
(
CelerityPhysicalResources
$resources
)
{
$this
->
resources
=
$resources
;
}
public
function
getNameMap
()
{
return
$this
->
nameMap
;
}
public
function
getSymbolMap
()
{
return
$this
->
symbolMap
;
}
public
function
getRequiresMap
()
{
return
$this
->
requiresMap
;
}
public
function
getPackageMap
()
{
return
$this
->
packageMap
;
}
public
function
setDebug
(
$debug
)
{
$this
->
debug
=
$debug
;
return
$this
;
}
protected
function
log
(
$message
)
{
if
(
$this
->
debug
)
{
$console
=
PhutilConsole
::
getConsole
();
$console
->
writeErr
(
"%s
\n
"
,
$message
);
}
}
public
function
generate
()
{
$binary_map
=
$this
->
rebuildBinaryResources
(
$this
->
resources
);
$this
->
log
(
pht
(
'Found %d binary resources.'
,
count
(
$binary_map
)));
$xformer
=
id
(
new
CelerityResourceTransformer
())
->
setMinify
(
false
)
->
setRawURIMap
(
ipull
(
$binary_map
,
'uri'
));
$text_map
=
$this
->
rebuildTextResources
(
$this
->
resources
,
$xformer
);
$this
->
log
(
pht
(
'Found %d text resources.'
,
count
(
$text_map
)));
$resource_graph
=
array
();
$requires_map
=
array
();
$symbol_map
=
array
();
foreach
(
$text_map
as
$name
=>
$info
)
{
if
(
isset
(
$info
[
'provides'
]))
{
$symbol_map
[
$info
[
'provides'
]]
=
$info
[
'hash'
];
// We only need to check for cycles and add this to the requires map
// if it actually requires anything.
if
(!
empty
(
$info
[
'requires'
]))
{
$resource_graph
[
$info
[
'provides'
]]
=
$info
[
'requires'
];
$requires_map
[
$info
[
'hash'
]]
=
$info
[
'requires'
];
}
}
}
$this
->
detectGraphCycles
(
$resource_graph
);
$name_map
=
ipull
(
$binary_map
,
'hash'
)
+
ipull
(
$text_map
,
'hash'
);
$hash_map
=
array_flip
(
$name_map
);
$package_map
=
$this
->
rebuildPackages
(
$this
->
resources
,
$symbol_map
,
$hash_map
);
$this
->
log
(
pht
(
'Found %d packages.'
,
count
(
$package_map
)));
$component_map
=
array
();
foreach
(
$package_map
as
$package_name
=>
$package_info
)
{
foreach
(
$package_info
[
'symbols'
]
as
$symbol
)
{
$component_map
[
$symbol
]
=
$package_name
;
}
}
$name_map
=
$this
->
mergeNameMaps
(
array
(
array
(
pht
(
'Binary'
),
ipull
(
$binary_map
,
'hash'
)),
array
(
pht
(
'Text'
),
ipull
(
$text_map
,
'hash'
)),
array
(
pht
(
'Package'
),
ipull
(
$package_map
,
'hash'
)),
));
$package_map
=
ipull
(
$package_map
,
'symbols'
);
ksort
(
$name_map
,
SORT_STRING
);
ksort
(
$symbol_map
,
SORT_STRING
);
ksort
(
$requires_map
,
SORT_STRING
);
ksort
(
$package_map
,
SORT_STRING
);
$this
->
nameMap
=
$name_map
;
$this
->
symbolMap
=
$symbol_map
;
$this
->
requiresMap
=
$requires_map
;
$this
->
packageMap
=
$package_map
;
return
$this
;
}
public
function
write
()
{
$map_content
=
$this
->
formatMapContent
(
array
(
'names'
=>
$this
->
getNameMap
(),
'symbols'
=>
$this
->
getSymbolMap
(),
'requires'
=>
$this
->
getRequiresMap
(),
'packages'
=>
$this
->
getPackageMap
(),
));
$map_path
=
$this
->
resources
->
getPathToMap
();
$this
->
log
(
pht
(
'Writing map "%s".'
,
Filesystem
::
readablePath
(
$map_path
)));
Filesystem
::
writeFile
(
$map_path
,
$map_content
);
return
$this
;
}
private
function
formatMapContent
(
array
$data
)
{
$content
=
phutil_var_export
(
$data
);
$generated
=
'@'
.
'generated'
;
return
<<<EOFILE
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
*
* {$generated}
*/
return {$content};
EOFILE;
}
/**
* Find binary resources (like PNG and SWF) and return information about
* them.
*
* @param CelerityPhysicalResources Resource map to find binary resources for.
* @return map<string, map<string, string>> Resource information map.
*/
private
function
rebuildBinaryResources
(
CelerityPhysicalResources
$resources
)
{
$binary_map
=
$resources
->
findBinaryResources
();
$result_map
=
array
();
foreach
(
$binary_map
as
$name
=>
$data_hash
)
{
$hash
=
$this
->
newResourceHash
(
$data_hash
.
$name
);
$result_map
[
$name
]
=
array
(
'hash'
=>
$hash
,
'uri'
=>
$resources
->
getResourceURI
(
$hash
,
$name
),
);
}
return
$result_map
;
}
/**
* Find text resources (like JS and CSS) and return information about them.
*
* @param CelerityPhysicalResources Resource map to find text resources for.
* @param CelerityResourceTransformer Configured resource transformer.
* @return map<string, map<string, string>> Resource information map.
*/
private
function
rebuildTextResources
(
CelerityPhysicalResources
$resources
,
CelerityResourceTransformer
$xformer
)
{
$text_map
=
$resources
->
findTextResources
();
$result_map
=
array
();
foreach
(
$text_map
as
$name
=>
$data_hash
)
{
$raw_data
=
$resources
->
getResourceData
(
$name
);
$xformed_data
=
$xformer
->
transformResource
(
$name
,
$raw_data
);
$data_hash
=
$this
->
newResourceHash
(
$xformed_data
);
$hash
=
$this
->
newResourceHash
(
$data_hash
.
$name
);
list
(
$provides
,
$requires
)
=
$this
->
getProvidesAndRequires
(
$name
,
$raw_data
);
$result_map
[
$name
]
=
array
(
'hash'
=>
$hash
,
);
if
(
$provides
!==
null
)
{
$result_map
[
$name
]
+=
array
(
'provides'
=>
$provides
,
'requires'
=>
$requires
,
);
}
}
return
$result_map
;
}
/**
* Parse the `@provides` and `@requires` symbols out of a text resource, like
* JS or CSS.
*
* @param string Resource name.
* @param string Resource data.
* @return pair<string|null, list<string>|null> The `@provides` symbol and
* the list of `@requires` symbols. If the resource is not part of the
* dependency graph, both are null.
*/
private
function
getProvidesAndRequires
(
$name
,
$data
)
{
$parser
=
new
PhutilDocblockParser
();
$matches
=
array
();
$ok
=
preg_match
(
'@/[*][*].*?[*]/@s'
,
$data
,
$matches
);
if
(!
$ok
)
{
throw
new
Exception
(
pht
(
'Resource "%s" does not have a header doc comment. Encode '
.
'dependency data in a header docblock.'
,
$name
));
}
list
(
$description
,
$metadata
)
=
$parser
->
parse
(
$matches
[
0
]);
$provides
=
$this
->
parseResourceSymbolList
(
idx
(
$metadata
,
'provides'
));
$requires
=
$this
->
parseResourceSymbolList
(
idx
(
$metadata
,
'requires'
));
if
(!
$provides
)
{
// Tests and documentation-only JS is permitted to @provide no targets.
return
array
(
null
,
null
);
}
if
(
count
(
$provides
)
>
1
)
{
throw
new
Exception
(
pht
(
'Resource "%s" must %s at most one Celerity target.'
,
$name
,
'@provide'
));
}
return
array
(
head
(
$provides
),
$requires
);
}
/**
* Check for dependency cycles in the resource graph. Raises an exception if
* a cycle is detected.
*
* @param map<string, list<string>> Map of `@provides` symbols to their
* `@requires` symbols.
* @return void
*/
private
function
detectGraphCycles
(
array
$nodes
)
{
$graph
=
id
(
new
CelerityResourceGraph
())
->
addNodes
(
$nodes
)
->
setResourceGraph
(
$nodes
)
->
loadGraph
();
foreach
(
$nodes
as
$provides
=>
$requires
)
{
$cycle
=
$graph
->
detectCycles
(
$provides
);
if
(
$cycle
)
{
throw
new
Exception
(
pht
(
'Cycle detected in resource graph: %s'
,
implode
(
' > '
,
$cycle
)));
}
}
}
/**
* Build package specifications for a given resource source.
*
* @param CelerityPhysicalResources Resource source to rebuild.
* @param map<string, string> Map of `@provides` to hashes.
* @param map<string, string> Map of hashes to resource names.
* @return map<string, map<string, string>> Package information maps.
*/
private
function
rebuildPackages
(
CelerityPhysicalResources
$resources
,
array
$symbol_map
,
array
$reverse_map
)
{
$package_map
=
array
();
$package_spec
=
$resources
->
getResourcePackages
();
foreach
(
$package_spec
as
$package_name
=>
$package_symbols
)
{
$type
=
null
;
$hashes
=
array
();
foreach
(
$package_symbols
as
$symbol
)
{
$symbol_hash
=
idx
(
$symbol_map
,
$symbol
);
if
(
$symbol_hash
===
null
)
{
throw
new
Exception
(
pht
(
'Package specification for "%s" includes "%s", but that symbol '
.
'is not %s by any resource.'
,
$package_name
,
$symbol
,
'@provided'
));
}
$resource_name
=
$reverse_map
[
$symbol_hash
];
$resource_type
=
$resources
->
getResourceType
(
$resource_name
);
if
(
$type
===
null
)
{
$type
=
$resource_type
;
}
else
if
(
$type
!==
$resource_type
)
{
throw
new
Exception
(
pht
(
'Package specification for "%s" includes resources of multiple '
.
'types (%s, %s). Each package may only contain one type of '
.
'resource.'
,
$package_name
,
$type
,
$resource_type
));
}
$hashes
[]
=
$symbol
.
':'
.
$symbol_hash
;
}
$hash
=
$this
->
newResourceHash
(
implode
(
"
\n
"
,
$hashes
));
$package_map
[
$package_name
]
=
array
(
'hash'
=>
$hash
,
'symbols'
=>
$package_symbols
,
);
}
return
$package_map
;
}
private
function
mergeNameMaps
(
array
$maps
)
{
$result
=
array
();
$origin
=
array
();
foreach
(
$maps
as
$map
)
{
list
(
$map_name
,
$data
)
=
$map
;
foreach
(
$data
as
$name
=>
$hash
)
{
if
(
empty
(
$result
[
$name
]))
{
$result
[
$name
]
=
$hash
;
$origin
[
$name
]
=
$map_name
;
}
else
{
$old
=
$origin
[
$name
];
$new
=
$map_name
;
throw
new
Exception
(
pht
(
'Resource source defines two resources with the same name, '
.
'"%s". One is defined in the "%s" map; the other in the "%s" '
.
'map. Each resource must have a unique name.'
,
$name
,
$old
,
$new
));
}
}
}
return
$result
;
}
private
function
parseResourceSymbolList
(
$list
)
{
if
(!
$list
)
{
return
array
();
}
// This is valid:
//
// @requires x y
//
// But so is this:
//
// @requires x
// @requires y
//
// Accept either form and produce a list of symbols.
$list
=
(
array
)
$list
;
// We can get `true` values if there was a bare `@requires` in the input.
foreach
(
$list
as
$key
=>
$item
)
{
if
(
$item
===
true
)
{
unset
(
$list
[
$key
]);
}
}
$list
=
implode
(
' '
,
$list
);
$list
=
trim
(
$list
);
$list
=
preg_split
(
'/
\s
+/'
,
$list
);
$list
=
array_filter
(
$list
);
return
$list
;
}
private
function
newResourceHash
(
$data
)
{
// This HMAC key is a static, hard-coded value because we don't want the
// hashes in the map to depend on database state: when two different
// developers regenerate the map, they should end up with the same output.
$hash
=
PhabricatorHash
::
digestHMACSHA256
(
$data
,
'celerity-resource-data'
);
return
substr
(
$hash
,
0
,
8
);
}
}
Event Timeline
Log In to Comment