SPAC: Publishing Apps

Assumptions & Observations

Modules: (ES5 != Node)

Imports & Bundling

  • Dynamic Imports: ES5 supports a dynamic import() statement. This statement needs a filename, and this file is asynchronously fetched from the server. Therefore, the server-side actually needs to deliver individual JavaScript pages.
  • Pre-Build Imports: Before bundling the app, I use a helper script that traverses the app directories, determines the pages, and then adds them to an inventory file. During bundling, the controller reads the inventory, and executes static imports of these file. Then, the application is bundled.
  • Static Imports: All entities, including pages, need to statically import their required assets. Then, bundling “just” packs the application into the target format.
  • Inventory: Before building, a script detects all pages, and creates a file called inventory.js
  • Imports: During the init phase, the controller loads all required pages from the inventory. These imports are dynamic at execution time, but...
  • Bundling: … the bundling determines and executes all imports before the code is assembled. Then, a bundled, optimized version of the app source code is produced.

Changing how the Controller Works

export default function bootstrap (rootDir) {
const inventory = { pages: [], components: [], actions: [] }
Object.keys(inventory).forEach(entity => {
const files = fs.readdirSync(path.join(rootDir, entity), {
withFileTypes: true
})
const fullPath = path.join(path.resolve(rootDir), entity)
files.forEach(file =>
inventory[entity].push(path.join(fullPath, file.name))
)
})
return inventory
}
init() {
this._initMap(Page, 'pages', /Page.js/)
this._initMap(Action, 'actions', /Action.js/)
this._initMap(Component, 'components', /Component.js/)
}
_initMap (parentClass, mapType, pattern) {
this.inventory[mapType].forEach(async filePath => {
try {
if (!filePath.match(pattern)) {
throw new Error()
}
const name = filePath
.split('/')
.pop()
.replace(pattern, '')
const clazz = (await import(`${filePath}`)).default
if (clazz.prototype instanceof parentClass) {
if (parentClass === Page) {
const route = `/${name.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase()}`
this[mapType].set(name, { route, clazz })
} else {
this[mapType].set(name, { clazz })
}
} else {
throw new Error()
}
} catch (e) {
console.error(e)
throw new (class EntityLoadError extends Error {
message = `Entity ${parentClass.name} from path ${filePath} could not be loaded`
})()
}
})
}

Building & Bundling Commands

Snowpack Config File

module.exports = {
mount: {
public: '/',
src: '/src'
},
devOptions: {
bundle: true,
clean: true
},
installOptions: {
treeshake: true
},
buildOptions: {
out: 'build',
clean: true,
metaDir: '/core',
webModulesUrl: '/lib'
}
}
  • mount: Configure additional folders to be served in your build, where src is the absolute path in your project, and public the folder to which these files will be copied
  • devOptions: Control how the dev command works, here I add options to clean the cache and to use the bundled version of the code. This option is important to save you valuable time when your builds are not working - figure out the errors rather earlier.
  • installOptions: During the bundling step, I use treeshake to eliminate redundant and dead code in the application and libraries
  • buildOptions: The bundled source code is copied to out, but before new files are copied, everything is deleted with the clean option. Then, all additional libraries are installed at the webModulesUrl folder, and the metaDir defines where snowpack modules will be installed.
build
├── core
│ └── env.js
├── img
│ └── favicon.ico
├── index.html
├── lib
│ ├── import-map.json
│ └── spac.js
├── src
│ ├── actions
│ │ ├── SearchApiAction.js
│ │ ├── ...
│ ├── components
│ │ ├── ApiSearchBarComponent.js
│ │ ├── ...
│ ├── globals
│ │ └── icons.js
│ ├── index.js
│ ├── inventory.json
│ ├── inventory.json.proxy.js
│ └── pages
│ ├── IndexPage.js
│ ├── ...
├── ...
└── style.css

The Final Frontier: Caching Dynamic Imports

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store