Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F540188
CelerityResourceMapGenerator.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
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 $resources 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 $resources Resource map to find text
* resources for.
* @param CelerityResourceTransformer $xformer 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 $name Resource name.
* @param string $data 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>> $nodes 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 $resources Resource source to rebuild.
* @param map<string, string> $symbol_map Map of `@provides` to hashes.
* @param map<string, string> $reverse_map 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
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Mon, May 12, 9:28 AM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
120414
Default Alt Text
CelerityResourceMapGenerator.php (11 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment