@financial-times/dotcom-server-asset-loader 7.3.1 → 7.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/dotcom-server-asset-loader",
3
- "version": "7.3.1",
3
+ "version": "7.3.3",
4
4
  "description": "",
5
5
  "main": "dist/node/index.js",
6
6
  "types": "src/index.ts",
@@ -22,7 +22,8 @@
22
22
  "npm": "7.x || 8.x"
23
23
  },
24
24
  "files": [
25
- "dist/"
25
+ "dist/",
26
+ "src/"
26
27
  ],
27
28
  "repository": {
28
29
  "type": "git",
@@ -39,4 +40,4 @@
39
40
  "devDependencies": {
40
41
  "check-engine": "^1.10.1"
41
42
  }
42
- }
43
+ }
@@ -0,0 +1,137 @@
1
+ import path from 'path'
2
+ import urlJoin from 'url-join'
3
+ import { loadFile } from './helpers/loadFile'
4
+ import { loadManifest } from './helpers/loadManifest'
5
+
6
+ export interface AssetLoaderOptions {
7
+ /**
8
+ * The name of the asset manifest file
9
+ * @default "manifest.json"
10
+ */
11
+ manifestFileName?: string
12
+
13
+ /**
14
+ * The public-facing URL for the static assets
15
+ */
16
+ publicPath?: string
17
+
18
+ /**
19
+ * The absolute path to the directory of static assets
20
+ * @default path.resolve('./public')
21
+ */
22
+ fileSystemPath?: string
23
+
24
+ /**
25
+ * Store files in memory when accessed
26
+ * @default false
27
+ */
28
+ cacheFileContents?: boolean
29
+
30
+ /**
31
+ * The asset manifest
32
+ */
33
+ manifest?: { [asset: string]: string }
34
+ }
35
+
36
+ const defaultOptions: AssetLoaderOptions = {
37
+ publicPath: '/',
38
+ manifestFileName: 'manifest.json',
39
+ fileSystemPath: path.resolve('./public'),
40
+ cacheFileContents: false
41
+ }
42
+
43
+ type TEntrypoint = {
44
+ [type: string]: string[]
45
+ }
46
+
47
+ type TEntrypoints = {
48
+ entrypoints?: {
49
+ [name: string]: TEntrypoint
50
+ }
51
+ }
52
+
53
+ type TFiles = {
54
+ [name: string]: string
55
+ }
56
+
57
+ export type TManifest = TEntrypoints & TFiles
58
+
59
+ export class AssetLoader {
60
+ public options: AssetLoaderOptions
61
+ public manifest: TManifest
62
+
63
+ constructor(userOptions?: AssetLoaderOptions) {
64
+ this.options = { ...defaultOptions, ...userOptions }
65
+ this.manifest =
66
+ this.options.manifest ||
67
+ loadManifest(path.resolve(this.options.fileSystemPath, this.options.manifestFileName))
68
+ }
69
+
70
+ getHashedAsset(asset: string): string {
71
+ if (this.manifest.hasOwnProperty(asset)) {
72
+ return this.manifest[asset]
73
+ } else {
74
+ throw Error(`Couldn't find asset "${asset}" in manifest`)
75
+ }
76
+ }
77
+
78
+ getFileContents(asset: string): string {
79
+ return loadFile(this.getFileSystemPath(asset), this.options.cacheFileContents)
80
+ }
81
+
82
+ getFileSystemPath(asset: string): string {
83
+ return this.formatFileSystemPath(this.getHashedAsset(asset))
84
+ }
85
+
86
+ getPublicURL(asset: string): string {
87
+ return this.formatPublicURL(this.getHashedAsset(asset))
88
+ }
89
+
90
+ //
91
+ // File name prefix methods
92
+ //
93
+
94
+ formatPublicURL(hashedAsset: string): string {
95
+ return urlJoin(this.options.publicPath, hashedAsset)
96
+ }
97
+
98
+ formatFileSystemPath(hashedAsset: string): string {
99
+ return path.join(this.options.fileSystemPath, hashedAsset)
100
+ }
101
+
102
+ //
103
+ // Webpack entry point methods
104
+ //
105
+
106
+ getFilesFor(entrypoint: string): TEntrypoint {
107
+ if (this.manifest.entrypoints && this.manifest.entrypoints[entrypoint]) {
108
+ return this.manifest.entrypoints[entrypoint]
109
+ } else {
110
+ throw Error(`Couldn't find entrypoint "${entrypoint}" in manifest`)
111
+ }
112
+ }
113
+
114
+ getScriptFilesFor(entrypoint: string): string[] {
115
+ return this.getFilesFor(entrypoint).js || []
116
+ }
117
+
118
+ getStylesheetFilesFor(entrypoint: string): string[] {
119
+ return this.getFilesFor(entrypoint).css || []
120
+ }
121
+
122
+ getScriptPathsFor(entrypoint: string): string[] {
123
+ return this.getScriptFilesFor(entrypoint).map(this.formatFileSystemPath.bind(this))
124
+ }
125
+
126
+ getStylesheetPathsFor(entrypoint: string): string[] {
127
+ return this.getStylesheetFilesFor(entrypoint).map(this.formatFileSystemPath.bind(this))
128
+ }
129
+
130
+ getScriptURLsFor(entrypoint: string): string[] {
131
+ return this.getScriptFilesFor(entrypoint).map(this.formatPublicURL.bind(this))
132
+ }
133
+
134
+ getStylesheetURLsFor(entrypoint: string): string[] {
135
+ return this.getStylesheetFilesFor(entrypoint).map(this.formatPublicURL.bind(this))
136
+ }
137
+ }
@@ -0,0 +1,181 @@
1
+ import { AssetLoader, AssetLoaderOptions } from '../AssetLoader'
2
+ import manifest from './__fixtures__/manifest.json'
3
+
4
+ jest.mock('../helpers/loadManifest', () => {
5
+ return {
6
+ loadManifest: jest.fn(() => manifest)
7
+ }
8
+ })
9
+
10
+ jest.mock('../helpers/loadFile', () => {
11
+ return {
12
+ loadFile: jest.fn(() => {
13
+ return 'FILE CONTENTS'
14
+ })
15
+ }
16
+ })
17
+
18
+ function createAssetLoader(options?: AssetLoaderOptions) {
19
+ return new AssetLoader(options)
20
+ }
21
+
22
+ describe('dotcom-server-asset-loader/src/AssetLoader', () => {
23
+ let loader
24
+
25
+ beforeEach(() => {
26
+ loader = createAssetLoader({
27
+ publicPath: '/public/assets/',
28
+ fileSystemPath: '/internal/path/to/assets'
29
+ })
30
+ })
31
+
32
+ afterEach(() => {
33
+ jest.restoreAllMocks()
34
+ })
35
+
36
+ describe('constructor', () => {
37
+ it('uses the supplied manifest instead of looking it up', () => {
38
+ const manifest = { foo: 'bar' }
39
+ const loader = createAssetLoader({ manifest })
40
+ const result = loader.getHashedAsset('foo')
41
+ expect(result).toBe(manifest.foo)
42
+ })
43
+ })
44
+
45
+ describe('.getHashedAsset()', () => {
46
+ it('returns the hashed name from a manifest', () => {
47
+ const result = loader.getHashedAsset('styles.css')
48
+ expect(result).toEqual('styles.12345.bundle.css')
49
+ })
50
+
51
+ it("errors if the file can't be found in the manifest", () => {
52
+ expect(() => {
53
+ loader.getHashedAsset('test')
54
+ }).toThrow(Error('Couldn\'t find asset "test" in manifest'))
55
+ })
56
+ })
57
+
58
+ describe('.getFileSystemPath()', () => {
59
+ it('returns the file system path for the requested file', () => {
60
+ const result = loader.getFileSystemPath('styles.css')
61
+ expect(result).toEqual('/internal/path/to/assets/styles.12345.bundle.css')
62
+ })
63
+ })
64
+
65
+ describe('.getPublicURL()', () => {
66
+ const tests = [
67
+ { expected: '/styles.12345.bundle.css' },
68
+ { publicPath: '', expected: 'styles.12345.bundle.css' },
69
+ { publicPath: '/', expected: '/styles.12345.bundle.css' },
70
+ { publicPath: '/lib', expected: '/lib/styles.12345.bundle.css' },
71
+ { publicPath: '/lib/', expected: '/lib/styles.12345.bundle.css' },
72
+ { publicPath: '../', expected: '../styles.12345.bundle.css' }
73
+ ]
74
+
75
+ tests.forEach(({ publicPath, expected }) => {
76
+ it(`publicPath: ${publicPath}`, () => {
77
+ const loader = typeof publicPath === 'undefined' ? new AssetLoader() : new AssetLoader({ publicPath })
78
+ const result = loader.getPublicURL('styles.css')
79
+ expect(result).toEqual(expected)
80
+ })
81
+ })
82
+ })
83
+
84
+ describe('.getFileContents()', () => {
85
+ it('returns the file contents for the requested file', () => {
86
+ const result = loader.getFileContents('styles.css')
87
+ expect(result).toEqual('FILE CONTENTS')
88
+ })
89
+ })
90
+
91
+ describe('.getFilesFor()', () => {
92
+ it('returns all file types', () => {
93
+ const result = loader.getFilesFor('main')
94
+
95
+ expect(Object.keys(result)).toEqual(['js', 'css'])
96
+ expect(result.js).toEqual(expect.any(Array))
97
+ expect(result.css).toEqual(expect.any(Array))
98
+ })
99
+
100
+ it('throws if the entry point cannot be found', () => {
101
+ expect(() => loader.getFilesFor('third')).toThrow()
102
+ })
103
+ })
104
+
105
+ describe('.getScriptFilesFor()', () => {
106
+ it('returns an array', () => {
107
+ const result = loader.getScriptFilesFor('main')
108
+ expect(result).toEqual(expect.any(Array))
109
+ })
110
+
111
+ it('returns an array of JS files', () => {
112
+ const result = loader.getScriptFilesFor('main')
113
+
114
+ expect(result.length).toBe(3)
115
+
116
+ result.forEach((item) => {
117
+ expect(item).toMatch(/\.js$/)
118
+ })
119
+ })
120
+ })
121
+
122
+ describe('.getStylesheetFilesFor()', () => {
123
+ it('returns an array of CSS files', () => {
124
+ const result = loader.getStylesheetFilesFor('main')
125
+
126
+ expect(result.length).toBe(2)
127
+
128
+ result.forEach((item) => {
129
+ expect(item).toMatch(/\.css$/)
130
+ })
131
+ })
132
+ })
133
+
134
+ describe('.getScriptPathsFor()', () => {
135
+ it('returns an array of JS file paths', () => {
136
+ const result = loader.getScriptPathsFor('main')
137
+
138
+ expect(result.length).toBe(3)
139
+
140
+ result.forEach((item) => {
141
+ expect(item).toMatch(/^\/internal\/path\/to\/assets\/.+\.js$/)
142
+ })
143
+ })
144
+ })
145
+
146
+ describe('.getStylesheetPathsFor()', () => {
147
+ it('returns an array of CSS file paths', () => {
148
+ const result = loader.getStylesheetPathsFor('main')
149
+
150
+ expect(result.length).toBe(2)
151
+
152
+ result.forEach((item) => {
153
+ expect(item).toMatch(/^\/internal\/path\/to\/assets\/.+\.css$/)
154
+ })
155
+ })
156
+ })
157
+
158
+ describe('.getScriptURLsFor()', () => {
159
+ it('returns an array of JS file URLs', () => {
160
+ const result = loader.getScriptURLsFor('main')
161
+
162
+ expect(result.length).toBe(3)
163
+
164
+ result.forEach((item) => {
165
+ expect(item).toMatch(/^\/public\/assets\/.+\.js$/)
166
+ })
167
+ })
168
+ })
169
+
170
+ describe('.getStylesheetURLsFor()', () => {
171
+ it('returns an array of CSS file URLs', () => {
172
+ const result = loader.getStylesheetURLsFor('main')
173
+
174
+ expect(result.length).toBe(2)
175
+
176
+ result.forEach((item) => {
177
+ expect(item).toMatch(/^\/public\/assets\/.+\.css$/)
178
+ })
179
+ })
180
+ })
181
+ })
@@ -0,0 +1,18 @@
1
+ {
2
+ "styles.css": "styles.12345.bundle.css",
3
+ "main.js": "main.12345.bundle.js",
4
+ "secondary.js": "secondary.12345.bundle.js",
5
+ "vendor.foo.js": "vendor.foo.12345.bundle.js",
6
+ "vendor.bar.js": "vendor.bar.12345.bundle.js",
7
+ "vendor.baz.js": "vendor.baz.12345.bundle.js",
8
+ "runtime.js": "runtime.bundle.js",
9
+ "entrypoints": {
10
+ "main": {
11
+ "js": ["vendor.foo.12345.bundle.js", "vendor.bar.12345.bundle.js", "main.12345.bundle.js"],
12
+ "css": ["styles.12345.bundle.css", "lazyload.12345.bundle.css"]
13
+ },
14
+ "secondary": {
15
+ "js": ["vendor.foo.12345.bundle.js", "vendor.baz.12345.bundle.js", "secondary.12345.bundle.js"]
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'fs'
2
+
3
+ // Avoid hitting the disk each time a file is requested and instead
4
+ // hold the file contents in memory for the lifecycle of the app
5
+ const store = new Map()
6
+
7
+ export function loadFile(fullPath: string, cache: boolean = false): string {
8
+ if (store.has(fullPath)) {
9
+ return store.get(fullPath)
10
+ } else {
11
+ const fileAsBuffer = fs.readFileSync(fullPath)
12
+ const fileAsString = String(fileAsBuffer)
13
+
14
+ if (cache) {
15
+ store.set(fullPath, fileAsString)
16
+ }
17
+
18
+ return fileAsString
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ import { loadFile } from './loadFile'
2
+
3
+ export function loadManifest(filePath: string) {
4
+ const manifest = loadFile(filePath)
5
+ return JSON.parse(manifest)
6
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './AssetLoader'