├── .format_exclude ├── .gitignore ├── .phan └── config.php ├── .php_cs.dist ├── .travis.yml ├── CHANGELOG.md ├── COPYING ├── Makefile ├── README.md ├── RELEASE.md ├── bin ├── compile ├── package ├── profile.php ├── profilers │ ├── css-optimizer.php │ └── pcre-tokenizer.php ├── publish └── update-ca-certificates ├── composer.json ├── composer.lock ├── docker ├── entrypoint └── php74 ├── package.json ├── phpunit.xml ├── src ├── Build │ ├── ASCIIPrettyPrinter.php │ ├── Compiler.php │ └── ScriptInliner.php ├── Cache │ ├── Cache.php │ └── Sqlite │ │ ├── Cache.php │ │ ├── Connection.php │ │ └── Manager.php ├── Common │ ├── Base64url.php │ ├── JSMinifier.php │ ├── JSON.php │ ├── ObjectifiedFunctions.php │ ├── OutputBufferHandler.php │ └── System.php ├── Diagnostics │ ├── Diagnostics.php │ ├── Status.php │ └── SystemDiagnostics.php ├── Environment │ ├── Configuration.php │ ├── DefaultConfiguration.php │ ├── Exceptions │ │ ├── PackageHasNoDiagnosticsException.php │ │ └── PackageHasNoFactoryException.php │ ├── Package.php │ ├── Switches.php │ └── config-default.php ├── Exceptions │ ├── CachedExceptionException.php │ ├── ItemNotFoundException.php │ ├── LogicException.php │ ├── RuntimeException.php │ ├── UnauthorizedException.php │ └── UndefinedObjectifiedFunction.php ├── Filters │ ├── CSS │ │ ├── CSSMinifier │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── CSSURLRewriter │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── CommentsRemoval │ │ │ └── Filter.php │ │ ├── Composite │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── FontSwap │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── ImageURLRewriter │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ └── ImportsStripper │ │ │ ├── Factory.php │ │ │ └── Filter.php │ ├── HTML │ │ ├── AMPCompatibleFilter.php │ │ ├── BaseHTMLStreamFilter.php │ │ ├── BaseURLSetter │ │ │ └── Filter.php │ │ ├── CSSInlining │ │ │ ├── Factory.php │ │ │ ├── Filter.php │ │ │ ├── Optimizer.php │ │ │ ├── OptimizerFactory.php │ │ │ ├── ie-fallback.js │ │ │ └── inlined-css-retriever.js │ │ ├── CommentsRemoval │ │ │ └── Filter.php │ │ ├── Composite │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── DelayedIFrameLoading │ │ │ └── Filter.php │ │ ├── Diagnostics │ │ │ ├── Factory.php │ │ │ ├── Filter.php │ │ │ └── diagnostics.js │ │ ├── HTMLFilterFactory.php │ │ ├── HTMLPageContext.php │ │ ├── HTMLStreamFilter.php │ │ ├── Helpers │ │ │ └── JSDetectorTrait.php │ │ ├── ImagesOptimizationService │ │ │ ├── CSS │ │ │ │ ├── Factory.php │ │ │ │ └── Filter.php │ │ │ ├── ImageInliningManager.php │ │ │ ├── ImageInliningManagerFactory.php │ │ │ ├── ImageURLRewriter.php │ │ │ ├── ImageURLRewriterFactory.php │ │ │ └── Tags │ │ │ │ ├── Factory.php │ │ │ │ └── Filter.php │ │ ├── LazyImageLoading │ │ │ └── Filter.php │ │ ├── MetaCharset │ │ │ └── Filter.php │ │ ├── Minify │ │ │ └── Filter.php │ │ ├── MinifyScripts │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── PhastScriptsCompiler │ │ │ ├── Factory.php │ │ │ ├── Filter.php │ │ │ ├── PhastJavaScriptCompiler.php │ │ │ ├── es6-promise.js │ │ │ ├── hash.js │ │ │ ├── phast.js │ │ │ ├── resources-loader.js │ │ │ ├── runner.js │ │ │ └── service-url.js │ │ ├── ScriptsDeferring │ │ │ ├── Factory.php │ │ │ ├── Filter.php │ │ │ ├── rewrite.js │ │ │ └── scripts-loader.js │ │ └── ScriptsProxyService │ │ │ ├── Factory.php │ │ │ ├── Filter.php │ │ │ └── rewrite-function.js │ ├── Image │ │ ├── CommonDiagnostics │ │ │ ├── DiagnosticsRetriever.php │ │ │ ├── kibo-logo.jpg │ │ │ └── kibo-logo.png │ │ ├── Composite │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── Exceptions │ │ │ └── ImageProcessingException.php │ │ ├── Image.php │ │ ├── ImageAPIClient │ │ │ ├── Diagnostics.php │ │ │ ├── Factory.php │ │ │ └── Filter.php │ │ ├── ImageFactory.php │ │ ├── ImageFilter.php │ │ ├── ImageFilterFactory.php │ │ └── ImageImplementations │ │ │ ├── BaseImage.php │ │ │ ├── DefaultImage.php │ │ │ └── DummyImage.php │ ├── JavaScript │ │ └── Minification │ │ │ └── JSMinifierFilter.php │ ├── Service │ │ ├── CachedResultServiceFilter.php │ │ ├── CachingServiceFilter.php │ │ └── CompositeFilter.php │ └── Text │ │ └── Decode │ │ ├── Factory.php │ │ └── Filter.php ├── HTTP │ ├── CURLClient.php │ ├── Client.php │ ├── ClientFactory.php │ ├── Exceptions │ │ ├── HTTPError.php │ │ └── NetworkError.php │ ├── Request.php │ ├── Response.php │ └── cacert.pem ├── JSMin │ ├── JSMin.php │ ├── UnterminatedCommentException.php │ ├── UnterminatedRegExpException.php │ └── UnterminatedStringException.php ├── Logging │ ├── Common │ │ └── JSONLFileLogTrait.php │ ├── Log.php │ ├── LogEntry.php │ ├── LogLevel.php │ ├── LogReader.php │ ├── LogReaders │ │ └── JSONLFile │ │ │ └── Reader.php │ ├── LogWriter.php │ ├── LogWriters │ │ ├── BaseLogWriter.php │ │ ├── Composite │ │ │ ├── Factory.php │ │ │ └── Writer.php │ │ ├── Dummy │ │ │ └── Writer.php │ │ ├── Factory.php │ │ ├── JSONLFile │ │ │ ├── Factory.php │ │ │ └── Writer.php │ │ ├── PHPError │ │ │ ├── Factory.php │ │ │ └── Writer.php │ │ └── RotatingTextFile │ │ │ ├── Factory.php │ │ │ └── Writer.php │ ├── Logger.php │ └── LoggingTrait.php ├── Parsing │ └── HTML │ │ ├── HTMLInfo.php │ │ ├── HTMLStreamElements │ │ ├── ClosingTag.php │ │ ├── Comment.php │ │ ├── Element.php │ │ ├── Junk.php │ │ └── Tag.php │ │ └── PCRETokenizer.php ├── PhastDocumentFilters.php ├── PhastServices.php ├── Retrievers │ ├── CachingRetriever.php │ ├── DynamicCacheSaltTrait.php │ ├── LocalRetriever.php │ ├── PostDataRetriever.php │ ├── RemoteRetriever.php │ ├── RemoteRetrieverFactory.php │ ├── Retriever.php │ └── UniversalRetriever.php ├── Security │ ├── ServiceSignature.php │ └── ServiceSignatureFactory.php ├── Services │ ├── BaseService.php │ ├── Bundler │ │ ├── BundlerParamsParser.php │ │ ├── Factory.php │ │ ├── Service.php │ │ ├── ServiceParams.php │ │ ├── ShortBundlerParamsParser.php │ │ ├── TokenRefMaker.php │ │ └── TokenRefMakerFactory.php │ ├── Css │ │ ├── Factory.php │ │ └── Service.php │ ├── Diagnostics │ │ ├── Factory.php │ │ └── Service.php │ ├── Factory.php │ ├── Images │ │ ├── Factory.php │ │ └── Service.php │ ├── Scripts │ │ ├── Factory.php │ │ └── Service.php │ ├── ServiceFactoryTrait.php │ ├── ServiceFilter.php │ └── ServiceRequest.php ├── ValueObjects │ ├── PhastJavaScript.php │ ├── Query.php │ ├── Resource.php │ └── URL.php ├── bootstrap.php ├── config-user.php ├── html-filters.php └── services.php ├── test ├── bootstrap.php ├── browser │ ├── bundler.php │ ├── index.php │ ├── phast-config.php │ ├── phast.php │ ├── public │ │ ├── es6-promise.js │ │ ├── hash.js │ │ ├── resources-loader.js │ │ ├── scripts-loader.js │ │ └── service-url.js │ ├── qunit.css │ ├── qunit.js │ ├── res │ │ ├── .htaccess │ │ ├── python.png │ │ ├── script_proxy.js │ │ ├── stylesheet.css │ │ ├── stylesheet_import.css │ │ ├── stylesheet_large.css │ │ ├── text-1.txt │ │ ├── text-2.txt │ │ └── text-3.txt │ ├── tests.js │ └── tests │ │ ├── _.php │ │ ├── csp.js │ │ ├── csp.php │ │ ├── csp_report_only.js │ │ ├── csp_report_only.php │ │ ├── currentscript.js │ │ ├── currentscript.php │ │ ├── currentscript.script.js │ │ ├── currentscript_async.js │ │ ├── currentscript_async.js.php │ │ ├── currentscript_async.php │ │ ├── document_write.js │ │ ├── document_write.php │ │ ├── document_write_async.js │ │ ├── document_write_async.load.js │ │ ├── document_write_async.php │ │ ├── document_write_read.js │ │ ├── document_write_read.php │ │ ├── document_write_restore.js │ │ ├── document_write_restore.php │ │ ├── document_write_script.js │ │ ├── document_write_script.load.js │ │ ├── document_write_script.php │ │ ├── event_count.js │ │ ├── event_count.php │ │ ├── hello_world.js │ │ ├── hello_world.php │ │ ├── iframe.content.php │ │ ├── iframe.js │ │ ├── iframe.php │ │ ├── iframe_js.js │ │ ├── iframe_js.php │ │ ├── inline_js.js │ │ ├── inline_js.php │ │ ├── multiple_set_timeout_before_onload.js │ │ ├── multiple_set_timeout_before_onload.php │ │ ├── request_animation_frame_before_onload.js │ │ ├── request_animation_frame_before_onload.php │ │ ├── resources_loader.js │ │ ├── rewrite_image_in_inline_style.js │ │ ├── rewrite_image_in_inline_style.php │ │ ├── script_error_fallback.js │ │ ├── script_error_fallback.php │ │ ├── script_once.done.js │ │ ├── script_once.js │ │ ├── script_once.php │ │ ├── script_once.script.js │ │ ├── script_order.defer.js │ │ ├── script_order.js │ │ ├── script_order.php │ │ ├── script_order.script.php │ │ ├── script_proxy_dynamic.js │ │ ├── script_proxy_dynamic.module.js │ │ ├── script_proxy_dynamic.php │ │ ├── script_proxy_php.js │ │ ├── script_proxy_php.php │ │ ├── script_proxy_php_script.php │ │ ├── script_proxy_static.js │ │ ├── script_proxy_static.php │ │ ├── scripts_load_external_async.js │ │ ├── scripts_load_external_async.php │ │ ├── scripts_loader.js │ │ ├── set_timeout_args.js │ │ ├── set_timeout_args.php │ │ ├── stylesheet.js │ │ ├── stylesheet.php │ │ ├── stylesheet_before_script.js │ │ ├── stylesheet_before_script.php │ │ ├── stylesheet_external.js │ │ ├── stylesheet_external.php │ │ ├── stylesheet_import.js │ │ ├── stylesheet_import.php │ │ ├── stylesheet_large.js │ │ ├── stylesheet_large.php │ │ ├── stylesheet_php.js │ │ ├── stylesheet_php.php │ │ └── stylesheet_php_css.php ├── browserstack │ ├── .gitignore │ ├── run │ └── test.php ├── php │ ├── Cache │ │ └── Sqlite │ │ │ └── CacheTest.php │ ├── Common │ │ ├── ObjectifiedFunctionsTest.php │ │ ├── OutputBufferHandlerTest.php │ │ └── PhastServicesTest.php │ ├── Environment │ │ ├── ConfigurationTest.php │ │ ├── PackageTest.php │ │ ├── TestPackage1 │ │ │ ├── Diagnostics.php │ │ │ └── Filter.php │ │ └── TestPackage2 │ │ │ ├── Cache.php │ │ │ └── Factory.php │ ├── Filters │ │ ├── CSS │ │ │ ├── CSSMinifier │ │ │ │ └── FilterTest.php │ │ │ ├── CSSURLRewriter │ │ │ │ └── CSSURLRewriterTest.php │ │ │ ├── Composite │ │ │ │ └── FilterTest.php │ │ │ ├── FontSwap │ │ │ │ └── FilterTest.php │ │ │ └── ImportsStripper │ │ │ │ └── FilterTest.php │ │ ├── HTML │ │ │ ├── BaseURLSetter │ │ │ │ └── FilterTest.php │ │ │ ├── CSSInlining │ │ │ │ ├── FilterTest.php │ │ │ │ └── OptimizerTest.php │ │ │ ├── CommentsRemoval │ │ │ │ └── FilterTest.php │ │ │ ├── Composite │ │ │ │ └── FilterTest.php │ │ │ ├── DelayedIFrameLoading │ │ │ │ └── FilterTest.php │ │ │ ├── HTMLFilterTestCase.php │ │ │ ├── ImagesOptimizationService │ │ │ │ ├── CSS │ │ │ │ │ └── FilterTest.php │ │ │ │ ├── ImageInliningManagerTest.php │ │ │ │ ├── ImageURLRewriterTest.php │ │ │ │ └── Tags │ │ │ │ │ └── TagsFilterTest.php │ │ │ ├── MetaCharset │ │ │ │ └── FilterTest.php │ │ │ ├── Minify │ │ │ │ └── FilterTest.php │ │ │ ├── MinifyScripts │ │ │ │ └── FilterTest.php │ │ │ ├── PhastScriptsCompiler │ │ │ │ ├── FilterTest.php │ │ │ │ └── PhastJavaScriptCompilerTest.php │ │ │ ├── ScriptsDeferring │ │ │ │ └── FilterTest.php │ │ │ └── ScriptsProxyService │ │ │ │ └── FilterTest.php │ │ ├── Image │ │ │ └── Composite │ │ │ │ └── FilterTest.php │ │ ├── JavaScript │ │ │ └── Minification │ │ │ │ └── JSMinifierFilterTest.php │ │ ├── Service │ │ │ ├── CachingServiceFilterTest.php │ │ │ └── CompositeFilterTest.php │ │ └── Text │ │ │ └── Decode │ │ │ └── FilterTest.php │ ├── HTTP │ │ ├── CURLClientTest.php │ │ ├── DefaultConfigurationTest.php │ │ └── RequestTest.php │ ├── Logging │ │ ├── LogReaders │ │ │ └── JSONLFile │ │ │ │ └── ReaderTest.php │ │ ├── LogWriters │ │ │ ├── BaseLogWriterTest.php │ │ │ ├── Composite │ │ │ │ └── WriterTest.php │ │ │ ├── JSONLFile │ │ │ │ └── WriterTest.php │ │ │ ├── PHPError │ │ │ │ └── WriterTest.php │ │ │ └── RotatingTextFile │ │ │ │ └── WriterTest.php │ │ └── LoggerTest.php │ ├── Parsing │ │ └── HTML │ │ │ ├── HTMLStreamElements │ │ │ └── TagTest.php │ │ │ └── PCRETokenizerTest.php │ ├── PhastDocumentFiltersTest.php │ ├── PhastTestCase.php │ ├── Retrievers │ │ ├── LocalRetrieverTest.php │ │ └── PostDataRetrieverTest.php │ ├── Security │ │ └── ServiceSignatureTest.php │ ├── Services │ │ ├── Bundler │ │ │ ├── BundlerParamsParserTest.php │ │ │ ├── ServiceTest.php │ │ │ └── ShortBundlerParamsParserTest.php │ │ ├── Scripts │ │ │ └── ServiceTest.php │ │ └── ServiceRequestTest.php │ ├── ValueObjects │ │ ├── PhastJavaScriptTest.php │ │ ├── ResourceTest.php │ │ └── URLTest.php │ ├── build.php │ └── resources │ │ └── large.css └── test-app │ ├── css │ ├── .htaccess │ ├── styles.css │ ├── styles2.css │ └── stylesheet_large.css │ ├── images │ ├── .htaccess │ ├── basset.png │ ├── basset@2x.png │ ├── batman.jpg │ ├── batman@2x.jpg │ └── python.png │ ├── index.php │ ├── js │ ├── .htaccess │ ├── deferred.js │ ├── injected.js │ └── main.js │ ├── phast.php │ └── test-config.php └── yarn.lock /.format_exclude: -------------------------------------------------------------------------------- 1 | src/Filters/HTML/PhastScriptsCompiler/es6-promise.js 2 | src/Filters/HTML/PhastScriptsCompiler/hash.js 3 | test/browser/qunit.js 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /docker/*.image 4 | /node_modules 5 | /.php_cs.cache 6 | /.phpunit.result.cache 7 | /vendor 8 | -------------------------------------------------------------------------------- /.phan/config.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'src', 6 | 'vendor', 7 | ], 8 | 9 | 'exclude_analysis_directory_list' => [ 10 | 'vendor', 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 5 | ->exclude('JSMin') 6 | ->in(__DIR__ . '/test') 7 | ; 8 | 9 | return PhpCsFixer\Config::create() 10 | ->setRules([ 11 | '@PSR1' => true, 12 | '@PSR2' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'braces' => ['position_after_functions_and_oop_constructs' => 'same'], 15 | 'cast_spaces' => true, 16 | 'class_attributes_separation' => true, 17 | 'concat_space' => ['spacing' => 'one'], 18 | 'include' => true, 19 | 'new_with_braces' => true, 20 | 'no_superfluous_elseif' => true, 21 | 'no_unneeded_control_parentheses' => true, 22 | 'no_unused_imports' => true, 23 | 'no_useless_else' => true, 24 | 'no_useless_return' => true, 25 | 'no_whitespace_before_comma_in_array' => true, 26 | 'no_whitespace_in_blank_line' => true, 27 | 'ordered_imports' => true, 28 | 'return_assignment' => true, 29 | 'return_type_declaration' => true, 30 | 'single_quote' => true, 31 | 'trailing_comma_in_multiline_array' => true, 32 | 'trim_array_spaces' => true, 33 | ]) 34 | ->setFinder($finder) 35 | ; 36 | 37 | // vim: set ft=php : 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.6 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | install: 8 | - composer install 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DELETE_ON_ERROR : 2 | 3 | PHP74 = docker run -v $(shell pwd):/data -w /data $(shell cat docker/php74.image) 4 | JSMIN_SOURCES := $(wildcard vendor/kiboit/jsmin-php/src/JSMin/*.php) 5 | JSMIN_TARGETS := $(patsubst vendor/kiboit/jsmin-php/src/%,src/%,$(JSMIN_SOURCES)) 6 | 7 | ifdef FILTER 8 | PHPUNIT_ARGS := --filter="$(FILTER)" 9 | endif 10 | 11 | .PHONY : all 12 | all : build/phast.php 13 | 14 | .PHONY : watch 15 | watch : 16 | git ls-files src test | entr -c -r $(MAKE) -s test74 17 | 18 | .PHONY : test 19 | test : test74 20 | 21 | .PHONY : test74 22 | test74 : all docker/php74.image 23 | $(PHP74) vendor/bin/phpunit $(PHPUNIT_ARGS) 24 | 25 | .PHONY : test-local 26 | test-local : all 27 | vendor/bin/phpunit 28 | 29 | .PHONY : dist 30 | dist : all 31 | bin/package 32 | 33 | .PHONY : clean 34 | clean : 35 | rm -rf build docker/php74.image 36 | 37 | .PHONY : format 38 | format : node_modules 39 | fd -g '*.js' -t f --ignore-file .format_exclude -X node_modules/.bin/prettier -w 40 | 41 | 42 | build/phast.php : vendor/autoload.php node_modules $(JSMIN_TARGETS) $(shell git ls-files src) 43 | bin/compile $(dir $@) 44 | 45 | src/JSMin/% : vendor/kiboit/jsmin-php/src/JSMin/% | vendor/autoload.php 46 | @mkdir -p $(dir $@) 47 | cat $< | perl -p -e 's~(\bnamespace\s+)(?=JSMin\b)~$$1Kibo\\Phast\\~g' > $@ 48 | 49 | vendor/autoload.php : composer.json composer.lock 50 | composer install 51 | touch vendor/autoload.php 52 | 53 | docker/%.image : docker/% docker/entrypoint 54 | docker build --pull --iidfile $@~ -f $< docker 55 | mv $@~ $@ 56 | 57 | node_modules : yarn.lock 58 | yarn 59 | touch $@ 60 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release instructions 2 | 3 | ## Phast 4 | 5 | We follow semantic versioning. Did you only fix bugs, then increment the 6 | patchlevel. Did you also add any new features, then increment the minor version 7 | and reset the patchlevel. In case of breaking changes we will increment the 8 | major version. 9 | 10 | Follow these steps to release a new version of Phast: 11 | 12 | 1. Update the change log. 13 | 1. Make sure all important changes since the last release are mentioned. 14 | 1. Make sure the header links are set. (Version numbers link to GitHub log.) 15 | 1. Commit changelog. 16 | 1. Tag release: `git tag 1.5.0` 17 | 1. Push commits and tags: `git push --tags origin master` 18 | 1. Release PhastPress. 19 | 20 | ## PhastPress 21 | 22 | PhastPress versions follow those of Phast. If a change is made to PhastPress 23 | without an accompanying update of Phast itself, a letter is used to indicate the 24 | change. Eg, 1.5.1 → 1.5.1a. 25 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | \n"); 8 | exit(1); 9 | } 10 | 11 | Kibo\Phast\Build\Compiler::getPhastCompiler()->compile($argv[1]); 12 | -------------------------------------------------------------------------------- /bin/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd "$(dirname "$0")"/.. 5 | 6 | release="$(git tag -l --points-at=HEAD | grep -P '^[0-9]\.' || :)" 7 | if [[ $release = "" ]]; then 8 | release=HEAD 9 | fi 10 | 11 | tmp="$(mktemp -d)" 12 | trap "rm -rf '$tmp'" EXIT 13 | 14 | mkdir -p dist 15 | 16 | git archive HEAD composer.{json,lock} src certificates | tar xf - -C "$tmp" 17 | 18 | ( 19 | cd "$tmp" 20 | composer install -o --no-dev 21 | rm -f composer.{json,lock} 22 | find -not -name . -not -name .. -name '.*' -exec rm -r {} + 23 | tar cJf - --xform 's/^\./phast/' . 24 | ) > dist/phast-$release.txz 25 | 26 | echo dist/phast-$release.txz 27 | -------------------------------------------------------------------------------- /bin/profile.php: -------------------------------------------------------------------------------- 1 | \n"); 12 | exit(1); 13 | } 14 | 15 | require_once __DIR__ . '/../vendor/autoload.php'; 16 | 17 | $file = $argv[1]; 18 | $iterations = (int) $argv[2]; 19 | 20 | $callback = require_once $file; 21 | if (!is_callable($callback)) { 22 | fwrite(STDERR, "$file did not return a callable\n"); 23 | exit(1); 24 | } 25 | 26 | $profile = function_exists('tideways_xhprof_enable'); 27 | 28 | if (!$profile) { 29 | fwrite(STDERR, "Warning: Tideways/XHProf extension is missing; not profiling...\n"); 30 | } 31 | 32 | if ($profile) { 33 | tideways_xhprof_enable(); 34 | } 35 | 36 | $timeStart = microtime(true); 37 | for ($i = 0; $i < $iterations; $i++) { 38 | $callback(); 39 | } 40 | $timeEnd = microtime(true); 41 | 42 | $timeElapsed = $timeEnd - $timeStart; 43 | fprintf(STDERR, "Ran %d iterations in %.4fs (%.4fs/iteration)\n", $iterations, $timeElapsed, $timeElapsed/$iterations); 44 | 45 | if ($profile) { 46 | echo serialize(tideways_xhprof_disable()); 47 | } 48 | -------------------------------------------------------------------------------- /bin/profilers/css-optimizer.php: -------------------------------------------------------------------------------- 1 | getMock(Cache::class, [], [], '', false); 11 | 12 | $optimizer = new Optimizer(new \ArrayIterator([]), $cache); 13 | 14 | return Closure::bind(function () use ($css) { 15 | $this->parseCSS($css); 16 | }, $optimizer, $optimizer); 17 | }); 18 | -------------------------------------------------------------------------------- /bin/profilers/pcre-tokenizer.php: -------------------------------------------------------------------------------- 1 | tokenize($data)); 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /bin/publish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | git describe --tags --exact-match HEAD 6 | 7 | git push git@github.com:kiboit/phast master --tags 8 | -------------------------------------------------------------------------------- /bin/update-ca-certificates: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd "$(dirname "$0")/.." 5 | 6 | file="src/HTTP/cacert.pem" 7 | 8 | wget -O "$file~" "https://curl.haxx.se/ca/cacert.pem" 9 | mv "$file~" "$file" 10 | 11 | git commit -m "Update CA certificates" "$file" 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kiboit/phast", 3 | "description": "A toolbox for optimizing web page performance", 4 | "type": "library", 5 | "license": "AGPL-3.0-or-later", 6 | "repositories": [ 7 | { 8 | "type": "vcs", 9 | "url": "https://github.com/kiboit/jsmin-php" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.2", 14 | "ext-json": "*" 15 | }, 16 | "require-dev": { 17 | "facebook/webdriver": "^1.5", 18 | "ext-dom": "*", 19 | "friendsofphp/php-cs-fixer": "^2.16", 20 | "kiboit/jsmin-php": "dev-master", 21 | "nikic/php-parser": "^4.13", 22 | "phpunit/phpunit": "^9.5" 23 | }, 24 | "suggest": { 25 | "nikic/php-parser": "Needed for compilation" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Kibo\\Phast\\": [ 30 | "src/" 31 | ] 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Kibo\\Phast\\": [ 37 | "test/php/" 38 | ] 39 | } 40 | }, 41 | "config": { 42 | "platform": { 43 | "php": "7.4" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docker/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | if [ -d /data ]; then 6 | usermod -u `stat -c %u /data` docker 2>&1 | grep -v '^usermod: no changes' || : 7 | fi 8 | 9 | sudo -u docker "$@" 10 | -------------------------------------------------------------------------------- /docker/php74: -------------------------------------------------------------------------------- 1 | FROM alpine:3.15 2 | 3 | RUN apk add --no-cache \ 4 | shadow \ 5 | sudo \ 6 | php7 \ 7 | php7-dom \ 8 | php7-json \ 9 | php7-mbstring \ 10 | php7-tokenizer \ 11 | php7-xml \ 12 | php7-xmlwriter \ 13 | php7-pdo_sqlite \ 14 | php7-posix 15 | 16 | RUN useradd -m docker 17 | 18 | COPY entrypoint /entrypoint 19 | ENTRYPOINT ["/bin/sh", "/entrypoint"] 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "uglify-js": "^3.6.0" 4 | }, 5 | "devDependencies": { 6 | "prettier": "^2.3.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | test/ 10 | test/php/HTTP/CURLClientTest.php 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Build/ASCIIPrettyPrinter.php: -------------------------------------------------------------------------------- 1 | cacheRoot = (string) $config['cacheRoot']; 23 | $this->name = (string) ($config['name'] ?? 'cache'); 24 | $this->maxSize = (int) $config['maxSize']; 25 | $this->namespace = $namespace; 26 | $this->functions = $functions ?? new ObjectifiedFunctions(); 27 | } 28 | 29 | public function get($key, callable $fn = null, $expiresIn = 0) { 30 | return $this->getManager()->get($this->getKey($key), $fn, $expiresIn, $this->functions); 31 | } 32 | 33 | private function getKey(string $key): string { 34 | return $this->namespace . "\0" . $key; 35 | } 36 | 37 | public function set($key, $value, $expiresIn = 0): void { 38 | $this->getManager()->set($this->getKey($key), $value, $expiresIn, $this->functions); 39 | } 40 | 41 | public function getManager(): Manager { 42 | $key = $this->cacheRoot . '/' . $this->name; 43 | if (!isset(self::$managers[$key])) { 44 | self::$managers[$key] = new Manager($this->cacheRoot, $this->name, $this->maxSize); 45 | } 46 | return self::$managers[$key]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Cache/Sqlite/Connection.php: -------------------------------------------------------------------------------- 1 | statements[$query])) { 13 | $this->statements[$query] = parent::prepare($query); 14 | } 15 | return $this->statements[$query]; 16 | } 17 | 18 | public function getPageSize(): int { 19 | return (int) $this->query('PRAGMA page_size')->fetchColumn(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Common/Base64url.php: -------------------------------------------------------------------------------- 1 | removeLicenseHeaders = $removeLicenseHeaders; 12 | } 13 | 14 | protected function consumeMultipleLineComment() { 15 | parent::consumeMultipleLineComment(); 16 | if ($this->removeLicenseHeaders) { 17 | $this->keptComment = preg_replace('~/\*!.*?\*/~s', '', $this->keptComment); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Common/ObjectifiedFunctions.php: -------------------------------------------------------------------------------- 1 | $name) && is_callable($this->$name)) { 14 | $fn = $this->$name; 15 | return $fn(...$arguments); 16 | } 17 | if (function_exists($name)) { 18 | return $name(...$arguments); 19 | } 20 | throw new UndefinedObjectifiedFunction("Undefined objectified function $name"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/System.php: -------------------------------------------------------------------------------- 1 | functions = $functions; 16 | } 17 | 18 | public function getUserId() { 19 | try { 20 | return (int) $this->functions->posix_geteuid(); 21 | } catch (UndefinedObjectifiedFunction $e) { 22 | return 0; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Diagnostics/Diagnostics.php: -------------------------------------------------------------------------------- 1 | package = $package; 37 | $this->available = $available; 38 | $this->reason = $reason; 39 | $this->enabled = $enabled; 40 | } 41 | 42 | /** 43 | * @return Package 44 | */ 45 | public function getPackage() { 46 | return $this->package; 47 | } 48 | 49 | /** 50 | * @return bool 51 | */ 52 | public function isAvailable() { 53 | return $this->available; 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function getReason() { 60 | return $this->reason; 61 | } 62 | 63 | /** 64 | * @return bool 65 | */ 66 | public function isEnabled() { 67 | return $this->enabled; 68 | } 69 | 70 | public function toArray(): array { 71 | return [ 72 | 'package' => [ 73 | 'type' => $this->package->getType(), 74 | 'name' => $this->package->getNamespace(), 75 | ], 76 | 'available' => $this->available, 77 | 'reason' => $this->reason, 78 | 'enabled' => $this->enabled, 79 | ]; 80 | } 81 | 82 | public function jsonSerialize(): array { 83 | return $this->toArray(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Environment/Exceptions/PackageHasNoDiagnosticsException.php: -------------------------------------------------------------------------------- 1 | true, 12 | self::SWITCH_DIAGNOSTICS => false, 13 | ]; 14 | 15 | private $switches = []; 16 | 17 | public static function fromArray(array $switches) { 18 | $instance = new self(); 19 | $instance->switches = array_merge($instance->switches, $switches); 20 | return $instance; 21 | } 22 | 23 | public static function fromString($switches) { 24 | $instance = new self(); 25 | if (empty($switches)) { 26 | return $instance; 27 | } 28 | foreach (explode(',', $switches) as $switch) { 29 | if ($switch[0] == '-') { 30 | $instance->switches[substr($switch, 1)] = false; 31 | } else { 32 | $instance->switches[$switch] = true; 33 | } 34 | } 35 | return $instance; 36 | } 37 | 38 | public function merge(Switches $switches) { 39 | $instance = new self(); 40 | $instance->switches = array_merge($this->switches, $switches->switches); 41 | return $instance; 42 | } 43 | 44 | public function isOn($switch) { 45 | if (isset($this->switches[$switch])) { 46 | return (bool) $this->switches[$switch]; 47 | } 48 | if (isset(self::$defaults[$switch])) { 49 | return (bool) self::$defaults[$switch]; 50 | } 51 | return true; 52 | } 53 | 54 | public function toArray() { 55 | return array_merge(self::$defaults, $this->switches); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Environment/config-default.php: -------------------------------------------------------------------------------- 1 | url = $failed; 17 | } 18 | 19 | /** 20 | * @return URL 21 | */ 22 | public function getUrl() { 23 | return $this->url; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exceptions/LogicException.php: -------------------------------------------------------------------------------- 1 | getContent(); 16 | 17 | // Normalize whitespace 18 | $content = preg_replace('~\s+~', ' ', $content); 19 | 20 | // Remove whitespace before and after operators 21 | $chars = [',', '{', '}', ';']; 22 | foreach ($chars as $char) { 23 | $content = str_replace("$char ", $char, $content); 24 | $content = str_replace(" $char", $char, $content); 25 | } 26 | 27 | // Remove whitespace after colons 28 | $content = str_replace(': ', ':', $content); 29 | 30 | return $resource->withContent(trim($content)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Filters/CSS/CSSURLRewriter/Factory.php: -------------------------------------------------------------------------------- 1 | getUrl(); 18 | $callback = function ($match) use ($baseUrl) { 19 | if (preg_match('~^[a-z]+:|^#~i', $match[3])) { 20 | return $match[0]; 21 | } 22 | return $match[1] . URL::fromString($match[3])->withBase($baseUrl) . $match[4]; 23 | }; 24 | 25 | $cssContent = preg_replace_callback( 26 | '~ 27 | \b 28 | ( url\( \s*+ ([\'"]?) ) 29 | ([A-Za-z0-9_/.:?&=+%,#@-]+) 30 | ( \2 \s*+ \) ) 31 | ~x', 32 | $callback, 33 | $resource->getContent() 34 | ); 35 | 36 | $cssContent = preg_replace_callback( 37 | '~ 38 | ( @import \s+ ([\'"]) ) 39 | ([A-Za-z0-9_/.:?&=+%,#@-]+) 40 | ( \2 ) 41 | ~x', 42 | $callback, 43 | $cssContent 44 | ); 45 | 46 | return $resource->withContent($cssContent); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Filters/CSS/CommentsRemoval/Filter.php: -------------------------------------------------------------------------------- 1 | getContent()); 12 | return $resource->withContent($content); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Filters/CSS/Composite/Factory.php: -------------------------------------------------------------------------------- 1 | addFilter( 24 | Package::fromPackageClass($filterClass) 25 | ->getFactory() 26 | ->make($config) 27 | ); 28 | } 29 | return $filter; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Filters/CSS/Composite/Filter.php: -------------------------------------------------------------------------------- 1 | addFilter(new CommentsRemoval\Filter()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Filters/CSS/FontSwap/Factory.php: -------------------------------------------------------------------------------- 1 | fontDisplayBlockPattern = $this->getFontDisplayBlockPattern(); 23 | } 24 | 25 | public function apply(Resource $resource, array $request) { 26 | $css = $resource->getContent(); 27 | $filtered = preg_replace_callback(self::FONT_FACE_REGEXP, function ($match) { 28 | list($block, $start, $contents) = $match; 29 | $mode = preg_match($this->fontDisplayBlockPattern, $contents) ? 'block' : 'swap'; 30 | return $start . 'font-display:' . $mode . ';' . $contents; 31 | }, $css); 32 | return $resource->withContent($filtered); 33 | } 34 | 35 | private function getFontDisplayBlockPattern() { 36 | $patterns = []; 37 | foreach (self::ICON_FONT_FAMILIES as $family) { 38 | $chars = str_split($family); 39 | $chars = array_map(function ($char) { 40 | return preg_quote($char, '~'); 41 | }, $chars); 42 | $patterns[] = implode('\s*', $chars); 43 | } 44 | return '~' . implode('|', $patterns) . '~i'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Filters/CSS/ImageURLRewriter/Factory.php: -------------------------------------------------------------------------------- 1 | make($config, Filter::class) 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Filters/CSS/ImageURLRewriter/Filter.php: -------------------------------------------------------------------------------- 1 | rewriter = $rewriter; 21 | } 22 | 23 | public function getCacheSalt(Resource $resource, array $request) { 24 | return $this->rewriter->getCacheSalt(); 25 | } 26 | 27 | public function apply(Resource $resource, array $request) { 28 | $content = $this->rewriter->rewriteStyle($resource->getContent()); 29 | $dependencies = $this->rewriter->getInlinedResources(); 30 | return $resource->withContent($content) 31 | ->withDependencies($dependencies); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Filters/CSS/ImportsStripper/Factory.php: -------------------------------------------------------------------------------- 1 | shouldStripImports($request) ? 'strip-imports' : 'no-strip-imports'; 16 | } 17 | 18 | public function apply(Resource $resource, array $request) { 19 | if (!$this->shouldStripImports($request)) { 20 | $this->logger()->info('No import stripping requested! Skipping!'); 21 | return $resource; 22 | } 23 | $css = $resource->getContent(); 24 | $stripped = preg_replace(CSSInlining\Filter::CSS_IMPORTS_REGEXP, '', $css); 25 | return $resource->withContent($stripped); 26 | } 27 | 28 | private function shouldStripImports(array $request) { 29 | return isset($request['strip-imports']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Filters/HTML/AMPCompatibleFilter.php: -------------------------------------------------------------------------------- 1 | context = $context; 28 | $this->elements = $elements; 29 | $this->beforeLoop(); 30 | foreach ($this->elements as $element) { 31 | if (($element instanceof Tag) && $this->isTagOfInterest($element)) { 32 | foreach ($this->handleTag($element) as $item) { 33 | yield $item; 34 | } 35 | } else { 36 | yield $element; 37 | } 38 | } 39 | $this->afterLoop(); 40 | } 41 | 42 | /** 43 | * @param Tag $tag 44 | * @return bool 45 | */ 46 | protected function isTagOfInterest(Tag $tag) { 47 | return true; 48 | } 49 | 50 | protected function beforeLoop() { 51 | } 52 | 53 | protected function afterLoop() { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Filters/HTML/BaseURLSetter/Filter.php: -------------------------------------------------------------------------------- 1 | getTagName() == 'base' && $tag->hasAttribute('href'); 13 | } 14 | 15 | protected function handleTag(Tag $tag) { 16 | $base = URL::fromString($tag->getAttribute('href')); 17 | $current = $this->context->getBaseUrl(); 18 | $this->context->setBaseUrl($base->withBase($current)); 19 | yield $tag; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Filters/HTML/CSSInlining/Factory.php: -------------------------------------------------------------------------------- 1 | addRetriever($localRetriever); 21 | $retriever->addRetriever( 22 | new CachingRetriever( 23 | new Cache($config['cache'], 'css') 24 | ) 25 | ); 26 | 27 | if (!isset($config['documents']['filters'][Filter::class]['serviceUrl'])) { 28 | $config['documents']['filters'][Filter::class]['serviceUrl'] = $config['servicesUrl']; 29 | } 30 | 31 | return new Filter( 32 | (new ServiceSignatureFactory())->make($config), 33 | URL::fromString($config['documents']['baseUrl']), 34 | $config['documents']['filters'][Filter::class], 35 | $localRetriever, 36 | $retriever, 37 | new OptimizerFactory($config), 38 | (new CSSCompositeFactory())->make($config), 39 | (new TokenRefMakerFactory())->make($config), 40 | $config['csp']['nonce'] 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Filters/HTML/CSSInlining/OptimizerFactory.php: -------------------------------------------------------------------------------- 1 | cache = new Cache($config['cache'], 'css-optimizitor'); 15 | } 16 | 17 | /** 18 | * @param \Traversable $elements 19 | * @return Optimizer 20 | */ 21 | public function makeForElements(\Traversable $elements) { 22 | return new Optimizer($elements, $this->cache); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Filters/HTML/CSSInlining/ie-fallback.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // https://github.com/filamentgroup/woff2-feature-test/blob/master/woff2.js 3 | var supportsWoff2 = (function () { 4 | if (!("FontFace" in window)) { 5 | return false; 6 | } 7 | 8 | var f = new FontFace( 9 | "t", 10 | 'url( "data:font/woff2;base64,d09GMgABAAAAAADwAAoAAAAAAiQAAACoAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAALAogOAE2AiQDBgsGAAQgBSAHIBuDAciO1EZ3I/mL5/+5/rfPnTt9/9Qa8H4cUUZxaRbh36LiKJoVh61XGzw6ufkpoeZBW4KphwFYIJGHB4LAY4hby++gW+6N1EN94I49v86yCpUdYgqeZrOWN34CMQg2tAmthdli0eePIwAKNIIRS4AGZFzdX9lbBUAQlm//f262/61o8PlYO/D1/X4FrWFFgdCQD9DpGJSxmFyjOAGUU4P0qigcNb82GAAA" ) format( "woff2" )', 11 | {} 12 | ); 13 | f.load()["catch"](function () {}); 14 | 15 | return f.status == "loading" || f.status == "loaded"; 16 | })(); 17 | 18 | if (supportsWoff2) { 19 | return; 20 | } 21 | 22 | console.log( 23 | "[Phast] Browser does not support WOFF2, falling back to original stylesheets" 24 | ); 25 | 26 | Array.prototype.forEach.call( 27 | document.querySelectorAll("style[data-phast-ie-fallback-url]"), 28 | function (el) { 29 | var link = document.createElement("link"); 30 | if (el.hasAttribute("media")) { 31 | link.setAttribute("media", el.getAttribute("media")); 32 | } 33 | link.setAttribute("rel", "stylesheet"); 34 | link.setAttribute("href", el.getAttribute("data-phast-ie-fallback-url")); 35 | el.parentNode.insertBefore(link, el); 36 | el.parentNode.removeChild(el); 37 | } 38 | ); 39 | 40 | Array.prototype.forEach.call( 41 | document.querySelectorAll("style[data-phast-nested-inlined]"), 42 | function (groupEl) { 43 | groupEl.parentNode.removeChild(groupEl); 44 | } 45 | ); 46 | })(); 47 | -------------------------------------------------------------------------------- /src/Filters/HTML/CSSInlining/inlined-css-retriever.js: -------------------------------------------------------------------------------- 1 | /* globals phast */ 2 | 3 | phast.stylesLoading = 0; 4 | 5 | var resourceLoader = phast.ResourceLoader.instance; 6 | 7 | phast.forEachSelectedElement("style[data-phast-params]", function (style) { 8 | var textParams = style.getAttribute("data-phast-params"); 9 | var params = phast.ResourceLoader.RequestParams.fromString(textParams); 10 | phast.stylesLoading++; 11 | resourceLoader 12 | .get(params) 13 | .then(function (css) { 14 | style.textContent = css; 15 | style.removeAttribute("data-phast-params"); 16 | }) 17 | .catch(function (err) { 18 | console.warn("[Phast] Failed to load CSS", params, err); 19 | var src = style.getAttribute("data-phast-original-src"); 20 | if (!src) { 21 | console.error("[Phast] No data-phast-original-src on 46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 |
54 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/browser/phast-config.php: -------------------------------------------------------------------------------- 1 | preg_replace('~^(.*/browser)?/.*~si', '$1/', $_SERVER['PHP_SELF']) . 'phast.php', 4 | 'csp' => [ 5 | 'nonce' => defined('PHAST_CSP_NONCE') ? PHAST_CSP_NONCE : null, 6 | 'reportOnly' => defined('PHAST_CSP_REPORT_ONLY') && PHAST_CSP_REPORT_ONLY, 7 | 'reportUri' => '/phast-report.php', 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /test/browser/phast.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 22 | 26 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/browser/tests/csp_report_only.js: -------------------------------------------------------------------------------- 1 | test("csp_report_only.php", function (assert, document) { 2 | assert.deepEqual( 3 | document.defaultView.SCRIPTS, 4 | ["correct nonce", "incorrect nonce", "no nonce"], 5 | "All scripts should be executed" 6 | ); 7 | assert.equal( 8 | document.defaultView.REPORTS, 9 | 2, 10 | "Two reports should have been made" 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /test/browser/tests/csp_report_only.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/browser/tests/currentscript.script.js: -------------------------------------------------------------------------------- 1 | if (document.currentScript) { 2 | window.SYNC_VALUE = document.currentScript.dataset.value; 3 | window.SYNC_SRC = document.currentScript.getAttribute("src"); 4 | } 5 | -------------------------------------------------------------------------------- /test/browser/tests/currentscript_async.js: -------------------------------------------------------------------------------- 1 | test("currentscript_async.php", function (assert, document) { 2 | if (!("currentScript" in document)) { 3 | return; 4 | } 5 | 6 | assert.equal( 7 | document.defaultView.OK.phast, 8 | 10, 9 | "phast script should have seen `phast' 10 times" 10 | ); 11 | 12 | assert.equal( 13 | document.defaultView.OK.async, 14 | 10, 15 | "async script should have seen `async' 10 times" 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/browser/tests/currentscript_async.js.php: -------------------------------------------------------------------------------- 1 | 5 | var type = ; 6 | 7 | console.log("From", type, "script:", document.currentScript.dataset.value); 8 | 9 | if (document.currentScript.dataset.value === type) { 10 | if (!window.OK) { 11 | window.OK = {}; 12 | } 13 | if (!window.OK[type]) { 14 | window.OK[type] = 0; 15 | } 16 | window.OK[type]++; 17 | } 18 | -------------------------------------------------------------------------------- /test/browser/tests/currentscript_async.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/browser/tests/document_write.js: -------------------------------------------------------------------------------- 1 | test("document_write.php", function (assert, document) { 2 | expectedHeaders = ["H1", "H2", "H3"]; 3 | 4 | assert.equal( 5 | getTagNamesInDocumentOrder(expectedHeaders).join(","), 6 | expectedHeaders.join(","), 7 | "Headers should be on the page in the right count and order" 8 | ); 9 | 10 | expectedHeaders.forEach(function (name) { 11 | var elements = document.getElementsByTagName(name); 12 | assert.equal( 13 | elements.length, 14 | 1, 15 | "There should be one <" + name.toUpperCase() + "> element on the page" 16 | ); 17 | assert.equal( 18 | elements[0].textContent, 19 | "Hello, World!", 20 | "The <" + name.toUpperCase() + "> should say 'Hello, World!'" 21 | ); 22 | }); 23 | 24 | function getTagNamesInDocumentOrder(tagNames) { 25 | var result = []; 26 | walk(document.body, function (el) { 27 | if (el.tagName && tagNames.indexOf(el.tagName) !== -1) { 28 | result.push(el.tagName); 29 | } 30 | }); 31 | return result; 32 | } 33 | 34 | function walk(el, fn) { 35 | fn(el); 36 | for (var i = 0; i < el.childNodes.length; i++) { 37 | walk(el.childNodes[i], fn); 38 | } 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /test/browser/tests/document_write.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 |
11 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_async.js: -------------------------------------------------------------------------------- 1 | test("document_write_async.php", function (assert, document) { 2 | assert.equal( 3 | document.defaultView.WRITES, 4 | 2, 5 | "document.write should've been called twice" 6 | ); 7 | assert.equal( 8 | document.querySelectorAll("h1").length, 9 | 0, 10 | "There should be no H1 elements" 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_async.load.js: -------------------------------------------------------------------------------- 1 | document.write("

Hello, World!

"); 2 | window.WRITES = (window.WRITES || 0) + 1; 3 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_async.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_read.js: -------------------------------------------------------------------------------- 1 | test("document_write_read.php", function (assert, document) { 2 | assert.ok(document.defaultView.TEST_RESULT, "TEST_RESULT is defined"); 3 | assert.ok( 4 | document.defaultView.TEST_RESULT.FOUND_H1, 5 | "

should be visible right after write" 6 | ); 7 | var h1s = document.getElementsByTagName("h1"); 8 | assert.equal(h1s.length, 1, "There should be 1

element on the page"); 9 | assert.equal( 10 | h1s[0].textContent, 11 | "Hello, World!", 12 | 'The

should say "Hello, World!"' 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_read.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_restore.js: -------------------------------------------------------------------------------- 1 | test("document_write_restore.php", function (assert, document) { 2 | var initialElements = document.getElementsByTagName("h1"); 3 | assert.equal( 4 | initialElements.length, 5 | 1, 6 | "There should be one h1 on the page before document.write" 7 | ); 8 | 9 | document.defaultView.callDocumentWrite(); 10 | 11 | var elements = document.getElementsByTagName("h1"); 12 | assert.equal( 13 | elements.length, 14 | 0, 15 | "There should be no h1 on the page after document.write" 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_restore.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

This should be removed by the async call to document.write

6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_script.js: -------------------------------------------------------------------------------- 1 | test("document_write_script.php", function (assert, document) { 2 | h1s = document.getElementsByTagName("h1"); 3 | assert.equal(h1s.length, 1, "There should be one

on the page"); 4 | assert.equal( 5 | h1s[0].textContent, 6 | "Hello, World!", 7 | 'The

should contain the string "Hello, World!"' 8 | ); 9 | }); 10 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_script.load.js: -------------------------------------------------------------------------------- 1 | document.write("

Hello, World!

"); 2 | -------------------------------------------------------------------------------- /test/browser/tests/document_write_script.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/browser/tests/event_count.js: -------------------------------------------------------------------------------- 1 | test("event_count.php", function (assert, document) { 2 | assert.propEqual(document.defaultView.events, [ 3 | { event: "readystatechange", readyState: "interactive" }, 4 | { event: "DOMContentLoaded", readyState: "interactive" }, 5 | { event: "readystatechange", readyState: "complete" }, 6 | { event: "load", readyState: "complete" }, 7 | ]); 8 | }); 9 | -------------------------------------------------------------------------------- /test/browser/tests/event_count.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/browser/tests/hello_world.js: -------------------------------------------------------------------------------- 1 | test("hello_world.php", function (assert, document) { 2 | var h1s = document.getElementsByTagName("h1"); 3 | assert.equal(h1s.length, 1, "There is one

element on the page"); 4 | assert.equal( 5 | h1s[0].textContent, 6 | "Hello, World!", 7 | "The

says 'Hello, World!'" 8 | ); 9 | }); 10 | -------------------------------------------------------------------------------- /test/browser/tests/hello_world.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Hello, World!

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/browser/tests/iframe.content.php: -------------------------------------------------------------------------------- 1 |

I'm the iframe!

2 | 6 | -------------------------------------------------------------------------------- /test/browser/tests/iframe.js: -------------------------------------------------------------------------------- 1 | test("iframe.php", function (assert, document) { 2 | wait( 3 | assert, 4 | function () { 5 | return document.defaultView.iframeLoads > 0; 6 | }, 7 | function () { 8 | assert.equal( 9 | document.defaultView.iframeLoads, 10 | 1, 11 | "The IFrame should have been loaded once" 12 | ); 13 | assert.ok( 14 | document.defaultView.loadingAttrSet, 15 | "The IFrame should have loading=lazy set" 16 | ); 17 | } 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /test/browser/tests/iframe.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/browser/tests/iframe_js.js: -------------------------------------------------------------------------------- 1 | test("iframe_js.php", function (assert, document) { 2 | wait( 3 | assert, 4 | function () { 5 | return document.defaultView.iframeLoads > 0; 6 | }, 7 | function () { 8 | var done = assert.async(); 9 | setTimeout(function () { 10 | done(); 11 | assert.equal( 12 | document.defaultView.iframeLoads, 13 | 1, 14 | "The IFrame should have been loaded once" 15 | ); 16 | assert.ok( 17 | document.defaultView.loadingAttrSet, 18 | "The IFrame should have loading=lazy set" 19 | ); 20 | }, 50); 21 | } 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /test/browser/tests/iframe_js.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/browser/tests/inline_js.js: -------------------------------------------------------------------------------- 1 | test("inline_js.php", function (assert, document) { 2 | assert.equal( 3 | document.defaultView.didLoad, 4 | true, 5 | "didLoad should be set to true by inline script" 6 | ); 7 | assert.equal( 8 | document.defaultView.srcWasEmpty, 9 | true, 10 | "srcWasEmpty should be set to true by inline script" 11 | ); 12 | assert.equal( 13 | document.defaultView.shouldBeGlobal, 14 | true, 15 | "shouldBeGlobal should be defined in global scope" 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/browser/tests/inline_js.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/browser/tests/multiple_set_timeout_before_onload.js: -------------------------------------------------------------------------------- 1 | test("multiple_set_timeout_before_onload.php", function (assert, document) { 2 | events = document.defaultView.events; 3 | events = events.filter(function (v, i) { 4 | return i === events.lastIndexOf(v); 5 | }); 6 | assert.deepEqual(events, [ 7 | "setTimeout", 8 | "DOMContentLoaded", 9 | "setTimeout via DOMContentLoaded", 10 | "load", 11 | ]); 12 | }); 13 | -------------------------------------------------------------------------------- /test/browser/tests/multiple_set_timeout_before_onload.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/browser/tests/request_animation_frame_before_onload.js: -------------------------------------------------------------------------------- 1 | test("request_animation_frame_before_onload.php", function (assert, document) { 2 | assert.deepEqual(document.defaultView.events, [ 3 | "requestAnimationFrame", 4 | "DOMContentLoaded", 5 | "requestAnimationFrame via DOMContentLoaded", 6 | "load", 7 | ]); 8 | }); 9 | -------------------------------------------------------------------------------- /test/browser/tests/request_animation_frame_before_onload.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/browser/tests/rewrite_image_in_inline_style.js: -------------------------------------------------------------------------------- 1 | test("rewrite_image_in_inline_style.php", function (assert, document) { 2 | var divs = document.getElementsByTagName("div"); 3 | assert.equal(divs.length, 1, "There is one
element on the page"); 4 | var match = /url\("?(.+?)"?\)/.exec(divs[0].style.backgroundImage); 5 | assert.ok( 6 | match, 7 | "We can parse the url() in the
's background-image style" 8 | ); 9 | var url = match[1]; 10 | assert.ok( 11 | /phast\.php/.test(url), 12 | "The
's background image is loaded through phast.php" 13 | ); 14 | 15 | // Load URL as image and check size 16 | async(assert, function (done) { 17 | var img = new Image(); 18 | img.onload = function () { 19 | done(); 20 | assert.equal(img.width, 256, "The image's width is as expected"); 21 | assert.equal(img.height, 256, "The image's height is as expected"); 22 | }; 23 | img.onerror = function () { 24 | done(); 25 | assert.ok(false, "The image loads"); 26 | }; 27 | img.src = url; 28 | }); 29 | 30 | // Load URL as string and check type 31 | async(assert, function (done) { 32 | retrieve(url, function (content) { 33 | done(); 34 | assert.ok(content.length > 0, "The image data is not empty"); 35 | assert.ok(/^.PNG/.test(content), "The image data has a PNG signature"); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/browser/tests/rewrite_image_in_inline_style.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /test/browser/tests/script_error_fallback.js: -------------------------------------------------------------------------------- 1 | test("script_error_fallback.php", function (assert, document) { 2 | assert.ok( 3 | document.defaultView.doesNotExistSrc, 4 | "window.doesNotExistSrc is defined" 5 | ); 6 | assert.equal( 7 | document.defaultView.doesNotExistSrc, 8 | "does-not-exist.js", 9 | "window.doesNotExistSrc is not correct" 10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /test/browser/tests/script_error_fallback.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/browser/tests/script_once.done.js: -------------------------------------------------------------------------------- 1 | DONE = 1; 2 | -------------------------------------------------------------------------------- /test/browser/tests/script_once.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IE would trigger a 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/browser/tests/script_once.script.js: -------------------------------------------------------------------------------- 1 | window.COUNT = (window.COUNT || 0) + 1; 2 | console.log("Script triggered (" + window.COUNT + ")"); 3 | 4 | setTimeout(function () { 5 | var s = document.createElement("script"); 6 | s.src = "script_once.done.js?" + Date.now(); 7 | document.body.appendChild(s); 8 | }); 9 | -------------------------------------------------------------------------------- /test/browser/tests/script_order.defer.js: -------------------------------------------------------------------------------- 1 | order = window.order || []; 2 | order.push("proxied defer"); 3 | -------------------------------------------------------------------------------- /test/browser/tests/script_order.js: -------------------------------------------------------------------------------- 1 | test("script_order.php", function (assert, document) { 2 | var expected = [ 3 | "inline", 4 | "deferred inline", 5 | "synchronous external", 6 | "second inline", 7 | "proxied defer", 8 | "deferred external", 9 | ]; 10 | assert.ok(document.defaultView.order, "window.order is defined"); 11 | var order = document.defaultView.order; 12 | wait( 13 | assert, 14 | function () { 15 | return order.length >= expected.length + 1; 16 | }, 17 | function () { 18 | assert.ok( 19 | order.indexOf("async external") !== -1, 20 | "async script was loaded" 21 | ); 22 | assert.ok( 23 | order.indexOf("async external") !== 0, 24 | "async script was not loaded first" 25 | ); 26 | order.splice(order.indexOf("async external"), 1); 27 | assert.equal( 28 | order.length, 29 | expected.length, 30 | "" + expected.length + " scripts were loaded" 31 | ); 32 | var strOrder = order.join(", "); 33 | var strExpected = expected.join(", "); 34 | assert.equal( 35 | strOrder, 36 | strExpected, 37 | "Scripts were loaded in the expected order" 38 | ); 39 | } 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /test/browser/tests/script_order.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/browser/tests/script_order.script.php: -------------------------------------------------------------------------------- 1 | 7 | order = window.order || []; 8 | order.push(); 9 | -------------------------------------------------------------------------------- /test/browser/tests/script_proxy_dynamic.js: -------------------------------------------------------------------------------- 1 | test("script_proxy_dynamic.php", function (assert, document) { 2 | wait( 3 | assert, 4 | function () { 5 | return document.getElementsByTagName("h1").length > 0; 6 | }, 7 | function () { 8 | var h1s = Array.prototype.slice.call(document.getElementsByTagName("h1")); 9 | assert.equal( 10 | h1s.length, 11 | 3, 12 | "There should be three

elements on the page" 13 | ); 14 | 15 | h1s.sort(function (a, b) { 16 | return a.textContent > b.textContent 17 | ? 1 18 | : a.textContent < b.textContent 19 | ? -1 20 | : 0; 21 | }); 22 | 23 | for (var i = 0; i < 2; i++) { 24 | assert.equal( 25 | h1s[i].textContent, 26 | "Hello, World!", 27 | "The

should say 'Hello, World!'" 28 | ); 29 | } 30 | 31 | assert.equal( 32 | h1s[i].textContent, 33 | "Hi from module!", 34 | "The third

should say 'Hi from module!'" 35 | ); 36 | 37 | var scripts = Array.prototype.slice.call( 38 | document.getElementsByTagName("script") 39 | ); 40 | 41 | scripts.sort(function (a, b) { 42 | return parseInt(a.dataset.testIndex) - parseInt(b.dataset.testIndex); 43 | }); 44 | 45 | assert.ok( 46 | scripts[0].hasAttribute("data-phast-rewritten"), 47 | "The first 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/browser/tests/script_proxy_php.js: -------------------------------------------------------------------------------- 1 | test("script_proxy_php.php", function (assert, document) { 2 | var h1s = document.getElementsByTagName("h1"); 3 | assert.equal(h1s.length, 1, "There should be one

element on the page"); 4 | }); 5 | -------------------------------------------------------------------------------- /test/browser/tests/script_proxy_php.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/browser/tests/script_proxy_php_script.php: -------------------------------------------------------------------------------- 1 | 4 | // 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/browser/tests/scripts_load_external_async.js: -------------------------------------------------------------------------------- 1 | test("scripts_load_external_async.php", function (assert, document) { 2 | assert.equal( 3 | document.defaultView.jQuerySrc, 4 | "https://code.jquery.com/jquery-3.3.1.slim.min.js", 5 | "jQuery src is correct" 6 | ); 7 | assert.notOk( 8 | document.defaultView.jQueryLoaded, 9 | "jQuery was loaded before sync script" 10 | ); 11 | 12 | wait( 13 | assert, 14 | function () { 15 | return document.defaultView.onLoadCalledAfterJQuery !== undefined; 16 | }, 17 | function () { 18 | assert.ok( 19 | document.defaultView.onLoadCalledAfterJQuery, 20 | "window.onload called after jQuery load" 21 | ); 22 | } 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /test/browser/tests/scripts_load_external_async.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/browser/tests/set_timeout_args.js: -------------------------------------------------------------------------------- 1 | test("set_timeout_args.php", function (assert, document) { 2 | assert.equal(document.defaultView.ARGA, "Hello"); 3 | assert.equal(document.defaultView.ARGB, "World"); 4 | }); 5 | -------------------------------------------------------------------------------- /test/browser/tests/set_timeout_args.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/browser/tests/stylesheet.js: -------------------------------------------------------------------------------- 1 | test("stylesheet.php", function (assert, document) { 2 | var h1 = document.getElementsByTagName("h1")[0]; 3 | var cs = document.defaultView.getComputedStyle(h1, null); 4 | var done = assert.async(); 5 | wait(); 6 | function wait() { 7 | if (document.querySelectorAll("style[data-phast-params]").length > 0) { 8 | return setTimeout(wait); 9 | } 10 | done(); 11 | assert.equal( 12 | cs.backgroundColor, 13 | "rgb(0, 0, 255)", 14 | "The

should have a blue background" 15 | ); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /test/browser/tests/stylesheet.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello, World!

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/browser/tests/stylesheet_before_script.js: -------------------------------------------------------------------------------- 1 | test("stylesheet_before_script.php", function (assert, document) { 2 | wait( 3 | assert, 4 | function () { 5 | return typeof document.defaultView.test !== "undefined"; 6 | }, 7 | function () { 8 | document.defaultView.test(assert); 9 | } 10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /test/browser/tests/stylesheet_before_script.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello, World!

9 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/browser/tests/stylesheet_external.js: -------------------------------------------------------------------------------- 1 | test("stylesheet_external.php", function (assert, document) { 2 | var h1 = document.getElementsByTagName("h1")[0]; 3 | var cs = document.defaultView.getComputedStyle(h1, null); 4 | wait( 5 | assert, 6 | function () { 7 | return document.querySelectorAll('link[media="all"]').length > 0; 8 | }, 9 | function () { 10 | assert.equal( 11 | cs.backgroundColor, 12 | "rgb(0, 0, 255)", 13 | "The

should have a blue background" 14 | ); 15 | } 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/browser/tests/stylesheet_external.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello, World!

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/browser/tests/stylesheet_import.js: -------------------------------------------------------------------------------- 1 | test("stylesheet_import.php", function (assert, document) { 2 | var h1 = document.getElementsByTagName("h1")[0]; 3 | var cs = document.defaultView.getComputedStyle(h1, null); 4 | var done = assert.async(); 5 | wait(); 6 | function wait() { 7 | if (document.querySelectorAll("style[data-phast-params]").length > 0) { 8 | return setTimeout(wait); 9 | } 10 | done(); 11 | assert.equal( 12 | cs.backgroundColor, 13 | "rgb(0, 0, 255)", 14 | "The

should have a blue background" 15 | ); 16 | 17 | var importStatements = 0; 18 | [].forEach.call(document.getElementsByTagName("style"), function (style) { 19 | if (/@import/.test(style.textContent)) { 20 | importStatements++; 21 | } 22 | }); 23 | assert.equal( 24 | importStatements, 25 | 0, 26 | "There should be no @import statements in any