Atualizando o Discourse para Ember 4

We want to update the Ember version used in Discourse.

Right now, we’re on 3.15, and we’d like to get to 4.1

We’ve set a goal to finish this work by the end of this year. Please note that this topic is a roadmap, and all the plans and estimates are tentative. This topic is not intended to house any significant discussions about particular upgrades or changes. So, let’s keep the conversation here a bit focused. Questions about how these updates will affect your site/plugin/theme are off-topic. The planned changes are 100% on the front-end (Ember app).

We’re going to be super careful about the changes we introduce. All the official plugins/themes will get updated (including any custom work CDCK has done for its customers). We’ll also send PRs to update all the popular unofficial plugins/themes.

No worries if you have a custom plugin/theme that you built for your site. We will add deprecation warnings and create the necessary announcements in a timely manner to give you enough time to make any changes required. We will also try to guide you through those changes if needed.

Let’s start with the goals. We obviously want to get to the highest version available, but we need to set intermediate goals between now and our final destination.

We plan to do incremental updates. Instead of one major update to 4.1, we’re going to break this work down into six stages. Each stage focuses on one Ember update.

Stage Size Updates
1 size-l Ember 3.15 → Ember 3.16
2 size-m Ember 3.16 → Ember 3.25
3 size-xl Ember 3.25 → Ember 3.26 (Part 1 - General)
Ember 3.25 → Ember 3.26 (Part 2 - Templates)
Ember 3.25 → Ember 3.26 (Part 3 - jQuery)
Ember 3.25 → Ember 3.26 (Part 4 - Octane prep work)
Ember 3.25 → Ember 3.26 (Part 5 - Octane)
4 size-l Ember 3.26 → Ember 3.27 (Part 1 - General)
Ember 3.26 → Ember 3.27 (Part 2 - RenderTemplate)
Ember 3.26 → Ember 3.27 (Part 3 - Legacy built-in components)
5 size-s Ember 3.27 → Ember 3.28
6 size-s Ember 3.28 → Ember 4.1

Choosing those specific versions is entirely based on the deprecations and amount of work they introduce - and the implied risk.

So, let’s break those down further for more clarity.

Deprecations per update

Stage 1 (size-l)

Ember 3.15 → Ember 3.16

This update only introduces one deprecation.

  1. Use ember-CLI resolver rather than legacy globals resolver :link: - until: 4.0.0 - size-l

    Discourse has its own custom resolver that currently extends Ember.DefaultResolver.

    The recommended fix is to do away with any custom resolvers and use the Ember-CLI resolver. This is easier said than done because we have quite a bit of custom logic to handle plugins and themes.

    A compromise that works is to extend the Ember-CLI resolver instead of extending Ember.DefaultResolver.

Stage 2 (size-m)

Ember 3.16 → Ember 3.25

This update introduces eight deprecations.

  1. @ember/string#loc and {{loc}} :link: until: 4.0.0 - :heavy_check_mark:

  2. Without for - Ember’s built-in deprecate :link: until: 4.0.0 - :heavy_check_mark:

  3. Without since - Ember’s built-in deprecate :link: until: 4.0.0 - :heavy_check_mark:

  4. tryInvoke from @ember/utils :link: until: 4.0.0 - :heavy_check_mark:

  5. Meta Destruction APIs :link: - until: 3.25.0 - :heavy_check_mark:

    We don’t use any of those, so there is nothing to do here.

  1. Use Ember getter and explicitly check for undefined :link: - until: 4.0.0 - size-s

    This is a simple deprecation. We only need to remove getWithDefault in our codebase, and there are only a few places where we use it.

  2. String prototype extensions :link: - until: 4.0.0 - size-m

    This is also a simple, low-risk swap, but we use Ember’s extended string prototype in quite a few places. From a quick check, I think we have about < 100 places where we do that between core and plugins/themes. Once we update those, we should also prevent Ember from extending that prototype with something like this.

    EXTEND_PROTOTYPES: {
       String: false
    }
    

    in our environment file.

  3. Importing htmlSafe and isHTMLSafe from @ember/string :link: - until: 4.0.0 - size-m

    There’s much overlap between this and #7 in this update, and it should be straightforward and low-risk.

Stage 3 (size-xl)

The upgrade from 3.25 to 3.26 is quite involved. This is where we do most of the “catching up.” There are sixteen deprecations in total. We’re going to break up this update into five parts - where we only do a version bump after all the deprecations have been handled.

Ember 3.25 → Ember 3.26 (Part 1 - General)

This part deals with nine deprecations that are relatively easier to handle.

  1. Array Observers :link: - until: 4.0.0 - :heavy_check_mark:

  2. Component Manager Capabilities :link: - until: 4.0.0 - :heavy_check_mark:

  3. Modifier Manager Capabilities :link: - until: 4.0.0 - :heavy_check_mark:

  4. Optional Feature: application-template-wrapper :link: - until: 4.0.0 - :heavy_check_mark:

  5. classBinding and classNameBindings as args in templates :link: - until: 4.0.0 - :heavy_check_mark:

    Nothing to do here; we don’t use those.

  1. Transition methods of routes and controllers :link: - until: 5.0.0 - size-m

    This deprecation removes a couple of methods from routes and controllers. It may look like it’s an involved change, but it should be straightforward. We’d only need to inject the Router as a service and call them from there instead - when required. The tricky part is that we use those in many places, and they all need to be updated.

  2. Browser Support Policy :link: - until: 4.0.0 - size-s

    ~~This should be a relatively simple change. Ember won’t support IE11 from 4.0 onwards. The nice thing here is that we haven’t supported it for a long time anyway. The only thing we need to change is to stop transpiling for IE11 in production.
    discourse/app/assets/javascripts/discourse/config/targets.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

    I did some basic testing, and this change will save us about 60kb (gzip) or ~6% of our main and vendor bundles in production Ember-CLI installs.

  3. {{hasBlock}} and {{hasBlockParams}} :link: - until: 4.0.0 - size-s

    We use these in a couple of places. This is a simple, low-risk rename.

  4. {{with}} helper :link: - until: 4.0.0 - size-s

    We rarely use this, but it still needs fixing. We just need to swap them out and use {{let}} or a combination of {{if}} / {{else}}

Ember 3.25 → Ember 3.26 (Part 2 - Templates)

This part will primarily focus on deprecations involving .hbs templates. There are three deprecations that we need to focus on here.

  1. Property Fallback Lookup :link: - until: 4.0.0 - size-l

    Starting from Ember 4.0, this won’t work anymore.

    Hello, {{name}}!
    

    If we have a property in a template, we have to look it up with a preceding this like so

    Hello, {{this.name}}!
    

    We’d have to do that with all of our templates. There are ways to reduce the pain here. We can try the ember-no-implicit-this-codemod and see how far it gets us.

    I’m in favor of limiting changes to 1 file per PR. This makes it easy to review - and revert if something goes wrong.

  2. Accessing named args via {{attrs}} :link: - until: 4.0.0 size-xl

    The {{attrs}} object is going to be dropped in Ember 4.0. The change itself is very straightforward, and Ember’s example is quite nice.

    Before:

    {{attrs.foo}}
    {{this.attrs.foo.bar}}
    {{deeply (nested attrs.foobar.baz)}}
    

    After:

    {{@foo}}
    {{@foo.bar}}
    {{deeply (nested @foobar.baz)}}
    

    We’d have to do this for all of our templates. We can combine this change with converting the templates to the angle-bracket syntax. I’m a huge fan of angle brackets because they’re much closer to the standard custom web-components syntax.

    One thing that might expedite our progress here is the ember-angle-brackets-codemod. We’ll have to experiment with it and see how far it takes us. It handles the deprecation and gives us shiny angle-bracket syntax.

    Similar to #1 in this part, I also favor fixing and testing one template per PR.

  3. <LinkTo> positional arguments :link: - until: 4.0.0 - size-m

    This is also a deprecation that aims to reduce confusion. There are a few places where we use positional arguments on link-to. We can fix those like so

    Before:

    {{link-to "About Us" "about"}}
    {{#link-to "about"}}About Us{{/link-to}}
    {{#link-to "post" @post}}Read {{@post.title}}...{{/link-to}}
    

    After (with angle brackets):

     <LinkTo @route="about">About Us</LinkTo>
     <LinkTo @route="about">About Us</LinkTo>
     <LinkTo @route="post" @model={{@post}}>Read {{@post.title}}...</LinkTo>
    

Ember 3.25 → Ember 3.26 (Part 3 - jQuery)

There’s not much I can say about this that you don’t already know. New Ember apps don’t use jQuery, and it will be dropped in Ember 4.0.

In this part, we’ll focus on one deprecation.

  1. Optional Feature: jquery-integration :link: - until: 4.0.0 - size-xl

    Over the last couple of years, we’ve made a ton of progress in reducing our jQuery usage. There are still places where we need it, particularly in the composer and as a dependency for some vendor libraries we use. I don’t want to get into the details of this change here. Long story short, we should move away from using jQuery.

    However, I would like to highlight that even if we get rid of jQuery EVERYWHERE, we should still keep this option set to true until we’re ready for Ember 4.0. We need a plan to ease the transition for sites with custom themes/plugins that we do not control. In other words, let’s do the work but live with the deprecation warning about turning the option off.

Ember 3.25 → Ember 3.26 (Part 4 - Octane prep work)

In this part, we should focus on getting our files ready for Octane. We will handle two deprecations.

  1. Optional Feature: template-only-glimmer-components :link: - until: 4.0.0 - size-m

    This is a simple change in theory, but there’s implied work we need to do before we can toggle that option.

    We need to ensure that our current template-only components work with glimmer semantics. I did some testing, and our tests choked with that option turned on. Here’s an example list of some template-only components we have in core

    - app/templates/components/activation-email-form.hbs
    - app/templates/components/cancel-link.hbs
    - app/templates/components/categories-with-featured-topics.hbs
    - app/templates/components/category-name-fields.hbs
    - app/templates/components/color-input.hbs
    - app/templates/components/custom-html-container.hbs
    - app/templates/components/emoji-group-buttons.hbs
    - app/templates/components/emoji-group-sections.hbs
    - app/templates/components/empty-state.hbs
    - app/templates/components/ip-lookup.hbs
    - app/templates/components/modal-footer-close.hbs
    - app/templates/components/popup-menu.hbs
    - app/templates/components/reviewable-created-by-name.hbs
    - app/templates/components/reviewable-created-by.hbs
    - app/templates/components/reviewable-field-editor.hbs
    - app/templates/components/reviewable-field-text.hbs
    - app/templates/components/reviewable-field-textarea.hbs
    - app/templates/components/reviewable-field.hbs
    - app/templates/components/reviewable-flagged-post.hbs
    - app/templates/components/reviewable-post-header.hbs
    - app/templates/components/reviewable-post.hbs
    - app/templates/components/reviewable-scores.hbs
    - app/templates/components/reviewable-tags.hbs
    - app/templates/components/reviewable-topic-link.hbs
    - app/templates/components/score-value.hbs
    - app/templates/components/selected-posts.hbs
    - app/templates/components/subcategories-with-featured-topics.hbs
    - app/templates/components/text-overflow.hbs
    - app/templates/components/user-fields/confirm.hbs
    - app/templates/components/user-fields/dropdown.hbs
    - app/templates/components/user-fields/multiselect.hbs
    - app/templates/components/user-fields/text.hbs
    - app/templates/components/user-profile-avatar.hbs
    - app/templates/components/user-summary-users-list.hbs
    

    We’ll also need to check our Admin/theme/plugin templates and ensure everything works before shipping that option.

    Off the top of my head, I’m not exactly sure how our raw .hbr templates will fit into this. However, @david has been working on glimmer-based topic lists. So, maybe we can do away with raw templates altogether.

  2. Implicit Injections :link: - until: 4.0.0 size-xl

    We use implicit injections everywhere. We do this in an initializer like so
    discourse/app/assets/javascripts/discourse/app/pre-initializers/inject-discourse-objects.js at ac79c5efc61d259705eeb487ca21d0ec3c535807 · discourse/discourse · GitHub

    Ember is moving away from implicit injections. The preferred path forward is to convert as many of our objects as possible to services and explicitly inject them where they’re needed. Of course, there might be a few things where a service is not ideal. In those cases, we can lookup those objects directly when they’re needed like so

    getOwner(this).lookup('thing:main')
    

    Another option we have (depending on performance implications) is to wrap the Ember Classes with our own Discourse Class. We’d then use our Class throughout the app. Kind of like we do with the GlimmerComponent Class
    discourse/app/assets/javascripts/discourse/app/components/glimmer.js at fa0c796baf9a7f64a3b27823b1aa4b370a74c3eb · discourse/discourse · GitHub. This would make this task a size-l or even size-m

    In any case, this change will need some thinking.

Ember 3.25 → Ember 3.26 (Part 5 - Octane)

This is the final stretch of the 3.25 → Ember 3.26 upgrade. We will only have one deprecation left at this point, but it’s a big one.

  1. Edition: Classic :link: - until: 4.0.0 - size-xl

    Before can flick our version to Octane, I’d like to spend time converting our Classes to native Classes and our components to Glimmer components. There will be a bit of pain involved, but it’s worth it. The ember-native-class-codemod should alleviate some of that pain. We’ll have to see how far it takes us.

    There are a lot of considerations and workflow issues to keep in mind. The only thing I want to highlight is that it will follow the same flow I mentioned for templates - fix and test 1 component per PR.

Stage 4

The Ember 3.26 → Ember 3.27 update introduces twelve deprecations. I propose we split them into three parts. We will do the version bump after all the deprecations have been handled.

Ember 3.26 → Ember 3.27 (part 1 - General)

  1. Reopening Classic Component Super Class :link: - until: 4.0.0 - :heavy_check_mark:

  2. Class-based template compilation plugins :link: - until: 4.0.0 - :heavy_check_mark:

  3. LinkTo @disabled-when argument :link: - until: 4.0.0 - :heavy_check_mark:

    I don’t think we use any of these, so there is nothing to do here.

  1. Deprecate Route#disconnectOutlet :link: - until: 4.0.0 - size-s

    We only do this in one place. It’s the build-category-route, and it should be straightforward to fix.

  2. Invoking Helpers Without Arguments and Parentheses In Named Argument Positions :link: - until: 4.0.0 - size-s

    I don’t think we do this anywhere, but I will confirm when the time comes. Essentially, calling a helper without passing it any arguments.

    Even if we use something like that, we’d only need to add parentheses. So, this

    <SomeComponent @arg={{someHelper}} />
    

    becomes

    <SomeComponent @arg={{(someHelper)}} />
    

    note the parentheses around someHelper

  3. Run loop and computed dot access :link: - until: 4.0.0 - size-m

    We use . to access computed functions in our decorators addon. They look like this, for example
    discourse/app/assets/javascripts/discourse-common/addon/utils/decorators.js at b05fddaa7ce3968ffc70cd8d4bf290e15d06eb11 · discourse/discourse · GitHub

    We also have a stray one-off here and there. As far as I can tell, fixing those is mostly about fixing how we import them.

    So, computed.filter should be imported like this instead.

    import { filter } from '@ember/object/computed';
    

    Our version of the external vendor buffered-proxy addon uses . to access computed functions; we’d need to bump it.

    We also use . to access run functions in a few places. That said, the same fix applies. We need to update the way we import them. I think themes and plugins especially might have quite a few places where we do that with run

  4. Deprecate the Ember Global :link: - until: 4.0.0 - (#size ?)

    Ember won’t be available in the global context after 4.0. It’s hard to estimate the amount of work/impact here without a deeper look. That said, I know @cvx has been doing tons of work to get rid of this pattern.

Ember 3.26 → Ember 3.27 (part 2 - renderTemplate)

This part will only focus on one deprecation.

  1. Deprecate Route#renderTemplate :link: - until: 4.0.0 - size-l

    In a nutshell, we can’t use named outlets in Ember 4.0. So, this won’t work.

    {{outlet "thing"}}
    

    We use renderTemplate in almost 30 places in core alone. The upgrade itself seems pretty straightforward. We can use {{#in-element}} and a plain empty HTML element as a placeholder for what we used to render in named outlets.

Ember 3.26 → Ember 3.27 (part 3 - Legacy built-in components)

This part will focus on built-in legacy components, and it will handle four deprecations.

  1. Importing Legacy Built-in Components :link: - until: 4.0.0
  2. Built-in Components Legacy Arguments :link: - until: 4.0.0
  3. Built-in Components Legacy HTML Attribute Arguments :link: - until: 4.0.0
  4. Reopening Legacy Built-in Components :link: - until: 4.0.0

I didn’t add sizes to these because… it really depends. Let me explain.

Legacy built-in components like Checkbox, TextField, TextArea, and LinkComponent will be removed in Ember 4.0. We use them in quite a few places, and we also use some deprecated patterns on them.

Ember offers an upgrade path that allows us to keep using them, but we have to import them differently. However, they will not receive any updates from Ember and will remain frozen. I’m hoping that we can do away with all of them; however, that might be a bit involved. This change will need more discussion when the time comes.

Stage 5

Ember 3.27 → Ember 3.28

This is a size-s since it’s only a version bump. 3.28 is the last LTS release in the 3.x development cycle. It doesn’t introduce any new deprecations after 3.27, and it’s a good version for us to stop on for a few weeks while things settle down.

3.28 LTS is supported through August 2022 (both bug fixes and security patches)

This “break” has a few benefits.

  1. It gives us more time to see if any issues arise
  2. when we ship a stable version, it should be on 3.28
  3. it gives us time to make any announcements we need to make regarding self-maintained themes and plugins
  4. it gives us time to ensure that the jQuery → no jQuery is as smooth as possible.

Stage 6

After a few weeks pass, we can finally bump our version to Ember 4

Ember 3.28 → Ember 4.1

We can now toggle the optional jQuery integration off as the last deprecation from the 3.x cycle.

This update introduces two minor deprecations.

  1. Deprecate Ember.assign :link: - until: 5.0.0 - size-s

    We don’t use that in core, but we’ll need to check themes/plugins. In any case, it’s a simple rename.

  2. AutoLocation Class :link: - until: 5.0.0 - size-s

    In theory, we’d only need to change locationType: 'auto' to locationType: 'history' in our Ember environment file, and it should just work.

Workflow

As I mentioned at the start, we will be very careful with these updates. We will test/fix/pin all the official plugins/themes with every update, and we’ll also send PRs to popular unofficial plugins/themes.

The goal here is not to slow down development or create headaches. So, the PRs should will be strictly-scoped, with one change per PR and nothing too large.

In an ideal world, all of the changes would happen in the background without interrupting anyone else’s work. This is why we plan to keep the PRs short and sweet. Also, we’re not too fond of mixed patterns. So, we don’t want to get stuck in an interim state on a per-file basis. A component is either classic or Glimmer, and a template either uses curly braces or angle brackets, nothing in between.

I hope this road map was clear. Like I mentioned in the beginning, this is just a general top-level overview. If anything is unclear, incorrect, or doesn’t sit right with you, please let us know.

28 curtidas

Core: Todos foram :white_check_mark:

Plugins:

discourse-events

discourse-data-explorer


7 curtidas

@Johani talvez não seja algo que bloqueie diretamente o Ember 4, mas os mixins podem valer a pena considerar no roadmap também:

Além disso, algumas novas classes de framework, como os componentes Glimmer, não suportam mixins do Ember. No futuro, os mixins serão removidos do framework e não serão substituídos diretamente.

6 curtidas

Temos uma ideia de quando as Listas de Tópicos migrarão para Glimmer Components e abandonarão os templates brutos?

5 curtidas

Esperando conseguir chegar a isso nos próximos 3 a 6 meses, mas isso não está definido.

No momento, o foco principal de nossa equipe de “modernização de JS” é colocar o Discourse no Ember 4.x+ (o 3.28 já atingiu o fim de vida útil - EOL).

6 curtidas

Olá @david,

Por curiosidade, qual seria sua recomendação em relação a temas? Estamos investigando a possibilidade de realizar uma grande reestruturação do tema do Discourse (simplificações, torná-lo mais parecido com “mídias sociais”, menos focado em desenvolvedores, usar posts com comentários em vez de threads).

Dada a quantidade de mudanças previstas no front-end do Discourse nos próximos 6 meses, devemos esperar antes de tentar fazer isso?

Abraços,
Simon

2 curtidas

Olá Simon, é difícil dar uma resposta definitiva aqui dada a incerteza sobre o cronograma.

Na CDCK, ainda estamos desenvolvendo novos temas para clientes contra a versão existente do core. Quaisquer grandes alterações (por exemplo, uma reescrita da lista de tópicos) serão opcionais inicialmente, então você terá tempo para adaptar as coisas.

Em geral, você terá um caminho de migração mais fácil se usar APIs “recomendadas” como plugin outlets e evitar substituir quaisquer templates.

5 curtidas

Obrigado @david, isso é útil.

Abraços,
Simon

1 curtida

Como vamos lidar com a modificação de modelos, dada a abordagem Glimmer do Octane e “dados para baixo, ações para cima”?

Temos um desafio com os plugin outlets, observo, onde anteriormente tínhamos o two-way binding, mas se anexarmos um Glimmer Component a um outlet, não teremos mais essa opção.

O two-way binding via plugin outlets é um padrão estabelecido, onde em alguns casos queremos atualizar o modelo passado via plugin outlet.

Notei esta recomendação na documentação do Ember:

Notavelmente:

“A segunda opção é que você pode executar o ember-native-class-codemod para todos os componentes restantes. Isso os transformará em componentes que importam de @ember/component, retendo todas as mesmas APIs que os componentes clássicos têm, mas apenas representados em uma sintaxe de Native Class.”

Quaisquer pensamentos aqui são muito bem-vindos.

2 curtidas

A alteração de ligação bidirecional refere-se à re-atribuição de argumentos, mas você ainda pode mutá-los.

Por exemplo, isso não é permitido em componentes Glimmer:

this.args.topic = blah

Mas este tipo de coisa:

this.args.topic.title = "blah"

ainda é possível.

Na verdade, não acho que a re-atribuição de argumentos seja atualmente possível em Plugin Outlets devido à forma como usamos um {{hash}} para passar os argumentos. Portanto, não espero nenhuma mudança nessa frente. :crossed_fingers:

Muitos temas/plugins oficiais já estão usando componentes Glimmer como conectores de plugin outlet, e a documentação atual no meta descreve como fazer isso.

Os componentes Glimmer proporcionam uma experiência de desenvolvedor aprimorada e melhor desempenho. Mas vale a pena notar que não há pressa imediata para converter de componentes clássicos para componentes Glimmer. Componentes clássicos ainda são suportados no Ember 5.

A coisa mais importante no momento é resolver quaisquer mensagens de depreciação em temas/plugins. Publicaremos mais sobre estratégias de atualização nas próximas semanas/meses, mas estamos progredindo bem em preparar o core para a atualização. Existe até um Ember 5.3 experimental branch do Discourse que estamos executando em uma instância interna nas últimas semanas com grande sucesso! :tada:

5 curtidas

Ah! Isso é muito interessante, obrigado!

2 curtidas

Compreensivelmente, há muito escopo na atualização e reconheço que é muito difícil dar um prazo para tudo isso, mas já houve algum avanço nas Listas de Tópicos?

2 curtidas

Sim, com certeza! O @cvx está trabalhando ativamente nisso, e já existe uma configuração no site “experimental glimmer topic list groups” se você quiser experimentar.

No entanto, ainda não começamos a explorar o aspecto de personalização disso, então, por favor, não tente criar nenhum tema/plugin com base nisso. Esperamos trabalhar nisso nas próximas semanas.

5 curtidas

Excelente progresso!

Sim, manter as opções de personalização o mais abertas possível seria muito apreciado.

Vemos muitas solicitações para layouts muito diferentes para o Item da Lista de Tópicos em relação ao que é regular.

4 curtidas

Notei os novos avisos de descontinuação, por exemplo:

“Use o transformador de valor topic-list-columns e outras novas APIs do plugin topic-list em vez disso.”

Haverá alguma comunicação sobre isso (talvez eu tenha perdido alguma? :thinking: )?

Sim, teremos a documentação disponível na próxima semana ou mais!

Ainda não são mensagens de descontinuação ‘adequadas’ - estamos registrando-as usando console.debug em vez de console.warn, então elas nem são visíveis na configuração padrão do Chrome DevTools. (cc @cvx)

2 curtidas

Vamos lá @merefield

1 curtida

OOh. Talvez eu queira saber sobre isso. Como você os vê? O console.debug não deixa os linters infelizes?

Eu acho que isso faz parte da minha resposta:

Sim!

O motivo pelo qual os tornamos debug foi para garantir que tudo estivesse pronto antes de abrir as comportamentais de avisos. É que o @merefield foi muito observador e os encontrou de qualquer maneira :wink:

Agora que o tópico foi publicado, atualizaremos para depreciações normais iminentemente :fire:

1 curtida

Mas talvez no meu próprio trabalho de desenvolvimento eu queira usar console.debug em vez de console.log. Como regra, as coisas que estou fazendo, só eu me importo.

1 curtida