diff --git a/.github/workflows/test.unit.yml b/.github/workflows/test.unit.yml
index 2e70b9860a..333ee6b5f2 100644
--- a/.github/workflows/test.unit.yml
+++ b/.github/workflows/test.unit.yml
@@ -36,3 +36,6 @@ jobs:
- name: yarn @bud test unit
run: yarn @bud test unit
+ env:
+ AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}}
+ AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}}
diff --git a/examples/s3/bud.config.ts b/examples/s3/bud.config.ts
new file mode 100644
index 0000000000..16576b9dfe
--- /dev/null
+++ b/examples/s3/bud.config.ts
@@ -0,0 +1,16 @@
+import {bud} from '@roots/bud'
+
+bud
+ .html()
+ .when(bud.isProduction, bud =>
+ bud.setPublicPath(
+ `https://bud-js-tests.s3.us-west-2.amazonaws.com/examples/s3/`,
+ ),
+ )
+ .fs.setCredentials({
+ accessKeyId: bud.env.get(`AWS_ACCESS_KEY_ID`),
+ secretAccessKey: bud.env.get(`AWS_SECRET_ACCESS_KEY`),
+ })
+ .setRegion(`us-west-2`)
+ .setBucket(`bud-js-tests`)
+ .upload({destination: `examples/s3`})
diff --git a/examples/s3/package.json b/examples/s3/package.json
new file mode 100644
index 0000000000..2af49ff210
--- /dev/null
+++ b/examples/s3/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@examples/s3",
+ "$schema": "https://bud.js.org/bud.package.json",
+ "private": true,
+ "type": "module",
+ "browserslist": [
+ "extends @roots/browserslist-config"
+ ],
+ "devDependencies": {
+ "@roots/bud": "workspace:*",
+ "@roots/bud-preset-recommend": "workspace:*",
+ "@roots/bud-react": "workspace:*"
+ }
+}
diff --git a/examples/s3/src/app.css b/examples/s3/src/app.css
new file mode 100644
index 0000000000..1443248ece
--- /dev/null
+++ b/examples/s3/src/app.css
@@ -0,0 +1,50 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
+ 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
+ 'Helvetica Neue', sans-serif;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+
+.App {
+ width: 100vw;
+ max-width: 100vw;
+ height: 100vh;
+ max-height: 100vh;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-content: center;
+ background-color: rgba(4, 1, 17, 1);
+
+ & .logo {
+ height: 30vmin;
+ pointer-events: none;
+ margin-bottom: 2rem;
+ animation: App-logo-spin infinite 20s linear;
+ }
+
+ & .header {
+ min-height: 80vh;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+ }
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/examples/s3/src/components/App.js b/examples/s3/src/components/App.js
new file mode 100644
index 0000000000..c68c336a5f
--- /dev/null
+++ b/examples/s3/src/components/App.js
@@ -0,0 +1,12 @@
+import logo from './logo.svg'
+
+export const App = () => {
+ return (
+
+
+
+ Edit
src/components/App.js
and save to reload
+
+
+ )
+}
diff --git a/examples/s3/src/components/logo.svg b/examples/s3/src/components/logo.svg
new file mode 100644
index 0000000000..a7c8baa474
--- /dev/null
+++ b/examples/s3/src/components/logo.svg
@@ -0,0 +1,18 @@
+
+
diff --git a/examples/s3/src/index.js b/examples/s3/src/index.js
new file mode 100644
index 0000000000..d13d278544
--- /dev/null
+++ b/examples/s3/src/index.js
@@ -0,0 +1,7 @@
+import {createRoot} from 'react-dom/client'
+
+import {App} from './components/App.js'
+
+import './app.css'
+
+createRoot(document.getElementById('root')).render()
diff --git a/examples/s3/tsconfig.json b/examples/s3/tsconfig.json
new file mode 100644
index 0000000000..76c443d568
--- /dev/null
+++ b/examples/s3/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@roots/bud/config/tsconfig.json",
+ "compilerOptions": {
+ "paths": {
+ "@src/*": ["./src/*"]
+ },
+ "types": ["@roots/bud", "@roots/bud-preset-recommend", "@roots/bud-react"]
+ },
+ "files": ["./bud.config.ts"],
+ "include": ["./src"],
+ "exclude": ["./dist"]
+}
diff --git a/sources/@roots/bud-framework/src/fs.ts b/sources/@roots/bud-framework/src/fs.ts
index 3f9e68f475..c58182f4e6 100644
--- a/sources/@roots/bud-framework/src/fs.ts
+++ b/sources/@roots/bud-framework/src/fs.ts
@@ -90,10 +90,7 @@ export class FS extends Filesystem implements Contract {
*/
@bind
public setBucket(bucket: string) {
- this.app.after(async () => {
- this.s3.config.set(`bucket`, bucket)
- })
-
+ this.s3.config.set(`bucket`, bucket)
return this
}
@@ -106,10 +103,7 @@ export class FS extends Filesystem implements Contract {
*/
@bind
public setCredentials(credentials: S3[`config`][`credentials`]) {
- this.app.after(async () => {
- this.s3.config.set(`credentials`, credentials)
- })
-
+ this.s3.config.set(`credentials`, credentials)
return this
}
@@ -122,10 +116,7 @@ export class FS extends Filesystem implements Contract {
*/
@bind
public setEndpoint(endpoint: S3[`config`][`endpoint`]) {
- this.app.after(async () => {
- this.s3.config.set(`endpoint`, endpoint)
- })
-
+ this.s3.config.set(`endpoint`, endpoint)
return this
}
@@ -138,10 +129,7 @@ export class FS extends Filesystem implements Contract {
*/
@bind
public setRegion(region: S3[`config`][`region`]) {
- this.app.after(async () => {
- this.s3.config.set(`region`, region)
- })
-
+ this.s3.config.set(`region`, region)
return this
}
@@ -159,33 +147,38 @@ export class FS extends Filesystem implements Contract {
keep?: false | number
source?: string
}): this {
- if (!this.s3.config.credentials) {
- throw BudError.normalize(
- `S3 is not configured. See https://bud.js.org/reference/bud.fs/s3`,
- )
- }
+ this.app.after(async () => {
+ if (!this.s3.config.credentials) {
+ throw BudError.normalize(
+ `S3 is not configured. See https://bud.js.org/reference/bud.fs/s3`,
+ )
+ }
- const {destination, files, keep, source} = {
- destination: options?.destination,
- files: options?.files ?? `**/*`,
- keep:
- isNumber(options?.keep) || isBoolean(options?.keep)
- ? options?.keep
- : 5,
- source: options?.source ?? this.app.path(`@dist`),
- }
+ const s3Path = (path: string) =>
+ destination ? join(destination, path) : path
- const s3Path = (path: string) =>
- destination ? join(destination, path) : path
+ const {destination, files, keep, source} = {
+ destination: options?.destination,
+ files: options?.files ?? `**/*`,
+ keep:
+ isNumber(options?.keep) || isBoolean(options?.keep)
+ ? options?.keep
+ : 5,
+ source: options?.source ?? this.app.path(`@dist`),
+ }
- this.app.after(async () => {
- // eslint-disable-next-line
- console.log(`Uploading...`)
+ // eslint-disable-next-line no-console
+ console.log(`\nUploading files to ${this.s3.ident}`)
await globby(files, {cwd: source}).then(async files => {
const descriptions = await Promise.all(
files.map(async file => {
+ this.logger.info(
+ `Attempting to read ${file}:`,
+ join(source, file),
+ )
const contents = await this.read(join(source, file), `buffer`)
+ this.logger.info(`Read ${file}:`, contents)
return {contents, file}
}),
)
@@ -261,6 +254,9 @@ export class FS extends Filesystem implements Contract {
this.logger.timeEnd(
`Write upload-manifest.json to ${this.s3.ident}`,
)
+
+ // eslint-disable-next-line no-console
+ console.log(`\nUpload complete`)
})
})
diff --git a/sources/@roots/filesystem/src/s3/config.ts b/sources/@roots/filesystem/src/s3/config.ts
index f784671329..4787e1a99b 100644
--- a/sources/@roots/filesystem/src/s3/config.ts
+++ b/sources/@roots/filesystem/src/s3/config.ts
@@ -36,9 +36,7 @@ export class Config {
* @returns value - {@link S3ClientConfig} value
*/
public get<
- K extends `${
- | (keyof Config & string)
- | (keyof S3ClientConfig & keyof Config & string)}`,
+ K extends `bucket` | `credentials` | `endpoint` | `public` | `region`,
>(key: K): this[K] {
return this[key]
}
@@ -51,9 +49,7 @@ export class Config {
* @returns void
*/
public set<
- K extends `${
- | (keyof Config & string)
- | (keyof S3ClientConfig & keyof Config & string)}`,
+ K extends `bucket` | `credentials` | `endpoint` | `public` | `region`,
>(key: K, value: this[K]): void {
this[key] = value
}
diff --git a/sources/@roots/filesystem/src/s3/index.ts b/sources/@roots/filesystem/src/s3/index.ts
index 69758dba8b..a19f7b43c6 100644
--- a/sources/@roots/filesystem/src/s3/index.ts
+++ b/sources/@roots/filesystem/src/s3/index.ts
@@ -9,7 +9,6 @@ import type {Readable} from 'node:stream'
import SDK from '@roots/filesystem/vendor/sdk'
import {bind} from 'helpful-decorators'
-import isString from 'lodash-es/isString.js'
import * as mimetypes from 'mime-types'
import {Client} from './client.js'
@@ -22,28 +21,19 @@ export class S3 {
/**
* Client instance
*/
- public client: Client
+ public client: Client = new Client()
/**
* S3 configuration
*/
- public config: Config
-
- /**
- * constructor
- */
- public constructor() {
- this.config = new Config()
- this.client = new Client()
- }
+ public config: Config = new Config()
/**
* Delete a file from s3
*
* @param key - The file key
* @returns S3 instance {@link S3}
- * @throws Error - If the file does not exist
- * @throws Error - If the file could not be deleted
+ * @throws Error - If the file does not exist or could not be deleted
*/
@bind
public async delete(key: string) {
@@ -53,9 +43,7 @@ export class S3 {
Bucket: this.config.bucket,
Key: key,
})
- // @ts-ignore
await client.send(DeleteObjectOutput)
-
return this
} catch (error) {
throw error
@@ -69,10 +57,10 @@ export class S3 {
* @returns boolean
*/
@bind
- public async exists(key: string) {
+ public async exists(key: string): Promise {
try {
- const files = (await this.list()) as Array
- return files.some(item => item === key)
+ const files = await this.list()
+ return files.some(item => item === key) ?? false
} catch (error) {
return false
}
@@ -100,20 +88,21 @@ export class S3 {
* Identifier (for loggers, etc)
*/
public get ident() {
- const maybeEndpoint = this.config.get(`endpoint`)
+ const [bucket, endpoint, region] = [
+ this.config.get(`bucket`),
+ this.config.get(`endpoint`),
+ this.config.get(`region`),
+ ]
+
+ if (!(typeof endpoint === `string`)) return `${bucket} (${region})`
- if (!maybeEndpoint)
- return `${this.config.get(`bucket`)} (${this.config.get(`region`)})`
+ if (endpoint.includes(`digitaloceanspaces`)) {
+ const hostname = new URL(endpoint).hostname
- if (
- isString(maybeEndpoint) &&
- maybeEndpoint.includes(`digitaloceanspaces`)
- )
- return `https://${this.config.get(`bucket`)}.${
- new URL(maybeEndpoint).hostname
- }`
+ return `https://${bucket}.${hostname}`
+ }
- return `https://${maybeEndpoint.toString()}`
+ return `https://${endpoint}`
}
/**
@@ -138,8 +127,9 @@ export class S3 {
...(props ?? {}),
}),
)
+
// @ts-ignore
- return results?.Contents.map(({Key}) => Key)
+ return results?.Contents?.map(({Key}) => Key) ?? []
} catch (error) {
throw error
}
@@ -159,7 +149,7 @@ export class S3 {
*/
@bind
public async read(key: string, raw = false): Promise {
- const streamToString = ({Body: stream}: {Body: Readable}) =>
+ const streamToString = async ({Body: stream}: {Body: Readable}) =>
new Promise((resolve, reject) => {
const chunks: Array = []
@@ -171,14 +161,17 @@ export class S3 {
const client = this.getClient()
- const GetObjectCommandOutput = new SDK.GetObjectCommand({
+ const GetObject = new SDK.GetObjectCommand({
Bucket: this.config.bucket,
Key: key,
})
try {
- const request: any = await client.send(GetObjectCommandOutput)
- if (!request) return raw ? request : streamToString(request)
+ const request: any = await client.send(GetObject)
+ if (!request) return undefined
+ if (raw) return request
+
+ return await streamToString(request)
} catch (error) {
throw error
}
@@ -197,7 +190,6 @@ export class S3 {
| [string, Blob | Readable | ReadableStream | string]
) {
const putProps = {
- ACL: this.config.public ? `public-read` : `private`,
Bucket: this.config.bucket,
ContentType: null,
Key: null,
diff --git a/sources/@roots/filesystem/test/s3/config.test.ts b/sources/@roots/filesystem/test/s3/config.test.ts
index 6ae6066291..7ee8efe4f9 100644
--- a/sources/@roots/filesystem/test/s3/config.test.ts
+++ b/sources/@roots/filesystem/test/s3/config.test.ts
@@ -3,7 +3,7 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
import {Config} from '../../src/s3/config.js'
describe(`s3 config`, () => {
- let config
+ let config: Config
beforeEach(async () => {
vi.clearAllMocks()
@@ -16,6 +16,7 @@ describe(`s3 config`, () => {
accessKeyId: `foo`,
secretAccessKey: `bar`,
})
+
expect(config.credentials).toEqual(
expect.objectContaining({
accessKeyId: `foo`,
diff --git a/sources/@roots/filesystem/test/s3/index.test.ts b/sources/@roots/filesystem/test/s3/index.test.ts
index 4f7d3a392a..53afe14eba 100644
--- a/sources/@roots/filesystem/test/s3/index.test.ts
+++ b/sources/@roots/filesystem/test/s3/index.test.ts
@@ -11,9 +11,9 @@ const mockConfigImplementation = {
accessKeyId: `foo`,
secretAccessKey: `bar`,
},
- region: `us-east-1`,
endpoint: `https://s3.amazonaws.com`,
get: vi.fn(),
+ region: `us-east-1`,
set: vi.fn(),
}
mockConfigImplementation.get = vi.fn(
diff --git a/tests/reproductions/issue-2574.test.ts b/tests/reproductions/issue-2574.test.ts
new file mode 100644
index 0000000000..03d89a990c
--- /dev/null
+++ b/tests/reproductions/issue-2574.test.ts
@@ -0,0 +1,48 @@
+/* eslint-disable n/no-process-env */
+import {sep} from 'node:path'
+
+import {path} from '@repo/constants'
+import execa from '@roots/bud-support/execa'
+import {Filesystem} from '@roots/bud-support/filesystem'
+import {beforeAll, describe, expect, it} from 'vitest'
+
+describe(`issue-2574`, () => {
+ let fs: Filesystem
+ let head: string
+
+ beforeAll(async () => {
+ fs = new Filesystem()
+
+ head = (await fs.read(path(`.git`, `HEAD`)))
+ .toString()
+ .split(sep)
+ .pop()
+ .trim()
+
+ await execa(`yarn`, [`bud`, `clean`], {
+ cwd: path(`tests`, `reproductions`, `issue-2574`),
+ })
+
+ await execa(`yarn`, [`bud`, `build`], {
+ cwd: path(`tests`, `reproductions`, `issue-2574`),
+ })
+ }, 30000)
+
+ it(`should upload to s3`, async () => {
+ const result = await fetch(
+ `https://bud-js-tests.s3.us-west-2.amazonaws.com/${head}/js/main.js`,
+ )
+ expect(await result.text()).toBe(
+ await fs.read(
+ path(
+ `tests`,
+ `reproductions`,
+ `issue-2574`,
+ `dist`,
+ `js`,
+ `main.js`,
+ ),
+ ),
+ )
+ })
+}, 120000)
diff --git a/tests/reproductions/issue-2574/bud.config.ts b/tests/reproductions/issue-2574/bud.config.ts
new file mode 100644
index 0000000000..c377ed19cd
--- /dev/null
+++ b/tests/reproductions/issue-2574/bud.config.ts
@@ -0,0 +1,23 @@
+import { sep } from 'node:path'
+
+import {bud} from '@roots/bud'
+
+const path = bud.path(`..`, `..`, `..`, `.git`, `HEAD`)
+
+const head = (await bud.fs.read(path))
+ ?.toString()
+ .split(sep)
+ .pop()
+ .trim()
+
+bud.fs
+ .setCredentials({
+ accessKeyId: bud.env.get(`AWS_ACCESS_KEY_ID`),
+ secretAccessKey: bud.env.get(`AWS_SECRET_ACCESS_KEY`),
+ })
+ .setRegion(`us-west-2`)
+ .setBucket(`bud-js-tests`)
+ .upload({
+ destination: head,
+ keep: 2,
+ })
diff --git a/tests/reproductions/issue-2574/package.json b/tests/reproductions/issue-2574/package.json
new file mode 100644
index 0000000000..b77f7ab0e7
--- /dev/null
+++ b/tests/reproductions/issue-2574/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@tests/issue-2574",
+ "$schema": "https://bud.js.org/bud.package.json",
+ "private": true,
+ "type": "module",
+ "devDependencies": {
+ "@roots/bud": "workspace:*"
+ }
+}
diff --git a/tests/reproductions/issue-2574/src/index.css b/tests/reproductions/issue-2574/src/index.css
new file mode 100644
index 0000000000..5878444be3
--- /dev/null
+++ b/tests/reproductions/issue-2574/src/index.css
@@ -0,0 +1,9 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+}
+
+body {
+ background-image: url(~@src/logo.svg?inline);
+}
diff --git a/tests/reproductions/issue-2574/src/index.js b/tests/reproductions/issue-2574/src/index.js
new file mode 100644
index 0000000000..51f5b151a9
--- /dev/null
+++ b/tests/reproductions/issue-2574/src/index.js
@@ -0,0 +1,3 @@
+import './index.css'
+
+window.test = true
diff --git a/tests/reproductions/issue-2574/src/logo.svg b/tests/reproductions/issue-2574/src/logo.svg
new file mode 100644
index 0000000000..97ffc493db
--- /dev/null
+++ b/tests/reproductions/issue-2574/src/logo.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/tests/reproductions/issue-2574/tsconfig.json b/tests/reproductions/issue-2574/tsconfig.json
new file mode 100644
index 0000000000..e6deaedd52
--- /dev/null
+++ b/tests/reproductions/issue-2574/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "@roots/bud/config/tsconfig.json",
+ "compilerOptions": {
+ "types": ["node", "@roots/bud"]
+ },
+ "files": ["./bud.config.ts"],
+ "include": ["./src"],
+ "exclude": ["./node_modules"]
+}
diff --git a/yarn.lock b/yarn.lock
index f5096e11fa..1f41e1babe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6191,6 +6191,16 @@ __metadata:
languageName: unknown
linkType: soft
+"@examples/s3@workspace:examples/s3":
+ version: 0.0.0-use.local
+ resolution: "@examples/s3@workspace:examples/s3"
+ dependencies:
+ "@roots/bud": "workspace:*"
+ "@roots/bud-preset-recommend": "workspace:*"
+ "@roots/bud-react": "workspace:*"
+ languageName: unknown
+ linkType: soft
+
"@examples/sage@workspace:examples/sage":
version: 0.0.0-use.local
resolution: "@examples/sage@workspace:examples/sage"
@@ -12012,6 +12022,14 @@ __metadata:
languageName: unknown
linkType: soft
+"@tests/issue-2574@workspace:tests/reproductions/issue-2574":
+ version: 0.0.0-use.local
+ resolution: "@tests/issue-2574@workspace:tests/reproductions/issue-2574"
+ dependencies:
+ "@roots/bud": "workspace:*"
+ languageName: unknown
+ linkType: soft
+
"@tests/minimize-flag@workspace:sources/@roots/bud/test/cli-flag-minimize/project":
version: 0.0.0-use.local
resolution: "@tests/minimize-flag@workspace:sources/@roots/bud/test/cli-flag-minimize/project"