diff --git a/.gitignore b/.gitignore
index 4c394dab75..803d38cc63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,6 @@ docker/certs
## exception: commit node_modules folders in test fixtures
!**/__fixtures__/*/node_modules
+
+## May temporarily create tarballs during scaffolding debug
+packages/**/*.tgz
diff --git a/dev.dockerfile b/dev.dockerfile
index e5bf955f16..9b8fb1aa09 100644
--- a/dev.dockerfile
+++ b/dev.dockerfile
@@ -17,8 +17,9 @@ RUN apk --no-cache --virtual add \
ENV CI=true
# copy just the dependency files and configs needed for install
-COPY packages/create-pwa/package.json ./packages/create-pwa/package.json
COPY packages/babel-preset-peregrine/package.json ./packages/babel-preset-peregrine/package.json
+COPY packages/create-pwa/package.json ./packages/create-pwa/package.json
+COPY packages/extensions/upward-security-headers/package.json ./packages/extensions/upward-security-headers/package.json
COPY packages/graphql-cli-validate-magento-pwa-queries/package.json ./packages/graphql-cli-validate-magento-pwa-queries/package.json
COPY packages/pagebuilder/package.json ./packages/pagebuilder/package.json
COPY packages/peregrine/package.json ./packages/peregrine/package.json
diff --git a/package.json b/package.json
index 80942ff22f..29e2b70a54 100755
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"workspaces": [
"packages/babel-preset-peregrine",
"packages/create-pwa",
+ "packages/extensions/*",
"packages/graphql-cli-validate-magento-pwa-queries",
"packages/pagebuilder",
"packages/peregrine",
diff --git a/packages/extensions/upward-security-headers/intercept.js b/packages/extensions/upward-security-headers/intercept.js
new file mode 100644
index 0000000000..9bc9880f0b
--- /dev/null
+++ b/packages/extensions/upward-security-headers/intercept.js
@@ -0,0 +1,26 @@
+const SECURITY_HEADER_DEFINITION = 'veniaSecurityHeaders';
+
+module.exports = targets => {
+ const builtins = targets.of('@magento/pwa-buildpack');
+
+ builtins.specialFeatures.tap(features => {
+ features[targets.name] = { upward: true };
+ });
+
+ builtins.transformUpward.tapPromise(async definitions => {
+ if (!definitions[SECURITY_HEADER_DEFINITION]) {
+ throw new Error(
+ `${
+ targets.name
+ } could not find its own definition in the emitted upward.yml`
+ );
+ }
+
+ const shellHeaders = definitions.veniaAppShell.inline.headers.inline;
+ const securityHeaders = definitions[SECURITY_HEADER_DEFINITION].inline;
+
+ for (const name of Object.keys(securityHeaders)) {
+ shellHeaders[name] = `${SECURITY_HEADER_DEFINITION}.${name}`;
+ }
+ });
+};
diff --git a/packages/extensions/upward-security-headers/package.json b/packages/extensions/upward-security-headers/package.json
new file mode 100644
index 0000000000..b6a7fe0182
--- /dev/null
+++ b/packages/extensions/upward-security-headers/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@magento/upward-security-headers",
+ "version": "0.0.1",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Add security headers to UPWARD",
+ "main": "intercept.js",
+ "scripts": {
+ "clean": " "
+ },
+ "repository": "github:magento/pwa-studio",
+ "author": "Magento Commerce",
+ "license": "(OSL-3.0 OR AFL-3.0)",
+ "peerDependencies": {
+ "@magento/pwa-buildpack": "^5.1.1",
+ "@magento/venia-ui": "^3.0.0",
+ "rimraf": "~2.6.3",
+ "webpack": "~4.38.0"
+ },
+ "pwa-studio": {
+ "targets": {
+ "intercept": "./intercept"
+ }
+ }
+}
diff --git a/packages/extensions/upward-security-headers/upward.yml b/packages/extensions/upward-security-headers/upward.yml
new file mode 100644
index 0000000000..6df4db9f9f
--- /dev/null
+++ b/packages/extensions/upward-security-headers/upward.yml
@@ -0,0 +1,25 @@
+veniaSecurityHeaders:
+ resolver: inline
+ inline:
+ content-security-policy:
+ resolver: template
+ engine: mustache
+ provide:
+ backend: env.MAGENTO_BACKEND_URL
+ template:
+ resolver: conditional
+ when:
+ - matches: env.NODE_ENV
+ pattern: development
+ use:
+ inline: ""
+ default:
+ inline: "script-src http: https: {{ backend }}; style-src 'self' https: 'unsafe-inline' {{ backend }}; img-src data: http: https:; object-src 'none'; base-uri 'none'; child-src 'self'; font-src 'self' fonts.gstatic.com; frame-src assets.braintreegateway.com"
+ strict-transport-security:
+ inline: max-age=31536000
+ x-content-type-options:
+ inline: nosniff
+ x-frame-options:
+ inline: SAMEORIGIN
+ x-xss-protection:
+ inline: '1; mode=block'
diff --git a/packages/pwa-buildpack/lib/BuildBus/declare-base.js b/packages/pwa-buildpack/lib/BuildBus/declare-base.js
index 9fdae0d51f..f3784e8073 100644
--- a/packages/pwa-buildpack/lib/BuildBus/declare-base.js
+++ b/packages/pwa-buildpack/lib/BuildBus/declare-base.js
@@ -138,7 +138,47 @@ module.exports = targets => {
* @param {Object<(string, boolean)>} featureFlags
* @memberof BuiltinTargets
*/
- specialFeatures: new targets.types.Sync(['special'])
+ specialFeatures: new targets.types.Sync(['special']),
+
+ /**
+ * @callback transformUpwardIntercept
+ * @param {object} Parsed UPWARD definition object.
+ * @returns {Promise} - Interceptors do not need to return.
+ */
+
+ /**
+ * Exposes the fully merged UPWARD definition for fine tuning. The
+ * UpwardIncludePlugin does a simple shallow merge of the upward.yml
+ * files in every package which sets the `upward: true` flag in the
+ * `specialFeatures` object. After that is complete,
+ * UpwardIncludePlugin calls this target with the parsed and merged
+ * definition.
+ *
+ * @example
Send empty responses in maintenance mode.
+ * targets.of('@magento/pwa-buildpack').transformUpward.tap(def => {
+ * const guardMaintenanceMode = (prop, inline) => {
+ * def[prop] = {
+ * when: [
+ * {
+ * matches: 'env.MAINTENANCE_MODE',
+ * pattern: '.',
+ * use: { inline }
+ * }
+ * ],
+ * default: def[prop]
+ * }
+ * }
+ *
+ * guardMaintenanceMode('status', 503);
+ * guardMaintenanceMode('body', '')
+ * })
+ *
+ *
+ * @type {tapable.AsyncSeriesHook}
+ * @param {transformUpwardIntercept} interceptor
+ * @memberof BuiltinTargets
+ */
+ transformUpward: new targets.types.AsyncSeries(['definitions'])
};
/**
diff --git a/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getClientConfig.js b/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getClientConfig.js
index c277f23438..351289c0a2 100644
--- a/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getClientConfig.js
+++ b/packages/pwa-buildpack/lib/WebpackTools/configureWebpack/getClientConfig.js
@@ -33,7 +33,8 @@ async function getClientConfig(opts) {
vendor,
projectConfig,
stats,
- resolver
+ resolver,
+ bus
} = opts;
let vendorTest = '[\\/]node_modules[\\/]';
@@ -82,6 +83,7 @@ async function getClientConfig(opts) {
}),
new webpack.EnvironmentPlugin(projectConfig.env),
new UpwardIncludePlugin({
+ bus,
upwardDirs: [...hasFlag('upward'), context]
}),
new WebpackAssetsManifest({
diff --git a/packages/pwa-buildpack/lib/WebpackTools/plugins/UpwardIncludePlugin.js b/packages/pwa-buildpack/lib/WebpackTools/plugins/UpwardIncludePlugin.js
index adadda8581..c6c8819b4b 100644
--- a/packages/pwa-buildpack/lib/WebpackTools/plugins/UpwardIncludePlugin.js
+++ b/packages/pwa-buildpack/lib/WebpackTools/plugins/UpwardIncludePlugin.js
@@ -10,7 +10,8 @@ const jsYaml = require('js-yaml');
* autodetects file assets relied on by those configurations
*/
class UpwardIncludePlugin {
- constructor({ upwardDirs }) {
+ constructor({ bus, upwardDirs }) {
+ this.bus = bus;
this.upwardDirs = upwardDirs;
this.definition = {};
debug('created with dirs: %s', upwardDirs);
@@ -33,7 +34,12 @@ class UpwardIncludePlugin {
context,
from: './upward.yml',
to: './upward.yml',
- transform: () => jsYaml.safeDump(this.definition)
+ transform: async () => {
+ await this.bus
+ .getTargetsOf('@magento/pwa-buildpack')
+ .transformUpward.promise(this.definition);
+ return jsYaml.safeDump(this.definition);
+ }
}
};
this.dirs = new Set([...this.upwardDirs, context]);
diff --git a/packages/pwa-buildpack/lib/WebpackTools/plugins/__tests__/UpwardIncludePlugin.spec.js b/packages/pwa-buildpack/lib/WebpackTools/plugins/__tests__/UpwardIncludePlugin.spec.js
index 8da1091f4e..89becb4a40 100644
--- a/packages/pwa-buildpack/lib/WebpackTools/plugins/__tests__/UpwardIncludePlugin.spec.js
+++ b/packages/pwa-buildpack/lib/WebpackTools/plugins/__tests__/UpwardIncludePlugin.spec.js
@@ -2,6 +2,8 @@ const { join } = require('path');
const MemoryFS = require('memory-fs');
const webpack = require('webpack');
const jsYaml = require('js-yaml');
+
+const { mockBuildBus } = require('../../../TestHelpers');
const UpwardIncludePlugin = require('../UpwardIncludePlugin');
const basic3PageProjectDir = join(
@@ -31,6 +33,12 @@ const compile = config =>
});
test('merges upward files and resources', async () => {
+ const bus = mockBuildBus({
+ context: __dirname,
+ dependencies: [{ name: '@magento/pwa-buildpack' }]
+ });
+ bus.runPhase('declare');
+
const config = {
context: basic1PageProjectDir,
entry: {
@@ -41,6 +49,7 @@ test('merges upward files and resources', async () => {
},
plugins: [
new UpwardIncludePlugin({
+ bus,
upwardDirs: [basic3PageProjectDir, basic1PageProjectDir]
})
]
diff --git a/packages/pwa-buildpack/lib/cli/create-project.js b/packages/pwa-buildpack/lib/cli/create-project.js
index 2e2c99638e..7c358ce871 100644
--- a/packages/pwa-buildpack/lib/cli/create-project.js
+++ b/packages/pwa-buildpack/lib/cli/create-project.js
@@ -219,15 +219,6 @@ module.exports.handler = async function buildpackCli(argv) {
prettyLogger.success(`Installed dependencies for '${name}' project`);
}
- if (process.env.DEBUG_PROJECT_CREATION) {
- prettyLogger.info('Debug: Removing generated tarballs');
- const pkgDir = require('pkg-dir');
- const monorepoDir = resolve(pkgDir.sync(__dirname), '../../');
- prettyLogger.info(
- execa.shellSync('rm -v packages/*/*.tgz', { cwd: monorepoDir })
- .stdout
- );
- }
const showCommand = command =>
' - ' + chalk.whiteBright(`${params.npmClient} ${command}`);
const buildpackPrefix = params.npmClient === 'npm' ? ' --' : '';
diff --git a/packages/venia-concept/_buildpack/__tests__/create.spec.js b/packages/venia-concept/_buildpack/__tests__/create.spec.js
index c494f09f55..6af48e7afd 100644
--- a/packages/venia-concept/_buildpack/__tests__/create.spec.js
+++ b/packages/venia-concept/_buildpack/__tests__/create.spec.js
@@ -182,13 +182,20 @@ test('forces yarn client, local deps, and console debugging if DEBUG_PROJECT_CRE
}),
[resolve(packagesRoot, 'peregrine/package.json')]: JSON.stringify({
name: '@magento/peregrine'
- }),
- [resolve(packagesRoot, 'bad-package/package.json')]: 'bad json',
- [resolve(packagesRoot, 'some-file.txt')]: 'not a package'
+ })
};
const fs = mockFs(files);
+ // mock the yarn workspaces response
+ execSync.mockReturnValueOnce(
+ JSON.stringify({
+ foo: { location: '/repo/packages/me' },
+ '@magento/peregrine': { location: 'packages/peregrine' },
+ '@magento/venia-ui': { location: 'packages/venia-ui' }
+ })
+ );
+
await runCreate(fs, {
name: 'foo',
author: 'bar',
diff --git a/packages/venia-concept/_buildpack/create.js b/packages/venia-concept/_buildpack/create.js
index 64bb9f35e4..77c6b95456 100644
--- a/packages/venia-concept/_buildpack/create.js
+++ b/packages/venia-concept/_buildpack/create.js
@@ -132,35 +132,45 @@ function setDebugDependencies(fs, pkg) {
console.warn(
'DEBUG_PROJECT_CREATION: Debugging Venia _buildpack/create.js, so we will assume we are inside the pwa-studio repo and replace those package dependency declarations with local file paths.'
);
+
+ const { execSync } = require('child_process');
const overridden = {};
- const workspaceDir = resolve(__dirname, '../../');
- fs.readdirSync(workspaceDir).forEach(packageDir => {
- const packagePath = resolve(workspaceDir, packageDir);
- if (!fs.statSync(packagePath).isDirectory()) {
- return;
- }
- let name;
- try {
- name = fs.readJsonSync(resolve(packagePath, 'package.json')).name;
- } catch (e) {} // eslint-disable-line no-empty
- if (
- // these should not be deps
- !name ||
- name === '@magento/create-pwa' ||
- name === '@magento/venia-concept'
- ) {
+ const monorepoDir = resolve(__dirname, '../../../');
+
+ // The Yarn "workspaces info" command outputs JSON as of v1.22.4.
+ // The -s flag suppresses all other non-JSON logging output.
+ const yarnWorkspaceInfoCmd = 'yarn -s workspaces info';
+ const workspaceInfo = execSync(yarnWorkspaceInfoCmd, { cwd: monorepoDir });
+
+ let packageDirs;
+ try {
+ packageDirs = Object.values(JSON.parse(workspaceInfo)).map(
+ ({ location }) => resolve(monorepoDir, location)
+ );
+ } catch (e) {
+ throw new Error(
+ `DEBUG_PROJECT_CREATION: Could not parse output of '${yarnWorkspaceInfoCmd}:\n${workspaceInfo}. Please check your version of yarn is v1.22.4+.`
+ );
+ }
+
+ packageDirs.forEach(packageDir => {
+ const name = fs.readJsonSync(resolve(packageDir, 'package.json')).name;
+ const packagesToSkip = [
+ '@magento/create-pwa',
+ '@magento/venia-concept'
+ ];
+
+ if (packagesToSkip.includes(name)) {
return;
}
+
console.warn(`DEBUG_PROJECT_CREATION: Packing ${name} for local usage`);
let filename;
let packOutput;
try {
- packOutput = require('child_process').execSync(
- 'npm pack -s --ignore-scripts --json',
- {
- cwd: packagePath
- }
- );
+ packOutput = execSync('npm pack -s --ignore-scripts --json', {
+ cwd: packageDir
+ });
filename = JSON.parse(packOutput)[0].filename;
} catch (e) {
throw new Error(
@@ -169,7 +179,7 @@ function setDebugDependencies(fs, pkg) {
}`
);
}
- const localDep = `file://${resolve(packagePath, filename)}`;
+ const localDep = `file://${resolve(packageDir, filename)}`;
['dependencies', 'devDependencies', 'optionalDependencies'].forEach(
depType => {
if (pkg[depType] && pkg[depType][name]) {
diff --git a/packages/venia-concept/package.json b/packages/venia-concept/package.json
index aca91c0609..8a578eb53a 100644
--- a/packages/venia-concept/package.json
+++ b/packages/venia-concept/package.json
@@ -52,6 +52,7 @@
"@magento/peregrine": "~6.0.0",
"@magento/pwa-buildpack": "~5.1.1",
"@magento/upward-js": "~4.0.1",
+ "@magento/upward-security-headers": "~0.0.1",
"@magento/venia-ui": "~3.0.0",
"@storybook/react": "~5.2.6",
"apollo-cache-inmemory": "~1.6.3",
diff --git a/prod.dockerfile b/prod.dockerfile
index de50163b38..16ca6d3604 100644
--- a/prod.dockerfile
+++ b/prod.dockerfile
@@ -13,8 +13,9 @@ RUN apk --no-cache --virtual add \
ENV CI=true
# copy just the dependency files and configs needed for install
-COPY packages/create-pwa/package.json ./packages/create-pwa/package.json
COPY packages/babel-preset-peregrine/package.json ./packages/babel-preset-peregrine/package.json
+COPY packages/create-pwa/package.json ./packages/create-pwa/package.json
+COPY packages/extensions/upward-security-headers/package.json ./packages/extensions/upward-security-headers/package.json
COPY packages/graphql-cli-validate-magento-pwa-queries/package.json ./packages/graphql-cli-validate-magento-pwa-queries/package.json
COPY packages/pagebuilder/package.json ./packages/pagebuilder/package.json
COPY packages/peregrine/package.json ./packages/peregrine/package.json