From 159e3f1ecd2af10067cdd699244e6c03d9fc2892 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 00:10:50 +0100 Subject: [PATCH 01/28] implement css & scss loader --- src/Bundlers/{ => Bun}/Bun.php | 2 +- src/Bundlers/{ => Bun}/bin/bun.js | 5 ++ src/Bundlers/Bun/bin/plugins/css-loader.js | 91 ++++++++++++++++++++++ src/Components/Import.php | 15 +++- src/InjectCore.php | 17 ++++ src/ServiceProvider.php | 2 +- 6 files changed, 126 insertions(+), 6 deletions(-) rename src/Bundlers/{ => Bun}/Bun.php (97%) rename src/Bundlers/{ => Bun}/bin/bun.js (92%) create mode 100644 src/Bundlers/Bun/bin/plugins/css-loader.js diff --git a/src/Bundlers/Bun.php b/src/Bundlers/Bun/Bun.php similarity index 97% rename from src/Bundlers/Bun.php rename to src/Bundlers/Bun/Bun.php index ea62fc4..e5fa645 100644 --- a/src/Bundlers/Bun.php +++ b/src/Bundlers/Bun/Bun.php @@ -1,6 +1,6 @@ { + + const contents = fs.readFileSync(args.path, "utf8"); + + return compile(contents, args.path, { + targets: opts.targets, + }); + }); + + // Build sass + build.onLoad({ filter: /\.scss$/ }, (args) => { + + if(!sass) { + throw `BUNDLING ERROR: You need to install sass in order to support sass loading` + } + + const result = sass.compile(args.path); + + return compile(result.css, args.path, { + targets: opts.targets, + }); + }); + }, + }; +} + +async function compile(content, path, options = {}) { + + if(!css) { + throw `BUNDLING ERROR: You need to install lightning CSS in order to support CSS loading` + } + + const imports = []; + const targets = options.targets?.length + ? css.browserslistToTargets(options.targets) + : undefined; + + const { code, exports } = css.transform({ + filename: path, + code: Buffer.from(content), + cssModules: Boolean(options.cssModules), + minify: true, + targets, + visitor: { + Rule: { + import(rule) { + imports.push(rule.value.url); + return []; + }, + }, + }, + }); + + + if (imports.length === 0) { + return { + contents: `export default ${JSON.stringify(code.toString())};`, + loader: "js", + }; + } + + const imported = imports + .map((url, i) => `import _css${i} from "${url}";`) + .join("\n"); + const exported = imports.map((_, i) => `_css${i}`).join(" + "); + + return { + contents: `${imported}\nexport default ${exported} + ${JSON.stringify( + code.toString() + )};`, + loader: "js", + }; + } diff --git a/src/Components/Import.php b/src/Components/Import.php index 7dcc827..12cc3ce 100644 --- a/src/Components/Import.php +++ b/src/Components/Import.php @@ -25,10 +25,10 @@ public function render() } } - /** Builds the core JavaScript & packages it up in a bundle */ + /** Builds the imported JavaScript & packages it up in a bundle */ protected function bundle() { - $js = $this->core(); + $js = $this->import(); // Render script tag with bundled code return view('x-import::script', [ @@ -58,8 +58,8 @@ protected function raiseConsoleErrorOrException(BundlingFailedException $e) HTML; } - /** Builds Bundle's core JavaScript */ - protected function core(): string + /** Builds a bundle for the JavaScript import */ + protected function import(): string { return <<< JS //-------------------------------------------------------------------------- @@ -84,6 +84,13 @@ protected function core(): string } } + // Handle CSS injection & return early (no need to add css to import map) + if('{$this->module}'.endsWith('.css') || '{$this->module}'.endsWith('.scss')) { + return import('{$this->module}').then(result => { + window.x_inject_styles(result.default, previous) + }) + } + // Assign the import to the window.x_import_modules object (or invoke IIFE) '{$this->as}' // Assign it under an alias diff --git a/src/InjectCore.php b/src/InjectCore.php index 028efd4..2eb7e0e 100644 --- a/src/InjectCore.php +++ b/src/InjectCore.php @@ -140,6 +140,23 @@ protected function core(): string } }; + //-------------------------------------------------------------------------- + // Inject styles + //-------------------------------------------------------------------------- + window.x_inject_styles = function (css, scriptTag) { + if (typeof document === 'undefined') { + return; + } + + // TODO: Add CSP nonce in style tags too when whe get implementing it + const style = document.createElement('style'); + style.dataset['module'] = scriptTag.dataset['module']; + style.innerHTML = css; + + // Inject the style tag after the script that invoked this function + scriptTag.parentNode.insertBefore(style, scriptTag.nextSibling); + } + JS; } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 2e2c5a2..e23c3c4 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,12 +4,12 @@ namespace Leuverink\Bundle; -use Leuverink\Bundle\Bundlers\Bun; use Leuverink\Bundle\Commands\Build; use Leuverink\Bundle\Commands\Clear; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; +use Leuverink\Bundle\Bundlers\Bun\Bun; use Leuverink\Bundle\Components\Import; use Illuminate\Foundation\Http\Events\RequestHandled; use Illuminate\Support\ServiceProvider as BaseServiceProvider; From 078671197a57a40f50d3b50b8df4e26896e5931d Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 00:11:00 +0100 Subject: [PATCH 02/28] refactor --- src/Bundlers/Bun/bin/bun.js | 91 ++++++++--------- src/Bundlers/Bun/bin/plugins/css-loader.js | 110 ++++++++------------- 2 files changed, 89 insertions(+), 112 deletions(-) diff --git a/src/Bundlers/Bun/bin/bun.js b/src/Bundlers/Bun/bin/bun.js index 95ce0e3..04fe915 100644 --- a/src/Bundlers/Bun/bin/bun.js +++ b/src/Bundlers/Bun/bin/bun.js @@ -2,60 +2,63 @@ import { parseArgs } from "util"; import cssLoader from "./plugins/css-loader"; const options = parseArgs({ - args: Bun.argv, - strict: true, - allowPositionals: true, + args: Bun.argv, + strict: true, + allowPositionals: true, - options: { - entrypoint: { - type: "string", - }, + options: { + entrypoint: { + type: "string", + }, - inputPath: { - type: "string", - }, + inputPath: { + type: "string", + }, - outputPath: { - type: "string", - }, + outputPath: { + type: "string", + }, - sourcemaps: { - type: "boolean", - }, + sourcemaps: { + type: "boolean", + }, - minify: { - type: "boolean", + minify: { + type: "boolean", + }, }, - }, }).values; const result = await Bun.build({ - entrypoints: [options.entrypoint], - publicPath: options.outputPath, - outdir: options.outputPath, - root: options.inputPath, - minify: options.minify, - - sourcemap: options.sourcemaps ? "external" : "none", - - naming: { - entry: '[dir]/[name].[ext]', - chunk: "chunks/[name]-[hash].[ext]", // Not in use without --splitting - asset: "assets/[name]-[hash].[ext]", // Not in use without --splitting - }, - - target: "browser", - format: "esm", - - plugins: [ - cssLoader() - ] + entrypoints: [options.entrypoint], + publicPath: options.outputPath, + outdir: options.outputPath, + root: options.inputPath, + minify: options.minify, + + sourcemap: options.sourcemaps ? "external" : "none", + + naming: { + entry: '[dir]/[name].[ext]', + chunk: "chunks/[name]-[hash].[ext]", // Not in use without --splitting + asset: "assets/[name]-[hash].[ext]", // Not in use without --splitting + }, + + target: "browser", + format: "esm", + + plugins: [ + cssLoader({ + minify: options.minify, + sourcemaps: options.sourcemaps + }) + ] }); if (!result.success) { - console.error("Build failed"); - for (const message of result.logs) { - console.error(message); - } - process.exit(1); // Exit with an error code + console.error("Build failed"); + for (const message of result.logs) { + console.error(message); + } + process.exit(1); // Exit with an error code } diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index d9ba2ad..729c836 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,91 +1,65 @@ -const [css, sass, fs] = await Promise.all([ - import("lightningcss-wasm"), - import("sass"), - import("fs") -]); +import { transform, browserslistToTargets } from "lightningcss-wasm"; +import { readFile } from "fs/promises"; const defaultOptions = { - targets: [], + targets: [], + minify: true }; export default function (options = {}) { - const opts = { ...defaultOptions, ...options }; - return { - name: "Bundle CSS loader", - async setup(build) { - - // Build plain css - build.onLoad({ filter: /\.css$/ }, (args) => { - - const contents = fs.readFileSync(args.path, "utf8"); + return { + name: "css-loader", + async setup(build) { - return compile(contents, args.path, { - targets: opts.targets, - }); - }); + build.onLoad({ filter: /\.css$|\.scss$/ }, async (args) => { - // Build sass - build.onLoad({ filter: /\.scss$/ }, (args) => { + const css = await compile(args, { ...defaultOptions, ...options }) - if(!sass) { - throw `BUNDLING ERROR: You need to install sass in order to support sass loading` + return { + contents: `export default ${css};`, + loader: "js", + } + }) } - - const result = sass.compile(args.path); - - return compile(result.css, args.path, { - targets: opts.targets, - }); - }); - }, - }; + } } -async function compile(content, path, options = {}) { - - if(!css) { - throw `BUNDLING ERROR: You need to install lightning CSS in order to support CSS loading` - } +const compile = async function (args, opts) { const imports = []; - const targets = options.targets?.length - ? css.browserslistToTargets(options.targets) - : undefined; - - const { code, exports } = css.transform({ - filename: path, - code: Buffer.from(content), - cssModules: Boolean(options.cssModules), - minify: true, - targets, - visitor: { - Rule: { - import(rule) { - imports.push(rule.value.url); - return []; - }, + const source = await readFile(args.path, "utf8"); + const targets = opts.targets?.length + ? browserslistToTargets(opts.targets) + : undefined; + + const { code } = transform({ + code: Buffer.from(source), + filename: args.path, + + minify: opts.minify, + sourceMap: opts.sourcemaps, + targets, + visitor: { + Rule: { + import(rule) { + imports.push(rule.value.url); + return []; + }, + }, }, - }, }); + const css = JSON.stringify(code.toString()) - if (imports.length === 0) { - return { - contents: `export default ${JSON.stringify(code.toString())};`, - loader: "js", - }; + if (!imports.length) { + return css } const imported = imports - .map((url, i) => `import _css${i} from "${url}";`) - .join("\n"); + .map((url, i) => `import _css${i} from "${url}";`) + .join("\n"); const exported = imports.map((_, i) => `_css${i}`).join(" + "); - return { - contents: `${imported}\nexport default ${exported} + ${JSON.stringify( - code.toString() - )};`, - loader: "js", - }; - } + return `${imported}\nexport default ${exported} + ${css};` +} From d693f9d53cb0ce976bfc449b9d5f347c0bb75303 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 00:21:29 +0100 Subject: [PATCH 03/28] fix typo --- src/InjectCore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InjectCore.php b/src/InjectCore.php index 2eb7e0e..5c188d5 100644 --- a/src/InjectCore.php +++ b/src/InjectCore.php @@ -148,7 +148,7 @@ protected function core(): string return; } - // TODO: Add CSP nonce in style tags too when whe get implementing it + // TODO: Add CSP nonce when adding CSP support const style = document.createElement('style'); style.dataset['module'] = scriptTag.dataset['module']; style.innerHTML = css; From 45b34db09fb824f283ffa70354cfac400a734168 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 10:14:56 +0100 Subject: [PATCH 04/28] update config & env keys --- config/bundle.php | 4 ++-- docs/advanced-usage.md | 2 +- src/BundleManager.php | 4 ++-- tests/Feature/IntegrationTest.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/bundle.php b/config/bundle.php index d830d3a..4e8d92a 100644 --- a/config/bundle.php +++ b/config/bundle.php @@ -11,7 +11,7 @@ | and disable this on your local development environment. | */ - 'caching_enabled' => env('BUNDLE_CACHING_ENABLED', app()->isProduction()), + 'caching' => env('BUNDLE_CACHING', app()->isProduction()), /* |-------------------------------------------------------------------------- @@ -47,7 +47,7 @@ | won't impact performance when your imports are build for prod. | */ - 'sourcemaps_enabled' => env('BUNDLE_SOURCEMAPS_ENABLED', false), + 'sourcemaps' => env('BUNDLE_SOURCEMAPS', false), /* |-------------------------------------------------------------------------- diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 3b3b968..2cfc6bb 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -95,7 +95,7 @@ All code is minified by default. This can make issues harder to debug at times. ## Sourcemaps -Sourcemaps are disabled by default. You may enable this by setting `BUNDLE_SOURCEMAPS_ENABLED` to true in your env file or by publishing and updating the bundle config. +Sourcemaps are disabled by default. You may enable this by setting `BUNDLE_SOURCEMAPS` to true in your env file or by publishing and updating the bundle config. Sourcemaps will be generated in a separate file so this won't affect performance for the end user. diff --git a/src/BundleManager.php b/src/BundleManager.php index 5c27304..a931215 100644 --- a/src/BundleManager.php +++ b/src/BundleManager.php @@ -34,7 +34,7 @@ public function bundle(string $script): SplFileInfo $file = "{$this->hash($script)}.min.js"; // Return cached file if available - if ($this->config()->get('caching_enabled') && $cached = $this->fromDisk($file)) { + if ($this->config()->get('caching') && $cached = $this->fromDisk($file)) { return $cached; } @@ -44,7 +44,7 @@ public function bundle(string $script): SplFileInfo // Attempt bundling & cleanup try { $processed = $this->bundler->build( - sourcemaps: $this->config()->get('sourcemaps_enabled'), + sourcemaps: $this->config()->get('sourcemaps'), minify: $this->config()->get('minify'), inputPath: $this->tempDisk()->path(''), outputPath: $this->buildDisk()->path(''), diff --git a/tests/Feature/IntegrationTest.php b/tests/Feature/IntegrationTest.php index 49d0a41..f480a21 100644 --- a/tests/Feature/IntegrationTest.php +++ b/tests/Feature/IntegrationTest.php @@ -50,7 +50,7 @@ it('generates sourcemaps when enabled') ->defer( - fn () => config()->set('bundle.sourcemaps_enabled', true) + fn () => config()->set('bundle.sourcemaps', true) ) ->bundle( <<< 'JS' From e0578b11582be347e4668ce89141861d25c1e07e Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 11:33:49 +0100 Subject: [PATCH 05/28] add tests & refactor --- composer.json | 3 +- src/BundleManager.php | 6 +- src/Bundlers/Bun/bin/bun.js | 4 +- src/Bundlers/Bun/bin/plugins/css-loader.js | 25 ++-- src/Components/Import.php | 3 +- tests/Browser/CssLoaderTest.php | 138 ++++++++++++++++++ tests/DuskTestCase.php | 12 +- workbench/jsconfig.json | 3 +- workbench/resources/css/blue-background.scss | 5 + .../resources/css/imported-red-background.css | 1 + workbench/resources/css/red-background.css | 3 + 11 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 tests/Browser/CssLoaderTest.php create mode 100644 workbench/resources/css/blue-background.scss create mode 100644 workbench/resources/css/imported-red-background.css create mode 100644 workbench/resources/css/red-background.css diff --git a/composer.json b/composer.json index 38eb72d..38cce54 100644 --- a/composer.json +++ b/composer.json @@ -78,7 +78,8 @@ "cd ./workbench && npm ci", "ln -sf $PWD/workbench/jsconfig.json ./vendor/orchestra/testbench-core/laravel", "ln -sf $PWD/workbench/node_modules/ ./vendor/orchestra/testbench-core/laravel", - "ln -sf $PWD/workbench/resources/js/ ./vendor/orchestra/testbench-core/laravel/resources" + "ln -sf $PWD/workbench/resources/js/ ./vendor/orchestra/testbench-core/laravel/resources", + "ln -sf $PWD/workbench/resources/css/ ./vendor/orchestra/testbench-core/laravel/resources" ], "serve": [ diff --git a/src/BundleManager.php b/src/BundleManager.php index a931215..67b3aab 100644 --- a/src/BundleManager.php +++ b/src/BundleManager.php @@ -31,7 +31,11 @@ public function __construct(BundlerContract $bundler) public function bundle(string $script): SplFileInfo { - $file = "{$this->hash($script)}.min.js"; + $min = $this->config()->get('minify') + ? '.min' + : ''; + + $file = "{$this->hash($script)}{$min}.js"; // Return cached file if available if ($this->config()->get('caching') && $cached = $this->fromDisk($file)) { diff --git a/src/Bundlers/Bun/bin/bun.js b/src/Bundlers/Bun/bin/bun.js index 04fe915..6b666c1 100644 --- a/src/Bundlers/Bun/bin/bun.js +++ b/src/Bundlers/Bun/bin/bun.js @@ -49,8 +49,8 @@ const result = await Bun.build({ plugins: [ cssLoader({ - minify: options.minify, - sourcemaps: options.sourcemaps + minify: Boolean(options.minify), + sourcemaps: Boolean(options.sourcemaps) }) ] }); diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 729c836..c011985 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -3,7 +3,8 @@ import { readFile } from "fs/promises"; const defaultOptions = { targets: [], - minify: true + minify: true, + sourcemaps: false }; export default function (options = {}) { @@ -14,10 +15,10 @@ export default function (options = {}) { build.onLoad({ filter: /\.css$|\.scss$/ }, async (args) => { - const css = await compile(args, { ...defaultOptions, ...options }) + const expression = await compile(args, { ...defaultOptions, ...options }) return { - contents: `export default ${css};`, + contents: expression, loader: "js", } }) @@ -34,12 +35,13 @@ const compile = async function (args, opts) { : undefined; const { code } = transform({ - code: Buffer.from(source), + targets, filename: args.path, + code: Buffer.from(source), minify: opts.minify, sourceMap: opts.sourcemaps, - targets, + visitor: { Rule: { import(rule) { @@ -51,15 +53,14 @@ const compile = async function (args, opts) { }); const css = JSON.stringify(code.toString()) + const imported = imports.map((url, i) => `import _css${i} from "${url}";`).join("\n"); + const exported = imports.map((_, i) => `_css${i}`).join(" + "); + // No CSS imports. Return processed file if (!imports.length) { - return css + return `export default ${css}` } - const imported = imports - .map((url, i) => `import _css${i} from "${url}";`) - .join("\n"); - const exported = imports.map((_, i) => `_css${i}`).join(" + "); - - return `${imported}\nexport default ${exported} + ${css};` + // Has both imports & CSS rules in processed file + return `${imported}\nexport default ${exported} + ${css}`; } diff --git a/src/Components/Import.php b/src/Components/Import.php index 12cc3ce..97b868b 100644 --- a/src/Components/Import.php +++ b/src/Components/Import.php @@ -74,7 +74,7 @@ protected function import(): string (() => { // Check if module is already loaded under a different alias - const previous = document.querySelector(`script[data-module="{$this->module}"`) + const previous = document.querySelector(`script[data-module="{$this->module}"]`) // Was previously loaded & needs to be pushed to import map if(previous && '{$this->as}') { @@ -87,6 +87,7 @@ protected function import(): string // Handle CSS injection & return early (no need to add css to import map) if('{$this->module}'.endsWith('.css') || '{$this->module}'.endsWith('.scss')) { return import('{$this->module}').then(result => { + console.dir(result.default) window.x_inject_styles(result.default, previous) }) } diff --git a/tests/Browser/CssLoaderTest.php b/tests/Browser/CssLoaderTest.php new file mode 100644 index 0000000..c1f4abe --- /dev/null +++ b/tests/Browser/CssLoaderTest.php @@ -0,0 +1,138 @@ +blade(<<< 'HTML' + + HTML); + + // Expect CSS rendered on page + $browser->assertScript( + 'document.querySelector(`style[data-module="css/red-background.css"]`).innerHTML', + 'html{background:red}' + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); + } + + /** @test */ + public function it_handles_css_files() + { + $browser = $this->blade(<<< 'HTML' + + HTML); + + // Expect CSS rendered on page + $browser->assertScript( + 'document.querySelector(`style[data-module="css/red-background.css"]`).innerHTML', + 'html{background:red}' + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); + } + + /** @test */ + public function it_handles_scss_files() + { + $browser = $this->blade(<<< 'HTML' + + HTML); + + // Expect CSS rendered on page + $browser->assertScript( + 'document.querySelector(`style[data-module="css/blue-background.scss"]`).innerHTML', + 'html{& body{background:#00f}}' + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); + } + + /** @test */ + public function it_processes_css_imports() + { + $browser = $this->blade(<<< 'HTML' + + HTML); + + // Expect CSS rendered on page + $browser->assertScript( + 'document.querySelector(`style[data-module="css/imported-red-background.css"]`).innerHTML', + 'html{background:red}' + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); + } + + /** @test */ + public function it_minifies_css_when_minification_enabled() + { + $this->beforeServingApplication(function ($app, $config) { + $config->set('bundle.minify', true); + }); + + $browser = $this->blade(<<< 'HTML' + + HTML); + + // Expect CSS rendered on page + $browser->assertScript( + 'document.querySelector(`style[data-module="css/red-background.css"]`).innerHTML', + 'html{background:red}' + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); + } + + /** @test */ + public function it_doesnt_minify_css_when_minification_disabled() + { + $this->beforeServingApplication(function ($app, $config) { + $config->set('bundle.minify', false); + }); + + $browser = $this->blade(<<< 'HTML' + + HTML); + + // Expect CSS rendered on page + $browser->assertScript( + 'document.querySelector(`style[data-module="css/red-background.css"]`).innerHTML', + <<< 'CSS' + html { + background: red; + } + + CSS + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); + } + + /** @test */ + public function it_generates_sourcemaps_when_enabled() + { + $this->markTestSkipped('not implemented'); + } + + /** @test */ + public function it_doesnt_generate_sourcemaps_by_default() + { + $this->markTestSkipped('not implemented'); + } +} diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php index 4c8f0bb..d1fde43 100644 --- a/tests/DuskTestCase.php +++ b/tests/DuskTestCase.php @@ -15,6 +15,12 @@ class DuskTestCase extends BaseTestCase { use WithWorkbench; + public static function setUpBeforeClass(): void + { + Options::withoutUI(); + parent::setUpBeforeClass(); + } + protected function setUp(): void { parent::setUp(); @@ -23,12 +29,6 @@ protected function setUp(): void $this->artisan('bundle:clear'); } - public static function setUpBeforeClass(): void - { - Options::withoutUI(); - parent::setUpBeforeClass(); - } - protected function tearDown(): void { $this->artisan('bundle:clear'); diff --git a/workbench/jsconfig.json b/workbench/jsconfig.json index 1292e87..2b3508e 100644 --- a/workbench/jsconfig.json +++ b/workbench/jsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "paths": { - "~/*": ["./resources/js/*"] + "~/*": ["./resources/js/*"], + "css/*": ["./resources/css/*"] } } } diff --git a/workbench/resources/css/blue-background.scss b/workbench/resources/css/blue-background.scss new file mode 100644 index 0000000..f078814 --- /dev/null +++ b/workbench/resources/css/blue-background.scss @@ -0,0 +1,5 @@ +html { + body { + background: blue; + } +} diff --git a/workbench/resources/css/imported-red-background.css b/workbench/resources/css/imported-red-background.css new file mode 100644 index 0000000..89abde6 --- /dev/null +++ b/workbench/resources/css/imported-red-background.css @@ -0,0 +1 @@ +@import "./red-background.css"; diff --git a/workbench/resources/css/red-background.css b/workbench/resources/css/red-background.css new file mode 100644 index 0000000..74031be --- /dev/null +++ b/workbench/resources/css/red-background.css @@ -0,0 +1,3 @@ +html { + background: red; +} From 4cbe0270fa6d7c7bb115f2ddf1b53782d09b4f63 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 11:45:44 +0100 Subject: [PATCH 06/28] disable css sourcemaps for now --- src/Bundlers/Bun/bin/plugins/css-loader.js | 3 ++- src/Components/Import.php | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index c011985..e45264b 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -40,7 +40,8 @@ const compile = async function (args, opts) { code: Buffer.from(source), minify: opts.minify, - sourceMap: opts.sourcemaps, + // sourceMap: opts.sourcemaps, + sourceMap: false, // Files not generated. must handle artifacts manually. disable for now visitor: { Rule: { diff --git a/src/Components/Import.php b/src/Components/Import.php index 97b868b..2e7a533 100644 --- a/src/Components/Import.php +++ b/src/Components/Import.php @@ -87,7 +87,6 @@ protected function import(): string // Handle CSS injection & return early (no need to add css to import map) if('{$this->module}'.endsWith('.css') || '{$this->module}'.endsWith('.scss')) { return import('{$this->module}').then(result => { - console.dir(result.default) window.x_inject_styles(result.default, previous) }) } From f70ac54d9dabb63ea961f00079425e934fb884ec Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 12:25:34 +0100 Subject: [PATCH 07/28] boyscouting --- src/Bundlers/Bun/bin/plugins/css-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index e45264b..7a9d7dd 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -47,7 +47,7 @@ const compile = async function (args, opts) { Rule: { import(rule) { imports.push(rule.value.url); - return []; + return []; // Can't be removed }, }, }, From 713df925c2de5638470754e51b90a1468081d62d Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Wed, 14 Feb 2024 12:36:12 +0100 Subject: [PATCH 08/28] update workflow triggers --- .github/workflows/browser-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index 896ca3b..b40e4fc 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -1,6 +1,8 @@ name: browser-tests on: + pull_request: + branches: [development, main] workflow_run: workflows: [tests] types: [completed] From 158479c9dac3b034b717ab332cf041fb26b0141d Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Thu, 15 Feb 2024 14:13:00 +0100 Subject: [PATCH 09/28] update docs --- docs/advanced-usage.md | 37 ++++++++++++++++++++++++++++++++++--- docs/roadmap.md | 16 +++++++++------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 2cfc6bb..8301d5b 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -149,8 +149,39 @@ BundleManager::fake(); When you'd like to use Dusk for browser testing you need to run Bundle in order for your tests not to blow up. Simply don't fake the BundleManager in your DuskTestCase. -## CSS Loader +## CSS Loading -Bun doesn't ship with a css loader. They have it on [the roadmap](https://github.com/oven-sh/bun/issues/159){:target="\_blank"} but no release date is known at this time. We plan to support css loading out-of-the-box as soon as Bun does! +**Beta** -We'd like to experiment with Bun plugin support soon. If that is released before Bun's builtin css loader does, it might be possible to write your own plugin to achieve this. +Bun doesn't ship with a CSS loader. They have it on [the roadmap](https://github.com/oven-sh/bun/issues/159){:target="\_blank"} but no release date is known at this time. + +We provide a custom CSS loader plugin that just works™. You only need to install [Lightning CSS](https://lightningcss.dev/), the rest is taken care of. + +``` bash +npm install lightningcss --save-dev +``` + +Now you can import `css` & `scss` files. Bundle transpiles them and injects it on your page with zero effort. + +``` html + + +``` + +### CSS browser targeting + +Bundle automatically compiles many modern CSS syntax features to more compatible output that is supported in your target browsers. + +You can define what browsers to target using your `package.json` file: + +``` json +{ + "browserslist": [ + "last 2 versions", + ">= 1%", + "IE 11" + ] +} +``` + +Note that this option only affects CSS transpilation. Bun does not support this at this time. diff --git a/docs/roadmap.md b/docs/roadmap.md index 85ae71a..88e431d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -8,7 +8,15 @@ image: "/assets/social-square.png" Bundle is under active development. If you feel there are features missing or you've got a great idea that's not on on the roadmap please [open a discussion](https://github.com/gwleuverink/bundle/discussions/categories/ideas){:target="\_blank"} on GitHub. -## CSS loader +## ✅ Injecting Bundle's core on every page + +**_Added in [v0.1.3](https://github.com/gwleuverink/bundle/releases/tag/v0.1.3)_** + +This will reduce every import's size slightly. But more importantly; it will greatly decrease the chance of unexpected behaviour caused by race conditions, since the Bundle's core is available on pageload. + +## ✅ CSS loader + +**_Added in [v0.1.4](https://github.com/gwleuverink/bundle/releases/tag/v0.1.4)_** Bun doesn't ship with a css loader. They have it on [the roadmap](https://github.com/oven-sh/bun/issues/159){:target="\_blank"} but no release date is known at this time. We plan to support css loading out-of-the-box as soon as Bun does! @@ -102,12 +110,6 @@ It would be incredible if this object could be forwarded to Alpine directly like ``` -## ✅ Injecting Bundle's core on every page - -**_Added in [v0.1.3](https://github.com/gwleuverink/bundle/releases/tag/v0.1.3)_** - -This will reduce every import's size slightly. But more importantly; it will greatly decrease the chance of unexpected behaviour caused by race conditions, since the Bundle's core is available on pageload. - ## Optionally assigning a import to the window scope It could be convenient to provide an api to assign imports to a window variable by use of a prop. This idea is still settling in so might change. From 38e74e53c5e91b0306253e0f4ec6928b0e14caac Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Thu, 15 Feb 2024 14:14:17 +0100 Subject: [PATCH 10/28] add browser targeting to CSS loader plugin --- src/Bundlers/Bun/bin/plugins/css-loader.js | 75 ++++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 7a9d7dd..f4cb8a7 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,8 +1,9 @@ import { transform, browserslistToTargets } from "lightningcss-wasm"; -import { readFile } from "fs/promises"; +import { readFile, exists } from "fs/promises"; +import path from "path"; const defaultOptions = { - targets: [], + browserslist: [], minify: true, sourcemaps: false }; @@ -30,9 +31,7 @@ const compile = async function (args, opts) { const imports = []; const source = await readFile(args.path, "utf8"); - const targets = opts.targets?.length - ? browserslistToTargets(opts.targets) - : undefined; + const targets = await determineTargets(opts.browserslist); const { code } = transform({ targets, @@ -65,3 +64,69 @@ const compile = async function (args, opts) { // Has both imports & CSS rules in processed file return `${imported}\nexport default ${exported} + ${css}`; } + + +//-------------------------------------------------------------------------- +// Utilities +//-------------------------------------------------------------------------- + +/** + * Use targets from config or when none given try + * to detect browserslist from package.json + */ +const determineTargets = async function (browserslist) { + + if (browserslist?.length) { + return browserslistToTargets(browserslist) + } + + // read from package.json + const pkg = await packageJson() + browserslist = pkg.browserslist || [] + + if (browserslist?.length) { + return browserslistToTargets(browserslist) + } + + return undefined +} + +/** + * Get contents of nearest package.json + */ +const packageJson = async function () { + + try { + const path = await findNearestPackageJson(); + const content = await readFile(path, 'utf8'); + return JSON.parse(content); + } catch (error) { + console.error('Error reading browserslist from package.json:', error.message); + return []; + } + +} + +/** + * Get path of nearest package.json + */ +const findNearestPackageJson = async function () { + + let currentDir = process.cwd(); + + while (true) { + + const packageJsonPath = path.join(currentDir, 'package.json'); + + if (await exists(packageJsonPath)) { + return packageJsonPath; + } + + // package.json file could not be found. + if (currentDir === path.dirname(currentDir)) { + return null; + } + + currentDir = path.dirname(currentDir); + } +} From 89d803ad52099e4d336e5829335e5de7e33682c2 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Thu, 15 Feb 2024 15:40:45 +0100 Subject: [PATCH 11/28] refactor --- src/Bundlers/Bun/bin/plugins/css-loader.js | 104 ++++-------------- src/Bundlers/Bun/bin/utils/browser-targets.js | 64 +++++++++++ 2 files changed, 85 insertions(+), 83 deletions(-) create mode 100644 src/Bundlers/Bun/bin/utils/browser-targets.js diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index f4cb8a7..8af0d22 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,34 +1,33 @@ -import { transform, browserslistToTargets } from "lightningcss-wasm"; -import { readFile, exists } from "fs/promises"; -import path from "path"; +import determineTargets from "./../utils/browser-targets"; +import { transform } from "lightningcss-wasm"; +import { readFile } from "fs/promises"; const defaultOptions = { browserslist: [], minify: true, - sourcemaps: false + sourcemaps: false, }; export default function (options = {}) { - return { name: "css-loader", async setup(build) { - build.onLoad({ filter: /\.css$|\.scss$/ }, async (args) => { - - const expression = await compile(args, { ...defaultOptions, ...options }) + const expression = await compile(args, { + ...defaultOptions, + ...options, + }); return { contents: expression, loader: "js", - } - }) - } - } + }; + }); + }, + }; } const compile = async function (args, opts) { - const imports = []; const source = await readFile(args.path, "utf8"); const targets = await determineTargets(opts.browserslist); @@ -52,81 +51,20 @@ const compile = async function (args, opts) { }, }); - const css = JSON.stringify(code.toString()) - const imported = imports.map((url, i) => `import _css${i} from "${url}";`).join("\n"); - const exported = imports.map((_, i) => `_css${i}`).join(" + "); + const css = JSON.stringify(code.toString()); // No CSS imports. Return processed file if (!imports.length) { - return `export default ${css}` + return `export default ${css}`; } // Has both imports & CSS rules in processed file - return `${imported}\nexport default ${exported} + ${css}`; -} - - -//-------------------------------------------------------------------------- -// Utilities -//-------------------------------------------------------------------------- - -/** - * Use targets from config or when none given try - * to detect browserslist from package.json - */ -const determineTargets = async function (browserslist) { - - if (browserslist?.length) { - return browserslistToTargets(browserslist) - } - - // read from package.json - const pkg = await packageJson() - browserslist = pkg.browserslist || [] - - if (browserslist?.length) { - return browserslistToTargets(browserslist) - } + const imported = imports + .map((url, i) => `import _css${i} from "${url}";`) + .join("\n"); - return undefined -} - -/** - * Get contents of nearest package.json - */ -const packageJson = async function () { - - try { - const path = await findNearestPackageJson(); - const content = await readFile(path, 'utf8'); - return JSON.parse(content); - } catch (error) { - console.error('Error reading browserslist from package.json:', error.message); - return []; - } - -} - -/** - * Get path of nearest package.json - */ -const findNearestPackageJson = async function () { - - let currentDir = process.cwd(); + const exported = imports.map((_, i) => `_css${i}`) + .join(" + "); - while (true) { - - const packageJsonPath = path.join(currentDir, 'package.json'); - - if (await exists(packageJsonPath)) { - return packageJsonPath; - } - - // package.json file could not be found. - if (currentDir === path.dirname(currentDir)) { - return null; - } - - currentDir = path.dirname(currentDir); - } -} + return `${imported}\nexport default ${exported} + ${css}`; +}; diff --git a/src/Bundlers/Bun/bin/utils/browser-targets.js b/src/Bundlers/Bun/bin/utils/browser-targets.js new file mode 100644 index 0000000..9f18661 --- /dev/null +++ b/src/Bundlers/Bun/bin/utils/browser-targets.js @@ -0,0 +1,64 @@ +import { browserslistToTargets } from "lightningcss-wasm"; +import { readFile, exists } from "fs/promises"; +import path from "path"; + +/** + * Use targets from config or when none given try + * to detect browserslist from package.json + */ +export default async function (browserslist) { + // If config was given, return browserlist immediately + if (browserslist?.length) { + return browserslistToTargets(browserslist); + } + + // Otherwise read from package.json + const pkg = await packageJson(); + browserslist = pkg.browserslist || []; + + if (browserslist?.length) { + return browserslistToTargets(browserslist); + } + + // If no package.json found or browserslist was not defined + return undefined; +} + +/** + * Get contents of nearest package.json + */ +async function packageJson() { + try { + const path = await findNearestPackageJson(); + const content = await readFile(path, "utf8"); + return JSON.parse(content); + } catch (error) { + console.error( + "Error reading browserslist from package.json:", + error.message + ); + return []; + } +} + +/** + * Get path of nearest package.json + */ +async function findNearestPackageJson() { + let currentDir = process.cwd(); + + while (true) { + const packageJsonPath = path.join(currentDir, "package.json"); + + if (await exists(packageJsonPath)) { + return packageJsonPath; + } + + // package.json file could not be found. + if (currentDir === path.dirname(currentDir)) { + return null; + } + + currentDir = path.dirname(currentDir); + } +} From bef72ac1fccbbad166eec25682d562361bbfa55d Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Thu, 15 Feb 2024 16:39:07 +0100 Subject: [PATCH 12/28] enable caching by default. also in dev environments --- config/bundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/bundle.php b/config/bundle.php index 4e8d92a..e50f667 100644 --- a/config/bundle.php +++ b/config/bundle.php @@ -11,7 +11,7 @@ | and disable this on your local development environment. | */ - 'caching' => env('BUNDLE_CACHING', app()->isProduction()), + 'caching' => env('BUNDLE_CACHING', true), /* |-------------------------------------------------------------------------- From df84d6b78d245603a641dc4af56409176e00a6ae Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Thu, 15 Feb 2024 17:06:53 +0100 Subject: [PATCH 13/28] fix test env --- phpunit.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/phpunit.xml b/phpunit.xml index 7d0904f..950f805 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,11 +4,18 @@ bootstrap="vendor/autoload.php" colors="true" > + + + + + + ./tests + ./app From 66c437fbcd8ab1faa3c577b06203a7868694cf64 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 14:46:19 +0100 Subject: [PATCH 14/28] move css loading docs to new page --- docs/advanced-usage.md | 36 --------------------- docs/caveats.md | 2 +- docs/css-loading.md | 66 ++++++++++++++++++++++++++++++++++++++ docs/integrations/index.md | 2 +- docs/production-builds.md | 2 +- docs/roadmap.md | 2 +- 6 files changed, 70 insertions(+), 40 deletions(-) create mode 100644 docs/css-loading.md diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 8301d5b..b344341 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -149,39 +149,3 @@ BundleManager::fake(); When you'd like to use Dusk for browser testing you need to run Bundle in order for your tests not to blow up. Simply don't fake the BundleManager in your DuskTestCase. -## CSS Loading - -**Beta** - -Bun doesn't ship with a CSS loader. They have it on [the roadmap](https://github.com/oven-sh/bun/issues/159){:target="\_blank"} but no release date is known at this time. - -We provide a custom CSS loader plugin that just works™. You only need to install [Lightning CSS](https://lightningcss.dev/), the rest is taken care of. - -``` bash -npm install lightningcss --save-dev -``` - -Now you can import `css` & `scss` files. Bundle transpiles them and injects it on your page with zero effort. - -``` html - - -``` - -### CSS browser targeting - -Bundle automatically compiles many modern CSS syntax features to more compatible output that is supported in your target browsers. - -You can define what browsers to target using your `package.json` file: - -``` json -{ - "browserslist": [ - "last 2 versions", - ">= 1%", - "IE 11" - ] -} -``` - -Note that this option only affects CSS transpilation. Bun does not support this at this time. diff --git a/docs/caveats.md b/docs/caveats.md index 419ac7f..3c9362c 100644 --- a/docs/caveats.md +++ b/docs/caveats.md @@ -1,5 +1,5 @@ --- -nav_order: 7 +nav_order: 8 title: Caveats image: "/assets/social-square.png" --- diff --git a/docs/css-loading.md b/docs/css-loading.md new file mode 100644 index 0000000..4e83036 --- /dev/null +++ b/docs/css-loading.md @@ -0,0 +1,66 @@ +--- +nav_order: 5 +title: CSS loading +image: "/assets/social-square.png" +--- + +## CSS Loading + +**Beta** + +Bun doesn't ship with a CSS loader. They have it on [the roadmap](https://github.com/oven-sh/bun/issues/159){:target="\_blank"} but no release date is known at this time. + +We provide a custom CSS loader plugin that just works™. You only need to install [Lightning CSS](https://lightningcss.dev/), the rest is taken care of. + +```bash +npm install lightningcss --save-dev +``` + +Now you can import `css` files. Bundle transpiles them and injects it on your page with zero effort. + +```html + + +``` + +### Local CSS loading + +This works similar to [local modules](https://laravel-bundle.dev/local-modules.html). Simply add a new path alias to your `jsconfig.json` file. + +```json +{ + "compilerOptions": { + "paths": { + "~/css": ["./resources/css/*"] + } + } +} +``` + +Now you can load css from your resources directory. + +```html + +``` + +### Browser targeting + +Bundle automatically compiles many modern CSS syntax features to more compatible output that is supported in your target browsers. This includes some features that are not supported by browsers yet, like nested selectors & media queries, without using a preprocessor like Sass. [Check here](https://lightningcss.dev/transpilation.html#syntax-lowering) for the list of the many cool new syntaxes Lightning CSS supports. + +You can define what browsers to target using your `package.json` file: + +```json +{ + "browserslist": ["last 2 versions", ">= 1%", "IE 11"] +} +``` + +### Sass + +In order to load `scss` files you need to install [Sass](https://sass-lang.com/) as a dependency. + +```bash +npm install lightningcss --save-dev +``` + +Bundle will detect Sass is installed and enable bundling scss files with zero configuration. diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 006ea8c..0112e3b 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -1,5 +1,5 @@ --- -nav_order: 6 +nav_order: 7 has_children: true title: Integration examples image: "/assets/social-square.png" diff --git a/docs/production-builds.md b/docs/production-builds.md index 3c65b4c..4b63249 100644 --- a/docs/production-builds.md +++ b/docs/production-builds.md @@ -1,5 +1,5 @@ --- -nav_order: 5 +nav_order: 6 title: Production builds image: "/assets/social-square.png" --- diff --git a/docs/roadmap.md b/docs/roadmap.md index 88e431d..7579305 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,5 @@ --- -nav_order: 8 +nav_order: 9 title: Roadmap image: "/assets/social-square.png" --- From 2828ec1dddbf316787f47d5996a360b8d891a55b Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 14:47:31 +0100 Subject: [PATCH 15/28] prepare scaffold for sass support --- src/Bundlers/Bun/bin/plugins/css-loader.js | 12 +++++++----- tests/Browser/CssLoaderTest.php | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 8af0d22..1e70a24 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -12,8 +12,10 @@ export default function (options = {}) { return { name: "css-loader", async setup(build) { - build.onLoad({ filter: /\.css$|\.scss$/ }, async (args) => { - const expression = await compile(args, { + build.onLoad({ filter: /\.css$/ }, async (args) => { + const source = await readFile(args.path, "utf8"); + + const expression = await compile(source, args.path, { ...defaultOptions, ...options, }); @@ -27,14 +29,14 @@ export default function (options = {}) { }; } -const compile = async function (args, opts) { +const compile = async function (source, filename, opts) { const imports = []; - const source = await readFile(args.path, "utf8"); + const targets = await determineTargets(opts.browserslist); const { code } = transform({ targets, - filename: args.path, + filename, code: Buffer.from(source), minify: opts.minify, diff --git a/tests/Browser/CssLoaderTest.php b/tests/Browser/CssLoaderTest.php index c1f4abe..2930f85 100644 --- a/tests/Browser/CssLoaderTest.php +++ b/tests/Browser/CssLoaderTest.php @@ -46,6 +46,8 @@ public function it_handles_css_files() /** @test */ public function it_handles_scss_files() { + $this->markTestSkipped('not implemented'); + $browser = $this->blade(<<< 'HTML' HTML); From b9cfa51f05d1153d944547887e9a018df43c97d4 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 15:16:54 +0100 Subject: [PATCH 16/28] add dd utility --- src/Bundlers/Bun/bin/utils/dd.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/Bundlers/Bun/bin/utils/dd.js diff --git a/src/Bundlers/Bun/bin/utils/dd.js b/src/Bundlers/Bun/bin/utils/dd.js new file mode 100644 index 0000000..7335eca --- /dev/null +++ b/src/Bundlers/Bun/bin/utils/dd.js @@ -0,0 +1,4 @@ +export default function(output) { + console.error(output); + process.exit(1); +} From b7e05c5240e1c40e55d7973e2b5c0e6d75e4bfb8 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 15:17:52 +0100 Subject: [PATCH 17/28] add tests for sass support --- tests/Browser/CssLoaderTest.php | 6 ++---- workbench/resources/css/blue-background.scss | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Browser/CssLoaderTest.php b/tests/Browser/CssLoaderTest.php index 2930f85..3a64c54 100644 --- a/tests/Browser/CssLoaderTest.php +++ b/tests/Browser/CssLoaderTest.php @@ -44,10 +44,8 @@ public function it_handles_css_files() } /** @test */ - public function it_handles_scss_files() + public function it_supports_sass() { - $this->markTestSkipped('not implemented'); - $browser = $this->blade(<<< 'HTML' HTML); @@ -55,7 +53,7 @@ public function it_handles_scss_files() // Expect CSS rendered on page $browser->assertScript( 'document.querySelector(`style[data-module="css/blue-background.scss"]`).innerHTML', - 'html{& body{background:#00f}}' + 'html body{background:#00f}' ); // Doesn't raise console errors diff --git a/workbench/resources/css/blue-background.scss b/workbench/resources/css/blue-background.scss index f078814..e4639da 100644 --- a/workbench/resources/css/blue-background.scss +++ b/workbench/resources/css/blue-background.scss @@ -1,5 +1,7 @@ +$bg_color: blue; + html { body { - background: blue; + background: $bg_color; } } From 53d38a81f1a5730c5078f1d2678b8c0e263f0da5 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 15:18:00 +0100 Subject: [PATCH 18/28] add sass support & handle missing dependencies gracefully --- src/Bundlers/Bun/bin/plugins/css-loader.js | 33 ++++++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 1e70a24..92aaa78 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,5 +1,5 @@ +import dd from "./../utils/dd"; import determineTargets from "./../utils/browser-targets"; -import { transform } from "lightningcss-wasm"; import { readFile } from "fs/promises"; const defaultOptions = { @@ -12,6 +12,8 @@ export default function (options = {}) { return { name: "css-loader", async setup(build) { + + // Compile plain css with Lightning CSS build.onLoad({ filter: /\.css$/ }, async (args) => { const source = await readFile(args.path, "utf8"); @@ -25,16 +27,41 @@ export default function (options = {}) { loader: "js", }; }); + + // Compile sass pass output through Lightning CSS + build.onLoad({ filter: /\.scss$/ }, async (args) => { + const sass = await import('sass').catch(error => { + console.error('bundle:sass-not-installed') + process.exit(1) + }) + + const source = sass.compile(args.path) + + const expression = await compile(source.css, args.path, { + ...defaultOptions, + ...options, + }); + + return { + contents: expression, + loader: "js", + }; + }); }, }; } const compile = async function (source, filename, opts) { - const imports = []; + const lightningcss = await import("lightningcss-wasm").catch(error => { + console.error('bundle:lightningcss-not-installed') + process.exit(1) + }) + + const imports = []; const targets = await determineTargets(opts.browserslist); - const { code } = transform({ + const { code } = lightningcss.transform({ targets, filename, code: Buffer.from(source), From a0f8c0990374e7036500cb18a0f9f7d05db53fb6 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 15:37:37 +0100 Subject: [PATCH 19/28] add exit util --- src/Bundlers/Bun/bin/plugins/css-loader.js | 8 +++----- src/Bundlers/Bun/bin/utils/dd.js | 4 ---- src/Bundlers/Bun/bin/utils/dump.js | 13 +++++++++++++ 3 files changed, 16 insertions(+), 9 deletions(-) delete mode 100644 src/Bundlers/Bun/bin/utils/dd.js create mode 100644 src/Bundlers/Bun/bin/utils/dump.js diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 92aaa78..97eee61 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,4 +1,4 @@ -import dd from "./../utils/dd"; +import { dd, error } from "./../utils/dump"; import determineTargets from "./../utils/browser-targets"; import { readFile } from "fs/promises"; @@ -31,8 +31,7 @@ export default function (options = {}) { // Compile sass pass output through Lightning CSS build.onLoad({ filter: /\.scss$/ }, async (args) => { const sass = await import('sass').catch(error => { - console.error('bundle:sass-not-installed') - process.exit(1) + error('sass-not-installed') }) const source = sass.compile(args.path) @@ -54,8 +53,7 @@ export default function (options = {}) { const compile = async function (source, filename, opts) { const lightningcss = await import("lightningcss-wasm").catch(error => { - console.error('bundle:lightningcss-not-installed') - process.exit(1) + error('lightningcss-not-installed') }) const imports = []; diff --git a/src/Bundlers/Bun/bin/utils/dd.js b/src/Bundlers/Bun/bin/utils/dd.js deleted file mode 100644 index 7335eca..0000000 --- a/src/Bundlers/Bun/bin/utils/dd.js +++ /dev/null @@ -1,4 +0,0 @@ -export default function(output) { - console.error(output); - process.exit(1); -} diff --git a/src/Bundlers/Bun/bin/utils/dump.js b/src/Bundlers/Bun/bin/utils/dump.js new file mode 100644 index 0000000..29bf313 --- /dev/null +++ b/src/Bundlers/Bun/bin/utils/dump.js @@ -0,0 +1,13 @@ +export function dd(output) { + console.error(output); + process.exit(1); +} + +export function error(id, output = '') { + console.error({ + id: 'bundle:' + id, + output + }); + + process.exit(1); +} From a43b9157b3ff92a0eefdb97bbf812fb2895b3c85 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 16:59:33 +0100 Subject: [PATCH 20/28] improve exception handling (needs work) --- src/Bundlers/Bun/bin/bun.js | 22 +++++---- src/Bundlers/Bun/bin/plugins/css-loader.js | 21 ++++----- src/Bundlers/Bun/bin/utils/dump.js | 14 +++--- src/Components/Import.php | 2 +- src/Exceptions/BundlingFailedException.php | 52 +++++++++++++++++++++- tests/Browser/ComponentTest.php | 2 +- tests/Feature/IntegrationTest.php | 2 +- 7 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/Bundlers/Bun/bin/bun.js b/src/Bundlers/Bun/bin/bun.js index 6b666c1..1fd3350 100644 --- a/src/Bundlers/Bun/bin/bun.js +++ b/src/Bundlers/Bun/bin/bun.js @@ -1,4 +1,7 @@ +// NOTE: we don't have to check if Bun is installed sinsce this script is invoked with the Bun runtime + import { parseArgs } from "util"; +import { error } from "./utils/dump"; import cssLoader from "./plugins/css-loader"; const options = parseArgs({ @@ -39,7 +42,7 @@ const result = await Bun.build({ sourcemap: options.sourcemaps ? "external" : "none", naming: { - entry: '[dir]/[name].[ext]', + entry: "[dir]/[name].[ext]", chunk: "chunks/[name]-[hash].[ext]", // Not in use without --splitting asset: "assets/[name]-[hash].[ext]", // Not in use without --splitting }, @@ -50,15 +53,16 @@ const result = await Bun.build({ plugins: [ cssLoader({ minify: Boolean(options.minify), - sourcemaps: Boolean(options.sourcemaps) - }) - ] + sourcemaps: Boolean(options.sourcemaps), + }), + ], }); if (!result.success) { - console.error("Build failed"); - for (const message of result.logs) { - console.error(message); - } - process.exit(1); // Exit with an error code + // for (const message of result.logs) { + // console.error(message); + // } + // process.exit(1); + + error('build-failed', '', result.logs.map(log => log.message)) } diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 97eee61..34e8fd8 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,5 +1,5 @@ -import { dd, error } from "./../utils/dump"; import determineTargets from "./../utils/browser-targets"; +import { error } from "./../utils/dump"; import { readFile } from "fs/promises"; const defaultOptions = { @@ -12,7 +12,6 @@ export default function (options = {}) { return { name: "css-loader", async setup(build) { - // Compile plain css with Lightning CSS build.onLoad({ filter: /\.css$/ }, async (args) => { const source = await readFile(args.path, "utf8"); @@ -30,11 +29,11 @@ export default function (options = {}) { // Compile sass pass output through Lightning CSS build.onLoad({ filter: /\.scss$/ }, async (args) => { - const sass = await import('sass').catch(error => { - error('sass-not-installed') - }) + const sass = await import("sass").catch((error) => { + error("sass-not-installed"); + }); - const source = sass.compile(args.path) + const source = sass.compile(args.path); const expression = await compile(source.css, args.path, { ...defaultOptions, @@ -51,10 +50,9 @@ export default function (options = {}) { } const compile = async function (source, filename, opts) { - - const lightningcss = await import("lightningcss-wasm").catch(error => { - error('lightningcss-not-installed') - }) + const lightningcss = await import("lightningcss-wasm").catch((error) => { + error("lightningcss-not-installed"); + }); const imports = []; const targets = await determineTargets(opts.browserslist); @@ -90,8 +88,7 @@ const compile = async function (source, filename, opts) { .map((url, i) => `import _css${i} from "${url}";`) .join("\n"); - const exported = imports.map((_, i) => `_css${i}`) - .join(" + "); + const exported = imports.map((_, i) => `_css${i}`).join(" + "); return `${imported}\nexport default ${exported} + ${css}`; }; diff --git a/src/Bundlers/Bun/bin/utils/dump.js b/src/Bundlers/Bun/bin/utils/dump.js index 29bf313..eeaf9d0 100644 --- a/src/Bundlers/Bun/bin/utils/dump.js +++ b/src/Bundlers/Bun/bin/utils/dump.js @@ -3,11 +3,15 @@ export function dd(output) { process.exit(1); } -export function error(id, output = '') { - console.error({ - id: 'bundle:' + id, - output - }); +/** Outputs a object to be caught by BundlingFailedException */ +export function error(id, message = "", output = "") { + console.error( + JSON.stringify({ + id: "bundle:" + id, + message, + output, + }) + ); process.exit(1); } diff --git a/src/Components/Import.php b/src/Components/Import.php index 2e7a533..7546cb3 100644 --- a/src/Components/Import.php +++ b/src/Components/Import.php @@ -53,7 +53,7 @@ protected function raiseConsoleErrorOrException(BundlingFailedException $e) return <<< HTML - + HTML; } diff --git a/src/Exceptions/BundlingFailedException.php b/src/Exceptions/BundlingFailedException.php index 8247582..604a728 100644 --- a/src/Exceptions/BundlingFailedException.php +++ b/src/Exceptions/BundlingFailedException.php @@ -5,6 +5,7 @@ namespace Leuverink\Bundle\Exceptions; use RuntimeException; +use Illuminate\Support\Arr; use Spatie\Ignition\Contracts\Solution; use Spatie\Ignition\Contracts\BaseSolution; use Illuminate\Contracts\Process\ProcessResult; @@ -19,7 +20,7 @@ public function __construct(ProcessResult $result, $script = null) $this->result = $result; $failed = $script ?? $result->command(); - // dd($result->errorOutput()); + // dd($this->output()); parent::__construct( "Bundling failed: {$failed}", @@ -28,14 +29,47 @@ public function __construct(ProcessResult $result, $script = null) // TODO: Consider different approach for providing contextual debug info if (app()->isLocal() && config()->get('app.debug')) { - dump(['error output', $result->errorOutput()]); + dump(['error output', $result->output()]); } } + /** Format output as defined in error function in bin/utils/dump.js */ + public function output(): object + { + $output = json_decode($this->result->errorOutput()); + if ( + gettype($output) === 'object' && + property_exists($output, 'id') && + property_exists($output, 'message') && + property_exists($output, 'output') + ) { + return $output; + } + + return (object) [ + 'id' => null, + 'message' => '', + 'output' => $this->result->errorOutput(), + ]; + } + + public function consoleOutput(): string + { + $output = $this->output(); + + if ($output->message) { + return $output->message; + } + + return Arr::wrap($output->output)[0]; + } + public function getSolution(): Solution { return match (true) { str_contains($this->result->errorOutput(), 'bun: No such file or directory') => $this->bunNotInstalledSolution(), + str_contains($this->output()->id, 'bundle:sass-not-installed') => $this->sassNotInstalledSolution(), + str_contains($this->output()->id, 'bundle:lightningcss-not-installed') => $this->lightningcssNotInstalledSolution(), str_contains($this->result->errorOutput(), 'error: Could not resolve') => $this->moduleNotResolvableSolution(), str_contains($this->result->errorOutput(), 'tsconfig.json: ENOENT') => $this->missingJsconfigFileSolution(), str_contains($this->result->errorOutput(), 'Cannot find tsconfig') => $this->missingJsconfigFileSolution(), @@ -52,6 +86,20 @@ private function bunNotInstalledSolution() ->setSolutionDescription('Bun is not installed. Try running `npm install bun --save-dev`'); } + private function sassNotInstalledSolution() + { + return BaseSolution::create() + ->setSolutionTitle('Sass is not installed.') + ->setSolutionDescription('You need to install Sass in order to load .scss files. Try running `npm install sass --save-dev`'); + } + + private function lightningcssNotInstalledSolution() + { + return BaseSolution::create() + ->setSolutionTitle('Lightning CSS is not installed.') + ->setSolutionDescription('You need to install Lightning CSS in order to load .css files. Try running `npm install lightningcss --save-dev`'); + } + private function moduleNotResolvableSolution() { $module = str($this->result->errorOutput())->after('"')->before('"')->toString(); diff --git a/tests/Browser/ComponentTest.php b/tests/Browser/ComponentTest.php index 31d1423..80aad95 100644 --- a/tests/Browser/ComponentTest.php +++ b/tests/Browser/ComponentTest.php @@ -195,6 +195,6 @@ public function it_logs_console_errors_when_debug_mode_disabled() HTML) ->assertScript(<<< 'JS' document.querySelector('script[data-module="~/nonexistent-module"').innerHTML - JS, 'throw "BUNDLING ERROR: No module found at path \'~/nonexistent-module\'"'); + JS, 'throw "BUNDLING ERROR: Could not resolve: "~/nonexistent-module". Maybe you need to "bun install"?"'); } } diff --git a/tests/Feature/IntegrationTest.php b/tests/Feature/IntegrationTest.php index f480a21..d5c4fc5 100644 --- a/tests/Feature/IntegrationTest.php +++ b/tests/Feature/IntegrationTest.php @@ -120,7 +120,7 @@ expect($component->render()) ->toContain( 'throw', - "BUNDLING ERROR: No module found at path '~/foo'" + 'BUNDLING ERROR: Could not resolve: "~/foo". Maybe you need to "bun install"?' ) ->not->toThrow(BundlingFailedException::class); }); From 70d5b44fa88804c2181bfb4631dd2584db9b0e22 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 17:09:10 +0100 Subject: [PATCH 21/28] boyscouting --- src/Bundlers/Bun/Bun.php | 2 ++ src/Exceptions/BundlingFailedException.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Bundlers/Bun/Bun.php b/src/Bundlers/Bun/Bun.php index e5fa645..fa8fb61 100644 --- a/src/Bundlers/Bun/Bun.php +++ b/src/Bundlers/Bun/Bun.php @@ -33,6 +33,8 @@ public function build( Process::run("{$bun} {$buildScript} {$this->args($options)}") ->throw(function ($res) use ($inputPath, $fileName): void { $failed = file_get_contents($inputPath . $fileName); + + // TODO: needs to be reworked throw new BundlingFailedException($res, $failed); }); diff --git a/src/Exceptions/BundlingFailedException.php b/src/Exceptions/BundlingFailedException.php index 604a728..09055c7 100644 --- a/src/Exceptions/BundlingFailedException.php +++ b/src/Exceptions/BundlingFailedException.php @@ -15,6 +15,8 @@ class BundlingFailedException extends RuntimeException implements ProvidesSoluti { public ProcessResult $result; + // TODO: needs to be reworked. It's getting too big & handles too many JS error cases + // Maybe split it up & devise a system that maps JS errors raised by Bun to appropriate Exception classes public function __construct(ProcessResult $result, $script = null) { $this->result = $result; From d31976b0a1727fb218404a1e6f95ecf4585311ba Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Fri, 16 Feb 2024 23:27:44 +0100 Subject: [PATCH 22/28] fix package.json resolution in testing environment --- src/Bundlers/Bun/bin/bun.js | 5 +++-- src/Bundlers/Bun/bin/plugins/css-loader.js | 6 +++--- src/Bundlers/Bun/bin/utils/browser-targets.js | 5 +++++ src/Bundlers/Bun/bin/utils/dump.js | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Bundlers/Bun/bin/bun.js b/src/Bundlers/Bun/bin/bun.js index 1fd3350..383d540 100644 --- a/src/Bundlers/Bun/bin/bun.js +++ b/src/Bundlers/Bun/bin/bun.js @@ -1,7 +1,7 @@ // NOTE: we don't have to check if Bun is installed sinsce this script is invoked with the Bun runtime import { parseArgs } from "util"; -import { error } from "./utils/dump"; +import { exit } from "./utils/dump"; import cssLoader from "./plugins/css-loader"; const options = parseArgs({ @@ -64,5 +64,6 @@ if (!result.success) { // } // process.exit(1); - error('build-failed', '', result.logs.map(log => log.message)) + // TODO: needs to be reworked + exit('build-failed', '', result.logs.map(log => log.message)) } diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 34e8fd8..df76e9f 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,5 +1,5 @@ import determineTargets from "./../utils/browser-targets"; -import { error } from "./../utils/dump"; +import { exit } from "./../utils/dump"; import { readFile } from "fs/promises"; const defaultOptions = { @@ -30,7 +30,7 @@ export default function (options = {}) { // Compile sass pass output through Lightning CSS build.onLoad({ filter: /\.scss$/ }, async (args) => { const sass = await import("sass").catch((error) => { - error("sass-not-installed"); + exit("sass-not-installed"); }); const source = sass.compile(args.path); @@ -51,7 +51,7 @@ export default function (options = {}) { const compile = async function (source, filename, opts) { const lightningcss = await import("lightningcss-wasm").catch((error) => { - error("lightningcss-not-installed"); + exit("lightningcss-not-installed"); }); const imports = []; diff --git a/src/Bundlers/Bun/bin/utils/browser-targets.js b/src/Bundlers/Bun/bin/utils/browser-targets.js index 9f18661..70e3a8b 100644 --- a/src/Bundlers/Bun/bin/utils/browser-targets.js +++ b/src/Bundlers/Bun/bin/utils/browser-targets.js @@ -45,8 +45,13 @@ async function packageJson() { * Get path of nearest package.json */ async function findNearestPackageJson() { + let currentDir = process.cwd(); + if(process.env['APP_ENV'] === 'testing') { + currentDir += '/workbench'; + } + while (true) { const packageJsonPath = path.join(currentDir, "package.json"); diff --git a/src/Bundlers/Bun/bin/utils/dump.js b/src/Bundlers/Bun/bin/utils/dump.js index eeaf9d0..8e0bf78 100644 --- a/src/Bundlers/Bun/bin/utils/dump.js +++ b/src/Bundlers/Bun/bin/utils/dump.js @@ -4,7 +4,7 @@ export function dd(output) { } /** Outputs a object to be caught by BundlingFailedException */ -export function error(id, message = "", output = "") { +export function exit(id, message = "", output = "") { console.error( JSON.stringify({ id: "bundle:" + id, From 9a6632c77e995ee8e583356f6d9c19e2ae3af887 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Sat, 17 Feb 2024 00:15:07 +0100 Subject: [PATCH 23/28] refactor --- src/Bundlers/Bun/bin/plugins/css-loader.js | 67 +++++++++------------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index df76e9f..e39c777 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,7 +1,5 @@ import determineTargets from "./../utils/browser-targets"; -import { exit } from "./../utils/dump"; -import { readFile } from "fs/promises"; - +import { exit, dd } from "./../utils/dump"; const defaultOptions = { browserslist: [], minify: true, @@ -14,34 +12,26 @@ export default function (options = {}) { async setup(build) { // Compile plain css with Lightning CSS build.onLoad({ filter: /\.css$/ }, async (args) => { - const source = await readFile(args.path, "utf8"); - - const expression = await compile(source, args.path, { + const css = await compileCss(args.path, { ...defaultOptions, ...options, }); return { - contents: expression, + contents: `export default ${css}`, loader: "js", }; }); // Compile sass pass output through Lightning CSS build.onLoad({ filter: /\.scss$/ }, async (args) => { - const sass = await import("sass").catch((error) => { - exit("sass-not-installed"); - }); - - const source = sass.compile(args.path); - - const expression = await compile(source.css, args.path, { + const css = await compileSass(args.path, { ...defaultOptions, ...options, }); return { - contents: expression, + contents: `export default ${css}`, loader: "js", }; }); @@ -49,46 +39,43 @@ export default function (options = {}) { }; } -const compile = async function (source, filename, opts) { +const compileCss = async function (filename, opts) { const lightningcss = await import("lightningcss-wasm").catch((error) => { exit("lightningcss-not-installed"); }); - const imports = []; const targets = await determineTargets(opts.browserslist); - - const { code } = lightningcss.transform({ + const { code } = lightningcss.bundle({ targets, filename, - code: Buffer.from(source), minify: opts.minify, // sourceMap: opts.sourcemaps, sourceMap: false, // Files not generated. must handle artifacts manually. disable for now - - visitor: { - Rule: { - import(rule) { - imports.push(rule.value.url); - return []; // Can't be removed - }, - }, - }, }); - const css = JSON.stringify(code.toString()); + return JSON.stringify(code.toString()); +}; + +const compileSass = async function (filename, opts) { + const lightningcss = await import("lightningcss-wasm").catch((error) => { + exit("lightningcss-not-installed"); + }); - // No CSS imports. Return processed file - if (!imports.length) { - return `export default ${css}`; - } + const sass = await import("sass").catch((error) => { + exit("sass-not-installed"); + }); - // Has both imports & CSS rules in processed file - const imported = imports - .map((url, i) => `import _css${i} from "${url}";`) - .join("\n"); + const source = sass.compile(filename); + const targets = await determineTargets(opts.browserslist); + const { code } = lightningcss.transform({ + targets, + code: Buffer.from(source.css), - const exported = imports.map((_, i) => `_css${i}`).join(" + "); + minify: opts.minify, + // sourceMap: opts.sourcemaps, + sourceMap: false, // Files not generated. must handle artifacts manually. disable for now + }); - return `${imported}\nexport default ${exported} + ${css}`; + return JSON.stringify(code.toString()); }; From 800a23f8cfc147afda47910e7ab42c72f165e9e1 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Sat, 17 Feb 2024 02:27:24 +0100 Subject: [PATCH 24/28] add css sourcemaps --- src/Bundlers/Bun/bin/plugins/css-loader.js | 36 +++++++++++++++++----- tests/Browser/CssLoaderTest.php | 12 ++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index e39c777..567ef81 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -1,5 +1,6 @@ import determineTargets from "./../utils/browser-targets"; import { exit, dd } from "./../utils/dump"; + const defaultOptions = { browserslist: [], minify: true, @@ -45,16 +46,22 @@ const compileCss = async function (filename, opts) { }); const targets = await determineTargets(opts.browserslist); - const { code } = lightningcss.bundle({ + let { code, map } = lightningcss.bundle({ targets, filename, minify: opts.minify, - // sourceMap: opts.sourcemaps, - sourceMap: false, // Files not generated. must handle artifacts manually. disable for now + sourceMap: opts.sourcemaps, + errorRecovery: true, }); - return JSON.stringify(code.toString()); + let css = code.toString(); + if (map) { + map = rewriteSourcemapPaths(map); + css = `${css}\n/*# sourceMappingURL=data:application/json;base64,${btoa(JSON.stringify(map))} */` + } + + return JSON.stringify(css); }; const compileSass = async function (filename, opts) { @@ -68,14 +75,29 @@ const compileSass = async function (filename, opts) { const source = sass.compile(filename); const targets = await determineTargets(opts.browserslist); - const { code } = lightningcss.transform({ + const { code, map } = lightningcss.transform({ targets, code: Buffer.from(source.css), minify: opts.minify, - // sourceMap: opts.sourcemaps, - sourceMap: false, // Files not generated. must handle artifacts manually. disable for now + sourceMap: opts.sourcemaps, + errorRecovery: true, + // sourceMap: false, // Files not generated. must handle artifacts manually. disable for now }); return JSON.stringify(code.toString()); }; + +const rewriteSourcemapPaths = function (map) { + const replace = + process.env["APP_ENV"] === "testing" + ? process.cwd().replace(/^\/+|\/+$/g, "") + "/workbench" + : process.cwd().replace(/^\/+|\/public+$/g, ""); + + map = JSON.parse(map); + map.sources = map.sources.map((path) => { + return path.replace(replace, ".."); + }); + + return map; +}; diff --git a/tests/Browser/CssLoaderTest.php b/tests/Browser/CssLoaderTest.php index 3a64c54..3100791 100644 --- a/tests/Browser/CssLoaderTest.php +++ b/tests/Browser/CssLoaderTest.php @@ -135,4 +135,16 @@ public function it_doesnt_generate_sourcemaps_by_default() { $this->markTestSkipped('not implemented'); } + + /** @test */ + public function it_generates_scss_sourcemaps_when_enabled() + { + $this->markTestSkipped('not implemented'); + } + + /** @test */ + public function it_doesnt_generate_scss_sourcemaps_by_default() + { + $this->markTestSkipped('not implemented'); + } } From a3546a30a90f7d3576d5b4218dbe5d7edc60ea33 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Sat, 17 Feb 2024 02:27:46 +0100 Subject: [PATCH 25/28] add browserslist to workbench package.json --- workbench/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/workbench/package.json b/workbench/package.json index dc897d8..dbb2b1a 100644 --- a/workbench/package.json +++ b/workbench/package.json @@ -1,4 +1,9 @@ { + "browserslist": [ + "last 2 versions", + ">= 1%", + "IE 11" + ], "devDependencies": { "bun": "^1.0.21", "lodash": "^4.17.21", From 5ac71f874a8c270655a4dfebcae69cbb15342333 Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Mon, 19 Feb 2024 00:25:26 +0100 Subject: [PATCH 26/28] add sass sourcemaps --- src/Bundlers/Bun/bin/plugins/css-loader.js | 39 ++++++++---- tests/Browser/CssLoaderTest.php | 70 ++++++++++++++++++++-- 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/src/Bundlers/Bun/bin/plugins/css-loader.js b/src/Bundlers/Bun/bin/plugins/css-loader.js index 567ef81..78c64d2 100644 --- a/src/Bundlers/Bun/bin/plugins/css-loader.js +++ b/src/Bundlers/Bun/bin/plugins/css-loader.js @@ -50,9 +50,9 @@ const compileCss = async function (filename, opts) { targets, filename, + errorRecovery: true, minify: opts.minify, sourceMap: opts.sourcemaps, - errorRecovery: true, }); let css = code.toString(); @@ -73,30 +73,47 @@ const compileSass = async function (filename, opts) { exit("sass-not-installed"); }); - const source = sass.compile(filename); const targets = await determineTargets(opts.browserslist); - const { code, map } = lightningcss.transform({ + + // NOTE: we could use a custom importer to remap sourcemap url's here. But might be able to reuse The one we use for the CSS loader + const source = await sass.compileAsync(filename, { + sourceMap: opts.sourcemaps, + sourceMapIncludeSources: opts.sourcemaps // inlines source countent. refactor when adding extenral sourcemaps + }); + + let { code, map } = lightningcss.transform({ targets, code: Buffer.from(source.css), + filename: opts.sourcemaps + ? filename + : null, + errorRecovery: true, minify: opts.minify, sourceMap: opts.sourcemaps, - errorRecovery: true, - // sourceMap: false, // Files not generated. must handle artifacts manually. disable for now + inputSourceMap: JSON.stringify(source.sourceMap), }); - return JSON.stringify(code.toString()); + let css = code.toString(); + + if (map) { + map = rewriteSourcemapPaths(map); + css = `${css}\n/*# sourceMappingURL=data:application/json;base64,${btoa(JSON.stringify(map))} */` + } + + return JSON.stringify(css); }; const rewriteSourcemapPaths = function (map) { - const replace = - process.env["APP_ENV"] === "testing" - ? process.cwd().replace(/^\/+|\/+$/g, "") + "/workbench" - : process.cwd().replace(/^\/+|\/public+$/g, ""); + + const replacePath = process.env["APP_ENV"] === "testing" + ? process.cwd().replace(/^\/+|\/+$/g, "") + "/workbench" + : process.cwd().replace(/^\/+|\/public+$/g, ""); map = JSON.parse(map); + map.sources = map.sources.map((path) => { - return path.replace(replace, ".."); + return path.replace(replacePath, "..") }); return map; diff --git a/tests/Browser/CssLoaderTest.php b/tests/Browser/CssLoaderTest.php index 3100791..71db189 100644 --- a/tests/Browser/CssLoaderTest.php +++ b/tests/Browser/CssLoaderTest.php @@ -127,24 +127,86 @@ public function it_doesnt_minify_css_when_minification_disabled() /** @test */ public function it_generates_sourcemaps_when_enabled() { - $this->markTestSkipped('not implemented'); + $this->beforeServingApplication(function ($app, $config) { + $config->set('bundle.minify', true); + $config->set('bundle.sourcemaps', true); + }); + + $browser = $this->blade(<<< 'HTML' + + HTML); + + // Assert output contains encoded sourcemap (flaky. asserting on encoded sting) + $browser->assertScript( + 'document.querySelector(`style[data-module="css/red-background.css"]`).innerHTML.startsWith("html{background:red}\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VSb290IjpudWxsLCJtYXBwaW5ncyI6IkFBQUEi")', + true + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); } /** @test */ public function it_doesnt_generate_sourcemaps_by_default() { - $this->markTestSkipped('not implemented'); + $this->beforeServingApplication(function ($app, $config) { + $config->set('bundle.minify', true); + $config->set('bundle.sourcemaps', false); + }); + + $browser = $this->blade(<<< 'HTML' + + HTML); + + $browser->assertScript( + 'document.querySelector(`style[data-module="css/red-background.css"]`).innerHTML', + 'html{background:red}' + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); } /** @test */ public function it_generates_scss_sourcemaps_when_enabled() { - $this->markTestSkipped('not implemented'); + $this->beforeServingApplication(function ($app, $config) { + $config->set('bundle.minify', true); + $config->set('bundle.sourcemaps', true); + }); + + $browser = $this->blade(<<< 'HTML' + + HTML); + + // Assert output contains encoded sourcemap (flaky. asserting on encoded sting) + $browser->assertScript( + 'document.querySelector(`style[data-module="css/blue-background.scss"]`).innerHTML.startsWith("html body{background:#00f}\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VSb290Ij")', + true + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); } /** @test */ public function it_doesnt_generate_scss_sourcemaps_by_default() { - $this->markTestSkipped('not implemented'); + $this->beforeServingApplication(function ($app, $config) { + $config->set('bundle.minify', true); + $config->set('bundle.sourcemaps', false); + }); + + $browser = $this->blade(<<< 'HTML' + + HTML); + + $browser->assertScript( + 'document.querySelector(`style[data-module="css/blue-background.scss"]`).innerHTML', + 'html body{background:#00f}' + ); + + // Doesn't raise console errors + $this->assertEmpty($browser->driver->manage()->getLog('browser')); } } From 44fbe4cbcc45d7a696e43ae649a4bb9646e6cafc Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Tue, 20 Feb 2024 08:19:02 +0100 Subject: [PATCH 27/28] update css loading docs --- docs/css-loading.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/css-loading.md b/docs/css-loading.md index 4e83036..5b495a8 100644 --- a/docs/css-loading.md +++ b/docs/css-loading.md @@ -10,19 +10,31 @@ image: "/assets/social-square.png" Bun doesn't ship with a CSS loader. They have it on [the roadmap](https://github.com/oven-sh/bun/issues/159){:target="\_blank"} but no release date is known at this time. -We provide a custom CSS loader plugin that just works™. You only need to install [Lightning CSS](https://lightningcss.dev/), the rest is taken care of. +We provide a custom CSS loader plugin that just works™. Built on top of [Lightning CSS](https://lightningcss.dev/). Just use the `x-import` directive to load a css file directly. Bundle transpiles them and injects it on your page with zero effort. + +```html + + +``` + +Because we use Bun as a runtime when processing your files there is no need to install Lightning CSS as a dependency. When Bun encounters a import that is not installed it will fall back to it's on internal [module resolution algorithm](https://bun.sh/docs/runtime/autoimport) & install the dependency on the fly. + +That being said; We do recommend installing Lightning CSS in your project. ```bash npm install lightningcss --save-dev ``` -Now you can import `css` files. Bundle transpiles them and injects it on your page with zero effort. +### Sass -```html - - +[Sass](https://sass-lang.com/) is supported out of the box. Just like with Lightning CSS you don't have to install Sass as a dependency, but it is recommended. + +```bash +npm install lightningcss --save-dev ``` +Note that compiled Sass is processed with LightningCSS afterwards, so if you plan on only processing scss files it is recommended to install both Lightning CSS & Sass. + ### Local CSS loading This works similar to [local modules](https://laravel-bundle.dev/local-modules.html). Simply add a new path alias to your `jsconfig.json` file. @@ -55,12 +67,8 @@ You can define what browsers to target using your `package.json` file: } ``` -### Sass - -In order to load `scss` files you need to install [Sass](https://sass-lang.com/) as a dependency. +
-```bash -npm install lightningcss --save-dev -``` +{: .note } -Bundle will detect Sass is installed and enable bundling scss files with zero configuration. +> Bundle currently only supports browserslist using your `package.json` file. A dedicated `.browserslistrc` is not suppported at this time. From 47dae8723529a0cc2f2f91300d585f1b3383a72e Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Tue, 20 Feb 2024 13:49:55 +0100 Subject: [PATCH 28/28] disable browser caching in dusk tests --- docs/advanced-usage.md | 3 +-- tests/DuskTestCase.php | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index b344341..5fdd917 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -101,7 +101,7 @@ Sourcemaps will be generated in a separate file so this won't affect performance {: .note } -> If your project stored previously bundled files you need to run the [bundle:clear](https://laravel-bundle.dev/advanced-usage.html#artisan-bundleclear) command +> If your project stored previously bundled files you need to run the [bundle:clear](https://laravel-bundle.dev/advanced-usage.html#artisan-bundleclear) command after enabling/disabling this feature. ## Cache-Control headers @@ -148,4 +148,3 @@ BundleManager::fake(); ``` When you'd like to use Dusk for browser testing you need to run Bundle in order for your tests not to blow up. Simply don't fake the BundleManager in your DuskTestCase. - diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php index d1fde43..3400bfb 100644 --- a/tests/DuskTestCase.php +++ b/tests/DuskTestCase.php @@ -63,6 +63,8 @@ public function blade(string $blade) // Create a temporary route $this->beforeServingApplication(function ($app, $config) use ($page) { $config->set('app.debug', true); + $config->set('bundle.cache_control_headers', 'no-cache, no-store, must-revalidate'); + $app->make(Route::class)::get('test-blade', fn () => $page); }); @@ -87,6 +89,7 @@ public function serveLivewire($component) $this->beforeServingApplication(function ($app, $config) use (&$component) { $config->set('app.debug', true); $config->set('app.key', 'base64:q1fQla64BmAKJBOnRKuXvfddVoqEuSLv1eOEEO91uGI='); + $config->set('bundle.cache_control_headers', 'no-cache, no-store, must-revalidate'); // Needs to register so component is findable in update calls Livewire::component($component);