@lowdefy/build
Version:
156 lines (146 loc) • 6.65 kB
JavaScript
/*
Copyright 2020-2026 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/ import fs from 'fs';
import path from 'path';
import { ConfigError } from '@lowdefy/errors';
import { mergeObjects } from '@lowdefy/helpers';
import { writeFile } from '@lowdefy/node-utils';
import collectBlockSourceContent from './collectBlockSourceContent.js';
const BRIDGE_DEFAULTS = {
color: {
primary: 'var(--ant-color-primary)',
'primary-hover': 'var(--ant-color-primary-hover)',
'primary-active': 'var(--ant-color-primary-active)',
'primary-bg': 'var(--ant-color-primary-bg)',
success: 'var(--ant-color-success)',
warning: 'var(--ant-color-warning)',
error: 'var(--ant-color-error)',
info: 'var(--ant-color-info)',
'text-primary': 'var(--ant-color-text)',
'text-secondary': 'var(--ant-color-text-secondary)',
'bg-container': 'var(--ant-color-bg-container)',
'bg-layout': 'var(--ant-color-bg-layout)',
border: 'var(--ant-color-border)'
},
radius: {
DEFAULT: 'var(--ant-border-radius)',
sm: 'var(--ant-border-radius-sm)',
lg: 'var(--ant-border-radius-lg)'
},
'font-size': {
DEFAULT: 'var(--ant-font-size)',
sm: 'var(--ant-font-size-sm)',
lg: 'var(--ant-font-size-lg)'
},
'font-family': {
sans: 'var(--ant-font-family)'
}
};
function objectToThemeVars(obj, prefix) {
const lines = [];
for (const [key, value] of Object.entries(obj)){
const varName = prefix ? `${prefix}-${key}` : `--${key}`;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
lines.push(...objectToThemeVars(value, varName));
} else {
lines.push(` ${varName}: ${value};`);
}
}
return lines;
}
function buildThemeVars(tailwindConfig) {
const merged = mergeObjects([
{},
BRIDGE_DEFAULTS,
tailwindConfig ?? {}
]);
return objectToThemeVars(merged).join('\n');
}
async function writeGlobalsCss({ components, context }) {
if (fs.existsSync(path.join(context.directories.config, 'public/styles.less'))) {
throw new ConfigError('public/styles.less is deprecated. Migrate to: (1) "theme" key in lowdefy.yaml for token overrides (recommended), (2) public/styles.css for custom CSS.');
}
const tailwindConfig = components.theme?.tailwind;
const themeVars = buildThemeVars(tailwindConfig);
const userStylesAbsolute = path.join(context.directories.config, 'public/styles.css');
const importUserStyles = fs.existsSync(userStylesAbsolute);
let userStylesImport = '';
if (importUserStyles) {
const relPath = path.relative(context.directories.build, userStylesAbsolute).split(path.sep).join('/');
userStylesImport = `/* User custom styles */\n "${relPath}" layer(components);\n\n`;
}
const css = `/* Generated by Lowdefy build */
/* Layer order — locks cascade priority before Tailwind declares its own layers */
theme, base, antd, components, utilities;
"tailwindcss";
"@lowdefy/layout/grid.css";
${userStylesImport}/* Content sources for Tailwind JIT — block JS content collected at build time */
"../lowdefy-build/tailwind/*.html";
/* Imported CSS file — when this changes, PostCSS re-runs and Tailwind re-scans @source */
"./tailwind-candidates.css";
/* Themed scrollbars — opts out of the browser default that clashes on dark surfaces,
especially on Windows/Linux where scrollbars are always visible and native-chrome.
Colors use antd CSS custom properties so they auto-swap with dark/light mode. */
base {
* {
scrollbar-width: thin;
scrollbar-color: var(--ant-color-border-secondary, rgba(0, 0, 0, 0.15)) transparent;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: var(--ant-color-border-secondary, rgba(0, 0, 0, 0.15));
background-clip: padding-box;
border: 2px solid transparent;
border-radius: 10px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: var(--ant-color-text-tertiary, rgba(0, 0, 0, 0.45));
}
*::-webkit-scrollbar-corner {
background: transparent;
}
}
/* Antd-to-Tailwind theme bridge — extends default Tailwind theme with antd design tokens */
inline {
${themeVars}
}
`;
await context.writeBuildArtifact('globals.css', css);
// Standalone layer order declaration — imported FIRST in _app.js so that
// Next.js/Turbopack generates a critical CSS chunk that loads before any
// JavaScript (including antd's CSS-in-JS). This guarantees the cascade
// layer priority is locked (antd > base/preflight) before antd injects
// @layer antd {} at runtime. Without this, antd wins the race and becomes
// the lowest-priority layer.
await context.writeBuildArtifact('layer-order.css', '/* Generated by Lowdefy build */\n@layer theme, base, antd, components, utilities;\n');
await context.writeBuildArtifact('tailwind-candidates.css', '/* Generated by Lowdefy build — rewritten on page changes to trigger CSS recompilation */\n');
for (const [pageId, content] of context.tailwindContentMap ?? []){
await writeFile(path.join(context.directories.server, 'lowdefy-build', 'tailwind', `${encodeURIComponent(pageId)}.html`), '<!-- Generated by Lowdefy build -->\n' + content);
}
// Collect Tailwind class candidates from block plugin JS source files.
// Resolves from the server directory so block packages are reachable regardless
// of package manager (pnpm strict isolation, yarn PnP, npm hoisting).
const blockContent = collectBlockSourceContent({
components,
serverDirectory: context.directories.server
});
if (blockContent) {
await writeFile(path.join(context.directories.server, 'lowdefy-build', 'tailwind', '_blocks.html'), '<!-- Block plugin JS content collected at build time -->\n' + blockContent);
}
}
export default writeGlobalsCss;