├── tests ├── __init__.py ├── util.py └── test_finder.py ├── yarn ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── yarn_add.py └── finders.py ├── setup.cfg ├── CONTRIBUTING.md ├── .gitignore ├── requirements_dev.txt ├── .travis.yml ├── LICENSE ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yarn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yarn/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yarn/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Run tests with 2 | 3 | `$ py.test` 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.sw? 3 | *.egg-info/ 4 | build/ 5 | dist/ 6 | .cache 7 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | Django==1.9 2 | py==1.4.31 3 | pytest==2.8.4 4 | wheel==0.24.0 5 | -------------------------------------------------------------------------------- /yarn/management/commands/yarn_add.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from yarn.finders import yarn_add 3 | 4 | 5 | class Command(BaseCommand): 6 | help = 'Run yarn add' 7 | 8 | def handle(self, *args, **options): 9 | yarn_add() 10 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def configure_settings(): 5 | settings.configure( 6 | DEBUG=True, 7 | CACHES={ 8 | 'default': { 9 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 10 | } 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - '3.5' 5 | - '3.6' 6 | - '3.7' 7 | env: 8 | - DJANGO_VERSION=2.2.1 9 | - DJANGO_VERSION=2.1.8 10 | - DJANGO_VERSION=1.11.20 11 | install: 12 | - pip install Django==$DJANGO_VERSION pytest 13 | - python setup.py install 14 | script: py.test 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin McCarthy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages # Always prefer setuptools over distutils 2 | from os import path 3 | 4 | here = path.abspath(path.dirname(__file__)) 5 | 6 | try: 7 | from collections import OrderedDict 8 | requirements = [] 9 | except ImportError: 10 | requirements = ['ordereddict'] 11 | 12 | setup( 13 | name='django-yarn', 14 | version='1.0.0', 15 | description='A django staticfiles finder that uses yarn. Based on django-npm from Kevin McCarthy https://github.com/kevin1024/django-npm.', 16 | url='https://github.com/epineda/django-yarn', 17 | author='Edgard Pineda', 18 | author_email='edgard.pineda@gmail.com', 19 | license='MIT', 20 | classifiers=[ 21 | 'Development Status :: 3 - Alpha', 22 | 'Intended Audience :: Developers', 23 | 'Topic :: Software Development :: Build Tools', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 2.6', 27 | 'Programming Language :: Python :: 2.7', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.2', 30 | 'Programming Language :: Python :: 3.3', 31 | 'Programming Language :: Python :: 3.4', 32 | ], 33 | 34 | keywords='django yarn npm staticfiles', 35 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 36 | install_requires=requirements, 37 | extras_require={ 38 | 'test': ['pytest'], 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-yarn 2 | 3 | Want to use yarn/yarn modules in your django project without vendoring them? django-yarn serves as a wrapper around the yarn command-line program as well as a staticfiles finder. 4 | 5 | ## Installation 6 | 7 | 1. `$ pip install django-yarn` 8 | 2. Install yarn, then install yarn (`npm install -g yarn`). If you use a private registry, make sure your `.yarnrc` is set up to connect to it 9 | 3. Have a `package.json` at the root of your project, listing your dependencies 10 | 4. Add `yarn.finders.YarnFinder` to `STATICFILES_FINDERS` 11 | 5. Configure your `settings.py` 12 | 6. `$ yarn add` with the command line, or with Python: `from yarn.finders import yarn_add; yarn_add()` 13 | 7. `$ ./manage.py collectstatic` will copy all selected node_modules files into your `STATIC_ROOT`. 14 | 15 | ## Configuration 16 | 17 | * `YARN_ROOT_PATH`: *absolute* path to the yarn "root" directory - this is where yarn will look for your `package.json`, put your `node_modules` folder and look for a `.yarnrc` file 18 | * `YARN_EXECUTABLE_PATH`: (optional) defaults to wherever `yarn` is on your PATH. If you specify this, you can override the path to the `yarn` executable. This is also an *absolute path*. 19 | * `YARN_STATIC_FILES_PREFIX`: (optional) Your yarn files will end up under this path inside static. I usually use something like 'js/lib' (so your files will be in /static/js/lib/react.js for example) but you can leave it blank and they will just end up in the root. 20 | * `YARN_FILE_PATTERNS`: (optional) By default, django-yarn will expose all files in `node_modules` to Django as staticfiles. You may not want *all* of them to be exposed. You can pick specific files by adding some additional configuration: 21 | 22 | ```python 23 | YARN_FILE_PATTERNS = { 24 | 'react': ['react.js'], 25 | 'express': ['lib/*.js', 'index.js'] 26 | } 27 | ``` 28 | 29 | Keys are the names of the npm/yarn modules, and values are lists containing strings. The strings match against glob patterns. 30 | 31 | * `YARN_FINDER_USE_CACHE`: (default True) A boolean that enables cache in the finder. If enabled, the file list will be computed only once, when the server is started. 32 | 33 | ## yarn add 34 | 35 | If you want to run `yarn add` programmatically, you can do: 36 | 37 | ```python 38 | from yarn.finders import yarn_add 39 | yarn_add() 40 | ``` 41 | 42 | ## Changelog 43 | 44 | * V1.0.0 - Initial release based on django-npm v1.0.0. 45 | -------------------------------------------------------------------------------- /tests/test_finder.py: -------------------------------------------------------------------------------- 1 | from .util import configure_settings 2 | configure_settings() 3 | 4 | import pytest 5 | 6 | from django.core.files.storage import FileSystemStorage 7 | from django.test.utils import override_settings 8 | 9 | from yarn.finders import get_files 10 | from yarn.finders import YarnFinder 11 | from yarn.finders import yarn_add 12 | 13 | 14 | @pytest.yield_fixture 15 | def yarn_dir(tmpdir): 16 | package_json = tmpdir.join('package.json') 17 | package_json.write('''{ 18 | "name": "test", 19 | "dependencies": {"mocha": "*"} 20 | }''') 21 | with override_settings(YARN_ROOT_PATH=str(tmpdir)): 22 | yarn_add() 23 | yield tmpdir 24 | 25 | 26 | def test_get_files(yarn_dir): 27 | storage = FileSystemStorage(location=str(yarn_dir)) 28 | files = get_files(storage, match_patterns='*') 29 | assert any([True for _ in files]) 30 | 31 | def test_finder_list_all(yarn_dir): 32 | f = YarnFinder() 33 | assert any([True for _ in f.list()]) 34 | 35 | def test_finder_find(yarn_dir): 36 | f = YarnFinder() 37 | file = f.find('mocha/mocha.js') 38 | assert file 39 | 40 | def test_finder_in_subdirectory(yarn_dir): 41 | with override_settings(YARN_STATIC_FILES_PREFIX='lib'): 42 | f = YarnFinder() 43 | assert f.find('lib/mocha/mocha.js') 44 | 45 | def test_finder_with_patterns_in_subdirectory(yarn_dir): 46 | with override_settings(YARN_STATIC_FILES_PREFIX='lib', YARN_FILE_PATTERNS={'mocha': ['*']}): 47 | f = YarnFinder() 48 | assert f.find('lib/mocha/mocha.js') 49 | 50 | def test_finder_with_patterns_in_directory_component(npm_dir): 51 | with override_settings(YARN_STATIC_FILES_PREFIX='lib', YARN_FILE_PATTERNS={'mocha': ['*/*js']}): 52 | f = YarnFinder() 53 | assert f.find('lib/mocha/lib/test.js') 54 | 55 | def test_no_matching_paths_returns_empty_list(npm_dir): 56 | with override_settings(YARN_FILE_PATTERNS={'foo': ['bar']}): 57 | f = YarnFinder() 58 | assert f.find('mocha/mocha.js') == [] 59 | 60 | def test_finder_cache(yarn_dir): 61 | with override_settings(YARN_FINDER_USE_CACHE=True): 62 | f = YarnFinder() 63 | f.list() 64 | assert f.cached_list is not None 65 | assert f.list() is f.cached_list 66 | 67 | def test_finder_no_cache(yarn_dir): 68 | with override_settings(YARN_FINDER_USE_CACHE=False): 69 | f = YarnFinder() 70 | f.list() 71 | assert f.cached_list is None 72 | assert f.list() is not f.cached_list 73 | -------------------------------------------------------------------------------- /yarn/finders.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from fnmatch import fnmatch 4 | 5 | from django.contrib.staticfiles import utils as django_utils 6 | from django.contrib.staticfiles.finders import FileSystemFinder 7 | from django.core.files.storage import FileSystemStorage 8 | from django.conf import settings 9 | 10 | try: 11 | from collections import OrderedDict 12 | except ImportError: 13 | from ordereddict import OrderedDict 14 | 15 | 16 | def yarn_add(): 17 | yarn_executable_path = getattr(settings, 'YARN_EXECUTABLE_PATH', 'yarn') 18 | command = [yarn_executable_path, 'add', '--prefix=' + get_yarn_root_path()] 19 | proc = subprocess.Popen( 20 | command, 21 | env={'PATH': os.environ.get('PATH')}, 22 | ) 23 | proc.wait() 24 | 25 | 26 | def get_yarn_root_path(): 27 | return getattr(settings, 'YARN_ROOT_PATH', '.') 28 | 29 | 30 | def flatten_patterns(patterns): 31 | if patterns is None: 32 | return None 33 | return [ 34 | os.path.join(module, module_pattern) 35 | for module, module_patterns in patterns.items() 36 | for module_pattern in module_patterns 37 | ] 38 | 39 | 40 | def fnmatch_sub(directory, pattern): 41 | """ 42 | Match a directory against a potentially longer pattern containing 43 | wildcards in the path components. fnmatch does the globbing, but there 44 | appears to be no built-in way to match only the beginning of a pattern. 45 | """ 46 | length = len(directory.split(os.sep)) 47 | components = pattern.split(os.sep)[:length] 48 | return fnmatch(directory, os.sep.join(components)) 49 | 50 | 51 | def may_contain_match(directory, patterns): 52 | return any(fnmatch_sub(directory, pattern) for pattern in patterns) 53 | 54 | 55 | def get_files(storage, match_patterns='*', ignore_patterns=None, location=''): 56 | if ignore_patterns is None: 57 | ignore_patterns = [] 58 | if match_patterns is None: 59 | match_patterns = [] 60 | 61 | if not os.path.isdir(storage.path(location)): 62 | return 63 | 64 | directories, files = storage.listdir(location) 65 | for fn in files: 66 | if django_utils.matches_patterns(fn, ignore_patterns): 67 | continue 68 | if location: 69 | fn = os.path.join(location, fn) 70 | if not django_utils.matches_patterns(fn, match_patterns): 71 | continue 72 | yield fn 73 | for dir in directories: 74 | if django_utils.matches_patterns(dir, ignore_patterns): 75 | continue 76 | if location: 77 | dir = os.path.join(location, dir) 78 | if may_contain_match(dir, match_patterns) or django_utils.matches_patterns(dir, match_patterns): 79 | for fn in get_files(storage, match_patterns, ignore_patterns, dir): 80 | yield fn 81 | 82 | 83 | class YarnFinder(FileSystemFinder): 84 | def __init__(self, apps=None, *args, **kwargs): 85 | self.node_modules_path = get_yarn_root_path() 86 | self.destination = getattr(settings, 'YARN_STATIC_FILES_PREFIX', '') 87 | self.cache_enabled = getattr(settings, 'YARN_FINDER_USE_CACHE', True) 88 | self.cached_list = None 89 | 90 | self.match_patterns = flatten_patterns(getattr(settings, 'YARN_FILE_PATTERNS', None)) or ['*'] 91 | self.locations = [(self.destination, os.path.join(self.node_modules_path, 'node_modules'))] 92 | self.storages = OrderedDict() 93 | 94 | filesystem_storage = FileSystemStorage(location=self.locations[0][1]) 95 | filesystem_storage.prefix = self.locations[0][0] 96 | self.storages[self.locations[0][1]] = filesystem_storage 97 | 98 | def find(self, path, all=False): 99 | relpath = os.path.relpath(path, self.destination) 100 | if not django_utils.matches_patterns(relpath, self.match_patterns): 101 | return [] 102 | return super(YarnFinder, self).find(path, all=all) 103 | 104 | def list(self, ignore_patterns=None): # TODO should be configurable, add setting 105 | """List all files in all locations.""" 106 | if self.cache_enabled: 107 | if self.cached_list is None: 108 | self.cached_list = list(self._make_list_generator(ignore_patterns)) 109 | return self.cached_list 110 | return self._make_list_generator(ignore_patterns) 111 | 112 | def _make_list_generator(self, ignore_patterns=None): 113 | for prefix, root in self.locations: 114 | storage = self.storages[root] 115 | for path in get_files(storage, self.match_patterns, ignore_patterns): 116 | yield path, storage 117 | --------------------------------------------------------------------------------