Найдите бесполезные модули в вашем проекте на основе babel и postcss

внешний интерфейс JavaScript
Найдите бесполезные модули в вашем проекте на основе babel и postcss

задний план

Хаохао — фронтенд-инженер в бизнес-линии (профессиональный паж), а я — инженер по цепочке инструментов (профессиональный инструментальный специалист) в архитектурной группе.Однажды Хаохао сказал мне, что в проекте слишком много неиспользуемых модулей, которые он поддерживает.На самом деле его можно удалить.Его сбросили,но теперь я не знаю,какие из них бесполезны,поэтому удалять не решаюсь,и спросите,можно ли сделать инструмент для поиска всех модулей на которые не ссылаются. В конце концов, я профессиональный инструментщик, такого рода требования для меня не представляют сложности, поэтому на реализацию этого инструмента у меня ушло более полдня.

Этот инструмент является общим инструментом, который может использоваться проектами узлов и проектами внешнего интерфейса для поиска неиспользуемых модулей, а идея обхода модулей может применяться во многих других местах. Поэтому я разобрался с идеями реализации и написал эту статью.

Анализ мыслей

Цель состоит в том, чтобы найти все неиспользуемые модули в проекте. В проекте всегда есть несколько входных модулей, и код будет запаковываться или запускаться из этих модулей. Сначала нам нужно знать все входные модули.

Получив входной модуль, проанализируйте, какие модули используются (зависят) от входного модуля, а затем проанализируйте зависимости от используемых модулей, чтобы рекурсивно анализировать до тех пор, пока не появятся новые зависимости. В этом процессе используются все пройденные модули, а непройденные не используются, то есть искомые модули можно удалить.

В процессе обхода мы можем сохранить информацию о модуле и взаимосвязь между модулями как взаимосвязь между объектами и объектами и построить граф зависимостей (потому что может быть модуль, от которого зависят два модуля, или даже круговая зависимость, так это график) Последующий анализ структуры данных этого графа зависимостей — это анализ зависимостей между модулями. Для этого требования нам нужно только сохранить пройденный путь модуля, и нам не нужно генерировать граф зависимостей.

Обходя разные модули, чтобы найти, от каких модулей он зависит, существуют разные способы анализа зависимостей для разных модулей:

  • модули js, ts, jsx, tsx определяют зависимости в соответствии с импортом модуля es или требованием commonjs
  • модули css, less, scss определяют зависимости в соответствии с синтаксисом @import и url()

И для получения зависимого пути может также потребоваться выполнить слой обработки, потому что, например, веб-пакет может настроить псевдоним, машинописный текст может настроить пути, а путь монорепозитория также имеет свои особенности. с, и только после обработки Узнайте, каков реальный путь модуля.

После анализа зависимостей от входного модуля завершите обход графа модулей, сохраните используемые пути модулей, а затем используйте все пути модулей, чтобы отфильтровать те, которые используются, а остальные — неиспользуемые модули.

Идея наверное такая, давайте реализуем:

Код

обход модуля

Мы хотим написать обходчик модуля, передавая путь к текущему модулю и функцию обратного вызова для обработки содержимого модуля.Процесс обработки выглядит следующим образом:

  • Попробуйте заполнить путь, потому что .js, .json, .tsx и т. д. могут опускать имя суффикса
  • Получить тип модуля на основе пути
  • Если это модуль js, обработайте его, пройдя js
  • Если это модуль css, обработайте его, пройдя css
const MODULE_TYPES = {
    JS: 1 << 0,
    CSS: 1 << 1,
    JSON: 1 << 2
};

function getModuleType(modulePath) {
    const moduleExt = extname(modulePath);
     if (JS_EXTS.some(ext => ext === moduleExt)) {
         return MODULE_TYPES.JS;
     } else if (CSS_EXTS.some(ext => ext === moduleExt)) {
         return MODULE_TYPES.CSS;
     } else if (JSON_EXTS.some(ext => ext === moduleExt)) {
         return MODULE_TYPES.JSON;
     }
}

function traverseModule (curModulePath, callback) {
    curModulePath = completeModulePath(curModulePath);

    const moduleType = getModuleType(curModulePath);

    if (moduleType & MODULE_TYPES.JS) {
        traverseJsModule(curModulePath, callback);
    } else if (moduleType & MODULE_TYPES.CSS) {
        traverseCssModule(curModulePath, callback);
    }
}

обход модуля js

Обход js-модулей требует анализа импорта и требует зависимостей. Мы используем babel для:

  • прочитать содержимое файла
  • Определите, следует ли включить плагин синтаксического анализа машинописного текста и jsx в соответствии с суффиксом имени .jsx, .tsx и т. д.
  • Преобразование кода в AST с помощью парсера babel
  • Обход AST с обходом Babel
  • Обработайте AST ImportDeclaration и CallExpression, чтобы извлечь из него пути зависимостей.
  • После обработки пути зависимостей и превращения его в реальный путь продолжаем обход модуля пути

код показывает, как показано ниже:

function traverseJsModule(curModulePath, callback) {
    const moduleFileContent = fs.readFileSync(curModulePath, {
        encoding: 'utf-8'
    });

    const ast = parser.parse(moduleFileContent, {
        sourceType: 'unambiguous',
        plugins: resolveBabelSyntaxtPlugins(curModulePath)
    });

    traverse(ast, {
        ImportDeclaration(path) {
            const subModulePath = moduleResolver(curModulePath, path.get('source.value').node);
            if (!subModulePath) {
                return;
            }
            callback && callback(subModulePath);
            traverseModule(subModulePath, callback);
        },
        CallExpression(path) {
            if (path.get('callee').toString() === 'require') {
                const subModulePath = moduleResolver(curModulePath, path.get('arguments.0').toString().replace(/['"]/g, ''));
                if (!subModulePath) {
                    return;
                }
                callback && callback(subModulePath);
                traverseModule(subModulePath, callback);
            }
        }
    })
}

обход модуля css

Для обхода модулей css требуется разбор @import и url(). Мы используем postcss для:

  • прочитать содержимое файла
  • Решите, следует ли включить синтаксические плагины less и scss в соответствии с путем к файлу .less, .scss
  • Преобразование содержимого файла в AST с помощью postcss.parse
  • Пройдите узлы @import для извлечения путей зависимостей
  • Обход объявлений стилей, отфильтровывание значений url() и извлечение путей зависимостей
  • После обработки пути зависимостей и превращения его в реальный путь продолжаем обход модуля пути

код показывает, как показано ниже:

function traverseCssModule(curModulePath, callback) {
    const moduleFileConent = fs.readFileSync(curModulePath, {
        encoding: 'utf-8'
    });

    const ast = postcss.parse(moduleFileConent, {
        syntaxt: resolvePostcssSyntaxtPlugin(curModulePath)
    });
    ast.walkAtRules('import', rule => {
        const subModulePath = moduleResolver(curModulePath, rule.params.replace(/['"]/g, ''));
        if (!subModulePath) {
            return;
        }
        callback && callback(subModulePath);
        traverseModule(subModulePath, callback);
    });
    ast.walkDecls(decl => {
        if (decl.value.includes('url(')) {
            const url = /.*url\((.+)\).*/.exec(decl.value)[1].replace(/['"]/g, '');
            const subModulePath = moduleResolver(curModulePath, url);
            if (!subModulePath) {
                return;
            }
            callback && callback(subModulePath);
        }
    } )
}

Обработка пути модуля

Модули css или js обрабатываются после извлечения пути:

  • Поддерживает пользовательскую логику анализа пути, позволяя пользователям настраивать правила анализа пути по мере необходимости.
  • Отфильтровать модули в node_modules, анализ не требуется
  • Суффикс пути завершения
  • Если модуль был пройден, пропустите обход, чтобы избежать циклических зависимостей

код показывает, как показано ниже:

const visitedModules = new Set();

function moduleResolver (curModulePath, requirePath) {
    if (typeof requirePathResolver === 'function') {// requirePathResolver 是用户自定义的路径解析逻辑
        const res = requirePathResolver(dirname(curModulePath), requirePath);
        if (typeof res === 'string') {
            requirePath = res;
        }
    }

    requirePath = resolve(dirname(curModulePath), requirePath);

    // 过滤掉第三方模块
    if (requirePath.includes('node_modules')) {
        return '';
    }

    requirePath =  completeModulePath(requirePath);

    if (visitedModules.has(requirePath)) {
        return '';
    } else {
        visitedModules.add(requirePath);
    }
    return requirePath;
}

Таким образом, мы завершили преобразование анализируемого пути зависимости в его реальный путь.

завершение пути

При написании кода можно опускать суффиксы некоторых файлов (.js, .tsx, .json и т.д.), нам нужно реализовать логику завершения:

  • Если суффикс уже есть, пропустите его
  • Если это каталог, попробуйте найти файл index.xxx и верните путь, если он найден.
  • Если это файл, попробуйте дополнить суффикс .xxx и вернуть путь, если он найден.
  • Если он не найден, он сообщит об ошибке: модуль не найден
const JS_EXTS = ['.js', '.jsx', '.ts', '.tsx'];
const JSON_EXTS = ['.json'];

function completeModulePath (modulePath) {
    const EXTS = [...JSON_EXTS, ...JS_EXTS];
    if (modulePath.match(/\.[a-zA-Z]+$/)) {
        return modulePath;
    }

    function tryCompletePath (resolvePath) {
        for (let i = 0; i < EXTS.length; i ++) {
            let tryPath = resolvePath(EXTS[i]);
            if (fs.existsSync(tryPath)) {
                return tryPath;
            }
        }
    }

    function reportModuleNotFoundError (modulePath) {
        throw chalk.red('module not found: ' + modulePath);
    }

    if (isDirectory(modulePath)) {
        const tryModulePath = tryCompletePath((ext) => join(modulePath, 'index' + ext));
        if (!tryModulePath) {
            reportModuleNotFoundError(modulePath);
        } else {
            return tryModulePath;
        }
    } else if (!EXTS.some(ext => modulePath.endsWith(ext))) {
        const tryModulePath = tryCompletePath((ext) => modulePath + ext);
        if (!tryModulePath) {
            reportModuleNotFoundError(modulePath);
        } else {
            return tryModulePath;
        }
    }
    return modulePath;
}

Согласно вышеизложенным идеям мы реализовали обход модулей и нашли все используемые модули.

Отфильтруйте бесполезные модули

Мы нашли все модули, использованные выше.Далее, пока все модули используются для фильтрации используемых модулей, модули, которые не используются, являются теми, которые не используются.

Мы инкапсулируем метод findUnusedModule.

Входящие параметры:

  • записи (массив входных модулей)
  • включает (глобальные выражения для всех модулей)
  • resolveRequirePath (настраиваемая логика разрешения пути)
  • cwd (корневой путь для разрешения модуля)

Возвращает объект, содержащий:

  • все (все модули)
  • используется (модуль используется)
  • unused (неиспользуемые модули)

Обработать:

  • Объединить параметры и параметры по умолчанию
  • Дескриптор включает пути к модулям на основе cwd
  • Найти все модули в соответствии с глобальным выражением include
  • Пройдите все в качестве записи и запишите используемые модули.
  • Отфильтруйте используемые модули и найдите неиспользуемые модули
const defaultOptions = {
    cwd: '',
    entries: [],
    includes: ['**/*', '!node_modules'],
    resolveRequirePath: () => {}
}

function findUnusedModule (options) {
    let {
        cwd,
        entries,
        includes,
        resolveRequirePath
    } = Object.assign(defaultOptions, options);

    includes = includes.map(includePath => (cwd ? `${cwd}/${includePath}` : includePath));

    const allFiles = fastGlob.sync(includes).map(item => normalize(item));
    const entryModules = [];
    const usedModules = [];

    setRequirePathResolver(resolveRequirePath);
    entries.forEach(entry => {
        const entryPath = resolve(cwd, entry);
        entryModules.push(entryPath);
        traverseModule(entryPath, (modulePath) => {
            usedModules.push(modulePath);
        });
    });

    const unusedModules = allFiles.filter(filePath => {
        const resolvedFilePath = resolve(filePath);
        return !entryModules.includes(resolvedFilePath) && !usedModules.includes(resolvedFilePath);
    });
    return {
        all: allFiles,
        used: usedModules,
        unused: unusedModules
    }
}

Таким образом, наш инкапсулированный findUnusedModule может выполнить исходное требование: найти неиспользуемые модули в рамках проекта.

Тестовая функция

Давайте проверим эффект с помощьюэтот каталогВ качестве тестового проекта:

const { all, used, unused } = findUnusedModule({
    cwd: process.cwd(),
    entries: ['./demo-project/fre.js', './demo-project/suzhe2.js'],
    includes: ['./demo-project/**/*'],
    resolveRequirePath (curDir, requirePath) {
        if (requirePath === 'b') {
            return path.resolve(curDir, './lib/ssh.js');
        }
        return requirePath;
    }
});

Результат выглядит следующим образом:

Успешно найдены неиспользуемые модули! (можно поставитькодСними и попробуй)

думать

Мы реализовали обходчик модулей, он может начинаться с одного модуля на обход. Для этого мы добились того, что модуль ходунка должен найти бесполезный, на самом деле, вы также можете использовать его для анализа других потребностей, это распространенный способ перемещения.

Мы знаем, что с помощью babel можно делать две вещи:

  • Перевод кода: перевод из es next, typescript и других кодов в js, поддерживаемый целевой средой.
  • Статический анализ: анализируйте содержимое кода, например проверку типов, lint и т. д., без генерации кода.

Этот итератор модуля также может делать то же самое:

  • Статический анализ: анализ зависимостей между модулями, построение графа зависимостей и выполнение некоторых функций анализа.
  • Упаковка: Распечатайте каждый модуль в графе зависимостей как объектный код с соответствующим шаблоном кода.

Суммировать

Сначала разбираем требования: выясняем модули, которые не используются в проекте. Это требует реализации обходчика модулей.

Обход модуля должен выполнять различную обработку для модуля js и модуля css: модуль js анализирует импорт и требование, css анализирует url() и @import.

После этого проанализированный путь необходимо обработать, чтобы он стал реальным путем. Для обработки node_modules, псевдонимов веб-пакетов, типов машинописных текстов и т. д. мы предоставляем функцию обратного вызова для расширения разработчиками.

После реализации обхода модуля, если указаны все модули и входные модули, мы можем узнать, какие модули используются, а какие нет.

Проверено и соответствует нашим требованиям.

Этот обходчик модулей универсален и может использоваться для различного статического анализа, а также может использоваться для последующей печати кода для формирования упаковщика.

Github адрес кодЗдесь, если вам интересно, вы можете снести его и запустить, и очень полезно научиться писать обходчик модулей.

пасхальные яйца

Когда я представил эту функцию Haohao в то время, я написал документ для реализации идеи, и он также размещен здесь:

Хао Хао: Брат Гуан, какова общая идея? Код становится беспорядочным, как только появляется.

я:Модуль - это графовая структура, и ему задано начало обхода с определенного входа.По сути, это процесс dfs, но есть циклические ссылки, которые надо решать, записывая обработанные модули. Рекурсивно обходим этот граф, и используются обработанные модули.

Хао Хао:dfs это модуль, как определить подмодуль?

я: разные модули имеют разные методы обработки, например, js-модули должны определяться с помощью import или require, а css — с помощью @import и url(). Но это всего лишь пути извлечения, этот путь пока недоступен, его нужно преобразовать в реальный путь, и должен быть процесс разрешения пути.

Хао Хао: Что делает путь разрешения?

я: это обработка псевдонимов, фильтрация модулей в node_modules, потому что мы их здесь не используем, а затем определение абсолютного пути подмодуля в соответствии с путем текущего модуля. Он также предоставляет функцию ловушки, позволяющую пользователям настраивать логику разрешения требуемого пути.

Хао Хао: Это requireRequirePath?

я: Да, это хук, позволяющий пользователям настраивать логику разрешения пути.

Хао Хао: Я вообще процесс понимаю?

я: Скажи мне

Хао Хао: Модули проекта составляют граф зависимости.Если мы хотим определить модули, которые не используются, мы должны сначала найти модули, которые используются, а затем отфильтровать их. Используемые модули должны запускать dfs с несколькими входными модулями.Существуют разные способы извлечения требуемого пути при обходе разных модулей.После извлечения путь необходимо разрешить, чтобы получить реальный путь, а затем обрабатываются подмодули рекурсивно. Таким образом, вы можете определить, какие из них используются, пройдя их один раз. При этом надо еще разобраться с проблемой циклических ссылок, ведь модуль ведь граф, и в dfs будут циклы.

я: Да, отлично.