Code Monkey home page Code Monkey logo

aemsync's Introduction

aemsync

The code and content synchronization for Sling / AEM (Adobe Experience Manager).

Synopsis

The tool pushes content to AEM instance(s) upon a file change.

  • There is no vault dependency.
  • It can push to multiple instances at the same time (e.g. author and publish).
  • IDE / editor agnostic.
  • Works on Windows, Linux and Mac.

Installation

With npm do:

npm install aemsync -g

Usage

Simply run aemsync on your project path, make a change to any of your files or directories and watch the magic happen.

Advanced usage

CLI

Usage:
  aemsync [OPTIONS]

Options:
  -t <target>           URL to AEM instance; multiple can be set.
                        Default: http://admin:admin@localhost:4502
  -w <path_to_watch>    Watch over folder.
                        Default: '.'
  -p <path_to_push>     Push specific file or folder.
  -e <exclude_filter>   Extended glob filter; multiple can be set.
                        Default:
                          **/jcr_root/*
                          **/@(.git|.svn|.hg|target)
                          **/@(.git|.svn|.hg|target)/**
  -d <delay>            Time to wait since the last change before push.
                        Default: 300 ms
  -q <packmgr_path>     Package manager path.
                        Default: /crx/packmgr/service.jsp
  -c                    Check if AEM is up and running before pushing.
  -v                    Enable verbose mode.
  -h                    Display this screen.

Examples:
  Magic:
    > aemsync
  Custom targets:
    > aemsync -t http://admin:admin@localhost:4502 -t http://admin:admin@localhost:4503 -w ~/workspace/my_project
  Custom exclude rules:
    > aemsync -e **/*.orig -e **/test -e -e **/test/**
  Just push, don't watch:
    > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component
  Push multiple:
    > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-other-component

JavaScript API

import { aemsync, push } from 'aemsync'

// Interactive watch example.
(async function () {
  const args = { workingDir }

  for await (const result of aemsync(args)) {
    console.log(result)
  }
})()

// Push example.
(async function () {
  const args = { payload: [
    './foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component',
    './foo/bar/my-workspace/jcr_content/apps/my-app/components/something-else'
  ]}

  for await (const result of aemsync(args)) {
    // Will yield one result for each target.
    console.log(result)
  }
})()

JavaScript arguments and defaults for aemsync() and push() functions:

const args = {
  workingDir: '.',
  exclude: ['**/jcr_root/*', '**/@(.git|.svn|.hg|target)', '**/@(.git|.svn|.hg|target)/**'],
  packmgrPath: '/crx/packmgr/service.jsp',
  targets: ['http://admin:admin@localhost:4501'],
  delay: 300,
  checkIfUp: false
}

Description

Watching for file changes is fast, since it uses Node's recursive option for fs.watch() where applicable.

Any changes inside jcr_root folders are detected and deployed to AEM instance(s) as a package. By default, there is an exclude filter in place:

  • Changes to first level directories under jcr_root are ingored. This is to avoid accidentally removing apps, libs or any other first level node in AEM.
  • Any paths containing .svn, .git, .hg or target are ignored.
  • The exclude filter can be overriden. Do note that this will remove the above rules completely and if required, they must be added manually.

Delay is the time to wait to pass since the last change before the package is created. In case of bulk changes (e.g. switching between code branches), creating a new package per file should be avoided and instead, all changes should be pushed in one go. Lowering the value decreases the delay for a single file change but may increase the delay for multiple file changes. If you are unsure, please leave the default.

Caveats

  1. Packages are installed using package manager service (/crx/packmgr/service.jsp), which takes some time to initialize after AEM startup. If the push happens before, the Sling Post Servlet will take over causing the /crx/packmgr/service.jsp/file node to be added to the repository. Use -c option to performs a status check before sending (all bundles must be active).
  2. Changing any XML file will cause the parent folder to be pushed. Given the many special cases around XML files, the handling is left to the package manager.

aemsync's People

Contributors

abmaonline avatar coffeemedia avatar dhughes-xumak avatar easingthemes avatar gavoja avatar gregorskii avatar maciej-panecki-wttech avatar mkochel-shell avatar rbotha78 avatar somoso avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aemsync's Issues

Cannot sync .xml files, such as cq_dialog fields

Dialog options within the _cq_dialog folders are often contained within nested folders and an XML file. It appears that aemsync is removing the .xml from these files, creating incorrect filters during package sync.

Using debug mode, I pulled the following filter settings:

<filter root="/apps/.../component/_cq_dialog/content/items/tabs/items/.../multivalue">
    <exclude pattern="/apps/.../component/_cq_dialog/content/items/.../multivalue/.*" />
    <include pattern="/apps/.../component/_cq_dialog/content/items/tabs/items/.../multivalue/field" />
    <include pattern="/apps/.../component/_cq_dialog/content/items/tabs/items/.../multivalue/field/.*" />
  </filter>

VS a standard css file

   <filter root="/etc/designs/.../clientlibs/css">
      <exclude pattern="/etc/designs/.../clientlibs/css/.*" />
      <include pattern="/etc/designs/.../clientlibs/css/main.css" />
      <include pattern="/etc/designs/.../clientlibs/css/main.css/.*" />
    </filter>

Note the difference between "main.css" and simply "field", where the latter should be "field.xml"

Code that performs this here:
https://github.com/gavoja/aemsync/blob/master/src/package.js#L128-L132

What is this filter meant to prevent? As a result of it, it seems that no edits to .xml files can be synced correctly. Is that purposeful?

Sync .tag files

Hi!
Is there any chance the code could make .tag files sync?
I have not enough knowledge about them to know if it's possible to build.

/Clara

Watching files not working on Windows

Hello,

In our project we are using aemsync (which uses simple-watcher), wrapped with aem-sync-webpack-plugin. I know the latter uses aemsync v1.1.1, but while debugging I updated the dependency to 4.0.1 on my local copy. We use aemsync to watch changes and push clientlibs during development.

Unfortunately, the aemsync/simple-watcher is not working correctly on our Windows machines. What I mean is I was able to observe that it triggers the push only when webpack is starting, and ignores all future updates. It works fine on our Macs.

As a workaround we switched to using gaze, like this:

    gaze(options.watchDir + '/**/*.*', {interval: options.pushInterval}, function (err, watcher) {
        this.on('all', function (event, filepath) {
            aemsync.push(options.watchDir, options);
        });
        this.on('error', function (error) {
            console.debug('Error: ', error.code);
        });
    });

If you need any additional information, I'll be happy to provide it.

Best regards,
Piotr

Package Manager clutter

I am using the latest version (3.0) and I've noticed it uploads every change with a new package name, leading to a clutter of packages in package manager:

Is it possible to do a cleanup, or to use the same name for the packages so that there will be only one?

Thanks!
Ittai

image

Changing many files with auto build scripts leads to exceptions and failure to process files

Problematic:
when using client lib generator to generate lots of files at once aemsync crashing on Windows ( 100% confirmed ).

On linux it is not an issue at all, but on windows it is real pain.

It happens when files already created and enumerated on disk were deleted when it started processing that file.

Another issue is when there is no directory in place and fs.lstatSync(filePath).isDirectory() returns false when it is not exists.
After this check code treat this entry as file and going to process it, at the same amount of time the directory being created and readFileSync crashing with exception file is directory :)

Solution:

  1. Debounce all enqueue of any files and start processing only when there no updates.
  2. add exception handling and retry for directory processing.

Error: "env: node\r: No such file or directory" when using aemsync v 0.7.0 on OSX

Hello,

Since upgrading to aemsync 0.7.0 this week I can't get it to run on OSX 10.10.3. I try to run:
aemsync -t http://admin:admin@localhost:4502 -w ~/workspace/my_project

And get error:
env: node\r: No such file or directory

I tried running dos2unix on aemsync.js suspecting a file ending error but couldn't get that to work either.

But uninstalling and reinstalling v 0.6.1 works fine:
npm install [email protected] -g

Apologies if this is not an error on aemsync side!

javax.jcr.InvalidItemStateException: Unable to update a stale item: item.save()

This error happens occasionally after switching between code branches if a large set of files is modified. This results in multiple packages being sent at the same time causing the following JCR error:
javax.jcr.InvalidItemStateException: Unable to update a stale item: item.save()

  • Solution A: Submit code synchronously.
  • Solution B: Use different package names.

Create better documentation

I tried running this in my local folder and it doesn't seem to be syncing.

$ aemsync -t http://admin:admin@localhost:4502 -i 50 .
Watching: jcr_root. Update interval: 50 ms.

Syncing entire directory tree when one file is edited

Sometimes, when a file is edited, it triggers changes in all files under the same root directory, syncing a whole directory unnecessarily. For instance, if I make a change in /jcr_root/apps/components/a/script.jsp it reports that there where changes in /jcr_root/apps/components/b/script.jsp, /jcr_root/apps/components/c/script.jsp, etc.. So, basically syncing everything under apps resulting in very long sync jobs.

We are seeing this in Windows 7 using Sublime Text 3 as the editor. Idk if this maybe has anything to do with Sublime Text.

I'm thinking if maybe there was a non-recursive flag it would solve the issue. This wasn't a big deal at first, but we then realized that whenever aemsync decided to sync the whole etc folder, it would override an non-versioned folder we have under etc in our local servers.

Node.JS

Hi, I've seen that you are developing new version, which will support common.js syntax for using in Node.js.
It's very good, but your code is supported only ES6 syntax. Could you add also version for ES5?

Exclusion doesn't work on a path when directory starts with a dot symbol

The problem is in a /src/watcher.js:16

The following code result is false:
console.log(mm('users/example/directory/node_modules/.micromatch.DELETE/node_modules', ['**/example/directory/**']).length > 0);

Possible solution:
if (exclude && mm([localPath], exclude, {dot: true}).length > 0) {

Files with similar names are skipped

I'm trying to sync the following files:

styles.css
styles.min.css
styles.css.map

Default output is

ADD styles.min.css
ADD styles.css.map
Removing child: styles.css
ADD styles.css

I don't think the filename comparison at package.js:45 is appropriate, it should probably check whether or not the paths in question are paths vs individual files. I have just removed this check in my branch to get around this, but recognize that a proper comparison is a better solution.

First change detection is delayed

After running the script (I work on large projects), it takes time for it to start recognizing changes (can take up to a minute). After that initial delay changes are picked up instantly (within the update interval that is).

This is presumably caused by an issue with the node-watch module.

Tested on Ubuntu 14.04 64-bit.

Minimatch produce different regex compared to micromatch

After updating from 3.0.1 to 3.0.2 I noticed the exclude filters were no longer working after micromatch was replaced. The two libraries yields very different output in certain cases, for example:

**/assets|**/assets/** 

micromatch => /^(?:(?:(?:(?!(?:\/|^)\.).)*?\/|)assets|(?:(?:(?!(?:\/|^)\.).)*?\/|)assets\/(?:(?!(?:\/|^)\.).)*?)$/
minimatch => /^(?:(?:(?!(?:\/|^)\.).)*?\/(?=.)assets\|[^\/]*?[^\/]*?\/assets\/(?:(?!(?:\/|^)\.).)*?)$/

when matched against nettsider/components/foobar/assets/index.js only micromatch gives a positive result.

Syncing complete folder when changing only one file (#23) - but don't filter excluded files

I'm observing the same issue as in #23 with the latest version - however more problematic than the original issue because this includes also files that should be excluded.
The problem arises when typescript and/or scss files shall be transpiled in the component's clientlib folder. The tsc and sass watchers create the js/css files which shall be part of the aemsync package. But the excluded ts/scss files are included as well.
Don't know how to handle this...
Note: it seems not to happen all the time - depends on the order of the files in the changed list ?

Not syncing content in aem archtype based project

I have a few projects that were created from the AEM Archtype Template and I cannot seem to get the content to sync using aemsync. The standard sync script is "sync": "aemsync -d -p ../ui.apps/src/main/content", which I think is part of the problem. Per package.js#L44, if the local path to be synced does not include jcr_root, the path is ignored. However, changing the script to "sync": "aemsync -d -p ../ui.apps/src/main/content/jcr_root/apps", still does not work. I believe this is because the default path exclusion includes **/jcr_root/* here.

If I am reading the code correctly, and forgive me if I am not here, the local path must include jcr_root but such a path is excluded by default. I was able to work around this by specifying more of the path I want synced: aemsync -d -p ../ui.apps/src/main/content/jcr_root/apps/brandA

Is this the way the code is meant to work? Does it require a deeper path like I found or is there room for or maybe a need for allowing the syncing of the /jcr_root/ folder or maybe /jcr_root/apps/? I have also found through troubleshooting this that some console logging for the user might be needed to help people uncover why their content is not syncing.

Error syncing package

I have created a project with the latest archetype version:

mvn -B archetype:generate
-D archetypeGroupId=com.adobe.aem
-D archetypeArtifactId=aem-project-archetype
-D archetypeVersion=30
-D appTitle="My Site"
-D appId="mysite"
-D groupId="com.mysite"

And tried to use te command npm run watch, from package.json in ui.frontend package:

"scripts": {
    "dev": "webpack -d --env dev --config ./webpack.dev.js && clientlib --verbose",
    "prod": "webpack -p --config ./webpack.prod.js && clientlib --verbose",
    "start": "webpack-dev-server --open --config ./webpack.dev.js",
    "sync": "aemsync -d -p ../ui.apps/src/main/content",
    "watch": "webpack-dev-server --config ./webpack.dev.js --env.writeToDisk & watch 'clientlib' ./dist & aemsync -w ../ui.apps/src/main/content"
  }

At first I did get an error when trying to sync saying:
http://admin:admin@localhost:4502 > Unauthorized

But I solved it by allowing anonymous access in http://localhost:4502/system/console/configMgr/org.apache.sling.engine.impl.auth.SlingAuthenticator

Finally I did get another error:
http://admin:admin@localhost:4502 > Unexpected response text format

Debugging the pipeline.js I did get this response from the aem instance:

<crx version="1.40-T20210831063907-0850a77" user="anonymous" workspace="crx.default">
  <request>
    <param name="file" value="aemsync.zip"/>
    <param name="force" value="true"/>
    <param name="install" value="true"/>
  </request>
  <response>
    <status code="500">javax.jcr.RepositoryException: could not crete intermediate nodes</status>
  </response>
</crx>

So I checked the aem logs with:
tail -f /Users/my_user/aem-sdk/author/crx-quickstart/logs/error.log

And I don't know if it's related, but I did see this error:
21.09.2021 10:19:52.499 *WARN* [qtp1053974551-462] org.apache.jackrabbit.vault.packaging.impl.PackagePropertiesImpl Package does not specify a version. setting to ''

Do you know what's happening?

Takes over 100% CPU

Hi, and thanks for this awesome library.

The only problem I have is that when starting it up, on my OSX 10 machine it instantly takes 100%+ CPU:

Any idea what could be the reason?

Don't skip files in target folders

The sync should not ignore */target/* files. Our build process only outputs to target and doesn't work with this package OOTB. There is mention here that it may be appropriate to remove here #8, if not removed, maybe it makes sense to expose the ability to include target via a switch?

use aemsync with Gulp/Webpack

It is very promising to employ aemsync with Gulp/Webpack so that we can write complex Javascript files (ES6, TypeScript) and compile to ES5 compatible that can run on AEM and have the app work in conjunction with BrowserSync, Lite-Server and/or Hot-Module-Reloader(HMR).

The question here is how to trigger the browser to automatically reload its tab current opening localhost:4502 for every change made to the code base (similar to BrowserSync). Any thoughts?

RE_SAFE_PATH test fails

i changed an html sightly file, the watcher detects it correctly but the RE_SAFE_PATH test fails.
if (!RE_SAFE_PATH.test(item)) {
return;
}
Maybe i used the wrong path?
Can you help me out, where exactly set the path too.

Issue Syncing:

Hi there,

Trying to get this plugin to work but I am seeing items in the CRX remaining stale.

I have this output from the plugin:

[16:58:40] Package pushed to 192.168.99.100:4502.
ADD jcr_root/apps/myapp/components/page/templateSightly/templateSightly.jsp
DEL jcr_root/apps/myapp/components/page/templateSightly/templateSightly.jsp___jb_old___
DEL jcr_root/apps/myapp/components/page/templateSightly/templateSightly.jsp___jb_tmp___
  Deploying to [192.168.99.100:4502] in 53 ms at 2016-05-21T00:01:15.619Z: OK
[17:01:15] Package pushed to 192.168.99.100:4502.

This output from AEM logs:

aem@5294030764fc:~/crx-quickstart/logs$ tail -f error.log
21.05.2016 00:00:00.049 *INFO* [pool-7-thread-2] com.day.cq.dam.core.impl.ExpiryNotificationJobImpl No assets passed to deactivate.
21.05.2016 00:00:30.002 *INFO* [pool-7-thread-1] com.adobe.granite.taskmanagement.impl.jcr.TaskArchiveService archiving tasks at: 'Sat May 21 00:00:30 UTC 2016'
21.05.2016 00:01:15.588 *WARN* [qtp2133258724-1974] org.apache.jackrabbit.vault.packaging.impl.PackagePropertiesImpl Package does not specify a version. setting to ''
21.05.2016 00:01:15.588 *WARN* [qtp2133258724-1974] org.apache.jackrabbit.vault.packaging.impl.PackagePropertiesImpl Package does not specify a path. setting to 'unknown'
21.05.2016 00:01:15.589 *INFO* [qtp2133258724-1974] org.apache.jackrabbit.vault.packaging.impl.JcrPackageDefinitionImpl unwrapping package :unknown
21.05.2016 00:01:15.600 *WARN* [qtp2133258724-1974] org.apache.jackrabbit.vault.packaging.impl.JcrPackageImpl Refusing to recreate snapshot .snapshot:aemsync, already exists.
21.05.2016 00:01:15.600 *INFO* [qtp2133258724-1974] org.apache.jackrabbit.vault.packaging.impl.ZipVaultPackage Extracting :unknown
21.05.2016 00:01:15.606 *INFO* [qtp2133258724-1974] org.apache.jackrabbit.vault.fs.io.AutoSave Threshold of 1024 reached. saving approx 2 transient changes. 0 unresolved
21.05.2016 00:01:15.612 *INFO* [qtp2133258724-1974] org.apache.jackrabbit.vault.packaging.impl.ZipVaultPackage Extracting :unknown completed

EDIT:

Looking into this more, looks like its building the includes filter incorrectly. Its picking the files with the ___jb_old___ extension rather than the correct file. These might be being created by Intellij. Working on debugging the excludes.

Push no longer works in v5

The below command is no longer working with v5.

./node_modules/.bin/aemsync -p /mnt/nvme1n1p1/patrickh/code/proj/ui.apps/src/main/content/jcr_root/apps/group/proj -t http://admin:admin@localhost:4502

It looks like in v5, the -p flag and value no longer have any effect when using the CLI.

aemsync in vscode gives error

not running as expected. giving below errror
checked with
node @14
node @ 16

node:fs:594
handleErrorFromBinding(ctx);
^

Error: ENOENT: no such file or directory, open './package.json'
at Object.openSync (node:fs:594:3)
at Object.readFileSync (node:fs:462:35)
at file:///Users/macbook/node_modules/aemsync/index.js:13:31
at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:533:24)
at async loadESM (node:internal/process/esm_loader:91:5)
at async handleMainPromise (node:internal/modules/run_main:65:12) {
errno: -2,
syscall: 'open',
code: 'ENOENT',
path: './package.json'
}

Watch over option no longer works in v5.0.4

Passing the -w option in the CLI does nothing. Downgrading to 5.0.3 works like a charm.

e.g.

aemsync -w ../ui.apps

Expected result to be watching over the ui.apps directory.

Aemsync throws exception after file is changed

I installed the latest version (v.1.1.1) of aemsync and after starting aemsync, when I change a file to be synchronized, it throws the exception:
Note: In version (v1.1.0) occurs the same problem, but in (v1.0.2) worked.

Awaiting changes ...
/usr/local/lib/node_modules/aemsync/node_modules/simple-watcher/index.js:35
     callback(filePath)
     ^
TypeError: callback is not a function
   at /usr/local/lib/node_modules/aemsync/node_modules/simple-watcher/index.js:35:7
   at FSReqWrap.oncomplete (fs.js:82:15)

Environment: Node - v4.2.6 and Linux 4.4.0-34-generic #53-Ubuntu

Having bundles in the Installed state should be acceptable

aemsync/src/pipeline.js

Lines 128 to 137 in 9350b99

async _check (target) {
try {
const res = await fetch(target + '/system/console/bundles.json')
const obj = await res.json()
return obj.s.length === 5 && obj.s[3] === 0 && obj.s[4] === 0
} catch (err) {
log.debug(err.message)
return false
}
}

This is a clever way to check the state of AEM, but it falsely reports that AEM is not ready if any bundle is in the Installed state. Having bundles in the installed state is completely valid, especially on a dev environment. Consider if the MDC Logging Support is occasionally used, the developer may install the bundle and start/stop it as needed.

Instead of checking /system/console/bundles.json, it could check /aem/start.html for a 200 response, or perhaps just that / does not respond with a 503.

Plugin adds but does not push .map files to server

I'm using aemsync with a webpack configuration, where I'm pushing bundled css and js files to the server. I'm also generating sourcemaps for this files.
According to the console output the .map files are added to the package, but when I download the pushed zip file from the servers crx package manager, the .map files are not included in the package.

Exclude is not working

If exclude is used, changes are not detected, since Micromatch condition is always true:

if (exclude && mm(exclude, localPath) {
  return
}

Micromatch arguments are in wrong order.
First argument must be an Array.
It also return an Array, so condition should be against Array length.

https://www.npmjs.com/package/micromatch#micromatch-1

Params

list {Array}: A list of strings to match
patterns {String|Array}: One or more glob patterns to use for matching.

vs webdav

the aem project i currently work on, we watch for file changes and push individual files via webdav into crx. aemsync seems to archive any changed folders within a certain threshold of time, and upload and install a zip package via the package manager service. have these 2 approaches been compared and performance tested? was webdav considered, and if so was there any details around why the package manager approach was used?

Feature request : Add complete event

Is it possible to add an event when the sync is completed? This way we could trigger browser-sync to reload the browser when the package is saved to the CRX.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.