Skip to content

Commit

Permalink
Merge branch 'release/2.0' into zetlen/fix-cart-retry-cycle
Browse files Browse the repository at this point in the history
  • Loading branch information
zetlen authored Oct 17, 2018
2 parents ff4aef4 + b32ff43 commit 123a35f
Show file tree
Hide file tree
Showing 18 changed files with 2,904 additions and 1,629 deletions.
3,737 changes: 2,399 additions & 1,338 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,7 @@
"nock": "^10.0.0",
"node-fetch": "^2.2.0",
"npm-run-all": "^4.1.2",
"one-time": "^0.0.4",
"portscanner": "^2.2.0",
"post-compile-webpack-plugin": "^0.1.2",
"prettier": "^1.13.5",
"prettier-check": "^2.0.0",
"prop-types": "^15.6.0",
Expand Down
1 change: 0 additions & 1 deletion packages/pwa-buildpack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,6 @@ or energy setting up their own services layer.
- [`MagentoResolver`](docs/MagentoResolver.md) -- Configures Webpack to resolve modules and assets in PWA Studio projects.
- [`UpwardPlugin`](docs/UpwardPlugin.md) -- Attaches a hot reloading UPWARD server, powered by [upward-js](../upward-js), to the Webpack dev server
- [`ServiceWorkerPlugin`](docs/ServiceWorkerPlugin.md) -- Creates a ServiceWorker with different settings based on dev scenarios
- [`DevServerReadyNotifierPlugin`](docs/DevServerReadyNotifierPlugin.md) -- Displays a prominent link in the console to a running dev environment once it is launched
- [`MagentoRootComponentsPlugin`](docs/MagentoRootComponentsPlugin.md) -- Divides static assets into bundled "chunks" based on components registered with the Magento PWA `RootComponent` interface
- [`magento-layout-loader`](docs/magento-layout-loader.md) -- Gives Magento modules/extensions the ability to inject or remove content blocks in a layout without modifying theme source files
Expand Down
111 changes: 84 additions & 27 deletions packages/pwa-buildpack/docs/PWADevServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ module.exports = async env => {
/* webpack entry, output, rules, etc */

devServer: await PWADevServer.configure({
publicPath: '/pub/static/frontend/Vendor/project/en_US/',
backendDomain: 'https://magento2.localdomain',
serviceWorkerFileName: 'sw.js',
paths: {
output: path.resolve(__dirname, 'dist/'),
},
id: 'magento-venia'
publicPath: '/',
provideSecureHost: true,
graphqlPlayground: {
queryDirs: ['src/queries']
}
})
};

Expand Down Expand Up @@ -54,17 +52,15 @@ PWA development has a couple of particular needs:
Furthermore, Magento PWAs are Magento 2 themes running on Magento 2 stores, so
they need to proxy backend requests to the backing store in a customized way.

PWADevServer` handles all these needs:
PWADevServer handles all these needs:

- Creates and caches a custom local hostname for the current project
- Adds the custom local hostname to `/etc/hosts` 🔐
- Creates and caches an SSL certificate for the custom local hostname
- Adds the certificate to the OS-level keychain so browsers trust it 🔐
- Customizes the `webpack-dev-server` instance to:
- Proxy all asset requests not managed by webpack to the Magento store
- Emulate the public path settings of the Magento store
- Automatically switch domain names in HTML attributes
- Debug or disable ServiceWorkers
- Adds verbose debugging information to error pages
- Provides a [GraphQL Playground][graphql-playground] to debug the GraphQL
queries in the project

*The 🔐 in the above list indicates that you may be asked for a password at
this step.*
Expand All @@ -77,19 +73,80 @@ configuration.

#### `PWADevServer.configure(options: PWADevServerOptions): Promise<devServer>`

#### `options`
#### `PWADevServerOptions`

- `id: string`: **Required.** A unique ID for this project. Project name is
recommended, but you can use any domain-name-safe string. If you're
developing several copies of a project simultaneously, you can use this ID to
distinguish them in the internal tooling; for example, this id will be used
to create your dev domain name.
- `publicPath: string`: **Required.** The public path of project assets in the
backend server, e.g. `'/'`: **Required.** The URL of the backing store.
- `paths: object`: **Required.** Local absolute paths to project folders.
- `output`: Directory for built JavaScript files.
- `serviceWorkerFileName: string`: **Required.** The name of the ServiceWorker
file this project generates, e.g. `'sw.js'`.
- `changeOrigin: boolean`: ⚠️ **(experimental)** Try to parse any HTML responses
from the proxied Magento backend, and replace its domain name with the
dev server domain name. Default `false`.
backend server, e.g. `'/'`.
- `provideSecureHost: boolean | SecureHostOptions`: Use a [secure and unique hostname for the dev server.](#securehostoptions)
- `graphqlPlayground: GraphQLPlaygroundOptions`: Add a [pre-populated GraphQL Playground to the dev server.](#graphqlplayground)
- `id: string`: :no_entry_sign: **_Deprecated._** A unique ID for this project. This id will be used to create your dev domain name. Setting `id: 'foo'` is equivalent to:

```js
provideSecureHost: { subdomain: 'foo', addUniqueHash: false }
```

#### `SecureHostOptions`

Most local development servers run on `http://localhost`, with a constant port
number like `3000` or `8080`. This common setup is problematic for PWA work:

- Browsers only enable PWA features like ServiceWorkers on secure HTTPS.
- Browsers cache PWAs by "scope", which is an origin plus a path (usually `/`).
Running all local development projects at a common domain, such as
`https://localhost:8000`, will cause conflicts between ServiceWorkers.

To solve these problems, enable autogeneration of a unique secure host using the
`provideSecureHost` configuration option. PWADevServer generates the unique
hostname based on the project name in `package.json` if available, or a
custom name if provided. It also uses a hash of the filesystem path of the
project to choose a persistent unique port and append a short random sequence
to the domain name. Since this port and string are derived from the
filesystem path, they will stay the same unless the project is moved.

:information_source: If PWADevServer detects that the project port is in use,
it will print a warning and use a different port temporarily.

Configuration options for this feature are:

- `subdomain: string`: A custom subdomain string to use. If provided, this
supersedes the name in `package.json`.
- `exactDomain: string` A fully qualified domain to use. By default,
PWADevServer generates all unique hosts as subdomains of `local.pwadev`. Use
this option to override this behavior and provide the exact domain to use.
The custom host will not have a unique hash, but it will have a unique port.
- `addUniqueHash: boolean`: Use the filesystem path hash to create a short
string of URL-safe characters to append to the subdomain. Ensures total
uniqueness of domain. Default `true`. If `exactDomain` is specified, this
option has no effect.

:information_source: Default behavior is to use the `package.json` plus hash
as a subdomain of `local.pwadev`. To use this default behavior, simply set
`provideSecureHost: true`.

:information_source: There is no configuration option for disabling unique port
generation and use. To override the port for one session, use the environment
variable `PWA_STUDIO_PORTS_DEVELOPMENT` to specify a port.

:information_source: The first time the DevServer runs, it will prompt you for
system password. It needs temporary permission to edit the hostfile and trust
the SSL certificate. Use the login password for your computer.

#### `GraphQLPlaygroundOptions`

[GraphQL Playground][graphql-playground] is an enhanced version of GraphiQL, an
in-browser GraphQL debugging tool. PWADevServer can provide a Playground
at the special path `/graphiql`. To enable it, add the `graphqlPlayground`
configuration option.

Configuration options for this feature are:

- `queryDirs: string[]`: A list of directories containing GraphQL files in your
project. If you provide this list, PWADevServer will scan these directories
for `.graphql` files, and prepopulate the playground with a new tab for each
query it finds.

:information_source: To enable the feature without providing queryDirs, simply
set `graphqlPlayground: true`.


[graphql-playground]: <https://github.com/prisma/graphql-playground>
3 changes: 1 addition & 2 deletions packages/pwa-buildpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@
"execa": "^1.0.0",
"express": "^4.16.3",
"graphql": "^0.13.2",
"graphql-playground-middleware-express": "^1.7.3",
"graphql-tag": "^2.9.2",
"http-proxy-middleware": "^0.19.0",
"lodash.get": "^4.4.2",
"node-fetch": "^2.2.0",
"one-time": "^0.0.4",
"portscanner": "^2.2.0",
"post-compile-webpack-plugin": "^0.1.2",
"react-dom": "^16.5.2",
"webpack-sources": "^1.1.0",
"workbox-webpack-plugin": "^3.0.0-beta.1",
Expand Down
78 changes: 77 additions & 1 deletion packages/pwa-buildpack/src/WebpackTools/PWADevServer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
const debug = require('../util/debug').makeFileLogger(__filename);
const debugErrorMiddleware = require('debug-error-middleware').express;
const {
default: playgroundMiddleware
} = require('graphql-playground-middleware-express');
const url = require('url');
const optionsValidator = require('../util/options-validator');
const chalk = require('chalk');
const configureHost = require('../Utilities/configureHost');
const portscanner = require('portscanner');
const { readdir: readdirAsync, readFile: readFileAsync } = require('fs');
const { promisify } = require('util');
const readdir = promisify(readdirAsync);
const readFile = promisify(readFileAsync);
const { resolve, relative } = require('path');
const boxen = require('boxen');

const secureHostWarning = chalk.redBright(
` To enable all PWA features and avoid ServiceWorker collisions, PWA Studio
Expand Down Expand Up @@ -46,8 +55,36 @@ const PWADevServer = {
version: true,
warnings: true
},
after(app) {
after(app, server) {
app.use(debugErrorMiddleware());
let readyNotice = chalk.green(
`PWADevServer ready at ${chalk.greenBright.underline(
devServerConfig.publicPath
)}`
);
if (config.graphqlPlayground) {
readyNotice +=
'\n' +
chalk.blueBright(
`GraphQL Playground ready at ${chalk.blueBright.underline(
new url.URL(
'/graphiql',
devServerConfig.publicPath
)
)}`
);
}
server.middleware.waitUntilValid(() =>
console.log(
boxen(readyNotice, {
borderColor: 'gray',
float: 'center',
align: 'center',
margin: 1,
padding: 1
})
)
);
}
};
const { id, provideSecureHost } = config;
Expand Down Expand Up @@ -125,6 +162,45 @@ be configured to have the same effect as 'id'.
console.warn(secureHostWarning + helpText);
}

const { graphqlPlayground } = config;
if (graphqlPlayground) {
const { queryDirs = [] } = config.graphqlPlayground;
const endpoint = '/graphql';

const tabs = await queryDirs.reduce(
async (queryTabs, dir) => [
...(await queryTabs),
...(await Promise.all(
(await readdir(dir))
.filter(filename => filename.endsWith('.graphql'))
.map(async queryFile => ({
endpoint,
name: relative(
process.cwd(),
resolve(dir, queryFile)
),
query: await readFile(
resolve(dir, queryFile),
'utf8'
)
}))
))
],
[]
);

devServerConfig.before = app => {
// this middleware has a bad habit of calling next() when it
// should not, so let's give it a noop next()
const noop = () => {};
const middleware = playgroundMiddleware({
endpoint,
tabs
});
app.get('/graphiql', (req, res) => middleware(req, res, noop));
};
}

// Public path must be an absolute URL to enable hot module replacement
devServerConfig.publicPath = url.format({
protocol: devServerConfig.https ? 'https:' : 'http:',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
jest.mock('portscanner');
jest.mock('graphql-playground-middleware-express');
jest.mock('../../Utilities/configureHost');

const { resolve } = require('path');
const portscanner = require('portscanner');
const stripAnsi = require('strip-ansi');
const {
default: playgroundMiddleware
} = require('graphql-playground-middleware-express');
const configureHost = require('../../Utilities/configureHost');
const { PWADevServer } = require('../');

Expand Down Expand Up @@ -32,8 +38,15 @@ const simulate = {
}
};

beforeEach(() => jest.spyOn(console, 'warn').mockImplementation());
afterEach(() => console.warn.mockRestore());
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation();
jest.spyOn(console, 'log').mockImplementation();
});

afterEach(() => {
console.warn.mockRestore();
console.log.mockRestore();
});

test('.configure() returns a configuration object for the `devServer` property of a webpack config', async () => {
const devServer = await PWADevServer.configure({
Expand Down Expand Up @@ -165,7 +178,7 @@ test('.configure() errors on bad "provideSecureHost" option', async () => {
).rejects.toThrowError('Unrecognized argument');
});

test('debugErrorMiddleware attached', async () => {
test('debugErrorMiddleware and notifier attached', async () => {
const config = {
publicPath: 'full/path/to/publicPath'
};
Expand All @@ -176,6 +189,81 @@ test('debugErrorMiddleware attached', async () => {
const app = {
use: jest.fn()
};
devServer.after(app);
const waitUntilValid = jest.fn();
const server = {
middleware: {
waitUntilValid
}
};
devServer.after(app, server);
expect(app.use).toHaveBeenCalledWith(expect.any(Function));
expect(waitUntilValid).toHaveBeenCalled();
const [notifier] = waitUntilValid.mock.calls[0];
expect(notifier).toBeInstanceOf(Function);
notifier();
const consoleOutput = stripAnsi(console.log.mock.calls[0][0]);
expect(consoleOutput).toMatch('PWADevServer ready at');
});

test('graphql-playground middleware attached', async () => {
const config = {
publicPath: 'full/path/to/publicPath',
graphqlPlayground: true
};

const middleware = jest.fn();
playgroundMiddleware.mockReturnValueOnce(middleware);

const devServer = await PWADevServer.configure(config);

expect(devServer.before).toBeInstanceOf(Function);
const app = {
get: jest.fn(),
use: jest.fn()
};
const waitUntilValid = jest.fn();
const server = {
middleware: {
waitUntilValid
}
};
devServer.before(app, server);
expect(playgroundMiddleware).toHaveBeenCalled();
expect(playgroundMiddleware.mock.calls[0][0]).toMatchSnapshot();
expect(app.get).toHaveBeenCalled();
const [endpoint, middlewareProxy] = app.get.mock.calls[0];
expect(endpoint).toBe('/graphiql');
expect(middlewareProxy).toBeInstanceOf(Function);
const req = {};
const res = {};
middlewareProxy(req, res);
expect(middleware).toHaveBeenCalledWith(req, res, expect.any(Function));
devServer.after(app, server);
expect(waitUntilValid).toHaveBeenCalled();
const [notifier] = waitUntilValid.mock.calls[0];
notifier();
const consoleOutput = stripAnsi(console.log.mock.calls[0][0]);
expect(consoleOutput).toMatch(/PWADevServer ready at/);
expect(consoleOutput).toMatch(/GraphQL Playground ready at .+?\/graphiql/);
});

test('graphql-playground middleware attached with custom queryDirs', async () => {
const config = {
publicPath: 'full/path/to/publicPath',
graphqlPlayground: {
queryDirs: [resolve(__dirname, '__fixtures__/queries')]
}
};

const middleware = jest.fn();
playgroundMiddleware.mockReturnValueOnce(middleware);

const devServer = await PWADevServer.configure(config);

expect(devServer.before).toBeInstanceOf(Function);
const app = {
get: jest.fn()
};
devServer.before(app);
expect(playgroundMiddleware.mock.calls[0][0]).toMatchSnapshot();
});
Loading

0 comments on commit 123a35f

Please sign in to comment.