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
4 changes: 2 additions & 2 deletions examples/hello-world/src/with-winter-spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createWithWinterSpec } from "dist"
import { withDefaultExceptionHandling } from "dist/middleware"
import { createWithDefaultExceptionHandling } from "dist/middleware"

export const withWinterSpec = createWithWinterSpec({
apiName: "hello-world",
productionServerUrl: "https://example.com",

authMiddleware: {},
beforeAuthMiddleware: [withDefaultExceptionHandling],
beforeAuthMiddleware: [createWithDefaultExceptionHandling()],
})
34 changes: 30 additions & 4 deletions src/bundle/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import esbuild from "esbuild"
import { constructManifest } from "./construct-manifest.js"
import { ResolvedWinterSpecConfig } from "src/config/utils.js"
import os from "node:os"
import path from "node:path"

export const bundle = async (config: ResolvedWinterSpecConfig) => {
export interface BundleResult {
code: string
sourceMap?: string
}

export const bundle = async (
config: ResolvedWinterSpecConfig,
options: { sourcemap?: boolean | "inline" | "external" } = {}
): Promise<BundleResult> => {
let platformBundleOptions: Partial<esbuild.BuildOptions> = {}

if (config.platform === "node") {
Expand All @@ -12,18 +22,34 @@ export const bundle = async (config: ResolvedWinterSpecConfig) => {
}
}

// esbuild does not support external source maps without writing to a temp file
// so we need to write to a temp file
const tempPath = path.join(os.tmpdir(), `bundle-${Date.now()}.js`)

const sourcemap = options.sourcemap ?? "inline"

const result = await esbuild.build({
stdin: {
contents: await constructManifest(config),
resolveDir: config.routesDirectory,
loader: "ts",
},
bundle: true,
format: "esm",
write: false,
sourcemap: "inline",
format: "esm",
sourcemap,
outfile: tempPath,
...platformBundleOptions,
})

return result.outputFiles![0].text
const code =
result.outputFiles?.find((file) => file.path.endsWith(".js"))?.text || ""
const sourceMapFile = result.outputFiles?.find((file) =>
file.path.endsWith(".map")
)

return {
code,
sourceMap: sourcemap === "external" ? sourceMapFile?.text : undefined,
}
}
75 changes: 63 additions & 12 deletions src/cli/commands/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,81 @@ export class BundleCommand extends BaseCommand {
details: `
This command bundles your app for distribution. It outputs a zero-dependency file that can be run in a variety of environments.
`,
examples: [[`Bundle your app`, `$0 bundle --output bundled.js`]],
examples: [
[`Bundle your app`, `$0 bundle --output bundled.js`],
[
`Bundle with external source map`,
`$0 bundle --output bundled.js --sourcemap external`,
],
[
`Bundle with inline source map`,
`$0 bundle --output bundled.js --sourcemap inline`,
],
[
`Bundle without source map`,
`$0 bundle --output bundled.js --sourcemap none`,
],
],
})

outputPath = Option.String("--output,-o", {
description: "The path to output the bundle",
required: true,
})

sourcemap = Option.String("--sourcemap", {
description:
"Source map generation: 'external' for .js.map files, 'inline' for inline sourcemaps, 'none' to disable",
})

async run(config: ResolvedWinterSpecConfig) {
// Validate sourcemap option
if (
this.sourcemap &&
!["external", "inline", "none"].includes(this.sourcemap)
) {
throw new Error("sourcemap must be one of: external, inline, none")
}

const spinner = ora("Bundling...").start()
const buildStartedAt = performance.now()

const output = await bundle(config)
// Determine sourcemap option
let sourcemapOption: boolean | "inline" | "external" = "inline"
if (this.sourcemap === "none") {
sourcemapOption = false
} else if (this.sourcemap === "external") {
sourcemapOption = "external"
} else if (this.sourcemap === "inline") {
sourcemapOption = "inline"
}

const result = await bundle(config, { sourcemap: sourcemapOption })

await fs.mkdir(path.dirname(this.outputPath), { recursive: true })
await fs.writeFile(this.outputPath, output)

spinner.stopAndPersist({
symbol: "☃️",
text: ` brr... bundled in ${durationFormatter({
allowMultiples: ["m", "s", "ms"],
})(performance.now() - buildStartedAt)} (${sizeFormatter()(
output.length
)})`,
})
await fs.writeFile(this.outputPath, result.code)

// Write source map file if external source map was generated
if (result.sourceMap) {
const sourcemapPath = `${this.outputPath}.map`
await fs.writeFile(sourcemapPath, result.sourceMap)
spinner.stopAndPersist({
symbol: "☃️",
text: ` brr... bundled in ${durationFormatter({
allowMultiples: ["m", "s", "ms"],
})(performance.now() - buildStartedAt)} (${sizeFormatter()(
result.code.length
)}) with source map`,
})
} else {
spinner.stopAndPersist({
symbol: "☃️",
text: ` brr... bundled in ${durationFormatter({
allowMultiples: ["m", "s", "ms"],
})(performance.now() - buildStartedAt)} (${sizeFormatter()(
result.code.length
)})`,
})
}
}
}
3 changes: 2 additions & 1 deletion src/cli/commands/codegen/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export class CodeGenOpenAPI extends BaseCommand {

async run(config: ResolvedWinterSpecConfig) {
const tempBundlePath = path.join(os.tmpdir(), `${randomUUID()}.mjs`)
await fs.writeFile(tempBundlePath, await bundle(config))
const bundleResult = await bundle(config)
await fs.writeFile(tempBundlePath, bundleResult.code)
const runtimeBundle = await loadBundle(pathToFileURL(tempBundlePath).href)

const globalRouteSpec = Object.values(
Expand Down
59 changes: 49 additions & 10 deletions src/cli2/commands/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,64 @@ export class BundleCommand extends BaseCommand {
.option("--tsconfig <path>", "Path to your tsconfig.json")
.option("--routes-directory <path>", "Path to your routes directory")
.option("--platform <platform>", "The platform to bundle for")
.option(
"--sourcemap <type>",
"Source map generation: 'external' for .js.map files, 'inline' for inline sourcemaps, 'none' to disable",
"inline"
)
.action(async (options) => {
const config = await this.loadConfig(options)

// Validate sourcemap option
if (
options.sourcemap &&
!["external", "inline", "none"].includes(options.sourcemap)
) {
throw new Error("sourcemap must be one of: external, inline, none")
}

const spinner = ora("Bundling...").start()
const buildStartedAt = performance.now()

const output = await bundle(config)
// Determine sourcemap option
let sourcemapOption: boolean | "inline" | "external" = "inline"
if (options.sourcemap === "none") {
sourcemapOption = false
} else if (options.sourcemap === "external") {
sourcemapOption = "external"
} else if (options.sourcemap === "inline") {
sourcemapOption = "inline"
}

const bundleResult = await bundle(config, {
sourcemap: sourcemapOption,
})

await fs.mkdir(path.dirname(options.output), { recursive: true })
await fs.writeFile(options.output, output)
await fs.writeFile(options.output, bundleResult.code)

spinner.stopAndPersist({
symbol: "☃️",
text: ` brr... bundled in ${durationFormatter({
allowMultiples: ["m", "s", "ms"],
})(performance.now() - buildStartedAt)} (${sizeFormatter()(
output.length
)})`,
})
// Write source map file if external source map was generated
if (bundleResult.sourceMap) {
const sourcemapPath = `${options.output}.map`
await fs.writeFile(sourcemapPath, bundleResult.sourceMap)
spinner.stopAndPersist({
symbol: "☃️",
text: ` brr... bundled in ${durationFormatter({
allowMultiples: ["m", "s", "ms"],
})(performance.now() - buildStartedAt)} (${sizeFormatter()(
bundleResult.code.length
)}) with source map`,
})
} else {
spinner.stopAndPersist({
symbol: "☃️",
text: ` brr... bundled in ${durationFormatter({
allowMultiples: ["m", "s", "ms"],
})(performance.now() - buildStartedAt)} (${sizeFormatter()(
bundleResult.code.length
)})`,
})
}
})
}
}
3 changes: 2 additions & 1 deletion src/cli2/commands/codegen/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export class CodeGenOpenAPI extends BaseCommand {
const config = await this.loadConfig(options)

const tempBundlePath = path.join(os.tmpdir(), `${randomUUID()}.mjs`)
await fs.writeFile(tempBundlePath, await bundle(config))
const bundleResult = await bundle(config)
await fs.writeFile(tempBundlePath, bundleResult.code)
const runtimeBundle = await loadBundle(
pathToFileURL(tempBundlePath).href
)
Expand Down
4 changes: 2 additions & 2 deletions tests/adapters/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test.serial("test bundle with Node adapter", async (t) => {
currentDirectory,
"bundled.entrypoint.mjs"
)
await fs.writeFile(bundlePath, bundled, "utf-8")
await fs.writeFile(bundlePath, bundled.code, "utf-8")
await fs.writeFile(
bundleEntrypointPath,
`
Expand Down Expand Up @@ -104,7 +104,7 @@ test.serial(
currentDirectory,
"bundled.entrypoint.mjs"
)
await fs.writeFile(bundlePath, bundled, "utf-8")
await fs.writeFile(bundlePath, bundled.code, "utf-8")
await fs.writeFile(
bundleEntrypointPath,
`
Expand Down
113 changes: 113 additions & 0 deletions tests/bundle/sourcemap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import test from "ava"
import { bundle } from "../../src/bundle/bundle.js"
import type { ResolvedWinterSpecConfig } from "../../src/config/utils.js"
import path from "node:path"

const createTestConfig = (): ResolvedWinterSpecConfig => ({
rootDirectory: path.join(process.cwd(), "examples", "hello-world"),
routesDirectory: path.join(
process.cwd(),
"examples",
"hello-world",
"routes"
),
tsconfigPath: path.join(process.cwd(), "tsconfig.json"),
platform: "wintercg-minimal",
})

test("bundle with inline sourcemap (default)", async (t) => {
const config = createTestConfig()
const result = await bundle(config)

t.is(typeof result.code, "string")
t.true(result.code.length > 0)
t.is(result.sourceMap, undefined)
t.true(result.code.includes("sourceMappingURL=data:application/json;base64,"))
})

test("bundle with inline sourcemap (explicit)", async (t) => {
const config = createTestConfig()
const result = await bundle(config, { sourcemap: "inline" })

t.is(typeof result.code, "string")
t.true(result.code.length > 0)
t.is(result.sourceMap, undefined)
t.true(result.code.includes("sourceMappingURL=data:application/json;base64,"))
})

test("bundle with external sourcemap", async (t) => {
const config = createTestConfig()
const result = await bundle(config, { sourcemap: "external" })

t.is(typeof result.code, "string")
t.is(typeof result.sourceMap, "string")
t.true(result.code.length > 0)
t.true((result.sourceMap as string).length > 0)
t.false(result.code.includes("sourceMappingURL"))

// Verify source map is valid JSON
const sourceMapObj = JSON.parse(result.sourceMap as string)
t.is(sourceMapObj.version, 3)
t.true(Array.isArray(sourceMapObj.sources))
t.is(typeof sourceMapObj.mappings, "string")
})

test("bundle without sourcemap", async (t) => {
const config = createTestConfig()
const result = await bundle(config, { sourcemap: false })

t.is(typeof result.code, "string")
t.true(result.code.length > 0)
t.is(result.sourceMap, undefined)
t.false(result.code.includes("sourceMappingURL"))
})

test("external sourcemap contains original source references", async (t) => {
const config = createTestConfig()
const result = await bundle(config, { sourcemap: "external" })

const sourceMapObj = JSON.parse(result.sourceMap as string)
t.true(
sourceMapObj.sources.some((source: string) =>
source.includes("routes/index.ts")
)
)
t.true(
sourceMapObj.sources.some((source: string) =>
source.includes("src/with-winter-spec.ts")
)
)
})

test("bundle sizes comparison", async (t) => {
const config = createTestConfig()

const [noSourceMap, inlineSourceMap, externalResult] = await Promise.all([
bundle(config, { sourcemap: false }),
bundle(config, { sourcemap: "inline" }),
bundle(config, { sourcemap: "external" }),
])

// External sourcemap code should be similar size to no sourcemap
t.true(Math.abs(noSourceMap.code.length - externalResult.code.length) < 100)

// Inline sourcemap should be significantly larger
t.true(inlineSourceMap.code.length > noSourceMap.code.length * 2)

// External sourcemap file should exist and have reasonable size
t.true((externalResult.sourceMap as string).length > 1000)
})

test("bundle works with node platform", async (t) => {
const config: ResolvedWinterSpecConfig = {
...createTestConfig(),
platform: "node",
}

const result = await bundle(config, { sourcemap: "external" })

t.is(typeof result.code, "string")
t.is(typeof result.sourceMap, "string")
t.true(result.code.length > 0)
t.true((result.sourceMap as string).length > 0)
})
Loading