From c9b00f692f3a47b36f74a5ec57ef13dccdecc4cb Mon Sep 17 00:00:00 2001 From: YvetteNikolov Date: Tue, 30 Jun 2026 22:34:48 +0200 Subject: [PATCH 1/4] feat(vite-config): support building from a single theme root --- packages/vite-config/README.md | 9 ++ .../src/configs/brave-theme-blocks.js | 26 ++++- packages/vite-config/src/configs/brave.js | 41 ++++++- .../vite-config/src/utils/generate-aliases.js | 19 +++- .../src/utils/generate-entry-points.js | 9 +- .../src/utils/get-all-theme-names.js | 36 ++---- .../src/utils/resolve-theme-context.js | 104 ++++++++++++++++++ 7 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 packages/vite-config/src/utils/resolve-theme-context.js diff --git a/packages/vite-config/README.md b/packages/vite-config/README.md index 267c0bd..3fae9f6 100644 --- a/packages/vite-config/README.md +++ b/packages/vite-config/README.md @@ -81,6 +81,15 @@ export default defineConfig( ); ``` +### brave-root vs theme-root + +`braveConfig` and `braveBlocksConfig` auto-detect where they run: + +- **brave-root** — cwd has `web/app/themes/`. Builds every theme; asset base `/app/themes//public/build/`. +- **theme-root** — cwd is a single theme (has `style.css`, no `web/app/themes/`). Builds that one theme; output to its own `public/`, asset base `/wp-content/themes//public/build/`. + +Same config file either way. + ## Package Vite configs Presets for npm and Laravel packages. Both wrappers use `createBasePackageConfig`. diff --git a/packages/vite-config/src/configs/brave-theme-blocks.js b/packages/vite-config/src/configs/brave-theme-blocks.js index 86ce987..e2c0ce1 100644 --- a/packages/vite-config/src/configs/brave-theme-blocks.js +++ b/packages/vite-config/src/configs/brave-theme-blocks.js @@ -19,14 +19,36 @@ import path from 'path'; */ import { generateAliases } from '../utils/generate-aliases.js'; import { getAllThemeNames } from '../utils/get-all-theme-names.js'; +import { resolveThemeContext } from '../utils/resolve-theme-context.js'; export const braveBlocksConfig = ( { blockPath } ) => { + const context = resolveThemeContext(); const entryDir = blockPath; - const themeName = blockPath.split( path.sep ).at( 3 ); // assumes: web/app/themes//... - const outDir = path.join( 'web/app/themes', themeName, 'public' ); + + /** + * Block output directory: + * - theme-root: the theme's own `public` directory. + * - brave-root: the owning theme's public directory, derived from the + * `web/app/themes//...` block path. + */ + const outDir = + context.mode === 'theme-root' + ? 'public' + : path.join( + 'web/app/themes', + blockPath.split( path.sep ).at( 3 ), + 'public' + ); const allThemes = getAllThemeNames(); return defineConfig( { + /** + * Blocks don't ship a static public directory. Disabling it also avoids + * a self-copy in theme-root, where the cwd's `public` dir (the build + * target) would otherwise be Vite's default publicDir and collide with + * the per-block outDir nested inside it. + */ + publicDir: false, resolve: { alias: generateAliases( allThemes ), }, diff --git a/packages/vite-config/src/configs/brave.js b/packages/vite-config/src/configs/brave.js index 88a2641..8049c7d 100644 --- a/packages/vite-config/src/configs/brave.js +++ b/packages/vite-config/src/configs/brave.js @@ -17,6 +17,7 @@ /** * External dependencies */ +import path from 'path'; import { defineConfig, loadEnv } from 'vite'; import { viteExternalsPlugin } from 'vite-plugin-externals'; import { wordpressPlugin } from '@roots/vite-plugin'; @@ -31,16 +32,25 @@ import { generateEntryPoints } from '../utils/generate-entry-points.js'; import { getAllThemeNames } from '../utils/get-all-theme-names.js'; import { getCheckerPlugin } from '../utils/get-checker-plugin.js'; import { getPostCssPrefixWrapPlugin } from '../utils/get-postcss-prefixwrap-plugin.js'; +import { resolveThemeContext } from '../utils/resolve-theme-context.js'; export const braveConfig = ( { checker: checkerOption = null, editorStylesPrefixWrap, entryPoints, mode, - theme = 'sage', + theme, } ) => { const env = loadEnv( mode, process.cwd(), '' ); const isDev = ! process.argv.includes( 'build' ); + const context = resolveThemeContext(); + + /** + * The theme to build. In build mode the THEME env var is passed per theme; + * in dev/theme-root it falls back to the context's default theme (the only + * theme in theme-root, `sage` in brave-root). + */ + const resolvedTheme = theme || context.defaultTheme; const allThemes = getAllThemeNames(); /** @@ -48,10 +58,20 @@ export const braveConfig = ( { * - In dev mode: include all themes for a unified dev server * - In build mode: only process the single specified theme */ - const themesToProcess = isDev ? allThemes : [ theme ]; + const themesToProcess = isDev ? allThemes : [ resolvedTheme ]; + + /** + * Production asset base (URL the built files are served from): + * - brave-root: the theme's Bedrock path. + * - theme-root: a standard WordPress theme path so the theme is portable. + */ + const buildBase = + context.mode === 'theme-root' + ? `/wp-content/themes/${ resolvedTheme }/public/build/` + : `/app/themes/${ resolvedTheme }/public/build/`; return defineConfig( { - base: isDev ? '' : `/app/themes/${ theme }/public/build/`, + base: isDev ? '' : buildBase, /** * Named host required for CSP whitelisting in dev mode. */ @@ -106,14 +126,23 @@ export const braveConfig = ( { } ), /** * Public directory: - * - Dev mode: Sage (assumes it hosts the dev server) + * - Dev mode: the default theme (assumes it hosts the dev server) * - Build mode: output to the specific theme directory + * - theme-root: the theme's own `public` directory */ - publicDirectory: `web/app/themes/${ theme }/public`, + publicDirectory: path.join( + context.themeRelDir( resolvedTheme ), + 'public' + ), /** * Files to watch for changes and trigger a refresh */ - refresh: [ 'web/app/themes/**/resources/views/**/*.blade.php' ], + refresh: + context.mode === 'theme-root' + ? [ 'resources/views/**/*.blade.php' ] + : [ + 'web/app/themes/**/resources/views/**/*.blade.php', + ], } ), /** * Externalizes React, ReactDOM and ReactJSXRuntime so they reference the global versions provided by WordPress' wp-element (window.React, window.ReactDOM). diff --git a/packages/vite-config/src/utils/generate-aliases.js b/packages/vite-config/src/utils/generate-aliases.js index 3f64dc4..0e621d3 100644 --- a/packages/vite-config/src/utils/generate-aliases.js +++ b/packages/vite-config/src/utils/generate-aliases.js @@ -3,12 +3,17 @@ */ import path from 'path'; +/** + * Internal dependencies + */ +import { resolveThemeContext } from './resolve-theme-context.js'; + /** * Generates the aliases for each theme to use like `@theme-name`. * Example: `background-image: url('@sage/images/logo.svg')`; or `import '@sage-child/scripts/frontend/frontend.js';` */ export const generateAliases = ( themeNames ) => { - const themesDir = path.resolve( process.cwd(), 'web/app/themes' ); + const context = resolveThemeContext(); const aliases = {}; if ( ! themeNames || themeNames.length === 0 ) { @@ -19,11 +24,19 @@ export const generateAliases = ( themeNames ) => { const sanitizedThemeName = themeName.replace( /[^a-zA-Z0-9-_]/g, '-' ); aliases[ `@${ sanitizedThemeName }` ] = path.join( - themesDir, - themeName, + context.themeDir( themeName ), 'resources' ); } ); + /** + * In a theme-root build the shared `@sage` alias (used by starter source for + * imports like `@sage/styles/base/config.css`) has no sage theme to point + * at, so map it to the local resources for backwards compatibility. + */ + if ( context.mode === 'theme-root' && ! aliases[ '@sage' ] ) { + aliases[ '@sage' ] = path.join( context.themeDir(), 'resources' ); + } + return aliases; }; diff --git a/packages/vite-config/src/utils/generate-entry-points.js b/packages/vite-config/src/utils/generate-entry-points.js index 09e7590..80e950e 100644 --- a/packages/vite-config/src/utils/generate-entry-points.js +++ b/packages/vite-config/src/utils/generate-entry-points.js @@ -4,6 +4,11 @@ import fs from 'fs'; import path from 'path'; +/** + * Internal dependencies + */ +import { resolveThemeContext } from './resolve-theme-context.js'; + /** * Returns an array of valid entry point paths for the specified themes. */ @@ -11,7 +16,7 @@ export const generateEntryPoints = ( { themesToProcess, entryPoints = [], } ) => { - const baseDir = process.cwd(); + const context = resolveThemeContext(); const resolvedEntryPoints = []; if ( ! themesToProcess || themesToProcess.length === 0 ) { @@ -21,7 +26,7 @@ export const generateEntryPoints = ( { } themesToProcess.forEach( ( themeName ) => { - const themeDir = path.join( baseDir, 'web/app/themes', themeName ); + const themeDir = context.themeDir( themeName ); // Push valid file paths to the input list entryPoints.forEach( ( entry ) => { diff --git a/packages/vite-config/src/utils/get-all-theme-names.js b/packages/vite-config/src/utils/get-all-theme-names.js index 7fb6510..0ef715a 100644 --- a/packages/vite-config/src/utils/get-all-theme-names.js +++ b/packages/vite-config/src/utils/get-all-theme-names.js @@ -1,36 +1,14 @@ /** - * External dependencies + * Internal dependencies */ -import fs from 'fs'; -import path from 'path'; +import { resolveThemeContext } from './resolve-theme-context.js'; /** - * Returns an array of all theme names in the `web/app/themes` directory. + * Returns an array of all theme names for the current project context. + * + * - brave-root: every theme in `web/app/themes` that has a `style.css`. + * - theme-root: a single-element array with the current theme's name. */ export const getAllThemeNames = () => { - const themesDir = path.resolve( 'web/app/themes' ); - - if ( ! fs.existsSync( themesDir ) ) { - console.error( 'Themes directory does not exist' ); // eslint-disable-line no-console - return []; - } - - return fs.readdirSync( themesDir ).filter( ( dirName ) => { - const fullPath = path.join( themesDir, dirName ); - - try { - const isDirectory = fs.statSync( fullPath ).isDirectory(); - - if ( ! isDirectory ) return false; - - // Check if the directory contains a style.css file - const hasStyleCss = fs.existsSync( - path.join( fullPath, 'style.css' ) - ); - return hasStyleCss; - } catch { - console.error( `Error checking directory: ${ fullPath }` ); // eslint-disable-line no-console - return false; - } - } ); + return resolveThemeContext().themes.map( ( theme ) => theme.name ); }; diff --git a/packages/vite-config/src/utils/resolve-theme-context.js b/packages/vite-config/src/utils/resolve-theme-context.js new file mode 100644 index 0000000..e155fd3 --- /dev/null +++ b/packages/vite-config/src/utils/resolve-theme-context.js @@ -0,0 +1,104 @@ +/** + * External dependencies + */ +import fs from 'fs'; +import path from 'path'; + +/** + * Resolves the project context for build/watch/lint/format tooling. + * + * Two layouts are supported: + * + * - `brave-root` (default): invoked from the root of a Brave/Bedrock + * project where themes live at `web/app/themes/`. + * - `theme-root`: invoked from inside a single theme directory (cwd is the + * theme). Every theme path collapses to the cwd, so a theme can build, watch, + * lint and format on its own. + * + * Detection is automatic and cheap: + * 1. A `web/app/themes` directory in cwd → brave-root. + * 2. A `style.css` in cwd → theme-root (cwd is a WordPress theme). + * 3. Otherwise throw with guidance. + */ + +const THEMES_SUBDIR = path.join( 'web', 'app', 'themes' ); + +const isDirectory = ( dirPath ) => { + try { + return fs.statSync( dirPath ).isDirectory(); + } catch { + return false; + } +}; + +const detectMode = ( cwd ) => { + if ( isDirectory( path.resolve( cwd, THEMES_SUBDIR ) ) ) { + return 'brave-root'; + } + + if ( fs.existsSync( path.resolve( cwd, 'style.css' ) ) ) { + return 'theme-root'; + } + + return null; +}; + +/** + * Discovers theme directories (dirs containing a `style.css`) inside a + * brave-root themes directory. + */ +const discoverThemeNames = ( themesDir ) => { + if ( ! isDirectory( themesDir ) ) { + return []; + } + + return fs.readdirSync( themesDir ).filter( ( dirName ) => { + const fullPath = path.join( themesDir, dirName ); + + if ( ! isDirectory( fullPath ) ) { + return false; + } + + return fs.existsSync( path.join( fullPath, 'style.css' ) ); + } ); +}; + +export const resolveThemeContext = () => { + const cwd = process.cwd(); + const mode = detectMode( cwd ); + + if ( mode === 'theme-root' ) { + const name = path.basename( cwd ); + + return { + mode, + themes: [ { name, dir: cwd, relDir: '.' } ], + themeDir: () => cwd, + themeRelDir: () => '.', + defaultTheme: name, + }; + } + + if ( mode === 'brave-root' ) { + const themesDir = path.resolve( cwd, THEMES_SUBDIR ); + const themes = discoverThemeNames( themesDir ).map( ( name ) => ( { + name, + dir: path.join( themesDir, name ), + relDir: path.join( THEMES_SUBDIR, name ), + } ) ); + + return { + mode, + themes, + themeDir: ( name ) => path.join( themesDir, name ), + themeRelDir: ( name ) => path.join( THEMES_SUBDIR, name ), + defaultTheme: 'sage', + }; + } + + throw new Error( + 'Unable to determine project context: no "web/app/themes" directory ' + + 'and no "style.css" in the current directory. Run from a Brave ' + + 'project root or from a theme directory.' + ); +}; From 7c9212894962b97e883a0eb86ca0d42d81b96b6f Mon Sep 17 00:00:00 2001 From: YvetteNikolov Date: Tue, 30 Jun 2026 22:35:02 +0200 Subject: [PATCH 2/4] feat(toolkit): support linting and building from a single theme root --- packages/toolkit/README.md | 9 ++ packages/toolkit/src/config/modes.js | 53 ++++++--- packages/toolkit/src/scripts/build-blocks.js | 3 +- packages/toolkit/src/scripts/watch-blocks.js | 3 +- .../toolkit/src/utils/get-all-theme-names.js | 39 +------ packages/toolkit/src/utils/get-block-paths.js | 9 +- .../toolkit/src/utils/get-block-theme-name.js | 25 +++++ .../src/utils/resolve-theme-context.js | 106 ++++++++++++++++++ 8 files changed, 190 insertions(+), 57 deletions(-) create mode 100644 packages/toolkit/src/utils/get-block-theme-name.js create mode 100644 packages/toolkit/src/utils/resolve-theme-context.js diff --git a/packages/toolkit/README.md b/packages/toolkit/README.md index 2d31678..0b97ea9 100644 --- a/packages/toolkit/README.md +++ b/packages/toolkit/README.md @@ -42,6 +42,15 @@ Options --version Shows version ``` +### brave-root vs theme-root + +`brave` mode auto-detects where the command runs: + +- **brave-root** — cwd has `web/app/themes/`. Operates on all themes (lint/format globs, build/watch). +- **theme-root** — cwd is a single theme (has `style.css`, no `web/app/themes/`). Operates on that one theme. + +Same commands either way. + ### Format For default for brave sites: diff --git a/packages/toolkit/src/config/modes.js b/packages/toolkit/src/config/modes.js index 33d81f6..f89d5e5 100644 --- a/packages/toolkit/src/config/modes.js +++ b/packages/toolkit/src/config/modes.js @@ -1,25 +1,44 @@ import { filetypes } from './filetypes.js'; +import { resolveThemeContext } from '../utils/resolve-theme-context.js'; + +/** + * Builds the lint/format globs for the `brave` mode. + * + * - brave-root: matches `resources/**` across every theme. + * - theme-root: matches the current theme's own `resources/**`. + * + * Computed lazily (via the getter below) so the project context is only + * resolved when brave-mode globs are actually requested. + */ +const buildBravePaths = () => { + const { mode } = resolveThemeContext(); + const prefix = mode === 'theme-root' ? '.' : './web/app/themes/**'; + + return [ + { + filetype: filetypes.js.name, + path: [ + `${ prefix }/resources/scripts/**/*.js`, + `${ prefix }/resources/scripts/**/*.jsx`, + ], + }, + { + filetype: filetypes.blade.name, + path: `${ prefix }/resources/views/**/*.blade.php`, + }, + { + filetype: filetypes.css.name, + path: `${ prefix }/resources/styles/{*,**/*}.css`, + }, + ]; +}; export const modes = { brave: { name: 'brave', - paths: [ - { - filetype: filetypes.js.name, - path: [ - './web/app/themes/**/resources/scripts/**/*.js', - './web/app/themes/**/resources/scripts/**/*.jsx', - ], - }, - { - filetype: filetypes.blade.name, - path: './web/app/themes/**/resources/views/**/*.blade.php', - }, - { - filetype: filetypes.css.name, - path: './web/app/themes/**/resources/styles/{*,**/*}.css', - }, - ], + get paths() { + return buildBravePaths(); + }, }, custom: { name: 'custom', diff --git a/packages/toolkit/src/scripts/build-blocks.js b/packages/toolkit/src/scripts/build-blocks.js index e4479cd..e6155ff 100644 --- a/packages/toolkit/src/scripts/build-blocks.js +++ b/packages/toolkit/src/scripts/build-blocks.js @@ -7,6 +7,7 @@ import path from 'path'; * Internal dependencies */ import { getBlockPaths } from '../utils/get-block-paths.js'; +import { getBlockThemeName } from '../utils/get-block-theme-name.js'; import { ensureFileExists, execWithEnv, @@ -29,7 +30,7 @@ export const buildBlocks = async ( configFile = 'vite-blocks.config.js' ) => { Promise.allSettled( blocks.map( async ( blockPath ) => { const blockName = path.basename( blockPath ); - const themeName = blockPath.split( path.sep ).at( 3 ); // web/app/themes/... + const themeName = getBlockThemeName( blockPath ); log.info( `Building block: ${ blockName } (${ themeName })` ); diff --git a/packages/toolkit/src/scripts/watch-blocks.js b/packages/toolkit/src/scripts/watch-blocks.js index a1cdf6b..5a2449e 100644 --- a/packages/toolkit/src/scripts/watch-blocks.js +++ b/packages/toolkit/src/scripts/watch-blocks.js @@ -8,6 +8,7 @@ import path from 'path'; * Internal dependencies */ import { getBlockPaths } from '../utils/get-block-paths.js'; +import { getBlockThemeName } from '../utils/get-block-theme-name.js'; import { ensureFileExists, setupGracefulShutdown } from '../utils/helpers.js'; import log from '../utils/logger.js'; @@ -27,7 +28,7 @@ export const watchBlocks = async ( configFile = 'vite-blocks.config.js' ) => { blocks.forEach( ( blockPath ) => { const blockName = path.basename( blockPath ); - const themeName = blockPath.split( path.sep ).at( 3 ); // web/app/themes/... + const themeName = getBlockThemeName( blockPath ); log.info( `Watching block: ${ blockName } (${ themeName })` ); diff --git a/packages/toolkit/src/utils/get-all-theme-names.js b/packages/toolkit/src/utils/get-all-theme-names.js index d9b41ad..0ef715a 100644 --- a/packages/toolkit/src/utils/get-all-theme-names.js +++ b/packages/toolkit/src/utils/get-all-theme-names.js @@ -1,41 +1,14 @@ /** * Internal dependencies */ -import log from './logger.js'; +import { resolveThemeContext } from './resolve-theme-context.js'; /** - * External dependencies - */ -import fs from 'fs'; -import path from 'path'; - -/** - * Returns an array of all theme names in the `web/app/themes` directory. + * Returns an array of all theme names for the current project context. + * + * - brave-root: every theme in `web/app/themes` that has a `style.css`. + * - theme-root: a single-element array with the current theme's name. */ export const getAllThemeNames = () => { - const themesDir = path.resolve( 'web/app/themes' ); - - if ( ! fs.existsSync( themesDir ) ) { - log.error( 'Themes directory does not exist' ); - return []; - } - - return fs.readdirSync( themesDir ).filter( ( dirName ) => { - const fullPath = path.join( themesDir, dirName ); - - try { - const isDirectory = fs.statSync( fullPath ).isDirectory(); - - if ( ! isDirectory ) return false; - - // Check if the directory contains a style.css file - const hasStyleCss = fs.existsSync( - path.join( fullPath, 'style.css' ) - ); - return hasStyleCss; - } catch { - log.error( `Error checking directory: ${ fullPath }` ); - return false; - } - } ); + return resolveThemeContext().themes.map( ( theme ) => theme.name ); }; diff --git a/packages/toolkit/src/utils/get-block-paths.js b/packages/toolkit/src/utils/get-block-paths.js index b5fda3c..2ff0103 100644 --- a/packages/toolkit/src/utils/get-block-paths.js +++ b/packages/toolkit/src/utils/get-block-paths.js @@ -7,16 +7,15 @@ import path from 'path'; /** * Internal dependencies */ -import { getAllThemeNames } from './get-all-theme-names.js'; +import { resolveThemeContext } from './resolve-theme-context.js'; export const getBlockPaths = async () => { - const themeNames = getAllThemeNames(); + const context = resolveThemeContext(); const blockPathsPerTheme = await Promise.all( - themeNames.map( async ( themeName ) => { + context.themes.map( async ( theme ) => { const blocksDir = path.join( - 'web/app/themes', - themeName, + theme.relDir, 'resources/scripts/blocks' ); diff --git a/packages/toolkit/src/utils/get-block-theme-name.js b/packages/toolkit/src/utils/get-block-theme-name.js new file mode 100644 index 0000000..5c22be9 --- /dev/null +++ b/packages/toolkit/src/utils/get-block-theme-name.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import path from 'path'; + +/** + * Internal dependencies + */ +import { resolveThemeContext } from './resolve-theme-context.js'; + +/** + * Returns the owning theme name for a discovered block path, for logging. + * + * - theme-root: the single theme's name. + * - brave-root: parsed from the `web/app/themes//...` block path. + */ +export const getBlockThemeName = ( blockPath ) => { + const context = resolveThemeContext(); + + if ( context.mode === 'theme-root' ) { + return context.defaultTheme; + } + + return blockPath.split( path.sep ).at( 3 ); +}; diff --git a/packages/toolkit/src/utils/resolve-theme-context.js b/packages/toolkit/src/utils/resolve-theme-context.js new file mode 100644 index 0000000..fd3e34d --- /dev/null +++ b/packages/toolkit/src/utils/resolve-theme-context.js @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import fs from 'fs'; +import path from 'path'; + +/** + * Resolves the project context for build/watch/lint/format tooling. + * + * Two layouts are supported: + * + * - `brave-root` (default): invoked from the root of a Brave/Bedrock + * project where themes live at `web/app/themes/`. + * - `theme-root`: invoked from inside a single theme directory (cwd is the + * theme). Every theme path collapses to the cwd, so a theme can build, watch, + * lint and format on its own. + * + * Detection is automatic and cheap: + * 1. A `web/app/themes` directory in cwd → brave-root. + * 2. A `style.css` in cwd → theme-root (cwd is a WordPress theme). + * 3. Otherwise throw with guidance. + * + * NOTE: kept in sync with the identical util in `@yardinternet/vite-config`. + */ + +const THEMES_SUBDIR = path.join( 'web', 'app', 'themes' ); + +const isDirectory = ( dirPath ) => { + try { + return fs.statSync( dirPath ).isDirectory(); + } catch { + return false; + } +}; + +const detectMode = ( cwd ) => { + if ( isDirectory( path.resolve( cwd, THEMES_SUBDIR ) ) ) { + return 'brave-root'; + } + + if ( fs.existsSync( path.resolve( cwd, 'style.css' ) ) ) { + return 'theme-root'; + } + + return null; +}; + +/** + * Discovers theme directories (dirs containing a `style.css`) inside a + * brave-root themes directory. + */ +const discoverThemeNames = ( themesDir ) => { + if ( ! isDirectory( themesDir ) ) { + return []; + } + + return fs.readdirSync( themesDir ).filter( ( dirName ) => { + const fullPath = path.join( themesDir, dirName ); + + if ( ! isDirectory( fullPath ) ) { + return false; + } + + return fs.existsSync( path.join( fullPath, 'style.css' ) ); + } ); +}; + +export const resolveThemeContext = () => { + const cwd = process.cwd(); + const mode = detectMode( cwd ); + + if ( mode === 'theme-root' ) { + const name = path.basename( cwd ); + + return { + mode, + themes: [ { name, dir: cwd, relDir: '.' } ], + themeDir: () => cwd, + themeRelDir: () => '.', + defaultTheme: name, + }; + } + + if ( mode === 'brave-root' ) { + const themesDir = path.resolve( cwd, THEMES_SUBDIR ); + const themes = discoverThemeNames( themesDir ).map( ( name ) => ( { + name, + dir: path.join( themesDir, name ), + relDir: path.join( THEMES_SUBDIR, name ), + } ) ); + + return { + mode, + themes, + themeDir: ( name ) => path.join( themesDir, name ), + themeRelDir: ( name ) => path.join( THEMES_SUBDIR, name ), + defaultTheme: 'sage', + }; + } + + throw new Error( + 'Unable to determine project context: no "web/app/themes" directory ' + + 'and no "style.css" in the current directory. Run from a Brave ' + + 'project root or from a theme directory.' + ); +}; From 695d187aae7a40141ebb1ed76a2373042a1c683c Mon Sep 17 00:00:00 2001 From: YvetteNikolov Date: Tue, 30 Jun 2026 22:35:21 +0200 Subject: [PATCH 3/4] feat(eslint-config): resolve @sage/scripts alias per project layout --- packages/eslint-config/README.md | 7 ++++ packages/eslint-config/src/index.js | 8 ++--- .../src/utils/resolve-import-aliases.js | 32 +++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 packages/eslint-config/src/utils/resolve-import-aliases.js diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md index 313258c..b9ab397 100644 --- a/packages/eslint-config/README.md +++ b/packages/eslint-config/README.md @@ -30,3 +30,10 @@ const eslintSettings = merge(require('@yardinternet/eslint-config'), [ module.exports = eslintSettings; ``` + +## brave-root vs theme-root + +The `@sage/scripts` import alias auto-detects where ESLint runs: + +- **brave-root** — cwd has `web/app/themes/`. `@sage/scripts` → `web/app/themes/sage/resources/scripts`. +- **theme-root** — cwd is a single theme (has `style.css`, no `web/app/themes/`). `@sage/scripts` and `@/scripts` → `./resources/scripts`. diff --git a/packages/eslint-config/src/index.js b/packages/eslint-config/src/index.js index cc5ebf2..cfbdd54 100644 --- a/packages/eslint-config/src/index.js +++ b/packages/eslint-config/src/index.js @@ -5,6 +5,7 @@ const tsPlugin = require( '@typescript-eslint/eslint-plugin' ); const { fixupConfigRules } = require( '@eslint/compat' ); const js = require( '@eslint/js' ); const { FlatCompat } = require( '@eslint/eslintrc' ); +const resolveImportAliases = require( './utils/resolve-import-aliases' ); const compat = new FlatCompat( { baseDirectory: __dirname, @@ -73,12 +74,7 @@ module.exports = [ settings: { ...sharedSettings, 'import/resolver': { - alias: [ - [ - '@sage/scripts', - './web/app/themes/sage/resources/scripts', - ], - ], + alias: resolveImportAliases(), }, }, }, diff --git a/packages/eslint-config/src/utils/resolve-import-aliases.js b/packages/eslint-config/src/utils/resolve-import-aliases.js new file mode 100644 index 0000000..5adbe8c --- /dev/null +++ b/packages/eslint-config/src/utils/resolve-import-aliases.js @@ -0,0 +1,32 @@ +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * Resolves the `import/resolver` aliases for the current project layout. + * + * - brave-root (default): the `@sage/scripts` alias, unchanged. + * - theme-root (cwd is a theme — no `web/app/themes`, has `style.css`): point + * both `@/scripts` and the `@sage/scripts` at the local + * `./resources/scripts` so theme imports resolve. + */ +const resolveImportAliases = () => { + const cwd = process.cwd(); + const isBraveRoot = fs.existsSync( path.resolve( cwd, 'web/app/themes' ) ); + + if ( isBraveRoot ) { + return [ + [ '@sage/scripts', './web/app/themes/sage/resources/scripts' ], + ]; + } + + const themeName = path.basename( cwd ); + + return [ + [ `@${ themeName }/scripts`, './resources/scripts' ], + // Brave-root alias kept so shared/starter code importing `@sage/scripts` + // keeps resolving in a theme-root build. + [ '@sage/scripts', './resources/scripts' ], + ]; +}; + +module.exports = resolveImportAliases; From 9f63e01575de8ad4d855463dd0408e44c2da5aeb Mon Sep 17 00:00:00 2001 From: YvetteNikolov Date: Tue, 30 Jun 2026 22:35:30 +0200 Subject: [PATCH 4/4] feat(prettier-config): find the tailwind stylesheet in a single theme --- packages/prettier-config/README.md | 7 +++++++ .../src/utils/find-tailwind-stylesheet.js | 14 +++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/prettier-config/README.md b/packages/prettier-config/README.md index 5e57259..74dae44 100644 --- a/packages/prettier-config/README.md +++ b/packages/prettier-config/README.md @@ -32,3 +32,10 @@ const prettierSettings = merge(require('@yardinternet/prettier-config'), { }) module.exports = prettierSettings; ``` + +## brave-root vs theme-root + +The Tailwind config stylesheet is located automatically, trying both layouts: + +- **brave-root** — `web/app/themes/sage/resources/styles/base/config.css`. +- **theme-root** — the theme's own `resources/styles/base/config.css`. diff --git a/packages/prettier-config/src/utils/find-tailwind-stylesheet.js b/packages/prettier-config/src/utils/find-tailwind-stylesheet.js index f87f525..c84ef3a 100644 --- a/packages/prettier-config/src/utils/find-tailwind-stylesheet.js +++ b/packages/prettier-config/src/utils/find-tailwind-stylesheet.js @@ -2,7 +2,13 @@ const fs = require( 'fs' ); const path = require( 'path' ); function findTailwindStylesheet() { - const relative = 'web/app/themes/sage/resources/styles/base/config.css'; + // Candidate locations of the Tailwind config stylesheet, tried in order: + // - brave-root: the sage theme inside `web/app/themes`. + // - theme-root: the theme's own resources (cwd is the theme). + const relatives = [ + 'web/app/themes/sage/resources/styles/base/config.css', + 'resources/styles/base/config.css', + ]; const roots = []; @@ -21,8 +27,10 @@ function findTailwindStylesheet() { roots.push( process.cwd() ); for ( const root of roots ) { - const candidate = path.resolve( root, relative ); - if ( fs.existsSync( candidate ) ) return candidate; + for ( const relative of relatives ) { + const candidate = path.resolve( root, relative ); + if ( fs.existsSync( candidate ) ) return candidate; + } } return null;