Skip to content

Commit

Permalink
Merge 92accc2 into 23b2067
Browse files Browse the repository at this point in the history
  • Loading branch information
ntarocco committed Jun 28, 2018
2 parents 23b2067 + 92accc2 commit 0af5174
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 44 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ python:
before_install:
- "nvm install 6; nvm use 6"
- "npm install -g webpack"
- "npm install -g webpack-cli"
- "travis_retry pip install --upgrade pip setuptools py"
- "travis_retry pip install twine wheel coveralls requirements-builder"
- "requirements-builder -e all --level=min setup.py > .travis-lowest-requirements.txt"
Expand Down
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ Optionally you can use PyWebpack to also:

* **Inject configuration:** PyWebpack will write a ``config.json`` into
your webpack project, which you can import in your webpack configuration. You
define what goes in the config, but you can use to let e.g. Webpack know
about output paths or dynamic entry points.
can define what goes in the config e.g. Let webpack know about output paths or
dynamic entry points.
* **Collect bundles:** If you Webpack project is spread over multiple Python
packages, PyWebpack can help you dynamically assemble the files into a
Webpack project. This is useful if you don't know until runtime which
Webpack project. This is useful if you don't know until build time which
packages are installed.
120 changes: 103 additions & 17 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Usage
A simple example
----------------

Let's take a simple ``package.json``::
Let's take a simple ``package.json`` that includes a Node module::

{
"private": true,
Expand All @@ -18,13 +18,8 @@ Let's take a simple ``package.json``::
}
}

Let's add to it a run script that executes webpack::

"scripts": {
"build": "webpack --config webpack.config.js"
}

And the corresponding ``webpack.config.js``::
And let's create a Webpack configuration file ``webpack.config.js`` to load the entry point ``src/index.js``
and output the built version in the ``dist`` folder::

var path = require('path');

Expand All @@ -37,34 +32,125 @@ And the corresponding ``webpack.config.js``::
}
};

Now, let's add to the ``packages.json`` a run script ``build`` that executes webpack::

"scripts": {
"build": "webpack --config webpack.config.js"
}

We can easily wrap our project in a :class:`~pywebpack.project.WebpackProject` object::

from pywebpack.project import WebpackProject
project = WebpackProject('.')
project_path = '.'
project = WebpackProject(project_path)

This will allow us to install the dependencies::
This will allow us to install the ``npm`` dependencies::

project.install()

And invoke webpack to build the assets::
And invoke the ``npm`` ``build`` command to execute ``webpack`` to build the entry points::

project.build()

Alternative, :meth:`~pywebpack.project.WebpackProject.buildall` can be used to execute both tasks at once.

Build time config
-----------------

If we need to inject extra configuration at build time we can define a
:class:`~pywebpack.project.WebpackTemplateProject`::

from pywebpack.bundle import WebpackTemplateProject
project = WebpackTemplateProject(
working_dir='tmp', # where config and assets files will be copied
project_template_dir='buildconfig', # where `package.json` and `webpack.config.js` reside
config={'debug': True},
config_path='build/config.json',
)

``debug: True`` is a configuration example that can be injected from ``python`` in the generated ``config.json``
and re-used in the ``webpack`` configuration.


Multiple asset bundles
----------------------

It is a common practice to split your assets in several files to decrease assets file sizes and speed up webpage
loading. Pywebpack lets you define a project that can have bundles, which are definitions of assets and ``npm``
dependencies.

Let's create two bundles::

from pywebpack.bundle import WebpackBundle
# the folder structure is:
# ./mainsite/
# - js/
mainsite = WebpackBundle(
'./mainsite',
entry={
'base': './js/base.js',
'products': './js/products.js',
},
dependencies={
'jquery': '^3.2.1'
}
)

# the folder structure is:
# ./backoffice/
# - js/
backoffice = WebpackBundle(
'./backoffice',
entry={
'base2': './js/base.js',
'admin': './js/admin.js',
},
dependencies={
'jquery': '^3.1.0'
}
)

Then, we prepare our ``webpack.config.js`` to build all the bundles (entrie), read from the ``config.json`` file which
is created by ``pywebpack``::

Using a manifest
----------------
var path = require('path');
var config = require('./config')

module.exports = {
context: path.resolve(__dirname),
entry: config.entry,
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
};

Let's create a project :class:`~pywebpack.project.WebpackBundleProject` which contains our bundles::

Manifests help you deal with long-term caching, by providing a mapping between the name of a resource and its "hashed"
from pywebpack.project import WebpackBundleProject
project = WebpackBundleProject(
working_dir='tmp', # where config and assets files will be copied
project_template_dir='buildconfig', # where `package.json` and `webpack.config.js` reside
bundles=[mainsite, backoffice]
)

When executing :meth:`~pywebpack.project.WebpackTemplateProject.buildall`, ``pywebpack`` will copy all the files contained
in the ``project_template_dir`` folder and all the assets of each bundle to the ``working_dir`` folder. Then, it will
install ``npm`` packages and run ``webpack`` to build the assets.

Manifest
--------

A manifest is the output file created by ``webpack`` which contains the list of all generated assets.
It also helps you deal with long-term caching, by providing a mapping between the name of a resource and its "hashed"
version::

{
"main.js": "main.75244bb780acd727ebd3.js"
}

This package supports manifest files that are compatible with `webpack-manifest-plugin`_, `webpack-yam-plugin`_ and
`webpack-bundle-tracker`_.
Pywebpack can parse the manifest file and make it available to your python project. It supports manifest files generated
using `webpack-manifest-plugin`_, `webpack-yam-plugin`_ and `webpack-bundle-tracker`_.

You will normally want webpack to add a hash of a file's contents to its name::

Expand All @@ -88,8 +174,8 @@ And then have it invoke your favorite manifest plugin::
The manifest entries can be retrieved as object attributes or items::

manifest.myresource
manifest['myresource']
manifest['main.js']
manifest['myresource']


.. _webpack-manifest-plugin: https://www.npmjs.com/package/webpack-manifest-plugin
Expand Down
42 changes: 27 additions & 15 deletions pywebpack/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from os import makedirs
from os.path import dirname, exists, join

import pkg_resources
from pynpm import NPMPackage, YarnPackage

from .helpers import cached, merge_deps
Expand Down Expand Up @@ -71,19 +70,19 @@ def buildall(self):
class WebpackTemplateProject(WebpackProject):
"""API for creating and building a webpack project based on a template.
Copies all files from a project template folder into a destionation path
and optionally writes a user provided config in JSON into the destionation
Copies all files from a project template folder into a destination path
and optionally writes a user provided config in JSON into the destination
path as well.
"""

def __init__(self, dest, project_template=None, config=None,
def __init__(self, working_dir, project_template_dir, config=None,
config_path=None, storage_cls=None):
"""Initialize templated folder."""
self._project_template = project_template
self._project_template_dir = project_template_dir
self._storage_cls = storage_cls or FileStorage
self._config = config
self._config_path = config_path or 'config.json'
super(WebpackTemplateProject, self).__init__(dest)
super(WebpackTemplateProject, self).__init__(working_dir)

@property
def config(self):
Expand All @@ -105,7 +104,7 @@ def storage_cls(self):

def create(self, force=None):
"""Create webpack project from a template."""
self.storage_cls(self._project_template, self.project_path).run(
self.storage_cls(self._project_template_dir, self.project_path).run(
force=force)

# Write config if not empty
Expand Down Expand Up @@ -133,13 +132,13 @@ def buildall(self):
class WebpackBundleProject(WebpackTemplateProject):
"""Build webpack project from multiple bundles."""

def __init__(self, dest, project_template=None, bundles=None, config=None,
config_path=None, storage_cls=None):
def __init__(self, working_dir, project_template_dir, bundles=None,
config=None, config_path=None, storage_cls=None):
"""Initialize templated folder."""
self._bundles_iter = bundles or []
super(WebpackBundleProject, self).__init__(
dest,
project_template=project_template,
working_dir,
project_template_dir=project_template_dir,
config=config or {},
config_path=config_path,
storage_cls=storage_cls,
Expand All @@ -155,10 +154,23 @@ def bundles(self):
@cached
def entry(self):
"""Get webpack entry points."""
res = {}
for b in self.bundles:
res.update(b.entry)
return res
entries = {}
entries_with_path = {}
error = 'Duplicated bundle entry for `{0}:{1}` in bundle `{2}` and ' \
'`{3}:{4}` in bundle `{5}`. Please choose another entry name.'

for bundle in self.bundles:
for name, filepath in bundle.entry.iteritems():
# check that there are no duplicated entries
if name in entries:
prev_filepath, prev_bundle_path = entries_with_path[name]
raise RuntimeError(error.format(name, prev_filepath,
prev_bundle_path, name,
filepath, bundle.path))
entries_with_path[name] = (filepath, bundle.path)

entries.update(bundle.entry)
return entries

@property
def config(self):
Expand Down
8 changes: 7 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ def builddir(sourcedir):

@pytest.fixture()
def bundledir(sourcedir):
"""Get build dir for projects."""
"""Get build dir for bundle."""
return join(sourcedir, 'bundle')


@pytest.fixture()
def bundledir2(sourcedir):
"""Get build dir for bundle."""
return join(sourcedir, 'bundle2')


@pytest.yield_fixture()
def destdir(tmpdir):
"""Get example output directory."""
Expand Down
Empty file added tests/projects/bundle2/main.js
Empty file.
44 changes: 36 additions & 8 deletions tests/test_pywebpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ def test_project_no_scripts(brokenprj):

def test_templateproject_create(templatedir, destdir):
"""Test template project creation."""
project = WebpackTemplateProject(destdir, project_template=templatedir)
project = WebpackTemplateProject(destdir, project_template_dir=templatedir)
assert not exists(project.npmpkg.package_json_path)
project.create()
assert exists(project.npmpkg.package_json_path)


def test_templateproject_clean(templatedir, destdir):
"""Test template project creation."""
project = WebpackTemplateProject(destdir, project_template=templatedir)
project = WebpackTemplateProject(destdir, project_template_dir=templatedir)
project.create()
assert exists(project.project_path)
project.clean()
Expand All @@ -109,8 +109,8 @@ def test_templateproject_create_config(templatedir, destdir):
expected_config = {'entry': './index.js'}

project = WebpackTemplateProject(
destdir,
project_template=templatedir,
working_dir=destdir,
project_template_dir=templatedir,
config=lambda: expected_config,
)

Expand All @@ -122,10 +122,10 @@ def test_templateproject_create_config(templatedir, destdir):
def test_templateproject_buildall(templatedir, destdir):
"""Test build all."""
project = WebpackTemplateProject(
destdir,
working_dir=destdir,
project_template_dir=templatedir,
config={'test': True},
config_path='build/config.json',
project_template=templatedir
)
project.buildall()
assert exists(project.config_path)
Expand All @@ -144,8 +144,8 @@ def test_bundleproject(builddir, bundledir, destdir):
}
)
project = WebpackBundleProject(
destdir,
project_template=builddir,
working_dir=destdir,
project_template_dir=builddir,
bundles=(x for x in [bundle]), # Test for iterator evaluation
config={'test': True, 'entry': False},
)
Expand Down Expand Up @@ -192,3 +192,31 @@ def test_bundleproject(builddir, bundledir, destdir):
project.build()
for p in distpaths:
assert exists(join(project.project_path, p))


@pytest.mark.xfail(raises=RuntimeError)
def test_bundle_duplicated_entries(builddir, bundledir, bundledir2, destdir):
"""Test bundles with duplicated entries."""
entry = {'app': './index.js'}
bundle1 = WebpackBundle(
bundledir,
entry={'app': './index.js'},
dependencies={
'lodash': '~4',
}
)
bundle2 = WebpackBundle(
bundledir2,
entry={'app': './main.js'},
dependencies={
'lodash': '~3',
}
)
project = WebpackBundleProject(
working_dir=destdir,
project_template_dir=builddir,
bundles=(x for x in [bundle1, bundle2]),
config={'test': True, 'entry': False},
)

project.create()
1 change: 1 addition & 0 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def test_iterfiles(sourcedir):
'buildtpl/package.json',
'buildtpl/webpack.config.js',
'bundle/index.js',
'bundle2/main.js',
'simple/index.js',
'simple/package.json',
'simple/webpack.config.js'
Expand Down

0 comments on commit 0af5174

Please sign in to comment.