Общая тенденция развития технологий характеризуется рывками и спадами. Рассмотрим, например, массовое перемещение человеческих тел. Изначально применялись лошади и повозки, которые постепенно стали сложными, и эта технология превратилась в отдельную индустрию. Затем внезапно появились поезда. Про лошадей быстро забыли, и фокус сместился на новое направление. Пар стал объектом исследований и превратился в сложную науку. Параллельно развивались дизель и электричество. В определенный момент паровые двигатели ушли в прошлое, и все перешли на дизель и электричество. Аналогично сейчас происходит переход на электромашины, требующие значительно меньшего количества жидкостей.
Технологии эволюционируют и функционируют, а новые технологии их полностью заменяют. Считаю, что сейчас наступает эпоха, когда технологии фреймворков и Электрона могут быть вытеснены генеративными AI. Рассмотрим несколько примеров.
Как и с нашими паровозами и автомобилями, языки программирования развиваются, функционируют и устаревают. Во время моего обучения в университете, акцент делался на великих Java и C#. В нашей группе большинство были убежденными приверженцами C#.
После того, как мы закончили учёбу и некоторые из нас ушли работать на втором и третьем курсе, мы быстро поняли, что знание языка ещё ничего не доказывает, поскольку с языком нужно было знать инструменты разработки и различные фреймворки. Тогда, работая в МЭСИ, я начал использовать новомодную функцию Generics, чтобы написать свой первый DLAC: Data Logic Access Component. В 2005 году это было новомодным веянием. В отличие от всяких там безмозглых PHP-проектиков, которые повально подвергались SQL-инъекциям из-за запросов типа 'SELECT * from user where name like '.$input.' and enabled = true', мы, шарписты, делали всё по-другому. У нас были классы, которые оборачивали наши Entity в хорошие обёрточки, создавали правильные SELECT-запросы и позволяли работать с классами. При этом все запросы были параметризированы по умолчанию, и, поскольку мы писали приложение для студентов университета, нам нужно было убедиться, что нас не взломают. (МЭСИшники тех годов могут всплакнуть)
На своей первой работе я просто молился на эти DLACи и жить без них не мог. Со временем я узнал, что существуют другие подобные фреймворки. В 2009 году в мире появился NHibernate, переписанный на .NET от Hibernate, который существовал с 2001 года.
С тех пор я пристально следил за развитием .NET. Я прилежно учил новые технологии, такие как WCF, WPF и прочие радости версии 3.5. Я воодушевлённо ждал нового релиза 4.0 и устанавливал все обновления, внимательно читая release notes. Я ахнул, когда Scott Hanselman показал, что ASP.NET становится открытой платформой и все исходные коды выкладываются на GitHub. Я был в авангарде изучения .NET…
А потом мне показали Golang. И я понял, что я был машинистом старого доброго паравоза.
Паровоз медленно тянул бесконечные стек-трейсы в моих приложениях. Технологии, с которыми я работал, умирали, когда сами разработчики из Microsoft прекращали их поддержку. WPF не стал новым кроссплатформенным стандартом для создания UI; его заменил всеобъемлющий Electron.
Не поймите меня неправильно. Изначально я сопротивлялся. Отсутствие классических модификаторов доступа в Go казалось мне ересью. Оказалось, что многие фичи, вроде модификаторов доступа, лишь усложняют разработку без явной выгоды. Мне не нужно было защищать поля класса как private protected, чтобы предотвратить попытки какого-то нерадивого программиста подключиться к полям этого класса и случайно их изменить.
Оказалось, что приложения живут просто отлично без Dependency Injection. Оказалось, что то, от чего мы воротили нос в 2009 году, не было нисколько сложным. Просто написать большой main.go и связать в нём все объекты, которые нужно - это не так и плохо. По крайней мере, когда что-то сломается, вы всегда сможете открыть main.go и выбрать Go to definition. В конце концов, следуя логической цепочке кода, вы найдёте ошибку. А вот если ваша программа написана в лучших традициях .NET, то после 20 минут копания в конфиг-файлах, в лучшем случае вы обнаружите себя перед какой-то фабрикой классов, которая выдаст вам компонент на основе магического заклинания по убийству дракона. Более того, с той скоростью, с которой компилировался Go, вам вообще не нужно было париться о бесконечных конфигах. Можно было просто захардкодить определенные вещи в main.go и менять их, вместе с программой.
Я стал машинистом паравоза. Но не надолго. Я сел и выучил Go. А после - Rust. Я выучил Node.js и множество других языков. Из всего этого я усвоил один урок.
Никакие технологии в компьютерной области не являются основополагающими или неизменными. Так что не нужно пытаться защищать какую-то технологию, или говорить, что вы будете писать на ней всю жизнь. Надо просто быть программистом и уметь разбираться с этими технологиями.
И вот, примерно год назад, мы все пережили очередной переворот. Мир захватил ChatGPT. И если в первые месяцы эта штука плоховато писала код, то к версии 4 она более-менее научилась простым вещам.
И теперь, мы готовы к тому, чтобы начинать избавляться от бесполезного ПО и переписывать всю эту муть.
Например, начнём с Postman.
По моему мнению, это отличный пример ужаса, в который что-то может превратиться. Для всех тех инженеров, которые не поняли, как использовать fetch для тестирования своих приложений, был написан Postman. А потом он превратился в бесконечно сложный комбаин, в котором сам черт ногу сломит. Единственное, что может греть в нём сердце, — так это тепло от процессора моего ноутбука, который пытается запустить этот бинарник, занимающий места больше, чем вся папка с торрентами.
Postman стоит заменить на curl.
"Что?" скажете вы. "Как можно заменить такой сложный инструмент на Curl?" Да очень просто, с помощью ChatGPT.
Давайте вежливо попросим бот, написать нам код для тестирования нашего нового сервера.
Certainly, Boss. Below is a Makefile
designed to test the specified endpoints with POST requests. It employs curl
for HTTP calls and incorporates Basic Authentication.
BASE_URL := {baseUrl}/v1
USERNAME := Bill
PASSWORD := Password
test-endpoint1:
curl -X POST -u $(USERNAME):$(PASSWORD) -H "Content-Type: application/json" -d '{"test": true}' $(BASE_URL)/endpoint1
test-endpoint2:
curl -X POST -u $(USERNAME):$(PASSWORD) -H "Content-Type: application/json" -d '{"test": true}' $(BASE_URL)/endpoint2
test-endpoint3:
curl -X POST -u $(USERNAME):$(PASSWORD) -H "Content-Type: application/json" -d '{"test": true}' $(BASE_URL)/endpoint3
test-all: test-endpoint1 test-endpoint2 test-endpoint3
Run individual tests with make test-endpoint1
, make test-endpoint2
, or make test-endpoint3
. To execute all, use make test-all
.
Отлично. Теперь вместо целого Postman у нас есть один файл, который можно положить в source control и который смогут запустить все ваши разработчики. Более того, его легко подключить в систему тестирования или CI/CD. В дополнение ко всему этому,
➜ ~ ls /usr/bin/make /usr/bin/curl -lah
-rwxr-xr-x 1 root root 271K Jul 17 14:53 /usr/bin/curl
-rwxr-xr-x 1 root root 250K Feb 15 2022 /usr/bin/make
Размер curl и make, вместе взятых, составляет 500 килобайт, против 500 мегабайт у Postman.
Вы только что избавились от ещё одного Electron-based приложения, которое будет клянчить у вас деньги при каждой возможности. Более того, вам вообще ничего не надо будет устанавливать.
В какой-то момент требования пользователей к удобству ПО полностью убили принципы Макилроя.
Дуг Макилрой — изобретатель каналов Unix и один из основателей традиции Unix — обобщил философию следующим образом:
-
Пишите программы, которые делают что-то одно и делают это хорошо
-
Пишите программы, которые бы работали вместе
-
Пишите программы, которые бы поддерживали текстовые потоки, поскольку это универсальный интерфейс.
Обычно эти высказывания сводятся к одному: «Делайте что-то одно, но делайте это хорошо». Из этих трёх принципов только третий является специфичным для Unix, хотя разработчики Unix чаще других акцентируют внимание на всех трёх принципах.
К сожалению, все эти программы начали разрастаться, постоянно меняться и обновляться без разбору и без толку. В итоге, сейчас я постоянно впадаю в ступор, пытаясь проверить открытые порты в Linux. nstat? ss? Или что-то ещё. Я теряюсь во всей этой пестроте ПО. Поэтому у нас появляются Postmanы. Чтобы было красиво и удобно.
Становится черезчур сложно. А вот ChatGPT как раз и является тем инструментом, который делает просто из сложного.
Да, год назад я бы не рискнул писать подобное на makefile. Но сейчас это легко сделать, ибо мне не надо возиться с расстановкой переносов строк и табуляциями. Более того, одно предложение и вуаля.
Absolutely, Boss. Below are the test scripts for Bash, PowerShell, and Taskfile.
Bash Script
#!/bin/bash
BASE_URL="{baseUrl}/v1"
USERNAME="Bill"
PASSWORD="Password"
test_endpoint() {
curl -X POST -u $USERNAME:$PASSWORD -H "Content-Type: application/json" -d '{"test": true}' $BASE_URL/$1
}
test_endpoint "endpoint1"
test_endpoint "endpoint2"
test_endpoint "endpoint3"
PowerShell Script
$baseUrl = "{baseUrl}/v1"
$username = "Bill"
$password = "Password"
Function Test-Endpoint ($endpoint) {
Invoke-RestMethod -Uri "$baseUrl/$endpoint" -Method POST -Headers @{ "Content-Type" = "application/json" } -Body '{"test": true}' -Credential (New-Object System.Management.Automation.PSCredential ($username, (ConvertTo-SecureString $password -AsPlainText -Force)))
}
Test-Endpoint "endpoint1"
Test-Endpoint "endpoint2"
Test-Endpoint "endpoint3"
Taskfile
Create a file named Taskfile.yml
and add the following content:
version: '3'
tasks:
test-endpoint1:
cmds:
- 'curl -X POST -u Bill:Password -H "Content-Type: application/json" -d ''{"test": true}'' {baseUrl}/v1/endpoint1'
test-endpoint2:
cmds:
- 'curl -X POST -u Bill:Password -H "Content-Type: application/json" -d ''{"test": true}'' {baseUrl}/v1/endpoint2'
test-endpoint3:
cmds:
- 'curl -X POST -u Bill:Password -H "Content-Type: application/json" -d ''{"test": true}'' {baseUrl}/v1/endpoint3'
test-all:
deps: [test-endpoint1, test-endpoint2, test-endpoint3]
Run individual tasks with task test-endpoint1
, task test-endpoint2
, or task test-endpoint3
. To execute all, use task test-all
.
Вот вам Postman на Taskfile, PowerShell и Bash.
Позволяет ли этот файл сделать ВСЁ, что делает Postman? Нет. Даже не близко. Зато позволяет сделать примерно 97% всех его функций за пару секунд без установки дополнительного ПО.
Так, давайте посмотрим, с чем ещё можно распрощаться.
Давайте возьмём область, которая является самой бесконечно раздутой в программировании: веб-разработку. Кто только не орёт благим матом и не плачет о том, что веб превратился в полный ужас, состоящий из миллионов фреймворков и бесполезностей.
И так, я сейчас сижу перед приложением, написанным на Nest.js. Nest — это замечательный пример бесполезной сложности, которую написали непонятно зачем. Документация у них «кот наплакал» (весь сайт забит плачущими неко), и большая часть этого специально написана для того, чтобы вы купили коммерческую лицензию с поддержкой. Фреймворк был рассчитан на создание сложностей там, где их не должно быть, а потом — на получение денег за распутывание этих сложностей. В начале этого года мне в руки попадает проект с просьбой починить одну функцию и задеплоить всё на AWS.
Я потратил два дня на поиск функции, которую мне нужно было поправить. Всё загорожено декораторами, сторами и прочей белибердой, взятой из React, так что концов там не сыщешь. У тебя нет основного файла, в который нужно идти, чтобы понять, что там не так. У тебя есть только ужас.
Мы-то знаем, что в сдаче проекта главное — это быть в состоянии поменять размер кнопки на два пикселя и перекрасить всё, что можно. В Nest решили, что CSS — это недостойно для нормального человека, и нужно будет заменить всё темами. Всё отлично, но вот на сдаче хотят поменять цвет кнопки. Вы думаете, это просто сделать на Nest?
Ладно, давай, ChatGPT, делай свою работу.
Certainly, Boss. Rendering TSX without React can be achieved using a custom JSX factory function. Below is a minimal TypeScript script that provides a way to render simple TSX files.
Create a tsx-renderer.ts
file:
const createElement = (tag: string, props: any, ...children: any[]) => {
let element = document.createElement(tag);
for (let key in props) {
element.setAttribute(key, props[key]);
}
children.forEach((child) => {
if (typeof child === "string") {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
return element;
};
// Define JSX factory in global scope
(globalThis as any).React = { createElement };
// Sample TSX content
const App = () => {
return (
<div id="app">
<h1>Hello, world!</h1>
</div>
);
};
// Render to document body
document.body.appendChild(App());
Configure tsconfig.json
:
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"jsx": "react",
"jsxFactory": "React.createElement"
},
"include": ["*.ts", "*.tsx"]
}
This script allows you to render TSX by utilizing a custom JSX factory function. Run the TypeScript compiler (tsc
) to transpile this to JavaScript, and execute it in a browser context.
Что? В смысле, на самом деле я могу рендерить TSX без React, просто пользуясь этим скриптом? В смысле, мне не нужно будет устанавливать CRA с 1500 зависимостями? Как?
Вы можете резонно заявить, что этот скрипт не сможет заменить весь React (на котором работает Nest). Конечно, нет! Но он заменит 80% всего того, что мне нужно от React. Остальное я допишу руками.
Но это же слишком много Boilerplate! Да, но писать-то буду не я.
Но ChatGPT иногда выдаёт ошибки, и даже в примерах, которые вы привели, есть ошибки! Вы абсолютно правы. Заодно я как раз поправлю эти ошибки кучками и изучу, как всё это работает. Буду знать, как всё устроено внутри.
Так, давайте выведем на расстрел нашего следующего заключённого. Bootstrap.
Велю видеть файл CSS с CSS-reset и с правилами для тёмной и светлой темы, а также с контейнером-сеткой.
Certainly, Boss. Here's the optimized CSS file incorporating dynamic theme switching based on user preferences.
/* CSS Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Base Variables */
:root {
/* Light Theme */
--light-bg-lighter: #f5f5f5;
--light-bg: #ffffff;
--light-bg-darker: #e0e0e0;
--light-accent-orange: #ff9800;
--light-accent-purple: #673ab7;
--light-gradient: linear-gradient(to right, #ff9800, #ff4081, #b71c1c, #673ab7);
/* Dark Theme */
--dark-bg-lighter: #424242;
--dark-bg: #303030;
--dark-bg-darker: #212121;
--dark-accent-orange: #ff9800;
--dark-accent-purple: #673ab7;
--dark-gradient: linear-gradient(to right, #ff9800, #ff4081, #b71c1c, #673ab7);
/* Default to Light Theme */
--bg-lighter: var(--light-bg-lighter);
--bg: var(--light-bg);
--bg-darker: var(--light-bg-darker);
--accent-orange: var(--light-accent-orange);
--accent-purple: var(--light-accent-purple);
--gradient: var(--light-gradient);
}
/* Automatic Theme Switch */
@media (prefers-color-scheme: dark) {
:root {
--bg-lighter: var(--dark-bg-lighter);
--bg: var(--dark-bg);
--bg-darker: var(--dark-bg-darker);
--accent-orange: var(--dark-accent-orange);
--accent-purple: var(--dark-accent-purple);
--gradient: var(--dark-gradient);
}
}
/* Mobile-First Base Styles */
body {
font-size: 16px;
background-color: var(--bg);
font-family: 'Arial', sans-serif;
line-height: 1.6;
}
/* Grid System */
.container {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.container {
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 1024px) {
.container {
grid-template-columns: 1fr 1fr 1fr;
}
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
margin-bottom: 16px;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.3em;
}
h4 {
font-size: 1.2em;
}
h5 {
font-size: 1.1em;
}
h6 {
font-size: 1em;
}
p {
margin-bottom: 16px;
color: var(--bg-darker);
}
This version utilizes CSS variables and media queries for automatic theme switching based on user preferences, in addition to the typography and header stipulations.
И это всё? У меня будет шикарная сетка для Grid, которая позволит мне в этом гриде верстать? Погодите, а почему так просто? Потому что, когда выпускался первый Bootstrap, мы видели вторую войну браузеров, и IE6 местами ещё жил. Но сейчас нам с этим не надо связываться. Мы можем запихнуть всё в grid, и у нас есть сетка для верстки.
Опять же, тут надо будет пару вещей допилить руками, но у вас в руках файл размером в килобайт, вместо безразмерного Bootstrap.
Но это же не всё! Вы правы. Темы для кнопок и всего остального допишет GPT. И у меня в руках появляется инструмент, который полностью находится под моим контролем.
Но погодите, а почему бы не избавиться от этого TypeScript? Ведь это просто ядерный замес, который притворяется строго типизированным языком программирования. Но только попробуйте в строго типизированный объект распарсить JSON с неправильными типами полей. Вы будете реветь, как слон, пытаясь понять, почему 1 !== 1.
Certainly, Boss. Below is the Vugu code that will accomplish the described tasks. Please make sure you have the necessary setup for a Vugu project.
<template>
<div>
<div>
<input type="number" @input="CalcSum" v-model="Num1" />
+
<input type="number" @input="CalcSum" v-model="Num2" />
<div>{{ Sum }}</div>
</div>
<div>
First Name: <input type="text" v-model="FirstName" @input="GenerateMessage" />
Last Name: <input type="text" v-model="LastName" @input="GenerateMessage" />
Birthday: <input type="date" v-model="Birthday" @input="GenerateMessage" />
<div>{{ Message }}</div>
</div>
</div>
</template>
<script>
import "time"
type MyComponent struct {
Num1 float64
Num2 float64
Sum float64
FirstName string
LastName string
Birthday string
Message string
}
func (c *MyComponent) CalcSum(event vugu.DOMEvent) {
c.Sum = c.Num1 + c.Num2
}
func (c *MyComponent) GenerateMessage(event vugu.DOMEvent) {
birthdayTime, _ := time.Parse("2006-01-02", c.Birthday)
secondsOld := time.Now().Sub(birthdayTime).Seconds()
c.Message = fmt.Sprintf("Hello %s %s! You are %.0f seconds old!", c.FirstName, c.LastName, secondsOld)
}
</script>
Make sure to adapt this code according to your specific Vugu project configuration and setup. If you need further assistance, please let me know.
Встречайте. Код на vugu. Достаточно тихий golang фреймворк, который позволяет вам писать webassembly код прямо на golang.
Но что это мы, мы же не обязательно пишем только на golang. Почему-бы не переделать это на Rust:
Certainly, Boss. Below is an example using Yew, a popular Rust framework for building web applications with WebAssembly.
use yew::{html, Component, ComponentLink, Html, ShouldRender};
use yew::services::ConsoleService;
use chrono::prelude::*;
pub struct App {
num1: f64,
num2: f64,
sum: f64,
first_name: String,
last_name: String,
birthday: NaiveDate,
message: String,
}
pub enum Msg {
Num1Changed(String),
Num2Changed(String),
FirstNameChanged(String),
LastNameChanged(String),
BirthdayChanged(String),
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
App {
num1: 0.0,
num2: 0.0,
sum: 0.0,
first_name: String::new(),
last_name: String::new(),
birthday: NaiveDate::from_ymd(2000, 1, 1),
message: String::new(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Num1Changed(val) => {
self.num1 = val.parse().unwrap_or(0.0);
self.sum = self.num1 + self.num2;
}
Msg::Num2Changed(val) => {
self.num2 = val.parse().unwrap_or(0.0);
self.sum = self.num1 + self.num2;
}
Msg::FirstNameChanged(val) => {
self.first_name = val;
}
Msg::LastNameChanged(val) => {
self.last_name = val;
}
Msg::BirthdayChanged(val) => {
self.birthday = NaiveDate::parse_from_str(&val, "%Y-%m-%d").unwrap();
}
}
let now = Local::now().naive_utc().date();
let duration = now.signed_duration_since(self.birthday);
let seconds = duration.num_seconds();
self.message = format!(
"Hello {}, {}! You are {} seconds old!",
self.first_name, self.last_name, seconds
);
true
}
fn view(&self) -> Html {
html! {
<div>
<div>
<input type="number" value=self.num1.to_string() oninput=self.link.callback(|e: InputData| Msg::Num1Changed(e.value)) />
{" + "}
<input type="number" value=self.num2.to_string() oninput=self.link.callback(|e: InputData| Msg::Num2Changed(e.value)) />
<div>{ self.sum }</div>
</div>
<div>
<span>{"First Name: "}</span>
<input type="text" value=&self.first_name oninput=self.link.callback(|e: InputData| Msg::FirstNameChanged(e.value)) />
<span>{"Last Name: "}</span>
<input type="text" value=&self.last_name oninput=self.link.callback(|e: InputData| Msg::LastNameChanged(e.value)) />
<span>{"Birthday: "}</span>
<input type="date" value=self.birthday.format("%Y-%m-%d").to_string() oninput=self.link.callback(|e: InputData| Msg::BirthdayChanged(e.value)) />
<div>{ &self.message }</div>
</div>
</div>
}
}
}
Make sure to add dependencies for yew
and chrono
in your Cargo.toml
file. This code should deliver on the requested functionality with precision and efficiency. Should you need further details, I stand ready to assist.
Запросто!
Ошибки? Да! Не компилируется? Конечно! В конце концов, это написано не прилежным программистом, а машиной, у которой нет ни компилятора, ни чувства совести за такой код. Но мы-то можем поправить это ручками и разобраться?
Прошло время, когда мы укрывались от boilerplate за стеной бесконечных фреймворков. Нам больше не нужны стактрейсы, которые уходят вникуда. Мы не должны ставить ещё одну надстройку на свой код, для того, чтобы избежать повторения чего-то.
Вы можете просто заставить GPT сгенерировать вам нужный код. Если в нём что-то не так, вы сможете это поправить. А когда придёт время это дописывать, вы сможете со спокойной душой это дописать. Сами. Без бесконечных фреймворков и зависимостей от чёрт знает чего. Весь ваш код будет лежать в репозитории. И если вдруг вам понадобится что-то в этом изменить, то по крайней мере, вы сможете это сделать без попыток разобраться в документации к чему-либо.
С тех пор как появился ChatGPT в моих репозиториях стало намного больше bash скриптов. И поначалу все мои коллеги дивились тому, что я не очень-то хотел устанавливать ещё один новомодный фреймворк. Но они недавно изменили своё мнение.
Мне надо было написать сервер, взамен старому на Next.js. Мы переезжали из Vercel на AWS. Старый сервер безостановочно падал. Я взял и написал новый сервер на чистом TS. На данный момент мой pnpm ls выглядит следующим образом.
-
@prisma/client 4.8.1
-
body-parser 1.20.1
-
cors 2.8.5
-
express-basic-auth 1.2.1
-
jsonwebtoken 9.0.0
-
otplib 12.0.1
-
prisma-json-types-generator 2.2.0
-
uuid 9.0.0
-
bcrypt 5.1.0
-
cookie-parser 1.4.6
-
dotenv 16.0.3
-
express 4.18.2
-
google-auth-library 8.7.0
-
oauth-1.0a 2.2.6
-
typescript 4.9.4
-
zxcvbn 4.4.2
И сейчас я осознаю грубейшую ошибку, которую я допустил при написании этого проекта. Я использовал Express там, где можно было бы обойтись простым httplisten. Сейчас я его выкорчёвываю из проекта. Единственное что я не буду переписывать - так это все крипто модули. Ибо мы все знаем, чем это чревато.
Но прикол был в том, что, когда я передавал этот проект другому программисту, он поначалу поворотил носом. А потом спросил: «Это значит, что я могу открыть main.ts и просто проследить все вызовы оттуда?» Я сказал, что так оно и есть. Он просиял. Это был как раз тот самый код, который является самодокументирующимся.
Безумие - это любая вещь доведенная до крайности. Вы можете пойти и съесть мороженое. А можете пойти и попытаться ограбить кондитерскую фабрику, с целью съесть всё мороженое в мире. Первое действие не будет безумным. Второе - более чем.
Я не призываю здесь возвращаться к копьям и писать веб-приложения на чистом Assembly. (Хотя, те кто меня знают тут осекутся). Но нам можно оставлять в прошлом бесконечно большие node-modules, потому что вам больше не придётся скачивать left-pad для своих проектов. GPT сможет переписать его для вас.
Автор: Ivan Roganov