From 2d88d089571c3d83b17a9de6a7146198317f4d38 Mon Sep 17 00:00:00 2001 From: X Date: Thu, 2 Sep 2021 01:38:02 +0800 Subject: [PATCH 1/3] Improve plugins system to support tailwindcss plugin --- bundler/mod.ts | 2 +- framework/core/style.ts | 10 ++++--- server/aleph.ts | 61 +++++++++++++++++++++++++++-------------- server/server.ts | 2 +- types.d.ts | 15 ++++++++-- 5 files changed, 61 insertions(+), 29 deletions(-) diff --git a/bundler/mod.ts b/bundler/mod.ts index 9bef6249d..a111d9c29 100644 --- a/bundler/mod.ts +++ b/bundler/mod.ts @@ -230,7 +230,7 @@ export class Bundler { entry.map((specifier) => { let mod = this.#aleph.getModule(specifier) if (mod) { - hasher.update(this.#aleph.gteModuleHash(mod)) + hasher.update(this.#aleph.computeModuleHash(mod)) } }) const bundleFilename = `${name}.bundle.${hasher.toString().slice(0, 8)}.js` diff --git a/framework/core/style.ts b/framework/core/style.ts index cdcbadcf4..991e9667b 100644 --- a/framework/core/style.ts +++ b/framework/core/style.ts @@ -45,16 +45,18 @@ export function applyCSS(url: string, { css, href }: { css?: string, href?: stri return el.getAttribute('data-module-id') === url }) const clean = () => { - if (prevEls.length > 0) { - prevEls.forEach(el => document.head.removeChild(el)) - } + setTimeout(() => { + if (prevEls.length > 0) { + prevEls.forEach(el => document.head.removeChild(el)) + } + }, 0) } let el: any if (util.isFilledString(css)) { el = document.createElement('style') el.type = 'text/css' el.appendChild(document.createTextNode(css)) - Promise.resolve().then(clean) + clean() } else if (util.isFilledString(href)) { el = document.createElement('link') el.rel = 'stylesheet' diff --git a/server/aleph.ts b/server/aleph.ts index d72b1a660..4d3173cc3 100644 --- a/server/aleph.ts +++ b/server/aleph.ts @@ -14,7 +14,7 @@ import cssPlugin, { cssLoader } from '../plugins/css.ts' import { ensureTextFile, existsDir, existsFile, lazyRemove } from '../shared/fs.ts' import log, { Measure } from '../shared/log.ts' import util from '../shared/util.ts' -import type { Aleph as IAleph, DependencyDescriptor, ImportMap, LoadInput, LoadOutput, Module, HtmlDescriptor, RouterURL, ResolveResult, TransformOutput, SSRData, RenderOutput } from '../types.d.ts' +import type { Aleph as IAleph, DependencyDescriptor, ImportMap, LoadInput, LoadOutput, Module, RouterURL, ResolveResult, TransformInput, TransformOutput, SSRData, RenderOutput } from '../types.d.ts' import { VERSION } from '../version.ts' import { Analyzer } from './analyzer.ts' import { cache } from './cache.ts' @@ -38,6 +38,7 @@ type CompileOptions = { forceRefresh?: boolean, ignoreDeps?: boolean, httpExternal?: boolean + virtual?: boolean } type ResolveListener = { @@ -52,7 +53,7 @@ type LoadListener = { type TransformListener = { test: RegExp | 'hmr' | 'main', - transform(input: TransformOutput & { module: Module }): TransformOutput | void | Promise | Promise, + transform(input: TransformInput): TransformOutput | void | Promise, } type RenderListener = (input: RenderOutput & { path: string }) => void | Promise @@ -318,7 +319,7 @@ export class Aleph implements IAleph { const module = await this.compile(specifier, { forceRefresh: true, ignoreDeps: true, - httpExternal: specifier.startsWith('/api/') + httpExternal: prevModule.httpExternal }) const refreshPage = ( this.#config.ssr && @@ -526,7 +527,7 @@ export class Aleph implements IAleph { } /** add a module by given path and optional source code. */ - async addModule(specifier: string, sourceCode: string): Promise { + async addModule(specifier: string, sourceCode: string, forceRefresh?: boolean): Promise { let sourceType = getSourceType(specifier) if (sourceType === SourceType.Unknown) { throw new Error("addModule: unknown souce type") @@ -535,7 +536,8 @@ export class Aleph implements IAleph { source: { code: sourceCode, type: sourceType, - } + }, + forceRefresh, }) if (specifier.startsWith('pages/') || specifier.startsWith('api/')) { specifier = '/' + specifier @@ -707,8 +709,7 @@ export class Aleph implements IAleph { specifier: '/main.js', deps: [], sourceHash: '', - jsFile: '', - ready: Promise.resolve() + jsFile: '' }, code, }) @@ -775,7 +776,7 @@ export class Aleph implements IAleph { ] } - gteModuleHash(module: Module) { + computeModuleHash(module: Module) { const hasher = createHash('md5').update(module.sourceHash) this.lookupDeps(module.specifier, dep => { const depMod = this.getModule(dep.specifier) @@ -939,7 +940,7 @@ export class Aleph implements IAleph { async importModule(module: Module): Promise { const path = join(this.#buildDir, module.jsFile) - const hash = this.gteModuleHash(module) + const hash = this.computeModuleHash(module) if (existsFile(path)) { return await import(`file://${path}#${(hash).slice(0, 6)}`) } @@ -979,7 +980,8 @@ export class Aleph implements IAleph { } for (const { test, transform } of this.#transformListeners) { if (test === 'hmr') { - const ret = await transform({ module: { ...module }, code }) + const { jsBuffer, ready, ...rest } = module + const ret = await transform({ module: structuredClone(rest), code }) if (util.isFilledString(ret?.code)) { code = ret!.code } @@ -1077,7 +1079,7 @@ export class Aleph implements IAleph { /** init the module by given specifier, don't transpile the code when the returned `source` is equal to null */ private async initModule( specifier: string, - { source: customSource, forceRefresh, httpExternal }: CompileOptions = {} + { source: customSource, forceRefresh, httpExternal, virtual }: CompileOptions = {} ): Promise<[Module, ModuleSource | null]> { let external = false let data: any = null @@ -1148,7 +1150,7 @@ export class Aleph implements IAleph { this.#appModule = mod } - if (await existsFile(metaFp)) { + if (!forceRefresh && await existsFile(metaFp)) { try { const { specifier: _specifier, sourceHash, deps, isStyle, ssrPropsFn, ssgPathsFn, denoHooks } = JSON.parse(await Deno.readTextFile(metaFp)) if (_specifier === specifier && util.isFilledString(sourceHash) && util.isArray(deps)) { @@ -1165,6 +1167,11 @@ export class Aleph implements IAleph { } catch (e) { } } + if (virtual) { + defer() + return [mod, null] + } + if (!isRemote || this.#reloading || mod.sourceHash === '' || !await existsFile(cacheFp)) { try { const src = customSource || await this.resolveModuleSource(specifier, data) @@ -1179,6 +1186,8 @@ export class Aleph implements IAleph { } } + console.log('----', mod.specifier, !!source) + defer() return [mod, source] } @@ -1272,15 +1281,20 @@ export class Aleph implements IAleph { } } + let extraDeps: DependencyDescriptor[] = [] for (const { test, transform } of this.#transformListeners) { if (test instanceof RegExp && test.test(specifier)) { - const ret = await transform({ module: { ...module }, code: jsCode, map: sourceMap }) + const { jsBuffer, ready, ...rest } = module + const ret = await transform({ module: { ...structuredClone(rest) }, code: jsCode, map: sourceMap }) if (util.isFilledString(ret?.code)) { jsCode = ret!.code } if (util.isFilledString(ret?.map)) { sourceMap = ret!.map } + if (Array.isArray(ret?.extraDeps)) { + extraDeps.push(...ret!.extraDeps) + } } } @@ -1291,7 +1305,7 @@ export class Aleph implements IAleph { module.jsBuffer = encoder.encode(jsCode) module.deps = deps.filter(({ specifier }) => specifier !== module.specifier).map(({ specifier, resolved, isDynamic }) => { - const dep: DependencyDescriptor = { specifier } + const dep: DependencyDescriptor = { specifier, } if (isDynamic) { dep.isDynamic = true } @@ -1303,7 +1317,7 @@ export class Aleph implements IAleph { } } return dep - }) + }).concat(extraDeps) ms.stop(`transpile '${specifier}'`) @@ -1312,11 +1326,16 @@ export class Aleph implements IAleph { if (module.deps.length > 0) { let fsync = false - await Promise.all(module.deps.map(async ({ specifier, hashLoc }) => { - let depModule: Module | null - if (ignoreDeps) { + await Promise.all(module.deps.map(async ({ specifier, hashLoc, virtual }) => { + let depModule: Module | null = null + if (ignoreDeps || virtual) { depModule = this.getModule(specifier) - } else { + if (depModule == null && virtual) { + const [mod] = await this.initModule(specifier, { virtual: true }) + depModule = mod + } + } + if (depModule === null) { const [mod, src] = await this.initModule(specifier, { httpExternal }) if (!mod.external) { await this.transpileModule(mod, src, false, __tracing) @@ -1325,7 +1344,7 @@ export class Aleph implements IAleph { } if (depModule) { if (hashLoc !== undefined) { - const hash = this.gteModuleHash(depModule) + const hash = this.computeModuleHash(depModule) if (await this.replaceDepHash(module, hashLoc, hash)) { fsync = true } @@ -1356,7 +1375,7 @@ export class Aleph implements IAleph { const { specifier, hashLoc } = dep if (specifier === by.specifier && hashLoc !== undefined) { if (hash == null) { - hash = this.gteModuleHash(by) + hash = this.computeModuleHash(by) } if (await this.replaceDepHash(mod, hashLoc, hash)) { fsync = true diff --git a/server/server.ts b/server/server.ts index e395bfca7..0176d8d0b 100644 --- a/server/server.ts +++ b/server/server.ts @@ -175,7 +175,7 @@ export class Server { if (module) { const content = await aleph.getModuleJS(module, aleph.isDev) if (content) { - const hash = aleph.gteModuleHash(module) + const hash = aleph.computeModuleHash(module) if (hash === req.headers.get('If-None-Match')) { end(304) return diff --git a/types.d.ts b/types.d.ts index 3ba1cd647..fc44a6af9 100644 --- a/types.d.ts +++ b/types.d.ts @@ -6,11 +6,11 @@ export interface Aleph { readonly workingDir: string readonly config: RequiredConfig addDist(path: string, content: Uint8Array): Promise - addModule(specifier: string, sourceCode: string): Promise<{ specifier: string, jsFile: string }> + addModule(specifier: string, sourceCode: string, forceRefresh?: boolean): Promise fetchModule(specifier: string): Promise<{ content: Uint8Array, contentType: string | null }> onResolve(test: RegExp, resolve: (specifier: string) => ResolveResult): void onLoad(test: RegExp, load: (input: LoadInput) => LoadOutput | Promise): void - onTransform(test: 'hmr' | 'main' | RegExp, transform: (input: TransformOutput & { module: Module }) => TransformOutput | void | Promise | Promise): void + onTransform(test: 'hmr' | 'main' | RegExp, transform: (input: TransformInput) => TransformOutput | void | Promise): void onRender(callback: (input: RenderOutput) => Promise | void): void } @@ -85,11 +85,21 @@ export type LoadOutput = { map?: string } +/** + * The input of the `onTransform` hook. + */ +export type TransformInput = { + module: Omit + code: string + map?: string +} + /** * The output of the `onTransform` hook. */ export type TransformOutput = { code: string + extraDeps?: DependencyDescriptor[] map?: string } @@ -113,6 +123,7 @@ export type Module = { /** The Dependency Descriptor. */ type DependencyDescriptor = { readonly specifier: string + virtual?: boolean isDynamic?: boolean hashLoc?: number } From 0360e78bb41db5f27b12b995217fe0c8e6ca88ff Mon Sep 17 00:00:00 2001 From: X Date: Thu, 2 Sep 2021 02:09:56 +0800 Subject: [PATCH 2/3] Remove console message --- server/aleph.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/aleph.ts b/server/aleph.ts index 4d3173cc3..092d8481a 100644 --- a/server/aleph.ts +++ b/server/aleph.ts @@ -1186,8 +1186,6 @@ export class Aleph implements IAleph { } } - console.log('----', mod.specifier, !!source) - defer() return [mod, source] } From 6c110a708cfaae6d3f982f689156967d246458a0 Mon Sep 17 00:00:00 2001 From: X Date: Thu, 2 Sep 2021 02:10:35 +0800 Subject: [PATCH 3/3] Add tailwindcss example --- examples/tailwindcss/aleph.config.ts | 6 ++++++ examples/tailwindcss/app.tsx | 12 ++++++++++++ examples/tailwindcss/pages/index.tsx | 23 +++++++++++++++++++++++ examples/tailwindcss/public/logo.svg | 15 +++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 examples/tailwindcss/aleph.config.ts create mode 100644 examples/tailwindcss/app.tsx create mode 100644 examples/tailwindcss/pages/index.tsx create mode 100644 examples/tailwindcss/public/logo.svg diff --git a/examples/tailwindcss/aleph.config.ts b/examples/tailwindcss/aleph.config.ts new file mode 100644 index 000000000..51d9989cd --- /dev/null +++ b/examples/tailwindcss/aleph.config.ts @@ -0,0 +1,6 @@ +import type { Config } from 'aleph/types' +import windicss from 'https://deno.land/x/aleph_plugin_windicss@v0.0.1/plugin.ts' + +export default { + plugins: [windicss] +} diff --git a/examples/tailwindcss/app.tsx b/examples/tailwindcss/app.tsx new file mode 100644 index 000000000..d91d915c9 --- /dev/null +++ b/examples/tailwindcss/app.tsx @@ -0,0 +1,12 @@ +import React, { ComponentType } from 'react' + +export default function App({ Page, pageProps }: { Page: ComponentType, pageProps: any }) { + return ( +
+ + + + +
+ ) +} diff --git a/examples/tailwindcss/pages/index.tsx b/examples/tailwindcss/pages/index.tsx new file mode 100644 index 000000000..a7628d6d7 --- /dev/null +++ b/examples/tailwindcss/pages/index.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +export default function Index() { + return ( +
+
+ Aleph.js +
+
+

Aleph.js

+

CSS Powered by Windi.

+
+ + Get started + +
+
+
+ ) +} diff --git a/examples/tailwindcss/public/logo.svg b/examples/tailwindcss/public/logo.svg new file mode 100644 index 000000000..8b9555aea --- /dev/null +++ b/examples/tailwindcss/public/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file