diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/props_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/props_utils.js index b115b1fb34ba4f5ddfdfd135f1e8c338d85a48cc..ec1d7803372879859dac803d0f0a6c1e838d6976 100644 --- a/app/assets/javascripts/vue_shared/components/lib/utils/props_utils.js +++ b/app/assets/javascripts/vue_shared/components/lib/utils/props_utils.js @@ -1,6 +1,6 @@ /** * Return the union of the given components' props options. Required props take - * precendence over non-required props of the same name. + * precedence over non-required props of the same name. * * This makes two assumptions: * - All given components define their props in verbose object format. diff --git a/app/helpers/vite_helper.rb b/app/helpers/vite_helper.rb index 9dbc1df03ae168186c58b7c1231cdb915d793244..fc95b15ce6f47a3d60289225a19f2bf024c802a4 100644 --- a/app/helpers/vite_helper.rb +++ b/app/helpers/vite_helper.rb @@ -30,10 +30,14 @@ def vite_page_entrypoint_paths end def universal_stylesheet_link_tag(path, **options) - stylesheet_link_tag(path, **options) + return stylesheet_link_tag(path, **options) unless vite_enabled? + + vite_stylesheet_tag("stylesheets/styles.#{path}.scss", **options) end - def universal_path_to_stylesheet(path) - ActionController::Base.helpers.stylesheet_path(path) + def universal_path_to_stylesheet(path, **options) + return ActionController::Base.helpers.stylesheet_path(path, **options) unless vite_enabled? + + ViteRuby.instance.manifest.path_for("stylesheets/styles.#{path}.scss", **options) end end diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 22a3caf249dc266acb886e753528a373373dbd82..4ce4572f4332b2955d5fe2d1df053e8446ac2463 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -3,8 +3,11 @@ %head %meta{ :content => "width=device-width, initial-scale=1", :name => "viewport" } %title= yield(:title) - %style - = error_css + - if vite_enabled? + = universal_stylesheet_link_tag 'errors' + - else + %style + = error_css %body .page-container = yield diff --git a/app/views/layouts/oauth_error.html.haml b/app/views/layouts/oauth_error.html.haml index ea02a44a7a6901f7180af1f3a28c56532e8e4c5b..d9f077531851b9b0f5ffb8a5ccc53c6e38069cbd 100644 --- a/app/views/layouts/oauth_error.html.haml +++ b/app/views/layouts/oauth_error.html.haml @@ -4,8 +4,11 @@ %meta{ :content => "width=device-width, initial-scale=1", :name => "viewport" } %title= yield(:title) = universal_stylesheet_link_tag 'application_utilities' - %style - = error_css + - if vite_enabled? + = universal_stylesheet_link_tag 'errors' + - else + %style + = error_css :css svg { width: 280px; diff --git a/config/helpers/vite_plugin_style.mjs b/config/helpers/vite_plugin_style.mjs new file mode 100644 index 0000000000000000000000000000000000000000..b8eb8946bbafce6803558205831dde41c805a5ff --- /dev/null +++ b/config/helpers/vite_plugin_style.mjs @@ -0,0 +1,84 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +/* eslint-disable import/extensions */ +import { + resolveCompilationTargetsForVite, + resolveLoadPaths, +} from '../../scripts/frontend/lib/compile_css.mjs'; +/* eslint-enable import/extensions */ + +const ROOT_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'); + +/** + * This Plugin provides virtual entrypoints for our SCSS files + * + * For example with an import like: + * universal_stylesheet_link_tag "application" + * it will try to load and compile + * app/assets/stylesheets/application.scss + * + * if the JH/EE variant exist, they take precendence over the CE file, so + * add_page_specific_style 'page_bundles/boards' + * will load: + * ee/app/assets/stylesheets/page_bundles/boards.scss in EE + * app/assets/stylesheets/page_bundles/boards.scss in CE + * + * If the file doesn't exist, it loads an empty SCSS file. + */ +export function StylePlugin({ shouldWatch = false } = {}) { + const imagesPath = path.resolve(ROOT_PATH, 'app/assets/images'); + const eeImagesPath = path.resolve(ROOT_PATH, 'ee/app/assets/images'); + const jhImagesPath = path.resolve(ROOT_PATH, 'jh/app/assets/images'); + + const stylesheetDir = '/stylesheets/'; + + const styles = resolveCompilationTargetsForVite(); + + const inputOptions = {}; + + Object.entries(styles).forEach(([source, importPath]) => { + inputOptions[`styles.${source}`] = importPath; + inputOptions[`stylesheets/styles.${source}`] = importPath; + }); + + return { + name: 'vite-plugin-style', + config() { + return { + css: { + preprocessorOptions: { + scss: { + sourceMap: shouldWatch, + sourceMapEmbed: shouldWatch, + sourceMapContents: shouldWatch, + includePaths: [...resolveLoadPaths(), imagesPath, eeImagesPath, jhImagesPath], + }, + }, + }, + build: { + rollupOptions: { + input: inputOptions, + }, + }, + }; + }, + load(id) { + if (!id.startsWith('styles.')) { + return undefined; + } + const fixedId = id.replace('styles.', '').replace('.scss', '.css').replace(/\?.+/, ''); + + if (fixedId === 'tailwind.css') { + return `@import '${path.join(ROOT_PATH, 'app/assets/builds/tailwind.css')}';`; + } + + return styles[fixedId] ? `@import '${styles[fixedId]}';` : '// Does not exist'; + }, + resolveId(source) { + if (!source.startsWith(`${stylesheetDir}styles.`)) { + return undefined; + } + return { id: source.replace(stylesheetDir, '') }; + }, + }; +} diff --git a/config/vite.json b/config/vite.json index 178f978687fbdd9ea8b3143b4d8c54e93354992d..01259674368992fa6396ba46ba26923d7c6c3d0e 100644 --- a/config/vite.json +++ b/config/vite.json @@ -9,8 +9,14 @@ "vendor/assets" ], "entrypointsDir": "javascripts/entrypoints", + "additionalEntrypoints": [ + "~/images/*", + "ee/images/*", + "jh/images/*" + ], "port": 3038, "publicOutputDir": "vite-dev", + "viteBinPath": "scripts/frontend/vite", "devServerConnectTimeout": 3 } } diff --git a/postcss.config.js b/postcss.config.js index a47ef4f95284d3103dd43c01eb27677a3ef833fd..dc898a42826e40daa6fe8c54d740cfe60b733cdd 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,6 @@ module.exports = { plugins: { + tailwindcss: { config: 'config/tailwind.config.js' }, autoprefixer: {}, }, }; diff --git a/scripts/frontend/lib/compile_css.mjs b/scripts/frontend/lib/compile_css.mjs index 515793f32216ad52c18c180364d6d41f60ec4aa9..7ca20edaa27ef041d3a440e2fd38634b7d525917 100644 --- a/scripts/frontend/lib/compile_css.mjs +++ b/scripts/frontend/lib/compile_css.mjs @@ -29,7 +29,7 @@ const SCSS_PARTIAL_GLOB = '**/_*.scss'; * It ensures that the `ee/` and `jh/` directories take precedence, so that the * correct file is loaded. */ -function resolveLoadPaths() { +export function resolveLoadPaths() { const loadPaths = { base: [BASE_PATH], vendor: [ @@ -158,12 +158,14 @@ function resolveCompilationTargets(filter) { for (const [sourcePath, options] of inputGlobs) { const sources = findSourceFiles(sourcePath, options); - console.log(`${sourcePath} resolved to:`, sources); + const log = []; for (const { source, dest } of sources) { if (filter(source, dest)) { + log.push({ source, dest }); result.set(dest, source); } } + console.log(`${sourcePath} resolved to:`, log); } /* @@ -173,6 +175,13 @@ function resolveCompilationTargets(filter) { return Object.fromEntries([...result.entries()].map((entry) => entry.reverse())); } +export function resolveCompilationTargetsForVite() { + const targets = resolveCompilationTargets(() => true); + return Object.fromEntries( + Object.entries(targets).map(([source, dest]) => [dest.replace(OUTPUT_PATH, ''), source]), + ); +} + function createPostCSSProcessors() { return { tailwind: postcss([tailwindcss(tailwindConfig), autoprefixer()]), @@ -263,19 +272,6 @@ export async function compileAllStyles({ return fileWatcher; } -export function viteCSSCompilerPlugin({ shouldWatch = true }) { - let fileWatcher = null; - return { - name: 'gitlab-css-compiler', - async configureServer() { - fileWatcher = await compileAllStyles({ shouldWatch }); - }, - buildEnd() { - return fileWatcher?.close(); - }, - }; -} - export function simplePluginForNodemon({ shouldWatch = true }) { let fileWatcher = null; return { diff --git a/scripts/frontend/tailwindcss.mjs b/scripts/frontend/tailwindcss.mjs index 30c6c8dfd00b39467b3acc96cd841b98b3742ffe..45dfaf1fc3ddd203b0fc7089e586404a9dfd1309 100644 --- a/scripts/frontend/tailwindcss.mjs +++ b/scripts/frontend/tailwindcss.mjs @@ -66,7 +66,6 @@ export function viteTailwindCompilerPlugin({ shouldWatch = true }) { return { name: 'gitlab-tailwind-compiler', async configureServer() { - await ensureCSSinJS(); return build({ shouldWatch }); }, }; diff --git a/scripts/frontend/vite b/scripts/frontend/vite new file mode 100755 index 0000000000000000000000000000000000000000..69540525accf4b3d9c99c62ebb45d488b35b02a6 --- /dev/null +++ b/scripts/frontend/vite @@ -0,0 +1,9 @@ +#!/bin/bash + +# Once the tailwind migration is done, we can remove this +# and remove the `viteBinPath` entrypoint in config/vite.json +echo "Ensure that css_in_js exists to aid tailwind migration" +yarn run tailwindcss:build + +echo "Starting vite" +exec node_modules/.bin/vite "$@" diff --git a/spec/helpers/vite_helper_spec.rb b/spec/helpers/vite_helper_spec.rb index 259b2683cfb3e7edf96e2b86257c955533e8e286..8298bf76401f4c3cd8e2b9bad914eb84a21eb8f3 100644 --- a/spec/helpers/vite_helper_spec.rb +++ b/spec/helpers/vite_helper_spec.rb @@ -25,17 +25,59 @@ end describe '#universal_stylesheet_link_tag' do - it do - link_tag = Capybara.string(helper.universal_stylesheet_link_tag('application')).first('link', visible: :all) + let_it_be(:path) { 'application' } - expect(link_tag[:rel]).to eq('stylesheet') - expect(link_tag[:href]).to match_asset_path('application.css') + context 'when Vite is disabled' do + before do + allow(helper).to receive(:vite_enabled?).and_return(false) + end + + it 'uses stylesheet_link_tag' do + expect(helper).to receive(:stylesheet_link_tag).with(path).and_call_original + link_tag = Capybara.string(helper.universal_stylesheet_link_tag(path)).first('link', visible: :all) + + expect(link_tag[:rel]).to eq('stylesheet') + expect(link_tag[:href]).to match_asset_path("#{path}.css") + end + end + + context 'when Vite is enabled' do + before do + allow(helper).to receive(:vite_enabled?).and_return(true) + end + + it 'uses vite_stylesheet_tag' do + expect(helper).to receive(:vite_stylesheet_tag).with("stylesheets/styles.#{path}.scss") + helper.universal_stylesheet_link_tag(path) + end end end describe '#universal_path_to_stylesheet' do - it do - expect(helper.universal_path_to_stylesheet('application')).to match_asset_path('application.css') + let_it_be(:path) { 'application' } + let_it_be(:out_path) { 'out/application' } + + context 'when Vite is disabled' do + before do + allow(helper).to receive(:vite_enabled?).and_return(false) + end + + it 'uses path_to_stylesheet' do + expect(helper.universal_path_to_stylesheet(path)).to match_asset_path("#{path}.css") + end + end + + context 'when Vite is enabled' do + before do + allow(helper).to receive(:vite_enabled?).and_return(true) + allow(ViteRuby.instance.manifest).to receive(:path_for) + .with("stylesheets/styles.#{path}.scss") + .and_return(out_path) + end + + it 'uses vite_asset_path' do + expect(helper.universal_path_to_stylesheet(path)).to be(out_path) + end end end end diff --git a/vite.config.js b/vite.config.js index d1406b55dce7c89c84e7a06038a195a0b6acc480..5da94f07a617eaa406dc070924c5526cc11779fc 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,12 +14,12 @@ import { copyFilesPatterns, } from './config/webpack.constants'; /* eslint-disable import/extensions */ -import { viteCSSCompilerPlugin } from './scripts/frontend/lib/compile_css.mjs'; import { viteTailwindCompilerPlugin } from './scripts/frontend/tailwindcss.mjs'; import { CopyPlugin } from './config/helpers/vite_plugin_copy.mjs'; import { AutoStopPlugin } from './config/helpers/vite_plugin_auto_stop.mjs'; import { PageEntrypointsPlugin } from './config/helpers/vite_plugin_page_entrypoints.mjs'; import { FixedRubyPlugin } from './config/helpers/vite_plugin_ruby_fixed.mjs'; +import { StylePlugin } from './config/helpers/vite_plugin_style.mjs'; /* eslint-enable import/extensions */ let viteGDKConfig; @@ -70,11 +70,21 @@ export default defineConfig({ find: '~katex', replacement: 'katex', }, + /* + Alias for GitLab Fonts + If we were to import directly from node_modules, + we would get the files under `public/assets/@gitlab` + with the assets pipeline. That seems less than ideal + */ + { + find: /^gitlab-(sans|mono)\//, + replacement: 'node_modules/@gitlab/fonts/gitlab-$1/', + }, ], }, plugins: [ PageEntrypointsPlugin(), - viteCSSCompilerPlugin({ shouldWatch: viteGDKConfig.hmr !== null }), + StylePlugin({ shouldWatch: viteGDKConfig.hmr !== null }), viteTailwindCompilerPlugin({ shouldWatch: viteGDKConfig.hmr !== null }), CopyPlugin({ patterns: copyFilesPatterns,