Come verificare se uno script è in esecuzione in Node.js?


159

Ho uno script che sto richiedendo da uno script Node.js, che voglio mantenere indipendente il motore JavaScript.

Ad esempio, voglio fare exports.x = y;solo se funziona sotto Node.js. Come posso eseguire questo test?


Quando ho pubblicato questa domanda, non sapevo che la funzione dei moduli Node.js fosse basata su CommonJS .

Per l'esempio specifico che ho dato, una domanda più accurata sarebbe stata:

Come può uno script dire se è stato richiesto come modulo CommonJS?


3
Non ho idea del motivo per cui stai provando a farlo, ma come regola generale dovresti utilizzare il rilevamento delle funzionalità piuttosto che il rilevamento del motore. quirksmode.org/js/support.html
Quentin

4
Questa è in realtà una richiesta su come implementare il rilevamento delle funzionalità, ma la domanda descrive male se stessa.
monocromo

ha pubblicato una libreria per mio uso personale, aiuta questo aiuterà npmjs.com/package/detect-is-node
abhirathore2006


Un problema con la domanda e la maggior parte delle risposte è l'assunto che ci sono solo due possibilità: Browser o Node.js. È possibile che non sia né browser né Node.js, come nel caso di Oracle Java Nashorn. Se è installato JDK, il comando jjs consente di eseguire script. Ma ci sono molte differenze tra Nashorn e Node.js, quindi non puoi ipotizzare. E chissà quali opzioni potrebbe offrire il futuro? È necessario il rilevamento delle funzionalità.

Risposte:


80

Cercando il supporto CommonJS , ecco come funziona la libreria Underscore.js :

Modifica: alla domanda aggiornata:

(function () {

    // Establish the root object, `window` in the browser, or `global` on the server.
    var root = this; 

    // Create a reference to this
    var _ = new Object();

    var isNode = false;

    // Export the Underscore object for **CommonJS**, with backwards-compatibility
    // for the old `require()` API. If we're not in CommonJS, add `_` to the
    // global object.
    if (typeof module !== 'undefined' && module.exports) {
            module.exports = _;
            root._ = _;
            isNode = true;
    } else {
            root._ = _;
    }
})();

L'esempio qui mantiene il modello del modulo.


45
Questo rileva il supporto CommonJS, che i browser possono supportare.
mikemaccana,

7
C'è un problema qui e inchiodare "inchiodato". Sto provando CommonJS nel browser e il caricatore di moduli che sto usando definisce module.exports, quindi questa soluzione mi direbbe erroneamente che sono nel nodo.
Mark Melville,

1
@MarkMelville senza dubbio, questo è esattamente ciò che l'OP sta chiedendo, quindi non è un problema .
Ross

13
Cattive parole da parte mia. Voglio dire, c'è un problema con questa soluzione. L'OP potrebbe averlo accettato, ma io no.
Mark Melville,

7
Questa sicuramente NON è la migliore risposta data.
user3751385

107

Bene, non esiste un modo affidabile per rilevare l'esecuzione in Node.js poiché ogni sito Web potrebbe facilmente dichiarare le stesse variabili, poiché non esiste window oggetto in Node.js per impostazione predefinita, puoi fare il contrario e verificare se stai eseguendo all'interno di un Browser.

Questo è quello che uso per le librerie che dovrebbero funzionare sia in un browser che in Node.js:

if (typeof window === 'undefined') {
    exports.foo = {};

} else {
    window.foo = {};
}

Potrebbe comunque esplodere nel caso in cui windowsia definito in Node.js ma non c'è alcun buon motivo per farlo, dato che dovresti esplicitamente lasciare fuori varo impostare la proprietà sulglobal sull'oggetto.

MODIFICARE

Per rilevare se il tuo script è stato richiesto come modulo CommonJS, non è ancora facile. L'unica cosa che commonJS specifica è che A: I moduli saranno inclusi tramite una chiamata alla funzione requiree B: I moduli esportano le cose tramite proprietà exportssull'oggetto. Ora come viene implementato questo è lasciato al sistema sottostante. Node.js racchiude il contenuto del modulo in una funzione anonima:

function (exports, require, module, __filename, __dirname) { 

Vedi: https://github.com/ry/node/blob/master/src/node.js#L325

Ma non provare a rilevarlo tramite alcune arguments.callee.toString()cose folli , invece usa semplicemente il mio codice di esempio sopra il quale controlla il browser. Node.js è un ambiente molto più pulito, quindi è improbabile che windowvenga dichiarato lì.


2
Informazioni su "Node.js è un ambiente molto più pulito, quindi è improbabile che la finestra venga dichiarata lì.": Bene, sono appena venuto qui alla ricerca di un modo per scoprire se il mio script era in esecuzione in un browser emulato da node.js + JSDOM o in un semplice browser ... Il motivo è che ho un ciclo infinito usando setTimeout per controllare la posizione dell'URL, che va bene in un browser, ma mantiene lo script node.js in esecuzione per sempre ... Quindi potrebbe esserci una finestra dopo tutto in uno script node.js :)
Eric Bréchemier,

1
@Eric Dubito fortemente che sarà presente nell'ambito globale, quindi se non importi qualcosa come windownella prima riga del tuo modulo non dovresti avere problemi. Si potrebbe anche eseguire una funzione anonima e controllare il [[Class]]di thisdentro (funziona solo in modalità non-oggettiva) vedere "Classe" sotto: bonsaiden.github.com/JavaScript-Garden/#typeof
Ivo Wetzel

1
Il mio problema differisce leggermente dai PO: non sto richiedendo lo script, è caricato da JSDOM con la finestra emulata come contesto globale ... È ancora gestito da node.js + V8, solo in un contesto diverso dai normali moduli.
Eric Bréchemier,

1
Probabilmente ... Sono andato in un'altra direzione: 1) rilevare il supporto per onhashchange ("onhashchange" nella finestra) per evitare di creare il ciclo infinito 2) simulare il supporto impostando la proprietà onhashchange sulla finestra emulata nello script principale node.js.
Eric Bréchemier,

1
typeof self === 'object'potrebbe essere più sicuro poiché typeof window === 'undefined'fallisce nell'ambito del Web.
Lewis,

45

Al momento mi sono imbattuto in un rilevamento errato di Node che non è a conoscenza dell'ambiente Node in Electron a causa di un rilevamento delle funzioni fuorviante. Le seguenti soluzioni identificano esplicitamente l'ambiente di processo.


Identifica solo Node.js

(typeof process !== 'undefined') && (process.release.name === 'node')

Questo scoprirà se stai eseguendo un processo Node, poiché process.releasecontiene i "metadati relativi all'attuale versione [Node-]".

Dopo la generazione di io.js, il valore di process.release.namepuò anche diventare io.js(vedere il processo-doc ). Per rilevare correttamente un ambiente pronto per il nodo, suppongo che si dovrebbe controllare come segue:

Identificare il nodo (> = 3.0.0) o io.js

(typeof process !== 'undefined') &&
(process.release.name.search(/node|io.js/) !== -1)

Questa affermazione è stata testata con Nodo 5.5.0, Electron 0.36.9 (con Nodo 5.1.1) e Chrome 48.0.2564.116.

Identificare il nodo (> = 0.10.0) o io.js

(typeof process !== 'undefined') &&
(typeof process.versions.node !== 'undefined')

Il commento di @ daluege mi ha ispirato a pensare a una prova più generale. Questo dovrebbe funzionare da Node.js> = 0.10 . Non ho trovato un identificatore univoco per le versioni precedenti.


Ps: sto postando quella risposta qui poiché la domanda mi porta qui, anche se l'OP stava cercando una risposta a una domanda diversa.


2
Questo sembra essere di gran lunga l'approccio più affidabile, grazie. Anche se funziona solo per la versione> = 3.0.0.
filip

@daluege - grazie per l'ispirazione. Purtroppo non ho trovato una prova per un valore inferiore a 0,10.
Florian Breisch,

3
Ho trovato l'utilizzo di un reattivo webpack, processed process.versionesiste all'interno del bundle, quindi ho aggiunto un controllo extra per process.versiondove process.release.nodenon è definito sul lato client ma ha una versione del nodo come valore sul lato server
Aaron

@Aaron: grazie per quel suggerimento. Non sono stato in grado di trovare alcuna definizione della process.versionvariabile (in reazioni, webpack o reazioni-webpack). Gradirei qualsiasi suggerimento in cui è definita la variabile versione per aggiungerla alla risposta. A seconda dei vincoli release.node sul nodo> = 3.xx
Florian Breisch,

2
One-liner e più sicuro:function isNodejs() { return typeof "process" !== "undefined" && process && process.versions && process.versions.node; }
brillout

25

Il problema con il tentativo di capire in quale ambiente è in esecuzione il tuo codice è che qualsiasi oggetto può essere modificato e dichiarato rendendo quasi impossibile capire quali oggetti sono nativi per l'ambiente e quali sono stati modificati dal programma.

Tuttavia, ci sono alcuni trucchi che possiamo usare per capire con certezza l'ambiente in cui ti trovi.

Iniziamo con la soluzione generalmente accettata utilizzata nella libreria di sottolineatura:

typeof module !== 'undefined' && module.exports

Questa tecnica è in realtà perfettamente valida per il lato server, poiché quando requireviene chiamata la funzione, reimposta l' thisoggetto su un oggetto vuoto e lo ridefinisce modulenuovamente per te, il che significa che non devi preoccuparti di manomissioni esterne. Finché il tuo codice è caricato require, sei al sicuro.

Tuttavia, questo cade a pezzi sul browser, poiché chiunque può facilmente definirlo moduleper far sembrare che sia l'oggetto che stai cercando. Da un lato questo potrebbe essere il comportamento desiderato, ma determina anche quali variabili l'utente della libreria può utilizzare nell'ambito globale. Forse qualcuno vuole usare una variabile con il nome moduleche ha exportsal suo interno per un altro uso. È improbabile, ma chi siamo noi per giudicare quali variabili qualcun altro può usare, solo perché un altro ambiente utilizza quel nome di variabile?

Il trucco, tuttavia, è che se stiamo assumendo che il tuo script sia caricato nell'ambito globale (che sarà se viene caricato tramite un tag di script) una variabile non può essere riservata in una chiusura esterna, perché il browser non lo consente . Ora ricorda nel nodo, l' thisoggetto è un oggetto vuoto, eppure la modulevariabile è ancora disponibile. Questo perché è dichiarato in una chiusura esterna. Quindi possiamo quindi correggere il controllo di sottolineatura aggiungendo un controllo extra:

this.module !== module

Con questo, se qualcuno dichiara modulenell'ambito globale nel browser, verrà inserito thisnell'oggetto, il che provocherà il fallimento del test, poiché this.modulesarà lo stesso oggetto del modulo. Sul nodo, this.modulenon esiste ed moduleesiste all'interno di una chiusura esterna, quindi il test avrà esito positivo, poiché non sono equivalenti.

Pertanto, il test finale è:

typeof module !== 'undefined' && this.module !== module

Nota: sebbene ciò ora consenta alla modulevariabile di essere utilizzata liberamente nell'ambito globale, è comunque possibile aggirarla nel browser creando una nuova chiusura e dichiarando al suo moduleinterno, quindi caricando lo script all'interno di quella chiusura. A quel punto l'utente sta replicando completamente l'ambiente del nodo e, si spera, sa cosa sta facendo e sta cercando di fare uno stile di nodo richiesto. Se il codice viene chiamato in un tag script, sarà comunque al sicuro da eventuali nuove chiusure esterne.


2
Wow, grazie per aver spiegato chiaramente la logica dietro ogni pezzo del tuo one-liner.
Jon Coombs,

ottenuto Cannot read property 'module' of undefinedperché questo non è definito nei test moka per esempio
srghma,

20

Quanto segue funziona nel browser a meno che non sia stato intenzionalmente sabotato esplicitamente:

if(typeof process === 'object' && process + '' === '[object process]'){
    // is node
}
else{
    // not node
}

Bam.


4
var process = {toString: function () {return '[processo oggetto]'; }};
Nick Desaulniers,

1
C'è qualche motivo per cui usi process+''invece di process.toString()?
Harmic

3
Quasi. Usa questo invece:Object.prototype.toString.call(process)
sospedra

2
Questa è la migliore risposta a questa domanda.
Loretoparisi,

3
@harmic: var process = null;causerebbe il fallimento del secondo caso. Sia in Javascript che in Java, l'espressione '' + xproduce lo stesso di x.toString()quando xè cattiva, la prima produce "null"o "undefined"dove la seconda genera un errore.
Joeytwiddle,

17

Ecco un bel modo per farlo anche:

const isBrowser = this.window === this;

Questo funziona perché nei browser la variabile globale "questa" ha un riferimento personale chiamato "finestra". Questo riferimento personale non esiste in Nodo.

  • Nel browser "questo" è un riferimento all'oggetto globale, chiamato "finestra".
  • Nel nodo 'this' è un riferimento all'oggetto module.exports.
    • 'questo' non lo è un riferimento all'oggetto globale Node, chiamato 'global'.
    • 'this' non è un riferimento allo spazio di dichiarazione delle variabili del modulo.

Per interrompere il controllo del browser suggerito sopra, dovresti fare qualcosa di simile al seguente

this.window = this;

prima di eseguire il controllo.


Perché non semplicemente const isBrowser = this.window !== undefined? E in teoria nel nodo posso fare this.window = thisper ingannare la soluzione.
Tyler Long,

11

Ancora un altro rilevamento ambientale :

(Significato: la maggior parte delle risposte qui sono a posto.)

function isNode() {
    return typeof global === 'object'
        && String(global) === '[object global]'
        && typeof process === 'object'
        && String(process) === '[object process]'
        && global === global.GLOBAL // circular ref
        // process.release.name cannot be altered, unlike process.title
        && /node|io\.js/.test(process.release.name)
        && typeof setImmediate === 'function'
        && setImmediate.length === 4
        && typeof __dirname === 'string'
        && Should I go on ?..
}

Un po 'paranoico vero? Puoi renderlo più dettagliato controllando più globuli .

Ma NON !.

Tutti questi sopra possono essere simulati / simulati comunque.

Ad esempio per falsificare l' globaloggetto:

global = {
    toString: function () {
        return '[object global]';
    },
    GLOBAL: global,
    setImmediate: function (a, b, c, d) {}
 };
 setImmediate = function (a, b, c, d) {};
 ...

Questo non verrà attaccato all'oggetto globale originale del Nodo ma verrà attaccato windowall'oggetto in un browser. Quindi implica che sei in Node env all'interno di un browser.

La vita è breve!

Ci preoccupiamo se il nostro ambiente è falso? Succederebbe quando uno stupido sviluppatore dichiarasse chiamata una variabile globaleglobal nell'ambito globale. O qualche sviluppatore malvagio inietta codice nel nostro ambiente in qualche modo.

Potremmo impedire l'esecuzione del nostro codice quando lo rileviamo, ma molte altre dipendenze della nostra app potrebbero essere coinvolte in questo. Quindi alla fine il codice si romperà. Se il tuo codice è abbastanza buono, non dovresti preoccuparti di ogni errore sciocco che avrebbe potuto essere fatto da altri.

E allora?

Se si rivolge a 2 ambienti: Browser e Nodo;
"use strict"; e controlla semplicemente per windowo global; e indica chiaramente che nei documenti che il tuo codice supporta solo questi ambienti. Questo è tutto!

var isBrowser = typeof window !== 'undefined'
    && ({}).toString.call(window) === '[object Window]';

var isNode = typeof global !== "undefined" 
    && ({}).toString.call(global) === '[object global]';

Se possibile per il tuo caso d'uso; invece del rilevamento dell'ambiente; eseguire il rilevamento sincrono delle funzioni all'interno di un blocco try / catch. (l'esecuzione richiederà alcuni millisecondi).

per esempio

function isPromiseSupported() {
    var supported = false;
    try {
        var p = new Promise(function (res, rej) {});
        supported = true;
    } catch (e) {}
    return supported;
}

9

La maggior parte delle soluzioni proposte può effettivamente essere simulata. Un modo affidabile è verificare la Classproprietà interna dell'oggetto globale usando il Object.prototype.toString. La classe interna non può essere falsificata in JavaScript:

var isNode = 
    typeof global !== "undefined" && 
    {}.toString.call(global) == '[object global]';

2
Questo tornerà vero sotto browserify.
alt

1
L'hai provato? Non riesco a vedere come browserify possa cambiare la classe interna di un oggetto. Ciò richiederebbe la modifica del codice nella VM JavaScript o la sovrascrittura, il Object.prototype.toStringche è davvero una cattiva pratica.
Fabian Jakobs,

L'ho provato. Ecco cosa fa browserify: var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};
Vanuan

Vedi, in Chrome, ({}.toString.call(window))è uguale "[object global]".
Vanuan,

2
È strano, perché window.toString()produce"[object Window]"
Vanuan

5

Cosa succede ad usare l'oggetto processo e controllo execPath per node?

process.execPath

Questo è il percorso assoluto dell'eseguibile che ha avviato il processo.

Esempio:

/ Usr / local / bin / node


2
Che dire window.process = {execPath: "/usr/local/bin/node"};?
Константин Ван


4

Ecco la mia variazione su quanto sopra:

(function(publish) {
    "use strict";

    function House(no) {
        this.no = no;
    };

    House.prototype.toString = function() {
        return "House #"+this.no;
    };

    publish(House);

})((typeof module == 'undefined' || (typeof window != 'undefined' && this == window))
    ? function(a) {this["House"] = a;}
    : function(a) {module.exports = a;});

Per usarlo, modifichi "House" sull'ultima riga in modo che sia qualsiasi cosa tu voglia che il nome del modulo sia nel browser e pubblichi qualunque cosa tu voglia che sia il valore del modulo (di solito un costruttore o un oggetto letterale ).

Nei browser l'oggetto globale è window e ha un riferimento a se stesso (c'è una finestra.window che è == window). Mi sembra improbabile che ciò accada a meno che tu non sia in un browser o in un ambiente che vuole farti credere di essere in un browser. In tutti gli altri casi, se viene dichiarata una variabile 'modulo' globale, la utilizza in caso contrario utilizza l'oggetto globale.


4

Sto usando processper verificare node.js in questo modo

if (typeof(process) !== 'undefined' && process.version === 'v0.9.9') {
  console.log('You are running Node.js');
} else {
  // check for browser
}

o

if (typeof(process) !== 'undefined' && process.title === 'node') {
  console.log('You are running Node.js');
} else {
  // check for browser
}

Documentato qui


2
process.titlepuò essere modificato
Ben Barkay il

Quindi controlla il titolo in cui lo hai modificato. Oppure usa process.version
Chris

Se stai scrivendo per una biblioteca (come dovresti), non potrai aspettarti quale dovrebbe essere il titolo
Ben Barkay,

3

Al momento della stesura di questo articolo, questa risposta è più un'opzione "coming soon", visto che sfrutta le nuovissime funzionalità di JavaScript.

const runtime = globalThis.process?.release?.name || 'not node'
console.log(runtime)

Il runtimevalore sarà nodeo not node.

Come accennato, questo si basa su alcune nuove funzionalità JavaScript. globalThisè una funzionalità finalizzata nelle specifiche ECMAScript 2020. Incatenamento facoltativo / coalescenza nulla (la ?parte diglobalThis.process?.release?.name ) è supportata nel motore V8, fornito con Chrome 80. A partire dal 4/8/2020, questo codice funzionerà nel browser ma non funzionerà in Nodo poiché il ramo Nodo 13 utilizza V8 7.9.xxx. Credo che il nodo 14 (che dovrebbe essere rilasciato il 21/04/2020) dovrebbe usare V8 8.x +.

Questo approccio prevede una buona dose delle attuali restrizioni. Però; il ritmo con cui vengono rilasciati i browser / nodo, alla fine sarà un one-liner affidabile.


1
Questa dovrebbe essere la risposta accettata! e tutti dovrebbero usare il nodo 14 a proposito
Sceat

2

Node.js ha un processoggetto, quindi finché non hai altri script da creare processPuoi usarlo per determinare se il codice viene eseguito su Node.

var isOnNodeJs = false;
if(typeof process != "undefined") {
  isOnNodeJs = true;
}

if(isOnNodeJs){
  console.log("you are running under node.js");
}
else {
  console.log("you are NOT running under node.js");
}

2

Questo è un modo abbastanza sicuro e diretto per assicurare la compatibilità tra javascript lato server e lato client, che funzionerà anche con browserify, RequireJS o CommonJS incluso lato client:

(function(){

  // `this` now refers to `global` if we're in NodeJS
  // or `window` if we're in the browser.

}).call(function(){
  return (typeof module !== "undefined" &&
    module.exports &&
    typeof window === 'undefined') ?
    global : window;
}())

1

Modifica : per quanto riguarda la tua domanda aggiornata: "Come può uno script dire se è stato richiesto come modulo commonjs?" Non penso che possa. Puoi controllare se exportsè un oggetto ( if (typeof exports === "object")), poiché la specifica richiede che sia fornita ai moduli, ma tutto ciò che ti dice è che ... exportsè un oggetto. :-)


Risposta originale:

Sono sicuro che ci sia un simbolo specifico per NodeJS ( EventEmitter, forse no, devi usare requireper ottenere il modulo eventi; vedi sotto ) che potresti controllare, ma come ha detto David, idealmente stai meglio a rilevare la funzione (piuttosto rispetto all'ambiente) se ha senso farlo.

Aggiornamento : forse qualcosa del tipo:

if (typeof require === "function"
    && typeof Buffer === "function"
    && typeof Buffer.byteLength === "function"
    && typeof Buffer.prototype !== "undefined"
    && typeof Buffer.prototype.write === "function") {

Ma questo ti dice solo che sei in un ambiente con requirequalcosa di molto simile a quello di NodeJS Buffer. :-)


Posso ancora romperlo impostando tutta quella roba in un sito Web ... questo è solo eccessivo;) Controllare per essere in un browser è più facile poiché l'ambiente Node è più pulito.
Ivo Wetzel,

1
@Ivo: Sì, vedi la mia ultima frase. Posso facilmente interrompere il controllo definendo una windowvariabile all'interno di un'applicazione NodeJS. :-)
TJ Crowder,

1
@Ivo: Non sarei affatto sorpreso se qualcuno fosse definito windowin un modulo NodeJS, quindi potrebbero includere codice che si basava windowsull'essere l'oggetto globale e non voleva modificarlo. Non lo farei, non lo faresti, ma scommetto che qualcuno l'ha fatto. :-) O hanno semplicemente windowsignificato qualcos'altro completamente.
TJ Crowder,

1
@Ivo: yuiblog.com/blog/2010/04/09/… è uno dei motivi per cui l'oggetto window può essere definito in node.js
slebetman

1
@TJCrowdertypeof process !== "undefined" && process.title === "node"
Raynos

0
const isNode =
  typeof process !== 'undefined' &&
  process.versions != null &&
  process.versions.node != null;


-1

Prendi la fonte di node.js e modificala per definire una variabile come runningOnNodeJS. Controlla quella variabile nel tuo codice.

Se non puoi avere la tua versione privata di node.js, apri una richiesta di funzionalità nel progetto. Chiedi che definiscano una variabile che ti dia la versione di node.js in cui stai eseguendo. Quindi controlla quella variabile.


1
Che di nuovo non risolve il suo problema (sostanzialmente irrisolvibile), posso nuovamente creare una tale variabile nel Browser. Il modo migliore sarebbe impedire agli script di creare un windowglobale, suppongo che invierò una richiesta di funzione su quello.
Ivo Wetzel,

@Ivo: Questa è una cattiva idea che spezzerebbe il codice che usa jsdom ( github.com/tmpvar/jsdom ) per manipolare il dom lato server usando librerie familiari come YUI e jQuery. E ci sono codici attualmente in produzione che lo fanno.
Slebetman,

@slebetman No, non si romperà jsdom. Sto parlando di globale , come in nessuna dichiarazione var globale , il codice di esempio lì usa la vardichiarazione, le persone che la immettono nello spazio dei nomi globale, quindi non ottengono il concetto di moduli autonomi allora
Ivo Wetzel

@Ivo è un po 'violento, è come dire che dovremmo prendere la possibilità di mangiare torte perché le persone ingrassano troppo. È necessario ingombrare lo spazio dei nomi globale per ottenere una libreria che funzionerà tra i moduli. Oppure potresti concludere tutto in un singolo modulo, ma che senso ha?
Ben Barkay,

-1

Post molto vecchio, ma l'ho risolto semplicemente racchiudendo le dichiarazioni richieste in un tentativo di cattura

try {
     var fs = require('fs')
} catch(e) {
     alert('you are not in node !!!')
}

2
Questo non è vero, puoi usare browserify per usare le chiamate "nodeish" richiedono ()
fat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.