diff --git a/.eslintrc.js b/.eslintrc.js index a34b25d7a0..96c2cfd7f1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,9 +87,10 @@ module.exports = { }, }, { - files: ["**/__fixtures__/**"], + files: ["test/**", "**/__fixtures__/**"], rules: { "import/no-default-export": "off", + "require-await": "off", }, }, ], diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7699cb53a..6092406a73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,7 +89,7 @@ jobs: path: ./* key: ${{ github.sha }} - build_and_test_pkgs: + testBlitzPackages: name: Blitz Packages Tests needs: build runs-on: ubuntu-latest @@ -108,7 +108,7 @@ jobs: env: CI: true - build_and_test_examples: + testBlitzExamples: timeout-minutes: 30 name: Blitz Example Apps Tests strategy: @@ -193,6 +193,32 @@ jobs: - run: node run-tests.js --timings --type unit -g 1/1 if: ${{needs.build.outputs.docsChange != 'docs only change'}} + testIntegrationBlitz: + name: Blitz - Test Integration + runs-on: ubuntu-latest + needs: build + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + HEADLESS: true + strategy: + fail-fast: false + steps: + - run: echo ${{needs.build.outputs.docsChange}} + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }} + + # TODO: remove after we fix watchpack watching too much + - run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + + - run: xvfb-run node nextjs/run-tests.js -c 3 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + testIntegration: name: Nextjs - Test Integration defaults: @@ -259,7 +285,15 @@ jobs: testsPass: name: thank you, next runs-on: ubuntu-latest - needs: [checkPrecompiled, testIntegration, testUnit] + needs: + [ + checkPrecompiled, + testIntegration, + testIntegrationBlitz, + testUnit, + testBlitzPackages, + testBlitzExamples, + ] steps: - run: exit 0 diff --git a/.prettierignore b/.prettierignore index de68853233..8470f1640c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,7 +4,7 @@ .log .DS_Store .jest-* -lib +packages/cli/lib node_modules reports *.log diff --git a/babel.config.js b/babel.config.js index 0f4bc29ec3..81744e75eb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -26,4 +26,24 @@ module.exports = { }, ], ], + overrides: [ + { + test: "./test/**/*", + presets: [ + [ + "@babel/preset-env", + { + modules: false, + // loose: true, + exclude: [ + "@babel/plugin-transform-async-to-generator", + "@babel/plugin-transform-regenerator", + ], + }, + ], + "blitz/babel", + ], + plugins: [], + }, + ], } diff --git a/examples/auth/blitz.config.js b/examples/auth/blitz.config.js index ac0eddeab8..938421be11 100644 --- a/examples/auth/blitz.config.js +++ b/examples/auth/blitz.config.js @@ -1,27 +1,25 @@ -const withMonorepoBuildTooling = require("@preconstruct/next") const {sessionMiddleware, simpleRolesIsAuthorized} = require("blitz") const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true", }) -module.exports = withMonorepoBuildTooling( - withBundleAnalyzer({ - middleware: [ - sessionMiddleware({ - isAuthorized: simpleRolesIsAuthorized, - // sessionExpiryMinutes: 4, - }), - ], - cli: { - clearConsoleOnBlitzDev: false, - }, - log: { - // level: "trace", - }, - experimental: { - isomorphicResolverImports: false, - }, - /* +module.exports = withBundleAnalyzer({ + middleware: [ + sessionMiddleware({ + isAuthorized: simpleRolesIsAuthorized, + // sessionExpiryMinutes: 4, + }), + ], + cli: { + clearConsoleOnBlitzDev: false, + }, + log: { + // level: "trace", + }, + experimental: { + isomorphicResolverImports: false, + }, + /* webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => { // Note: we provide webpack above so you should not `require` it // Perform customizations to webpack config @@ -34,5 +32,4 @@ module.exports = withMonorepoBuildTooling( return config }, */ - }), -) +}) diff --git a/examples/custom-server/blitz.config.js b/examples/custom-server/blitz.config.js index 60b3a9e995..ccd8b2f16f 100644 --- a/examples/custom-server/blitz.config.js +++ b/examples/custom-server/blitz.config.js @@ -1,7 +1,6 @@ const {sessionMiddleware, simpleRolesIsAuthorized} = require("blitz") -const withMonorepoBuildTooling = require("@preconstruct/next") -module.exports = withMonorepoBuildTooling({ +module.exports = { middleware: [ sessionMiddleware({ isAuthorized: simpleRolesIsAuthorized, @@ -15,4 +14,4 @@ module.exports = withMonorepoBuildTooling({ return config }, */ -}) +} diff --git a/examples/fauna/blitz.config.js b/examples/fauna/blitz.config.js index f9a47d0b86..c02de61653 100644 --- a/examples/fauna/blitz.config.js +++ b/examples/fauna/blitz.config.js @@ -1,5 +1,4 @@ const { sessionMiddleware, simpleRolesIsAuthorized } = require("blitz") -const withMonorepoBuildTooling = require("@preconstruct/next") const { GraphQLClient, gql } = require("graphql-request") const graphQLClient = new GraphQLClient("https://graphql.fauna.com/graphql", { @@ -18,7 +17,7 @@ const normalizeSession = (faunaSession) => { } } -module.exports = withMonorepoBuildTooling({ +module.exports = { middleware: [ sessionMiddleware({ isAuthorized: simpleRolesIsAuthorized, @@ -159,4 +158,4 @@ module.exports = withMonorepoBuildTooling({ return config }, */ -}) +} diff --git a/examples/no-prisma/blitz.config.js b/examples/no-prisma/blitz.config.js index 891863f454..3b17b02ef1 100644 --- a/examples/no-prisma/blitz.config.js +++ b/examples/no-prisma/blitz.config.js @@ -1,6 +1,4 @@ -const withMonorepoBuildTooling = require("@preconstruct/next") - -module.exports = withMonorepoBuildTooling({ +module.exports = { webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { // Note: we provide webpack above so you should not `require` it // Perform customizations to webpack config @@ -12,4 +10,4 @@ module.exports = withMonorepoBuildTooling({ // Important: return the modified config return config }, -}) +} diff --git a/examples/static/blitz.config.js b/examples/static/blitz.config.js index 53a72de17c..1554aa5af5 100644 --- a/examples/static/blitz.config.js +++ b/examples/static/blitz.config.js @@ -1,6 +1,4 @@ -const withMonorepoBuildTooling = require("@preconstruct/next") - -module.exports = withMonorepoBuildTooling({ +module.exports = { middleware: [], /* Uncomment this to customize the webpack config webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { @@ -10,4 +8,4 @@ module.exports = withMonorepoBuildTooling({ return config }, */ -}) +} diff --git a/examples/store/blitz.config.js b/examples/store/blitz.config.js index c80a855737..926699068b 100644 --- a/examples/store/blitz.config.js +++ b/examples/store/blitz.config.js @@ -1,6 +1,4 @@ -const withMonorepoBuildTooling = require("@preconstruct/next") - -module.exports = withMonorepoBuildTooling({ +module.exports = { middleware: [ (req, res, next) => { res.blitzCtx.referer = req.headers.referer @@ -24,4 +22,4 @@ module.exports = withMonorepoBuildTooling({ // // Important: return the modified config // return config // }, -}) +} diff --git a/jest-unit.config.js b/jest-unit.config.js new file mode 100644 index 0000000000..ba82415013 --- /dev/null +++ b/jest-unit.config.js @@ -0,0 +1,37 @@ +const {jsWithBabel: tsjPreset} = require("ts-jest/preset") + +module.exports = { + testEnvironment: "node", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + modulePathIgnorePatterns: ["/tmp", "/dist", "/templates"], + moduleNameMapper: {}, + setupFilesAfterEnv: ["/jest.setup.js"], + transform: { + ...tsjPreset.transform, + }, + transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"], + testMatch: ["/**/*.(spec|test).{ts,tsx,js,jsx}"], + testURL: "http://localhost", + // watchPlugins: [ + // require.resolve("jest-watch-typeahead/filename"), + // require.resolve("jest-watch-typeahead/testname"), + // ], + coverageReporters: ["json", "lcov", "text", "clover"], + // collectCoverage: !!`Boolean(process.env.CI)`, + collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"], + coveragePathIgnorePatterns: ["/templates/"], + // coverageThreshold: { + // global: { + // branches: 100, + // functions: 100, + // lines: 100, + // statements: 100, + // }, + // }, + globals: { + "ts-jest": { + tsconfig: __dirname + "/tsconfig.test.json", + isolatedModules: true, + }, + }, +} diff --git a/jest.config.js b/jest.config.js index ba82415013..a4cbaddfc2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,37 +1,10 @@ -const {jsWithBabel: tsjPreset} = require("ts-jest/preset") - module.exports = { - testEnvironment: "node", - moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], - modulePathIgnorePatterns: ["/tmp", "/dist", "/templates"], - moduleNameMapper: {}, - setupFilesAfterEnv: ["/jest.setup.js"], - transform: { - ...tsjPreset.transform, - }, - transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"], - testMatch: ["/**/*.(spec|test).{ts,tsx,js,jsx}"], - testURL: "http://localhost", - // watchPlugins: [ - // require.resolve("jest-watch-typeahead/filename"), - // require.resolve("jest-watch-typeahead/testname"), - // ], - coverageReporters: ["json", "lcov", "text", "clover"], - // collectCoverage: !!`Boolean(process.env.CI)`, - collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"], - coveragePathIgnorePatterns: ["/templates/"], - // coverageThreshold: { - // global: { - // branches: 100, - // functions: 100, - // lines: 100, - // statements: 100, - // }, - // }, - globals: { - "ts-jest": { - tsconfig: __dirname + "/tsconfig.test.json", - isolatedModules: true, - }, - }, + testMatch: ["**/*.test.js", "**/*.test.ts"], + verbose: true, + rootDir: "test", + modulePaths: ["/lib"], + globalSetup: "/jest-global-setup.js", + globalTeardown: "/jest-global-teardown.js", + setupFilesAfterEnv: ["/jest-setup-after-env.js"], + testEnvironment: "/jest-environment.js", } diff --git a/nextjs/packages/next/package.json b/nextjs/packages/next/package.json index 2f2b3c47cd..0e86cd1a02 100644 --- a/nextjs/packages/next/package.json +++ b/nextjs/packages/next/package.json @@ -48,7 +48,7 @@ "dev": "taskr", "release": "taskr release", "prepublish": "npm run release && yarn types", - "types": "tsc --declaration --emitDeclarationOnly --declarationDir dist", + "types": "rimraf \"dist/**/*.d.ts\" && tsc --declaration --emitDeclarationOnly --declarationDir dist", "typescript": "tsc --noEmit --declaration", "ncc-compiled": "ncc cache clean && taskr ncc" }, diff --git a/nextjs/run-tests.js b/nextjs/run-tests.js index aeee9bda1a..ce970d8cf1 100644 --- a/nextjs/run-tests.js +++ b/nextjs/run-tests.js @@ -64,7 +64,7 @@ async function main() { tests = ( await glob('**/*.test.js', { nodir: true, - cwd: path.join(__dirname, 'test'), + cwd: path.join(process.cwd(), 'test'), }) ).filter((test) => { // only include the specified type @@ -79,7 +79,7 @@ async function main() { if (outputTimings && groupArg) { console.log('Fetching previous timings data') try { - const timingsFile = path.join(__dirname, 'test-timings.json') + const timingsFile = path.join(process.cwd(), 'test-timings.json') try { prevTimings = JSON.parse(await fs.readFile(timingsFile, 'utf8')) console.log('Loaded test timings from disk successfully') @@ -234,7 +234,7 @@ async function main() { } catch (err) { if (i < NUM_RETRIES) { try { - const testDir = path.dirname(path.join(__dirname, test)) + const testDir = path.dirname(path.join(process.cwd(), test)) console.log('Cleaning test files at', testDir) await exec(`git clean -fdx "${testDir}"`) await exec(`git checkout "${testDir}"`) diff --git a/package.json b/package.json index e1385ad0fb..4a56a038c3 100644 --- a/package.json +++ b/package.json @@ -24,17 +24,23 @@ }, "scripts": { "postinstall": "husky install && patch-package && symlink-dir node_modules/@blitzjs/next node_modules/next", + "wait:nextjs": "wait-on -d 1000 nextjs/packages/next/dist/build/index.js", + "wait:nextjs-types": "wait-on -d 1000 nextjs/packages/next/dist/build/index.d.ts", "dev:nextjs": "yarn workspace @blitzjs/next dev", - "dev:tsc": "tsc --watch --pretty --preserveWatchOutput", - "dev:cli": "yarn workspace @blitzjs/cli dev", + "dev:nextjs-types": "yarn wait:nextjs && yarn workspace @blitzjs/next types && echo 'Finished building nextjs types'", + "dev:blitz": "preconstruct watch", + "dev:tsc": "yarn dev:nextjs-types && tsc --watch --pretty --preserveWatchOutput", + "dev:cli": "yarn wait:nextjs && yarn workspace @blitzjs/cli dev", "dev:templates": "yarn workspace @blitzjs/generator dev", - "dev": "yarn workspace @blitzjs/next prepublish && preconstruct dev && concurrently --names \"typecheck,cli,templates\" -c \"blue,green,yellow,magenta\" -p \"{name}\" \"npm:dev:tsc\" \"npm:dev:cli\" \"npm:dev:templates\"", + "dev": "concurrently --names \"nextjs,blitz,typecheck,cli,templates\" -c \"magenta,cyan,green,yellow,black\" -p \"{name}\" \"npm:dev:nextjs\" \"npm:dev:blitz\" \"npm:dev:tsc\" \"npm:dev:cli\" \"npm:dev:templates\"", "build:nextjs": "yarn workspace @blitzjs/next prepublish", "build": "yarn build:nextjs && cross-env BLITZ_PROD_BUILD=true preconstruct build && ultra -r --filter \"packages/*\" buildpkg && tsc", "lint": "eslint --ext \".js,.ts,.tsx\" .", "link-cli": "yarn workspace blitz link", "unlink-cli": "yarn workspace blitz unlink", "test": "yarn run lint && yarn run build && ultra -r test", + "testheadless": "cross-env HEADLESS=true yarn test:integration", + "test:integration": "jest --runInBand", "test:packages": "yarn run build && yarn testonly:packages", "test:examples": "yarn run build && yarn testonly:examples", "test:nextjs-size": "yarn --cwd nextjs testheadless --testPathPattern \"integration/(build-output|size-limit|fallback-modules)\"", @@ -77,8 +83,7 @@ "@babel/preset-typescript": "7.12.7", "@juanm04/cpx": "2.0.0", "@manypkg/cli": "0.17.0", - "@preconstruct/cli": "2.0.5", - "@preconstruct/next": "2.0.0", + "@preconstruct/cli": "2.0.7", "@rollup/pluginutils": "4.1.0", "@size-limit/preset-small-lib": "4.9.2", "@testing-library/jest-dom": "5.11.9", @@ -95,6 +100,7 @@ "@types/flush-write-stream": "1.0.0", "@types/from2": "2.3.0", "@types/fs-extra": "8.1.0", + "@types/get-port": "4.2.0", "@types/gulp-if": "0.0.33", "@types/htmlescape": "^1.1.1", "@types/ink-spinner": "3.0.0", @@ -153,6 +159,7 @@ "eslint-plugin-simple-import-sort": "7.0.0", "eslint-plugin-unicorn": "26.0.1", "eslint_d": "10.0.0", + "get-port": "5.1.1", "husky": "5.1.2", "jest": "27.0.0-next.5", "lerna": "4.0.0", @@ -179,6 +186,7 @@ "stdout-stderr": "0.1.13", "strip-ansi": "6.0.0", "test-listen": "1.1.0", + "tree-kill": "1.2.2", "ts-jest": "27.0.0-next.10", "tslib": "2.1.0", "typescript": "4.1.5", diff --git a/packages/babel-preset/jest.config.js b/packages/babel-preset/jest.config.js index 5c2164c438..452afd8cf1 100644 --- a/packages/babel-preset/jest.config.js +++ b/packages/babel-preset/jest.config.js @@ -1,4 +1,4 @@ module.exports = { - preset: '../../jest.config.js', + preset: '../../jest-unit.config.js', testEnvironment: 'jest-environment-jsdom', }; diff --git a/packages/babel-preset/src/index.ts b/packages/babel-preset/src/index.ts index 4ce300a678..a8a89f54cf 100644 --- a/packages/babel-preset/src/index.ts +++ b/packages/babel-preset/src/index.ts @@ -4,7 +4,8 @@ import RewriteImports from './rewrite-imports'; // eslint-disable-next-line import/no-default-export export default function preset(_api: any, options = {}) { // const isTest = _api.env('test'); - const isRunningInJest = Boolean(process.env.JEST_WORKER_ID); + const isRunningInJest = + process.env.JEST_WORKER_ID && !process.env.__NEXT_TEST_MODE; const config = { presets: [[require('next/babel'), options]], diff --git a/packages/blitz/jest.config.js b/packages/blitz/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/blitz/jest.config.js +++ b/packages/blitz/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 111f511f81..4ddc7a0b88 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", // collectCoverage: !!`Boolean(process.env.CI)`, modulePathIgnorePatterns: ["/tmp", "/lib", "/commands/.test"], testPathIgnorePatterns: ["src/commands/test.ts", "test/commands/.test"], diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index e0a19f3f21..0907ea1b00 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -40,7 +40,10 @@ export class Dev extends Command { const {getConfig} = await import("@blitzjs/config") const blitzConfig = getConfig() - if (blitzConfig.cli?.clearConsoleOnBlitzDev !== false) { + if ( + blitzConfig.cli?.clearConsoleOnBlitzDev !== false && + !process.env.BLITZ_TEST_ENVIRONMENT + ) { const {log} = await import("@blitzjs/display") log.clearConsole() } diff --git a/packages/cli/src/utils/is-blitz-root.ts b/packages/cli/src/utils/is-blitz-root.ts index b0faf0025e..461f0af340 100644 --- a/packages/cli/src/utils/is-blitz-root.ts +++ b/packages/cli/src/utils/is-blitz-root.ts @@ -1,4 +1,4 @@ -import {readJSON} from "fs-extra" +import {existsSync, readJSON} from "fs-extra" import {resolve} from "path" import pkgDir from "pkg-dir" @@ -54,6 +54,10 @@ export const isBlitzRoot = async (): Promise<{ if (err.code === "ENOENT") { const out = await checkParent() + if (existsSync("./blitz.config.js") || existsSync("./blitz.config.ts")) { + return {err: false} + } + if (out === false) { return { err: true, diff --git a/packages/config/jest.config.js b/packages/config/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/config/jest.config.js +++ b/packages/config/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index c8925c5779..f795af785d 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -5,7 +5,9 @@ import pkgDir from "pkg-dir" const debug = require("debug")("blitz:config") export function getProjectRoot() { - return pkgDir.sync() || process.cwd() + return ( + path.dirname(path.resolve(process.cwd(), "blitz.config.js")) || pkgDir.sync() || process.cwd() + ) } export interface BlitzConfig extends Record { @@ -40,11 +42,16 @@ export const getConfig = (reload?: boolean): BlitzConfig => { const {PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_SERVER} = require("next/constants") - const pkgJson = readJSONSync(join(getProjectRoot(), "package.json")) + let pkgJson: any + + const pkgJsonPath = join(getProjectRoot(), "package.json") + if (existsSync(pkgJsonPath)) { + pkgJson = readJSONSync(join(getProjectRoot(), "package.json")) + } let blitzConfig = { _meta: { - packageName: pkgJson.name, + packageName: pkgJson?.name, }, } @@ -89,10 +96,10 @@ export const getConfig = (reload?: boolean): BlitzConfig => { ...loadedNextConfig, ...loadedBlitzConfig, } - } catch { + } catch (error) { // https://github.com/blitz-js/blitz/issues/2080 if (!process.env.JEST_WORKER_ID) { - console.error("Failed to load config in getConfig()") + console.error("Failed to load config in getConfig()", error) } } diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index 5686ada8d1..5f73125866 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -1,4 +1,4 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", testEnvironment: "jest-environment-jsdom", } diff --git a/packages/display/jest.config.js b/packages/display/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/display/jest.config.js +++ b/packages/display/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/packages/file-pipeline/jest.config.js b/packages/file-pipeline/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/file-pipeline/jest.config.js +++ b/packages/file-pipeline/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/packages/generator/jest.config.js b/packages/generator/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/generator/jest.config.js +++ b/packages/generator/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/packages/installer/jest.config.js b/packages/installer/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/installer/jest.config.js +++ b/packages/installer/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/packages/repl/jest.config.js b/packages/repl/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/repl/jest.config.js +++ b/packages/repl/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/packages/server/jest.config.js b/packages/server/jest.config.js index caa51b037b..4a5b929e59 100644 --- a/packages/server/jest.config.js +++ b/packages/server/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: "../../jest.config.js", + preset: "../../jest-unit.config.js", } diff --git a/patches/@preconstruct+cli+2.0.5.patch b/patches/@preconstruct+cli+2.0.5.patch deleted file mode 100644 index e587eac59e..0000000000 --- a/patches/@preconstruct+cli+2.0.5.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js -index 7e40755..fde60cc 100644 ---- a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js -+++ b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js -@@ -1770,6 +1770,8 @@ let getRollupConfig = (pkg, entrypoints, aliases, type, reportTransformedFile) = - external.push(...builtInModules); - } - -+ external.push('next', 'react', '@babel/core', 'prettier') -+ - let input = {}; - entrypoints.forEach(entrypoint => { - input[path__default.relative(pkg.directory, path__default.join(entrypoint.directory, "dist", getNameForDistForEntrypoint(entrypoint)))] = entrypoint.source; diff --git a/patches/@preconstruct+cli+2.0.7.patch b/patches/@preconstruct+cli+2.0.7.patch new file mode 100644 index 0000000000..bd8c1c87bd --- /dev/null +++ b/patches/@preconstruct+cli+2.0.7.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js +index 972d57b..e6bc64b 100644 +--- a/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js ++++ b/node_modules/@preconstruct/cli/cli/dist/cli.cjs.dev.js +@@ -166,7 +166,7 @@ function format(args, messageType, scope) { + info: chalk.cyan("info"), + none: "" + }[messageType]; +- let fullPrefix = "🎁 " + prefix + (scope === undefined ? "" : " " + chalk.cyan(scope)); ++ let fullPrefix = prefix + (scope === undefined ? "" : " " + chalk.cyan(scope)); + return fullPrefix + util.format("", ...args).split("\n").reduce((str, line) => { + const prefixed = `${str}\n${fullPrefix}`; + return line ? `${prefixed} ${line}` : prefixed; +@@ -1917,6 +1917,8 @@ let getRollupConfig = (pkg, entrypoints, aliases, type, reportTransformedFile) = + external.push(...builtInModules); + } + ++ external.push('next', 'react', '@babel/core', 'prettier', '.blitz') ++ + let input = {}; + entrypoints.forEach(entrypoint => { + input[path__default.relative(pkg.directory, path__default.join(entrypoint.directory, "dist", getNameForDistForEntrypoint(entrypoint)))] = entrypoint.source; diff --git a/test/integration/queries/babel.config.js b/test/integration/queries/babel.config.js new file mode 100644 index 0000000000..dfdf62cea1 --- /dev/null +++ b/test/integration/queries/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["blitz/babel"], + plugins: [], +} diff --git a/test/integration/queries/blitz.config.js b/test/integration/queries/blitz.config.js new file mode 100644 index 0000000000..6654107b32 --- /dev/null +++ b/test/integration/queries/blitz.config.js @@ -0,0 +1,11 @@ +module.exports = { + // replace me + async rewrites() { + return [ + { + source: "/blog/post/:pid", + destination: "/blog/:pid", + }, + ] + }, +} diff --git a/test/integration/queries/pages/blog/[post].js b/test/integration/queries/pages/blog/[post].js new file mode 100644 index 0000000000..17d0253d4b --- /dev/null +++ b/test/integration/queries/pages/blog/[post].js @@ -0,0 +1,16 @@ +import {useRouter} from "blitz" +import React from "react" + +const Post = () => { + const router = useRouter() + + return ( + <> +
{router.asPath}
+ + ) +} + +Post.getInitialProps = () => ({hello: "hi"}) + +export default Post diff --git a/test/integration/queries/pages/index.js b/test/integration/queries/pages/index.js new file mode 100644 index 0000000000..602c45b76d --- /dev/null +++ b/test/integration/queries/pages/index.js @@ -0,0 +1,3 @@ +const page = () => "hello from sub id" +page.getInitialProps = () => ({hello: "hi"}) +export default page diff --git a/test/integration/queries/pages/normal.js b/test/integration/queries/pages/normal.js new file mode 100644 index 0000000000..75ad8dfee1 --- /dev/null +++ b/test/integration/queries/pages/normal.js @@ -0,0 +1 @@ +export default () =>

a normal page

diff --git a/test/integration/queries/test/index.test.ts b/test/integration/queries/test/index.test.ts new file mode 100644 index 0000000000..e584a53ab0 --- /dev/null +++ b/test/integration/queries/test/index.test.ts @@ -0,0 +1,81 @@ +import { + blitzBuild, + blitzStart, + File, + findPort, + killApp, + launchApp, + renderViaHTTP, +} from "blitz-test-utils" +import cheerio from "cheerio" +import {join} from "path" + +jest.setTimeout(1000 * 60 * 5) +let app: any +let appPort: number +const appDir = join(__dirname, "..") +const blitzConfig = new File(join(appDir, "blitz.config.js")) + +const runTests = () => { + it("should have gip in __NEXT_DATA__", async () => { + const html = await renderViaHTTP(appPort, "/") + const $ = cheerio.load(html) + expect(JSON.parse($("#__NEXT_DATA__").text()).gip).toBe(true) + }) + + it("should not have gip in __NEXT_DATA__ for non-GIP page", async () => { + const html = await renderViaHTTP(appPort, "/normal") + const $ = cheerio.load(html) + expect("gip" in JSON.parse($("#__NEXT_DATA__").text())).toBe(false) + }) + + it("should have correct router.asPath for direct visit dynamic page", async () => { + const html = await renderViaHTTP(appPort, "/blog/1") + const $ = cheerio.load(html) + expect($("#as-path").text()).toBe("/blog/1") + }) + + it("should have correct router.asPath for direct visit dynamic page rewrite direct", async () => { + const html = await renderViaHTTP(appPort, "/blog/post/1") + const $ = cheerio.load(html) + expect($("#as-path").text()).toBe("/blog/post/1") + }) +} + +describe("getInitialProps", () => { + describe("dev mode", () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) + + describe("serverless mode", () => { + beforeAll(async () => { + blitzConfig.replace("// replace me", `target: 'serverless', `) + await blitzBuild(appDir) + appPort = await findPort() + app = await blitzStart(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + blitzConfig.restore() + }) + + runTests() + }) + + describe("production mode", () => { + beforeAll(async () => { + await blitzBuild(appDir) + appPort = await findPort() + app = await blitzStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) +}) diff --git a/test/jest-environment.js b/test/jest-environment.js new file mode 100644 index 0000000000..1b732367be --- /dev/null +++ b/test/jest-environment.js @@ -0,0 +1,89 @@ +// my-custom-environment +const http = require("http") +const getPort = require("get-port") +const seleniumServer = require("selenium-standalone") +const NodeEnvironment = require("jest-environment-node") + +const {BROWSER_NAME: browserName = "chrome", SKIP_LOCAL_SELENIUM_SERVER} = process.env + +const newTabPg = ` + + + + new tab + + + Click me + + +` + +class CustomEnvironment extends NodeEnvironment { + async setup() { + await super.setup() + // Since ie11 doesn't like dataURIs we have to spin up a + // server to handle the new tab page + this.server = http.createServer((req, res) => { + res.statusCode = 200 + res.end(newTabPg) + }) + const newTabPort = await getPort() + + await new Promise((resolve, reject) => { + this.server.listen(newTabPort, (err) => { + if (err) return reject(err) + resolve() + }) + }) + + let seleniumServerPort + + if (browserName !== "chrome" && SKIP_LOCAL_SELENIUM_SERVER !== "true") { + console.log("Installing selenium server") + await new Promise((resolve, reject) => { + seleniumServer.install((err) => { + if (err) return reject(err) + resolve() + }) + }) + + console.log("Starting selenium server") + await new Promise((resolve, reject) => { + seleniumServer.start((err, child) => { + if (err) return reject(err) + this.seleniumServer = child + resolve() + }) + }) + console.log("Started selenium server") + seleniumServerPort = 4444 + } + + this.global.wd = null + this.global._newTabPort = newTabPort + this.global.browserName = browserName + this.global.seleniumServerPort = seleniumServerPort + this.global.browserStackLocalId = global.browserStackLocalId + } + + async teardown() { + await super.teardown() + + if (this.server) { + this.server.close() + } + if (this.global.wd) { + try { + await this.global.wd.quit() + } catch (err) { + console.log(`Failed to quit webdriver instance`, err) + } + } + // must come after wd.quit() + if (this.seleniumServer) { + this.seleniumServer.kill() + } + } +} + +module.exports = CustomEnvironment diff --git a/test/jest-global-setup.js b/test/jest-global-setup.js new file mode 100644 index 0000000000..108f68e997 --- /dev/null +++ b/test/jest-global-setup.js @@ -0,0 +1,24 @@ +let globalSetup = () => {} + +if (process.env.BROWSERSTACK) { + const {Local} = require("browserstack-local") + const browserStackLocal = new Local() + const localBrowserStackOpts = { + key: process.env.BROWSERSTACK_ACCESS_KEY, + localIdentifier: new Date().getTime(), // Adding a unique local identifier to run parallel tests on BrowserStack + } + global.browserStackLocal = browserStackLocal + global.browserStackLocalId = localBrowserStackOpts.localIdentifier + + globalSetup = () => { + return new Promise((resolve, reject) => { + browserStackLocal.start(localBrowserStackOpts, (err) => { + if (err) return reject(err) + console.log("Started BrowserStackLocal", browserStackLocal.isRunning()) + resolve() + }) + }) + } +} + +module.exports = globalSetup diff --git a/test/jest-global-teardown.js b/test/jest-global-teardown.js new file mode 100644 index 0000000000..7fe5275a5f --- /dev/null +++ b/test/jest-global-teardown.js @@ -0,0 +1,9 @@ +let globalTeardown = () => {} + +if (process.env.BROWSERSTACK) { + globalTeardown = () => global.browserStackLocal.killAllProcesses(() => {}) +} + +module.exports = async () => { + await globalTeardown() +} diff --git a/test/jest-setup-after-env.js b/test/jest-setup-after-env.js new file mode 100644 index 0000000000..9c224f5d9e --- /dev/null +++ b/test/jest-setup-after-env.js @@ -0,0 +1,9 @@ +/* eslint-env jest */ + +process.env.BLITZ_TEST_ENVIRONMENT = true + +if (process.env.JEST_RETRY_TIMES) { + const retries = Number(process.env.JEST_RETRY_TIMES) + console.log(`Configuring jest retries: ${retries}`) + jest.retryTimes(retries) +} diff --git a/test/lib/blitz-test-utils.ts b/test/lib/blitz-test-utils.ts new file mode 100644 index 0000000000..fa7a1dded7 --- /dev/null +++ b/test/lib/blitz-test-utils.ts @@ -0,0 +1,640 @@ +import {ChildProcess} from "child_process" +import spawn from "cross-spawn" +import express from "express" +import {existsSync, readFileSync, unlinkSync, writeFileSync} from "fs" +import {writeFile} from "fs-extra" +import getPort from "get-port" +import http from "http" +// `next` here is the symlink in `test/node_modules/next` which points to the root directory. +// This is done so that requiring from `next` works. +// The reason we don't import the relative path `../../dist/` is that it would lead to inconsistent module singletons +// import server from "next/dist/server/next" +import _pkg from "next/package.json" +import fetch from "node-fetch" +import path from "path" +import qs from "querystring" +import treeKill from "tree-kill" + +// export const nextServer = server +export const pkg = _pkg + +// polyfill Object.fromEntries for the test/integration/relay-analytics tests +// on node 10, this can be removed after we no longer support node 10 +if (!Object.fromEntries) { + Object.fromEntries = require("core-js/features/object/from-entries") +} + +export function initBlitzServerScript( + scriptPath: string, + successRegexp: RegExp, + env: Record, + failRegexp: RegExp, + opts?: { + onStdout?: (stdout: string) => void + onStderr?: (stderr: string) => void + }, +) { + return new Promise((resolve, reject) => { + const instance = spawn("node", ["--no-deprecation", scriptPath], {env}) + + function handleStdout(data: Buffer) { + const message = data.toString() + if (successRegexp.test(message)) { + resolve(instance) + } + process.stdout.write(message) + + if (opts && opts.onStdout) { + opts.onStdout(message.toString()) + } + } + + function handleStderr(data: Buffer) { + const message = data.toString() + if (failRegexp && failRegexp.test(message)) { + instance.kill() + return reject(new Error("received failRegexp")) + } + process.stderr.write(message) + + if (opts && opts.onStderr) { + opts.onStderr(message.toString()) + } + } + + instance.stdout.on("data", handleStdout) + instance.stderr.on("data", handleStderr) + + instance.on("close", () => { + instance.stdout.removeListener("data", handleStdout) + instance.stderr.removeListener("data", handleStderr) + }) + + instance.on("error", (err) => { + reject(err) + }) + }) +} + +export function renderViaAPI(app: any, pathname: string, query: Record) { + const url = `${pathname}${query ? `?${qs.stringify(query)}` : ""}` + return app.renderToHTML({url}, {}, pathname, query) +} + +export async function renderViaHTTP( + appPort: number, + pathname: string, + query?: Record, + opts?: Record, +) { + return fetchViaHTTP(appPort, pathname, query, opts).then((res) => res.text()) +} + +export function fetchViaHTTP( + appPort: number, + pathname: string, + query?: Record, + opts?: Record, +) { + const url = `http://localhost:${appPort}${pathname}${query ? `?${qs.stringify(query)}` : ""}` + return fetch(url, opts) +} + +export function findPort() { + return getPort() +} + +interface RunBlitzCommandOptions { + cwd?: string + env?: Record + spawnOptions?: any + instance?: any + stderr?: boolean + stdout?: boolean + ignoreFail?: boolean +} + +export function runBlitzCommand(argv: any[], options: RunBlitzCommandOptions = {}) { + const blitzDir = path.dirname(require.resolve("blitz/package")) + const blitzBin = path.join(blitzDir, "bin/blitz") + const cwd = options.cwd || blitzDir + // Let Next.js decide the environment + const env = { + ...process.env, + ...options.env, + NODE_ENV: "", + __NEXT_TEST_MODE: "true", + } + + return new Promise((resolve, reject) => { + console.log(`Running command "blitz ${argv.join(" ")}"`) + const instance = spawn("node", ["--no-deprecation", blitzBin, ...argv], { + ...options.spawnOptions, + cwd, + env, + stdio: ["ignore", "pipe", "pipe"], + }) + + if (typeof options.instance === "function") { + options.instance(instance) + } + + let stderrOutput = "" + if (options.stderr) { + instance.stderr.on("data", function (chunk) { + stderrOutput += chunk + }) + } + + let stdoutOutput = "" + if (options.stdout) { + instance.stdout.on("data", function (chunk) { + stdoutOutput += chunk + }) + } + + instance.on("close", (code, signal) => { + if (!options.stderr && !options.stdout && !options.ignoreFail && code !== 0) { + console.log(stderrOutput) + return reject(new Error(`command failed with code ${code}`)) + } + + resolve({ + code, + signal, + stdout: stdoutOutput, + stderr: stderrOutput, + }) + }) + + instance.on("error", (err: any) => { + console.log(stderrOutput) + err.stdout = stdoutOutput + err.stderr = stderrOutput + reject(err) + }) + }) +} + +interface RunBlitzLaunchOptions { + cwd?: string + env?: Record + onStdout?: (stdout: string) => void + onStderr?: (stderr: string) => void + stderr?: boolean + stdout?: boolean + blitzStart?: boolean +} + +export function runBlitzLaunchCommand( + argv: any[], + stdOut: unknown, + opts: RunBlitzLaunchOptions = {}, +) { + const blitzDir = path.dirname(require.resolve("blitz/package")) + const blitzBin = path.join(blitzDir, "bin/blitz") + const cwd = opts.cwd ?? path.dirname(require.resolve("blitz/package")) + console.log(cwd) + const env = { + ...process.env, + NODE_ENV: undefined, + __NEXT_TEST_MODE: "true", + ...opts.env, + } + + return new Promise((resolve, reject) => { + const instance = spawn( + "node", + ["--no-deprecation", blitzBin, opts.blitzStart ? "start" : "dev", ...argv], + {cwd, env}, + ) + let didResolve = false + + function handleStdout(data: Buffer) { + const message = data.toString() + const bootupMarkers = { + dev: /compiled successfully/i, + start: /started server/i, + } + if (bootupMarkers[opts.blitzStart || stdOut ? "start" : "dev"].test(message)) { + if (!didResolve) { + didResolve = true + resolve(stdOut ? message : instance) + } + } + + if (typeof opts.onStdout === "function") { + opts.onStdout(message) + } + + if (opts.stdout !== false) { + process.stdout.write(message) + } + } + + function handleStderr(data: Buffer) { + const message = data.toString() + if (typeof opts.onStderr === "function") { + opts.onStderr(message) + } + + if (opts.stderr !== false) { + process.stderr.write(message) + } + } + + instance.stdout.on("data", handleStdout) + instance.stderr.on("data", handleStderr) + + instance.on("close", () => { + instance.stdout.removeListener("data", handleStdout) + instance.stderr.removeListener("data", handleStderr) + if (!didResolve) { + didResolve = true + resolve() + } + }) + + instance.on("error", (err) => { + reject(err) + }) + }) +} + +// Launch the app in dev mode. +export function launchApp(dir: string, port: number, opts: RunBlitzLaunchOptions = {}) { + return runBlitzLaunchCommand(["-p", port], undefined, {cwd: dir, ...opts}) +} + +export function blitzBuild(dir: string, args = [], opts: RunBlitzCommandOptions = {}) { + return runBlitzCommand(["build", ...args], {cwd: dir, ...opts}) +} + +export function blitzExport(dir: string, {outdir}, opts: RunBlitzCommandOptions = {}) { + return runBlitzCommand(["export", "--outdir", outdir], {cwd: dir, ...opts}) +} + +export function blitzExportDefault(dir: string, opts: RunBlitzCommandOptions = {}) { + return runBlitzCommand(["export"], {cwd: dir, ...opts}) +} + +export function blitzStart(dir: string, port: number, opts: RunBlitzLaunchOptions = {}) { + return runBlitzLaunchCommand(["-p", port], undefined, { + cwd: dir, + ...opts, + blitzStart: true, + }) +} + +export function buildTS(args = [], cwd: string, env = {}) { + cwd = cwd || path.dirname(require.resolve("@blitzjs/cli/package")) + env = {...process.env, NODE_ENV: undefined, ...env} + + return new Promise((resolve, reject) => { + const instance = spawn( + "node", + ["--no-deprecation", require.resolve("typescript/lib/tsc"), ...args], + {cwd, env}, + ) + let output = "" + + const handleData = (chunk) => { + output += chunk.toString() + } + + instance.stdout.on("data", handleData) + instance.stderr.on("data", handleData) + + instance.on("exit", (code) => { + if (code) { + return reject(new Error("exited with code: " + code + "\n" + output)) + } + resolve() + }) + }) +} + +// Kill a launched app +export async function killApp(instance) { + await new Promise((resolve, reject) => { + treeKill(instance.pid, (err) => { + if (err) { + if ( + process.platform === "win32" && + typeof err.message === "string" && + (err.message.includes(`no running instance of the task`) || + err.message.includes(`not found`)) + ) { + // Windows throws an error if the process is already dead + // + // Command failed: taskkill /pid 6924 /T /F + // ERROR: The process with PID 6924 (child process of PID 6736) could not be terminated. + // Reason: There is no running instance of the task. + return resolve() + } + return reject(err) + } + + resolve() + }) + }) +} + +export async function startApp(app: any) { + await app.prepare() + const handler = app.getRequestHandler() + const server = http.createServer(handler) + ;(server as any).__app = app + + await promiseCall(server, "listen") + return server +} + +export async function stopApp(server: any) { + if (server.__app) { + await server.__app.close() + } + await promiseCall(server, "close") +} + +export function promiseCall(obj: any, method: any, ...args: any[]) { + return new Promise((resolve, reject) => { + const newArgs = [ + ...args, + function (err: any, res: any) { + if (err) return reject(err) + resolve(res) + }, + ] + + obj[method](...newArgs) + }) +} + +export function waitFor(millis: number) { + return new Promise((resolve) => setTimeout(resolve, millis)) +} + +export async function startStaticServer(dir: string) { + const app = express() + const server = http.createServer(app) + app.use(express.static(dir)) + + await promiseCall(server, "listen") + return server +} + +export async function startCleanStaticServer(dir: string) { + const app = express() + const server = http.createServer(app) + app.use(express.static(dir, {extensions: ["html"]})) + + await promiseCall(server, "listen") + return server +} + +// check for content in 1 second intervals timing out after +// 30 seconds +export async function check(contentFn: Function, regex: RegExp, hardError = true) { + let content: any + let lastErr: any + + for (let tries = 0; tries < 30; tries++) { + try { + content = await contentFn() + if (typeof regex === "string") { + if (regex === content) { + return true + } + } else if (regex.test(content)) { + // found the content + return true + } + await waitFor(1000) + } catch (err) { + await waitFor(1000) + lastErr = err + } + } + console.error("TIMED OUT CHECK: ", {regex, content, lastErr}) + + if (hardError) { + throw new Error("TIMED OUT: " + regex + "\n\n" + content) + } + return false +} + +export class File { + path: string + originalContent: any + constructor(path: string) { + this.path = path + this.originalContent = existsSync(this.path) ? readFileSync(this.path, "utf8") : null + } + + write(content: any) { + if (!this.originalContent) { + this.originalContent = content + } + writeFileSync(this.path, content, "utf8") + } + + replace(pattern: any, newValue: any) { + const currentContent = readFileSync(this.path, "utf8") + if (pattern instanceof RegExp) { + if (!pattern.test(currentContent)) { + throw new Error( + `Failed to replace content.\n\nPattern: ${pattern.toString()}\n\nContent: ${currentContent}`, + ) + } + } else if (typeof pattern === "string") { + if (!currentContent.includes(pattern)) { + throw new Error( + `Failed to replace content.\n\nPattern: ${pattern}\n\nContent: ${currentContent}`, + ) + } + } else { + throw new Error(`Unknown replacement attempt type: ${pattern}`) + } + + const newContent = currentContent.replace(pattern, newValue) + this.write(newContent) + } + + delete() { + unlinkSync(this.path) + } + + restore() { + this.write(this.originalContent) + } +} + +export async function evaluate(browser: any, input: any) { + if (typeof input === "function") { + const result = await browser.executeScript(input) + await new Promise((resolve) => setTimeout(resolve, 30)) + return result + } else { + throw new Error(`You must pass a function to be evaluated in the browser.`) + } +} + +export async function retry(fn: Function, duration = 3000, interval = 500, description: string) { + if (duration % interval !== 0) { + throw new Error( + `invalid duration ${duration} and interval ${interval} mix, duration must be evenly divisible by interval`, + ) + } + + for (let i = duration; i >= 0; i -= interval) { + try { + return await fn() + } catch (err) { + if (i === 0) { + console.error(`Failed to retry${description ? ` ${description}` : ""} within ${duration}ms`) + throw err + } + console.warn(`Retrying${description ? ` ${description}` : ""} in ${interval}ms`) + await waitFor(interval) + } + } +} + +export async function hasRedbox(browser: any, expected = true) { + let attempts = 30 + do { + const has = await evaluate(browser, () => { + return Boolean( + [].slice + .call(document.querySelectorAll("nextjs-portal")) + .find((p: any) => + p.shadowRoot.querySelector( + "#nextjs__container_errors_label, #nextjs__container_build_error_label", + ), + ), + ) + }) + if (has) { + return true + } + if (--attempts < 0) { + break + } + + await new Promise((resolve) => setTimeout(resolve, 1000)) + } while (expected) + return false +} + +export async function getRedboxHeader(browser: any) { + return retry( + () => + evaluate(browser, () => { + const portal = [].slice + .call(document.querySelectorAll("nextjs-portal")) + .find((p: any) => p.shadowRoot.querySelector("[data-nextjs-dialog-header")) + const root = portal.shadowRoot + return root + .querySelector("[data-nextjs-dialog-header]") + .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, "Unknown") + }), + 3000, + 500, + "getRedboxHeader", + ) +} + +export async function getRedboxSource(browser: any) { + return retry( + () => + evaluate(browser, () => { + const portal = [].slice + .call(document.querySelectorAll("nextjs-portal")) + .find((p: any) => + p.shadowRoot.querySelector( + "#nextjs__container_errors_label, #nextjs__container_build_error_label", + ), + ) + const root = portal.shadowRoot + return root + .querySelector("[data-nextjs-codeframe], [data-nextjs-terminal]") + .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, "Unknown") + }), + 3000, + 500, + "getRedboxSource", + ) +} + +export function getBrowserBodyText(browser: any) { + return browser.eval('document.getElementsByTagName("body")[0].innerText') +} + +export function normalizeRegEx(src: string) { + return new RegExp(src).source.replace(/\^\//g, "^\\/") +} + +function readJson(path: string) { + return JSON.parse(readFileSync(path) as any) +} + +export function getBuildManifest(dir: string) { + return readJson(path.join(dir, ".next/build-manifest.json")) +} + +export function getPageFileFromBuildManifest(dir: string, page: string) { + const buildManifest = getBuildManifest(dir) + const pageFiles = buildManifest.pages[page] + if (!pageFiles) { + throw new Error(`No files for page ${page}`) + } + + const pageFile = pageFiles.find( + (file: string) => + file.endsWith(".js") && file.includes(`pages${page === "" ? "/index" : page}`), + ) + if (!pageFile) { + throw new Error(`No page file for page ${page}`) + } + + return pageFile +} + +export function readBlitzBuildClientPageFile(appDir: string, page: string) { + const pageFile = getPageFileFromBuildManifest(appDir, page) + return readFileSync(path.join(appDir, ".next", pageFile), "utf8") +} + +export function getPagesManifest(dir: string) { + const serverFile = path.join(dir, ".next/server/pages-manifest.json") + + if (existsSync(serverFile)) { + return readJson(serverFile) + } + return readJson(path.join(dir, ".next/serverless/pages-manifest.json")) +} + +export function updatePagesManifest(dir: string, content: any) { + const serverFile = path.join(dir, ".next/server/pages-manifest.json") + + if (existsSync(serverFile)) { + return writeFile(serverFile, content) + } + return writeFile(path.join(dir, ".next/serverless/pages-manifest.json"), content) +} + +export function getPageFileFromPagesManifest(dir: string, page: string) { + const pagesManifest = getPagesManifest(dir) + const pageFile = pagesManifest[page] + if (!pageFile) { + throw new Error(`No file for page ${page}`) + } + + return pageFile +} + +export function readBlitzBuildServerPageFile(appDir: string, page: string) { + const pageFile = getPageFileFromPagesManifest(appDir, page) + return readFileSync(path.join(appDir, ".next", "server", pageFile), "utf8") +} diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000000..7e5f900099 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + /* "module": "esnext", */ + /* "target": "esnext", */ + "allowJs": true, + "baseUrl": "./lib", + "resolveJsonModule": true, + "noEmit": true + } +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 3742ab3cd5..99b9dd0f1b 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,5 +1,5 @@ { "extends": "./tsconfig.json", - "include": ["types/**/*", "packages/**/*", "recipes/**/*"], + "include": ["types/**/*", "packages/**/*", "recipes/**/*", "test/**/*"], "exclude": [] } diff --git a/yarn.lock b/yarn.lock index 626df25325..906b10af93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3131,10 +3131,10 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== -"@preconstruct/cli@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@preconstruct/cli/-/cli-2.0.5.tgz#bd952ecae0fbe5cba37040d7f05c41cab3ac4250" - integrity sha512-yWB0GwqZi9tTpSmsEkcVYxnieCT89L9938XR7s9ffjKf59GFMwy8O0G8wMA8Rm6jNjf3ob5zuVWvCaoHppUS8Q== +"@preconstruct/cli@2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@preconstruct/cli/-/cli-2.0.7.tgz#368b0313bc3e04da2442e0133d7bdc3a076a3a55" + integrity sha512-xXKbIZa5k39fLs3ufLo2/PgZjQK/ZBzUeK0nFt+t6xE3i++e6y/RN8GNNzGxOgwgM6+m+OL7rB54ruwB/HVWqw== dependencies: "@babel/code-frame" "^7.5.5" "@babel/core" "^7.7.7" @@ -3184,13 +3184,6 @@ pirates "^4.0.1" source-map-support "^0.5.16" -"@preconstruct/next@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@preconstruct/next/-/next-2.0.0.tgz#050042c13dde0c671bee0681acb49c31e80ab415" - integrity sha512-jpNffjgVKSilBCi3tNs2MEqqGdQBOo5n97B9OCfMDqO9SoiH7MyCmQ+tHCYQvY5gmD6Bf3Fas79N7Rzj6vJBsQ== - dependencies: - resolve "^1.17.0" - "@prisma/client@2.19.0": version "2.19.0" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.19.0.tgz#a45f17a59fd109e95b61bf4b56d4a7642169ec0e" @@ -3879,6 +3872,13 @@ dependencies: "@types/node" "*" +"@types/get-port@4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/get-port/-/get-port-4.2.0.tgz#4fc44616c737d37d3ee7926d86fa975d0afba5e4" + integrity sha512-Iv2FAb5RnIk/eFO2CTu8k+0VMmIR15pKbcqRWi+s3ydW+aKXlN2yemP92SrO++ERyJx+p6Ie1ggbLBMbU1SjiQ== + dependencies: + get-port "*" + "@types/glob-stream@*": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc" @@ -10665,7 +10665,7 @@ get-pkg-repo@^1.0.0: parse-github-repo-url "^1.3.0" through2 "^2.0.0" -get-port@5.1.1, get-port@^5.1.1: +get-port@*, get-port@5.1.1, get-port@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==