From 49753e540fc454ebad4b50851c8f5742a043205c Mon Sep 17 00:00:00 2001 From: Cody Finegan Date: Fri, 19 Apr 2024 09:23:21 +1200 Subject: [PATCH 1/2] TL-40011: Limiting tests to just 8.1+ --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df967883..534390f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ] + php: [ '8.1', '8.2' ] extensions: [ '' ] os: [ubuntu-latest] include: From efc309fcf38ffd4fc310936b2c28f3ef3a6fabee Mon Sep 17 00:00:00 2001 From: Cody Finegan Date: Thu, 18 Apr 2024 16:25:15 +1200 Subject: [PATCH 2/2] TL-40011: Adding in webkit-style transformation support --- src/Compiler.php | 80 +++++++++++-- src/Transforms/Resource.php | 33 ++++++ src/Transforms/ResourceFactory.php | 20 ++++ src/Transforms/Transform.php | 11 ++ src/Transforms/TransformCompiler.php | 67 +++++++++++ src/Transforms/TransformResource.php | 83 +++++++++++++ src/Transforms/TransformResourceFactory.php | 21 ++++ tests/TransformerTest.php | 122 ++++++++++++++++++++ 8 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 src/Transforms/Resource.php create mode 100644 src/Transforms/ResourceFactory.php create mode 100644 src/Transforms/Transform.php create mode 100644 src/Transforms/TransformCompiler.php create mode 100644 src/Transforms/TransformResource.php create mode 100644 src/Transforms/TransformResourceFactory.php create mode 100644 tests/TransformerTest.php diff --git a/src/Compiler.php b/src/Compiler.php index dd4ae8e3..df0d381c 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -37,6 +37,7 @@ use ScssPhp\ScssPhp\Logger\StreamLogger; use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator; +use ScssPhp\ScssPhp\Transforms\TransformCompiler; use ScssPhp\ScssPhp\Util\Path; /** @@ -351,6 +352,20 @@ class Compiler */ private $warnedChildFunctions = []; + /** + * Optional file loader + * + * @var callable|null + */ + protected $fileLoader = null; + + /** + * Used to handle transformations of the tree. + * + * @var TransformCompiler + */ + protected TransformCompiler $transformer; + /** * Constructor * @@ -369,6 +384,7 @@ public function __construct($cacheOptions = null) } $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true); + $this->transformer = new TransformCompiler(); } /** @@ -5748,6 +5764,8 @@ public function addFeature($name) */ protected function importFile($path, OutputBlock $out) { + [$path, $transforms] = $this->transformer->extractTransformsFromPath($path); + $this->pushCallStack('import ' . $this->getPrettyPath($path)); // see if tree is cached $realPath = realpath($path); @@ -5756,6 +5774,8 @@ protected function importFile($path, OutputBlock $out) $realPath = $path; } + $cacheKey = ($transforms ? implode('!', $transforms) : '') . $realPath; + if (substr($path, -5) === '.sass') { $this->sourceIndex = \count($this->sourceNames); $this->sourceNames[] = $path; @@ -5765,16 +5785,28 @@ protected function importFile($path, OutputBlock $out) throw $this->error('The Sass indented syntax is not implemented.'); } - if (isset($this->importCache[$realPath])) { + if (isset($this->importCache[$cacheKey])) { $this->handleImportLoop($realPath); - $tree = $this->importCache[$realPath]; + $tree = $this->importCache[$cacheKey]; } else { - $code = file_get_contents($path); - $parser = $this->parserFactory($path); - $tree = $parser->parse($code); + // Allow the custom file loaders + if ($this->fileLoader !== null) { + $code = call_user_func($this->fileLoader, $path); + } else { + $code = file_get_contents($path); + } - $this->importCache[$realPath] = $tree; + // Apply webpack-style transforms to the tree + if ($transforms) { + // Apply the named transformations + $tree = $this->transformer->applyTransformations($transforms, $path, $code, fn($path, $code) => $this->parserFactory($path)->parse($code)); + } else { + $parser = $this->parserFactory($path); + $tree = $parser->parse($code); + } + + $this->importCache[$cacheKey] = $tree; } $currentDirectory = $this->currentDirectory; @@ -5817,7 +5849,9 @@ public static function isCssImport($url) } /** - * Return the file path for an import url if it exists + * Return the file path for an import url if it exists. + * + * This includes an override supporting webkit transformations. * * @internal * @@ -5828,6 +5862,16 @@ public static function isCssImport($url) */ public function findImport($url, $currentDir = null) { + $pos = strrpos($url, '!'); + if ($pos !== false) { + [$path, $transforms] = $this->transformer->extractTransformsFromPath($url); + $result = $this->findImport($path, $currentDir); + if ($result === null) { + throw $this->error("`$path` file not found for @import"); + } + return implode('!', $transforms) . "!$result"; + } + // Vanilla css and external requests. These are not meant to be Sass imports. // Callback importers are still called for BC. if (self::isCssImport($url)) { @@ -6250,7 +6294,7 @@ protected function handleImportLoop($name) continue; } - $file = $this->sourceNames[$env->block->sourceIndex] ?? ''; + $file = $this->sourceNames[$env->block->sourceIndex] ?? null; if ($file === null) { continue; @@ -10470,4 +10514,24 @@ protected function libScssphpGlob($args) return [Type::T_LIST, ',', $listParts]; } + + /** + * Set the file loader. + * + * @param callable|null $fileLoader + * @return void + */ + public function setFileLoader(?callable $fileLoader): void + { + $this->fileLoader = $fileLoader; + } + + /** + * @param TransformCompiler $compiler + * @return void + */ + public function setTransformer(TransformCompiler $compiler): void + { + $this->transformer = $compiler; + } } diff --git a/src/Transforms/Resource.php b/src/Transforms/Resource.php new file mode 100644 index 00000000..5b07b3d6 --- /dev/null +++ b/src/Transforms/Resource.php @@ -0,0 +1,33 @@ + $transforms + */ + public function __construct(protected array $transforms = [], ?ResourceFactory $resourceFactory = null) + { + $this->resourceFactory = $resourceFactory ?? new TransformResourceFactory(); + } + + public function registerTransform(string $name, Transform $transform): void + { + $this->transforms[$name] = $transform; + } + + /** + * @param string[] $transforms + * @param string $path + * @param string $code + * @param callable $astParserFactory + * @return Block + */ + public function applyTransformations(array $transforms, string $path, string $code, callable $astParserFactory): Block + { + // Make the resource + $resource = $this->resourceFactory->createResource($path, $code, $astParserFactory); + + // transforms execute from right to left (like webpack) + $transforms = array_reverse($transforms, true); + foreach ($transforms as $name) { + if (!isset($this->transforms[$name])) { + throw new \Exception('Unknown transform "' . $name . '"'); + } + $this->transforms[$name]->execute($resource); + } + + return $resource->getAst(); + } + + /** + * @param string $path + * @return array{0: string, 1: string[]} + */ + public function extractTransformsFromPath(string $path): array + { + $pos = strrpos($path, '!'); + $transforms = []; + + if ($pos !== false) { + $pathTransforms = substr($path, 0, $pos); + $transforms = !empty($pathTransforms) ? explode('!', $pathTransforms) : []; + $path = substr($path, $pos + 1); + } + return [$path, $transforms]; + } +} diff --git a/src/Transforms/TransformResource.php b/src/Transforms/TransformResource.php new file mode 100644 index 00000000..f44531e3 --- /dev/null +++ b/src/Transforms/TransformResource.php @@ -0,0 +1,83 @@ +parserFactory = $parserFactory; + } + + public function getCode(): string + { + if ($this->modified) { + throw new \Exception('Cannot access code as it is in AST only mode'); + } + + return $this->code; + } + + public function setCode(string $code): void + { + $this->code = $code; + $this->ast = null; + $this->modified = false; + } + + /** + * Return the modified Tree + * + * @return Block + */ + public function getAst(): Block + { + if ($this->ast === null) { + if (empty($this->code)) { + throw new \Exception('AST and source are both unavailable for this resource'); + } + + $this->ast = ($this->parserFactory)($this->path, $this->code); + } + return $this->ast; + } + + public function setAst(Block $ast): void + { + $this->ast = $ast; + $this->markASTModified(); + } + + /** + * Marks the AST has been modified + * + * @return void + */ + public function markASTModified(): void + { + $this->modified = true; + } + + /** + * Indicates if the resource AST has been modified or not. + * + * @return bool + */ + public function isASTOnly(): bool + { + return $this->modified; + } +} diff --git a/src/Transforms/TransformResourceFactory.php b/src/Transforms/TransformResourceFactory.php new file mode 100644 index 00000000..3f92a8bf --- /dev/null +++ b/src/Transforms/TransformResourceFactory.php @@ -0,0 +1,21 @@ +extractTransformsFromPath('/a/b/c'); + $this->assertSame('/a/b/c', $path); + $this->assertEmpty($transforms); + + [$path, $transforms] = $transformer->extractTransformsFromPath('a!/a/b/c'); + $this->assertSame('/a/b/c', $path); + $this->assertSame(['a'], $transforms); + + [$path, $transforms] = $transformer->extractTransformsFromPath('a!b!c!/a/b/c'); + $this->assertSame('/a/b/c', $path); + $this->assertSame(['a', 'b', 'c'], $transforms); + } + + /** + * Assert that transforms can be applied via code + */ + public function testTransformCode(): void + { + $transform = new class () implements Transform + { + public function execute(Resource $resource): void + { + $code = $resource->getCode(); + $code = str_replace(["'", "\n"], ["\\\'", '\A'], $code); + $resource->setCode("wrap { content: '{$code}'; }"); + } + }; + + $transformer = new TransformCompiler(); + $transformer->registerTransform('wrap', $transform); + + $compiler = new Compiler(); + $compiler->setOutputStyle(OutputStyle::COMPRESSED); + $compiler->setTransformer($transformer); + + $dummyData = fn($url) => 'a { background: blue; }'; + + $compiler->setImportPaths([$dummyData]); + $compiler->setFileLoader($dummyData); + + $input = '@import "wrap!my_file"; a { color: blue; }'; + $result = $compiler->compileString($input)->getCss(); + $this->assertSame('wrap{content:"a { background: blue; }"}a{color:blue}', $result); + } + + /** + * Assert that a custom transformer can be applied + */ + public function testCustomTransform(): void + { + $transform = new class () implements Transform + { + public function execute(Resource $resource): void + { + $resource->getAst()->children[0][1] = '/* TestTestD */'; + } + }; + + $transformer = new TransformCompiler(); + $transformer->registerTransform('test', $transform); + + $compiler = new Compiler(); + $compiler->setOutputStyle(OutputStyle::EXPANDED); + $compiler->setTransformer($transformer); + + $dummyData = fn($url) => '/* TestC */ a { color: red; }'; + + $compiler->setImportPaths([$dummyData]); + $compiler->setFileLoader($dummyData); + + $input = '@import "test!my_file"; a { color: blue; }'; + $expected = <<compileString($input)->getCss(); + $this->assertSame($expected, $result); + + $compiler->setOutputStyle(OutputStyle::COMPRESSED); + $result = $compiler->compileString($input)->getCss(); + $this->assertSame('a{color:red}a{color:blue}', $result); + + $input = '@import "my_file"; a { color: blue; }'; + $compiler->setOutputStyle(OutputStyle::EXPANDED); + $expected = <<compileString($input)->getCss(); + $this->assertSame($expected, $result); + } +}