MaxMilton/ekscss

View on GitHub
packages/svelte-ekscss/src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable no-console */

import { type XCSSCompileOptions, compile, resolvePlugins } from 'ekscss';
import JoyCon from 'joycon';
import * as colors from 'kleur/colors';
import type { Preprocessor, PreprocessorGroup } from 'svelte/compiler';

export type XCSSConfig = Omit<XCSSCompileOptions, 'from' | 'to'>;

interface PluginOptions {
  /** An XCSS config object or the path to a config file. */
  config?: XCSSConfig | string;
}

export const style = ({ config }: PluginOptions = {}): Preprocessor => {
  const reBadValue =
    /UNDEFINED|INVALID|#apply:|null|undefined|NaN|\[object \w+]/;
  const joycon = new JoyCon({
    files: [
      '.xcssrc.cjs',
      '.xcssrc.js',
      '.xcssrc.json',
      'xcss.config.cjs',
      'xcss.config.js',
      'xcss.config.json',
      'package.json',
    ],
    packageKey: 'xcss',
  });
  let configData: XCSSConfig;
  let configPath: string | undefined;

  return async ({ attributes, content, filename }) => {
    if (attributes.lang !== 'xcss') return;

    // XXX: Svelte has no way to identify when the config was changed when
    // watching during dev mode, so to update the config the whole svelte
    // processes must be manually restarted

    if (!config || typeof config === 'string') {
      // load user defined config or fall back to default file locations
      const result = await joycon.load(config ? [config] : undefined);
      configData = (result.data as XCSSConfig | undefined) ?? {};
      configPath = result.path;

      if (!result.path) {
        console.warn(colors.yellow('Warning:'), 'Unable to locate XCSS config');
      }
    } else {
      configData = config;
    }

    if (configData.plugins) {
      configData.plugins = resolvePlugins(configData.plugins);
    }

    const compiled = compile(content, {
      from: filename,
      globals: configData.globals,
      map: configData.map,
      plugins: configData.plugins,
      rootDir: configData.rootDir,
    });

    for (const warning of compiled.warnings) {
      console.warn(colors.yellow('Warning:'), warning.message || warning);

      if (warning.file) {
        // biome-ignore lint/suspicious/noConsoleLog: provide feedback
        console.log(
          '  at',
          colors.dim(
            [warning.file, warning.line, warning.column]
              .filter(Boolean)
              .join(':'),
          ),
        );
      }
    }

    if (reBadValue.test(compiled.css)) {
      console.warn(
        colors.yellow('Warning:'),
        'XCSS output may contain unwanted value',
      );
    }

    const { css, dependencies, map } = compiled;
    if (configPath) dependencies.push(configPath);

    // eslint-disable-next-line consistent-return
    return {
      code: css,
      dependencies,
      ...(map && { map: map.toString() }),
    };
  };
};

const preprocessor = (opts: PluginOptions): PreprocessorGroup => ({
  name: 'ekscss',
  style: style(opts),
});

export default preprocessor;