forked from embroider-build/embroider
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ref: embroider-build#609 -- start esbuild prototype replacement for w…
…ebpack
- Loading branch information
1 parent
0b0c8a2
commit 663216b
Showing
8 changed files
with
430 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/node_modules | ||
/src/**/*.js | ||
/src/**/*.d.ts | ||
/src/**/*.map | ||
/*/tests/**/*.js | ||
/*/tests/**/*.d.ts | ||
/*/tests/**/*.map |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = { | ||
testEnvironment: 'node', | ||
testMatch: [ | ||
'<rootDir>/tests/**/*.test.js', | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"name": "@embroider/esbuild", | ||
"version": "0.37.0", | ||
"private": false, | ||
"description": "Builds EmberJS apps with ESBuild", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/embroider-build/embroider.git", | ||
"directory": "packages/esbuild" | ||
}, | ||
"license": "MIT", | ||
"author": "NullVoxPopuli", | ||
"main": "src/ember-esbuild.js", | ||
"files": [ | ||
"src/**/*.js", | ||
"src/**/*.d.ts", | ||
"src/**/*.js.map" | ||
], | ||
"scripts": { | ||
"prepare": "tsc" | ||
}, | ||
"dependencies": { | ||
"debug": "^3.1.0", | ||
"esbuild": "^0.9.0", | ||
"fs-extra": "^7.0.0", | ||
"jsdom": "^16.4.0", | ||
"lodash": "^4.17.10" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^10.5.2", | ||
"@types/fs-extra": "^5.0.4", | ||
"@types/lodash": "^4.14.116", | ||
"typescript": "~4.0.0" | ||
}, | ||
"peerDependencies": { | ||
"@embroider/core": "0.37.0" | ||
}, | ||
"engines": { | ||
"node": "10.* || 12.* || >= 14" | ||
}, | ||
"volta": { | ||
"extends": "../../package.json" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
Most of the work this module does is putting an HTML-oriented facade around | ||
Webpack. That is, we want both the input and output to be primarily HTML files | ||
with proper spec semantics, and we use webpack to optimize the assets referred | ||
to by those files. | ||
While there are webpack plugins for handling HTML, none of them handle | ||
multiple HTML entrypoints and apply correct HTML semantics (for example, | ||
getting script vs module context correct). | ||
*/ | ||
|
||
import { getOrCreate, Variant, applyVariantToBabelConfig } from '@embroider/core'; | ||
import { PackagerInstance, AppMeta, Packager } from '@embroider/core'; | ||
import { readFileSync, outputFileSync, copySync, realpathSync, Stats, statSync, readJsonSync } from 'fs-extra'; | ||
import { join, dirname, relative, sep } from 'path'; | ||
import isEqual from 'lodash/isEqual'; | ||
import mergeWith from 'lodash/mergeWith'; | ||
import flatMap from 'lodash/flatMap'; | ||
import { format } from 'util'; | ||
import makeDebug from 'debug'; | ||
import { tmpdir } from 'os'; | ||
import { HTMLEntrypoint } from './html-entrypoint'; | ||
import { StatSummary } from './stat-summary'; | ||
import crypto from 'crypto'; | ||
import type { HbsLoaderConfig } from '@embroider/hbs-loader'; | ||
|
||
const debug = makeDebug('embroider:debug'); | ||
|
||
// This is a type-only import, so it gets compiled away. At runtime, we load | ||
// terser lazily so it's only loaded for production builds that use it. Don't | ||
// add any non-type-only imports here. | ||
import type { MinifyOptions } from 'terser'; | ||
|
||
interface AppInfo { | ||
entrypoints: HTMLEntrypoint[]; | ||
otherAssets: string[]; | ||
templateCompiler: AppMeta['template-compiler']; | ||
babel: AppMeta['babel']; | ||
rootURL: AppMeta['root-url']; | ||
publicAssetURL: string; | ||
resolvableExtensions: AppMeta['resolvable-extensions']; | ||
} | ||
|
||
// AppInfos are equal if they result in the same webpack config. | ||
function equalAppInfo(left: AppInfo, right: AppInfo): boolean { | ||
return ( | ||
isEqual(left.babel, right.babel) && | ||
left.entrypoints.length === right.entrypoints.length && | ||
left.entrypoints.every((e, index) => isEqual(e.modules, right.entrypoints[index].modules)) | ||
); | ||
} | ||
|
||
interface Options { | ||
// the base public URL for your assets in production. Use this when you want | ||
// to serve all your assets from a different origin (like a CDN) than your | ||
// actual index.html will be served on. | ||
// | ||
// This should be a URL ending in "/". | ||
publicAssetURL?: string; | ||
} | ||
|
||
// we want to ensure that not only does our instance conform to | ||
// PackagerInstance, but our constructor conforms to Packager. So instead of | ||
// just exporting our class directly, we export a const constructor of the | ||
// correct type. | ||
export const ESBuild: Packager<Options> = class ESBuild implements PackagerInstance { | ||
static annotation = '@embroider/esbuild'; | ||
|
||
pathToVanillaApp: string; | ||
private passthroughCache: Map<string, Stats> = new Map(); | ||
private publicAssetURL: string | undefined; | ||
|
||
constructor( | ||
pathToVanillaApp: string, | ||
private outputPath: string, | ||
private variants: Variant[], | ||
private consoleWrite: (msg: string) => void, | ||
options?: Options | ||
) { | ||
this.pathToVanillaApp = realpathSync(pathToVanillaApp); | ||
this.publicAssetURL = options?.publicAssetURL; | ||
} | ||
|
||
async build(): Promise<void> { | ||
// let appInfo = this.examineApp(); | ||
// let webpack = this.getWebpack(appInfo); | ||
// let stats = this.summarizeStats(await this.runWebpack(webpack)); | ||
// await this.writeFiles(stats, appInfo); | ||
} | ||
|
||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { getOrCreate } from '@embroider/core'; | ||
import { readFileSync } from 'fs-extra'; | ||
import { join } from 'path'; | ||
import { JSDOM } from 'jsdom'; | ||
import partition from 'lodash/partition'; | ||
import zip from 'lodash/zip'; | ||
import Placeholder from './html-placeholder'; | ||
import { StatSummary } from './stat-summary'; | ||
|
||
export class HTMLEntrypoint { | ||
private dom: JSDOM; | ||
private placeholders: Map<string, Placeholder[]> = new Map(); | ||
modules: string[] = []; | ||
scripts: string[] = []; | ||
styles: string[] = []; | ||
|
||
constructor( | ||
private pathToVanillaApp: string, | ||
private rootURL: string, | ||
private publicAssetURL: string, | ||
public filename: string | ||
) { | ||
this.dom = new JSDOM(readFileSync(join(this.pathToVanillaApp, this.filename), 'utf8')); | ||
|
||
for (let tag of this.handledStyles()) { | ||
let styleTag = tag as HTMLLinkElement; | ||
let href = styleTag.href; | ||
if (!isAbsoluteURL(href)) { | ||
let url = this.relativeToApp(href); | ||
this.styles.push(url); | ||
let placeholder = new Placeholder(styleTag); | ||
let list = getOrCreate(this.placeholders, url, () => []); | ||
list.push(placeholder); | ||
} | ||
} | ||
|
||
for (let scriptTag of this.handledScripts()) { | ||
// scriptTag.src include rootURL. Convert it to be relative to the app. | ||
let src = this.relativeToApp(scriptTag.src); | ||
|
||
if (scriptTag.type === 'module') { | ||
this.modules.push(src); | ||
} else { | ||
this.scripts.push(src); | ||
} | ||
|
||
let placeholder = new Placeholder(scriptTag); | ||
let list = getOrCreate(this.placeholders, src, () => []); | ||
list.push(placeholder); | ||
} | ||
} | ||
|
||
private relativeToApp(rootRelativeURL: string) { | ||
return rootRelativeURL.replace(this.rootURL, ''); | ||
} | ||
|
||
private handledScripts() { | ||
let scriptTags = [...this.dom.window.document.querySelectorAll('script')] as HTMLScriptElement[]; | ||
let [ignoredScriptTags, handledScriptTags] = partition(scriptTags, scriptTag => { | ||
return !scriptTag.src || scriptTag.hasAttribute('data-embroider-ignore') || isAbsoluteURL(scriptTag.src); | ||
}); | ||
for (let scriptTag of ignoredScriptTags) { | ||
scriptTag.removeAttribute('data-embroider-ignore'); | ||
} | ||
return handledScriptTags; | ||
} | ||
|
||
private handledStyles() { | ||
let styleTags = [...this.dom.window.document.querySelectorAll('link[rel="stylesheet"]')] as HTMLLinkElement[]; | ||
let [ignoredStyleTags, handledStyleTags] = partition(styleTags, styleTag => { | ||
return !styleTag.href || styleTag.hasAttribute('data-embroider-ignore') || isAbsoluteURL(styleTag.href); | ||
}); | ||
for (let styleTag of ignoredStyleTags) { | ||
styleTag.removeAttribute('data-embroider-ignore'); | ||
} | ||
return handledStyleTags; | ||
} | ||
|
||
// bundles maps from input asset to a per-variant map of output assets | ||
render(stats: StatSummary): string { | ||
let insertedLazy = false; | ||
let fastbootVariant = stats.variants.findIndex(v => Boolean(v.runtime === 'fastboot')); | ||
let supportsFastboot = stats.variants.some(v => v.runtime === 'fastboot' || v.runtime === 'all'); | ||
|
||
for (let [src, placeholders] of this.placeholders) { | ||
let match = stats.entrypoints.get(src); | ||
if (match) { | ||
let firstVariant = stats.variants.findIndex((_, index) => Boolean(match!.get(index))); | ||
let matchingBundles = match.get(firstVariant)!; | ||
let matchingFastbootBundles = fastbootVariant >= 0 ? match.get(fastbootVariant) || [] : []; | ||
|
||
for (let placeholder of placeholders) { | ||
if (supportsFastboot) { | ||
// if there is any fastboot involved, we will emit the lazy bundles | ||
// right before our first script. | ||
insertedLazy = maybeInsertLazyBundles(insertedLazy, stats.lazyBundles, placeholder, this.publicAssetURL); | ||
} | ||
for (let [base, fastboot] of zip(matchingBundles, matchingFastbootBundles)) { | ||
if (!base) { | ||
// this bundle only exists in the fastboot variant | ||
let element = placeholder.start.ownerDocument.createElement('fastboot-script'); | ||
element.setAttribute('src', this.publicAssetURL + fastboot); | ||
placeholder.insert(element); | ||
placeholder.insertNewline(); | ||
} else if (!fastboot || base === fastboot) { | ||
// no specialized fastboot variant | ||
let src = this.publicAssetURL + base; | ||
placeholder.insertURL(src); | ||
} else { | ||
// we have both and they differ | ||
let src = this.publicAssetURL + base; | ||
let element = placeholder.insertURL(src); | ||
if (element) { | ||
element.setAttribute('data-fastboot-src', this.publicAssetURL + fastboot); | ||
} | ||
} | ||
} | ||
} | ||
} else { | ||
// no match means keep the original HTML content for this placeholder. | ||
// (If we really wanted it empty instead, there would be matchingBundles | ||
// and it would be an empty list.) | ||
for (let placeholder of placeholders) { | ||
placeholder.reset(); | ||
} | ||
} | ||
} | ||
return this.dom.serialize(); | ||
} | ||
} | ||
|
||
function isAbsoluteURL(url: string) { | ||
return /^(?:[a-z]+:)?\/\//i.test(url); | ||
} | ||
|
||
// we (somewhat arbitrarily) decide to put the lazy bundles before the very | ||
// first <script> that we have rewritten | ||
function maybeInsertLazyBundles( | ||
insertedLazy: boolean, | ||
lazyBundles: Set<string>, | ||
placeholder: Placeholder, | ||
publicAssetURL: string | ||
): boolean { | ||
if (!insertedLazy && placeholder.isScript()) { | ||
for (let bundle of lazyBundles) { | ||
let element = placeholder.start.ownerDocument.createElement('fastboot-script'); | ||
element.setAttribute('src', publicAssetURL + bundle); | ||
placeholder.insert(element); | ||
placeholder.insertNewline(); | ||
} | ||
return true; | ||
} | ||
return insertedLazy; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
export default class Placeholder { | ||
end: InDOMNode; | ||
start: StartNode; | ||
|
||
// remove the target Element from the DOM, and track where it was so we can | ||
// update that location later. | ||
constructor(private target: HTMLElement) { | ||
if (!target.ownerDocument || !target.parentElement) { | ||
throw new Error('can only construct a placeholder for an element that is in DOM'); | ||
} | ||
let start = target.ownerDocument.createTextNode(''); | ||
target.parentElement.insertBefore(start, target); | ||
let endNode = target.ownerDocument.createTextNode(''); | ||
target.replaceWith(endNode); | ||
|
||
// Type cast is justified because start always has a nextSibling (it's | ||
// "end") and because we know we already inserted the node. | ||
this.start = start as StartNode; | ||
|
||
// Type cast is justified because we know we already inserted the node. | ||
this.end = endNode as InDOMNode; | ||
} | ||
|
||
reset() { | ||
this.clear(); | ||
this.insert(this.target); | ||
} | ||
|
||
clear() { | ||
while (this.start.nextSibling !== this.end) { | ||
this.start.parentElement.removeChild(this.start.nextSibling); | ||
} | ||
} | ||
|
||
insert(node: Node) { | ||
this.end.parentElement.insertBefore(node, this.end); | ||
} | ||
|
||
isScript(): boolean { | ||
return this.target.tagName === 'SCRIPT'; | ||
} | ||
|
||
insertURL(url: string) { | ||
if (url.endsWith('.js')) { | ||
return this.insertScriptTag(url); | ||
} | ||
if (url.endsWith('.css')) { | ||
return this.insertStyleLink(url); | ||
} | ||
throw new Error(`don't know how to insertURL ${url}`); | ||
} | ||
|
||
insertScriptTag(src: string) { | ||
let newTag = this.end.ownerDocument.createElement('script'); | ||
for (let { name, value } of [...this.target.attributes]) { | ||
if (name === 'type' && value === 'module') { | ||
// we always convert modules to scripts | ||
continue; | ||
} | ||
// all other attributes are copied forward unchanged | ||
newTag.setAttribute(name, value); | ||
} | ||
newTag.src = src; | ||
this.insert(newTag); | ||
this.insertNewline(); | ||
return newTag; | ||
} | ||
|
||
insertStyleLink(href: string) { | ||
let newTag = this.end.ownerDocument.createElement('link'); | ||
newTag.href = href; | ||
newTag.rel = 'stylesheet'; | ||
this.insert(newTag); | ||
this.insertNewline(); | ||
} | ||
|
||
insertNewline() { | ||
this.end.parentElement.insertBefore(this.end.ownerDocument.createTextNode('\n'), this.end); | ||
} | ||
} | ||
|
||
// an html node that's definitely inserted into the DOM | ||
interface InDOMNode extends Node { | ||
parentElement: HTMLElement; | ||
ownerDocument: Document; | ||
} | ||
|
||
// an html node that definitely has a next sibling. | ||
interface StartNode extends InDOMNode { | ||
nextSibling: InDOMNode & ChildNode; | ||
} |
Oops, something went wrong.