plugin.js 14.9 KB
// @ts-check
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "createProcessor", {
    enumerable: true,
    get: ()=>createProcessor
});
const _path = /*#__PURE__*/ _interopRequireDefault(require("path"));
const _fs = /*#__PURE__*/ _interopRequireDefault(require("fs"));
const _postcssLoadConfig = /*#__PURE__*/ _interopRequireDefault(require("postcss-load-config"));
const _lilconfig = require("lilconfig");
const _plugins = /*#__PURE__*/ _interopRequireDefault(require("postcss-load-config/src/plugins" // Little bit scary, looking at private/internal API
));
const _options = /*#__PURE__*/ _interopRequireDefault(require("postcss-load-config/src/options" // Little bit scary, looking at private/internal API
));
const _processTailwindFeatures = /*#__PURE__*/ _interopRequireDefault(require("../../processTailwindFeatures"));
const _deps = require("./deps");
const _utils = require("./utils");
const _shared = require("../shared");
const _resolveConfigJs = /*#__PURE__*/ _interopRequireDefault(require("../../../resolveConfig.js"));
const _getModuleDependenciesJs = /*#__PURE__*/ _interopRequireDefault(require("../../lib/getModuleDependencies.js"));
const _contentJs = require("../../lib/content.js");
const _watchingJs = require("./watching.js");
const _fastGlob = /*#__PURE__*/ _interopRequireDefault(require("fast-glob"));
const _findAtConfigPathJs = require("../../lib/findAtConfigPath.js");
const _log = /*#__PURE__*/ _interopRequireDefault(require("../../util/log"));
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
/**
 *
 * @param {string} [customPostCssPath ]
 * @returns
 */ async function loadPostCssPlugins(customPostCssPath) {
    let config = customPostCssPath ? await (async ()=>{
        let file = _path.default.resolve(customPostCssPath);
        // Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
        // @ts-ignore
        let { config ={}  } = await (0, _lilconfig.lilconfig)("postcss").load(file);
        if (typeof config === "function") {
            config = config();
        } else {
            config = Object.assign({}, config);
        }
        if (!config.plugins) {
            config.plugins = [];
        }
        return {
            file,
            plugins: (0, _plugins.default)(config, file),
            options: (0, _options.default)(config, file)
        };
    })() : await (0, _postcssLoadConfig.default)();
    let configPlugins = config.plugins;
    let configPluginTailwindIdx = configPlugins.findIndex((plugin)=>{
        if (typeof plugin === "function" && plugin.name === "tailwindcss") {
            return true;
        }
        if (typeof plugin === "object" && plugin !== null && plugin.postcssPlugin === "tailwindcss") {
            return true;
        }
        return false;
    });
    let beforePlugins = configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx);
    let afterPlugins = configPluginTailwindIdx === -1 ? configPlugins : configPlugins.slice(configPluginTailwindIdx + 1);
    return [
        beforePlugins,
        afterPlugins,
        config.options
    ];
}
function loadBuiltinPostcssPlugins() {
    let postcss = (0, _deps.loadPostcss)();
    let IMPORT_COMMENT = "__TAILWIND_RESTORE_IMPORT__: ";
    return [
        [
            (root)=>{
                root.walkAtRules("import", (rule)=>{
                    if (rule.params.slice(1).startsWith("tailwindcss/")) {
                        rule.after(postcss.comment({
                            text: IMPORT_COMMENT + rule.params
                        }));
                        rule.remove();
                    }
                });
            },
            (0, _deps.loadPostcssImport)(),
            (root)=>{
                root.walkComments((rule)=>{
                    if (rule.text.startsWith(IMPORT_COMMENT)) {
                        rule.after(postcss.atRule({
                            name: "import",
                            params: rule.text.replace(IMPORT_COMMENT, "")
                        }));
                        rule.remove();
                    }
                });
            }
        ],
        [],
        {}
    ];
}
let state = {
    /** @type {any} */ context: null,
    /** @type {ReturnType<typeof createWatcher> | null} */ watcher: null,
    /** @type {{content: string, extension: string}[]} */ changedContent: [],
    configDependencies: new Set(),
    contextDependencies: new Set(),
    /** @type {import('../../lib/content.js').ContentPath[]} */ contentPaths: [],
    refreshContentPaths () {
        var ref;
        this.contentPaths = (0, _contentJs.parseCandidateFiles)(this.context, (ref = this.context) === null || ref === void 0 ? void 0 : ref.tailwindConfig);
    },
    get config () {
        return this.context.tailwindConfig;
    },
    get contentPatterns () {
        return {
            all: this.contentPaths.map((contentPath)=>contentPath.pattern),
            dynamic: this.contentPaths.filter((contentPath)=>contentPath.glob !== undefined).map((contentPath)=>contentPath.pattern)
        };
    },
    loadConfig (configPath, content) {
        if (this.watcher && configPath) {
            this.refreshConfigDependencies(configPath);
        }
        let config = configPath ? require(configPath) : {};
        // @ts-ignore
        config = (0, _resolveConfigJs.default)(config, {
            content: {
                files: []
            }
        });
        // Override content files if `--content` has been passed explicitly
        if ((content === null || content === void 0 ? void 0 : content.length) > 0) {
            config.content.files = content;
        }
        return config;
    },
    refreshConfigDependencies (configPath) {
        _shared.env.DEBUG && console.time("Module dependencies");
        for (let file of this.configDependencies){
            delete require.cache[require.resolve(file)];
        }
        if (configPath) {
            let deps = (0, _getModuleDependenciesJs.default)(configPath).map(({ file  })=>file);
            for (let dependency of deps){
                this.configDependencies.add(dependency);
            }
        }
        _shared.env.DEBUG && console.timeEnd("Module dependencies");
    },
    readContentPaths () {
        let content = [];
        // Resolve globs from the content config
        // TODO: When we make the postcss plugin async-capable this can become async
        let files = _fastGlob.default.sync(this.contentPatterns.all);
        for (let file of files){
            content.push({
                content: _fs.default.readFileSync(_path.default.resolve(file), "utf8"),
                extension: _path.default.extname(file).slice(1)
            });
        }
        // Resolve raw content in the tailwind config
        let rawContent = this.config.content.files.filter((file)=>{
            return file !== null && typeof file === "object";
        });
        for (let { raw: htmlContent , extension ="html"  } of rawContent){
            content.push({
                content: htmlContent,
                extension
            });
        }
        return content;
    },
    getContext ({ createContext , cliConfigPath , root , result , content  }) {
        if (this.context) {
            this.context.changedContent = this.changedContent.splice(0);
            return this.context;
        }
        _shared.env.DEBUG && console.time("Searching for config");
        var ref;
        let configPath = (ref = (0, _findAtConfigPathJs.findAtConfigPath)(root, result)) !== null && ref !== void 0 ? ref : cliConfigPath;
        _shared.env.DEBUG && console.timeEnd("Searching for config");
        _shared.env.DEBUG && console.time("Loading config");
        let config = this.loadConfig(configPath, content);
        _shared.env.DEBUG && console.timeEnd("Loading config");
        _shared.env.DEBUG && console.time("Creating context");
        this.context = createContext(config, []);
        Object.assign(this.context, {
            userConfigPath: configPath
        });
        _shared.env.DEBUG && console.timeEnd("Creating context");
        _shared.env.DEBUG && console.time("Resolving content paths");
        this.refreshContentPaths();
        _shared.env.DEBUG && console.timeEnd("Resolving content paths");
        if (this.watcher) {
            _shared.env.DEBUG && console.time("Watch new files");
            this.watcher.refreshWatchedFiles();
            _shared.env.DEBUG && console.timeEnd("Watch new files");
        }
        _shared.env.DEBUG && console.time("Reading content files");
        for (let file of this.readContentPaths()){
            this.context.changedContent.push(file);
        }
        _shared.env.DEBUG && console.timeEnd("Reading content files");
        return this.context;
    }
};
async function createProcessor(args, cliConfigPath) {
    var ref;
    let postcss = (0, _deps.loadPostcss)();
    let input = args["--input"];
    let output = args["--output"];
    let includePostCss = args["--postcss"];
    let customPostCssPath = typeof args["--postcss"] === "string" ? args["--postcss"] : undefined;
    let [beforePlugins, afterPlugins, postcssOptions] = includePostCss ? await loadPostCssPlugins(customPostCssPath) : loadBuiltinPostcssPlugins();
    if (args["--purge"]) {
        _log.default.warn("purge-flag-deprecated", [
            "The `--purge` flag has been deprecated.",
            "Please use `--content` instead."
        ]);
        if (!args["--content"]) {
            args["--content"] = args["--purge"];
        }
    }
    var ref1;
    let content = (ref1 = (ref = args["--content"]) === null || ref === void 0 ? void 0 : ref.split(/(?<!{[^}]+),/)) !== null && ref1 !== void 0 ? ref1 : [];
    let tailwindPlugin = ()=>{
        return {
            postcssPlugin: "tailwindcss",
            Once (root, { result  }) {
                _shared.env.DEBUG && console.time("Compiling CSS");
                (0, _processTailwindFeatures.default)(({ createContext  })=>{
                    console.error();
                    console.error("Rebuilding...");
                    return ()=>{
                        return state.getContext({
                            createContext,
                            cliConfigPath,
                            root,
                            result,
                            content
                        });
                    };
                })(root, result);
                _shared.env.DEBUG && console.timeEnd("Compiling CSS");
            }
        };
    };
    tailwindPlugin.postcss = true;
    let plugins = [
        ...beforePlugins,
        tailwindPlugin,
        !args["--minify"] && _utils.formatNodes,
        ...afterPlugins,
        !args["--no-autoprefixer"] && (0, _deps.loadAutoprefixer)(),
        args["--minify"] && (0, _deps.loadCssNano)()
    ].filter(Boolean);
    /** @type {import('postcss').Processor} */ // @ts-ignore
    let processor = postcss(plugins);
    async function readInput() {
        // Piping in data, let's drain the stdin
        if (input === "-") {
            return (0, _utils.drainStdin)();
        }
        // Input file has been provided
        if (input) {
            return _fs.default.promises.readFile(_path.default.resolve(input), "utf8");
        }
        // No input file provided, fallback to default atrules
        return "@tailwind base; @tailwind components; @tailwind utilities";
    }
    async function build() {
        let start = process.hrtime.bigint();
        return readInput().then((css)=>processor.process(css, {
                ...postcssOptions,
                from: input,
                to: output
            })).then((result)=>{
            if (!state.watcher) {
                return result;
            }
            _shared.env.DEBUG && console.time("Recording PostCSS dependencies");
            for (let message of result.messages){
                if (message.type === "dependency") {
                    state.contextDependencies.add(message.file);
                }
            }
            _shared.env.DEBUG && console.timeEnd("Recording PostCSS dependencies");
            // TODO: This needs to be in a different spot
            _shared.env.DEBUG && console.time("Watch new files");
            state.watcher.refreshWatchedFiles();
            _shared.env.DEBUG && console.timeEnd("Watch new files");
            return result;
        }).then((result)=>{
            if (!output) {
                process.stdout.write(result.css);
                return;
            }
            return Promise.all([
                (0, _utils.outputFile)(output, result.css),
                result.map && (0, _utils.outputFile)(output + ".map", result.map.toString())
            ]);
        }).then(()=>{
            let end = process.hrtime.bigint();
            console.error();
            console.error("Done in", (end - start) / BigInt(1e6) + "ms.");
        }).then(()=>{}, (err)=>{
            // TODO: If an initial build fails we can't easily pick up any PostCSS dependencies
            // that were collected before the error occurred
            // The result is not stored on the error so we have to store it externally
            // and pull the messages off of it here somehow
            // This results in a less than ideal DX because the watcher will not pick up
            // changes to imported CSS if one of them caused an error during the initial build
            // If you fix it and then save the main CSS file so there's no error
            // The watcher will start watching the imported CSS files and will be
            // resilient to future errors.
            console.error(err);
        });
    }
    /**
   * @param {{file: string, content(): Promise<string>, extension: string}[]} changes
   */ async function parseChanges(changes) {
        return Promise.all(changes.map(async (change)=>({
                content: await change.content(),
                extension: change.extension
            })));
    }
    if (input !== undefined && input !== "-") {
        state.contextDependencies.add(_path.default.resolve(input));
    }
    return {
        build,
        watch: async ()=>{
            state.watcher = (0, _watchingJs.createWatcher)(args, {
                state,
                /**
         * @param {{file: string, content(): Promise<string>, extension: string}[]} changes
         */ async rebuild (changes) {
                    let needsNewContext = changes.some((change)=>{
                        return state.configDependencies.has(change.file) || state.contextDependencies.has(change.file);
                    });
                    if (needsNewContext) {
                        state.context = null;
                    } else {
                        for (let change of (await parseChanges(changes))){
                            state.changedContent.push(change);
                        }
                    }
                    return build();
                }
            });
            await build();
        }
    };
}