├── .coveragerc ├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── Changelog.rst ├── LICENSE.txt ├── README.rst ├── bin └── trace-deps.js ├── docs ├── Makefile ├── _ext │ └── djangodocs.py ├── _theme │ └── djangodocs │ │ ├── static │ │ ├── default.css │ │ └── djangodocs.css │ │ └── theme.conf ├── conf.py ├── config.rst ├── contributing.rst ├── index.rst ├── installation.rst ├── make.bat └── usage.rst ├── example └── jspm_0_17 │ ├── .gitignore │ ├── README.md │ ├── frontend.sh │ ├── gulpfile.js │ ├── jspm_0_17 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ ├── manage.py │ ├── myapp │ ├── __init__.py │ ├── static │ │ └── js │ │ │ └── myapp │ │ │ └── app.js │ └── templates │ │ └── base.html │ ├── package.json │ ├── requirements.txt │ ├── static │ ├── js │ │ └── main.js │ ├── jspm.browser.js │ └── jspm.config.js │ └── staticfiles │ └── .gitkeep ├── runtests.py ├── setup.cfg ├── setup.py ├── systemjs ├── __init__.py ├── apps.py ├── base.py ├── compat.py ├── conf.py ├── finders.py ├── jspm.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── _mixins.py │ │ ├── systemjs_bundle.py │ │ ├── systemjs_show_packages.py │ │ └── systemjs_write_depcaches.py ├── storage.py └── templatetags │ ├── __init__.py │ └── system_tags.py ├── tests ├── __init__.py ├── app │ ├── __init__.py │ ├── jinja2 │ │ └── base.html │ ├── static │ │ └── app │ │ │ ├── dependency.js │ │ │ └── dummy.js │ └── templates │ │ ├── base.html │ │ ├── ignore_me.txt │ │ └── jinja.html ├── manage.py ├── package.json ├── settings.py ├── static │ ├── jspm_packages │ │ └── system.js │ └── node_modules │ │ └── system.js ├── tests │ ├── __init__.py │ ├── files │ │ ├── jspm_nested_package.json │ │ ├── package.json │ │ └── simple_package.json │ ├── helpers.py │ ├── static1 │ │ └── dummy2.js │ ├── templates1 │ │ └── same_bundle.html │ ├── templates2 │ │ └── extra.html │ ├── test_bundle.py │ ├── test_finders.py │ ├── test_management.py │ ├── test_package_json.py │ ├── test_templatetags.py │ └── test_tracing.py └── urls.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = systemjs 4 | 5 | [report] 6 | omit = */migrations/*,*/tests/* 7 | exclude_lines = 8 | pragma: no cover 9 | noqa 10 | 11 | [html] 12 | directory = cover 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__ 3 | 4 | # dist 5 | /dist 6 | .eggs 7 | *.egg-info 8 | /build 9 | 10 | # tests 11 | .tox 12 | .coverage 13 | cover 14 | /tests/static 15 | !/tests/static/jspm_packages/* 16 | 17 | # docs 18 | docs/_build 19 | docs/_static 20 | docs/_templates 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: 3 | - pip install coverage codecov coveralls tox 4 | addons: 5 | apt: 6 | sources: 7 | - deadsnakes 8 | packages: 9 | - python3.5 10 | script: 11 | - tox 12 | after_success: 13 | - coveralls 14 | - codecov 15 | env: 16 | - TOXENV=py27-django18 17 | - TOXENV=py27-django19 18 | - TOXENV=py27-django110 19 | 20 | - TOXENV=pypy-django18 21 | - TOXENV=pypy-django19 22 | - TOXENV=pypy-django110 23 | 24 | - TOXENV=py33-django18 25 | 26 | - TOXENV=py34-django18 27 | - TOXENV=py34-django19 28 | - TOXENV=py34-django110 29 | 30 | - TOXENV=py35-django18 31 | - TOXENV=py35-django19 32 | - TOXENV=py35-django110 33 | 34 | - TOXENV=docs 35 | 36 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/CONTRIBUTORS -------------------------------------------------------------------------------- /Changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.4.3 5 | ----- 6 | 7 | Added the ``--skip-source-maps`` option to the bundle command. (by `@funn`_) 8 | 9 | .. _@funn: https://github.com/funn 10 | 11 | 12 | 1.4.2 13 | ----- 14 | * Added a fix for the ``--sfx`` option on jspm 0.17 (thanks @funn) 15 | 16 | 1.4 17 | --- 18 | 19 | * ``--template``, ``-t`` option added to management commands. These limit the 20 | templates that are parsed for ``{% systemjs_import ... %}`` tags. 21 | 22 | * Added ``--minimal`` option to ``systemjs_bundle`` management command. This will 23 | rebundle an app/package only when something has changed in the dependency tree. 24 | Usefull when you have many bundles and only a small number changed. 25 | 26 | * Added two new management commands: 27 | 28 | - ``systemjs_show_packages``: lists all the packages imported in templates 29 | - ``systemjs_write_depcaches``: writes out the dependency tree for each package 30 | 31 | 1.3.3 32 | ----- 33 | 34 | * Optimization: build a bundle only once. Before, if a bundle was used in 35 | multiple templates, it would be bundled again for every appearance. Thanks to 36 | **pcompassion** for the report (`#15`_). 37 | 38 | .. _#15: https://github.com/sergei-maertens/django-systemjs/issues/15 39 | 40 | 1.3.2 41 | ----- 42 | 43 | * let the finder also serve ``SYSTEMJS_OUTPUT_DIR``, next to the ``jspm_packages`` 44 | folder 45 | 46 | 1.3.1 47 | ----- 48 | 49 | * Fixed a url path splitting bug 50 | 51 | 1.3.0 52 | ----- 53 | * Fixes in the documentation 54 | * Added the ``--minify`` option for bundling 55 | 56 | 1.2.0 57 | ----- 58 | 59 | * Added a staticifles finder: ``systemjs.finders.SystemFinder``. Thanks to 60 | https://github.com/wkevina for the report that lead to this. 61 | 62 | * Added proper documentation on readthedocs.org 63 | 64 | 65 | 1.1.1 66 | ----- 67 | 68 | Fixes loading of the configuration options in tests. 69 | 70 | In previous releases, using ``django.test.utils.override_settings`` in certain 71 | ways happened before the django-systemjs settings were appended. This was 72 | probably only the case for Django < 1.9. 73 | 74 | 75 | 1.1.0 76 | ----- 77 | Small feature release with improvements: 78 | 79 | * Added ``systemjs.storage.SystemJSManifestStaticFilesStorage`` 80 | staticfiles storage backend. **It is required if you want to use the 81 | ``django.contrib.staticfiles.storage.ManifestStaticFilesStorage`` with 82 | django-systemjs.** 83 | 84 | django-systemjs runs the post-process hook, which in turn calls 85 | ``save_manifest``. The Django shipped version then deletes the old manifest, 86 | but we don't know anything about the staticfiles in django-systemjs. This 87 | storage backend makes sure to not delete the staticfiles manifest, it only 88 | adds to it. 89 | 90 | * Better treatment of ``sourceMap`` comments. 91 | 92 | JSPM creates the sourcemap and adds a ``// sourceMap=...`` comment to the 93 | generated bundle. django-systemjs added the ``System.import(...)`` statement 94 | to this bundle, causing the sourcemap comment to not be the last line of the 95 | file. This is fixed in this release. 96 | 97 | 1.0.2 98 | ----- 99 | Too soon on 1.0.1, missed another extension append case. 100 | 101 | 1.0.1 102 | ----- 103 | Bugfix related to ``DEFAULT_JS_EXTENSIONS``: when bundling a file that has the 104 | extension, it would be added again, leading to duplicate extensions. 105 | 106 | 1.0 107 | --- 108 | Added DEFAULT_JS_EXTENSIONS setting, as present in ``jspm``. 109 | Added Django 1.9 to the build matrix. 110 | 111 | 0.3.4 112 | ----- 113 | Fixes some mistakes from 0.3.3. Mondays... 114 | 115 | 0.3.3 116 | ----- 117 | Added post-processing of ``jspm_packages/system.js`` if it is within 118 | ``settings.STATIC_ROOT``. 119 | 120 | This introduces a new setting: ``SYSTEMJS_PACKAGE_JSON_DIR``. Set this to the path 121 | of the folder containing ``package.json``. ``package.json`` will be inspected to 122 | figure out the install path of jspm - and only post-process if the latter path 123 | is within ``settings.STATIC_ROOT``. By default, ``settings.BASE_DIR`` (provided by 124 | the default ``django-admin.py startproject`` template) will be chosen for this 125 | directory. 126 | 127 | 0.3.2 128 | ----- 129 | Fixed incorrect passing of the --log option. 130 | 131 | 0.3.1 132 | ----- 133 | 134 | No API changes where made - this release is preparation for Django 1.9 and 135 | improves robustness of bundling. 136 | 137 | In version 0.16.3 ``--log`` was added to JSPM. This allows us to properly check 138 | for build errors, where before you would find out the hard way. Build errors 139 | will now show as output from the management command. 140 | 141 | An check has been added to figure out the JSPM version. If you get a 142 | ``BundleError`` saying that the JSPM version could not be determined, check that 143 | ``jspm`` is available in your path or modify the ``SYSTEMJS_JSPM_EXECUTABLE`` 144 | setting accordingly. 145 | 146 | 147 | 0.3.0 148 | ----- 149 | 150 | Changed the templatetag to add the .js extension. Existing code should still 151 | work, possibly you need to set System.defaultJSExtensions to ``true``. 152 | 153 | 154 | .. note:: 155 | 156 | Possibly breaking change! This release is required if you use SystemJS >= 157 | 0.17. The default.js extension is no longer added by SystemJS. 158 | 159 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Sergei Maertens 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Django SystemJS 3 | ===================== 4 | 5 | .. image:: https://travis-ci.org/sergei-maertens/django-systemjs.svg?branch=master 6 | :target: https://travis-ci.org/sergei-maertens/django-systemjs 7 | 8 | 9 | .. image:: https://coveralls.io/repos/sergei-maertens/django-systemjs/badge.svg 10 | :target: https://coveralls.io/r/sergei-maertens/django-systemjs 11 | 12 | .. image:: https://img.shields.io/pypi/v/django-systemjs.svg 13 | :target: https://pypi.python.org/pypi/django-systemjs 14 | 15 | 16 | .. image:: https://readthedocs.org/projects/django-systemjs/badge/?version=latest 17 | :target: http://django-systemjs.readthedocs.org/en/latest/?badge=latest 18 | :alt: Documentation Status 19 | 20 | 21 | Django SystemJS brings the Javascript of tomorrow to Django, today. 22 | 23 | It leverages JSPM (https://jspm.io) to do the heavy lifting for your 24 | client side code, while keeping development flow easy and deployment 25 | without worries. In DEBUG mode, your Javascript modules are loaded 26 | asynchronously. In production, your app is nicely bundled via JSPM 27 | and ties in perfectly with `django.contrib.staticfiles`. 28 | 29 | 30 | Installation 31 | ============ 32 | 33 | Follow the documentation on http://django-systemjs.readthedocs.org/en/latest/. 34 | -------------------------------------------------------------------------------- /bin/trace-deps.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Utility script to leverage jpsm/SystemJS' power to build the depcache 5 | * for a given package. 6 | * 7 | * This traces the module and extracts the relative paths to all the 8 | * files used. 9 | */ 10 | 11 | var fs = require('fs'); 12 | var Builder = require('jspm').Builder; 13 | 14 | 15 | var jsapp = process.argv[2]; // 0 is node, 1 is the file 16 | 17 | 18 | var builder = new Builder(); 19 | builder.trace(jsapp).then(function(trace) { 20 | var deps = {} 21 | for( var jsapp in trace ) { 22 | var skip = trace[jsapp] === false; // e.g. with google maps, see #13 23 | deps[jsapp] = { 24 | name: jsapp, 25 | timestamp: skip ? null : trace[jsapp].timestamp, 26 | path: skip ? null : trace[jsapp].path, 27 | skip: skip 28 | }; 29 | } 30 | process.stdout.write(JSON.stringify(deps, null)); 31 | }); 32 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-Systemjs.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-Systemjs.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Django-Systemjs" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-Systemjs" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/_ext/djangodocs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Taken from djangoproject/django docs. 3 | 4 | Sphinx plugins for Django documentation. 5 | """ 6 | import re 7 | 8 | from sphinx import addnodes 9 | from sphinx.util.compat import Directive 10 | from sphinx.writers.html import SmartyPantsHTMLTranslator 11 | 12 | # RE for option descriptions without a '--' prefix 13 | simple_option_desc_re = re.compile( 14 | r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') 15 | 16 | 17 | def setup(app): 18 | app.add_directive('versionadded', VersionDirective) 19 | app.add_directive('versionchanged', VersionDirective) 20 | app.set_translator('djangohtml', DjangoHTMLTranslator) 21 | return {'parallel_read_safe': True} 22 | 23 | 24 | class VersionDirective(Directive): 25 | has_content = True 26 | required_arguments = 1 27 | optional_arguments = 1 28 | final_argument_whitespace = True 29 | option_spec = {} 30 | 31 | def run(self): 32 | if len(self.arguments) > 1: 33 | msg = """Only one argument accepted for directive '{directive_name}::'. 34 | Comments should be provided as content, 35 | not as an extra argument.""".format(directive_name=self.name) 36 | raise self.error(msg) 37 | 38 | env = self.state.document.settings.env 39 | ret = [] 40 | node = addnodes.versionmodified() 41 | ret.append(node) 42 | node['version'] = self.arguments[0] 43 | node['type'] = self.name 44 | if self.content: 45 | self.state.nested_parse(self.content, self.content_offset, node) 46 | env.note_versionchange(node['type'], node['version'], node, self.lineno) 47 | return ret 48 | 49 | 50 | class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): 51 | 52 | version_text = { 53 | 'versionchanged': 'Changed in %s', 54 | 'versionadded': 'Added in %s', 55 | } 56 | 57 | def visit_versionmodified(self, node): 58 | self.body.append( 59 | self.starttag(node, 'div', CLASS=node['type']) 60 | ) 61 | version_text = self.version_text.get(node['type']) 62 | if version_text: 63 | title = "%s%s" % ( 64 | version_text % node['version'], 65 | ":" if len(node) else "." 66 | ) 67 | self.body.append('%s ' % title) 68 | 69 | def depart_versionmodified(self, node): 70 | self.body.append("\n") 71 | -------------------------------------------------------------------------------- /docs/_theme/djangodocs/static/default.css: -------------------------------------------------------------------------------- 1 | @import url(djangodocs.css); 2 | -------------------------------------------------------------------------------- /docs/_theme/djangodocs/static/djangodocs.css: -------------------------------------------------------------------------------- 1 | /*** versionadded/changes ***/ 2 | div.versionadded, div.versionchanged { } 3 | div.versionadded span.title, div.versionchanged span.title, span.versionmodified { font-weight: bold; } 4 | div.versionadded, div.versionchanged, div.deprecated { color:#555; } 5 | -------------------------------------------------------------------------------- /docs/_theme/djangodocs/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = sphinx_rtd_theme 3 | # stylesheet = default.css 4 | # pygments_style = trac 5 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Django-Systemjs documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Sep 10 22:03:38 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | from os.path import abspath, join, dirname 19 | 20 | sys.path.append(abspath(join(dirname(__file__), "_ext"))) 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | #sys.path.insert(0, os.path.abspath('.')) 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | #needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "djangodocs" 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = u'Django-Systemjs' 55 | copyright = u'2015-2016, Sergei Maertens' 56 | author = u'Sergei Maertens' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = '1.4' 64 | # The full version, including alpha/beta/rc tags. 65 | release = '1.4.1' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ['_build'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | #keep_warnings = False 107 | 108 | suppress_warnings = ['image.nonlocal_uri', 'app.add_directive'] 109 | 110 | # If true, `todo` and `todoList` produce output, else they produce nothing. 111 | todo_include_todos = False 112 | 113 | 114 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | # html_theme = 'djangodocs' 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | #html_theme_options = {} 124 | 125 | # Add any paths that contain custom themes here, relative to this directory. 126 | # html_theme_path = ["_theme"] 127 | 128 | # The name for this set of Sphinx documents. If None, it defaults to 129 | # " v documentation". 130 | #html_title = None 131 | 132 | # A shorter title for the navigation bar. Default is the same as html_title. 133 | #html_short_title = None 134 | 135 | # The name of an image file (relative to this directory) to place at the top 136 | # of the sidebar. 137 | #html_logo = None 138 | 139 | # The name of an image file (within the static path) to use as favicon of the 140 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 141 | # pixels large. 142 | #html_favicon = None 143 | 144 | # Add any paths that contain custom static files (such as style sheets) here, 145 | # relative to this directory. They are copied after the builtin static files, 146 | # so a file named "default.css" will overwrite the builtin "default.css". 147 | # html_static_path = ['_static'] 148 | 149 | # Add any extra paths that contain custom files (such as robots.txt or 150 | # .htaccess) here, relative to this directory. These files are copied 151 | # directly to the root of the documentation. 152 | #html_extra_path = [] 153 | 154 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 155 | # using the given strftime format. 156 | #html_last_updated_fmt = '%b %d, %Y' 157 | 158 | # If true, SmartyPants will be used to convert quotes and dashes to 159 | # typographically correct entities. 160 | html_use_smartypants = True 161 | 162 | # Custom sidebar templates, maps document names to template names. 163 | #html_sidebars = {} 164 | 165 | # Additional templates that should be rendered to pages, maps page names to 166 | # template names. 167 | #html_additional_pages = {} 168 | 169 | # If false, no module index is generated. 170 | #html_domain_indices = True 171 | 172 | # If false, no index is generated. 173 | #html_use_index = True 174 | 175 | # If true, the index is split into individual pages for each letter. 176 | #html_split_index = False 177 | 178 | # If true, links to the reST sources are added to the pages. 179 | #html_show_sourcelink = True 180 | 181 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 182 | #html_show_sphinx = True 183 | 184 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 185 | #html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages will 188 | # contain a tag referring to it. The value of this option must be the 189 | # base URL from which the finished HTML is served. 190 | #html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | #html_file_suffix = None 194 | 195 | # Language to be used for generating the HTML full-text search index. 196 | # Sphinx supports the following languages: 197 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 198 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 199 | #html_search_language = 'en' 200 | 201 | # A dictionary with options for the search language support, empty by default. 202 | # Now only 'ja' uses this config value 203 | #html_search_options = {'type': 'default'} 204 | 205 | # The name of a javascript file (relative to the configuration directory) that 206 | # implements a search results scorer. If empty, the default will be used. 207 | #html_search_scorer = 'scorer.js' 208 | 209 | # Output file base name for HTML help builder. 210 | htmlhelp_basename = 'Django-Systemjsdoc' 211 | 212 | # -- Options for LaTeX output --------------------------------------------- 213 | 214 | latex_elements = { 215 | # The paper size ('letterpaper' or 'a4paper'). 216 | #'papersize': 'letterpaper', 217 | 218 | # The font size ('10pt', '11pt' or '12pt'). 219 | #'pointsize': '10pt', 220 | 221 | # Additional stuff for the LaTeX preamble. 222 | #'preamble': '', 223 | 224 | # Latex figure (float) alignment 225 | #'figure_align': 'htbp', 226 | } 227 | 228 | # Grouping the document tree into LaTeX files. List of tuples 229 | # (source start file, target name, title, 230 | # author, documentclass [howto, manual, or own class]). 231 | latex_documents = [ 232 | (master_doc, 'Django-Systemjs.tex', u'Django-Systemjs Documentation', 233 | u'Sergei Maertens', 'manual'), 234 | ] 235 | 236 | # The name of an image file (relative to this directory) to place at the top of 237 | # the title page. 238 | #latex_logo = None 239 | 240 | # For "manual" documents, if this is true, then toplevel headings are parts, 241 | # not chapters. 242 | #latex_use_parts = False 243 | 244 | # If true, show page references after internal links. 245 | #latex_show_pagerefs = False 246 | 247 | # If true, show URL addresses after external links. 248 | #latex_show_urls = False 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #latex_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #latex_domain_indices = True 255 | 256 | 257 | # -- Options for manual page output --------------------------------------- 258 | 259 | # One entry per manual page. List of tuples 260 | # (source start file, name, description, authors, manual section). 261 | man_pages = [ 262 | (master_doc, 'Django-Systemjs', u'Django-Systemjs Documentation', 263 | [author], 1) 264 | ] 265 | 266 | # If true, show URL addresses after external links. 267 | #man_show_urls = False 268 | 269 | 270 | # -- Options for Texinfo output ------------------------------------------- 271 | 272 | # Grouping the document tree into Texinfo files. List of tuples 273 | # (source start file, target name, title, author, 274 | # dir menu entry, description, category) 275 | texinfo_documents = [ 276 | (master_doc, 'Django-Systemjs', u'Django-Systemjs Documentation', 277 | author, 'Django-Systemjs', 'One line description of project.', 278 | 'Miscellaneous'), 279 | ] 280 | 281 | # Documents to append as an appendix to all manuals. 282 | #texinfo_appendices = [] 283 | 284 | # If false, no module index is generated. 285 | #texinfo_domain_indices = True 286 | 287 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 288 | #texinfo_show_urls = 'footnote' 289 | 290 | # If true, do not generate a @detailmenu in the "Top" node's menu. 291 | #texinfo_no_detailmenu = False 292 | 293 | 294 | # on_rtd is whether we are on readthedocs.org 295 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 296 | 297 | if not on_rtd: # only import and set the theme if we're building docs locally 298 | import sphinx_rtd_theme 299 | html_theme = 'sphinx_rtd_theme' 300 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 301 | # html_theme = 'djangodocs' 302 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + html_theme_path 303 | 304 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 305 | -------------------------------------------------------------------------------- /docs/config.rst: -------------------------------------------------------------------------------- 1 | .. _available-settings: 2 | 3 | ================== 4 | Available settings 5 | ================== 6 | 7 | ``SYSTEMJS_ENABLED``: defaults to ``not settings.DEBUG``. If disabled, the loading 8 | of modules will happen in the 'standard' jspm way (transpiling in browser). 9 | 10 | ``SYSTEMJS_JSPM_EXECUTABLE``: path to the ``jspm-cli`` executable. Defaults to 11 | ``jspm``, which should be available if installed globally with ``npm``. 12 | 13 | ``SYSTEMJS_OUTPUT_DIR``: name of the subdirectory within ``settings.STATIC_ROOT``. 14 | Bundled files will end up in this directory, and this is the place the 15 | templatetag will point static files to. 16 | 17 | ``SYSTEMJS_PACKAGE_JSON_DIR``: directory containing your ``package.json`` file. 18 | This is automatically guessed from ``BASE_DIR``. You will get an error in the 19 | shell if you need to set it yourself. 20 | 21 | ``SYSTEMJS_DEFAULT_JS_EXTENSIONS``: in prior verions of jspm, the ``.js`` extension 22 | for imports was optional. This is being phased out, and matches the 23 | ``defaultJSExtensions`` settings in ``config.js``. 24 | 25 | ``SYSTEMJS_CACHE_DIR``: directory to keep the dependency cache (when generating 26 | :ref:`minimal ` bundles) 27 | 28 | ``SYSTEMJS_SERVER_URL``: if you're using a frontend asset-server and want to use 29 | that instead of letting Django serve the modules, specify the url with this 30 | settings. Defaults to ``None``. Example: ``http://localhost:3000/assets/``. 31 | 32 | 33 | Environment variables 34 | ===================== 35 | 36 | ``NODE_PATH`` 37 | ------------- 38 | 39 | When generating :ref:`minimal ` bundles, a NodeJS script 40 | (``trace-deps.js``) is called. This script needs to be called from the directory 41 | containing ``package.json``. If Django-SystemJS cannot figure out this directory 42 | by itself, you may need to set the environment variable: 43 | 44 | .. code-block:: sh 45 | 46 | export NODE_PATH=/path/to/project/node_modules 47 | 48 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | To get up and running quickly, fork the github repository and make all 8 | your changes in your local clone. 9 | 10 | Git-flow is prefered as git workflow, but as long as you make pull requests 11 | against the ``develop`` branch, all should be well. Pull requests should 12 | always have tests, and if relevant, documentation updates. 13 | 14 | Feel free to create unfinished pull-requests to get the tests to build 15 | and get work going, someone else might always want to pick up the tests 16 | and/or documentation. 17 | 18 | Add yourself to the CONTRIBUTORS file if you want, we want you to be proud of 19 | your work! 20 | 21 | 22 | Testing 23 | ======= 24 | 25 | Django's testcases are used to run the tests. 26 | 27 | To run the tests in your (virtual) environment, simple execute 28 | 29 | .. code-block:: sh 30 | 31 | python runtests.py 32 | 33 | This will run the tests with the current python version and Django version 34 | installed in your virtual environment. 35 | 36 | To run the tests on all supported python/Django versions, use tox_. 37 | 38 | .. code-block:: sh 39 | 40 | pip install tox 41 | tox 42 | 43 | If you want to speed this up, you can also use detox_. This library will 44 | run as much in parallel as possible. 45 | 46 | 47 | Documentation 48 | ============= 49 | 50 | The documentation is built with Sphinx. Run `make` to build the documentation: 51 | 52 | .. code-block: sh 53 | 54 | cd docs/ 55 | make html 56 | 57 | You can now open `_build/index.html`. 58 | 59 | 60 | Coding style 61 | ============ 62 | 63 | Please stick to PEP8, and use pylint or similar tools to check the code style. Also sort your imports, you may use ``isort`` 64 | for this. In general, we adhere to Django's coding style. 65 | 66 | 67 | .. _tox: https://testrun.org/tox/latest/ 68 | .. _detox: https://pypi.python.org/pypi/detox/ 69 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django-Systemjs documentation master file, created by 2 | sphinx-quickstart on Thu Apr 7 22:03:38 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | =============== 8 | Django-SystemJS 9 | =============== 10 | 11 | .. rubric:: Modern Javascript in Django. 12 | 13 | .. image:: https://travis-ci.org/sergei-maertens/django-systemjs.png 14 | :target: http://travis-ci.org/sergei-maertens/django-systemjs 15 | 16 | .. image:: https://coveralls.io/repos/sergei-maertens/django-systemjs/badge.svg 17 | :target: https://coveralls.io/github/sergei-maertens/django-systemjs?branch=master 18 | 19 | .. image:: https://img.shields.io/pypi/v/django-systemjs.svg 20 | :target: https://pypi.python.org/pypi/django-systemjs 21 | 22 | 23 | Overview 24 | ======== 25 | 26 | Django SystemJS brings the Javascript of tomorrow to Django, today. 27 | 28 | It leverages JSPM (https://jspm.io) to do the heavy lifting for your 29 | client side code, while keeping development flow easy and deployment 30 | without worries. In DEBUG mode, your Javascript modules are loaded 31 | asynchronously. In production, your app is nicely bundled via JSPM 32 | and ties in perfectly with ``django.contrib.staticfiles``. 33 | 34 | 35 | Requirements 36 | ============ 37 | 38 | Django-SystemJS runs on Python 2.7, 3.3, 3.4 and 3.5. Django versions 1.8, 1.9 and 39 | 1.10 are supported. 40 | 41 | 42 | 43 | Contents 44 | ======== 45 | 46 | .. toctree:: 47 | :maxdepth: 2 48 | 49 | installation 50 | usage 51 | config 52 | contributing 53 | 54 | 55 | License 56 | ======= 57 | 58 | Licensed under the `MIT License`_. 59 | 60 | 61 | Souce Code and contributing 62 | =========================== 63 | 64 | The source code can be found on github_. 65 | 66 | Bugs can also be reported on the github_ repository, and pull requests 67 | are welcome. See :ref:`contributing` for more details. 68 | 69 | 70 | Indices and tables 71 | ================== 72 | 73 | * :ref:`genindex` 74 | * :ref:`modindex` 75 | * :ref:`search` 76 | 77 | 78 | .. _django: http://www.djangoproject.com/ 79 | .. _MIT License: http://en.wikipedia.org/wiki/MIT_License 80 | .. _github: https://github.com/sergei-maertens/django-systemjs 81 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | Django 6 | ====== 7 | 8 | Install by running: 9 | 10 | .. code-block:: sh 11 | 12 | pip install django-systemjs 13 | 14 | Then, add ``systemjs`` to your ``settings.INSTALLED_APPS``, and add the custom 15 | staticfiles finder: 16 | 17 | .. code-block:: python 18 | 19 | STATICFILES_FINDERS = [ 20 | 'django.contrib.staticfiles.finders.FileSystemFinder', 21 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 22 | 'systemjs.finders.SystemFinder', 23 | ] 24 | 25 | The custom finder looks up files in ``STATIC_ROOT`` directly. It is not needed 26 | if you use ``django-compressor`` and have ``COMPRESS_ROOT`` set to 27 | ``STATIC_ROOT`` (which is the default). 28 | 29 | If you want to use 30 | ``django.contrib.staticfiles.storage.ManifestStaticFilesStorage`` as 31 | staticfiles storage backend, you need to use the systemjs-version: 32 | ``systemjs.storage.SystemJSManifestStaticFilesStorage``. This storage ensures 33 | two things: 34 | 35 | * during bundling the collected staticfiles (from ``collectstatic``) aren't 36 | removed from the manifest file. 37 | * during collectstatic the bundled files are kept in the manifest. 38 | 39 | There are some :ref:`application settings ` to customize 40 | behaviour. Usually you shouldn't need to change these, since they have sane 41 | defaults. 42 | 43 | 44 | JSPM 45 | ==== 46 | 47 | Since you found this django app, you probably already know what jspm is and how 48 | to install it. There is some django-specific configuration to be done to make 49 | everything play nicely together. 50 | 51 | .. note:: 52 | Currently there are two jspm branches active: 0.16.x and 0.17.x (beta). The 53 | beta has some backwards incompatibilities with 0.16, and most of all, some 54 | great features to speed up development. In general, I recommend using 0.17.x, 55 | even more so if you're going to do React.js stuff. 56 | 57 | Setting up jspm for the first time 58 | ---------------------------------- 59 | 60 | If you have worked with jspm before, you can skip this. 61 | 62 | To install ``jspm`` (http://jspm.io), you'll need some front-end tooling. 63 | Make sure you have ``nodejs`` and ``npm`` installed on your system. See the 64 | `nodejs installation instructions`_. 65 | 66 | If you never installed ``jspm`` before, install it globally for the first time: 67 | 68 | .. code-block:: sh 69 | 70 | sudo npm install -g jspm 71 | 72 | This ensures that the ``jspm`` cli is available in your ``$PATH``. 73 | 74 | .. _nodejs installation instructions: https://nodejs.org/en/download/package-manager/ 75 | 76 | 77 | jspm 0.17.x (beta) instructions 78 | ------------------------------- 79 | 80 | There is an `example project`_ with detailed installation and configuration 81 | information, aimed at the 0.17 beta version of jspm. The README.md contains a 82 | step-by-step history to get to a working setup with the standard Django project 83 | layout. 84 | 85 | .. _example project: https://github.com/sergei-maertens/django-systemjs/tree/develop/example/jspm_0_17 86 | 87 | 88 | jspm 0.16.x (stable) instructions 89 | --------------------------------- 90 | 91 | jspm uses the ``package.json`` from NodeJS, so get that set-up first: 92 | 93 | .. code-block:: sh 94 | 95 | npm init 96 | 97 | This will bring up an interactive prompt to ask for some package information. 98 | 99 | Next, install ``jspm`` locally in your project, and pin its version: 100 | 101 | .. code-block:: sh 102 | 103 | npm install --save-dev jspm 104 | 105 | It's now time to initialize your ``jspm`` project. This is an interactive prompt 106 | again, but we'll need to deviate from the defaults a bit to make it play nice 107 | with Django. 108 | 109 | .. code-block:: sh 110 | 111 | jspm init 112 | 113 | Would you like jspm to prefix the jspm package.json properties under jspm? [yes]: yes # easier to keep track of jspm-specific settings/packages 114 | 115 | Enter server baseURL (public folder path) [/]: static # same as settings.STATIC_ROOT, relative to package.json 116 | 117 | Enter jspm packages folder [static/jspm_packages]: # keep it within settings.STATIC_ROOT 118 | 119 | Enter config file path [static/config.js]: my-project/static/config.js # must be kept in version control, so somewhere where collectstatic can find it 120 | 121 | Enter client baseURL (public folder URL) [/]: /static/ # set to settings.STATIC_URL 122 | 123 | Do you wish to use a transpiler? [yes]: # current browsers don't have full support for ES6 yet 124 | 125 | Which ES6 transpiler would you like to use, Traceur or Babel? [traceur]: babel # better tracebacks 126 | 127 | 128 | Take some time to read the `jspm docs`_ if you're not familiar with it yet. 129 | 130 | .. note:: 131 | A few settings are remarkable. We put ``jspm_packages`` in ``settings.STATIC_ROOT``. 132 | This means that collectstatic will not post-process the files in here, which 133 | can be a problem. Django-SystemJS deals with this specific use case as it is 134 | intended for ``jspm``-users. There is an inherent limitation within jspm 135 | which should be lifted with the 0.18 release. 136 | 137 | .. _jspm docs: https://github.com/jspm/jspm-cli/tree/master/docs 138 | 139 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Django-Systemjs.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Django-Systemjs.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | 6 | Template tag 7 | ============ 8 | 9 | Usually, in your template you would write something like: 10 | 11 | .. code-block:: html 12 | 13 | 14 | 15 | 16 | 17 | 18 | With Django SystemJS you can replace this with: 19 | 20 | .. code-block:: django 21 | 22 | {% load staticfiles system_tags %} 23 | 24 | 25 | 26 | {% systemjs_import 'my/awesome/app.js' %} 27 | 28 | 29 | In development mode (``DEBUG = True``), the tag will output the previous 30 | ``System.import('my/awesome/app.js');`` statement. In production mode, it will 31 | output: 32 | 33 | .. code-block:: html 34 | 35 | 36 | 37 | This url is generated by the configured static files backend, so if you 38 | use the ``CachedStaticFilesStorage``, you will get the hashed path: 39 | 40 | .. code-block:: html 41 | 42 | 43 | 44 | 45 | .. note:: 46 | django-storages(-redux) and S3 is untested. If you run into any issues, 47 | please raise an issue on Github. 48 | 49 | .. versionadded:: 1.1 50 | 51 | If you want to use ``django.contrib.staticfiles.storage.ManifestStaticFilesStorage``, 52 | you need to use the systemjs-version: 53 | ``systemjs.storage.SystemJSManifestStaticFilesStorage``. This storage ensures 54 | that during bundling the collected staticfiles (from ``collectstatic``) 55 | aren't removed from the manifest file. 56 | 57 | 58 | The tag accepts any number of (custom) attributes you may want to add, making it 59 | possible to load scripts async, or specify html ids for example. 60 | 61 | .. code-block:: django 62 | 63 | {% load system_tags %} 64 | {% systemjs_import 'my/awesome/app.js' async id="my-awesome-app" %} 65 | 66 | this will output (in production mode) 67 | 68 | .. code-block:: html 69 | 70 | 71 | 72 | 73 | Management commands 74 | =================== 75 | 76 | Django-SystemJS comes with a few management commands to create and manage all 77 | the bundles. It does so by checking all your template files and extracting the 78 | ``{% systemjs_import '...' %}`` nodes. 79 | 80 | .. _systemjs_bundle: 81 | 82 | ``systemjs_bundle`` 83 | ------------------- 84 | 85 | Process the project templates, extract the apps and bundle them with jspm. 86 | 87 | .. code-block:: sh 88 | 89 | python manage.py systemjs_bundle 90 | 91 | 92 | By default it will look at all templates in your app directories, and the 93 | additional template dirs for the vanilla Django template engine - Jinja2 is 94 | unsupported. 95 | 96 | Supporting all the jspm command line arguments is work in progress. Currently 97 | the following options are supported: 98 | 99 | Options 100 | ------- 101 | 102 | * ``--minify``: passes the ``--minify`` flag down to jspm to generate minified 103 | bundles 104 | 105 | * ``--sfx``: generates a self-executing bundle. 106 | 107 | * ``--template, -t``: pass the name of a template (for example ``myapp/base.html``), 108 | and Django SystemJS will only look in those files for imported apps. It will 109 | no longer parse all project templates. This option can be specified multiple 110 | times to look in a set of templates. 111 | 112 | .. versionadded:: 1.4 113 | 114 | .. _minimal: 115 | 116 | * ``--minimal``: the minimal option will only rebundle apps that have changed. If 117 | you have multiple ``{% systemjs_import <...> %}`` statements, and only one app 118 | was changed, this can speed up the total bundle time. Comparison happens based 119 | on mtimes and md5 hashes of the involved files. 120 | 121 | .. note:: 122 | 123 | Changes to the source files for the bundles are detected, but changes to jspm 124 | config files (``jspm.config.js``, ``jspm.browser.js``) are not included. These 125 | files can change relatively frequently, hereby invalidating the depcache when 126 | it's not needed. Be careful when making bundle-altering config file changes. 127 | 128 | .. versionadded:: 1.4 129 | 130 | The first time you use the ``--minimal`` option, you will get an error saying 131 | that the ``deps.json`` file cannot be located. This is because we have never 132 | written the dependency tree yet. 133 | 134 | You can do this manually the first time by executing the 135 | :ref:`systemjs_write_depcaches` command. 136 | 137 | * ``--node-path``: path to the ``node_modules`` directory of your project. Required 138 | if Django-SystemJS cannot figure it out by itself and the ``NODE_PATH`` environment 139 | variable is not set. 140 | 141 | .. versionadded:: 1.4 142 | 143 | 144 | ``systemjs_show_packages`` 145 | -------------------------- 146 | 147 | Parses the templates and reports the apps found in them. Useful to get a quick 148 | overview of all the bundles to be generated. 149 | 150 | .. _systemjs_write_depcaches: 151 | 152 | ``systemjs_write_depcaches`` 153 | ---------------------------- 154 | 155 | Parses the templates and extracts the apps found in them. For every app, the 156 | dependencies are traced and written to disk. This depcache is used with the 157 | ``--minimal`` option of the :ref:`systemjs_bundle` command. 158 | 159 | .. note:: 160 | 161 | If you bundle with any of the ``--sfx``, ``--minimal`` or ``minify`` options, 162 | you need to use the same options to write the depcache. A difference in bundle 163 | options will trigger a re-bundle. 164 | 165 | 166 | Example workflow 167 | ================ 168 | 169 | Django SystemJS is designed as a non-intrusive library in development mode, 170 | so that it won't sit in your way too much. Simply using the template tag 171 | will be all you have to do as long as you're running with ``DEBUG=True``. 172 | 173 | Example steps for deployment: 174 | 175 | * Run ``git pull`` to update your copy of the code 176 | * Install the dependencies: ``npm install``, followed by ``jspm install`` 177 | * Run collectstatic: ``python manage.py collectstatic`` 178 | * Bundle the apps in your project: ``python manage.py systemjs_bundle``. 179 | 180 | The order of operations matters: to bundle, all the bits and pieces must be 181 | collected so that ``jspm`` can retrieve them in your ``STATIC_ROOT``. It has no 182 | notion of your ``static`` folders within your apps. 183 | -------------------------------------------------------------------------------- /example/jspm_0_17/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | 3 | staticfiles 4 | node_modules 5 | jspm_packages 6 | -------------------------------------------------------------------------------- /example/jspm_0_17/README.md: -------------------------------------------------------------------------------- 1 | # Example django-systemjs project - JSPM 0.17.x (beta) 2 | 3 | This is an example project with the default Django project layout. 4 | 5 | It uses the JSPM 0.17, which is currently in beta. 6 | 7 | In `frontend.sh` you see the tasks that would typically be executed during 8 | deployment. 9 | 10 | 11 | ## Setup 12 | 13 | This is a history of the commands used to come to this basic set-up. 14 | 15 | ### Bootstrap the `package.json` 16 | 17 | ```sh 18 | $ npm init 19 | ``` 20 | 21 | `nspm init` starts an interactive shell prompt. The defaults should be fine, 22 | but I made some changes to show that it's possible. 23 | 24 | 25 | ```plain 26 | This utility will walk you through creating a package.json file. 27 | It only covers the most common items, and tries to guess sensible defaults. 28 | 29 | See `npm help json` for definitive documentation on these fields 30 | and exactly what they do. 31 | 32 | Use `npm install --save` afterwards to install a package and 33 | save it as a dependency in the package.json file. 34 | 35 | Press ^C at any time to quit. 36 | name: (jspm_0_17) 37 | version: (1.0.0) 38 | description: Example django+jspm 0.17 project 39 | entry point: (index.js) main.js 40 | test command: 41 | git repository: https://github.com/sergei-maertens/django-systemjs.git 42 | keywords: django,jspm,es6 43 | author: Sergei Maertens 44 | license: (ISC) MIT 45 | About to write to /home/bbt/coding/django-systemjs/example/jspm_0_17/package.json: 46 | 47 | { 48 | "name": "jspm_0_17", 49 | "version": "1.0.0", 50 | "description": "Example django+jspm 0.17 project", 51 | "main": "main.js", 52 | "scripts": { 53 | "test": "echo \"Error: no test specified\" && exit 1" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+https://github.com/sergei-maertens/django-systemjs.git" 58 | }, 59 | "keywords": [ 60 | "django", 61 | "jspm", 62 | "es6" 63 | ], 64 | "author": "Sergei Maertens ", 65 | "license": "MIT", 66 | "bugs": { 67 | "url": "https://github.com/sergei-maertens/django-systemjs/issues" 68 | }, 69 | "homepage": "https://github.com/sergei-maertens/django-systemjs#readme" 70 | } 71 | 72 | 73 | Is this ok? (yes) 74 | ``` 75 | 76 | The basic `package.json` is ready now. We will only use `npm` for development 77 | tooling, not for project dependencies. 78 | 79 | 80 | ### Adding `jspm` as dev-dependency. 81 | 82 | ```sh 83 | $ npm install jspm@beta --save-dev 84 | ``` 85 | 86 | Expected output (version numbers can differ): 87 | ``` 88 | jspm_0_17@1.0.0 /home/bbt/coding/django-systemjs/example/jspm_0_17 89 | └─┬ jspm@0.17.0-beta.14 90 | ├── bluebird@3.3.5 91 | ├─┬ chalk@1.1.3 92 | │ ├── ansi-styles@2.2.1 93 | │ ├── escape-string-regexp@1.0.5 94 | │ ├─┬ has-ansi@2.0.0 95 | │ │ └── ansi-regex@2.0.0 96 | │ ├── strip-ansi@3.0.1 97 | │ └── supports-color@2.0.0 98 | ├── core-js@1.2.6 99 | ├─┬ glob@6.0.4 100 | │ ├─┬ inflight@1.0.4 101 | │ │ └── wrappy@1.0.1 102 | │ ├── inherits@2.0.1 103 | │ ├── once@1.3.3 104 | │ └── path-is-absolute@1.0.0 105 | ├── graceful-fs@4.1.3 106 | ├─┬ jspm-github@0.14.6 107 | │ ├─┬ expand-tilde@1.2.1 108 | │ │ └── os-homedir@1.0.1 109 | │ ├── netrc@0.1.4 110 | │ ├─┬ request@2.53.0 111 | │ │ ├── aws-sign2@0.5.0 112 | │ │ ├─┬ bl@0.9.5 113 | │ │ │ └─┬ readable-stream@1.0.34 114 | │ │ │ └── isarray@0.0.1 115 | │ │ ├── caseless@0.9.0 116 | │ │ ├─┬ combined-stream@0.0.7 117 | │ │ │ └── delayed-stream@0.0.5 118 | │ │ ├── forever-agent@0.5.2 119 | │ │ ├─┬ form-data@0.2.0 120 | │ │ │ └── async@0.9.2 121 | │ │ ├── hawk@2.3.1 122 | │ │ ├─┬ http-signature@0.10.1 123 | │ │ │ ├── asn1@0.1.11 124 | │ │ │ ├── assert-plus@0.1.5 125 | │ │ │ └── ctype@0.5.3 126 | │ │ ├─┬ mime-types@2.0.14 127 | │ │ │ └── mime-db@1.12.0 128 | │ │ ├── oauth-sign@0.6.0 129 | │ │ └── qs@2.3.3 130 | │ ├─┬ rimraf@2.3.4 131 | │ │ └─┬ glob@4.5.3 132 | │ │ └── minimatch@2.0.10 133 | │ ├─┬ tar@2.2.1 134 | │ │ ├── block-stream@0.0.9 135 | │ │ └── fstream@1.0.8 136 | │ └─┬ which@1.2.4 137 | │ ├─┬ is-absolute@0.1.7 138 | │ │ └── is-relative@0.1.3 139 | │ └── isexe@1.1.2 140 | ├─┬ jspm-npm@0.28.10 141 | │ ├─┬ readdirp@2.0.0 142 | │ │ ├── minimatch@2.0.10 143 | │ │ └─┬ readable-stream@2.1.2 144 | │ │ ├── core-util-is@1.0.2 145 | │ │ ├── isarray@1.0.0 146 | │ │ ├── process-nextick-args@1.0.6 147 | │ │ ├── string_decoder@0.10.31 148 | │ │ └── util-deprecate@1.0.2 149 | │ ├─┬ request@2.58.0 150 | │ │ ├── caseless@0.10.0 151 | │ │ ├─┬ combined-stream@1.0.5 152 | │ │ │ └── delayed-stream@1.0.0 153 | │ │ ├── forever-agent@0.6.1 154 | │ │ ├─┬ form-data@1.0.0-rc4 155 | │ │ │ ├── async@1.5.2 156 | │ │ │ └─┬ mime-types@2.1.11 157 | │ │ │ └── mime-db@1.23.0 158 | │ │ ├─┬ har-validator@1.8.0 159 | │ │ │ └── bluebird@2.10.2 160 | │ │ ├── http-signature@0.11.0 161 | │ │ ├── oauth-sign@0.8.1 162 | │ │ └── qs@3.1.0 163 | │ ├─┬ rmdir@1.1.0 164 | │ │ └─┬ node.flow@1.2.3 165 | │ │ └─┬ node.extend@1.0.8 166 | │ │ ├── is@0.2.7 167 | │ │ └── object-keys@0.4.0 168 | │ └── tar@1.0.3 169 | ├─┬ jspm-registry@0.4.1 170 | │ ├── rsvp@3.2.1 171 | │ └── semver@4.3.6 172 | ├─┬ liftoff@2.2.1 173 | │ ├── extend@2.0.1 174 | │ ├─┬ findup-sync@0.3.0 175 | │ │ └── glob@5.0.15 176 | │ ├── flagged-respawn@0.3.2 177 | │ ├── rechoir@0.6.2 178 | │ └── resolve@1.1.7 179 | ├─┬ minimatch@3.0.0 180 | │ └─┬ brace-expansion@1.1.4 181 | │ ├── balanced-match@0.4.1 182 | │ └── concat-map@0.0.1 183 | ├─┬ mkdirp@0.5.1 184 | │ └── minimist@0.0.8 185 | ├── ncp@2.0.0 186 | ├─┬ proper-lockfile@1.1.2 187 | │ ├── err-code@1.1.1 188 | │ ├── extend@3.0.0 189 | │ └── retry@0.9.0 190 | ├─┬ request@2.72.0 191 | │ ├── aws-sign2@0.6.0 192 | │ ├─┬ aws4@1.3.2 193 | │ │ └─┬ lru-cache@4.0.1 194 | │ │ ├── pseudomap@1.0.2 195 | │ │ └── yallist@2.0.0 196 | │ ├─┬ bl@1.1.2 197 | │ │ └─┬ readable-stream@2.0.6 198 | │ │ └── isarray@1.0.0 199 | │ ├── caseless@0.11.0 200 | │ ├─┬ combined-stream@1.0.5 201 | │ │ └── delayed-stream@1.0.0 202 | │ ├── extend@3.0.0 203 | │ ├── forever-agent@0.6.1 204 | │ ├─┬ form-data@1.0.0-rc4 205 | │ │ └── async@1.5.2 206 | │ ├─┬ har-validator@2.0.6 207 | │ │ ├─┬ is-my-json-valid@2.13.1 208 | │ │ │ ├── generate-function@2.0.0 209 | │ │ │ ├─┬ generate-object-property@1.2.0 210 | │ │ │ │ └── is-property@1.0.2 211 | │ │ │ ├── jsonpointer@2.0.0 212 | │ │ │ └── xtend@4.0.1 213 | │ │ └─┬ pinkie-promise@2.0.1 214 | │ │ └── pinkie@2.0.4 215 | │ ├─┬ hawk@3.1.3 216 | │ │ ├── boom@2.10.1 217 | │ │ ├── cryptiles@2.0.5 218 | │ │ ├── hoek@2.16.3 219 | │ │ └── sntp@1.0.9 220 | │ ├─┬ http-signature@1.1.1 221 | │ │ ├── assert-plus@0.2.0 222 | │ │ ├─┬ jsprim@1.2.2 223 | │ │ │ ├── extsprintf@1.0.2 224 | │ │ │ ├── json-schema@0.2.2 225 | │ │ │ └── verror@1.3.6 226 | │ │ └─┬ sshpk@1.8.3 227 | │ │ ├── asn1@0.2.3 228 | │ │ ├── assert-plus@1.0.0 229 | │ │ ├─┬ dashdash@1.13.1 230 | │ │ │ └── assert-plus@1.0.0 231 | │ │ ├── ecc-jsbn@0.1.1 232 | │ │ ├─┬ getpass@0.1.6 233 | │ │ │ └── assert-plus@1.0.0 234 | │ │ ├── jodid25519@1.0.2 235 | │ │ ├── jsbn@0.1.0 236 | │ │ └── tweetnacl@0.13.3 237 | │ ├── is-typedarray@1.0.0 238 | │ ├── isstream@0.1.2 239 | │ ├── json-stringify-safe@5.0.1 240 | │ ├─┬ mime-types@2.1.11 241 | │ │ └── mime-db@1.23.0 242 | │ ├── node-uuid@1.4.7 243 | │ ├── oauth-sign@0.8.1 244 | │ ├── qs@6.1.0 245 | │ ├── stringstream@0.0.5 246 | │ ├── tough-cookie@2.2.2 247 | │ └── tunnel-agent@0.4.2 248 | ├─┬ rimraf@2.5.2 249 | │ └── glob@7.0.3 250 | ├─┬ sane@1.3.4 251 | │ ├─┬ exec-sh@0.2.0 252 | │ │ └── merge@1.2.0 253 | │ ├─┬ fb-watchman@1.9.0 254 | │ │ └─┬ bser@1.0.2 255 | │ │ └── node-int64@0.4.0 256 | │ ├─┬ minimatch@0.2.14 257 | │ │ ├── lru-cache@2.7.3 258 | │ │ └── sigmund@1.0.1 259 | │ ├── minimist@1.2.0 260 | │ ├─┬ walker@1.0.7 261 | │ │ └─┬ makeerror@1.0.11 262 | │ │ └── tmpl@1.0.4 263 | │ └── watch@0.10.0 264 | ├── semver@5.1.0 265 | ├─┬ systemjs@0.19.27 266 | │ └── when@3.7.7 267 | ├─┬ systemjs-builder@0.15.16 268 | │ ├─┬ babel-core@6.8.0 269 | │ │ ├─┬ babel-code-frame@6.8.0 270 | │ │ │ ├── esutils@2.0.2 271 | │ │ │ └── js-tokens@1.0.3 272 | │ │ ├─┬ babel-generator@6.8.0 273 | │ │ │ ├─┬ detect-indent@3.0.1 274 | │ │ │ │ ├── get-stdin@4.0.1 275 | │ │ │ │ └── minimist@1.2.0 276 | │ │ │ ├─┬ is-integer@1.0.6 277 | │ │ │ │ └─┬ is-finite@1.0.1 278 | │ │ │ │ └── number-is-nan@1.0.0 279 | │ │ │ ├── repeating@1.1.3 280 | │ │ │ └── trim-right@1.0.1 281 | │ │ ├── babel-helpers@6.8.0 282 | │ │ ├── babel-messages@6.8.0 283 | │ │ ├─┬ babel-register@6.8.0 284 | │ │ │ ├── core-js@2.3.0 285 | │ │ │ └─┬ home-or-tmp@1.0.0 286 | │ │ │ ├── os-tmpdir@1.0.1 287 | │ │ │ └── user-home@1.1.1 288 | │ │ ├─┬ babel-runtime@6.6.1 289 | │ │ │ └── core-js@2.3.0 290 | │ │ ├── babel-template@6.8.0 291 | │ │ ├─┬ babel-traverse@6.8.0 292 | │ │ │ ├── globals@8.18.0 293 | │ │ │ └─┬ invariant@2.2.1 294 | │ │ │ └── loose-envify@1.1.0 295 | │ │ ├─┬ babel-types@6.8.1 296 | │ │ │ └── to-fast-properties@1.0.2 297 | │ │ ├─┬ babylon@6.7.0 298 | │ │ │ └── babel-runtime@5.8.38 299 | │ │ ├── convert-source-map@1.2.0 300 | │ │ ├─┬ debug@2.2.0 301 | │ │ │ └── ms@0.7.1 302 | │ │ ├── json5@0.4.0 303 | │ │ ├── lodash@3.10.1 304 | │ │ ├── minimatch@2.0.10 305 | │ │ ├── path-exists@1.0.0 306 | │ │ ├── private@0.1.6 307 | │ │ ├── shebang-regex@1.0.0 308 | │ │ └── slash@1.0.0 309 | │ ├─┬ babel-plugin-transform-es2015-modules-systemjs@6.8.0 310 | │ │ ├── babel-helper-hoist-variables@6.8.0 311 | │ │ └── babel-plugin-transform-strict-mode@6.8.0 312 | │ ├─┬ es6-template-strings@2.0.0 313 | │ │ ├─┬ es5-ext@0.10.11 314 | │ │ │ ├── es6-iterator@2.0.0 315 | │ │ │ └── es6-symbol@3.0.2 316 | │ │ └─┬ esniff@1.0.0 317 | │ │ └── d@0.1.1 318 | │ ├── glob@7.0.3 319 | │ ├─┬ rollup@0.25.8 320 | │ │ ├── minimist@1.2.0 321 | │ │ └─┬ source-map-support@0.3.3 322 | │ │ └── source-map@0.1.32 323 | │ └── source-map@0.5.6 324 | ├─┬ traceur@0.0.105 325 | │ ├─┬ commander@2.9.0 326 | │ │ └── graceful-readlink@1.0.1 327 | │ ├── glob@5.0.15 328 | │ ├── semver@4.3.6 329 | │ └─┬ source-map-support@0.2.10 330 | │ └─┬ source-map@0.1.32 331 | │ └── amdefine@1.0.0 332 | └─┬ uglify-js@2.6.2 333 | ├── async@0.2.10 334 | ├── uglify-to-browserify@1.0.2 335 | └─┬ yargs@3.10.0 336 | ├── camelcase@1.2.1 337 | ├─┬ cliui@2.1.0 338 | │ ├─┬ center-align@0.1.3 339 | │ │ ├─┬ align-text@0.1.4 340 | │ │ │ ├─┬ kind-of@3.0.3 341 | │ │ │ │ └── is-buffer@1.1.3 342 | │ │ │ ├── longest@1.0.1 343 | │ │ │ └── repeat-string@1.5.4 344 | │ │ └── lazy-cache@1.0.4 345 | │ ├── right-align@0.1.3 346 | │ └── wordwrap@0.0.2 347 | ├── decamelize@1.2.0 348 | └── window-size@0.1.0 349 | ``` 350 | 351 | 352 | ### Configuring jspm for the first time 353 | 354 | Next, it's time to initialize the JSPM configuration, in a way that works well 355 | with `django.contrib.staticfiles`. 356 | 357 | For this project, the `STATIC_ROOT` is the directory `staticfiles`. It's where 358 | the static files from the project/apps are copied to. This folder is normally 359 | excluded from version control. 360 | 361 | So, setting up jspm: 362 | 363 | ```sh 364 | jspm init 365 | ``` 366 | 367 | Note: in the section below, the order of commands can differ because of different 368 | jspm beta versions. 369 | 370 | This kicks of another interactive terminal session: 371 | 372 | ``` 373 | Init Mode (Quick, Standard, Custom) [Quick]: Custom 374 | ``` 375 | 376 | We pick `Custom`, for maximum control about which file is put where. 377 | 378 | ``` 379 | Prefix package.json properties under jspm? [Yes]: 380 | ``` 381 | Namespaces are good, let's do more of them! 382 | 383 | ``` 384 | Enter a name for the project package. 385 | 386 | This name will be used for importing local code. 387 | eg via System.import('jspm_0_17/module.js'). 388 | 389 | Local package name (recommended, optional): jspm_0_17 390 | ``` 391 | The name is derived from `package.json`. 392 | 393 | ``` 394 | Enter the path to the folder containing the local project code. 395 | 396 | This is the folder containing the jspm_0_17 package code. 397 | 398 | package.json directories.lib [src]: staticfiles/js 399 | ``` 400 | This is where it gets a bit tricky for a non-javascript-only project. Point this 401 | to the folder that holds your javascript. For example, in this project, in all 402 | `static` subfolders there is a `js` folder containing the javascript. Within 403 | the `js` folder you can do the app-namespacing. 404 | 405 | For JSPM, you should consider all `*/static/*` folders as one - just like the 406 | result of `collectstatic`. 407 | 408 | ``` 409 | Enter the file path to the public folder served to the browser. 410 | 411 | By default this is taken to be the root project folder. 412 | 413 | package.json directories.baseURL (optional): staticfiles 414 | ``` 415 | In essence, this boils down to 'the parent of `directories.lib`' in the 416 | previous question. 417 | 418 | ``` 419 | Enter the path to the jspm packages folder. 420 | 421 | Only necessary if you would like to customize this folder name or 422 | location. 423 | 424 | package.json directories.packages [staticfiles/jspm_packages]: 425 | ``` 426 | This is installing straight into `collectstatic`, because it has to be within 427 | `baseURL`. The custom finder in systemjs ensures that files are served. 428 | 429 | ``` 430 | Enter a custom config file path. 431 | 432 | Only necessary if you would like to customize the config file name or 433 | location. 434 | 435 | package.json configFiles.jspm [staticfiles/jspm.config.js]: static/jspm.config.js 436 | ``` 437 | The same goes here - make sure that it can be found by `django.contrib.staticfiles`. 438 | This config file should be kept in version control. 439 | 440 | This configuration file holds the flattened dependency tree and translates package 441 | imports to specific versions to load. Consider it the equivalent of a `requirements.txt` 442 | where all the dependencies are pinned to a known functioning version. 443 | 444 | The first section of this file can be hand-edited, the second section is auto- 445 | generated and will be overwritten by jspm. 446 | 447 | ``` 448 | 449 | Enter a custom browser config file path. 450 | 451 | This is also a SystemJS config file, but for browser-only jspm 452 | configurations. 453 | 454 | package.json configFiles.jspm:browser [staticfiles/jspm.browser.js]: static/jspm.browser.js 455 | ``` 456 | Same remark as above. 457 | 458 | This sets up some basepaths and basic browser-related configuration. It should 459 | generally not be hand-edited. 460 | 461 | ``` 462 | Enter the browser baseURL. 463 | 464 | This is the absolute URL of the directories.baseURL public folder in 465 | the browser. 466 | 467 | System.config browser baseURL (optional): /static/ 468 | ``` 469 | This url **must** match your `STATIC_URL` setting. 470 | 471 | ``` 472 | 473 | Enter the browser URL for the folder containing the local project 474 | code. 475 | 476 | This should be the served directories.lib folder. Leave out the 477 | leading / to set a baseURL-relative path. 478 | 479 | System.config browser URL to staticfiles/js [js]: 480 | ``` 481 | This is useful for mapping the project namespace to the actual location that 482 | will be served. In this case, modules in `jspm_0_17/foo/bar.js` will actually 483 | make a request for `/static/js/foo/bar.js`. 484 | 485 | ``` 486 | Enter the browser URL for the jspm_packages folder. 487 | 488 | Leave out the leading / to set a baseURL-relative path. 489 | 490 | System.config browser URL to staticfiles/jspm_packages [jspm_packages]: 491 | ``` 492 | The default is correct here. 493 | 494 | ``` 495 | Enter the main entry point of your package within the static/js 496 | folder. 497 | 498 | System.config local package main [jspm_0_17.js]: main.js 499 | ``` 500 | We have the file `static/js/main.js`, so point to that actual file. 501 | 502 | ``` 503 | Enter the module format of your local project code (within 504 | `static/js`). 505 | 506 | The default option is esm (ECMAScript Module). 507 | 508 | System.config local package format (esm, cjs, amd): esm 509 | ``` 510 | The future is now, so stick with an official spec! 511 | 512 | ``` 513 | Select a transpiler to use for ES module conversion. 514 | 515 | The transpiler is used when detecting modules with import or export 516 | statements, or for modules with format: "esm" metadata set. 517 | 518 | System.config transpiler (Babel, Traceur, TypeScript, None) [babel]: 519 | ``` 520 | Good default, stick with it, unless you know why not. You can switch them around 521 | anyway if you want. 522 | 523 | After this, JSPM sets up the basic dependencies, such as downloading babel. 524 | 525 | ``` 526 | Updating registry cache... 527 | ok Installed dev dependency plugin-babel to npm:systemjs-plugin-babel@^0.0.9 528 | (0.0.9) 529 | Install tree has no forks. 530 | 531 | ok Verified package.json at package.json 532 | Verified config files at static/jspm.config.js and static/jspm.browser.js 533 | Looking up loader files... 534 | system.js 535 | system.js.map 536 | system.src.js 537 | system-polyfills.js 538 | system-polyfills.src.js 539 | system-polyfills.js.map 540 | 541 | Using loader versions: 542 | systemjs@0.19.27 543 | ok Loader files downloaded successfully 544 | ``` 545 | 546 | ## Loading your own code 547 | 548 | Next, I've added a simple dummy app to load the template and staticfiles: 549 | 550 | ``` 551 | myapp 552 | ├── static 553 | │   └── js 554 | │   └── myapp 555 | │   └── app.js 556 | └── templates 557 | └── base.html 558 | 559 | 3 directories, 2 files 560 | ``` 561 | 562 | In `static/main.js` we perform an import to our project-namespaced and then 563 | app-namespaced module. 564 | 565 | 566 | ## Bundling for production 567 | 568 | Generate the production bundle(s) with 569 | ```sh 570 | python manage.py systemjs_bundle --minify 571 | ``` 572 | 573 | ``` 574 | Bundled jspm_0_17/main.js into SYSTEMJS/jspm_0_17/main.js 575 | ``` 576 | 577 | That file resides in `staticfiles/SYSTEMJS/jspm_0_17/main.js`, and the contents 578 | are: 579 | 580 | ```javascript 581 | System.registerDynamic("npm:systemjs-plugin-babel@0.0.9.json",[],!1,function(){return{main:"plugin-babel.js",map:{"systemjs-babel-build":{browser:"./systemjs-babel-browser.js","default":"./systemjs-babel-browser.js"}},meta:{"./plugin-babel.js":{format:"cjs"}}}}),System.register("npm:systemjs-plugin-babel@0.0.9/babel-helpers/classCallCheck.js",[],function(a,b){return{setters:[],execute:function(){a("default",function(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")})}}}),System.register("jspm_0_17/myapp/app.js",["npm:systemjs-plugin-babel@0.0.9/babel-helpers/classCallCheck.js"],function(a,b){var c,d;return{setters:[function(a){c=a["default"]}],execute:function(){d=function b(){c(this,b),document.getElementById("js-hook").innerText="myapp/app.js was loaded"},a("default",d)}}}),System.register("jspm_0_17/main.js",["jspm_0_17/myapp/app.js"],function(a,b){var c;return{setters:[function(a){c=a["default"]}],execute:function(){new c}}}); 582 | System.import('jspm_0_17/main.js'); 583 | //# sourceMappingURL=main.js.map 584 | ``` 585 | 586 | 587 | ## Fast development workflow 588 | 589 | It's also possible to have fast development work, which is particularly useful 590 | when dealing with a lot of dependencies that take a long time to download. 591 | 592 | Two components are relevant here: 593 | 594 | * watching files for changes 595 | * doing incremental builds and injecting the configuration with JSPM. 596 | 597 | ### Watch files for changes 598 | 599 | You will need a global install of gulp (if you haven't yet): 600 | 601 | ```sh 602 | npm install -g gulp 603 | ``` 604 | 605 | Next, make sure you have a local `gulp` and `gulp-watch`: 606 | ```sh 607 | npm install gulp gulp-watch --save-dev 608 | ``` 609 | 610 | And create your `gulpfile.js` (it's in the repo). 611 | 612 | Start `gulp`, and it will watch file changes in each `static` directory. If the 613 | file is changed, it will be copied to `staticfiles` (similar to `collectstatic`). 614 | 615 | 616 | ### Bundle your application incrementally 617 | 618 | The JSPM beta has a nice feature where you can watch the files involved for a 619 | bundle, and watch them for changes. Only the changes are then recompilet, 620 | resulting in a fast development process. The bundle is then injected in the 621 | browser config, where it's loaded by SystemJS for the fastest development 622 | experience. 623 | 624 | In a different shell (or shell tab), run: 625 | 626 | ``` 627 | jspm bundle jspm_0_17/main.js staticfiles/SYSTEMJS/bundles/main.js -wid 628 | ``` 629 | 630 | `-w` makes sure the files are being watched, 631 | `-i` injects the bundle (`SYSTEMJS/bundles/main.js`) into the browser config, and 632 | `-d` bundles in `dev` mode where optimizations are skipped, minification is skipped etc. 633 | 634 | Output: 635 | 636 | ``` 637 | Building the bundle tree for jspm_0_17/main.js... 638 | 639 | jspm_0_17/main.js 640 | jspm_0_17/myapp/app.js 641 | npm:systemjs-plugin-babel@0.0.9.json 642 | npm:systemjs-plugin-babel@0.0.9/babel-helpers/classCallCheck.js 643 | 644 | ok SYSTEMJS/bundles/main.js added to config bundles. 645 | ok Built into staticfiles/SYSTEMJS/bundles/main.js with source maps, 646 | unminified. 647 | Watching staticfiles/js for changes with Watchman... 648 | 649 | ``` 650 | 651 | It has an (optional) dependency on Facebooks Watchman. 652 | 653 | This command can also be added to the `scripts` section in `package.json`, so 654 | you can shortcut it to `npm run dev-bundle`. 655 | 656 | ### Summary 657 | 658 | With these things combined, you have 3 processes running, each in its own 659 | shell / shell tab / screen or tmux session: 660 | 661 | `python manage.py runserver`, `gulp` and `jspm bundle <...>`. 662 | -------------------------------------------------------------------------------- /example/jspm_0_17/frontend.sh: -------------------------------------------------------------------------------- 1 | # ensure that the dev dependencies are up to date 2 | npm install 3 | 4 | # collect the static files, and remove the old files first 5 | python manage.py collectstatic -c --noinput 6 | 7 | # install the front-end dependencies (JS libs) 8 | jspm install 9 | 10 | # bundle the apps into efficient bundles 11 | python manage.py systemjs_bundle 12 | -------------------------------------------------------------------------------- /example/jspm_0_17/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | watch = require('gulp-watch'), 5 | rename = require('gulp-rename'); 6 | 7 | /** 8 | * exactly the same paths as in the DJANGO settings. You could manage this with 9 | * environment variables to be DRY 10 | */ 11 | var STATIC_ROOT = './staticfiles'; 12 | var STATICFILES_DIRS = [ 13 | './static/', 14 | ]; 15 | 16 | var static_src = [ 17 | './*/static/**/*.js', // APP_DIRS 18 | '!./{staticfiles,staticfiles/**}', // exclude STATIC_ROOT (otherwise we end up with infite loops) 19 | ].concat(STATICFILES_DIRS.map(function(dir) { // include all js files in STATICFILES_DIRS 20 | return dir + '**/*.js'; 21 | })); 22 | 23 | 24 | // watches STATICFILES_DIRS 25 | gulp.task('collectstatic', function() { 26 | return watch(static_src, {verbose: true, base: './static'}) 27 | .pipe(rename(function(file) { 28 | var bits = file.dirname.split('/'); 29 | // strip off '..', 'appname' and 'static' - .. comes from base ./static 30 | file.dirname = bits.slice(3).join('/'); 31 | return file; 32 | })) 33 | .pipe(gulp.dest(STATIC_ROOT)); 34 | }); 35 | 36 | 37 | // watch task 38 | gulp.task('default', ['collectstatic']); 39 | -------------------------------------------------------------------------------- /example/jspm_0_17/jspm_0_17/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/example/jspm_0_17/jspm_0_17/__init__.py -------------------------------------------------------------------------------- /example/jspm_0_17/jspm_0_17/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for jspm_0_17 project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.10. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 's&nrt!5w@#58jl9v&uhjejaywk+5fhg32-&#^!nz69cu45w@@1' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'systemjs', 42 | 43 | 'myapp', 44 | ) 45 | 46 | MIDDLEWARE_CLASSES = ( 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | 'django.middleware.security.SecurityMiddleware', 55 | ) 56 | 57 | ROOT_URLCONF = 'jspm_0_17.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'jspm_0_17.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 85 | } 86 | } 87 | 88 | 89 | # Internationalization 90 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 91 | 92 | LANGUAGE_CODE = 'en-us' 93 | 94 | TIME_ZONE = 'UTC' 95 | 96 | USE_I18N = True 97 | 98 | USE_L10N = True 99 | 100 | USE_TZ = True 101 | 102 | 103 | # Static files (CSS, JavaScript, Images) 104 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 105 | 106 | STATIC_URL = '/static/' 107 | 108 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 109 | 110 | STATICFILES_DIRS = [ 111 | os.path.join(BASE_DIR, 'static'), 112 | ] 113 | 114 | STATICFILES_FINDERS = [ 115 | 'django.contrib.staticfiles.finders.FileSystemFinder', 116 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 117 | 'systemjs.finders.SystemFinder', 118 | ] 119 | -------------------------------------------------------------------------------- /example/jspm_0_17/jspm_0_17/urls.py: -------------------------------------------------------------------------------- 1 | """jspm_0_17 URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 14 | """ 15 | from django.conf.urls import include, url 16 | from django.contrib import admin 17 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 18 | from django.views.generic import TemplateView 19 | 20 | 21 | urlpatterns = [ 22 | url(r'^admin/', include(admin.site.urls)), 23 | url(r'^$', TemplateView.as_view(template_name='base.html')), 24 | ] + staticfiles_urlpatterns() 25 | -------------------------------------------------------------------------------- /example/jspm_0_17/jspm_0_17/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for jspm_0_17 project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jspm_0_17.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/jspm_0_17/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jspm_0_17.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/jspm_0_17/myapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/example/jspm_0_17/myapp/__init__.py -------------------------------------------------------------------------------- /example/jspm_0_17/myapp/static/js/myapp/app.js: -------------------------------------------------------------------------------- 1 | import google from 'google-maps'; 2 | 3 | 4 | export default class App { 5 | constructor() { 6 | document.getElementById('js-hook').innerText = 'myapp/app.js was loaded'; 7 | 8 | new google.maps.Map(document.getElementById('map'), { 9 | center: {lat: -34.397, lng: 150.644}, 10 | zoom: 8 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/jspm_0_17/myapp/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles system_tags %} 2 | 3 | 4 | Example app 5 | 15 | 16 | 17 |

Example app

18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | {% systemjs_import "jspm_0_17/main.js" %} 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/jspm_0_17/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jspm_0_17", 3 | "version": "1.0.0", 4 | "description": "Example django+jspm 0.17 project", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev-bundle": "jspm bundle jspm_0_17/main.js staticfiles/SYSTEMJS/bundles/main.js -wid" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/sergei-maertens/django-systemjs.git" 13 | }, 14 | "keywords": [ 15 | "django", 16 | "jspm", 17 | "es6" 18 | ], 19 | "author": "Sergei Maertens ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/sergei-maertens/django-systemjs/issues" 23 | }, 24 | "homepage": "https://github.com/sergei-maertens/django-systemjs#readme", 25 | "devDependencies": { 26 | "gulp": "^3.9.1", 27 | "gulp-rename": "^1.2.2", 28 | "gulp-watch": "^4.3.5", 29 | "jspm": "^0.17.0-beta.28" 30 | }, 31 | "jspm": { 32 | "name": "jspm_0_17", 33 | "main": "main.js", 34 | "directories": { 35 | "baseURL": "staticfiles", 36 | "lib": "staticfiles/js" 37 | }, 38 | "configFiles": { 39 | "jspm:browser": "static/jspm.browser.js", 40 | "jspm": "static/jspm.config.js" 41 | }, 42 | "devDependencies": { 43 | "plugin-babel": "npm:systemjs-plugin-babel@^0.0.13" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/jspm_0_17/requirements.txt: -------------------------------------------------------------------------------- 1 | django==1.10.1 2 | django-systemjs==1.3.2 3 | -------------------------------------------------------------------------------- /example/jspm_0_17/static/js/main.js: -------------------------------------------------------------------------------- 1 | import MyApp from 'jspm_0_17/myapp/app.js'; 2 | 3 | // bootstrap it 4 | new MyApp(); 5 | -------------------------------------------------------------------------------- /example/jspm_0_17/static/jspm.browser.js: -------------------------------------------------------------------------------- 1 | SystemJS.config({ 2 | baseURL: "/static", 3 | paths: { 4 | "google-maps": "https://maps.googleapis.com/maps/api/js?libraries=places&sensor=false&language=ko", 5 | "npm:": "jspm_packages/npm/", 6 | "jspm_0_17/": "js/" 7 | }, 8 | bundles: { 9 | "SYSTEMJS/bundles/main.js": [ 10 | "jspm_0_17/main.js", 11 | "jspm_0_17/myapp/app.js", 12 | "npm:systemjs-plugin-babel@0.0.13/babel-helpers/classCallCheck.js", 13 | "npm:systemjs-plugin-babel@0.0.13.json" 14 | ] 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /example/jspm_0_17/static/jspm.config.js: -------------------------------------------------------------------------------- 1 | SystemJS.config({ 2 | nodeConfig: { 3 | "paths": { 4 | "google-maps": "https://maps.googleapis.com/maps/api/js?libraries=places&sensor=false&language=ko", 5 | "jspm_0_17/": "js/" 6 | } 7 | }, 8 | devConfig: { 9 | "map": { 10 | "plugin-babel": "npm:systemjs-plugin-babel@0.0.13" 11 | } 12 | }, 13 | transpiler: "plugin-babel", 14 | packages: { 15 | "jspm_0_17": { 16 | "format": "esm", 17 | "main": "main.js", 18 | "meta": { 19 | "*.js": { 20 | "loader": "plugin-babel" 21 | } 22 | } 23 | }, 24 | "https://maps.googleapis.com": { 25 | "defaultExtension": false, 26 | "meta": { 27 | "*": { 28 | "scriptLoad": true, 29 | "exports": "google" 30 | } 31 | } 32 | }, 33 | } 34 | }); 35 | 36 | SystemJS.config({ 37 | packageConfigPaths: [ 38 | "npm:@*/*.json", 39 | "npm:*.json" 40 | ], 41 | map: {}, 42 | packages: {} 43 | }); 44 | -------------------------------------------------------------------------------- /example/jspm_0_17/staticfiles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/example/jspm_0_17/staticfiles/.gitkeep -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | def runtests(*tests): 7 | test_dir = os.path.dirname(__file__) 8 | sys.path.insert(0, test_dir) 9 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 10 | 11 | import django 12 | from django.test.utils import get_runner 13 | from django.conf import settings 14 | 15 | django.setup() 16 | 17 | TestRunner = get_runner(settings) 18 | test_runner = TestRunner(verbosity=1, interactive=True) 19 | 20 | if not tests: 21 | tests = (['.'],) 22 | failures = test_runner.run_tests(*tests) 23 | sys.exit(failures) 24 | 25 | 26 | if __name__ == '__main__': 27 | runtests(sys.argv[1:]) 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | import systemjs 6 | 7 | 8 | def read_file(name): 9 | with open(os.path.join(os.path.dirname(__file__), name)) as f: 10 | return f.read() 11 | 12 | 13 | readme = read_file('README.rst') 14 | requirements = [ 15 | 'django-appconf>=0.6', 16 | 'semantic_version', 17 | ] 18 | test_requirements = [ 19 | 'mock', 20 | 'coverage', 21 | ] 22 | 23 | setup( 24 | name='django-systemjs', 25 | version=systemjs.__version__, 26 | license='MIT', 27 | 28 | # Packaging 29 | packages=find_packages(exclude=('tests', 'tests.*', 'examples')), 30 | install_requires=requirements, 31 | include_package_data=True, 32 | extras_require={ 33 | 'test': test_requirements, 34 | }, 35 | scripts=['bin/trace-deps.js'], 36 | tests_require=test_requirements, 37 | test_suite='runtests.runtests', 38 | 39 | # PyPI metadata 40 | description='Brings SystemJS to Django staticfiles', 41 | long_description=readme, 42 | author='Sergei Maertens', 43 | author_email='sergeimaertens@gmail.com', 44 | platforms=['any'], 45 | url='https://github.com/sergei-maertens/django-systemjs', 46 | classifiers=[ 47 | 'Development Status :: 5 - Production/Stable', 48 | 'Framework :: Django :: 1.8', 49 | 'Framework :: Django :: 1.9', 50 | 'Intended Audience :: Developers', 51 | 'Operating System :: Unix', 52 | 'Operating System :: MacOS', 53 | 'Operating System :: Microsoft :: Windows', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Programming Language :: Python :: 3.3', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Topic :: Software Development :: Libraries :: Application Frameworks' 59 | ] 60 | ) 61 | -------------------------------------------------------------------------------- /systemjs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Version code taken from django.utils.version. 3 | """ 4 | 5 | import datetime 6 | import os 7 | import subprocess 8 | 9 | 10 | VERSION = (1, 4, 3, 'final', 0) 11 | 12 | 13 | default_app_config = 'systemjs.apps.SystemJSConfig' 14 | 15 | 16 | def get_version(version=None): 17 | "Returns a PEP 386-compliant version number from VERSION." 18 | version = get_complete_version(version) 19 | 20 | # Now build the two parts of the version number: 21 | # main = X.Y[.Z] 22 | # sub = .devN - for pre-alpha releases 23 | # | {a|b|c}N - for alpha, beta and rc releases 24 | 25 | main = get_main_version(version) 26 | 27 | sub = '' 28 | if version[3] == 'alpha' and version[4] == 0: 29 | git_changeset = get_git_changeset() 30 | if git_changeset: 31 | sub = '.dev%s' % git_changeset 32 | 33 | elif version[3] != 'final': 34 | mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} 35 | sub = mapping[version[3]] + str(version[4]) 36 | 37 | return str(main + sub) 38 | 39 | 40 | def get_main_version(version=None): 41 | "Returns main version (X.Y[.Z]) from VERSION." 42 | version = get_complete_version(version) 43 | parts = 2 if version[2] == 0 else 3 44 | return '.'.join(str(x) for x in version[:parts]) 45 | 46 | 47 | def get_complete_version(version=None): 48 | """Returns a tuple of the django-systemjs version. If version argument is non-empty, 49 | then checks for correctness of the tuple provided. 50 | """ 51 | if version is None: 52 | version = VERSION 53 | else: 54 | assert len(version) == 5 55 | assert version[3] in ('alpha', 'beta', 'rc', 'final') 56 | 57 | return version 58 | 59 | 60 | def get_git_changeset(): 61 | """Returns a numeric identifier of the latest git changeset. 62 | 63 | The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. 64 | This value isn't guaranteed to be unique, but collisions are very unlikely, 65 | so it's sufficient for generating the development version numbers. 66 | """ 67 | repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 68 | git_log = subprocess.Popen( 69 | 'git log --pretty=format:%ct --quiet -1 HEAD', 70 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 71 | shell=True, cwd=repo_dir, universal_newlines=True 72 | ) 73 | timestamp = git_log.communicate()[0] 74 | try: 75 | timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) 76 | except ValueError: 77 | return None 78 | return timestamp.strftime('%Y%m%d%H%M%S') 79 | 80 | 81 | __version__ = get_version(VERSION) 82 | -------------------------------------------------------------------------------- /systemjs/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.apps import AppConfig 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class SystemJSConfig(AppConfig): 8 | name = 'systemjs' 9 | verbose_name = _('Django SystemJS') 10 | 11 | def ready(self): 12 | from . import conf # noqa 13 | -------------------------------------------------------------------------------- /systemjs/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import hashlib 4 | import io 5 | import json 6 | import logging 7 | import os 8 | import posixpath 9 | import subprocess 10 | 11 | from django.conf import settings 12 | from django.contrib.staticfiles.storage import staticfiles_storage 13 | from django.utils.encoding import force_text 14 | 15 | import semantic_version 16 | 17 | from .jspm import locate_package_json 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | JSPM_LOG_VERSION = semantic_version.Version('0.16.3') 23 | 24 | SOURCEMAPPING_URL_COMMENT = b'//# sourceMappingURL=' 25 | 26 | NODE_ENV_VAR = 'NODE_PATH' 27 | 28 | 29 | class BundleError(OSError): 30 | pass 31 | 32 | 33 | class System(object): 34 | 35 | def __init__(self, **opts): 36 | self.opts = opts 37 | self.stdout = self.stdin = self.stderr = subprocess.PIPE 38 | self.cwd = None 39 | self.version = None # JSPM version 40 | 41 | def _has_jspm_log(self): 42 | return self.jspm_version and self.jspm_version >= JSPM_LOG_VERSION 43 | 44 | def get_jspm_version(self, opts): 45 | jspm_bin = opts['jspm'] 46 | cmd = '{} --version'.format(jspm_bin) 47 | proc = subprocess.Popen( 48 | cmd, shell=True, cwd=self.cwd, stdout=self.stdout, 49 | stdin=self.stdin, stderr=self.stderr) 50 | result, err = proc.communicate() # block until it's done 51 | if err: 52 | raise BundleError("Could not determine JSPM version, error: %s", err) 53 | version_string = result.decode().split()[0] 54 | return semantic_version.Version(version_string, partial=True) 55 | 56 | @property 57 | def jspm_version(self): 58 | if not self.version: 59 | options = self.opts.copy() 60 | options.setdefault('jspm', settings.SYSTEMJS_JSPM_EXECUTABLE) 61 | self.version = self.get_jspm_version(options) 62 | return self.version 63 | 64 | def bundle(self, app): 65 | bundle = SystemBundle(self, app, **self.opts) 66 | return bundle.bundle() 67 | 68 | @staticmethod 69 | def get_bundle_path(app): 70 | """ 71 | Returns the path relative to STATIC_URL for the bundle for app. 72 | """ 73 | bundle = SystemBundle(None, app) 74 | return bundle.get_paths()[1] 75 | 76 | 77 | class SystemBundle(object): 78 | """ 79 | Represents a single app to be bundled. 80 | """ 81 | 82 | def __init__(self, system, app, **options): 83 | """ 84 | Initialize a SystemBundle object. 85 | 86 | :param system: a System instance that holds the non-bundle specific 87 | meta information (such as jspm version, configuration) 88 | 89 | :param app: string, the name of the JS package to bundle. This may be 90 | missing the '.js' extension. 91 | 92 | :param options: dict containing the bundle-specific options. Possible 93 | options: 94 | `jspm`: `jspm` executable (if it's not on $PATH, for example) 95 | `log`: logging mode for jspm, can be ok|warn|err. Only available 96 | for jspm >= 0.16.3 97 | `minify`: boolean, whether go generate minified bundles or not 98 | `sfx`: boolean, generate a self-executing bundle or not 99 | `skip-source-maps: boolean, whether to generate source maps or not 100 | """ 101 | self.system = system 102 | self.app = app 103 | 104 | # set the bundle options 105 | options.setdefault('jspm', settings.SYSTEMJS_JSPM_EXECUTABLE) 106 | self.opts = options 107 | 108 | bundle_cmd = self.get_bundle_sfx_cmd() if self.opts.get('sfx') else 'bundle' 109 | self.command = '{jspm} ' + bundle_cmd + ' {app} {outfile}' 110 | 111 | self.stdout = self.stdin = self.stderr = subprocess.PIPE 112 | 113 | def get_bundle_sfx_cmd(self): 114 | spec = semantic_version.Spec('>=0.17.0') 115 | return 'build' if self.system.jspm_version in spec else 'bundle-sfx' 116 | 117 | def get_outfile(self): 118 | js_file = '{app}{ext}'.format(app=self.app, ext='.js' if self.needs_ext() else '') 119 | outfile = os.path.join(settings.STATIC_ROOT, settings.SYSTEMJS_OUTPUT_DIR, js_file) 120 | return outfile 121 | 122 | def get_paths(self): 123 | """ 124 | Return a tuple with the absolute path and relative path (relative to STATIC_URL) 125 | """ 126 | outfile = self.get_outfile() 127 | rel_path = os.path.relpath(outfile, settings.STATIC_ROOT) 128 | return outfile, rel_path 129 | 130 | def needs_ext(self): 131 | """ 132 | Check whether `self.app` is missing the '.js' extension and if it needs it. 133 | """ 134 | if settings.SYSTEMJS_DEFAULT_JS_EXTENSIONS: 135 | name, ext = posixpath.splitext(self.app) 136 | if not ext: 137 | return True 138 | return False 139 | 140 | def bundle(self): 141 | """ 142 | Bundle the app and return the static url to the bundle. 143 | """ 144 | outfile, rel_path = self.get_paths() 145 | 146 | options = self.opts 147 | if self.system._has_jspm_log(): 148 | self.command += ' --log {log}' 149 | options.setdefault('log', 'err') 150 | 151 | if options.get('minify'): 152 | self.command += ' --minify' 153 | 154 | if options.get('skip_source_maps'): 155 | self.command += ' --skip-source-maps' 156 | 157 | try: 158 | cmd = self.command.format(app=self.app, outfile=outfile, **options) 159 | proc = subprocess.Popen( 160 | cmd, shell=True, cwd=self.system.cwd, stdout=self.stdout, 161 | stdin=self.stdin, stderr=self.stderr) 162 | 163 | result, err = proc.communicate() # block until it's done 164 | if err and self.system._has_jspm_log(): 165 | fmt = 'Could not bundle \'%s\': \n%s' 166 | logger.warn(fmt, self.app, err) 167 | raise BundleError(fmt % (self.app, err)) 168 | if result.strip(): 169 | logger.info(result) 170 | except (IOError, OSError) as e: 171 | if isinstance(e, BundleError): 172 | raise 173 | raise BundleError('Unable to apply %s (%r): %s' % ( 174 | self.__class__.__name__, cmd, e)) 175 | else: 176 | if not options.get('sfx'): 177 | # add the import statement, which is missing for non-sfx bundles 178 | sourcemap = find_sourcemap_comment(outfile) 179 | with open(outfile, 'a') as of: 180 | of.write("\nSystem.import('{app}{ext}');\n{sourcemap}".format( 181 | app=self.app, 182 | ext='.js' if self.needs_ext() else '', 183 | sourcemap=sourcemap if sourcemap else '', 184 | )) 185 | return rel_path 186 | 187 | 188 | class TraceError(Exception): 189 | pass 190 | 191 | 192 | class SystemTracer(object): 193 | 194 | def __init__(self, node_path=None): 195 | node_env = os.environ.copy() 196 | if node_path and NODE_ENV_VAR not in node_env: 197 | node_env[NODE_ENV_VAR] = node_path 198 | self.env = node_env 199 | self.name = 'deps.json' 200 | self.storage = staticfiles_storage 201 | self._trace_cache = {} 202 | self._package_json_dir = os.path.dirname(locate_package_json()) 203 | 204 | @property 205 | def cache_file_path(self): 206 | if not os.path.exists(settings.SYSTEMJS_CACHE_DIR): 207 | os.makedirs(settings.SYSTEMJS_CACHE_DIR) 208 | return os.path.join(settings.SYSTEMJS_CACHE_DIR, self.name) 209 | 210 | def trace(self, app): 211 | """ 212 | Trace the dependencies for app. 213 | 214 | A tracer-instance is shortlived, and re-tracing the same app should 215 | yield the same results. Since tracing is an expensive process, cache 216 | the result on the tracer instance. 217 | """ 218 | if app not in self._trace_cache: 219 | process = subprocess.Popen( 220 | "trace-deps.js {}".format(app), shell=True, 221 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 222 | env=self.env, universal_newlines=True, cwd=self._package_json_dir 223 | ) 224 | out, err = process.communicate() 225 | if err: 226 | raise TraceError(err) 227 | self._trace_cache[app] = json.loads(out) 228 | return self._trace_cache[app] 229 | 230 | def get_hash(self, path): 231 | md5 = hashlib.md5() 232 | with self.storage.open(path) as infile: 233 | for chunk in infile.chunks(): 234 | md5.update(chunk) 235 | return md5.hexdigest() 236 | 237 | def write_depcache(self, app_deps, bundle_options): # TODO: use storage 238 | all_deps = { 239 | 'version': 1, 240 | 'packages': app_deps, 241 | 'hashes': {}, 242 | 'options': bundle_options, 243 | } 244 | 245 | for pkg_deptree in app_deps.values(): 246 | for module, info in pkg_deptree.items(): 247 | # issue #13 - external resources are not included in the bundle, 248 | # so don't include them in the depcache either 249 | if info.get('skip', False): 250 | continue 251 | path = info['path'] 252 | if path not in all_deps['hashes']: 253 | all_deps['hashes'][path] = self.get_hash(path) 254 | 255 | with open(self.cache_file_path, 'w') as outfile: 256 | json.dump(all_deps, outfile) 257 | 258 | @property 259 | def cached_deps(self): 260 | if not hasattr(self, '_depcache'): 261 | with open(self.cache_file_path, 'r') as infile: 262 | self._depcache = json.load(infile) 263 | return self._depcache 264 | 265 | def get_depcache(self, app): 266 | # cache in memory for faster lookup 267 | if self.cached_deps.get('version') == 1: 268 | return self.cached_deps['packages'].get(app) 269 | else: 270 | raise NotImplementedError # noqa 271 | 272 | def get_hashes(self): 273 | if self.cached_deps.get('version') == 1: 274 | return self.cached_deps['hashes'] 275 | else: 276 | raise NotImplementedError # noqa 277 | 278 | def get_bundle_options(self): 279 | if self.cached_deps.get('version') == 1: 280 | return self.cached_deps.get('options') 281 | else: 282 | raise NotImplementedError # noqa 283 | 284 | def hashes_match(self, dep_tree): 285 | """ 286 | Compares the app deptree file hashes with the hashes stored in the 287 | cache. 288 | """ 289 | hashes = self.get_hashes() 290 | for module, info in dep_tree.items(): 291 | md5 = self.get_hash(info['path']) 292 | if md5 != hashes[info['path']]: 293 | return False 294 | return True 295 | 296 | def check_needs_update(self, app): 297 | cached_deps = self.get_depcache(app) 298 | deps = self.trace(app) 299 | # no re-bundle needed if the trees, mtimes and file hashes match 300 | if deps == cached_deps and self.hashes_match(deps): 301 | return False 302 | return True 303 | 304 | 305 | def find_sourcemap_comment(filepath, block_size=100): 306 | """ 307 | Seeks and removes the sourcemap comment. If found, the sourcemap line is 308 | returned. 309 | 310 | Bundled output files can have massive amounts of lines, and the sourceMap 311 | comment is always at the end. So, to extract it efficiently, we read out the 312 | lines of the file starting from the end. We look back at most 2 lines. 313 | 314 | :param:filepath: path to output bundle file containing the sourcemap comment 315 | :param:blocksize: integer saying how many bytes to read at once 316 | :return:string with the sourcemap comment or None 317 | """ 318 | 319 | MAX_TRACKBACK = 2 # look back at most 2 lines, catching potential blank line at the end 320 | 321 | block_number = -1 322 | # blocks of size block_size, in reverse order starting from the end of the file 323 | blocks = [] 324 | sourcemap = None 325 | 326 | try: 327 | # open file in binary read+write mode, so we can seek with negative offsets 328 | of = io.open(filepath, 'br+') 329 | # figure out what's the end byte 330 | of.seek(0, os.SEEK_END) 331 | block_end_byte = of.tell() 332 | 333 | # track back for maximum MAX_TRACKBACK lines and while we can track back 334 | while block_end_byte > 0 and MAX_TRACKBACK > 0: 335 | if (block_end_byte - block_size > 0): 336 | # read the last block we haven't yet read 337 | of.seek(block_number*block_size, os.SEEK_END) 338 | blocks.append(of.read(block_size)) 339 | else: 340 | # file too small, start from begining 341 | of.seek(0, os.SEEK_SET) 342 | # only read what was not read 343 | blocks = [of.read(block_end_byte)] 344 | 345 | # update variables that control while loop 346 | content = b''.join(reversed(blocks)) 347 | lines_found = content.count(b'\n') 348 | MAX_TRACKBACK -= lines_found 349 | block_end_byte -= block_size 350 | block_number -= 1 351 | 352 | # early check and bail out if we found the sourcemap comment 353 | if SOURCEMAPPING_URL_COMMENT in content: 354 | offset = 0 355 | # splitlines eats the last \n if its followed by a blank line 356 | lines = content.split(b'\n') 357 | for i, line in enumerate(lines): 358 | if line.startswith(SOURCEMAPPING_URL_COMMENT): 359 | offset = len(line) 360 | sourcemap = line 361 | break 362 | while i+1 < len(lines): 363 | offset += 1 # for the newline char 364 | offset += len(lines[i+1]) 365 | i += 1 366 | # track back until the start of the comment, and truncate the comment 367 | if sourcemap: 368 | offset += 1 # for the newline before the sourcemap comment 369 | of.seek(-offset, os.SEEK_END) 370 | of.truncate() 371 | return force_text(sourcemap) 372 | finally: 373 | of.close() 374 | return sourcemap 375 | -------------------------------------------------------------------------------- /systemjs/compat.py: -------------------------------------------------------------------------------- 1 | from django import VERSION 2 | from django.template.base import Lexer 3 | 4 | 5 | if VERSION < (1, 9): 6 | class Lexer19(Lexer): 7 | 8 | def __init__(self, template_string): 9 | super(Lexer19, self).__init__(template_string, None) 10 | 11 | Lexer = Lexer19 12 | -------------------------------------------------------------------------------- /systemjs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | 5 | from django.conf import settings 6 | from django.core.exceptions import ImproperlyConfigured 7 | 8 | from appconf import AppConf 9 | 10 | 11 | class SystemJSConf(AppConf): 12 | # Main switch 13 | ENABLED = not settings.DEBUG 14 | 15 | # Path to JSPM executable. Must be on the path, or a full path 16 | JSPM_EXECUTABLE = 'jspm' 17 | 18 | OUTPUT_DIR = 'SYSTEMJS' 19 | 20 | CACHE_DIR = None 21 | 22 | PACKAGE_JSON_DIR = getattr(settings, 'BASE_DIR', None) 23 | 24 | DEFAULT_JS_EXTENSIONS = True 25 | 26 | SERVER_URL = None 27 | 28 | class Meta: 29 | prefix = 'systemjs' 30 | 31 | def configure_package_json_dir(self, value): 32 | if value is None: 33 | raise ImproperlyConfigured( 34 | "Tried auto-guessing the location of 'package.json', " 35 | "but settings.BASE_DIR does not exist. Either set BASE_DIR " 36 | "or SYSTEMJS_PACKAGE_JSON_DIR" 37 | ) 38 | return os.path.abspath(value) 39 | 40 | def configure_cache_dir(self, value): 41 | base_dir = getattr(settings, 'BASE_DIR', None) 42 | if not base_dir: 43 | raise ImproperlyConfigured( 44 | "Tried to set a default cache directory. Either set BASE_DIR " 45 | "or SYSTEMJS_CACHE_DIR" 46 | ) 47 | value = os.path.join(base_dir, '_cache', 'systemjs') 48 | return os.path.abspath(value) 49 | -------------------------------------------------------------------------------- /systemjs/finders.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf import settings 4 | from django.contrib.staticfiles.finders import BaseStorageFinder 5 | 6 | from .storage import JSPMFileStorage 7 | 8 | 9 | class SystemFinder(BaseStorageFinder): 10 | """ 11 | A staticfiles finder that looks in STATIC_ROOT for jspm files. 12 | 13 | Inspired by the CompressorFinder from django-compressor. 14 | """ 15 | storage = JSPMFileStorage 16 | 17 | def find(self, path, all=False): 18 | """ 19 | Only allow lookups for jspm_packages. 20 | 21 | # TODO: figure out the 'jspm_packages' dir from packag.json. 22 | """ 23 | bits = path.split('/') 24 | dirs_to_serve = ['jspm_packages', settings.SYSTEMJS_OUTPUT_DIR] 25 | if not bits or bits[0] not in dirs_to_serve: 26 | return [] 27 | return super(SystemFinder, self).find(path, all=all) 28 | 29 | def list(self, ignore_patterns): 30 | # skip this finder alltogether during collectstatic 31 | return [] 32 | -------------------------------------------------------------------------------- /systemjs/jspm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from django.conf import settings 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | 8 | def locate_package_json(): 9 | """ 10 | Find and return the location of package.json. 11 | """ 12 | directory = settings.SYSTEMJS_PACKAGE_JSON_DIR 13 | if not directory: 14 | raise ImproperlyConfigured( 15 | "Could not locate 'package.json'. Set SYSTEMJS_PACKAGE_JSON_DIR " 16 | "to the directory that holds 'package.json'." 17 | ) 18 | path = os.path.join(directory, 'package.json') 19 | if not os.path.isfile(path): 20 | raise ImproperlyConfigured("'package.json' does not exist, tried looking in %s" % path) 21 | return path 22 | 23 | 24 | def parse_package_json(): 25 | """ 26 | Extract the JSPM configuration from package.json. 27 | """ 28 | with open(locate_package_json()) as pjson: 29 | data = json.loads(pjson.read()) 30 | return data 31 | 32 | 33 | def find_systemjs_location(): 34 | """ 35 | Figure out where `jspm_packages/system.js` will be put by JSPM. 36 | """ 37 | location = os.path.abspath(os.path.dirname(locate_package_json())) 38 | conf = parse_package_json() 39 | 40 | if 'jspm' in conf: 41 | conf = conf['jspm'] 42 | 43 | try: 44 | conf = conf['directories'] 45 | except TypeError: 46 | raise ImproperlyConfigured("`package.json` doesn't appear to be a valid json object. " 47 | "Location: %s" % location) 48 | except KeyError: 49 | raise ImproperlyConfigured("The `directories` configuarion was not found in package.json. " 50 | "Please check your jspm install and/or configuarion. `package.json` " 51 | "location: %s" % location) 52 | 53 | # check for explicit location, else fall back to the default as jspm does 54 | jspm_packages = conf['packages'] if 'packages' in conf else 'jspm_packages' 55 | base = conf['baseURL'] if 'baseURL' in conf else '.' 56 | return os.path.join(location, base, jspm_packages, 'system.js') 57 | -------------------------------------------------------------------------------- /systemjs/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/systemjs/management/__init__.py -------------------------------------------------------------------------------- /systemjs/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/systemjs/management/commands/__init__.py -------------------------------------------------------------------------------- /systemjs/management/commands/_mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mixin for various command to retreive locations of systemjs_import calls. 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | import io 7 | import os 8 | import re 9 | from collections import OrderedDict 10 | 11 | from django.conf import settings 12 | from django.core.management.base import CommandError 13 | from django.core.management.utils import handle_extensions 14 | from django.template import loader, TemplateDoesNotExist 15 | from django.template.base import TOKEN_BLOCK 16 | from django.template.loaders.app_directories import get_app_template_dirs 17 | 18 | from systemjs.compat import Lexer 19 | from systemjs.templatetags.system_tags import SystemImportNode 20 | 21 | 22 | SYSTEMJS_TAG_RE = re.compile(r"""systemjs_import\s+(['\"])(?P.*)\1""") 23 | 24 | RESOLVE_CONTEXT = {} 25 | 26 | 27 | class TemplateDiscoveryMixin(object): 28 | 29 | def add_arguments(self, parser): 30 | tpl_group = parser.add_mutually_exclusive_group() 31 | tpl_group.add_argument( 32 | '--extension', '-e', dest='extensions', 33 | help='The file extension(s) to examine (default: "html"). Separate ' 34 | 'multiple extensions with commas, or use -e multiple times.', 35 | action='append') 36 | tpl_group.add_argument( 37 | '--template', '-t', dest='templates', 38 | help='The templates to examine. Separate multiple template names with' 39 | 'commas, or use -t multiple times', 40 | action='append') 41 | 42 | parser.add_argument( 43 | '--symlinks', '-s', action='store_true', dest='symlinks', 44 | default=False, help='Follows symlinks to directories when examining ' 45 | 'source code and templates for SystemJS imports.') 46 | 47 | super(TemplateDiscoveryMixin, self).add_arguments(parser) 48 | 49 | def handle(self, **options): 50 | self.symlinks = options.get('symlinks') 51 | extensions = options.get('extensions') or ['html'] 52 | self.extensions = handle_extensions(extensions) 53 | 54 | def discover_templates(self): 55 | template_dirs = list(get_app_template_dirs('templates')) 56 | for config in settings.TEMPLATES: 57 | # only support vanilla Django templates 58 | if config['BACKEND'] != 'django.template.backends.django.DjangoTemplates': 59 | continue 60 | template_dirs += list(config['DIRS']) 61 | 62 | all_files = [] 63 | for template_dir in template_dirs: 64 | for dirpath, dirnames, filenames in os.walk(template_dir, topdown=True, followlinks=self.symlinks): 65 | for filename in filenames: 66 | filepath = os.path.join(dirpath, filename) 67 | file_ext = os.path.splitext(filename)[1] 68 | if file_ext not in self.extensions: 69 | continue 70 | template_name = os.path.relpath(filepath, template_dir) 71 | all_files.append((template_name, filepath)) 72 | 73 | return all_files 74 | 75 | def find_apps(self, templates=None): 76 | """ 77 | Crawls the (specified) template files and extracts the apps. 78 | 79 | If `templates` is specified, the template loader is used and the template 80 | is tokenized to extract the SystemImportNode. An empty context is used 81 | to resolve the node variables. 82 | """ 83 | 84 | all_apps = OrderedDict() 85 | 86 | if not templates: 87 | all_files = self.discover_templates() 88 | for tpl_name, fp in all_files: 89 | # this is the most performant - a testcase that used the loader with tpl_name 90 | # was about 8x slower for a project with ~5 apps in different templates :( 91 | with io.open(fp, 'r', encoding=settings.FILE_CHARSET) as template_file: 92 | src_data = template_file.read() 93 | 94 | for t in Lexer(src_data).tokenize(): 95 | if t.token_type == TOKEN_BLOCK: 96 | imatch = SYSTEMJS_TAG_RE.match(t.contents) 97 | if imatch: 98 | all_apps.setdefault(tpl_name, []) 99 | all_apps[tpl_name].append(imatch.group('app')) 100 | else: 101 | for tpl_name in templates: 102 | try: 103 | template = loader.get_template(tpl_name) 104 | except TemplateDoesNotExist: 105 | raise CommandError('Template \'%s\' does not exist' % tpl_name) 106 | import_nodes = template.template.nodelist.get_nodes_by_type(SystemImportNode) 107 | for node in import_nodes: 108 | app = node.path.resolve(RESOLVE_CONTEXT) 109 | if not app: 110 | self.stdout.write(self.style.WARNING( 111 | '{tpl}: Could not resolve path with context {ctx}, skipping.'.format( 112 | tpl=tpl_name, ctx=RESOLVE_CONTEXT) 113 | )) 114 | continue 115 | all_apps.setdefault(tpl_name, []) 116 | all_apps[tpl_name].append(app) 117 | 118 | return all_apps 119 | 120 | 121 | class BundleOptionsMixin(object): 122 | 123 | def add_arguments(self, parser): 124 | super(BundleOptionsMixin, self).add_arguments(parser) 125 | 126 | parser.add_argument( 127 | '--sfx', 128 | action='store_true', dest='sfx', 129 | help="Generate self-executing bundles.") 130 | 131 | parser.add_argument('--node-path', help='Path to the project `node_modules` directory') 132 | parser.add_argument('--minify', action='store_true', help='Let jspm minify the bundle') 133 | parser.add_argument('--minimal', action='store_true', help='Only (re)bundle if changes detected') 134 | parser.add_argument('--skip-source-maps', action='store_true', help='Skip source maps generation') 135 | 136 | def get_system_opts(self, options): 137 | system_options = ['minimal', 'minify', 'sfx', 'skip_source_maps'] 138 | return {opt: options.get(opt) for opt in system_options} 139 | -------------------------------------------------------------------------------- /systemjs/management/commands/systemjs_bundle.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | import logging 5 | from collections import OrderedDict 6 | from copy import copy 7 | 8 | from django.conf import settings 9 | from django.contrib.staticfiles.storage import staticfiles_storage 10 | from django.core.exceptions import SuspiciousFileOperation 11 | from django.core.management.base import BaseCommand 12 | from django.core.files.storage import FileSystemStorage 13 | 14 | from systemjs.base import System, SystemTracer 15 | from systemjs.jspm import find_systemjs_location 16 | from ._mixins import BundleOptionsMixin, TemplateDiscoveryMixin 17 | 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class Command(BundleOptionsMixin, TemplateDiscoveryMixin, BaseCommand): 23 | help = "Find {% systemjs_import %} tags and bundle the JS apps." 24 | requires_system_checks = False 25 | 26 | def log(self, msg, level=2): 27 | """ 28 | Small log helper 29 | """ 30 | if self.verbosity >= level: 31 | self.stdout.write(msg) 32 | 33 | def add_arguments(self, parser): 34 | super(Command, self).add_arguments(parser) 35 | 36 | parser.add_argument( 37 | '--no-post-process', 38 | action='store_false', dest='post_process', default=True, 39 | help="Do NOT post process collected files.") 40 | 41 | def handle(self, **options): 42 | super(Command, self).handle(**options) 43 | 44 | self.post_process = options['post_process'] 45 | self.minimal = options.get('minimal') 46 | 47 | self.verbosity = 2 48 | self.storage = copy(staticfiles_storage) 49 | self.storage.systemjs_bundling = True # set flag to check later 50 | 51 | # initialize SystemJS specific objects to process the bundles 52 | tracer = SystemTracer(node_path=options.get('node_path')) 53 | system_opts = self.get_system_opts(options) 54 | system = System(**system_opts) 55 | 56 | has_different_options = self.minimal and tracer.get_bundle_options() != system_opts 57 | 58 | # discover the apps being imported in the templates 59 | all_apps = self.find_apps(templates=options.get('templates')) 60 | all_apps = set(sum(all_apps.values(), [])) 61 | 62 | bundled_files = OrderedDict() 63 | # FIXME: this should be configurable, if people use S3BotoStorage for example, it needs to end up there 64 | storage = FileSystemStorage(settings.STATIC_ROOT, base_url=settings.STATIC_URL) 65 | for app in all_apps: 66 | # do we need to generate the bundle for this app? 67 | if self.minimal and not (has_different_options or tracer.check_needs_update(app)): 68 | # check if the bundle actually exists - if it doesn't, don't skip it 69 | # this happens on the first ever bundle 70 | bundle_path = System.get_bundle_path(app) 71 | if self.storage.exists(bundle_path): 72 | self.stdout.write('Checked bundle for app \'{app}\', no changes found'.format(app=app)) 73 | continue 74 | 75 | rel_path = system.bundle(app) 76 | if not self.storage.exists(rel_path): 77 | self.stderr.write('Could not bundle {app}'.format(app=app)) 78 | else: 79 | self.stdout.write('Bundled {app} into {out}'.format(app=app, out=rel_path)) 80 | bundled_files[rel_path] = (storage, rel_path) 81 | 82 | if self.minimal and bundled_files: 83 | self.stdout.write('Generating the new depcache and writing to file...') 84 | all_deps = {app: tracer.trace(app) for app in all_apps} 85 | tracer.write_depcache(all_deps, system_opts) 86 | 87 | if self.post_process and hasattr(self.storage, 'post_process'): 88 | # post-process system.js if it's within settings.STATIC_ROOT 89 | systemjs_path = find_systemjs_location() 90 | try: 91 | within_static_root = self.storage.exists(systemjs_path) 92 | except SuspiciousFileOperation: 93 | within_static_root = False 94 | if within_static_root: 95 | relative = os.path.relpath(systemjs_path, settings.STATIC_ROOT) 96 | bundled_files[relative] = (storage, relative) 97 | 98 | processor = self.storage.post_process(bundled_files, dry_run=False) 99 | for original_path, processed_path, processed in processor: 100 | if isinstance(processed, Exception): # pragma: no cover 101 | self.stderr.write("Post-processing '%s' failed!" % original_path) 102 | # Add a blank line before the traceback, otherwise it's 103 | # too easy to miss the relevant part of the error message. 104 | self.stderr.write("") 105 | raise processed 106 | if processed: # pragma: no cover 107 | self.log("Post-processed '%s' as '%s'" % (original_path, processed_path), level=1) 108 | else: 109 | self.log("Skipped post-processing '%s'" % original_path) # pragma: no cover 110 | -------------------------------------------------------------------------------- /systemjs/management/commands/systemjs_show_packages.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.management.base import BaseCommand 4 | 5 | from ._mixins import TemplateDiscoveryMixin 6 | 7 | 8 | class Command(TemplateDiscoveryMixin, BaseCommand): 9 | help = "Find packages imported in the templates and list them" 10 | requires_system_checks = False 11 | 12 | def handle(self, **options): 13 | super(Command, self).handle(**options) 14 | 15 | all_apps = self.find_apps(templates=options.get('templates')) 16 | for tpl_name, apps in sorted(all_apps.items()): 17 | self.stdout.write(self.style.MIGRATE_LABEL(tpl_name)) 18 | for app in apps: 19 | self.stdout.write(' {}'.format(app)) 20 | -------------------------------------------------------------------------------- /systemjs/management/commands/systemjs_write_depcaches.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.management.base import BaseCommand 4 | 5 | from systemjs.base import SystemTracer 6 | from ._mixins import BundleOptionsMixin, TemplateDiscoveryMixin 7 | 8 | 9 | class Command(BundleOptionsMixin, TemplateDiscoveryMixin, BaseCommand): 10 | help = "Writes the depcache for all discovered JS apps" 11 | requires_system_checks = False 12 | 13 | def handle(self, **options): 14 | super(Command, self).handle(**options) 15 | 16 | system_opts = self.get_system_opts(options) 17 | 18 | all_apps = self.find_apps(templates=options.get('templates')) 19 | all_apps = set(sum(all_apps.values(), [])) 20 | 21 | tracer = SystemTracer(node_path=options.get('node_path')) 22 | 23 | all_deps = { 24 | app: tracer.trace(app) for app in all_apps 25 | } 26 | tracer.write_depcache(all_deps, system_opts) 27 | -------------------------------------------------------------------------------- /systemjs/storage.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.conf import settings 4 | from django.contrib.staticfiles.storage import FileSystemStorage, ManifestStaticFilesStorage 5 | from django.core.files.base import ContentFile 6 | from django.core.management import CommandError 7 | 8 | 9 | class SystemJSManifestStaticFilesMixin(object): 10 | """ 11 | Do not delete the old manifest, but append to it. 12 | """ 13 | 14 | systemjs_bundling = False 15 | 16 | systemjs_manifest_name = 'systemjs.json' 17 | 18 | def save_systemjs_manifest(self, bundle_files): 19 | payload = {'paths': bundle_files, 'version': self.manifest_version} 20 | contents = json.dumps(payload).encode('utf-8') 21 | self._save(self.systemjs_manifest_name, ContentFile(contents)) 22 | 23 | def load_systemjs_manifest(self): 24 | """ 25 | Load the existing systemjs manifest and remove any entries that no longer 26 | exist on the storage. 27 | """ 28 | # backup the original name 29 | _manifest_name = self.manifest_name 30 | 31 | # load the custom bundle manifest 32 | self.manifest_name = self.systemjs_manifest_name 33 | bundle_files = self.load_manifest() 34 | # reset the manifest name 35 | self.manifest_name = _manifest_name 36 | 37 | # check that the files actually exist, if not, remove them from the manifest 38 | for file, hashed_file in bundle_files.copy().items(): 39 | if not self.exists(file) or not self.exists(hashed_file): 40 | del bundle_files[file] 41 | return bundle_files 42 | 43 | def save_manifest(self): 44 | # load the systemjs manifest 45 | bundle_files = self.load_systemjs_manifest() 46 | 47 | if self.systemjs_bundling: 48 | if not self.exists(self.manifest_name): 49 | raise CommandError('You need to run collectstatic first') 50 | # update the systemjs manifest with the bundle hashes 51 | bundle_files.update(self.hashed_files) 52 | if self.exists(self.systemjs_manifest_name): 53 | self.delete(self.systemjs_manifest_name) 54 | self.save_systemjs_manifest(bundle_files) 55 | # load the existing manifest 56 | hashed_files = self.load_manifest() 57 | else: # regular collectstatic behaviour - add the bundle manifest 58 | self.hashed_files.update(bundle_files) 59 | 60 | super(SystemJSManifestStaticFilesMixin, self).save_manifest() 61 | 62 | # the default has now removed the manifest, and overwritten the new version 63 | if self.systemjs_bundling: 64 | # add to hashed_files 65 | hashed_files.update(bundle_files) 66 | payload = {'paths': hashed_files, 'version': self.manifest_version} 67 | contents = json.dumps(payload).encode('utf-8') 68 | self.delete(self.manifest_name) # delete old file 69 | self._save(self.manifest_name, ContentFile(contents)) 70 | 71 | 72 | class SystemJSManifestStaticFilesStorage(SystemJSManifestStaticFilesMixin, ManifestStaticFilesStorage): 73 | pass 74 | 75 | 76 | class JSPMFileStorage(FileSystemStorage): 77 | 78 | def __init__(self, location=None, base_url=None, *args, **kwargs): 79 | if location is None: 80 | location = settings.STATIC_ROOT 81 | if base_url is None: 82 | base_url = settings.STATIC_URL 83 | super(JSPMFileStorage, self).__init__(location, base_url, *args, **kwargs) 84 | -------------------------------------------------------------------------------- /systemjs/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/systemjs/templatetags/__init__.py -------------------------------------------------------------------------------- /systemjs/templatetags/system_tags.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import posixpath 4 | import re 5 | 6 | from django import template 7 | from django.conf import settings 8 | from django.contrib.staticfiles.storage import staticfiles_storage 9 | from django.forms.utils import flatatt 10 | from django.template.base import token_kwargs 11 | 12 | from systemjs.base import System 13 | 14 | register = template.Library() 15 | 16 | 17 | # Regex for token keyword arguments 18 | kwarg_re = re.compile(r"(?:(\w+)=)?(.+)") 19 | 20 | 21 | class SystemImportNode(template.Node): 22 | 23 | def __init__(self, path, tag_attrs=None): 24 | self.path = path 25 | self.tag_attrs = tag_attrs 26 | 27 | def render(self, context): 28 | """ 29 | Build the filepath by appending the extension. 30 | """ 31 | module_path = self.path.resolve(context) 32 | if not settings.SYSTEMJS_ENABLED: 33 | if settings.SYSTEMJS_DEFAULT_JS_EXTENSIONS: 34 | name, ext = posixpath.splitext(module_path) 35 | if not ext: 36 | module_path = '{}.js'.format(module_path) 37 | 38 | if settings.SYSTEMJS_SERVER_URL: 39 | tpl = """""" 40 | else: 41 | tpl = """""" 42 | return tpl.format(app=module_path, url=settings.SYSTEMJS_SERVER_URL) 43 | 44 | # else: create a bundle 45 | rel_path = System.get_bundle_path(module_path) 46 | url = staticfiles_storage.url(rel_path) 47 | 48 | tag_attrs = {'type': 'text/javascript'} 49 | for key, value in self.tag_attrs.items(): 50 | if not isinstance(value, bool): 51 | value = value.resolve(context) 52 | tag_attrs[key] = value 53 | 54 | return """""".format( 55 | url=url, attrs=flatatt(tag_attrs) 56 | ) 57 | 58 | @classmethod 59 | def handle_token(cls, parser, token): 60 | bits = token.split_contents() 61 | attrs = {} 62 | 63 | if len(bits) < 2: 64 | raise template.TemplateSyntaxError("'%s' takes at least one argument (js module)" % bits[0]) 65 | 66 | if len(bits) > 2: 67 | for bit in bits[2:]: 68 | # First we try to extract a potential kwarg from the bit 69 | kwarg = token_kwargs([bit], parser) 70 | if kwarg: 71 | attrs.update(kwarg) 72 | else: 73 | attrs[bit] = True # for flatatt 74 | 75 | path = parser.compile_filter(bits[1]) 76 | return cls(path, tag_attrs=attrs) 77 | 78 | 79 | @register.tag 80 | def systemjs_import(parser, token): 81 | """ 82 | Import a Javascript module via SystemJS, bundling the app. 83 | 84 | Syntax:: 85 | 86 | {% systemjs_import 'path/to/file' %} 87 | 88 | Example:: 89 | 90 | {% systemjs_import 'mydjangoapp/js/myapp' %} 91 | 92 | Which would be rendered like:: 93 | 94 | 95 | 96 | where /static/CACHE can be configured through settings. 97 | 98 | In DEBUG mode, the result would be 99 | 100 | 101 | """ 102 | 103 | return SystemImportNode.handle_token(parser, token) 104 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/tests/__init__.py -------------------------------------------------------------------------------- /tests/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/tests/app/__init__.py -------------------------------------------------------------------------------- /tests/app/jinja2/base.html: -------------------------------------------------------------------------------- 1 | {% systemjs_import 'app/dummy2' %} 2 | -------------------------------------------------------------------------------- /tests/app/static/app/dependency.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/tests/app/static/app/dependency.js -------------------------------------------------------------------------------- /tests/app/static/app/dummy.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | console.log($.fn.jQuery()); 4 | -------------------------------------------------------------------------------- /tests/app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load system_tags %}{% systemjs_import 'app/dummy' %} 2 | -------------------------------------------------------------------------------- /tests/app/templates/ignore_me.txt: -------------------------------------------------------------------------------- 1 | {% systemjs_import 'app/dummy3' %} 2 | -------------------------------------------------------------------------------- /tests/app/templates/jinja.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/tests/app/templates/jinja.html -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ["DJANGO_SETTINGS_MODULE"] = "settings" 7 | from django.core.management import execute_from_command_line 8 | 9 | root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 10 | sys.path.insert(0, root) 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-systemjs", 3 | "version": "0.1.0", 4 | "description": "Dummy package.json for unit tests", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sergei-maertens/django-systemjs.git" 12 | }, 13 | "keywords": [ 14 | "jspm", 15 | "systemjs", 16 | ], 17 | "author": "Sergei Maertens ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/sergei-maertens/django-systemjs/issues" 21 | }, 22 | "homepage": "https://github.com/sergei-maertens/django-systemjs", 23 | "devDependencies": { 24 | "jspm": "^0.16.14" 25 | }, 26 | "jspm": { 27 | "directories": { 28 | "baseURL": "static", 29 | "doc": "doc" 30 | }, 31 | "configFile": "tests/app/static/config.js", 32 | "dependencies": {}, 33 | "devDependencies": {} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PROJECT_DIR = os.path.dirname(__file__) 4 | BASE_DIR = PROJECT_DIR # setting present in new startproject 5 | 6 | 7 | INSTALLED_APPS = [ 8 | 'django.contrib.staticfiles', 9 | 'systemjs', 10 | 'tests.app', 11 | ] 12 | 13 | ROOT_URLCONF = 'tests.urls' 14 | 15 | DEBUG = False 16 | STATIC_URL = '/static/' 17 | SECRET_KEY = 'this-is-really-not-a-secret' 18 | 19 | MIDDLEWARE_CLASSES = ( 20 | 'django.middleware.common.CommonMiddleware', 21 | ) 22 | 23 | DATABASES = { 24 | 'default': { 25 | 'ENGINE': 'django.db.backends.sqlite3', 26 | 'NAME': os.path.join(PROJECT_DIR, 'database.db'), 27 | } 28 | } 29 | 30 | TEMPLATES = [ 31 | { 32 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 33 | 'APP_DIRS': True, 34 | 'DIRS': [ 35 | os.path.join(PROJECT_DIR, 'templates'), 36 | ], 37 | 'OPTIONS': { 38 | 'context_processors': [ 39 | "django.template.context_processors.i18n", 40 | ], 41 | }, 42 | }, 43 | ] 44 | 45 | STATIC_URL = '/static/' 46 | STATIC_ROOT = os.path.join(PROJECT_DIR, 'static') 47 | -------------------------------------------------------------------------------- /tests/static/jspm_packages/system.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/tests/static/jspm_packages/system.js -------------------------------------------------------------------------------- /tests/static/node_modules/system.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/tests/static/node_modules/system.js -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sergei-maertens/django-systemjs/efd4a3862a39d9771609a25a5556f36023cf6e5c/tests/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests/files/jspm_nested_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dummy", 3 | "jspm": { 4 | "directories": { 5 | "baseURL": "static", 6 | "packages": "jspm" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/tests/files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-systemjs", 3 | "version": "0.1.0", 4 | "description": "Dummy package.json for unit tests", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sergei-maertens/django-systemjs.git" 12 | }, 13 | "keywords": [ 14 | "jspm", 15 | "systemjs", 16 | ], 17 | "author": "Sergei Maertens ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/sergei-maertens/django-systemjs/issues" 21 | }, 22 | "homepage": "https://github.com/sergei-maertens/django-systemjs", 23 | "devDependencies": { 24 | "jspm": "^0.16.14" 25 | }, 26 | "jspm": { 27 | "directories": { 28 | "baseURL": "static", 29 | "doc": "doc" 30 | }, 31 | "configFile": "tests/app/static/config.js", 32 | "dependencies": {}, 33 | "devDependencies": {} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/tests/files/simple_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dummy", 3 | "directories": { 4 | "baseURL": "static" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/tests/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from copy import deepcopy 4 | 5 | from django.conf import settings 6 | from django.test import override_settings 7 | 8 | import mock 9 | 10 | 11 | def mock_Popen(mock_subproc_popen, return_value=None, side_effect=None): 12 | process_mock = mock.Mock() 13 | attrs = {} 14 | if side_effect is None: 15 | attrs['communicate.return_value'] = return_value or ('output', 'error') 16 | else: 17 | attrs['communicate.side_effect'] = side_effect 18 | process_mock.configure_mock(**attrs) 19 | mock_subproc_popen.return_value = process_mock 20 | return process_mock 21 | 22 | 23 | def add_tpl_dir(path): 24 | """ 25 | Adds a template dir to the settings. 26 | 27 | Decorator that wraps around `override_settings`. 28 | """ 29 | templates = deepcopy(settings.TEMPLATES) 30 | templates[0]['DIRS'].append(path) 31 | return override_settings(TEMPLATES=templates) 32 | -------------------------------------------------------------------------------- /tests/tests/static1/dummy2.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | console.log($.fn.jQuery()); 4 | -------------------------------------------------------------------------------- /tests/tests/templates1/same_bundle.html: -------------------------------------------------------------------------------- 1 | {% load system_tags %}{% systemjs_import 'app/dummy' %} 2 | -------------------------------------------------------------------------------- /tests/tests/templates2/extra.html: -------------------------------------------------------------------------------- 1 | {% load system_tags %}{% systemjs_import 'dummy2' %} 2 | -------------------------------------------------------------------------------- /tests/tests/test_bundle.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import mock 4 | import os 5 | import shutil 6 | import subprocess 7 | import tempfile 8 | 9 | from django.conf import settings 10 | from django.test import SimpleTestCase, override_settings 11 | 12 | from semantic_version import Version 13 | 14 | from systemjs.base import System, BundleError 15 | from .helpers import mock_Popen 16 | from .test_management import _bundle 17 | 18 | 19 | @override_settings(STATIC_ROOT=tempfile.mkdtemp()) 20 | class BundleTests(SimpleTestCase): 21 | 22 | def setUp(self): 23 | super(BundleTests, self).setUp() 24 | self.patcher = mock.patch.object(System, 'get_jspm_version') 25 | mocked = self.patcher.start() 26 | mocked.return_value = Version('0.15.0') 27 | 28 | def tearDown(self): 29 | super(BundleTests, self).tearDown() 30 | self.patcher.stop() 31 | 32 | try: 33 | shutil.rmtree(settings.STATIC_ROOT) 34 | except (OSError, IOError): 35 | pass 36 | 37 | @override_settings(SYSTEMJS_OUTPUT_DIR='SYSJS') 38 | @mock.patch('subprocess.Popen') 39 | def test_bundle_result(self, mock_subproc_popen): 40 | """ 41 | Test that bundling an app returns the correct relative path. 42 | """ 43 | system = System() 44 | 45 | def side_effect(*args, **kwargs): 46 | _bundle('app/dummy') 47 | return ('output', 'error') 48 | 49 | # mock Popen/communicate 50 | mock_Popen(mock_subproc_popen, side_effect=side_effect) 51 | 52 | path = system.bundle('app/dummy') 53 | expected_path = os.path.join(settings.SYSTEMJS_OUTPUT_DIR, 'app/dummy.js') 54 | self.assertEqual(path, expected_path) 55 | 56 | @mock.patch('subprocess.Popen') 57 | def test_bundle_suprocess(self, mock_subproc_popen): 58 | """ 59 | Test that bundling calls the correct subprocess command 60 | """ 61 | app_name = 'app/dummy' 62 | 63 | def side_effect(*args, **kwargs): 64 | _bundle(app_name) 65 | return ('output', 'error') 66 | 67 | # mock Popen/communicate 68 | process_mock = mock_Popen(mock_subproc_popen, side_effect=side_effect) 69 | 70 | # Bundle app/dummy 71 | system = System() 72 | system.bundle(app_name) 73 | self.assertEqual(mock_subproc_popen.call_count, 1) 74 | command = mock_subproc_popen.call_args[0][0] 75 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/{0}.js'.format(app_name)) 76 | self.assertEqual(command, 'jspm bundle {0} {1}'.format(app_name, outfile)) 77 | 78 | with open(outfile, 'r') as of: 79 | js = of.read() 80 | self.assertEqual(js, "alert('foo')\nSystem.import('app/dummy.js');\n") 81 | 82 | self.assertEqual(process_mock.communicate.call_count, 1) 83 | 84 | @mock.patch('subprocess.Popen') 85 | def test_bundlesfx_suprocess(self, mock_subproc_popen): 86 | """ 87 | Test that bundling calls the correct subprocess command 88 | """ 89 | # mock Popen/communicate 90 | process_mock = mock_Popen(mock_subproc_popen) 91 | 92 | # Bundle app/dummy 93 | system = System(sfx=True) 94 | system.bundle('app/dummy') 95 | self.assertEqual(mock_subproc_popen.call_count, 1) 96 | command = mock_subproc_popen.call_args[0][0] 97 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/app/dummy.js') 98 | self.assertEqual(command, 'jspm bundle-sfx app/dummy {0}'.format(outfile)) 99 | 100 | self.assertEqual(process_mock.communicate.call_count, 1) 101 | 102 | @mock.patch('subprocess.Popen') 103 | def test_bundle_minify_suprocess(self, mock_subproc_popen): 104 | """ 105 | Test that bundling calls the correct subprocess command 106 | """ 107 | 108 | app_name = 'app/dummy' 109 | 110 | def side_effect(*args, **kwargs): 111 | _bundle(app_name) 112 | return ('output', 'error') 113 | 114 | # mock Popen/communicate 115 | process_mock = mock_Popen(mock_subproc_popen, side_effect=side_effect) 116 | 117 | # Bundle app/dummy 118 | system = System(minify=True) 119 | system.bundle('app/dummy') 120 | self.assertEqual(mock_subproc_popen.call_count, 1) 121 | command = mock_subproc_popen.call_args[0][0] 122 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/app/dummy.js') 123 | self.assertEqual(command, 'jspm bundle app/dummy {0} --minify'.format(outfile)) 124 | 125 | self.assertEqual(process_mock.communicate.call_count, 1) 126 | 127 | @mock.patch('subprocess.Popen') 128 | def test_bundle_skip_source_maps_suprocess(self, mock_subproc_popen): 129 | """ 130 | Test that bundling calls the correct subprocess command 131 | """ 132 | 133 | app_name = 'app/dummy' 134 | 135 | def side_effect(*args, **kwargs): 136 | _bundle(app_name) 137 | return ('output', 'error') 138 | 139 | # mock Popen/communicate 140 | process_mock = mock_Popen(mock_subproc_popen, side_effect=side_effect) 141 | 142 | # Bundle app/dummy 143 | system = System(skip_source_maps=True) 144 | system.bundle('app/dummy') 145 | self.assertEqual(mock_subproc_popen.call_count, 1) 146 | command = mock_subproc_popen.call_args[0][0] 147 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/app/dummy.js') 148 | self.assertEqual(command, 149 | 'jspm bundle app/dummy {0} --skip-source-maps'.format(outfile)) 150 | 151 | outfile = os.path.join(settings.STATIC_ROOT, 152 | 'SYSTEMJS/{0}.js'.format(app_name)) 153 | with open(outfile, 'r') as of: 154 | js = of.read() 155 | self.assertEqual(js, "alert('foo')\nSystem.import('app/dummy.js');\n") 156 | 157 | self.assertEqual(process_mock.communicate.call_count, 1) 158 | 159 | @mock.patch('subprocess.Popen') 160 | def test_oserror_caught(self, mock): 161 | def oserror(): 162 | raise OSError('Error') 163 | 164 | mock_Popen(mock, side_effect=oserror) 165 | with self.assertRaises(BundleError): 166 | system = System() 167 | system.bundle('app/dummy') 168 | 169 | @mock.patch('subprocess.Popen') 170 | def test_ioerror_caught(self, mock): 171 | 172 | def ioerror(): 173 | raise IOError('Error') 174 | 175 | mock_Popen(mock, side_effect=ioerror) 176 | with self.assertRaises(BundleError): 177 | system = System() 178 | system.bundle('app/dummy') 179 | 180 | 181 | class JSPMIntegrationTests(SimpleTestCase): 182 | 183 | @mock.patch('subprocess.Popen') 184 | def test_jspm_version_suprocess(self, mock_subproc_popen): 185 | """ 186 | Test that JSPM version discovery works. 187 | """ 188 | # mock Popen/communicate 189 | return_value = (b'0.15.7\nRunning against global jspm install.\n', '') 190 | process_mock = mock_Popen(mock_subproc_popen, return_value=return_value) 191 | 192 | system = System() 193 | 194 | # Call version 195 | version = system.get_jspm_version({'jspm': 'jspm'}) 196 | self.assertEqual(mock_subproc_popen.call_count, 1) 197 | self.assertEqual(version, Version('0.15.7')) 198 | 199 | command = mock_subproc_popen.call_args[0][0] 200 | self.assertEqual(command, 'jspm --version') 201 | self.assertEqual(process_mock.communicate.call_count, 1) 202 | 203 | @mock.patch('subprocess.Popen') 204 | def test_jspm_version_suprocess_error(self, mock_subproc_popen): 205 | """ 206 | Test that bundling calls the correct subprocess command 207 | """ 208 | # mock Popen/communicate 209 | return_value = (b'gibberish', 'a jspm error') 210 | process_mock = mock_Popen(mock_subproc_popen, return_value=return_value) 211 | 212 | system = System() 213 | 214 | # Call version 215 | with self.assertRaises(BundleError): 216 | system.get_jspm_version({'jspm': 'jspm'}) 217 | 218 | self.assertEqual(mock_subproc_popen.call_count, 1) 219 | 220 | command = mock_subproc_popen.call_args[0][0] 221 | self.assertEqual(command, 'jspm --version') 222 | self.assertEqual(process_mock.communicate.call_count, 1) 223 | 224 | @mock.patch('subprocess.Popen') 225 | @mock.patch.object(System, 'get_jspm_version') 226 | def test_jspm_016_log(self, mock_version, mock_subproc_popen): 227 | """ 228 | Test that bundles are generated with --log=err. 229 | 230 | JSPM > 0.16.0 has the --log option that surpresses levels of 231 | output. 232 | """ 233 | mock_version.return_value = Version('0.16.3') 234 | 235 | app_name = 'app/dummy' 236 | 237 | def side_effect(*args, **kwargs): 238 | _bundle(app_name) 239 | return ('', '') # no stdout, no stderr -> success 240 | 241 | # mock Popen/communicate 242 | process_mock = mock_Popen(mock_subproc_popen, side_effect=side_effect) 243 | 244 | # Bundle app/dummy 245 | system = System() 246 | system.bundle(app_name) 247 | self.assertEqual(mock_subproc_popen.call_count, 1) 248 | 249 | command = mock_subproc_popen.call_args 250 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/{0}.js'.format(app_name)) 251 | 252 | self.assertEqual( 253 | command, 254 | mock.call( 255 | 'jspm bundle {0} {1} --log err'.format(app_name, outfile), 256 | stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE, 257 | shell=True, cwd=None 258 | ) 259 | ) 260 | 261 | with open(outfile, 'r') as of: 262 | js = of.read() 263 | self.assertEqual(js, "alert('foo')\nSystem.import('app/dummy.js');\n") 264 | 265 | self.assertEqual(process_mock.communicate.call_count, 1) 266 | 267 | @mock.patch('subprocess.Popen') 268 | @mock.patch.object(System, 'get_jspm_version') 269 | def test_jspm_016_log_error(self, mock_version, mock_subproc_popen): 270 | """ 271 | Test that bundles are generated with --log=err. 272 | 273 | JSPM > 0.16.0 has the --log option that surpresses levels of 274 | output. 275 | """ 276 | mock_version.return_value = Version('0.16.3') 277 | 278 | app_name = 'app/dummy' 279 | 280 | def side_effect(*args, **kwargs): 281 | _bundle(app_name) 282 | return ('', 'Something went wrong') # no stdout, no stderr -> success 283 | 284 | # mock Popen/communicate 285 | process_mock = mock_Popen(mock_subproc_popen, side_effect=side_effect) 286 | 287 | # Bundle app/dummy 288 | with self.assertRaises(BundleError) as ctx: 289 | system = System() 290 | system.bundle(app_name) 291 | 292 | self.assertEqual(ctx.exception.args[0], "Could not bundle \'app/dummy\': \nSomething went wrong") 293 | 294 | self.assertEqual(mock_subproc_popen.call_count, 1) 295 | self.assertEqual(process_mock.communicate.call_count, 1) 296 | 297 | @mock.patch('subprocess.Popen') 298 | @mock.patch.object(System, 'get_jspm_version') 299 | def test_sourcemap_comment(self, mock_version, mock_subproc_popen): 300 | """ 301 | Asserts that the sourcemap comment is still at the end. 302 | """ 303 | mock_version.return_value = Version('0.15.7') 304 | app_name = 'app/dummy' 305 | 306 | def side_effect(*args, **kwargs): 307 | content = 'alert(\'foo\')\n//# sourceMappingURL=dummy.js.map' 308 | _bundle(app_name, content=content) 309 | return ('output', 'error') 310 | 311 | # mock Popen/communicate 312 | mock_Popen(mock_subproc_popen, side_effect=side_effect) 313 | 314 | # Bundle app/dummy 315 | system = System() 316 | system.bundle(app_name) 317 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/{0}.js'.format(app_name)) 318 | with open(outfile, 'r') as of: 319 | js = of.read() 320 | self.assertEqual(js, "alert('foo')\nSystem.import('app/dummy.js');\n" 321 | "//# sourceMappingURL=dummy.js.map") 322 | 323 | @mock.patch('subprocess.Popen') 324 | @mock.patch.object(System, 'get_jspm_version') 325 | def test_sourcemap_comment_end_newline(self, mock_version, mock_subproc_popen): 326 | """ 327 | Asserts that the sourcemap comment is still at the end - with ending newline 328 | """ 329 | mock_version.return_value = Version('0.15.7') 330 | app_name = 'app/dummy' 331 | 332 | def side_effect(*args, **kwargs): 333 | content = 'alert(\'foo\')\n//# sourceMappingURL=dummy.js.map\n' 334 | _bundle(app_name, content=content) 335 | return ('output', 'error') 336 | 337 | # mock Popen/communicate 338 | mock_Popen(mock_subproc_popen, side_effect=side_effect) 339 | 340 | # Bundle app/dummy 341 | system = System() 342 | system.bundle(app_name) 343 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/{0}.js'.format(app_name)) 344 | with open(outfile, 'r') as of: 345 | js = of.read() 346 | self.assertEqual(js, "alert('foo')\nSystem.import('app/dummy.js');\n" 347 | "//# sourceMappingURL=dummy.js.map") 348 | 349 | @mock.patch('subprocess.Popen') 350 | @mock.patch.object(System, 'get_jspm_version') 351 | def test_sourcemap_comment_large_file(self, mock_version, mock_subproc_popen): 352 | """ 353 | Same test as test_sourcemap_comment, except with a 'file' that's more 354 | than 100 bytes (to read multiple blocks). 355 | """ 356 | mock_version.return_value = Version('0.15.7') 357 | app_name = 'app/dummy' 358 | 359 | lorem = ''' 360 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 361 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 362 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 363 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 364 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 365 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 366 | ''' 367 | 368 | def side_effect(*args, **kwargs): 369 | content = 'alert(\'{}\')\n//# sourceMappingURL=dummy.js.map'.format(lorem) 370 | _bundle(app_name, content=content) 371 | return ('output', 'error') 372 | 373 | # mock Popen/communicate 374 | mock_Popen(mock_subproc_popen, side_effect=side_effect) 375 | 376 | # Bundle app/dummy 377 | system = System() 378 | system.bundle(app_name) 379 | outfile = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS/{0}.js'.format(app_name)) 380 | with open(outfile, 'r') as of: 381 | js = of.read() 382 | self.assertEqual(js, "alert('{}')\nSystem.import('app/dummy.js');\n" 383 | "//# sourceMappingURL=dummy.js.map".format(lorem)) 384 | -------------------------------------------------------------------------------- /tests/tests/test_finders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests related to staticfiles finders. 3 | """ 4 | import os 5 | 6 | from django.conf import settings 7 | from django.test import override_settings, SimpleTestCase 8 | from django.contrib.staticfiles import finders 9 | 10 | 11 | class FinderTests(SimpleTestCase): 12 | 13 | def test_jspm_packages_in_static_root(self): 14 | """ 15 | Test that the files in jspm_packages are found when jspm 16 | installs directly in settings.STATIC_ROOT. 17 | 18 | Regression test for #9 where django-compressor turns out to 19 | be a hidden dependency. 20 | """ 21 | # assert that it's not found with the standard finders 22 | location = finders.find('jspm_packages/system.js', all=False) 23 | self.assertIsNone(location) 24 | 25 | # but it is found with the custom finder... 26 | with override_settings(STATICFILES_FINDERS=('systemjs.finders.SystemFinder',)): 27 | location = finders.find('jspm_packages/system.js', all=False) 28 | self.assertEqual( 29 | location, 30 | os.path.abspath(os.path.join(settings.STATIC_ROOT, 'jspm_packages', 'system.js')) 31 | ) 32 | 33 | def test_finder_non_jspm_package_file(self): 34 | # assert that it's not found with the standard finders 35 | location = finders.find('node_modules/system.js', all=False) 36 | self.assertIsNone(location) 37 | 38 | # but it is found with the custom finder... 39 | with override_settings(STATICFILES_FINDERS=('systemjs.finders.SystemFinder',)): 40 | location = finders.find('node_modules/system.js', all=False) 41 | self.assertIsNone(location) 42 | -------------------------------------------------------------------------------- /tests/tests/test_management.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import io 4 | import json 5 | import mock 6 | import os 7 | import shutil 8 | import tempfile 9 | import time 10 | try: # Py2 11 | from StringIO import StringIO 12 | except ImportError: # Py3 13 | from io import StringIO 14 | 15 | from django.conf import settings 16 | from django.core.management import call_command, CommandError 17 | from django.test import SimpleTestCase, override_settings 18 | 19 | from semantic_version import Version 20 | 21 | from .helpers import add_tpl_dir 22 | 23 | 24 | JINJA_TEMPLATES = [{ 25 | 'BACKEND': 'django.template.backends.jinja2.Jinja2', 26 | 'DIRS': [ 27 | os.path.join(settings.PROJECT_DIR, 'app', 'jinja2') 28 | ] 29 | }] 30 | 31 | 32 | def _bundle(app, **opts): 33 | path = os.path.join(settings.SYSTEMJS_OUTPUT_DIR, '{0}.js'.format(app)) 34 | outfile = os.path.join(settings.STATIC_ROOT, path) 35 | if not os.path.isdir(os.path.dirname(outfile)): 36 | os.makedirs(os.path.dirname(outfile)) 37 | with io.open(outfile, 'w') as f: 38 | content = opts.get('content', 'alert(\'foo\')') 39 | f.write(content) 40 | return path 41 | 42 | 43 | def _num_files(dir): 44 | return sum([len(files) for r, d, files in os.walk(dir)]) 45 | 46 | 47 | class ClearStaticMixin(object): 48 | 49 | def tearDown(self): 50 | self._clear_static() 51 | 52 | def _clear_static(self): 53 | try: 54 | shutil.rmtree(settings.STATIC_ROOT) 55 | except (OSError, IOError): 56 | pass 57 | 58 | 59 | class MockFindSystemJSLocation(object): 60 | 61 | def setUp(self): 62 | super(MockFindSystemJSLocation, self).setUp() 63 | self.patcher = mock.patch('systemjs.management.commands.systemjs_bundle.find_systemjs_location') 64 | self.mock = self.patcher.start() 65 | self.mock.return_value = '/dummy/path/system.js' 66 | 67 | def tearDown(self): 68 | super(MockFindSystemJSLocation, self).tearDown() 69 | self.patcher.stop() 70 | 71 | 72 | @override_settings(STATIC_ROOT=tempfile.mkdtemp()) 73 | @mock.patch('systemjs.base.System.bundle') 74 | class ManagementCommandTests(MockFindSystemJSLocation, ClearStaticMixin, SimpleTestCase): 75 | 76 | def setUp(self): 77 | super(ManagementCommandTests, self).setUp() 78 | self.out = StringIO() 79 | self.err = StringIO() 80 | self._clear_static() 81 | 82 | self.now = int(time.time()) 83 | 84 | def _create_deps_json(self, deps=None, **overrides): 85 | deps = deps or { 86 | 'version': 1, 87 | 'packages': { 88 | 'app/dummy': { 89 | 'app/dummy.js': { 90 | 'name': 'app/dummy.js', 91 | 'timestamp': self.now, 92 | 'path': 'app/dummy.js', 93 | } 94 | } 95 | }, 96 | 'hashes': { 97 | 'app/dummy.js': '65d75b61cae058018d3de1fa433a43da', 98 | }, 99 | 'options': { 100 | 'minimal': True, 101 | 'sfx': False, 102 | 'minify': False, 103 | 'skip_source_maps': False 104 | } 105 | } 106 | deps.update(**overrides) 107 | with open(os.path.join(settings.SYSTEMJS_CACHE_DIR, 'deps.json'), 'w') as _file: 108 | json.dump(deps, _file) 109 | 110 | def test_no_arguments(self, bundle_mock): 111 | """ 112 | Test the correct functioning of calling 'systemjs_bundle' with 113 | no additional arguments. 114 | """ 115 | bundle_mock.side_effect = _bundle 116 | 117 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 118 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 119 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 120 | 121 | self.assertEqual(bundle_mock.call_count, 1) # only one app should be found 122 | self.assertEqual(bundle_mock.call_args, mock.call('app/dummy',)) 123 | 124 | def test_sfx_option(self, bundle_mock): 125 | bundle_mock.side_effect = _bundle 126 | 127 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 128 | call_command('systemjs_bundle', '--sfx', stdout=self.out, stderr=self.err) 129 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 130 | 131 | self.assertEqual(bundle_mock.call_count, 1) # only one app should be found 132 | self.assertEqual(bundle_mock.call_args, mock.call('app/dummy',)) 133 | 134 | def test_minify_option(self, bundle_mock): 135 | bundle_mock.side_effect = _bundle 136 | 137 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 138 | call_command('systemjs_bundle', '--minify', stdout=self.out, stderr=self.err) 139 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 140 | 141 | self.assertEqual(bundle_mock.call_count, 1) # only one app should be found 142 | self.assertEqual(bundle_mock.call_args, mock.call('app/dummy',)) 143 | 144 | def test_skip_source_maps_option(self, bundle_mock): 145 | bundle_mock.side_effect = _bundle 146 | 147 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 148 | call_command('systemjs_bundle', '--skip-source-maps', stdout=self.out, stderr=self.err) 149 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 150 | 151 | self.assertEqual(bundle_mock.call_count, 1) # only one app should be found 152 | self.assertEqual(bundle_mock.call_args, mock.call('app/dummy',)) 153 | 154 | @override_settings(TEMPLATES=JINJA_TEMPLATES) 155 | def test_different_backend(self, bundle_mock): 156 | bundle_mock.side_effect = _bundle 157 | 158 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 159 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 160 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 161 | 162 | self.assertEqual(bundle_mock.call_count, 1) # we only support vanilla templates 163 | # as opposed to app/dummy2 164 | self.assertEqual(bundle_mock.call_args, mock.call('app/dummy',)) 165 | 166 | @override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage') 167 | def test_post_process(self, bundle_mock): 168 | bundle_mock.side_effect = _bundle 169 | 170 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 171 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 172 | # created one file + the file with hashed filename 173 | self.assertEqual(_num_files(settings.STATIC_ROOT), 2) 174 | 175 | self.assertEqual(bundle_mock.call_count, 1) # only one bundle call made 176 | 177 | @override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage') 178 | def test_skip_post_process(self, bundle_mock): 179 | bundle_mock.side_effect = _bundle 180 | 181 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 182 | call_command('systemjs_bundle', '--no-post-process', stdout=self.out, stderr=self.err) 183 | # created one file + skipped post processing 184 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 185 | self.assertEqual(bundle_mock.call_count, 1) # only one bundle call made 186 | 187 | def test_templates_option(self, bundle_mock): 188 | bundle_mock.side_effect = _bundle 189 | 190 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 191 | call_command('systemjs_bundle', '--template', 'base.html', stdout=self.out, stderr=self.err) 192 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 193 | 194 | self.assertEqual(bundle_mock.call_count, 1) # only one app should be found 195 | self.assertEqual(bundle_mock.call_args, mock.call('app/dummy',)) 196 | 197 | def test_templates_option_wrong_tpl(self, bundle_mock): 198 | bundle_mock.side_effect = _bundle 199 | 200 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 201 | with self.assertRaises(CommandError): 202 | call_command('systemjs_bundle', '--template', 'nothere.html', stdout=self.out, stderr=self.err) 203 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 204 | self.assertEqual(bundle_mock.call_count, 0) 205 | 206 | @add_tpl_dir(os.path.join(os.path.dirname(__file__), 'templates1')) 207 | def test_same_bundle_multiple_templates(self, bundle_mock): 208 | """ 209 | Test that a module is bundled only once if it appears in multiple 210 | template files. 211 | """ 212 | bundle_mock.side_effect = _bundle 213 | 214 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 215 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 216 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 217 | 218 | # one app found in multiple templates -> should only be bundled once 219 | self.assertEqual(bundle_mock.call_count, 1) 220 | self.assertEqual(bundle_mock.call_args, mock.call('app/dummy')) 221 | 222 | @override_settings(SYSTEMJS_CACHE_DIR=tempfile.mkdtemp()) 223 | @mock.patch('systemjs.base.SystemTracer.trace') 224 | def test_minimal_bundle(self, trace_mock, bundle_mock): 225 | """ 226 | Assert that minimal bundles are generated only if needed 227 | 228 | This is the case where the bundle file does not exist. 229 | """ 230 | trace_mock.return_value = { 231 | 'app/dummy.js': { 232 | 'name': 'app/dummy.js', 233 | 'timestamp': self.now, 234 | 'path': 'app/dummy.js', 235 | } 236 | } 237 | self._create_deps_json() 238 | 239 | call_command('collectstatic', link=True, interactive=False, stdout=self.out, sterr=self.err) 240 | call_command('systemjs_bundle', '--minimal', stdout=self.out, stderr=self.err) 241 | # no new bundles should have been created 242 | self.assertEqual(bundle_mock.call_count, 1) 243 | 244 | @override_settings(SYSTEMJS_CACHE_DIR=tempfile.mkdtemp()) 245 | @mock.patch('systemjs.base.SystemTracer.trace') 246 | def test_minimal_bundle_initial_exists(self, trace_mock, bundle_mock): 247 | """ 248 | Assert that minimal bundles are generated only if needed 249 | """ 250 | trace_mock.return_value = { 251 | 'app/dummy.js': { 252 | 'name': 'app/dummy.js', 253 | 'timestamp': self.now, 254 | 'path': 'app/dummy.js', 255 | } 256 | } 257 | self._create_deps_json() 258 | 259 | # put the 'bundle' there 260 | dirs = os.path.join(settings.STATIC_ROOT, 'SYSTEMJS', 'app') 261 | os.makedirs(dirs) 262 | with open(os.path.join(dirs, 'dummy.js'), 'w') as bundle: 263 | bundle.write('I am bundle') 264 | 265 | call_command('collectstatic', link=True, interactive=False, stdout=self.out, sterr=self.err) 266 | call_command('systemjs_bundle', '--minimal', stdout=self.out, stderr=self.err) 267 | # no new bundles should have been created 268 | self.assertEqual(bundle_mock.call_count, 0) 269 | 270 | @override_settings(SYSTEMJS_CACHE_DIR=tempfile.mkdtemp()) 271 | @mock.patch('systemjs.base.SystemTracer.trace') 272 | def test_minimal_bundle_different_options(self, trace_mock, bundle_mock): 273 | """ 274 | Assert that minimal bundles are generated only if needed 275 | """ 276 | trace_mock.return_value = { 277 | 'app/dummy.js': { 278 | 'name': 'app/dummy.js', 279 | 'timestamp': self.now, 280 | 'path': 'app/dummy.js', 281 | } 282 | } 283 | self._create_deps_json() 284 | 285 | call_command('collectstatic', link=True, interactive=False, stdout=self.out, sterr=self.err) 286 | call_command('systemjs_bundle', '--minimal', '--sfx', stdout=self.out, stderr=self.err) 287 | # no new bundles should have been created 288 | self.assertEqual(bundle_mock.call_count, 1) 289 | 290 | 291 | @override_settings(STATIC_ROOT=tempfile.mkdtemp()) 292 | class FailedBundleTests(MockFindSystemJSLocation, ClearStaticMixin, SimpleTestCase): 293 | 294 | def setUp(self): 295 | super(FailedBundleTests, self).setUp() 296 | self.out = StringIO() 297 | self.err = StringIO() 298 | 299 | self._clear_static() 300 | 301 | @override_settings(SYSTEMJS_JSPM_EXECUTABLE='gibberish') 302 | @mock.patch('systemjs.base.System.get_jspm_version') 303 | def test_bundle_failed(self, mock): 304 | mock.return_value = Version('0.15.0') 305 | 306 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 307 | call_command('systemjs_bundle', '--sfx', stdout=self.out, stderr=self.err) 308 | 309 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 310 | 311 | self.err.seek(0) 312 | self.assertEqual(self.err.read(), 'Could not bundle app/dummy\n') 313 | 314 | 315 | @override_settings( 316 | STATIC_ROOT=tempfile.mkdtemp(), 317 | STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage') 318 | @mock.patch('systemjs.base.System.bundle') 319 | class PostProcessSystemJSTests(ClearStaticMixin, SimpleTestCase): 320 | 321 | """ 322 | Test that `system.js` is post processed as well if it resides 323 | inside settings.STATIC_ROOT. 324 | """ 325 | 326 | def setUp(self): 327 | super(PostProcessSystemJSTests, self).setUp() 328 | 329 | self.out = StringIO() 330 | self.err = StringIO() 331 | self._clear_static() 332 | 333 | def write_systemjs(self): 334 | basedir = os.path.abspath(os.path.join(settings.STATIC_ROOT, 'jspm_packages')) 335 | self.systemjs_location = os.path.join(basedir, 'system.js') 336 | if not os.path.exists(basedir): 337 | os.makedirs(basedir) 338 | 339 | # put a dummy system.js in place 340 | with open(self.systemjs_location, 'w') as of: 341 | of.write('alert("I am system.js");') 342 | 343 | @mock.patch('systemjs.management.commands.systemjs_bundle.find_systemjs_location') 344 | def test_systemjs_outside_of_static_root(self, systemjs_mock, bundle_mock): 345 | """ 346 | If `system.js` is not inside of settings.STATIC_ROOT, it 347 | should not get post-processed explicitly as collectstatic does this. 348 | """ 349 | bundle_mock.side_effect = _bundle 350 | # patch the location to outside of settings.STATIC_ROOT 351 | systemjs_mock.return_value = '/non/existant/path' 352 | 353 | self.assertEqual(_num_files(settings.STATIC_ROOT), 0) 354 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 355 | # created one file + did post processing 356 | self.assertEqual(_num_files(settings.STATIC_ROOT), 2) 357 | self.assertEqual(bundle_mock.call_count, 1) # only one bundle call made 358 | 359 | @mock.patch('systemjs.management.commands.systemjs_bundle.find_systemjs_location') 360 | def test_systemjs_inside_static_root(self, systemjs_mock, bundle_mock): 361 | """ 362 | See issue #5: if jspm installs directly into settings.STATIC_ROOT, 363 | with the CachedStaticFilesStorage, the `system.js` file is not post-processed. 364 | """ 365 | bundle_mock.side_effect = _bundle 366 | self.write_systemjs() 367 | systemjs_mock.return_value = self.systemjs_location 368 | 369 | self.assertEqual(_num_files(settings.STATIC_ROOT), 1) 370 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 371 | # system.js exists + created one file + did post processing for both 372 | self.assertEqual(_num_files(settings.STATIC_ROOT), 4) 373 | self.assertEqual(bundle_mock.call_count, 1) # only one bundle call made 374 | 375 | 376 | @override_settings( 377 | STATIC_ROOT=tempfile.mkdtemp(), 378 | STATICFILES_STORAGE='systemjs.storage.SystemJSManifestStaticFilesStorage') 379 | @mock.patch('systemjs.base.System.bundle') 380 | class ManifestStorageTests(ClearStaticMixin, SimpleTestCase): 381 | 382 | """ 383 | Test that the storage works as expected - do not wipe collectstatic results 384 | """ 385 | 386 | def setUp(self): 387 | super(ManifestStorageTests, self).setUp() 388 | 389 | self.out = StringIO() 390 | self.err = StringIO() 391 | self._clear_static() 392 | 393 | @mock.patch('systemjs.management.commands.systemjs_bundle.find_systemjs_location') 394 | def test_collectstatic_not_broken(self, systemjs_mock, bundle_mock): 395 | bundle_mock.side_effect = _bundle 396 | systemjs_mock.return_value = '/non/existant/path/' 397 | 398 | base = os.path.abspath(settings.STATIC_ROOT) 399 | self.assertEqual(_num_files(base), 0) 400 | 401 | call_command('collectstatic', link=True, interactive=False, stdout=self.out, sterr=self.err) 402 | # dummy.js + dummy.hash.js + staticfiles.json + dependency.js + dependency.hash.js 403 | self.assertEqual(_num_files(base), 5) 404 | with open(os.path.join(base, 'staticfiles.json')) as infile: 405 | manifest = json.loads(infile.read()) 406 | self.assertEqual(manifest['paths'], { 407 | 'app/dummy.js': 'app/dummy.65d75b61cae0.js', 408 | 'app/dependency.js': 'app/dependency.d41d8cd98f00.js' 409 | }) 410 | 411 | # bundle the files and check that the bundled file is post-processed 412 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 413 | 414 | # + bundled file + post-processed file (not staticfiles.json!) 415 | self.assertEqual(_num_files(base), 8) 416 | with open(os.path.join(base, 'staticfiles.json')) as infile: 417 | manifest = json.loads(infile.read()) 418 | self.assertEqual(manifest['paths'], { 419 | 'app/dummy.js': 'app/dummy.65d75b61cae0.js', 420 | 'app/dependency.js': 'app/dependency.d41d8cd98f00.js', 421 | 'SYSTEMJS/app/dummy.js': 'SYSTEMJS/app/dummy.5d1dad25dae3.js' 422 | }) 423 | 424 | @mock.patch('systemjs.management.commands.systemjs_bundle.find_systemjs_location') 425 | def test_no_collectstatic(self, m1, m2): 426 | m1.return_value = '/fake/path/' 427 | m2.side_effect = _bundle 428 | 429 | with self.assertRaises(CommandError): 430 | call_command('systemjs_bundle') 431 | 432 | @mock.patch('systemjs.management.commands.systemjs_bundle.find_systemjs_location') 433 | def test_collectstatic_retain_bundle_manifest(self, systemjs_mock, bundle_mock): 434 | """ 435 | Issue #13: bundling full, followed by collectstatic, followed by 436 | bundling a single template removes the bundle entries from the manifest 437 | (staticfiles.json). This may not happen. 438 | 439 | Reference: https://github.com/sergei-maertens/django-systemjs/issues/13#issuecomment-243968551 440 | """ 441 | bundle_mock.side_effect = _bundle 442 | systemjs_mock.return_value = '/non/existant/path/' 443 | 444 | base = os.path.abspath(settings.STATIC_ROOT) 445 | self.assertEqual(_num_files(base), 0) 446 | 447 | call_command('collectstatic', interactive=False) 448 | 449 | # dummy.js + dummy.hash.js + staticfiles.json + dependency.js + dependency.hash.js 450 | self.assertEqual(_num_files(base), 5) 451 | with open(os.path.join(base, 'staticfiles.json')) as infile: 452 | manifest = json.loads(infile.read()) 453 | self.assertEqual(manifest['paths'], { 454 | 'app/dummy.js': 'app/dummy.65d75b61cae0.js', 455 | 'app/dependency.js': 'app/dependency.d41d8cd98f00.js', 456 | }) 457 | 458 | # bundle the files and check that the bundled file is post-processed 459 | call_command('systemjs_bundle', stdout=self.out, stderr=self.err) 460 | # + 2 bundled files + 2 post-processed files (not staticfiles.json!) + systemjs manifest 461 | self.assertEqual(_num_files(base), 8) 462 | with open(os.path.join(base, 'staticfiles.json')) as infile: 463 | manifest = json.loads(infile.read()) 464 | self.assertEqual(manifest['paths'], { 465 | 'app/dummy.js': 'app/dummy.65d75b61cae0.js', 466 | 'app/dependency.js': 'app/dependency.d41d8cd98f00.js', 467 | 'SYSTEMJS/app/dummy.js': 'SYSTEMJS/app/dummy.5d1dad25dae3.js', 468 | }) 469 | 470 | # wipes the bundles from the manifest without the systemjs manifest mixin 471 | with override_settings(STATICFILES_DIRS=[os.path.join(os.path.dirname(__file__), 'static1')]): 472 | call_command('collectstatic', interactive=False) 473 | self.assertEqual(_num_files(base), 10) 474 | with open(os.path.join(base, 'staticfiles.json')) as infile: 475 | manifest = json.loads(infile.read()) 476 | 477 | self.assertEqual(manifest['paths'], { 478 | 'app/dummy.js': 'app/dummy.65d75b61cae0.js', 479 | 'app/dependency.js': 'app/dependency.d41d8cd98f00.js', 480 | 'dummy2.js': 'dummy2.65d75b61cae0.js', 481 | 'SYSTEMJS/app/dummy.js': 'SYSTEMJS/app/dummy.5d1dad25dae3.js', 482 | }) 483 | 484 | with add_tpl_dir(os.path.join(os.path.dirname(__file__), 'templates2')): 485 | call_command('systemjs_bundle', '--template', 'extra.html', stdout=self.out, stderr=self.err) 486 | 487 | with open(os.path.join(base, 'staticfiles.json')) as infile: 488 | manifest = json.loads(infile.read()) 489 | self.assertEqual(_num_files(base), 12) 490 | self.assertEqual(manifest['paths'], { 491 | 'app/dummy.js': 'app/dummy.65d75b61cae0.js', 492 | 'app/dependency.js': 'app/dependency.d41d8cd98f00.js', 493 | 'dummy2.js': 'dummy2.65d75b61cae0.js', 494 | 'SYSTEMJS/app/dummy.js': 'SYSTEMJS/app/dummy.5d1dad25dae3.js', 495 | 'SYSTEMJS/dummy2.js': 'SYSTEMJS/dummy2.5d1dad25dae3.js', 496 | }) 497 | 498 | 499 | class ShowPackagesTests(SimpleTestCase): 500 | 501 | def test_command(self): 502 | """ 503 | Check that listing the packages works as expected. 504 | """ 505 | stdout = StringIO() 506 | stderr = StringIO() 507 | call_command('systemjs_show_packages', stdout=stdout, sterr=stderr) 508 | stderr.seek(0) 509 | stdout.seek(0) 510 | self.assertEqual(stderr.read(), '') # no errors 511 | output = stdout.read() 512 | self.assertIn('base.html', output) 513 | self.assertIn('app/dummy', output) 514 | 515 | 516 | class WriteDepCachesTests(SimpleTestCase): 517 | 518 | @mock.patch('systemjs.base.SystemTracer.write_depcache') 519 | @mock.patch('systemjs.base.SystemTracer.trace') 520 | def test_command(self, mock_trace, mock_write_depcache): 521 | """ 522 | Check that writing the depcaches works as expected. 523 | """ 524 | mock_trace.return_value = {} 525 | stdout = StringIO() 526 | stderr = StringIO() 527 | call_command('systemjs_write_depcaches', stdout=stdout, sterr=stderr) 528 | stderr.seek(0) 529 | stdout.seek(0) 530 | self.assertEqual(stderr.read(), '') # no errors 531 | self.assertEqual(stdout.read(), '') # no output either 532 | 533 | mock_trace.assert_called_once_with('app/dummy') 534 | mock_write_depcache.assert_called_once_with( 535 | {'app/dummy': {}}, 536 | {'minimal': False, 'sfx': False, 'minify': False, 'skip_source_maps': False} 537 | ) 538 | -------------------------------------------------------------------------------- /tests/tests/test_package_json.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that deal with locating package.json, parsing it and extracting 3 | relevant information. 4 | """ 5 | from __future__ import unicode_literals 6 | 7 | import mock 8 | import os 9 | 10 | from django.conf import settings 11 | from django.core.exceptions import ImproperlyConfigured 12 | from django.test import SimpleTestCase, override_settings 13 | 14 | from systemjs.jspm import find_systemjs_location, locate_package_json, parse_package_json 15 | 16 | 17 | overridden_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'files')) 18 | 19 | simple = os.path.join(overridden_path, 'simple_package.json') 20 | nested = os.path.join(overridden_path, 'jspm_nested_package.json') 21 | 22 | 23 | class PackageJsonTests(SimpleTestCase): 24 | 25 | def test_auto_locate(self): 26 | """ 27 | Test that based on settings.BASE_DIR, the package.json folder 28 | can be found. 29 | """ 30 | root = os.path.abspath(settings.PROJECT_DIR) 31 | path = settings.SYSTEMJS_PACKAGE_JSON_DIR 32 | self.assertEqual(path, root) 33 | package_json = locate_package_json() 34 | self.assertEqual(package_json, os.path.join(root, 'package.json')) 35 | 36 | @override_settings(SYSTEMJS_PACKAGE_JSON_DIR=overridden_path) 37 | def test_load_overridden_path(self): 38 | package_json = locate_package_json() 39 | self.assertEqual(package_json, os.path.join(overridden_path, 'package.json')) 40 | 41 | @override_settings(SYSTEMJS_PACKAGE_JSON_DIR='/false/path') 42 | def test_invalid_dir(self): 43 | with self.assertRaises(ImproperlyConfigured): 44 | locate_package_json() 45 | 46 | @mock.patch('systemjs.jspm.locate_package_json') 47 | def test_read_package_json(self, mock): 48 | """ 49 | Test that package.json correctly gets loaded as JSON into a dict. 50 | """ 51 | # return a simple json blob path 52 | mock.return_value = simple 53 | 54 | data = parse_package_json() 55 | self.assertEqual(data, { 56 | "name": "dummy", 57 | "directories": { 58 | "baseURL": "static", 59 | } 60 | }) 61 | 62 | @override_settings(STATIC_ROOT=os.path.join(overridden_path, 'static')) 63 | @mock.patch('systemjs.jspm.locate_package_json') 64 | def test_nested_jspm_extract(self, mock): 65 | """ 66 | JSPM has an option to nest all configuration in package.json in some 67 | sort of 'jspm' namespace. Test that is dealt with accordingly. 68 | """ 69 | mock.return_value = nested 70 | 71 | location = find_systemjs_location() 72 | expected_location = os.path.join(settings.STATIC_ROOT, 'jspm', 'system.js') 73 | self.assertEqual(location, expected_location) 74 | 75 | @override_settings(STATIC_ROOT=os.path.join(overridden_path, 'static')) 76 | @mock.patch('systemjs.jspm.locate_package_json') 77 | def test_non_nested_jspm_extract(self, mock): 78 | """ 79 | JSPM has an option to nest all configuration in package.json in some 80 | sort of 'jspm' namespace. Test that is dealt with accordingly if the 81 | config is not namespaced. 82 | """ 83 | mock.return_value = simple 84 | 85 | location = find_systemjs_location() 86 | expected_location = os.path.join(settings.STATIC_ROOT, 'jspm_packages', 'system.js') 87 | self.assertEqual(location, expected_location) 88 | 89 | @mock.patch('systemjs.jspm.parse_package_json') 90 | def test_invalid_package_json(self, mock): 91 | mock.return_value = 'I am invalid' 92 | 93 | with self.assertRaises(ImproperlyConfigured): 94 | find_systemjs_location() 95 | 96 | @mock.patch('systemjs.jspm.parse_package_json') 97 | def test_invalid_package_json2(self, mock): 98 | mock.return_value = { 99 | "name": "I am invalid" 100 | } 101 | 102 | with self.assertRaises(ImproperlyConfigured): 103 | find_systemjs_location() 104 | 105 | @override_settings(BASE_DIR=None) 106 | def test_missing_package_json_dir_setting(self): 107 | from systemjs.conf import SystemJSConf 108 | 109 | with self.assertRaises(ImproperlyConfigured): 110 | SystemJSConf().configure_package_json_dir(None) 111 | 112 | @override_settings(SYSTEMJS_PACKAGE_JSON_DIR=None) 113 | def test_invalid_systemjs_package_json_dir_setting(self): 114 | with self.assertRaises(ImproperlyConfigured): 115 | locate_package_json() 116 | -------------------------------------------------------------------------------- /tests/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf import settings 4 | from django.test import SimpleTestCase, override_settings 5 | from django.template import Context, engines, TemplateSyntaxError 6 | from django.utils.six.moves.urllib.parse import urljoin 7 | 8 | 9 | django_engine = engines['django'] 10 | 11 | 12 | class TemplateTagTests(SimpleTestCase): 13 | 14 | TEMPLATE = """{% load system_tags %}{% systemjs_import 'myapp/main' %}""" 15 | 16 | def setUp(self): 17 | self.template = django_engine.from_string(self.TEMPLATE) 18 | self.context = Context() 19 | 20 | def _render(self): 21 | return self.template.render(self.context) 22 | 23 | @override_settings(SYSTEMJS_ENABLED=False) 24 | def test_debug(self): 25 | rendered = self._render() 26 | self.assertEqual(rendered, """""") 27 | 28 | @override_settings(SYSTEMJS_ENABLED=True, SYSTEMJS_OUTPUT_DIR='SJ') 29 | def test_normal(self): 30 | rendered = self._render() 31 | expected_url = urljoin(settings.STATIC_URL, 'SJ/myapp/main.js') 32 | self.assertEqual(rendered, """""".format(expected_url)) 33 | 34 | def test_incorrect_number_arguments(self): 35 | with self.assertRaises(TemplateSyntaxError): 36 | django_engine.from_string("""{% load system_tags %}{% systemjs_import %}""") 37 | 38 | @override_settings(SYSTEMJS_ENABLED=False, SYSTEMJS_DEFAULT_JS_EXTENSIONS=False) 39 | def test_debug_module_without_extension_no_default(self): 40 | rendered = self._render() 41 | self.assertEqual(rendered, """""") 42 | 43 | self.template = django_engine.from_string("""{% load system_tags %}{% systemjs_import 'myapp/main.js' %}""") 44 | rendered = self._render() 45 | self.assertEqual(rendered, """""") 46 | 47 | @override_settings(SYSTEMJS_ENABLED=False, SYSTEMJS_DEFAULT_JS_EXTENSIONS=True) 48 | def test_debug_module_without_extension_default_js(self): 49 | rendered = self._render() 50 | self.assertEqual(rendered, """""") 51 | 52 | self.template = django_engine.from_string("""{% load system_tags %}{% systemjs_import 'myapp/main.js' %}""") 53 | rendered = self._render() 54 | self.assertEqual(rendered, """""") 55 | 56 | @override_settings(SYSTEMJS_ENABLED=True, SYSTEMJS_OUTPUT_DIR='SJ') 57 | def test_script_tag_attributes(self): 58 | template = """{% load system_tags %}{% systemjs_import 'myapp/main' async foo="bar" %}""" 59 | template = django_engine.from_string(template) 60 | rendered = template.render(self.context) 61 | expected_url = urljoin(settings.STATIC_URL, 'SJ/myapp/main.js') 62 | self.assertHTMLEqual( 63 | rendered, 64 | """""".format(expected_url) 65 | ) 66 | 67 | @override_settings(SYSTEMJS_ENABLED=False, SYSTEMJS_SERVER_URL='http://localhost:3000/assets/') 68 | def test_external_server(self): 69 | rendered = self._render() 70 | self.assertHTMLEqual( 71 | rendered, 72 | """""" 73 | ) 74 | -------------------------------------------------------------------------------- /tests/tests/test_tracing.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import json 4 | import os 5 | import shutil 6 | import tempfile 7 | import time 8 | 9 | from django.conf import settings 10 | from django.core.management import call_command 11 | from django.test import SimpleTestCase, override_settings 12 | 13 | from systemjs.base import SystemTracer 14 | from .helpers import mock_Popen 15 | 16 | try: 17 | from unittest.mock import patch 18 | except ImportError: 19 | from mock import patch 20 | 21 | 22 | class TracerTests(SimpleTestCase): 23 | 24 | def test_env_var(self): 25 | """ 26 | Test that the NODE_PATH environment variable is handled properly 27 | """ 28 | ENV_VAR = 'NODE_PATH' 29 | 30 | current_environ = os.environ.copy() 31 | self.assertNotIn(ENV_VAR, current_environ) 32 | 33 | # not in environ, not passed in explicitly -> tracer doesn't have it 34 | tracer = SystemTracer() 35 | self.assertNotIn(ENV_VAR, tracer.env) 36 | 37 | # in environ, not passed in -> tracer has it 38 | os.environ[ENV_VAR] = 'foo' 39 | tracer = SystemTracer() 40 | self.assertEqual(tracer.env[ENV_VAR], 'foo') 41 | 42 | # in environ, and passed it -> should take existing value 43 | tracer = SystemTracer(node_path='bar') 44 | self.assertEqual(tracer.env[ENV_VAR], 'foo') 45 | 46 | # not in environ, but passed in -> should take passed in value 47 | os.environ = current_environ 48 | tracer = SystemTracer(node_path='bar') 49 | self.assertEqual(tracer.env[ENV_VAR], 'bar') 50 | 51 | @patch('systemjs.base.subprocess.Popen') 52 | def test_trace_app(self, mock): 53 | """ 54 | Test that tracing an app is delegated to the Node script. 55 | """ 56 | trace_result = { 57 | 'app/dummy.js': { 58 | 'name': 'app/dummy.js', 59 | 'timestamp': int(time.time()), 60 | 'path': 'app/dummy.js', 61 | 'skip': False, 62 | } 63 | } 64 | return_value = (json.dumps(trace_result), '') # no stdout, no stderr 65 | process_mock = mock_Popen(mock, return_value=return_value) 66 | 67 | tracer = SystemTracer() 68 | tracer.trace('app/dummy') 69 | self.assertEqual(mock.call_count, 1) 70 | self.assertEqual(mock.call_args[0], ('trace-deps.js app/dummy',)) 71 | self.assertEqual(process_mock.communicate.call_count, 1) 72 | 73 | # check the cache functionality - once an app was traced, the result 74 | # should be stored on the tracer instance 75 | tracer.trace('app/dummy') 76 | self.assertEqual(mock.call_count, 1) # should still be only one 77 | self.assertEqual(process_mock.communicate.call_count, 1) 78 | 79 | 80 | @override_settings(SYSTEMJS_CACHE_DIR=tempfile.mkdtemp()) 81 | class TracerFileTests(SimpleTestCase): 82 | """ 83 | Tracer tests that write/read files/directories 84 | """ 85 | 86 | def setUp(self): 87 | super(TracerFileTests, self).setUp() 88 | call_command('collectstatic', interactive=False) 89 | 90 | self.now = int(time.time()) 91 | self._depcache = { 92 | 'version': 1, 93 | 'packages': { 94 | 'app/dummy': { 95 | 'app/dummy.js': { 96 | 'name': 'app/dummy.js', 97 | 'timestamp': self.now, 98 | 'path': 'app/dummy.js', 99 | 'skip': False, 100 | }, 101 | 'app/dependency.js': { 102 | 'name': 'app/dependency.js', 103 | 'timestamp': self.now, 104 | 'path': 'app/dependency.js', 105 | 'skip': False, 106 | } 107 | } 108 | }, 109 | 'hashes': { 110 | 'app/dummy.js': '65d75b61cae058018d3de1fa433a43da', 111 | 'app/dependency.js': 'd41d8cd98f00b204e9800998ecf8427e' 112 | }, 113 | 'options': { 114 | 'option1': True, 115 | 'option2': False, 116 | } 117 | } 118 | 119 | def empty_cache_dir(): 120 | for root, dirs, files in os.walk(settings.SYSTEMJS_CACHE_DIR): 121 | for f in files: 122 | os.unlink(os.path.join(root, f)) 123 | for d in dirs: 124 | shutil.rmtree(os.path.join(root, d)) 125 | 126 | self.addCleanup(empty_cache_dir) 127 | 128 | @patch('systemjs.base.os.makedirs', return_value=None) 129 | def test_create_cache_dir(self, mock_makedirs): 130 | tracer = SystemTracer() 131 | with self.settings(SYSTEMJS_CACHE_DIR='non-existent'): 132 | self.assertEqual(tracer.cache_file_path, os.path.join('non-existent', 'deps.json')) 133 | mock_makedirs.assert_called_once_with('non-existent') 134 | 135 | @patch('systemjs.base.subprocess.Popen') 136 | def test_write_deps(self, mock): 137 | """ 138 | Trace an app and write the depcache for it. 139 | """ 140 | now = int(time.time()) 141 | trace_result = { 142 | 'app/dummy.js': { 143 | 'name': 'app/dummy.js', 144 | 'timestamp': now, 145 | 'path': 'app/dummy.js', 146 | 'skip': False, 147 | }, 148 | 'app/dependency.js': { 149 | 'name': 'app/dependency.js', 150 | 'timestamp': now, 151 | 'path': 'app/dependency.js', 152 | 'skip': False, 153 | } 154 | } 155 | 156 | return_value = (json.dumps(trace_result), '') # no stdout, no stderr 157 | mock_Popen(mock, return_value=return_value) 158 | 159 | tracer = SystemTracer() 160 | all_deps = {'app/dummy': tracer.trace('app/dummy')} 161 | 162 | path = os.path.join(settings.SYSTEMJS_CACHE_DIR, 'deps.json') 163 | self.assertFalse(os.path.exists(path)) 164 | tracer.write_depcache(all_deps, {}) 165 | self.assertTrue(os.path.exists(path)) 166 | 167 | @patch('systemjs.base.subprocess.Popen') 168 | def test_write_deps_external_resource(self, mock): 169 | """ 170 | Issue #13: google maps is scriptLoaded and has no physical file on disk. 171 | As such, there is no `info['path']` to read. 172 | """ 173 | now = int(time.time()) 174 | trace_result = { 175 | 'app/dummy.js': { 176 | 'name': 'app/dummy.js', 177 | 'timestamp': now, 178 | 'path': 'app/dummy.js', 179 | 'skip': False, 180 | }, 181 | 'google-maps': { 182 | 'name': 'google-maps', 183 | 'path': None, 184 | 'timestamp': None, 185 | 'skip': True, 186 | }, 187 | } 188 | 189 | return_value = (json.dumps(trace_result), '') # no stdout, no stderr 190 | mock_Popen(mock, return_value=return_value) 191 | 192 | tracer = SystemTracer() 193 | all_deps = {'app/dummy': tracer.trace('app/dummy')} 194 | 195 | path = os.path.join(settings.SYSTEMJS_CACHE_DIR, 'deps.json') 196 | self.assertFalse(os.path.exists(path)) 197 | tracer.write_depcache(all_deps, {}) 198 | self.assertTrue(os.path.exists(path)) 199 | with open(path) as infile: 200 | depcache = json.load(infile) 201 | # google maps may not be included 202 | self.assertEqual(list(depcache['packages'].keys()), ['app/dummy']) 203 | 204 | @patch('systemjs.base.json.load') 205 | def test_read_deps(self, mock_json_load): 206 | _file = open(os.path.join(settings.SYSTEMJS_CACHE_DIR, 'deps.json'), 'w') 207 | _file.write(json.dumps(self._depcache)) 208 | self.addCleanup(_file.close) 209 | self.addCleanup(lambda: os.remove(_file.name)) 210 | 211 | mock_json_load.return_value = self._depcache 212 | tracer = SystemTracer() 213 | 214 | # checking reading the hashes 215 | hashes = tracer.get_hashes() 216 | self.assertEqual(hashes, { 217 | 'app/dummy.js': '65d75b61cae058018d3de1fa433a43da', 218 | 'app/dependency.js': 'd41d8cd98f00b204e9800998ecf8427e' 219 | }) 220 | 221 | # check reading the depcache for an app 222 | depcache = tracer.get_depcache('app/dummy') 223 | self.assertEqual(depcache, { 224 | 'app/dummy.js': { 225 | 'name': 'app/dummy.js', 226 | 'timestamp': self.now, 227 | 'path': 'app/dummy.js', 228 | 'skip': False, 229 | }, 230 | 'app/dependency.js': { 231 | 'name': 'app/dependency.js', 232 | 'timestamp': self.now, 233 | 'path': 'app/dependency.js', 234 | 'skip': False, 235 | } 236 | }) 237 | 238 | # check retrieving bundle options 239 | bundle_options = tracer.get_bundle_options() 240 | self.assertEqual(bundle_options, { 241 | 'option1': True, 242 | 'option2': False, 243 | }) 244 | 245 | self.assertEqual(mock_json_load.call_count, 1) 246 | 247 | @patch('systemjs.base.json.load') 248 | def test_hashes_match(self, mock_json_load): 249 | _file = open(os.path.join(settings.SYSTEMJS_CACHE_DIR, 'deps.json'), 'w') 250 | self.addCleanup(_file.close) 251 | self.addCleanup(lambda: os.remove(_file.name)) 252 | 253 | mock_json_load.return_value = self._depcache 254 | tracer = SystemTracer() 255 | 256 | dep_tree = { 257 | 'app/dummy.js': { 258 | 'name': 'app/dummy.js', 259 | 'timestamp': self.now, 260 | 'path': 'app/dummy.js', 261 | }, 262 | 'app/dependency.js': { 263 | 'name': 'app/dependency.js', 264 | 'timestamp': self.now, 265 | 'path': 'app/dependency.js', 266 | } 267 | } 268 | 269 | self.assertTrue(tracer.hashes_match(dep_tree)) 270 | 271 | # mock the hashes to return different result 272 | with patch.object(tracer, 'get_hashes') as mocked_hashes: 273 | mocked_hashes.return_value = { 274 | 'app/dummy.js': 'hash.app.dummy.js', 275 | 'app/dependency.js': 'hash.app.dependency.js', 276 | } 277 | self.assertFalse(tracer.hashes_match(dep_tree)) 278 | 279 | @patch('systemjs.base.json.load') 280 | def test_needs_update(self, mock_json_load): 281 | """ 282 | Assert that the check correctly reports if a module needs re-bundling or not 283 | """ 284 | tracer = SystemTracer() 285 | 286 | _file = open(os.path.join(settings.SYSTEMJS_CACHE_DIR, 'deps.json'), 'w') 287 | self.addCleanup(_file.close) 288 | self.addCleanup(lambda: os.remove(_file.name)) 289 | 290 | _depcache = self._depcache.copy() 291 | del _depcache['packages']['app/dummy']['app/dependency.js'] 292 | mock_json_load.return_value = _depcache 293 | 294 | # matching depcaches and hashes 295 | trace_result1 = { 296 | 'app/dummy.js': { 297 | 'name': 'app/dummy.js', 298 | 'timestamp': self.now, 299 | 'path': 'app/dummy.js', 300 | 'skip': False, 301 | } 302 | } 303 | with patch.object(tracer, 'trace', return_value=trace_result1): 304 | self.assertFalse(tracer.check_needs_update('app/dummy')) 305 | 306 | # now try a different trace result, which should trigger the need for an update 307 | trace_result2 = { 308 | 'app/dummy.js': { 309 | 'name': 'app/dummy.js', 310 | 'timestamp': self.now, 311 | 'path': 'app/dummy.js', 312 | 'skip': False, 313 | }, 314 | 'app/another_module.js': { 315 | 'name': 'app/another_module.js', 316 | 'timestamp': self.now, 317 | 'path': 'app/another_module.js', 318 | 'skip': False, 319 | } 320 | } 321 | with patch.object(tracer, 'trace', return_value=trace_result2): 322 | self.assertTrue(tracer.check_needs_update('app/dummy')) 323 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf.urls import patterns 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.views.generic import TemplateView 6 | 7 | 8 | urlpatterns = patterns( 9 | '', 10 | (r'^/', TemplateView.as_view(template_name='base.html')), 11 | ) 12 | 13 | urlpatterns += staticfiles_urlpatterns() 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py33-django18,py{27,34,35,py}-django{18,19,110} 3 | skip_missing_interpreters = true 4 | 5 | [testenv] 6 | deps = 7 | coverage 8 | coveralls 9 | django18: Django>=1.8,<1.9 10 | django19: Django>=1.9,<1.10 11 | django110: Django>=1.10,<1.11 12 | commands= 13 | coverage run --rcfile={toxinidir}/.coveragerc {toxinidir}/setup.py test 14 | 15 | [testenv:docs] 16 | basepython=python 17 | changedir=docs 18 | skipsdist=true 19 | deps= 20 | sphinx 21 | sphinx_rtd_theme 22 | commands= 23 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 24 | --------------------------------------------------------------------------------