Node.js: разрабатываем пакетный менеджер

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.



Привет, друзья!


Вам когда-нибудь хотелось узнать, как под капотом работают пакетные менеджеры (Package Manager, PM) — интерфейсы командной строки (Command Line Interface, CLI) для установки зависимостей проектов наподобие npm или yarn? Если хотелось, тогда эта статья для вас.


В данном туториале мы разработаем простой пакетный менеджер на Node.js и TypeScript. В качестве образца для подражания мы будем использовать yarn. Если вы не знакомы с TS, советую взглянуть на эту карманную книгу.


Наш CLI будет называться my-yarn. В качестве lock-файла (yarn.lock, package-lock.json) он будет использовать файл my-yarn.yml.


Источник вдохновения.


Код проекта.


В процессе разработки CLI мы будем использовать несколько интересных npm-пакетов. Давайте начнем наше путешествие с краткого знакомства с ними.


Пакеты


find-up


find-up — это утилита для поиска файла или директории в родительских директориях.


Установка


yarn add find-up

Использование


import { findUp } from 'find-up'
import fs from 'fs-extra'

// находим файл `package.json` (путь к нему)
const filePath = await findUp('package.json')
// читаем его содержимое как `JSON`
const fileContent = await fs.readJson(filePath)

fs-extra


fs-extra — это просто fs на стероидах.


Установка


yarn add fs-extra

js-yaml


js-yaml — это утилита для разбора (парсинга) файла в формате YAML в объект и сериализации объекта обратно в yaml.


Установка


yarn add js-yaml

Использование


import { findUp } from 'find-up'
import fs from 'fs-extra'
import yaml from 'js-yaml'

const filePath = await findUp('my-yarn.yml')
const fileContent = await fs.readFile(filePath, 'utf-8')
// разбираем файл
// метод `load` принимает строку и опциональный объект с настройками
const fileObj = yaml.load(fileContent)

// сериализуем файл
// метод `dump` принимает объект c содержимым файла и опциональный объект с настройками
await fs.writeFile(
  filePath,
  yaml.dump(fileObj, { noRefs: true, sortKeys: true })
)
// `noRefs: true` запрещает преобразование дублирующихся объектов в ссылки
// `sortKeys: true` выполняет сортировку ключей объекта при формировании файла

log-update


log-update — это утилита для вывода сообщений в терминал с перезаписью предыдущего вывода. Может использоваться для рендеринга индикаторов прогресса, анимации и др.


Установка


yarn add log-update

Использование


import logUpdate from 'log-update'

export function logResolving(pkgName: string) {
  logUpdate(`[1/2] Resolving: ${pkgName}`)
}

node-fetch


node-fetch — это обертка над Fetch API для Node.js.


Установка


yarn add node-fetch

progress


progress — это утилита для создания индикаторов загрузки, состоящих из ASCII-символов.


Установка


yarn add progress

Использование


import logUpdate from 'log-update'
import ProgressBar from 'progress'

export function prepareInstall(total: number) {
  logUpdate('[1/2] Finished resolving.')
  // конструктор принимает строку с токенами и опциональный объект с настройками
  progress = new ProgressBar('[2/2] Installing [:bar]', {
    // символ заполнения
    complete: '#',
    // общее количество тиков (ticks)
    total
  })
}

semver


semver — это семантический "версионер" (semantic versioner) для npm.


Установка


yarn add semver

Использование


import semver from 'semver'

const versions = ['1.0.0', '3.0.0', '5.0.0']
const range = '2.0.0 - 4.0.0'

// возвращает наиболее близкую к диапазону версию или `null`
semver.maxSatisfying(versions, range) // 3.0.0

// возвращает `true`, если версия удовлетворяет диапазону
semver.satisfies(versions[1], range) // true

tar


tar — это обертка над tar для Node.js.


Установка


yarn add tar

Использование


import fetch from 'node-fetch'
import fs from 'fs-extra'
import tar from 'tar'

// адрес реестра
const REGISTRY_URI = 'https://registry.npmjs.org'
// название пакета
const pkgName = 'nodemon'
// путь к директории для пакета
const dirPath = `${process.cwd()}/node_modules/${pkgName}`

// получаем информацию о пакете
const pkgJson = await (await fetch(`${REGISTRY_URI}/${pkgName}`)).json()
// получаем последнюю версию пакета
const latestVersion = Object.keys(pkgJson.versions).at(-1)
// путь к тарбалу (tarball) пакета
const tarUrl = pkgJson.versions[latestVersion].dist.tarball

// создаем директорию для пакета при отсутствии
if (!(await fs.pathExists(dirPath))) {
  await fs.mkdirp(dirPath)
}

// тело ответа представляет собой поток данных (application/octet-stream),
// доступный для чтения
const { body: tarReadableStream } = await fetch(tarUrl)
tarReadableStream
  // извлекаем содержимое пакета
  // `cwd` - путь к директории
  // `strip` - количество ведущих элементов пути (leading path elements) для удаления
  .pipe(tar.extract({ cwd: dirPath, strip: 1 }))
  .on('close', () =>
    console.log(`Package ${pkgName} has been successfully extracted.`)
  )

yargs


yargs — это библиотека для разработки CLI с отличной документацией.


Установка


yarn add yargs

Использование yargs — тема для отдельной статьи. Я немного расскажу об этом, когда мы дойдем до разработки соответствующей части приложения.


Подготовка и настройка проекта


Создаем директорию для проекта, переходим в нее и инициализируем Node.js-проект:


mkdir ts-package-manager
cd $!

yarn init -y
# or
npm init -y

Устанавливаем зависимости:


# производственные зависимости
yarn add find-up fs-extra js-yaml log-update node-fetch progress semver tar yargs
# or
npm i ...

# зависимости для разработки
# компилятор `tsc` и типы для пакетов
yarn add -D typescript @types/find-up @types/fs-extra @types/js-yaml @types/log-update @types/node-fetch @types/progress @types/semver @types/tar @types/yargs
# or
npm i -D ...

Редактируем файл package.json:


{
  "name": "my-yarn",
  "private": false,
  "license": "MIT",
  "version": "0.0.1",
  "main": "dist/main.js",
  "bin": {
    "my-yarn": "dist/cli.js"
  },
  "files": [
    "dist"
  ],
  "engines": {
    "node": ">= 13.14.0"
  },
  "scripts": {
    "build": "tsc"
  },
  "type": "module",
  "devDependencies": {
    ...
  },
  "dependencies": {
    ...
  }
}

При запуске команды my-yarn будет выполняться код из файла dist/cli.js.


Создаем файл tsconfig.json с настройками для компиляции TS в JS (настройками, которые будут использоваться tsc при выполнении команды build):


{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "lib": [
      "ESNext"
    ],
    "moduleResolution": "Node",
    "strict": true,
    "esModuleInterop": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "outDir": "dist"
  },
  "include": [
    "src"
  ]
}

Не забываем про файл .gitignore:


node_modules
dist
# если вы используете `yarn`
yarn-error.log
# если вы работаете на `mac`
.DS_Store

Все файлы нашего проекта будут находиться в директории src и компилироваться в директорию dist. Структура директории src будет следующей:


- cli.ts
- install.ts
- list.ts
- lock.ts
- log.ts
- main.ts
- resolve.ts
- utils.ts

С подготовкой и настройкой проекта мы закончили. Можно приступать к разработке CLI.


CLI


Начнем с основного файла нашего CLImain.ts. В этом файле происходит следующее:


  • функция main, вызываемая при выполнении команды my-yarn install <packageName>, в качестве аргумента принимает массив устанавливаемых пакетов, передаваемый yargs;
  • находим и читаем файл package.json; предполагается, что он существует в проекте;
  • извлекаем из аргумента названия устанавливаемых пакетов и расширяем ими package.json;
  • читаем lock-файл; данный файл создается при отсутствии;
  • получаем информацию о зависимостях на основе расширенного package.json;
  • записываем обновленный lock-файл;
  • устанавливаем пакеты;
  • записываем обновленный package.json.

import fs from 'fs-extra'
import { findUp } from 'find-up'
import yargs from 'yargs'
// обратите внимание, что мы импортируем `JS-файлы`
import * as utils from './utils.js'
import list, { PackageJson } from './list.js'
import install from './install.js'
import * as log from './log.js'
import * as lock from './lock.js'

export default async function main(args: yargs.Arguments) {
  // находим и читаем `package.json`
  const jsonPath = (await findUp('package.json'))!
  const root = await fs.readJson(jsonPath)

  // собираем новые пакеты, добавляемые с помощью `my-yarn install <packageName>`,
  // через аргументы `CLI`
  const additionalPackages = args._.slice(1)
  if (additionalPackages.length) {
    if (args['save-dev'] || args.dev) {
      root.devDependencies = root.devDependencies || {}

      // мы заполним эти объекты после получения информации о пакетах
      additionalPackages.forEach((pkg) => (root.devDependencies[pkg] = ''))
    } else {
      root.dependencies = root.dependencies || {}

      additionalPackages.forEach((pkg) => (root.dependencies[pkg] = ''))
    }
  }

  // в продакшне нас интересуют только производственные зависимости
  if (args.production) {
    delete root.devDependencies
  }

  // читаем `lock-файл`
  await lock.readLock()

  // получаем информацию о зависимостях
  const info = await list(root)

  // сохраняем `lock-файл` асинхронно
  lock.writeLock()

  /*
  * готовимся к установке
  * обратите внимание, что здесь мы повторно вычисляем количество пакетов
  *
  * по причине дублирования
  * количество разрешенных пакетов не будет совпадать
  * с количеством устанавливаемых пакетов
  */
  log.prepareInstall(
    Object.keys(info.topLevel).length + info.unsatisfied.length
  )

  // устанавливаем пакеты верхнего уровня
  await Promise.all(
    Object.entries(info.topLevel).map(([name, { url }]) => install(name, url))
  )

  // устанавливаем пакеты с конфликтами
  await Promise.all(
    info.unsatisfied.map((item) =>
      install(item.name, item.url, `/node_modules/${item.parent}`)
    )
  )

  // форматируем `package.json`
  beautifyPackageJson(root)

  // сохраняем `package.json`
  fs.writeJSON(jsonPath, root, { spaces: 2 })
}

// форматируем поля `dependencies` и `devDependencies`
function beautifyPackageJson(packageJson: PackageJson) {
  if (packageJson.dependencies) {
    packageJson.dependencies = utils.sortKeys(packageJson.dependencies)
  }

  if (packageJson.devDependencies) {
    packageJson.devDependencies = utils.sortKeys(packageJson.devDependencies)
  }
}

Рассмотрим утилиты для логгирования (log.ts):


  • утилита logResolving выводит в терминал название устанавливаемого пакета;
  • утилита prepareInstall сообщает о завершении разрешения устанавливаемых пакетов и создает индикатор прогресса установки;
  • утилита tickInstalling обновляет индикатор прогресса установки после извлечения тарбала пакета.

import logUpdate from 'log-update'
import ProgressBar from 'progress'

let progress: ProgressBar

// разрешаемый модуль
// по аналогии с `yarn`
export function logResolving(name: string) {
  logUpdate(`[1/2] Resolving: ${name}`)
}

export function prepareInstall(count: number) {
  logUpdate('[1/2] Finished resolving.')

  // индикатор прогресса установки
  progress = new ProgressBar('[2/2] Installing [:bar]', {
    complete: '#',
    total: count
  })
}

// обновляем индикатор прогресса
// после извлечения `tarball`
export function tickInstalling() {
  progress.tick()
}

Рассмотрим утилиты для работы с lock-файлом (lock.ts):


  • утилита updateOrCreate записывает информацию о пакете в lock;
  • утилита getItem извлекает информацию о пакете по названию и версии;
  • утилита readLock читает lock;
  • утилита writeLock пишет lock.

import { findUp } from 'find-up'
import fs from 'fs-extra'
import yaml from 'js-yaml'
import { Manifest } from './resolve.js'
import { Obj } from './utils.js'

interface Lock {
  // название пакета
  [index: string]: {
    // версия
    version: string
    // путь к тарбалу
    url: string
    // хеш-сумма (контрольная сумма) файла
    shasum: string
    // зависимости
    dependencies: { [dep: string]: string }
  }
}

// находим `lock-файл`
const lockPath = (await findUp('my-yarn.yml'))!

// зачем нам 2 отдельных `lock`?
// это может быть полезным при удалении пакетов

// при добавлении или удалении пакетов
// `lock` может обновляться автоматически

// старый `lock` предназначен только для чтения
const oldLock: Lock = Object.create(null)

// новый `lock` предназначен только для записи
const newLock: Lock = Object.create(null)

// записываем информацию о пакете в `lock`
export function updateOrCreate(name: string, info: Obj) {
  if (!newLock[name]) {
    newLock[name] = Object.create(null)
  }

  Object.assign(newLock[name], info)
}

/*
* извлекаем информацию о пакете по его названию и версии (семантическому диапазону)
* обратите внимание, что мы не возвращаем данные,
* а форматируем их для того,
* чтобы структура данных соответствовала реестру пакетов (`npm`)
* это позволяет сохранить логику функции `collectDeps`
* из модуля `list`
*/
export function getItem(name: string, constraint: string): Manifest | null {
  // извлекаем элемент `lock` по ключу,
  // формат вдохновлен `yarn.lock`
  const item = oldLock[`${name}@${constraint}`]

  if (!item) {
    return null
  }

  // преобразуем структуру данных
  return {
    [item.version]: {
      dependencies: item.dependencies,
      dist: { tarball: item.url, shasum: item.shasum }
    }
  }
}

// читаем `lock`
export async function readLock() {
  if (await fs.pathExists(lockPath)) {
    Object.assign(oldLock, yaml.load(await fs.readFile(lockPath, 'utf-8')))
  }
}

// сохраняем `lock`
export async function writeLock() {
  // необходимость сортировки ключей обусловлена тем,
  // что при каждом использовании менеджера
  // порядок пакетов будет разным
  //
  // сортировка может облегчить сравнение версий `lock` с помощью `git diff`
  await fs.writeFile(
    lockPath,
    yaml.dump(newLock, { sortKeys: true, noRefs: true })
  )
}

Утилита для установки пакета (install.ts):


import fetch from 'node-fetch'
import tar from 'tar'
import fs from 'fs-extra'
import * as log from './log.js'

export default async function install(
  name: string,
  url: string,
  location = ''
) {
  // путь к директории для устанавливаемого пакета
  const path = `${process.cwd()}${location}/node_modules/${name}`

  // создаем директории рекурсивно
  await fs.mkdirp(path)

  const response = await fetch(url)

  /*
  * тело ответа - это поток данных, доступный для чтения
  * (readable stream, application/octet-stream)
  *
  * `tar.extract` принимает такой поток
  * это позволяет извлекать содержимое напрямую -
  * без его записи на диск
  */
  response
    .body!.pipe(tar.extract({ cwd: path, strip: 1 }))
    // обновляем индикатор прогресса установки после извлечения тарбала
    .on('close', log.tickInstalling)
}

Утилита для сортировки ключей объекта (utils.ts):


export type Obj = { [key: string]: any }

export const sortKeys = (obj: Obj) =>
  Object.keys(obj)
    .sort()
    .reduce((_obj: Obj, cur) => {
      _obj[cur] = obj[cur]
      return _obj
    }, Object.create(null))

Теперь рассмотрим, пожалуй, самое интересное — формирование списка зависимостей верхнего уровня и зависимостей с конфликтами (дубликатов) в файле list.ts.


Импортируем пакеты, определяем типы и переменные:


import semver from 'semver'
import resolve from './resolve.js'
import * as log from './log.js'
import * as lock from './lock.js'
import { Obj } from './utils.js'

type DependencyStack = Array<{
  name: string
  version: string
  dependencies: Obj
}>

export interface PackageJson {
  dependencies?: Obj
  devDependencies?: Obj
}

// переменная `topLevel` предназначена для выравнивания (flatten)
// дерева пакетов во избежание их дублирования
const topLevel: {
  [name: string]: { version: string; url: string }
} = Object.create(null)

// переменная `unsatisfied` предназначена для аккумулирования конфликтов (дублирующихся пакетов)
const unsatisfied: Array<{ name: string; url: string; parent: string }> = []

Определяем функцию для формирования списка зависимостей collectDeps:


// @ts-ignore
async function collectDeps(
  name: string,
  constraint: string,
  stack: DependencyStack = []
) {
  // извлекаем манифест пакета из `lock` по названию и версии
  const fromLock = lock.getItem(name, constraint)

  // получаем информацию о манифесте
  //
  // если манифест отсутствует в `lock`,
  // получаем его из сети
  const manifest = fromLock || (await resolve(name))

  // выводим в терминал название разрешаемого пакета
  log.logResolving(name)

  // используем версию пакета,
  // которая ближе всего к семантическому диапазону
  //
  // если диапазон не определен,
  // используем последнюю версию
  const versions = Object.keys(manifest)
  const matched = constraint
    ? semver.maxSatisfying(versions, constraint)
    : versions.at(-1)
  if (!matched) {
    throw new Error('Cannot resolve suitable package.')
  }

  // если пакет отсутствует в `topLevel`
  if (!topLevel[name]) {
    // добавляем его
    topLevel[name] = { url: manifest[matched].dist.tarball, version: matched }
  // если пакет имеется в `topLevel` и удовлетворяет диапазону
  } else if (semver.satisfies(topLevel[name].version, constraint)) {
    // определяем наличие конфликтов
    const conflictIndex = checkStackDependencies(name, matched, stack)

    // пропускаем проверку зависимостей при наличии конфликта
    // это позволяет избежать возникновения циклических зависимостей
    if (conflictIndex === -1) return

    /*
    * из-за особенностей алгоритма, используемого `Node.js`
    * для разрешения модулей,
    * между зависимостями зависимостей могут возникать конфликты
    *
    * одним из решений данной проблемы
    * является извлечение информации о двух предыдущих зависимостях зависимости,
    * конфликтующих между собой
    */
    unsatisfied.push({
      name,
      parent: stack
        .map(({ name }) => name)
        .slice(conflictIndex - 2)
        .join('/node_modules/'),
      url: manifest[matched].dist.tarball
    })
  // если пакет уже содержится в `topLevel`
  // но имеет другую версию
  } else {
    unsatisfied.push({
      name,
      parent: stack.at(-1)!.name,
      url: manifest[matched].dist.tarball
    })
  }

  // не забываем о зависимостях зависимости
  const dependencies = manifest[matched].dependencies || null

  // записываем манифест в `lock`
  lock.updateOrCreate(`${name}@${constraint}`, {
    version: matched,
    url: manifest[matched].dist.tarball,
    shasum: manifest[matched].dist.shasum,
    dependencies
  })

  // собираем зависимости зависимости
  if (dependencies) {
    stack.push({
      name,
      version: matched,
      dependencies
    })
    await Promise.all(
      Object.entries(dependencies)
        // предотвращаем циклические зависимости
        .filter(([dep, range]) => !hasCirculation(dep, range, stack))
        .map(([dep, range]) => collectDeps(dep, range, stack.slice()))
    )
    // удаляем последний элемент
    stack.pop()
  }

  // возвращаем семантический диапазон версии
  // для добавления в `package.json`
  if (!constraint) {
    return { name, version: `^${matched}` }
  }
}

Определяем 2 вспомогательные функции:


// данная функция определяет наличие конфликтов в зависимостях зависимости
const checkStackDependencies = (
  name: string,
  version: string,
  stack: DependencyStack
) =>
  stack.findIndex(({ dependencies }) =>
    // если пакет не является зависимостью другого пакета,
    // возвращаем `true`
    !dependencies[name] ? true : semver.satisfies(version, dependencies[name])
  )

// данная функция определяет наличие циклической зависимости
//
// если пакет содержится в стеке и имеет такую же версию
// значит, имеет место циклическая зависимость
const hasCirculation = (name: string, range: string, stack: DependencyStack) =>
  stack.some(
    (item) => item.name === name && semver.satisfies(item.version, range)
  )

Наконец, определяем основную функцию:


// наша программа поддерживает только поля
// `dependencies` и `devDependencies`
export default async function list(rootManifest: PackageJson) {
  // добавляем в `package.json` названия и версии пакетов

  // обрабатываем производственные зависимости
  if (rootManifest.dependencies) {
    ;(
      await Promise.all(
        Object.entries(rootManifest.dependencies).map((pair) =>
          collectDeps(...pair)
        )
      )
    )
      .filter(Boolean)
      .forEach(
        (item) => (rootManifest.dependencies![item!.name] = item!.version)
      )
  }

  // обрабатываем зависимости для разработки
  if (rootManifest.devDependencies) {
    ;(
      await Promise.all(
        Object.entries(rootManifest.devDependencies).map((pair) =>
          collectDeps(...pair)
        )
      )
    )
      .filter(Boolean)
      .forEach(
        (item) => (rootManifest.devDependencies![item!.name] = item!.version)
      )
  }

  // возвращаем пакеты верхнего уровня и пакеты с конфликтами
  return { topLevel, unsatisfied }
}

Определяем интерфейс командной строки (cli.ts):


#!/usr/bin/env node
import yargs from 'yargs'
import main from './main.js'

yargs
  // пример использования
  .usage('my-yarn <command> [args]')
  // получение информации о версии
  .version()
  // псевдоним
  .alias('v', 'version')
  // получение информации о порядке использования
  .help()
  // псевдоним
  .alias('h', 'help')
  // единственной командной, выполняемой нашим `CLI`,
  // будет команда `add`
  // данная команда предназначена для установки зависимостей
  .command(
    'add',
    'Install dependencies',
    (argv) => {
      // по умолчанию устанавливаются производственные зависимости
      argv.option('production', {
        type: 'boolean',
        description: 'Install production dependencies only'
      })

      // при наличии флагов `save-dev`, `dev` или `D`
      // выполняется установка зависимостей для разработки
      argv.boolean('save-dev')
      argv.boolean('dev')
      argv.alias('D', 'dev')

      return argv
    },
    // при выполнении команды `yarn add <packageName>`
    // запускается код из файла `main.js`
    main
  )
  // парсим аргументы, переданные `CLI`
  .parse()

На этом разработка нашего CLI завершена. Пришло время убедиться в его работоспособности.


Пример


Выполняем сборку проекта с помощью команды yarn build или npm run build.





Это приводит к генерации директории dist с JS-файлами проекта.





Находясь в корневой директории, подключаем наш CLI к npm с помощью команды npm link (данная команда позволяет тестировать разрабатываемые пакеты локально) и получаем список глобально установленных пакетов с помощью команды npm -g list --depth 0.





Видим в списке глобальных пакетов my-yarn@0.0.1. Для удаления my-yarn необходимо выполнить команду npm -g rm my-yarn.


Получаем информацию о версии my-yarn с помощью команды my-yarn -v и информацию о порядке использования CLI с помощью команды my-yarn -h.





Разработаем простой сервер на Express, который будет запускаться в режиме для разработки и возвращать некоторую статическую разметку.


Создаем директорию my-yarn, переходим в нее и инициализируем Node.js-проект:


mkdir my-yarn
cd $!

yarn init -yp
# или
npm init -y

Создаем файл index.html следующего содержания:


<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MyYarn</title>
  </head>
  <body>
    <h1>MyYarn - простой пакетный менеджер</h1>
  </body>
</html>

И такой файл index.js:


// для того, чтобы иметь возможность использовать `ESM`,
// необходимо определить `"type": "module"` в файле `package.json`
import express from 'express'

const app = express()

// возвращаем статику при получении `GET-запроса` по адресу `/my-yarn`
app.get('/my-yarn', (_, res) => {
  res.sendFile(`${process.cwd()}/index.html`)
})

const PORT = process.env.PORT || 3124
// запускаем сервер
app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`)
})

Для работы сервера требуется пакет express, а для его запуска в режиме для разработки — пакет nodemon. Мы выполним установку этих пакетов с помощью нашего CLI.


Находясь в директории my-yarn, устанавливаем express с помощью команды my-yarn add express и nodemon с помощью команды my-yarn add -D nodemon.





Это приводит к генерации директории node_modules, файла my-yarn.yml и обновлению файла package.json.


Добавляем команду для запуска сервера для разработки в package.json:


"scripts": {
  "dev": "node_modules/nodemon/bin/nodemon.js"
}

Обратите внимание: наш CLI не умеет выполнять скрипты, поэтому для запуска команды dev мы будем использовать yarn. Однако, поскольку мы устанавливали зависимости с помощью my-yarn, у нас отсутствует файл yarn.lock, который используется yarn для разрешения путей к пакетам. Это обуславливает необходимость указания полного пути к выполняемому файлу nodemon.


Запускаем сервер для разработки с помощью команды yarn dev.





Получаем сообщение о готовности сервера к обработке запросов.


Открываем вкладку браузера по адресу http://localhost:3124/my-yarn.





Получаем наш index.html.


Отлично. Приложение работает, как ожидается.


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


Благодарю за внимание и happy coding!




Источник: https://habr.com/ru/company/timeweb/blog/662830/


Интересные статьи

Интересные статьи

...или разбор пары комиксов с точки зрения самой Совы.Автор: Артём НаливайкоКомиксы "про Сову" от Александра Дьякова стали уже классикой ироничного осмысления косяков взаимоотношений с подчинёнными. О...
Маркетплейс – это сервис от 1С-Битрикс, который позволяет разработчикам делиться своими решениями с широкой аудиторией, состоящей из клиентов и других разработчиков.
Статья о том, как упорядочить найм1. Информируем о вакансии2. Ведём до найма3. Автоматизируем скучное4. Оформляем и выводим на работу5. Отчитываемся по итогам6. Помогаем с адаптацией...
Эта публикация написана после неоднократных обращений как клиентов, так и (к горести моей) партнеров. Темы обращений были разные, но причиной в итоге оказывался один и тот же сценарий, реализу...
С версии 12.0 в Bitrix Framework доступно создание резервных копий в автоматическом режиме. Задание параметров автоматического резервного копирования производится в Административной части на странице ...