Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ ARTIFACT_DESTINATION_FILE ?= ./tmp/idp.tar.gz
brakeman \
build_artifact \
check \
check_asset_strings \
docker_setup \
fast_setup \
fast_test \
Expand Down Expand Up @@ -77,8 +76,6 @@ lint: ## Runs all lint tests
@echo "--- es5-safe ---"
NODE_ENV=production yarn build && yarn es5-safe
# Other
@echo "--- asset check ---"
make check_asset_strings
@echo "--- lint yaml ---"
make lint_yaml
@echo "--- check assets are optimized ---"
Expand Down Expand Up @@ -164,9 +161,6 @@ update_pinpoint_supported_countries: ## Updates list of countries supported by P
lint_country_dialing_codes: update_pinpoint_supported_countries ## Checks that countries supported by Pinpoint for voice and SMS are up to date
(! git diff --name-only | grep config/country_dialing_codes.yml) || (echo "Error: Run 'make update_pinpoint_supported_countries' to update country codes"; exit 1)

check_asset_strings: ## Checks for strings
find ./app/javascript -name "*.js*" | xargs ./scripts/check-assets

build_artifact $(ARTIFACT_DESTINATION_FILE): ## Builds zipped tar file artifact with IDP source code and Ruby/JS dependencies
@echo "Building artifact into $(ARTIFACT_DESTINATION_FILE)"
bundle config set --local cache_all true
Expand Down
14 changes: 14 additions & 0 deletions app/helpers/script_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def render_javascript_pack_once_tags(*names)
if @scripts && (sources = AssetSources.get_sources(*@scripts)).present?
safe_join(
[
javascript_assets_tag(*@scripts),
javascript_polyfill_pack_tag,
javascript_include_tag(*sources, crossorigin: local_crossorigin_sources? ? true : nil),
],
Expand All @@ -34,6 +35,19 @@ def local_crossorigin_sources?
Rails.env.development? && ENV['WEBPACK_PORT'].present?
end

def javascript_assets_tag(*names)
assets = AssetSources.get_assets(*names)
if assets.present?
asset_map = assets.index_with { |path| asset_path(path) }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect I'll need to revisit this in future work, where we'll want to use this for the design system icon sprite, which can't be loaded across domains, so we'll need to have a way to pass host: asset_host like we do in IconComponent:

asset_path([design_system_asset_path('img/sprite.svg'), '#', icon].join, host: asset_host)

Thinking we might need to have some sort of allowlist, e.g.

Suggested change
asset_map = assets.index_with { |path| asset_path(path) }
asset_map = assets.index_with do |path|
asset_path(path, host: SAME_ORIGIN_ASSETS.include?(path) && asset_host)
end

(host option uses falsey assignment for defaulting)

content_tag(
:script,
asset_map.to_json,
{ type: 'application/json', data: { asset_map: '' } },
false,
)
end
end

def javascript_polyfill_pack_tag
javascript_include_tag_without_preload(
*AssetSources.get_sources('polyfill'),
Expand Down
32 changes: 32 additions & 0 deletions app/javascript/packages/assets/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getAssetPath } from './index';

describe('getAssetPath', () => {
beforeEach(() => {
delete getAssetPath.cache;
});

it('returns undefined', () => {
expect(getAssetPath('foo.svg')).to.be.undefined();
});

context('with global assets not including the provided asset', () => {
beforeEach(() => {
document.body.innerHTML = '<script type="application/json" data-asset-map>{}</script>';
});

it('returns undefined for missing assets', () => {
expect(getAssetPath('foo.svg')).to.be.undefined();
});
});

context('with global assets including the provided asset', () => {
beforeEach(() => {
document.body.innerHTML =
'<script type="application/json" data-asset-map>{"foo.svg":"bar.svg"}</script>';
});

it('returns the mapped asset path', () => {
expect(getAssetPath('foo.svg')).to.equal('bar.svg');
});
});
});
16 changes: 16 additions & 0 deletions app/javascript/packages/assets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
type AssetPaths = Record<string, string>;

export function getAssetPath(path: string): string | undefined {
if (!getAssetPath.cache) {
try {
const script = document.querySelector('[data-asset-map]') as HTMLScriptElement;
getAssetPath.cache = JSON.parse(script.textContent!);
} catch {
getAssetPath.cache = {};
}
}

return getAssetPath.cache![path];
}

getAssetPath.cache = undefined as AssetPaths | undefined;
9 changes: 9 additions & 0 deletions app/javascript/packages/assets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@18f/identity-assets",
"private": true,
"version": "1.0.0",
"main": "index.js",
"peerDependencies": {
"webpack": ">=5"
}
}
1 change: 1 addition & 0 deletions app/javascript/packages/assets/spec/fixtures/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
actual*
3 changes: 3 additions & 0 deletions app/javascript/packages/assets/spec/fixtures/in.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const getAssetPath = () => {};

globalThis.path = getAssetPath('foo.svg');
72 changes: 72 additions & 0 deletions app/javascript/packages/assets/webpack-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const { Compilation } = require('webpack');

/** @typedef {import('webpack/lib/ChunkGroup')} ChunkGroup */
/** @typedef {import('webpack/lib/Entrypoint')} Entrypoint */

/**
* Webpack plugin name.
*/
const PLUGIN = 'AssetsWebpackPlugin';

/**
* Regular expression matching calls to retrieve asset path.
*/
const GET_ASSET_CALL = /getAssetPath\)?\(\s*['"](.+?)['"]/g;

/**
* Given a file name, returns true if the file is a JavaScript file, or false otherwise.
*
* @param {string} filename
*
* @return {boolean}
*/
const isJavaScriptFile = (filename) => filename.endsWith('.js');

/**
* Given a string of source code, returns array of asset paths.
*
* @param source Source code.
*
* @return {string[]} Asset paths.
*/
const getAssetPaths = (source) =>
Array.from(source.matchAll(GET_ASSET_CALL)).map(([, path]) => path);

/**
* Adds the given asset file name to the list of files of the group's parent entrypoint.
*
* @param {string[]} filenames Asset filename.
* @param {ChunkGroup|Entrypoint} group Chunk group.
*/
const addFilesToEntrypoint = (filenames, group) =>
typeof group.getEntrypointChunk === 'function'
? filenames.forEach((filename) => group.getEntrypointChunk().files.add(filename))
: group.parentsIterable.forEach((parent) => addFilesToEntrypoint(filenames, parent));

class AssetsWebpackPlugin {
/**
* @param {import('webpack').Compiler} compiler
*/
apply(compiler) {
compiler.hooks.compilation.tap('compile', (compilation) => {
compilation.hooks.processAssets.tap(
{ name: PLUGIN, stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS },
() => {
compilation.chunks.forEach((chunk) => {
[...chunk.files].filter(isJavaScriptFile).forEach((filename) => {
const source = compilation.assets[filename].source();
const assetPaths = getAssetPaths(source);
if (assetPaths.length) {
Array.from(chunk.groupsIterable).forEach((group) => {
addFilesToEntrypoint(assetPaths, group);
});
}
});
});
},
);
});
}
}

module.exports = AssetsWebpackPlugin;
48 changes: 48 additions & 0 deletions app/javascript/packages/assets/webpack-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const path = require('path');
const { promises: fs } = require('fs');
const webpack = require('webpack');
const WebpackAssetsManifest = require('webpack-assets-manifest');
const AssetsWebpackPlugin = require('./webpack-plugin');

describe('AssetsWebpackPlugin', () => {
it('generates expected output', (done) => {
webpack(
{
mode: 'development',
devtool: false,
entry: path.resolve(__dirname, 'spec/fixtures/in.js'),
plugins: [
new AssetsWebpackPlugin(),
new WebpackAssetsManifest({
entrypoints: true,
publicPath: true,
writeToDisk: true,
output: 'actualmanifest.json',
}),
],
output: {
path: path.resolve(__dirname, 'spec/fixtures'),
filename: 'actual[name].js',
},
},
async (webpackError) => {
try {
expect(webpackError).to.be.null();

const manifest = JSON.parse(
await fs.readFile(
path.resolve(__dirname, 'spec/fixtures/actualmanifest.json'),
'utf-8',
),
);

expect(manifest.entrypoints.main.assets.svg).to.include.all.members(['foo.svg']);

done();
} catch (error) {
done(error);
}
},
);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useContext, useEffect, useRef, useState } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { getAssetPath } from '@18f/identity-assets';
import AcuantContext from '../context/acuant';
import useAsset from '../hooks/use-asset';

/**
* Capture type.
Expand Down Expand Up @@ -33,7 +33,6 @@ export function defineObservableProperty(object, property, onChangeCallback) {

function AcuantCaptureCanvas() {
const { isReady } = useContext(AcuantContext);
const { getAssetPath } = useAsset();
const { t } = useI18n();
const cameraRef = useRef(/** @type {HTMLDivElement?} */ (null));
const [captureType, setCaptureType] = useState(/** @type {AcuantCaptureType} */ ('AUTO'));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useI18n } from '@18f/identity-react-i18n';
import useAsset from '../hooks/use-asset';
import { getAssetPath } from '@18f/identity-assets';
import Warning from './warning';
import DocumentCaptureTroubleshootingOptions from './document-capture-troubleshooting-options';

Expand All @@ -17,7 +17,6 @@ import DocumentCaptureTroubleshootingOptions from './document-capture-troublesho
* @param {CaptureAdviceProps} props
*/
function CaptureAdvice({ onTryAgain, isAssessedAsGlare, isAssessedAsBlurry }) {
const { getAssetPath } = useAsset();
const { t } = useI18n();

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useI18n } from '@18f/identity-react-i18n';
import { useIfStillMounted } from '@18f/identity-react-hooks';
import useAsset from '../hooks/use-asset';
import { getAssetPath } from '@18f/identity-assets';
import useToggleBodyClassByPresence from '../hooks/use-toggle-body-class-by-presence';
import useImmutableCallback from '../hooks/use-immutable-callback';
import useFocusTrap from '../hooks/use-focus-trap';
Expand Down Expand Up @@ -57,7 +57,6 @@ export function useInertSiblingElements(containerRef) {
*/
function FullScreen({ onRequestClose = () => {}, label, children }, ref) {
const { t } = useI18n();
const { getAssetPath } = useAsset();
const ifStillMounted = useIfStillMounted();
const containerRef = useRef(/** @type {HTMLDivElement?} */ (null));
const onFocusTrapDeactivate = useImmutableCallback(ifStillMounted(onRequestClose));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useEffect } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { PageHeading } from '@18f/identity-components';
import useAsset from '../hooks/use-asset';
import { getAssetPath } from '@18f/identity-assets';

/**
* @typedef SubmissionInterstitialProps
Expand All @@ -14,7 +14,6 @@ import useAsset from '../hooks/use-asset';
*/
function SubmissionInterstitial({ autoFocus = false }) {
const { t } = useI18n();
const { getAssetPath } = useAsset();
const headingRef = useRef(/** @type {?HTMLHeadingElement} */ (null));
useEffect(() => {
if (autoFocus) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useContext, useEffect } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { PageHeading } from '@18f/identity-components';
import { getAssetPath } from '@18f/identity-assets';
import AnalyticsContext from '../context/analytics';
import useAsset from '../hooks/use-asset';

/** @typedef {import('react').ReactNode} ReactNode */

Expand Down Expand Up @@ -30,7 +30,6 @@ function Warning({
location,
remainingAttempts,
}) {
const { getAssetPath } = useAsset();
const { addPageAction } = useContext(AnalyticsContext);
const { t } = useI18n();
useEffect(() => {
Expand Down
7 changes: 0 additions & 7 deletions app/javascript/packages/document-capture/context/asset.js

This file was deleted.

1 change: 0 additions & 1 deletion app/javascript/packages/document-capture/context/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { default as AppContext } from './app';
export { default as AssetContext } from './asset';
export { default as DeviceContext } from './device';
export { default as AcuantContext, Provider as AcuantContextProvider } from './acuant';
export { default as HelpCenterContext, Provider as HelpCenterContextProvider } from './help-center';
Expand Down
21 changes: 0 additions & 21 deletions app/javascript/packages/document-capture/hooks/use-asset.js

This file was deleted.

4 changes: 0 additions & 4 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { composeComponents } from '@18f/identity-compose-components';
import {
AppContext,
DocumentCapture,
AssetContext,
DeviceContext,
AcuantContextProvider,
UploadContextProvider,
Expand Down Expand Up @@ -64,8 +63,6 @@ import { trackEvent } from '@18f/identity-analytics';
* @see UploadContext
*/

const { assets } = /** @type {DocumentCaptureGlobal} */ (window).LoginGov;

const appRoot = /** @type {HTMLDivElement} */ (document.getElementById('document-capture-form'));
const isMockClient = appRoot.hasAttribute('data-mock-client');
const keepAliveEndpoint = /** @type {string} */ (appRoot.getAttribute('data-keep-alive-endpoint'));
Expand Down Expand Up @@ -198,7 +195,6 @@ const noticeError = (error) =>
},
],
[ServiceProviderContextProvider, { value: getServiceProvider() }],
[AssetContext.Provider, { value: assets }],
[
FailedCaptureAttemptsContextProvider,
{
Expand Down
Loading